Combat System Scripting V: our first combat meter

I have suddenly remembered this here series of combat scripting posts that I was doing the other Geological Epoch!

So I will do another one. A pretty simple, but important one; our first Combat Meter.

Now as I didn’t explain in the first post but probably should have, when it comes to people (AVs) taking “damage” in Second Life, there are two distinctly different ways that can work. There is a notion of “health” and “damage” that are built directly into the world (but as far as I can tell very seldom used, because they’re inflexible and kind of silly), and then there’s “health” and “damage” as enforced by scripts in things that you wear.

(In a way this is like the difference between vendors that let you buy using “buy”, and those that let you buy using “pay”, as explained in my extremely thorough post on the subject. “Buy” is built into the world whereas “Pay” enables scripting, just like Linden Damage is build into the world whereas Scripted Damage is scripted. Not that there is anything all that significant about the analogy. :) )

Many, even most, Residents probably don’t even know that every AV is always at some state of Health, and that if that Health goes to zero, we “die”, in the sense that we are suddenly teleported Home, just as though we’d hit control-shift-h or whatever.

The reason that people don’t know this is that the vast majority of SL land has the “damage enabled” flag turned off, so you’re always at 100% health, and the viewer doesn’t normally even bother displaying the health meter in that case.

But if you stop by my Park in the center of Hughes Rise, or various other places, and then look carefully, you’ll be able to find a little heart on the screen, probably with a “100%” next to it. It you fly or teleport a few dozen meters into the sky and let yourself fall, or fly at high speed into a tree, or other amusing things, the number may go down from 100% (and then rather quickly go back up again). If you lose enough health fast enough, you may even hear your AV make a little supposedly gender-appropriate pained-grunt sort of noise.

And if you get the little number next to the heart down to 0%, you will suddenly be teleported home.

Now that sounds like a useful basis for a combat system, but it really isn’t, because it’s relatively easy to script up an object that instantly kills anyone it’s pointed at, and being teleported home isn’t a very flexible way to implement defeat (for instance it’s tough to script a scoreboard of “kills” or anything, just for one example), and healing is always exactly the same over time (no way to make “medikits” or “health potions” or anything), and it doesn’t apply to non-AVs (targets, monsters, robots, etc) and so on and so on.

So basically no one uses the built-in health and damage system for actual combat. What they use instead is Scripted Damage, which works by having the combatants wear some sort of attachment or other, that somehow finds out when they are “damaged” by “weapons”, or “healed” by whatever, and does whatever the system designer thought appropriate when they “die”.

The things that you wear that keep track of your damage generally have some sort of indicator of how healthy you are right now, in Scripted Damage terms, and are therefore generically called Damage Meters.

In today’s posting here, we will make a Damage Meter. Or, actually, we will discover that we’ve Already Got One!

(“Oh, yes, it’s very nice.“)

Already got one with a few modifications, that is; because it turns out that our self-healing target, from the self-healing target post, makes a very nice basis for an AV damage meter.

This is because of this Very Important Fact: when something hits your AV, all of your attachments see the collision event.

(I understand that this may not be true in OpenSim, or in some OpenSims, in which case damage meters there will have to work in some entirely other way of which I amn’t aware; I may explore that at some point too.)

So for instance if you were to take our self-healing target into inventory, and attach it to the center of your HUD say, and stand in front of the auto-popgun, you would see the health value above it go gradually down; and then if you step aside out of the stream of pellets, it will go slowly up again.

The only problem with it is that when it gets down to zero nothing useful happens. The script does an “llDie()” at that point, which you might think would cause the attachment to Cease To Be, or to detach or something, but in fact (another Interesting Fact) llDie() does nothing at all in attachments, so nothing actually happens.

This is easy to change, though. For now let’s just have the meter say something amusing when you “die”, to demonstrate that it is working. We will just change the process_collision routine so that it does something slightly different when health is zero, as in:

process_collision(integer index) {
    if (llVecMag(llDetectedVel(index))>15) {
        health = health - 1;
        show_health(health,MAX_HEALTH);
        if (health<=0) {
            llSay(0,"Arg! "+llKey2Name(llGetOwner())+
                    " has been defeated!");
        }
        llSetTimerEvent(HEAL_SECONDS);
    }
}

(We also took out the “pow” and “clunk”, because we’re confident enough that things are generally working that we don’t need them anymore.)

So modify the target script as above, put it into a prim, wear that prim somewhere appropriate on your HUD, and stand in front of the auto-popgun or have a friend shoot at you, and you will find yourself being “damaged” and eventually being “defeated”.

Combat! Shazam!

Some other stuff one might want to do:

  • Support “medikits” or “health potions” or whatever, that heal you when, say, you walk (run) across them (I have code for that!),
  • Cause you to, say, fall down and stop running around when you “die” (I have code for that too!),
  • Remove your ability to use your own weapons while you are “dead” (I don’t have code for that yet, but I might talk about various approaches).

(The other thing I’m currently playing with, which I haven’t quite scripted up yet, is to have the targets, the “monsters”, be “lootable”, so that once you’ve defeated one you can touch it or something and get I dunno some kind of reward. If anyone knows of a sim or a system of scripts that does this sort of WoW-like lootable-monsters thing, let me know; I’d like to take a look!)

So that’s all for this time. Pew pew! :)

Advertisements

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!

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! :)

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…

Battling Bots

The Sinister Inventor contemplates his latest Evil Creation.

Contemplating improvements

“I believe the lower edge is dragging on the ground; perhaps if I boosted the hoverheight a bit…”

Well, that worked

“Well, that worked.”

Removing the combat HUD leads to instant resurrection.

Dale's Killbots

“Ah, my faithful lethal servants! What shall I call you? Dale’s Killbots, perhaps. ‘Daleks’ for short!

“Nah, that’s a silly name…”

(The combat scripting proceeds apace; the shooting robots there can be damaged and ultimately destroyed using anything that shoots projectiles, pretty much, although new ones get rezzed as long as the system is on. The combat HUD tosses one rudely to the ground after a certain number of hits by any projectile. I also made a full-auto infinite-ammo version of the standard Linden popgun, which is pretty awesome, but probably banned in most RP sims. I am vaguely thinking of doing a series of weblog posts, tutorial-style, on how it all works. Unless I do something else instead…)