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!

Advertisements

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…)

The secret is out! “Placemaker: Plazas” released to worldwide acclaim!

Well, I’m sure there will be worldwide acclaim soon, anyway. Probably!

Placemaker: Plazas / publicity shot

There is the publicity shot for Placemaker: Plazas (Basic Edition), just released onto the SL Marketplace.

For a mere 400 Linden-dollars, you can own a device capable of producing literally a whole lot of different and sometimes even interesting (and in any case full-perm) plaza-like structures!

I have no doubt if that price-point makes any sense :) but I thought I’d give it a try.

So buy it if it sounds interesting! Let me know if it works for you, if the instructions were confusing, or anything else. If I continue to find the general idea interesting, maybe someday there will be a “Placemaker: Houses”. That was actually my original thought, but that turned out to be a little grandiose to code from scratch, so I started smaller…

SL Merchants: stop with the cruft!

… and in this case I don’t mean the mediocre-quality 2007 freebies on sale for 99L.

The cruft I’m referring to here is (are) the unnecessary scripts that many of your vendor devices are delivering to customers’ inventories, with names like “Floating Text” or “rotation script” or “vendor script – delete me”.

(Or most adorably of all, “New Script”.)

Vendors all too often deliver this stuff along with the stuff that the customer actually wanted (and sometimes annoying non-script cruft like pose stands, multiple landmarks, and so on), and they (you) ought to stop. Not only because it’s annoying to customers, but because it makes you look sort of clueless and unprofessional, and because annoyed customers are that much less likely to be repeat customers.

Why does this cruft come up in the first place? A little background: there are two different basic ways a vendor can work:

  • Vendors that you buy things from with the “Buy” option on the menu, where you get a little popup that says “Buy a copy of Whatever and its contents [list of contents] from Whoever Resident for 235L?” or “Buy contents [list of contents] from…”; we’ll call these “Buy vendors”, and
  • Vendors that you buy things from with the “Pay” option on the menu, and you get a little popup asking how much you want to pay, usually with just a single button with the price on it, but no list of exactly what you’re getting; we’ll call these “Pay vendors”.

A vendor can be set up so you don’t need to select either Buy or Pay, but you just need to touch it, and it acts as though you’d selected Buy (or Pay). You can still tell a Buy vendor from a Pay vendor, though, by the kind of popup you get after you touch it: either the list of contents that you are buying, or just an amount to pay.

(Not being much of a Viewer 2 or Viewer 3 user, I haven’t verified the exact contents of these popups on those viewers, but I think they are similar; feel free to leave comments if things are significantly different there.)

But anyway!

The interesting difference between the vendors for our purposes is this:

  • a Buy vendor always delivers everything in the prim to the buyer, but it doesn’t need to contain any scripts at all, whereas
  • a Pay vendor does have to contain scripts, but it has total control over what it delivers to the customer (so in particular it doesn’t have to deliver any unwanted scripts to anyone).

So the two primary sources of cruft are:

  • Buy vendors that contain scripts that they don’t need mixed in with the goods, and
  • Pay vendors whose scripts are poorly written, and deliver stuff (often including themselves!) that shouldn’t be delivered.

So how do we fix these things?

Remove the scripts! (Buy vendors)

So Buy vendors don’t need to contain any scripts at all in order to sell things (they just need to have the appropriate “For Sale” boxes filled in in the Edit window). Then why do we so often end up with useless scripts in our inventories when buying from Buy vendors? Because sellers put scripts into the Buy vendors in order to get various effects (floating text, spinning, etc), and then they leave the scripts there, even though they don’t actually need to.

If a box that is a Buy vendor has a Floating Text script in it whose only function is to make the box say “Awesome Blue Hat 240L” in floating text above it, the merchant should remove that script; the floating text will stay (it’s like, after you paint a wall, you don’t have to keep the paintbrush around for the wall to stay painted).

If a box has a rotation script in it whose only function is to make the box rotate around in an eye-catching fashion, the merchant should remove that script; the rotation will continue. (This is true for the common ways that people make sale boxes rotate; there are some kinds of rotation that will actually stop if you remove the script, but your sale box is unlikely to be using any of those.)

If a box has that ubiquitous “anim SMOOTH” script in it that makes the textures on the box rotate around, the merchant should remove that script; the cheesy attractive texture animation will continue.

If a box has a “New Script” in it whose only function is to make the box shoot pretty colored sparks out the top, the merchant should remove that script; the particle display will continue. (Again this is true for the common kinds of particle-displays. More elaborate kinds may stop if the script is removed, but in that case you should consider switching to a Pay vendor instead, or moving the particle script to a different prim, or otherwise avoiding delivering it to your customers.)

