JanetRossini.github.io

Lua, LSL, Blender, Python in Second Life


Project maintained by JanetRossini

Prim(e) Mover

Jun 11, 2025 • [advancedluamovers]


It’s time to try a serious Bezier mover in SLua. The result, in my biased view, is quite nice. Lurvely. Added: Caveat.

I begin by defining a figure-eight path of beziers, 2 meters max height, 3 meters width, in each direction. I laid out the points on an index card.

function define_path()
   Path = {}
   local w = 3
   local h = 2
   local p0, p1, p2, p3
   p0 = vector(0,0,0)
   p1 = vector(0, h, 0)
   p2 = vector(w, h, 0)
   p3 = vector(w, 0, 0)
   table.insert(Path, Bezier(p0, p1, p2, p3))
   p0, p3 = p3, p0
   p1 = vector(w, -h, 0)
   p2 = vector(0, -h, 0)
   table.insert(Path, Bezier(p0, p1, p2, p3))
   p0 = vector(0,0,0)
   p1 = vector(0, h, 0)
   p2 = vector(-w, h, 0)
   p3 = vector(-w, 0, 0)
   table.insert(Path, Bezier(p0, p1, p2, p3))
   p0, p3 = p3, p0
   p1 = vector(-w, -h, 0)
   p2 = vector(0, -h, 0)
   table.insert(Path, Bezier(p0, p1, p2, p3))
end

The basic plan is this:

  1. On touch, sense for a prim named Base;
  2. Having found it in sensor event, move safely to it;
  3. Create a coroutine to provide the points from the four beziers;
  4. Start a timer at 0.1.
  5. In timer, resume the coroutine to get one point from the bezier;
  6. If the coroutine is finished, stop the timer;
  7. Otherwise, normal case, move to Base + the point provided.

We’ll look at the coroutine last. Here’s the rest:

function state_entry()
    --Tests:run_tests()
    Home = ll.GetPos()
    define_path()
    print("ok")
end

Nothing much, just a bit of init. Then:

function touch_start(number_of_touches)
    ll.Sensor("Base", "", bit32.bor(ACTIVE, PASSIVE), 10, PI)
end

Look for the Base. When we find it:

function sensor(number_of_prims)
   Base = ll.DetectedPos(0)
   move_safely_to(Base)
   Provider = coroutine.create(provide)
   ll.SetTimerEvent(0.1)
end

Move to the Base, create the coroutine, set the timer. In the timer:

function timer()
   ok, vec = coroutine.resume(Provider)
   if ok ~= true or vec == nil then
      ll.SetTimerEvent(0)
   else
      move_safely_to(Base + vec)
   end
end

I wrote move_safely_to because things can go wrong and I didn’t want the prim moving out of range, since it has my script in it.

local SafeDistance = 10
function move_safely_to(pos)
   local dist = ll.VecDist(pos, Home)
   if dist < SafeDistance then
      local move = {PRIM_POSITION, pos}
      ll.SetLinkPrimitiveParamsFast(0, move)
   else
      print("unsafe move to ", pos)
   end
end

So long as the move is within SafeDistance, we move. Otherwise we do not and we complain.

All that’s left, I guess, is the coroutine that provides points. It looks like this:

function provide()
   for _, bezier in ipairs(Path) do
      for t = 0, 1, Step do
         coroutine.yield(bezier:at(t))
      end
   end
end

Look at that! Loop over however many beziers there are, four in our case. Loop over t by some Step. yield the bezier value at that t.

What makes this so simple and nice is that there are no globals keeping track of which bezier we’re in or what our current t value is. It’s all inside the coroutine.

Nifty!

This is a small example of why I think our SLua scripts will be able to be much simpler and more clear than what we have to do in LSL.

Caveat

This scheme will not serve as-is for practical movers for trains. It might serve for single-bogie vehicles such as short trams. More complex vehicles need a series of points spaced at fixed distances along the curve. At the junctions between curves, they even call for points on different curves.

Will we still want to use the coroutine style? Too soon to tell. But even if not, the timer code should wind up being nearly as simple as we have here. It’s still early days, we are still learning.

Safe paths!


Here’s the whole program:

local Home = nil
local Base = nil
local SafeDistance = 10
local Path = nil
local Provider = nil
local Step = 1/32

function provide()
  -- while true do -- uncomment this and it runs forever
         for _, bezier in ipairs(Path) do
            for t = 0, 1, Step do
               coroutine.yield(bezier:at(t))
            end
         end
   -- end
end

function move_safely_to(pos)
   local dist = ll.VecDist(pos, Home)
   if dist < SafeDistance then
      local move = {PRIM_POSITION, pos}
      ll.SetLinkPrimitiveParamsFast(0, move)
   else
      print("unsafe move to ", pos)
   end
end

function define_path()
   Path = {}
   local w = 3
   local h = 2
   local p0, p1, p2, p3
   p0 = vector(0,0,0)
   p1 = vector(0, h, 0)
   p2 = vector(w, h, 0)
   p3 = vector(w, 0, 0)
   table.insert(Path, Bezier(p0, p1, p2, p3))
   p0, p3 = p3, p0
   p1 = vector(w, -h, 0)
   p2 = vector(0, -h, 0)
   table.insert(Path, Bezier(p0, p1, p2, p3))
   p0 = vector(0,0,0)
   p1 = vector(0, h, 0)
   p2 = vector(-w, h, 0)
   p3 = vector(-w, 0, 0)
   table.insert(Path, Bezier(p0, p1, p2, p3))
   p0, p3 = p3, p0
   p1 = vector(-w, -h, 0)
   p2 = vector(0, -h, 0)
   table.insert(Path, Bezier(p0, p1, p2, p3))
end

function state_entry()
    Home = ll.GetPos()
    define_path()
end

function sensor(number_of_prims)
   Base = ll.DetectedPos(0)
   move_safely_to(Base)
   Provider = coroutine.create(provide)
   ll.SetTimerEvent(0.1)
end

function timer()
   ok, vec = coroutine.resume(Provider)
   if ok ~= true or vec == nil then
      ll.SetTimerEvent(0)
   else
      move_safely_to(Base + vec)
   end
end

function no_sensor()
   print("base not found)")
end

function touch_start(number_of_touches)
    ll.Sensor("Base", "", bit32.bor(ACTIVE, PASSIVE), 10, PI)
end