skimi3d Grid Log in Visit the grid
All scripts
Make things move

Move Along Points

Give it a list of places and it travels between them: once, round and round, or there and back. Carries anybody sitting on it.

Goes inThe root prim of the thing that travels
Setup10 to set
Code253 lines
Open toOpen to everyone

What it does

It turns your list of places into a journey the region works out properly, rather than a script nudging a prim along and hoping. Speed is set in metres a second and the timings follow from the distances, so changing one point does not force you to re-time the rest. It can pause at every stop, and it can turn to point the way it is going, which is the difference between a boat and a boat sailing sideways. Places are read as steps away from wherever you rez it, so the same build works anywhere you drop it.

What you can build with it

Ferries, lifts, patrolling airships, a cable car, a cloud crossing a valley, a fairground ride, a security drone doing rounds, a door too big to hinge. Anything that has to really move, as opposed to merely look like it: unlike Spin It, this one moves the object for real, so people can stand on it and it takes them with it.

How to use it

1 Build the thing that travels and link it, then put this script in the root prim.
2 Make sure Physical is switched OFF in the build tool. Keyframed motion refuses to work on physical objects, silently.
3 Write your places into POINTS. They are steps away from where the object stands, in metres, with X east, Y north and Z up.
4 Set SPEED in metres a second, and MODE to 0 for once, 1 for round and round, 2 for there and back.
5 Rez it where the journey should start. It sets off by itself; touch it to stop and start.

Settings you can change

10

Edit these at the top of the script. Everything works on the defaults · change them only when you need to.

POINTS The places it visits, in order, as steps away from where it stands. X is east, Y is north, Z is up. Reading them as steps rather than coordinates is what lets the same build work anywhere you drop it. a ten by eight rectangle on the flat
ABSOLUTE Read POINTS as real region coordinates instead. Handy when you have copied positions out of the build tool, but the object then only works in that one spot. FALSE
SPEED How fast it travels, in metres a second. Walking pace is about 1.5, a running avatar about 5, a slow boat about 3. 1.5
MODE 0 goes once and stops, 1 goes round and round, 2 goes there and back. A loop gets a closing leg home added for it, so it cannot drift. 1
WAIT_AT_EACH Seconds to wait at every place before moving on. 0.0 means it never stops. This is what turns a moving platform into a lift with floors. 0.0
TURN_TO_FACE Turn so it points the way it is going. Leave it off for a lift or a cloud, turn it on for a boat, a cart or a bird. FALSE
NOSE Which way the front of your build points, in its own body. If your boat sails sideways, this is the line to change. <1.0, 0.0, 0.0>
START_MOVING Whether it sets off by itself the moment it is rezzed. TRUE
TOUCH_TO_TOGGLE Touch to stop it, touch again to send it on. TRUE
WHO_MAY_TOUCH 0 for everyone, 1 for the owner alone, 2 for anyone wearing the same group tag. 0
The code LSL · 253 lines · updated 8 Sep 2026 Download .lsl
// =====================================================================
//  Move Along Points
//  skimi3d script library
//  https://grid.skimi3d.com/scripts/move-along-points
//
//  Give it a list of places and it travels between them: once, round
//  and round, or there and back. Smoothly, at a speed you set in metres
//  a second, and carrying anybody sitting on it.
//
//  This is the one to reach for when Spin It is not enough, because
//  this really moves the object. A ferry, a lift, a patrolling airship,
//  a cloud crossing a valley, a door too big to hinge.
//
//  Plain LSL. It uses keyframed motion, which the region works out
//  properly rather than the viewers guessing at it.
// =====================================================================


// ---------------------------------------------------------------------
//  Settings
// ---------------------------------------------------------------------

// The places it visits, in order. By default these are read as steps
// away from wherever you rez the object, along the region's own axes:
// X is east, Y is north, Z is up. That way the same build works
// anywhere you drop it.
//
// The example below walks a ten by eight rectangle on the flat.
list POINTS = [
    < 0.0,  0.0, 0.0>,
    < 0.0, 10.0, 0.0>,
    < 8.0, 10.0, 0.0>,
    < 8.0,  0.0, 0.0>
];

// Read POINTS as real region coordinates instead of as steps from where
// it stands. Useful when you have copied positions out of the build
// tool, but the object then only works in that one spot.
integer ABSOLUTE = FALSE;

// How fast it travels, in metres a second. Walking pace is about 1.5,
// a running avatar about 5, a slow boat about 3.
float SPEED = 1.5;

// 0 = go once and stop
// 1 = round and round for ever
// 2 = there and back, for ever
integer MODE = 1;

// Seconds to wait at every place before moving on. 0.0 means it never
// stops. Anything under a tenth of a second is treated as no wait.
float WAIT_AT_EACH = 0.0;

// Turn so that it points the way it is going. Leave this off for a
// lift or a cloud; turn it on for a boat, a cart or a bird.
integer TURN_TO_FACE = FALSE;

// Which way the front of your build points, in its own body. The
// default is its X. If your boat sails sideways, this is the line.
vector NOSE = <1.0, 0.0, 0.0>;

// Should it set off as soon as it is rezzed?
integer START_MOVING = TRUE;

