// ===================================================================== // 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(); } }