Scripting a Follower – 2

Now that we’ve taught our robot, R Jacob, to face his owner, let’s see about giving him a bit more life.

One fairly easy extension would be to make R Jacob face other people or objects. That’s not very interesting, though, so I plan to let that capability drop out of something that’s more fun.

R Jacob is supposed to be a follower. So far, he just stays in one place and looks for me. Let’s work on the following aspect. We can think about a few ways he might do it.

Perhaps he would fly at a fixed altitude above his target, and fly directly toward them until he was within some fixed range, say 2 meters, then stop. Then he’d just stay there until the target moved, at which point he’d move again. Would he back away if we approached him? Could go either way. But that doesn’t seem very interesting. Here’s my more cunning plan.

R Jacob will stay behind me. I have in mind a sort of cut ring-shaped volume, like in the pic below. R Jacob will stay in that volume, and will occasionally move around randomly within it, so he seems kind of alive. I think I’ll make him choose a location in the volume, turn toward it and move to it, then turn again to look at me. Then, randomly, or if I move, he’ll pick a new location and do it again. This seems like about the right size of new capability to add. We’l see how it goes.

I see the problem as coming in a few parts:

  • Pick a random target location in the cut ring;
  • Face the target (ideally smoothly);
  • Move to the target;
  • Notice the need for a new target;
  • … and so on.

I don’t feel that I need to analyze all the parts of the problem, though you might want to be more careful than I am. I’ve moved a lot of objects in my years in SL and so I’m quite sure I won’t paint myself into a corner. And if I do, that’s all part of the learning anyway, in articles like this one.

Anyway, let’s start with the random location in the cut ring.

The piece of ring I’m starting with goes behind me, and is 45 degrees to my left and right. Seen with me at the center, it goes from an angle of 135 degrees to 225 degrees. Its inner edge is 2 meters back from me, and the outer edge 4 meters back. It’s one meter thick. Of course, I’ll tune all those numbers until I like how it looks but you have to start somewhere.

R Jacob’s final location, of course, depends on where I’m standing, and on what direction I’m facing. I want him to be above me, probably his lowest position about my height and highest a meter above that. And the definition of "behind me" clearly depends on my own facing direction. We’ll factor all those things in, one at a time. Or maybe two at a time if I’m feeling like taking a risk, but usually I do things one at a time, so when something goes wrong, I know what it was.

To pick a random location in the cut ring, we’ll need a random angle between 135 and 225 (or the equivalent in radians, 3*PI/4 and 5*PI/4), a random radius between two and four, and a random height between zero and one. We’ll rotate <0,0,radius> by the angle (around the z axis), rotate again by my personal rotation, add in the height in z, and add in my personal location. That should be about right. This is just planning, of course, and when I script it I’ll get into the details of how to do it step by step and safely. It’s time to get started. Here’s our starting code:

// Follower
// JR 2019-07-21

float Interval = 0.1;
vector Initial = <0.00000, 90.00000, 0.00000>;
rotation InitialRot;

vector getOwnerPos() {
  list ownerDetails = llGetObjectDetails(llGetOwner(), [OBJECT_POS]);
  return llList2Vector(ownerDetails, 0);
}

integer root() {
  if ( llGetNumberOfPrims() == 1 ) return 0;
  return 1;
}

setPosition() {
  vector objectPos = llGetPos();
  vector ownerPos = getOwnerPos();
  vector front = <1.0, 0.0, 0.0>;
  vector toOwner = ownerPos - objectPos;
  vector sameHeight = <toOwner.x, toOwner.y, 0>;
  rotation flat = llRotBetween(front, sameHeight);
  rotation pointAt = llRotBetween(sameHeight, toOwner);
  llSetLinkPrimitiveParamsFast(root(), [PRIM_ROTATION, InitialRot*flat*pointAt]);
}

default {
  state_entry() {
    llSetTimerEvent(Interval);
    InitialRot = llEuler2Rot(Initial*DEG_TO_RAD);
  }

  timer() {
    setPosition();
  }
}

I think I’ll adjust the setPosition function to position R Jacob right above my head. That should be simple, and pretty safe. What do I mean by "safe"? Well, a common mistake that I make when scripting objects to move is to forget to position them relative to the target (in this case just now, me) and so I calculate some small offset from the target, and then poof, off they go to the corner of the region. Let’s avoid that right from the start:

setPosition() {
  vector objectPos = llGetPos();
  vector ownerPos = getOwnerPos();
  vector front = <1.0, 0.0, 0.0>;
  vector toOwner = ownerPos - objectPos;
  vector sameHeight = <toOwner.x, toOwner.y, 0>;
  rotation flat = llRotBetween(front, sameHeight);
  rotation pointAt = llRotBetween(sameHeight, toOwner);
  vector offset = <0,0,2>;
  vector robotPosition = ownerPos + offset;
  llSetLinkPrimitiveParamsFast(root(), [PRIM_POSITION, robotPosition, PRIM_ROTATION, InitialRot*flat*pointAt]);
}

This positions R Jacob right over my head, as planned, but he also can’t decide whether to face directly downward or horizontally. I’m not going to worry about that, because he won’t be positioned over my head in general.

I think I’ll rename offset to height, and then carry on with positioning him a couple of meters behind me:

rotation getOwnerRot() {
  list ownerDetails = llGetObjectDetails(llGetOwner(), [OBJECT_ROT]);
  return llList2Rot(ownerDetails, 0);
}

setPosition() {
  vector objectPos = llGetPos();
  vector ownerPos = getOwnerPos();
  rotation ownerRot = getOwnerRot();
  vector front = <1.0, 0.0, 0.0>;
  vector toOwner = ownerPos - objectPos;
  vector sameHeight = <toOwner.x, toOwner.y, 0>;
  rotation flat = llRotBetween(front, sameHeight);
  rotation pointAt = llRotBetween(sameHeight, toOwner);
  vector height = <0,0,2>;
  vector offset = <-3,0,0>*ownerRot;
  vector robotPosition = ownerPos + offset + height;
  llSetLinkPrimitiveParamsFast(root(), [PRIM_POSITION, robotPosition, PRIM_ROTATION, InitialRot*flat*pointAt]);
}

Now I have the feeling someone is following me:

Anyway, let’s talk a bit about the few changes it took to get R Jacob to hang out behind me.

  vector height = <0,0,2>;
  vector offset = <-3,0,0>*ownerRot;
  vector robotPosition = ownerPos + offset + height;
  llSetLinkPrimitiveParamsFast(root(), [PRIM_POSITION, robotPosition, PRIM_ROTATION, InitialRot*flat*pointAt]);

Beginning at the end, we’re calculating robotPosition and setting his position to that with our llSetLinkPrimitiveParamsFast. Nothing very special there. The position we choose is made up of three parts … ownerPos, my current position, height, two meters up, and offset. The offset variable is set to <-3,0,0>*ownerRot, my rotation. Since avatars, like most vehicles and such, have their X axis forward, <-3,0,0> is three meters behind me when I’m facing due east. Multiply that value by my rotation, and it’s always 3 meters behind me, and sure enough, there’s old R Jacob, three meters behind me and two meters above my center.

Now we just need to randomize all that a bit. But first we need some adjustment. Right now, on every timer tick, R Jacob is recomputing that he needs to go 3 meters back and 2 up. So he seems to stand still, when in fact he’s repositioning himself all the time. If we just randomized the distance on every tick, he’d bounce all around. So what we need to do is randomize only every so often. Let’s randomize every 3 seconds and see what happens.

And after that, I want to talk a bit about the style of this code, and improve it.

But first, a little thinking. What do we want R Jacob to do? Right now, he sticks right behind me and moves instantly when I do. It would look more natural if he would "notice" that he’s out of position and move briskly but not instantly to get back in position. And then from time to time (3 seconds or something) he’d decide on another position and move over there, and so on.

I think I’ll do that in two parts, first with instant motion and then with the gradual. Even so, it’s a bit tricky to think about. Let’s do this. Let’s randomize the <-3,0,0> every 3 seconds, with no other change, and see what happens. To do that, I’ll pull that out into a global and set it in the timer.

vector Offset = <-3,0,0>;

