Combat System Scripting, interlude: OpenSim

So on our last post in this thread someone asked if this stuff would work in OpenSim.

I did a little playing around, and the answer seems to be “it depends”.

Unlike Second Life, which is both (arguably) a bunch of software, and a particular virtual world that uses that software, OpenSim is just a bunch of software (and an Open Source one at that, which means that there are all various forks and versions and levels and patches around out there). So what you get when you log into one virtual world that’s running (some level of) OpenSim may be different from what you get in another.

What I found when I tried out the Scripts So Far in the first OpenSim-based grid that came to hand (running perhaps “OpenSim 0.7.2 ReactionGrid”, although it wasn’t ReactionGrid) was:

There’s one simple thing in the self-healing target script that this OpenSim didn’t like; this line gets a syntax error:

integer health = MAX_HEALTH;

because apparently this OpenSim is even more silly than Second Life is, about global initializers not having any hard stuff like math or variables in them. So I changed that to just:

integer health;

and added a

    health = MAX_HEALTH;

down in the state_entry() handler, and that got rid of the compilation error.

The autopopgun script worked fine except that (as one of the commentors anticipated) I had to create my own bullet for it because there was no popgun in the library to cheat with. :) I won’t post the bullet script and other bullet details here right now ’cause it would be a distraction, but it worked.

Then I pointed the autoshooter at the target, and the target said “Clunk”.

Repeatedly.

As you may recall, this means that it thinks it’s being hit by something that’s not moving fast enough to cause damage.

A little poking around revealed that in this particular OpenSim, llDetectedVel() was always returning 0.000, making it not very useful for our purposes!

As a temporary test, I changed the

    if (llVecMag(llDetectedVel(index))>15) {

in the target script to the obvious

    if (llVecMag(llDetectedVel(index))>=0) {

so that instead of “is the thing that hit us moving at least fifteen meters per second?”, it was instead asking “is the thing that hit us moving at all, or for that matter not moving at all?”. To which the answer must always be Yes! :)

With that change the target worked fine; its health count went down while the shooter was shooting at it, went back up again when I moved the shooter away, and then went down and turned yellow and then red and then the target Ceased to Be when I left the shooter pointed at it long enough.

There may be OpenSim-based grids where llDetectedVel() works (this list of LSL functions that work in OpenSim seems to claim that it is implemented), but it didn’t seem to in mine. But other than that, and the one tiny syntax change mentioned above, everything we’ve done so far seems to work in OpenSim!

In case anyone else was wondering. :)

Combat System Scripting IV: the auto-popgun

Before we continue in our series from targets to personal combat-meters, it will be convenient to have something that shoots.

So far we’ve been using whatever projectile weapons happen to be in inventory to shoot at targets, but as soon as we want something to shoot back, we’re going to need either a patient and/or interested friend (but those aren’t always available at short notice), or something that will shoot on its own.

And to avoid the complexities of bullets and all, we’re going to cheat. :)

Here’s what you do to get our project for today, the auto-popgun:

  1. Search inventory for “popgun”,
  2. Find the last one that shows up; it should be in the library rather than your personal inventory, and be called something like “Popgun (drag onto yourself)”,
  3. Don’t drag it onto yourself, but just onto the ground.
  4. Edit it, rename it to say “autopopgun”, set the rotations to 354, 0, and 0 (so it’s pointing down a little), and raise it up a bit off the ground.
  5. Go into Contents, open the Popgun script in there, and replace all the text with the script below.

It should then sit there shooting a little ball every second or so, and the little balls should vanish on their own, just as if you were shooting it yourself.

You can rez one of the targets we made earlier in its path, or rez a target and then rotate and move the gun so it’s pointing at it, and observe how the target satisfyingly gets damaged and vanishes, just like you’d expect.

Here’s the autopopgun script:

float SPEED         = 18.0;   //  Speed of bullet in meters/sec
integer LIFETIME    = 7;      //  How many seconds will bullets live 
float DELAY         = 1.0;    //  Delay between shots

default
{
    state_entry() {
        llSetTimerEvent(DELAY); 
    }
    
    on_rez(integer param) {
        llSetTimerEvent(DELAY);
    }
    
    timer() {
        string bullet_name = llGetInventoryName(INVENTORY_OBJECT,0);
        vector vel = <0,SPEED,0>*llGetRootRotation();
        vector pos = llGetPos()+vel/SPEED;      
        llRezObject(bullet_name, pos, vel, ZERO_ROTATION, LIFETIME); 
    }
  
}

Pretty simple script, boiled down from the original popgun script by taking out all of the user-interaction stuff. Basically, once a second, in the timer() event, it figures out the name of the first object in inventory, figures out which way it (the gun) is pointing, and rezzes one of those objects moving in that direction at the given speed SPEED.