The result of removing these scripts is that when your customer buys one of your products from your Buy vendor, they get only the product, and none of those baffling and annoying extra scripts. They get a more satisfying shopping experience, and you get a higher customer retention rate. Goodness all around!

Don’t deliver the scripts! (Pay vendors)

Okay, so for Buy vendors the important thing is to just take out all of those visual effects scripts that don’t need to be in there anymore. What about Pay vendors?

In a Pay vendor, there’s always at least one script involved, with the job of noticing when someone has paid money to the vendor, and figuring out what to do about that. (For Buy vendors, SL itself takes care of seeing the buy action and delivering the goods.)

The script in the Pay vendor can do basically anything it likes; it can deliver copies of the items that are in the prim’s contents, it can deliver just the single boxed item that was selected at the time, it can contact some central server that will handle the actual delivery to the buyer. (That last kind is what you are using when you use a Pay vendor to buy something, and the actual item is sent to you by “Fred’s Central Vendor Server” or whatever.)

So if the script in a Pay vendor can control exactly what is delivered, why do these vendors also end up delivering unwanted cruft? Because so many Pay vendor scripts are incredibly simple and basically just deliver everything found in a particular prim. So this can include both all of the kinds of unnecessary scripts that Buy vendors tend to deliver (Floating Text and family), and also the vendor script itself!

This is easy to fix. Typically your vendor shouldn’t be delivering any scripts at all, so you or your friend who scripts can just look in the vendor script, find the place where it’s making a list of prim contents to deliver, and be sure not to add any scripts to that list.

So for instance if the script has a place that says:

    integer n = llGetInventoryNumber(INVENTORY_ALL);
    for(i=0; i<n; i++) {
        items += [ llGetInventoryName(INVENTORY_ALL, i) ];
    }

that is building up the list of things to deliver, you can change it to:

    string item_name;
    integer n = llGetInventoryNumber(INVENTORY_ALL);
    for(i=0; i<n; i++) {
        item_name = llGetInventoryName(INVENTORY_ALL, i);
        if (llGetInventoryType(item_name)!=INVENTORY_SCRIPT)
            items += [ item_name ];
    }

which leaves any scripts (including itself!) out of that list.

(If you do need to deliver scripts, you can use llGetInventoryName() to find out the name of the vendor script, and at least not deliver that. More sophisticated vendors have a configuration notecard or something like that, so you can explicitly tell them what to deliver; those don’t generally have the problem of delivering extra script cruft in the first place.)

Waring: llAllowInventoryDrop (PSA for all)

As an Extra Bonus for those who have read (or even just skimmed) this far, here’s a Public Service Announcement and a warning.

There are a bunch of legacy “copied from person to person for years” scripts out there that contain the line:

    llAllowInventoryDrop(TRUE);

I’ve seen this line in at least some Floating Text scripts; it may well occur in some other kinds of scripts as well.

While I’m sure that whoever put it into the original script had some good reason to do it given what that particular script was intended for, it’s definitely not something that you want in a random vendor.

If you have a script in a vendor that contains this llAllowInventoryDrop TRUE line, you may be opening your customers (and therefore yourself) up to being pranked and griefed in ways that might range from amusing to very harmful, depending on the prankster and the point of view.

(I’m not going to go into detail here on the exact vulnerability to avoid encouraging pranksters and griefers; a few minutes of web searching will find all the necessary information quickly enough.)

And if you do have one or more of these it’s not enough to remove the script or to remove the offending line from the script. If you find one or more vendors of yours have this line in a script, and you don’t know exactly what it’s doing there, you should change the TRUE to FALSE and re-save the script. So instead of the line above, it should say:

    llAllowInventoryDrop(FALSE);

You should also check the contents of any vendors that you find the dangerous line in, to make sure that they don’t contain anything that you didn’t put there.

And about those posing stands…

Okay, that’s all for the “avoiding script cruft” issue. But while we’re on the general subject, I doubt I’m the only one that’s annoyed when Every Single Item I buy at a store comes in a box that also includes a posing stand with the store’s logo, and two or three landmarks for the store and its various branches (and the owner’s boyfriend’s Rock club, and…). I mean sure, have a little vendor that gives away free logo posing stands to people who want them, and by all means have a landmark-giver in the doorway, but if I buy five items from your store, I don’t want five copies of your posing stand, ‘kay?

So could y’all please leave those out, too? :)