setPosition() {
  vector objectPos = llGetPos();
  vector ownerPos = getOwnerPos();
  rotation ownerRot = getOwnerRot();
  vector front = <1.0, 0.0, 0.0>;
  vector toOwner = ownerPos - objectPos;
  vector sameHeight = <toOwner.x, toOwner.y, 0>;
  rotation flat = llRotBetween(front, sameHeight);
  rotation pointAt = llRotBetween(sameHeight, toOwner);
  vector height = <0,0,2>;
  vector rotatedOffset = Offset*ownerRot;
  vector robotPosition = ownerPos + rotatedOffset + height;
  llSetLinkPrimitiveParamsFast(root(), [PRIM_POSITION, robotPosition, PRIM_ROTATION, InitialRot*flat*pointAt]);
}

Here, I just made the vector global and used it. Note that I renamed the offset inside the setPosition function to be rotatedOffset, a better name anyway. Now to change it every three seconds (30 ticks) in timer:


integer Change = 30;
vector Offset = <-3,0,0>;

randomize() {
  Offset = <-2 - llFrand(2), 0, 0>;
}

default {
  state_entry() {
    llSetTimerEvent(Interval);
    InitialRot = llEuler2Rot(Initial*DEG_TO_RAD);
    Offset = <-3,0,0>;
    Change = 30;
  }

  timer() {
    if ( --Change <= 0 ) {
      randomize();
      Change = 30;
    }
    setPosition();
  }
}

And sure enough, now R Jacob moves forward or backward every three seconds. He’s still right behind me, because we aren’t randomizing his angular position, and he’s always at the same height because we’re not randomizing his height. I’ll randomize his height first, because it’s easier:

integer Change = 30;
vector Offset = <-3,0,0>;
vector Height = <0,0,2>;

randomize() {
  Offset = <-2 - llFrand(2), 0, 0>;
  Height = <0,0,1 + llFrand(2)>;
}

default {
  state_entry() {
    llSetTimerEvent(Interval);
    InitialRot = llEuler2Rot(Initial*DEG_TO_RAD);
    Offset = <-3,0,0>;
    Height = <0,0,2>;
    Change = 30;
  }

  timer() {
    if ( --Change <= 0 ) {
      randomize();
      Change = 30;
    }
    setPosition();
  }
}

And here are a couple of pics of R Jacob in different random positions:

Let’s look at setPosition as it has most of what I’d like to command about in the coding style:

setPosition() {
  vector objectPos = llGetPos();
  vector ownerPos = getOwnerPos();
  rotation ownerRot = getOwnerRot();
  vector front = <1.0, 0.0, 0.0>;
  vector toOwner = ownerPos - objectPos;
  vector sameHeight = <toOwner.x, toOwner.y, 0>;
  rotation flat = llRotBetween(front, sameHeight);
  rotation pointAt = llRotBetween(sameHeight, toOwner);
  vector rotatedOffset = Offset*ownerRot;
  vector robotPosition = ownerPos + rotatedOffset + Height;
  llSetLinkPrimitiveParamsFast(root(), [PRIM_POSITION, robotPosition, PRIM_ROTATION, InitialRot*flat*pointAt]);
}

Right now, I’ve got most everything broken out into tiny separate variables. I’m not necessarily loving the names, but each calculation means something to me and they are all separate. What this does for me, at the stage, is it helps me to keep my thinking clear. Here’s my position, here’s his height, and so on.

Some people think it’s more efficient to wrap up all those calls into big long statements and save the use of those temporary variables. I would usually only do that when I had a measured efficiency problem and was sure it would help. Fact is, with a decent compiler, folding things together doesn’t help much, and it confuses my tiny brain. YMMV, but I like to keep things explicit.

There is one thing here, though, that I definitely do not like, and that’s these lines:

  vector front = <1.0, 0.0, 0.0>;
  vector toOwner = ownerPos - objectPos;
  vector sameHeight = <toOwner.x, toOwner.y, 0>;
  rotation flat = llRotBetween(front, sameHeight);
  rotation pointAt = llRotBetween(sameHeight, toOwner);

These five lines aren’t independent, they have a meaning together, and that is to calculate the variable pointAt, which positions R Jacob to look at me. So I want to break those lines out into a function and then use it. My practice is to extract it, giving things reasonable names, and then maybe rename things better. Here goes:

rotation toPointAt(vector targetPos, vector objectPos) {
  vector front = <1.0, 0.0, 0.0>;
  vector toOwner = targetPos - objectPos;
  vector sameHeight = <toOwner.x, toOwner.y, 0>;
  rotation flat = llRotBetween(front, sameHeight);
  rotation pointAt = llRotBetween(sameHeight, toOwner);
  return flat*pointAt;
}

