Moving Objects BasicsEdit
If you want an object to fall, collide, and bounce, then you can just let the physics system handle it. But if you want an object to move in some other way – say, for instance, a blimp that drifts across the sky without falling – then you need to control that movement procedurally in Lua script. To control movement procedurally, you need to use the movement operators.
Position, Rotation, and Scale
There are three ways to move an object:
- Change position: ie, move it north, south, east, west, up, or down.
- Change scale: make it bigger or smaller.
- Change rotation: ie, turn it on its axes.
Position is stored as three numbers: X,Y,Z. We like to think of the global +X axis as 'East', the global +Y axis as 'North', and the global +Z axis as 'Up.' The units are meters, and objects should be modeled using the appropriate scale.
Each object also has its own "local" axes. If a human character is modeled properly, then the local +X axis is to the character's right, the local +Y axis points in front of the character, and the local +Z axis points up out of the top of the character's head.
You can set the position of a character using many different methods. Probably the simplest is object:setPosition(x,y,z). The X,Y,Z values are interpreted as relative to the global coordinate system: increasing Y moves the character north, increasing X moves the character east, and so forth. However, the setPosition method takes an optional parameter: the name of another object. If this is specified, then the position is relative to the other object. For example, fred:setPosition(0,5,0,alice) positions Fred 5 meters in front of Alice.
Functions that get the position of an object can also be relative: for example, fred:getPosition(alice) returns the position of fred relative to alice. In other words, if Fred is 2 meters in front of Alice and 1 meter to her right, it returns 1,2,0.
Scale is stored as three numbers: SX,SY,SZ. These numbers measure the size of the object relative to the object's initial size. So for example, if you set the scale of an object using obj:setScale(2,2,2), it means you made it twice as big as it was initially created.
When you scale a model, be aware that the scaling is relative to the model itself. To understand what this means, imagine enlarging enlarging a character using obj:setScaleZ(2) - this makes him twice as tall. But what if you lay the character down on his back, with his nose pointed upwards before you scale him? The answer is that even though he is lying down with his nose pointing in the Z-direction, scaling him in the Z-axis makes him taller, it doesn’t make his nose longer. No matter his orientation, scaling in the Z-direction makes him taller. That is what is meant by saying that the scaling is relative to the model itself.
Because scaling is always relative to the model itself, scaling commands do not take a "relative-to" argument.
Rotation of an object can be controlled in many different ways. The easiest case is when you want the object to remain completely upright while it pivots like a compass. In that simple case, you can use functions like obj:setHeading(h) which takes an angle in degrees (0 north, 90 west, 180 south, 270 east). You can also use obj:setCompassDirection("NE") or the like. These commands only work correctly if the model is modeled correctly (facing north).
Another common thing to do is to tell the object to turn to face another object, using the method "obj:setLookAt(otherobj)". The object will instantaneously rotate to face the other object. You can also say "obj:setLookAt(vec(x,y,z))" to make the object look at any arbitrary point in space.
Another common way is to adjust the object's heading, pitch, and roll using object:setHPR(h,p,r). To understand what these numbers mean, think of an airplane. Roll is when the airplane lowers one wing and raises the other. Pitch is when the airplane raises its nose and lowers its tail, or vice versa. Heading is when the airplane turns north, south, east, or west. The numbers H,P,R are in degrees.
Many of the rotation commands take a relative-to argument. The command fred:setHeading(0, alice) causes fred to face in the same direction as alice. What is slightly surprising about this is that you normally think of "setHeading" as rotating an object within the horizontal plane, like an old-fashioned vinyl record player rotates a record. But when used relative to Alice, the plane of rotation itself is relative to Alice. That is, if Alice is tilted slightly, then it is as if the record player were tilted. If Alice is lying down facing upward, then the setHeading plane of rotation becomes vertical.
Like position-setting operations, rotation-setting operations can be relative to self. For example, fred:setHeading(5,fred) causes Fred to turn 5 degrees to the left of where Fred used to be.
Other Things with Position, Rotation, and ScaleEdit
Class 'Transform' is a simple class containing nothing but a position, rotation, and scale field. This class has all the same methods like 'setPosition', 'setHPR', and so forth that move SceneObjects around. In fact, it can be convenient, when searching for a movement method, to look at the documentation for class Transform instead of the documentation for class SceneObject: the same movement methods are all there, without the other clutter.
There are other classes, such as CameraBoom, that also have position, rotation, and scale. All of these classes are called 'transformable' objects. These transformable objects all share the same movement methods. Methods like 'setPosition' that take an optional relative-to argument will accept any transformable class.
Radio-Controlled Movement BasicsEdit
If you have ever driven a radio-controlled car, when you push forward on the stick, it moves not in the direction the driver is facing, but in the direction the car is facing. If you tell it to turn left, it turns to the car's left, not to the driver's left. We call this radio-controlled movement: movement in which directions are expressed relative to the vehicle's own perspective.
This sort of "radio-controlled movement" has a long history in graphics. Probably the first example was a language called Logo which contained a feature called "Turtle Graphics", in which you could give an imaginary turtle commands like "move forward," "turn left 3 degrees," and so forth. The turtle would leave behind a trail, and this is how you did graphics in Logo.
Wild Pockets doesn't contain the commands "move forward," "turn left," and "turn right," but you can get the same effect very easily.
How to Get Radio-Controlled MovementEdit
As mentioned earlier, the commands setPosition, setHeading, and so forth, all take an optional relative-to parameter. What may not be immediately obvious is that relative-to parameter can be the object itself, like this:
Think of this as occuring in two steps: first, it calculates (0,1,0) relative to the car: ie, the point one meter in front of the car. Then, it moves the car to this point. So the net effect is that the car moves forward one meter.
Here's another example:
In this example, we calculate a rotation which is 3 degrees left of where the car is currently oriented. Then, we set the car's rotation. The net effect is that the car turns 3 degrees to the left. (If we had said -3, the car would turn 3 degrees to the right.)
Here is a list of commands that do similar things to what turtle graphics commands do:
Look Out for Floating-Point ErrorsEdit
If you use radio-controlled movement on an object thousands of times, the resulting floating point errors can slowly accumulate. An object which is supposed to remain upright at all times can slowly tilt to one side. An object which is supposed to stay on the ground can slowly start rising or sinking. The solution is to do periodic corrections. There are two commands that are particularly useful:
If an object is supposed to stay upright, force it to stay upright.
If an object is supposed to stay at ground level, force it to stay there.
Moving Objects in Tiny Increments
Motion pictures are actually a series of still images. They only appear to be moving because the frames are flashing in front of you very fast, and the incremental movement from one frame to the next is very small.
You can use this same strategy in Wild Pockets. If you use setPosition to move an object just one millimeter each frame, and repeat this for 1000 frames, it will appear that the object is gliding smoothly across a one-meter distance. In reality, it is moving in 1000 discrete, millimeter steps. But since the steps are so tiny, the user will not see them and will only see the continuous motion.
This is the basic recipe for all procedurally-controlled movement: create a function or background job whose responsibility is to keep moving the object, over and over, in tiny increments.
Straightforward Example: Keyboard-Driven MovementEdit
Here is a function that lets you drive an object around using keyboard keys. It very straighforwardly uses the idea of tiny, incremental movements adding up when applied over multiple frames:
function drive(obj) while true do if (EventHandler.keyIsDown("w")) then obj:setPosition(0,0.1,0, obj) end if (EventHandler.keyIsDown("s")) then obj:setPosition(0,-0.1,0, obj) end if (EventHandler.keyIsDown("a")) then obj:setHeading(3, obj) end if (EventHandler.keyIsDown("d")) then obj:setHeading(-3, obj) end obj:setUpright() Timer.sleep(0) end end
You can test this in the development environment by loading this into the debugging window, then typing:
s = SceneManager.getObject("Small Cube") Coroutines.schedule(drive, s)
Then, use the w,a,s,d keys to drive the small box around.
The way it works is: the function checks the keyboard key "w" and if it is pressed, it moves the object a tiny bit forward. The "s" key causes a tiny backward movement, the "a" key a tiny left-turn, and the "d" key a tiny right-turn. Then, the routine sleeps for one frame. Although the movements are very small, they occur inside a while-loop which causes them to be repeated every frame, so they add up to significant movement.
A More Subtle ExampleEdit
Here is a piece of code which will cause one object to stay within two meters of another:
function stayClose(obj, other) while true do local dist = obj:distance(other) if dist > 2.0 then local direction = obj:getPosition() - other:getPosition() direction = direction:normalize() obj:setPosition(other:getPosition() + direction * 2.0) end Timer.sleep(0) end end
You can test this in the development environment by loading this into the debugging window, then typing:
s = SceneManager.getObject("Small Cube") b = SceneManager.getObject("Big Cube") Coroutines.schedule(stayClose, b, s)
Then, drag the small cube around and watch the big cube follow it.
The way it works is that the subroutine measures the distance between the two. If it's more than two meters, it uses vector mathematics to determine how to move the large box to within two meters of the small one.
This function can move the large box rapidly. If the large box is 100 meters from the small one, this function will move the large box 98 meters in a single frame. But more likely, assuming the small box is moving in gradual, tiny amounts, the large box will only need to move in gradual, tiny amounts to keep up. So even though this function is not explicitly programmed to move the large box a small amount every frame, it will effectively do just that, moving the large box just exactly the amount necessary to keep up, and no more. This is usually a very small amount, so the large box's movement usually appears quite smooth.
Paths: Keyframed MovementEdit
A path is a data structure designed to make it easier to move an object along a complex trajectory. The path stores a series of keyframes - places that the object should visit along the path. Each keyframe has a time indicating when the object should arrive at that position. When a path is applied to an object, the object will move gradually from point to point along the path, linearly interpolating in between keyframes. Here is some sample code that constructs a path:
local path = Path.new() path:setPosition(0, vec(0,0,0)) path:setPosition(1, vec(1,1,0)) path:setPosition(2, vec(2,0,0))
An object that follows this path will start at position (0,0,0) at time zero, gradually move to position (1,1,0) by the time one second has elapsed, then it will move to position (2,0,0) by the time two seconds have elapsed. The easiest way to make an object follow this path is this way:
The first command links the path to a particular object. The second inserts the path into the Path Manager. From that point forward, the path manager checks the path every frame, calculating where the object should be at that point in time, and updating the object's position.
Rotation and Scale PathsEdit
You can also make rotation and scale paths. Here is a rotation path:
local path = Path.new() path:setRotation(0, Rotation.newCompassDirection("N")) path:setRotation(1, Rotation.newCompassDirection("E")) path:setRotation(2, Rotation.newCompassDirection("N")) path:setRotation(3, Rotation.newCompassDirection("W")) path:setRotation(4, Rotation.newCompassDirection("N"))
If an object is made to follow this path, the object will start facing north, then rotate to face east, then north, then west, then north again. The position of the object will not be affected, only the orientation.
Paths actually contain separate position, rotation, and scale "tracks." You can insert position keyframes into a path without ever specifying rotation or scale, for instance. In that case, the path won't affect the object's rotation or scale. A path can contain just one track, two tracks, or all three tracks (position, rotation, and scale).
Keyframes don't have to be spaced evenly: for instance, you could have a path with keyframes at times 0,1,37,and 111. If a path contains multiple tracks (ie, both position and rotation), the keyframes from the two tracks don't have to line up. For instance, a path could have position keyframes at times 0, 5, 10, and rotation keyframes at times 13,14,15,16,17.
The path automatically sorts keyframes by time, so the following two paths are exactly the same:
local path1 = Path.new() path1:setPosition(0, vec(0,0,0)) path1:setPosition(1, vec(1,1,1)) path1:setPosition(2, vec(2,2,2)) local path2 = Path.new() path2:setPosition(0, vec(0,0,0)) path2:setPosition(2, vec(2,2,2)) path2:setPosition(1, vec(1,1,1))
Querying a PathEdit
You can ask a path where an object will be at a given point in time using the method Path:getPosition, Path:getRotation, or Path:getScale.
Paths can ConflictEdit
An object can't be in two places at the same time. If you construct two different position-paths and ask an object to follow them both at the same time, the results will be undefined. Typically, the object will follow one and ignore the other.
However, the position, rotation, and scale fields of an object are separate. So are the position, rotation, and scale tracks of the path. If you construct and activate a position-path, and simultaneously construct and activate a rotation-path, then there is no conflict. The position path will continously update the object's position field, the rotation path will continuously update the object's rotation field. The effects will combine properly.
Sometimes, paths do not appear to be conflicting, when they actually are. For example, consider a path that moves an object in the X-direction, and another path that moves an object in the Y-direction. These seem to be independent axes. But paths don't have separate X, Y, and Z tracks: they have position tracks. If you construct a path that affects X, it also affects Y and Z.
What Paths Cannot DoEdit
When you build a path, you build it all at once, setting up all the keyframes. From that point forward, the object follows the planned trajectory blindly, regardless of what's happening in the scene. This is a fundamental limitation of paths: they execute blindly. A path can be very complicated in the sense that it can wiggle and zig and zag all over the place. But what it cannot do is adjust itself dynamically in response to changing conditions.
For this reason, some types of movement simply can't be done with paths. A good alternative, in these situations, is to create a background job whose responsbility is to move the object a little bit every frame. This process will be explained in another section.
Splining and SmoothingEdit
You can construct a path with nice smooth curves by first inserting some keyframes, and then calling the smooth method. This method will round out the corners of the path. It does so by inserting additional keyframes.
The smooth operator uses splines to calculate the positions of the new keyframes. It treats the keyframes that you inserted manually as control points for a spline calculation. All of the splining algorithms supported go through the control points. To put it differently, the keyframes you originally inserted are not altered - the path will indeed go through your manually-inserted keyframes.
Quick Path OperatorsEdit
For every movement operator starting with the word set such as setPosition, setHPR, setUpright, setCompassDirection, and so forth, there is a corresponding function whose name starts with pathTo, including such functions as pathToPosition, pathToHPR, pathToUpright, or pathToCompassDirection. These functions do nothing to the object - instead, they return paths. The path has already been targeted at the object. If the path is put into the Path Manager using the Path:go method, the object will gradually move from its current location to the target location over a period of one second. For example:
local path = obj:pathToPosition(1,2,3) path:go()
The first command constructs a path which contains two keyframes: time T0 at obj's current location, and time T1 at cartesian coordinate (1,2,3). The path is already targeted at obj. Simply invoking the "go" method causes obj to start moving. It is usually convenient to invoke the "go" method on the same line as the "pathToPosition" method, like this:
Sometimes, you want a path whose duration is other than one second. In that case, the method Path:duration can be used to rescale the path after it has been constructed, like this:
local path = obj:pathToPosition(1,2,3) path:duration(5) path:go()
Now path contains two keyframes, one at time T0 at obj's current position, and one at time T5 at cartesian coordinate (1,2,3). When you launch the path using the "go" method, the object will take 5 seconds to reach its destination.
The duration method returns the path itself. This makes it convenient to invoke all this in one line of code, like this:
Quick Path RulesEdit
Quick Path methods have a set of standardized rules that they all follow:
- All movement operators begin with the word 'set'.
- Set-methods are instantaneous.
- PathTo-methods return a path, whose duration is 1 second.
- For every set-method, there is a corresponding pathTo-method.
Unfortunately, following these rules rigidly has led to some functions with somewhat strange names. For example, the function to snap an object into the upright position is "setUpright" - because all functions that snap an object must start with the word "set." As another example, the function "pathToScaleX" will adjust the X-scale over time. The name of the function doesn't sound quite right, but given the rule that any function that returns a path must must start with the word "pathTo," that's what the function name has to be.
The upshot is that when you see these function names, don't interpret them literally as English sentences. Instead, remember the function naming rules.