I’ll begin, as I generally do, with a cube. I’ll color it yellow, with the top (positive Z) blue and the front (positive X) red. SL vehicles generally move in the direction of +X, and generally have their Z axis pointing up, so this is the natural place to start. We’ll be extending our script to deal with other situations, but I always like to start with the simplest possible problem and work up.
Here is my first cut at the script. It’s a little more highly factored than I would usually start with, because I’m going to be writing about the script and I want to break out all the key design aspects for clarity.
// Follower
// JR 2019-07-21
float Interval = 0.1;
setPosition() {
}
default {
state_entry() {
llSetTimerEvent(Interval);
}
timer() {
setPosition();
}
}
My object is going to adjust its position every tenth of a second, so I created a constant called Interval
, and set it to 0.1. My state_entry
just starts the timer. In the timer
event, I’m going to set the object’s position, so I call an empty function setPosition
that will do that work as we grow the script.
Since I want my follower object to look always at its owner, I’ll need to calculate a rotation for the object that makes it do that. And of course part of the input to that will need to be the position of the owner. Let’s begin to sketch that in:
setPosition() {
vector objectPos = llGetPos();
vector ownerPos = getOwnerPos();
}
vector getOwnerPos() {
list ownerDetails = llGetObjectDetails(llGetOwner(), [OBJECT_POS]);
return llList2Vector(ownerDetails, 0);
}
Let me say a few words about these tiny functions. Some LSL scripters will argue that they are inefficient and that you should write everything in line that you possibly can. And it’s true that calling a function is slower than just writing out the statements in line. For purposes of explaining, however, I like to break things out into meaningful chunks. You can take those chunks and thread them back together if you want to.
In my own work, I will sometimes write in line and sometimes break things out in a very fine grain like this. I find that things seem to go better with the fine-grain functions. You might want to try the approach and see how it works for you. Or not: I’m here to show you how I do things and to try to make them clear. You get to script however you like.
Now I have compiled and run the initial script and the new one with those additional lines shown above. It is my practice to compile often and test often. As soon as I’ve made the tiniest possible change to the script that should leave it working, I test. This means that as soon as I make whatever silly mistake makes it not work, I find out about it, and since I’ll only have typed a few lines, the mistake is easy to find. Working this way, I rarely get into long debugging sessions of trying to figure out what is going wrong. Again, you might want to try working in little tiny steps. Or not.
Now let’s work on getting the cube to point at me. Once that works, as I walk around it, it should turn its red face to be toward me. I’m going to do this in two steps, because of an oddity in how the llRotBetween
function works.
Imagine a vector pointing out the front of the cube. That might be <1,0,0>. Given another vector from the cube to me, we could use llRotBetween
to return the rotation needed to point at me. But when we do that, bad things happen. Let’s go ahead and do that so you can see.
setPosition() {
vector objectPos = llGetPos();
vector ownerPos = getOwnerPos();
vector front = <1.0, 0.0, 0.0>;
vector toOwner = ownerPos - objectPos;
rotation pointAt = llRotBetween(front, toOwner);
llSetRot(pointAt);
}
As you can see in the picture, the red face does point at me … but the cube rotates to some weird angle. That’s because llRotBetween
just does the simplest rotation it can find, and ignores any other axes. So what we need to do instead is to first rotate around the Z axis until we’re pointed toward me, and then tilt up or down around the Y axis to point right at me. I have a function that I use to do that but let’s see first if we can derive it.
If the cube and I were at the same altitude then the cube would just rotate around its Z axis. So let’s fudge the points to make that seem to be the case:
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 pointAt = llRotBetween(front, sameHeight);
llSetRot(pointAt);
}
The vector sameHeight
refers to the owner’s X and Y, but uses Z = zero, as does our starting vector <1,0,0>. So the cube stays flat as you can see in the picture below:
However, suppose the cube were raised far above my head. It would still point flat, instead of pointing at me. That would be bad, as you see here:
We fix that by putting in the tilt up and down as a separate step:
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);
llSetRot(flat*pointAt);
}
Notice that I changed the name of pointAt
in the first example to flat
, and used pointAt
as my new rotation, and notice that the two rotations are multiplied together. As you probably know, multiplying rotations just applies them one after the other. Or you might think of it as "adding" them. Anyway, flat*pointAt
means rotate to the flat
rotation then to the pointAt
one, and that’s just what we want.
Now as you see in the pics below, when I walk around, the cube always faces me.
Now it’s time for a little refinement. The llSetRot
function isn’t really a good way to do this: it includes a delay, which means that our cube’s rotation as it follows me is pretty bumpy. We need to use the more complex function llSetLinkPrimitiveParamsFast
, and we’ll do that in two steps. First:
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(0, [PRIM_ROTATION, flat*pointAt]);
}
You can look up the wiki on this function but basically it just says to set the parameters for link zero (the root of my single prim), and the parameter to set is PRIM_ROTATION
and the value is flat*pointAt
, as before. With this change in place, the cube moves much more smoothly. Life is good.
There is an issue, however. In their wisdom, Linden Lab have set the root of a single prim to be zero and the root of a multi-prim object to be one. I do not know why they did that. Anyway, we should deal with that, and my favorite way to do that is with a little function that I call link
. (Arguably root
would be a better name. Today, let’s call it `root’.)
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, flat*pointAt]);
}
Now if you’ve been paying close attention, you’ll have noticed that root
is almost the first thing I’ve done that I don’t really need. And in fact I usually try never to put in even the least bit of generality before I need it. It helps keep my scripts simple. But this one, I usually do put in because usually I’m going to have something more complicated than a cube to work with. And in fact I do. Meet my new robot friend R Jacob:
I’m going to put my script in him and make him do the following. But as soon as I do, something’s going to go wrong. Just watch!
Instead of facing me properly, R Jacob points his feet at me. How rude! I’m glad I didn’t really give him any feet, just stubby little legs. So we need to make him rotate correctly to face me. To do that we first position him at zero rotation, which means I’ll have to stop his script.
In the pics above, first he is lying at zero rotation … and then I stand him up in his proper standing position, facing in the +X direction. Remember we are using +X as the direction we start with, so if we have a complex object, we edit it until it is oriented facing +X. Then we just copy the rotation values from the editor and paste them into the program. Here’s the whole script. We’ll talk about the little changes below:
// 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();
}
}
Up at the top, I’ve added the vector Initial
, where I pasted R Jacob’s standing rotation from the editor, and a rotation variable InitialRot. Notice that in state_entry
, I initialize InitialRot
… and don’t forget the DEG_TO_RAD
to convert the editor’s degrees to radians! Then, finally, in setPosition
, I set R Jacob’s rotation to InitialRot*flat*pointAt
. That means, in essence:
- Make R Jacob point east;
- Make him turn around z toward me;
- Make him tilt up or down toward me.
And sure enough he does just that! Here’s a picture of the original cube, and R Jacob, both properly looking at me from a bit above.
This is a good place to stop. Next time we’ll make them move around.
Leave a Reply