// Touch to stop it, touch again to send it on.
integer TOUCH_TO_TOGGLE = TRUE;

// Who may touch it. 0 = everyone, 1 = only the owner, 2 = only people
// wearing the same group tag.
integer WHO_MAY_TOUCH = 0;


// ---------------------------------------------------------------------
//  Everything below here is the machinery
// ---------------------------------------------------------------------

// Nothing shorter than this counts as a journey. Two points a
// millimetre apart would otherwise ask for a frame lasting no time at
// all, and the region has to divide by that.
float MIN_LEG = 0.01;

// The shortest a frame may last. Keyframed motion measures in whole
// milliseconds and a frame of nothing confuses it.
float MIN_TIME = 0.1;

integer gGoing = FALSE;


// Build the whole journey as a list of steps, because that is what
// keyframed motion wants: not "go to here" but "go this far from where
// you are".
list plan()
{
    vector   here = llGetPos();
    rotation face = llGetRot();

    // The settings first become real places, once, so that the rest of
    // this only ever subtracts one place from another.
    list places = [];
    integer n = llGetListLength(POINTS);
    integer i;

    for (i = 0; i < n; ++i)
    {
        vector p = llList2Vector(POINTS, i);
        if (!ABSOLUTE) p = here + p;
        places += [p];
    }

    // ROUND AND ROUND HAS TO COME HOME, AND THIS IS WHY.
    //
    // A loop does not replay the same places. It replays the same
    // STEPS, starting from wherever the last lap happened to end. If
    // the steps do not add up to nothing, the object is somewhere new
    // at the end of every lap, and it walks off across the region a
    // little further each time. It is a slow, baffling drift, and by
    // the time anybody notices, the thing is three regions away.
    //
    // So a loop gets a closing step home, added here rather than left
    // for you to remember.
    if (MODE == 1) places += [here];

    list frames = [];
    vector from = here;
    integer count = llGetListLength(places);

    for (i = 0; i < count; ++i)
    {
        vector to   = llList2Vector(places, i);
        vector step = to - from;
        float  len  = llVecMag(step);

        if (len >= MIN_LEG)
        {
            float t = len / SPEED;
            if (t < MIN_TIME) t = MIN_TIME;

            if (TURN_TO_FACE)
            {
                // Point the nose along this leg. The turn is spread
                // over the leg, so it swings round as it travels
                // rather than pivoting at the corner.
                rotation want = llRotBetween(llVecNorm(NOSE), llVecNorm(step));

                // Keyframes carry the turn from the last one, so what
                // goes in the list is the difference, not the heading.
                frames += [step, llRotInverse(face) * want, t];
                face = want;
            }
            else
            {
                frames += [step, ZERO_ROTATION, t];
            }

            from = to;
        }

        if (WAIT_AT_EACH >= MIN_TIME)
        {
            // A wait is a step of no distance that still takes time.
            frames += [ZERO_VECTOR, ZERO_ROTATION, WAIT_AT_EACH];
        }
    }

    return frames;
}


go(integer on)
{
    if (!on)
    {
        llSetKeyframedMotion([], [KFM_COMMAND, KFM_CMD_STOP]);
        gGoing = FALSE;
        return;
    }

    // Two things make keyframed motion do nothing at all, without a
    // word of complaint. Both are worth saying out loud, because
    // silence here costs an hour.
    if (llGetStatus(STATUS_PHYSICS))
    {
        llOwnerSay("I cannot move: this object is physical, and keyframed motion only works on things that are not. Turn Physical off in the build tool.");
        return;
    }
    if (llGetAttached())
    {
        llOwnerSay("I cannot move: keyframed motion does nothing while I am worn.");
        return;
    }

    list frames = plan();
    if (llGetListLength(frames) == 0)
    {
        llOwnerSay("I have nowhere to go: every place in POINTS is where I already am.");
        return;
    }

    integer mode = KFM_FORWARD;
    if (MODE == 1) mode = KFM_LOOP;
    if (MODE == 2) mode = KFM_PING_PONG;

    llSetKeyframedMotion(frames, [KFM_MODE, mode]);
    gGoing = TRUE;
}


integer mayTouch(key who)
{
    if (WHO_MAY_TOUCH == 1) return who == llGetOwner();
    if (WHO_MAY_TOUCH == 2) return llSameGroup(who);
    return TRUE;
}


default
{
    state_entry()
    {
        // The journey is worked out from where the object is standing
        // now, so it has to be planned after every move, not once and
        // for ever.
        go(START_MOVING);
    }

    touch_start(integer total)
    {
        if (!TOUCH_TO_TOGGLE) return;

        key who = llDetectedKey(0);
        if (!mayTouch(who))
        {
            llRegionSayTo(who, 0, "This one is not yours to stop.");
            return;
        }

        go(!gGoing);
    }

    on_rez(integer start)
    {
        // A rezzed copy stands somewhere new, so the plan it was
        // carrying points at the old place. Draw it again.
        llResetScript();
    }

    changed(integer what)
    {
        // After a region restart the object is back at its start but
        // the motion is not. Plan again from where it really is.
        if (what & CHANGED_REGION_START) llResetScript();
    }
}

Copy it, make a new script inworld, paste over what is there and save. Nothing here phones home.