This morning I plan to look at the main loop for one of our movers and see what it might be like in SLua. I’ll just follow my nose: I have no idea where this leads. Just a quick look.
The action I’m interested in takes place in the timer
event:
timer() {
if ( VehicleType == FollowerType ) {
list details = llGetObjectDetails( EngineKey, [OBJECT_DESC]);
DistanceAroundTrack = llList2Float (details, 0) - CarOffset;
update_position();
} else { // ENGINE, TRAM, SHUNTER
update_position();
update_speed();
}
moveToTrackPosition();
}
We see some duplication here, just the two calls to update_position
, and, of course there’s a conditional check taking place on every cycle. With SLua, functions are first-class values, and we might do something like this:
local vehicle_timer = nil
function initialize_timer()
if VehicleType == FollowerType then
vehicle_timer = follower_update
else
vehicle_timer = engine_update
end
end
function follower_update()
local details = ll.GetObjectDetails(etc)
etc
update_position()
end
function engine_update()
update_position()
update_speed()
end
function timer()
vehicle_timer()
moveToTrackPosition()
end
We have notably more code with the scheme above, but we have a simpler and faster timer
function. I would agree with the objection that saving a function in a variable is kind of new and initially confusing, and I would offer the kind and thoughtful advice to Get Over It and to Catch Up With The Times!
I also note two conflicting coding standards here. Are we going to name functions in snake_case or camelCase? We need to make up our minds. Using both can only lead to errors and duplication. I believe that Roblox Luau recommends camelCase for variables and functions. Personally, I am currently used to snake_case but mostly I think we just need a standard.
Let’s look at moveToTrackPosition
and its friends:
moveToTrackPosition() {
// Check other llSetObjectDesc calls??
if (RootPos == ZERO_VECTOR) return;
if ( VehicleType == EngineType ) {
llSetObjectDesc((string)DistanceAroundTrack);
}
integer trackIndex = (integer)DistanceAroundTrack;
checkTriggers(trackIndex);
move_and_bank(trackIndex);
}
move_and_bank(integer trackIndex) {
// should check here that RootPos isn't zero
float fraction = DistanceAroundTrack - trackIndex;
vector prevNode = getTrackPosition(trackIndex-1);
vector currNode = getTrackPosition(trackIndex);
vector nextNode = getTrackPosition(trackIndex + 1);
vector movePos = currNode + fraction*(nextNode-currNode) + <0,0,HeightAboveRails>;
rotation trackRot = getTrackRotation(prevNode, currNode, nextNode, fraction);
rotation bankRot = calculateBankRotation(DistanceAroundTrack);
rotation moveRot = bankRot*trackRot;
llSetLinkPrimitiveParamsFast(link(), [PRIM_POSITION, movePos, PRIM_ROTATION, moveRot]);
}
vector getTrackPosition(integer nodeIndex) {
vector pos = (vector)llLinksetDataRead("datakey"+(string)within(nodeIndex,ActualTrackLength));;
return pos*Scale*RootRot + RootPos;
}
Not much in moveToTrackPosition
itself, except for the mixing of code formats (again!).
There is a global variable DistanceAroundTrack
, which, I happen to know, was updated in update_position
. It is referenced as a global both in moveToTrackPosition
and getTrackPosition
. I also happen to know that check_triggers
thinks it wants the integer part of DistanceAroundTrack
.
I would argue that there should be far fewer functions looking at that global. Perhaps something like this:
function timer()
vehicle_timer()
moveToTrackPosition(DistanceAroundTrack)
end
function move_to_track_position(distance)
if RootPos == ZERO_VECTOR then return end
if VehicleType == EngineType then
ll.SetObjectDesc(tostring(distance))
end
check_triggers(distance)
move_and_bank(distance)
end
function move_and_bank(distance)
local track_index = distance // 1
local fraction = distance - track_index
local prev_node, curr_node, next_node = get_track_positions(track_index)
local move_pos = curr_node + fraction(next_node-curr_node) + HeightOffset
local track_rot = get_track_rotation(prev_node, curr_node, next_node)
local bank_rot = calculate_bank_rotation(distance)
rotation move_rot = bank_rot * track_rot
ll.SetLinkPrimitiveParamsFast(link(), etc)
end
function get_track_positions(node)
return
get_track_position(node-1),
get_track_position(node),
get_track_position(node+1)
end
function get_track_position(node)
local adjusted = within(node, ActualTrackLength)
local vec_string = ll.LinkSetDataRead('datakey'..tostring(adjusted))
return -- whatever code converts the string to a vector
return
end
move_and_bank
appears to me to need some improvement. Certainly the three lines that compute move_rot
could be extracted. I might explore extracting the three nodes prev, curr, next
and then calling a function that uses them to compute position and rotation. The code there isn’t bad, and, within reason, we don’t want to call too many functions when we’re running on the timer.
Ah. No one uses track_index other than to get the nodes. We do need fraction
however.
If I had a few decent tests for this code, I’d experiment with some refactoring. Again, it’s not bad … but my intuition says it could be better.
For example, I wonder whether a little “method object” for computing the position and rotation from three nodes might be a useful item. I’d want to have the code running under tests to figure that out.
I think that the next time I do this, I’ll actually rig up some testing. That will serve two purposes: it’ll surely help us learn how to test this code, and it will probably allow us to refactor it fearlessly to get it as tight and nice as we can.
An open question—just because I haven’t done it at all—is how we’ll convert our LSD notation “<123.45, 234.56, 345.67>” into an SLua vector. Casting won’t do it. Possibly there will be facilities in SLua vector, possibly we’ll have to code something. Either way not a big issue, just an issue.
Standard naming would help. There are names in camelCase, CapitalCamelCase, snake_case, LOUD_SNAKE_CASE, and probably more schemes. I presently favor snake_case, and as I mentioned, I think there are Luau recommendations in Roblox. I don’t know what scheme, if any, SL scripts will generally recommend. Maybe I’ll file a “canny” about it.
At this level of the code, I’m not seeing anything very thrilling in the conversion to SLua. I would like to think about some little objects that might be helpful, just because I’m an object-oriented kind of person, but in SLua, and our code here, there are reasons to imagine that we’d often do better with vanilla functions.
I think we could create code that is marginally better, and possibly marginally faster than LSL at this level. I’m thinking ten percent kind of improvement, not fifty. Other areas may provide more opportunity. Menus, for example, look to be easier to build with SLua facilities. And we might find other interesting code not so close to the timer
event.
We’ll see. We’re just exploring the world, reporting on what we encounter.
Safe paths!
In order to decide what to do, I’ll continue to try things, learning good ways—and not so good ways—to do things.