It passes the LIFETIME number along to the objects that it rezzes, and as it happens the bullets in the popgun read that number and delete themselves after that many seconds. But we don’t have to worry about any of that, because we cheated and didn’t have to do the bullet scripting ourselves! We are so lazy… :)

Next time, or some time after, or something: our first personal combat meter, so we can stand in the way of the popgun and be shot and take damage!

NeoVictoria Photo Contest

So I got a note about this awhile back, but I am lazy and/or busy so now of course it is almost over, but not quite! And anyway this is a steampunk sort of place that I hadn’t run across before, and those are always fun.

Click through the poster above there for details, or go to the page via this link here.

Combat System Scripting III: target, heal theyself

Okay, short one! No picture! :)

Starting with the final script from last time, as usual, the simplest way to allow our targets to gradually heal themselves over time, is to just stick in a little timer that will increase health back up to maximum:

  timer() {
    if (health<MAX_HEALTH) {
      health += 1;
      show_health(health,MAX_HEALTH);
    }
  }

and add something to, say, state_entry(), that starts the timer, so that state_entry() would now look like this:

    state_entry() {
        show_health(health,MAX_HEALTH);
        llSetTimerEvent(HEAL_SECONDS);
    }

with HEAL_SECONDS set to like 2.0 or something at the top.

And if you do that and play with it, you’ll find that it works. You may also notice that once in awhile when you shoot the target, it will instantly heal again, which is annoying.

To fix that, we can make sure that the timer is running only when there is actually damage to heal, by moving the “start the timer” line to the place where the target notices damage, and adding a “stop the timer” line to the timer itself, whenever it’s at full health.

And so tonight’s final result looks like this:

integer MAX_HEALTH = 20;
integer health = MAX_HEALTH;
float HEAL_SECONDS = 2.0;

vector color_for(integer h,integer max) {
    float ratio = (float)h/(float)max;
    if (ratio>0.666) return <0,1,0>;   // Green
    if (ratio>0.333) return <1,1,0>;   // Yellow
    return <1,0,0>;  // Red
}

show_health(integer h,integer max) {
    llSetText((string)h+"/"+(string)max,color_for(h,max),1);
}

process_collision(integer index) {
    if (llVecMag(llDetectedVel(index))>15) {
        health = health - 1;
        show_health(health,MAX_HEALTH);
        if (health<=0) {
            llSay(0,"boom");
            llDie();
        } else {
            llSay(0,"pow");
        }
        llSetTimerEvent(HEAL_SECONDS);
    } else {
        llSay(0,"clunk");
    }
}
default {
    state_entry() {
        show_health(health,MAX_HEALTH);
    }
    collision_start(integer n) {
        integer i;
        for (i=0;i<n;i++) process_collision(i);
    }
    timer() {
        if (health<MAX_HEALTH) {
            health += 1;
            show_health(health,MAX_HEALTH);
        } 
        if (health>=MAX_HEALTH) llSetTimerEvent(0.0);
    }
}

We make such progress! :)

Lt. John “Pepper” Pike

We interrupt the combat-scripting exercises :) for this important bulletin. This must-have item is now available in the Marketplace, apparently for the very affordable price of 0 (zero) Lindenbucks:

(If anyone doesn’t know the story of “Pepper” Pike and his elevation to memehood, see for instance this HuffPo piece.)

I saw this by the side of the road and knew I had to have one. :) Not being swift enough to look in that there new-fangled Marketplace for it, I left an IM for the creator, who very kindly not only gave me a copy, but pointed me to the Marketplace link.

So spread the word! Defend SL against dissent and nonviolent protest! Or even just annoying weeds!

Combat System Scripting II: slightly buffer target

Ah, now I remember! Before going on to other things besides the basic target, I was first going to make the basic target a little less basic.

In this posting, we’ll make the targets a little more robust, so it takes more than one collision to make them Cease To Be (or, in more violent language, more than one shot to kill them).

We’ll also, by the end, stick some nice color-changing floaty text over them, so you can tell how many collisions away from Ceasing to Be they are. (I’m sort of swerving between showing how a combat system might work, and giving little lectures on programming style, here, but it’s my weblog and I can do what I want! Feedback welcome if you’d like to see it go faster or slower or whatever, although I don’t promise to necessarily do what any feedback might suggest.)

The nice simple target script from last time was this:

process_collision(integer index) {
    if (llVecMag(llDetectedVel(index))>15) {
        llSay(0,"boom");
        llDie();
    } else {
        llSay(0,"clunk");
    }
}
default {
    collision_start(integer n) {
        integer i;
        for (i=0;i<n;i++) process_collision(i);
    }
}

To make the target a little harder to kill, we’ll add a “health” variable to keep track of just how many more hits it has left, and update process_collision to count that variable down, and do the Cease to Be thing only when it reaches zero. Other hits will just make some I dunno “pow” noise. So:

integer health = 20;

process_collision(integer index) {
    if (llVecMag(llDetectedVel(index))>15) {
        health = health - 1;
        if (health<=0) {
            llSay(0,"boom");
            llDie();
        } else {
            llSay(0,"pow");
        }
    } else {
        llSay(0,"clunk");
    }
}

The rest of the script (the collision_start() handler in the default state there) stays the same.

So play with that a little. It’s very nice, but it’s lacking some indication of how damaged the target is. We’ll add the floaty-text for that. It’ll require a new event handler in the default state to initialize the floaty text in the first place, and then some new code in process_collision to update the text when it’s hit.

That gets us to something like this:

integer health = 20;

process_collision(integer index) {
    if (llVecMag(llDetectedVel(index))>15) {
        health = health - 1;
        llSetText((string)health+"/20",<1,1,1>,1);
        if (health<=0) {
            llSay(0,"boom");
            llDie();
        } else {
            llSay(0,"pow");
        }
    } else {
        llSay(0,"clunk");
    }
}
default {
    state_entry() {
        llSetText("20/20",<1,1,1>,1);
    }
    collision_start(integer n) {
        integer i;
        for (i=0;i<n;i++) process_collision(i);
    }
}

This works nicely, but it is Evil because very similar llSetText() calls appear in two different places, and the “20” appears in four different places, which means that if I decided to change it to only take 10 hits, say, I would be sure to miss at least one of them.

With the maintainability improved, it does exactly the same thing, but looks nicer:

integer MAX_HEALTH = 20;
integer health = MAX_HEALTH;

show_health(integer h,integer max) {
    llSetText((string)h+"/"+(string)max,<1,1,1>,1);
}

process_collision(integer index) {
    if (llVecMag(llDetectedVel(index))>15) {
        health = health - 1;
        show_health(health,MAX_HEALTH);
        if (health<=0) {
            llSay(0,"boom");
            llDie();
        } else {
            llSay(0,"pow");
        }
    } else {
        llSay(0,"clunk");
    }
}
default {
    state_entry() {
        show_health(health,MAX_HEALTH);
    }
    collision_start(integer n) {
        integer i;
        for (i=0;i<n;i++) process_collision(i);
    }
}

Much better.

And for one final touch, we can make the floaty text be green or yellow or red depending on how damaged the thing is, by just changing the <1,1,1> in show_health(), which causes it to always appear white, to a call to yet another function that will calculate the right color for us.

The final form for today, then:

integer MAX_HEALTH = 20;
integer health = MAX_HEALTH;

vector color_for(integer h,integer max) {
    float ratio = (float)h/(float)max;
    if (ratio>0.666) return <0,1,0>;   // Green
    if (ratio>0.333) return <1,1,0>;   // Yellow
    return <1,0,0>;  // Red
}

show_health(integer h,integer max) {
    llSetText((string)h+"/"+(string)max,color_for(h,max),1);
}

process_collision(integer index) {
    if (llVecMag(llDetectedVel(index))>15) {
        health = health - 1;
        show_health(health,MAX_HEALTH);
        if (health<=0) {
            llSay(0,"boom");
            llDie();
        } else {
            llSay(0,"pow");
        }
    } else {
        llSay(0,"clunk");
    }
}
default {
    state_entry() {
        show_health(health,MAX_HEALTH);
    }
    collision_start(integer n) {
        integer i;
        for (i=0;i<n;i++) process_collision(i);
    }
}

Isn’t Nature Wonderful?

Next time, maybe: target, heal thyself!

Combat System Scripting I: intro and our first target

As foreshadowed previously, we will now begin a series of posts about the Combat System Scripting we have been doing, because (a) it will be fun, and (b) it might be interesting.

What I’ve done so far, and hope to get to in this series, includes:

  • Stationary targets to shoot at,
  • Moving targets that shoot back,
  • A combat HUD that keeps track of your “damage” and makes you fall down when you take too much,
  • A “medikit” that makes you be less damaged when you walk through it.

Which is enough to have fun battles with your friends and/or Daleks. :) We’ll talk about just that first one in this episode.

A couple of notes:

  • If you just want to have a working combat/RP system to play with, and don’t want to learn scripting of it from first principles, take a look at Myriad Lite (Preview 4) on the Second Life wiki; lots of neat features (and a certain amount of complexity) there.
  • I’m going to assume here that you know how to rez a cube, and how to get a script into one. Not much more than that, though!
  • Loose design goals of the project will include compatibility with various existing things (e.g. it would be good to be able to shoot at stuff with any normal sort of SL weapon), and a preference for less inter-object communication over more, where that’s possible (to reduce lag and complexity).
  • I’m not going to worry too much about preventing ‘cheating’ of various kinds; this is mostly to play with, not to organize serious combat RP with untrusted others around.

Okay, off we go!

The first thing we’ll make is a simple target. Here’s probably the simplest possible target script:

default {
    collision_start(integer n) {
        llSay(0,"clunk");
    }
}

This says, basically, “whenever something collides with you, say ‘clunk'”. (You can change that line to ‘llOwnerSay(“clunk”);’ if you don’t want anyone else to hear the object talking for whatever reason.) Technically the stuff inside “collision_start” is an event-handler, and in this case the event is something hitting the prim that the script is in.

So make a cube, put that script into it, and walk into the cube. It will say “clunk”. Amazing!

Of course walking into things is sort of boring, so let’s get out a weapon and shoot at the cube bang bang. Any weapon that fires actual projectiles will do; if you don’t have one, or aren’t sure of what you have, you can use the simple popgun that is in the standard SL library (the lower part of your inventory, where many people never go; there’s some pretty good stuff in there).

Find the popgun, wear it, go into Mouselook (via the Mouselook button, or pressing M, or whatever), mouse so the little crosshair is on the box (the popgun has weightless projectiles, so you don’t need to compensate for any drop), and click. A thing will emerge and hit your cube (with some amusing colors and sounds and bubbles), and the cube will say “clunk”; huzzah!

Just saying “clunk” is good to be able to tell that you’ve actually hit it, but if we’re going to do Combat we want the target to explode into a flaming ball of gas. Well, or at least to Cease To Be, since I’m not going to figure out a particle-system for a Flaming Ball of Gas today.

So here’s another script:

default {
    collision_start(integer n) {
        llSay(0,"boom");
        llDie();
    }
}

Replace the original one with this one, make a copy of the prim, and then walk into it or shoot at it, and it will say “boom” (which sounds more final than “clunk”), and Cease To Be (quite thoroughly Cease To Be, not in your trash or Lost and Found or anything, which is why I suggested you make a copy of the prim).

You can use shift-drag (or just rez a bunch of copies) to make a whole row of these, and either run along it or go into mouselook and pop them one at a time, boom boom boom boom.

Not bad for a six-line script!

Now it’d be better if the target didn’t actually Cease To Be if someone just casually bumped into it. There are various ways to try to tell casual bumps from official Weapon Projectiles; we’ll use one of the simplest: speed. If somehthing is moving more than, say, fifteen meters per second (which is about as fast as it’s possible for a lone AV to move in normal circumstances), we’ll assume it’s a Weapon Projectile (or maybe a car; getting hit by a speeding car counts); but if it’s moving slower than that, it isn’t. So:

default {
    collision_start(integer n) {
        if (llVecMag(llDetectedVel(0))>15) {
            llSay(0,"boom");
            llDie();
        } else {
            llSay(0,"clunk");
        }
    }
}

The only new complexity here is the “llVecMag(llDetectedVel(0))” bit, which just means “the scalar magnitude of the velocity vector of the thing that hit us” which is to say, how fast it was going.

The (0) means “the first of the N things that hit us”, where N is almost always 1 (so it looks at the first and only thing that hit it), but in some situations, especially when there are lots of bullets flying about, two or more things can collide with the cube so quickly that they’ll both get reported in the same collision event.

If we were just saying “clunk” that wouldn’t matter too much, but if (say) a slow bump happened at the same time as a fast projectile impact, we wouldn’t want to look at just the slow bump. So we’ll add a loop that looks at each of the things that hit us, in case there’s more than one.

default {
    collision_start(integer n) {
        integer i;
        for (i=0;i<n;i++) {
            if (llVecMag(llDetectedVel(i))>15) {
                llSay(0,"boom");
                llDie();
            } else {
                llSay(0,"clunk");
            }    
        }
    }
}

Relatively straightforward.

And then, and finally for this episode, just because I like to keep blocks of code simple wherever possible, we’ll pull out the bit of code that gets done for each colliding thing in to a subroutine of its own, and call it from the event handler:

process_collision(integer index) {
    if (llVecMag(llDetectedVel(index))>15) {
        llSay(0,"boom");
        llDie();
    } else {
        llSay(0,"clunk");
    }    
}
    
default {
    collision_start(integer n) {
        integer i;
        for (i=0;i<n;i++) process_collision(i);
    }
}

And there you have it! A nice target that can be bumped into with impunity, but that goes away with a boom when shot (or when run into at high speed in a Chevy, I expect, although I haven’t tested that… yet).

Next time, hm, I dunno… maybe the Combat HUD or something…