setPosition() {
  vector objectPos = llGetPos();
  vector ownerPos = getOwnerPos();
  rotation ownerRot = getOwnerRot();
  rotation pointAt = toPointAt(ownerPos, objectPos);
  vector rotatedOffset = Offset*ownerRot;
  vector robotPosition = ownerPos + rotatedOffset + Height;
  llSetLinkPrimitiveParamsFast(root(), [PRIM_POSITION, robotPosition, PRIM_ROTATION, InitialRot*pointAt]);
}

Notice that I changed the PRIM_ROTATION parameter not to refer to flat, folding that into the function. That makes more sense because the function returns the rotation to set to except for the initial rotation of the robot, which, I think, shouldn’t be known to the function. It could be passed in as a third parameter, but we’ll leave that for now.

Shall we randomize a bit more? We’ve done his radius and his height, we just need to randomize the Offset a bit more. Let’s do this:

randomize() {
  Offset = <2 + llFrand(2), 0, 0>;
  float angle = 3*PI/4 + llFrand(PI_BY_TWO);
  rotation rot = llEuler2Rot(<0,0,angle>);
  Offset = Offset*rot;
  Height = <0,0,1 + llFrand(2)>;
}

Sure enough, now he moves from side to side, near and far, and up and down. A couple more pictures:

How did we accomplish this? Two changes. First, I made Offset always a forward facing positive vector, between <2,0,0> and <4,0,0>. Then I computed a random angle between 3*PI/4 and 5*PI/4, put that into a rotation around the z axis, and rotated Offset by that much.

Now R Jacob changes his mind about where to position himself every 3 seconds, and moves himself there. We’ll call that done for today, I think. Here’s the script as it stands. We’ll look at it fresh next time and see if we want to clean anythng up before making him move more smoothly.

Tune in then!

// Follower
// JR 2019-07-21

// JR Make R Jacob move randomly 2019-07-25

float   Interval = 0.1;
integer Change = 30;

// R Jacob robot information

vector   Initial = <0.00000, 90.00000, 0.00000>;
rotation InitialRot;

vector   Offset = <-3,0,0>;
vector   Height = <0,0,2>;

vector getOwnerPos() {
  list ownerDetails = llGetObjectDetails(llGetOwner(), [OBJECT_POS]);
  return llList2Vector(ownerDetails, 0);
}

rotation getOwnerRot() {
  list ownerDetails = llGetObjectDetails(llGetOwner(), [OBJECT_ROT]);
  return llList2Rot(ownerDetails, 0);
}

randomize() {
  Offset = <2 + llFrand(2), 0, 0>;
  float angle = 3*PI/4 + llFrand(PI_BY_TWO);
  rotation rot = llEuler2Rot(<0,0,angle>);
  Offset = Offset*rot;
  Height = <0,0,1 + llFrand(2)>;
}

integer root() {
  if ( llGetNumberOfPrims() == 1 ) return 0;
  return 1;
}

rotation toPointAt(vector targetPos, vector objectPos) {
  vector front = <1.0, 0.0, 0.0>;
  vector toOwner = targetPos - objectPos;
  vector sameHeight = <toOwner.x, toOwner.y, 0>;
  rotation flat = llRotBetween(front, sameHeight);
  rotation pointAt = llRotBetween(sameHeight, toOwner);
  return flat*pointAt;
}

setPosition() {
  vector objectPos = llGetPos();
  vector ownerPos = getOwnerPos();
  rotation ownerRot = getOwnerRot();
  rotation pointAt = toPointAt(ownerPos, objectPos);
  vector rotatedOffset = Offset*ownerRot;
  vector robotPosition = ownerPos + rotatedOffset + Height;
  llSetLinkPrimitiveParamsFast(root(), [PRIM_POSITION, robotPosition, PRIM_ROTATION, InitialRot*pointAt]);
}

default {
  state_entry() {
    llSetTimerEvent(Interval);
    InitialRot = llEuler2Rot(Initial*DEG_TO_RAD);
    Offset = <-3,0,0>;
    Height = <0,0,2>;
    Change = 30;
  }

  timer() {
    if ( --Change <= 0 ) {
      randomize();
      Change = 30;
    }
    setPosition();
  }
}

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

Blog at WordPress.com.

Up ↑

%d bloggers like this: