This manual is about making games in Wild Pockets. (If you just want to play games, you don't need a manual. Just read the instructions on the webpage with the game.) To understand this manual, you will need programming skills. It is probably sufficient to have taken a single high-school course in programming. If you are a professional programmer, JavaScript developer, or ActionScript developer, you will have no difficulty. In addition to this manual, you will find several other sources of documentation:
Click Here to search the manual by keyword
Good luck, and have fun!
Note: This page only applies to WP version 1.5.
The Wild Pockets development environment is, first and foremost, a saved-game editor. It allows you to open up saved-game files, view the objects, move them around and alter their properties, and then write the data back out to a new saved-game file.
To gain access to the development environment, you need a free account on the Wild Pockets web site. This account gives you access to our development environment, and creates a directory for you on our file server where you can store your scripts, art files, and other assets.
Every game in Wild Pockets consists of at least two files: a Lua script file for the main program, and a saved-game file describing the initial state of the world. The saved-game file must contain the name of the Lua script file.
By default, the development environment loads up the "standard" saved-game file: a file containing two cubes, a sky full of clouds, and a light-colored ground plane (an infinite collision plane set at Z value 0). The standard game has a Lua script file associated with it to allow for basic camera movement in test mode. If you try to 'Test' the standard saved-game, the only thing that will happen is that the unsupported cube will fall to the ground and you will be able to move around the world freely. If you decide to try this experiment, close the test window when you are done.
You will be starting by making a hello-world game. This will consist of a script file called HelloScript, and a saved-game file called HelloGame.
To create the Lua script file, go to the Global Properties window (the gear icon on the right hand side of the development environment), and select the second tab (Script Properties). Click the 'Open Script Directory' button. This will open up a directory on the local machine specifically for Lua script files. The development environment is automatically mirroring the Lua files in this directory to the Wild Pockets file server. Using your favorite programmer's editor, create a file in this directory called HelloScript.lua. This code should be sufficient for now:
function onSceneStart()
print("Hello World")
end
If you now use the 'My Library' icon (red folder) on the right hand side of the development environment, you will be able to see that this script has already been uploaded to our server. Keep the personal library open, and drag the window to the left side of the development environment.
The next step is to create the saved-game file. Since the development environment has already loaded the standard saved-game file, the only step needed is to put the filename of the HelloScript into the world. Do this by opening the 'Global Properties' window and selecting the 'Script Properties' tag again. Next, drag your script from the personal library window into the appropriate slot on the 'script properties' panel. You have now configured this saved game file to run your script. Using the File menu, select 'File/Save As...', and store it as HelloGame.
You are now ready to test your game. Select 'Begin Test' from the 'Test' menu. When you test it, it will look exactly the same as if you had tested the standard game: the unsupported cube will fall to the ground. However, if you use the little icon on the right to open the debugging console, you will see the words 'Hello World' inside the console. Congratulations, you have written your first game.
From this point forward, you can edit both the Lua script file and the saved game file. If you edit the Lua script file, it will be automatically re-uploaded. Then, you can retest it by simply reloading the test window. If you edit the saved-game file, you need to close the test window and relaunch it using the 'Test' menu in the development environment.
Whenever you hit 'Test', the development environment writes out your saved-game file to an autosave location. If you should ever experience a crash in the development environment, there is a good chance that this autosave might contain a great deal of your work. You can load the autosave from the development environment's File-Open menu under the name "__tp__".
Note: This page only applies to WP version 1.6.
To gain access to the development environment, you need a free account on the Wild Pockets web site. This account gives you access to our development environment, and creates a folder for you on our file server where you can store your scripts, art files, and other assets. Once you have created your account, select Build from the menu. This launches the builder, which is the name of our development environment. To use the builder, you will need to install the Wild Pockets plugin if you have not done so already.
Wild Pockets also provides every account-holder with a server folder on our file server. This is your main repository for scripts, art assets, and other game-related files. Wild Pockets also creates a local folder on your hard drive. These two folders are meant to be mirror-images of each other - they will usually contain identical files. You can view the two folders by clicking on the Folders menu in the development environment, and select Open Server Folder and Open Local Folder.
When using Wild Pockets, scripts are created by using your programmer's editor to save them into the local folder. Then, a synchronization tool we provide is used to "mirror" the script to the server folder. Models are created the other way around: the exporter writes to the server folder. Then, the synchronization tool is used to "mirror" the file to the local folder.
You don't need to worry at all about which "side" (local or server) a file is written to. The development environment will look on both sides - server and local. It is smart enough to find the most-recent version, regardless of which side it is stored on. Likewise, the synchronization tool is smart enough to mirror all files, regardless of which "side" they were saved to. It may take a little while to get comfortable with the idea that you can write a file to either side, and things will just work. But that is indeed how it functions.
In fact, the only reason to worry about which side a file is on is for sharing: your game needs to be entirely on the server if you want other people to be able to play it. At any time, you can use the menu item Folders/Sync [my username] to copy all source files to both sides. The synchronization tool is described in greater detail in the chapter on the file server.
For the purposes of this quick start, you won't need the synchronization tool. Instead, you'll just rely on the development environment's ability to find files regardless of which side they're on.
Every game in Wild Pockets consists of at least two files: a Lua script file for the main program, and a scene file describing the initial state of the world. The lua script contains the code of your world. The scene file contains the initial object layout. The scene file must also contain the name of the Lua script file.
You will be starting by making a hello-world game. This will consist of a script file called HelloScript.lua, and a scene file called HelloGame.scene.
To create the Lua script file, open your local folder. Using your favorite programmer's editor, create a file in this directory called HelloScript.lua. (Make sure you have Windows Explorer configured to not hide file extensions, otherwise you may accidentally create HelloScript.lua.txt, which is not correct.) Put the following code into HelloScript.lua:
function onSceneStart()
print("Hello World")
end
In addition to being a general-purpose tool, the development environment is also an editor for scene files. It always has a scene open for editing. By default, it opens the standard scene, consisting of two blue boxes, a sky, and some ground. You probably already have the standard scene open for editing.
Since the development environment is already editing the standard scene, the only step needed is to insert the filename of HelloScript.lua. Do this by selecting File/Global Properties from the menubar. Click on the "Script Properties" tab. You will see that the scene already has a main script, called "defaultSceneScript". Click the red X next to the word "defaultSceneScript" to get rid of that. Then, click the "set" button to bring up a prompt where you can type your script's name. The correct name is your username, a slash, and then "HelloScript.lua". For example, if your username is Bob, you must type Bob/HelloScript.lua into the box.
Now that you have edited the scene, you need to save it. Using the File menu, select File/Save As..., and store it as HelloGame.scene.
You are now ready to test your game. Select 'Begin Test' from the 'Test' menu. When you test it, the unsupported cube will fall to the ground. This is the only obvious behavior. However, if you use the little icon on the right to open the debugging console, you will see the words 'Hello World' inside the console. Congratulations, you have written your first game.
From this point forward, you can edit both the Lua script file and the scene file. If you do, you will need to close and reopen the testing window. If you do not, it will not notice changes to the scene.
Whenever you hit 'Test', the development environment writes out your saved-game file to an autosave location. If you should ever experience a crash in the development environment, there is a good chance that this autosave might contain a great deal of your work. You can load the autosave from the development environment's File-Open menu under the name "__tp__.scene".
When a game is running, it contains three primary pieces of state:
Scene objects are the physical objects that move around your virtual world. In the standard game, there are four scene objects: the two cubes, the sky, and the ground. Normally, scene objects will bump into each other and rebound using the physics system, although some can be immune, such as the sky in the standard game.
Wild Pockets games are scripted in Lua. The programmer creates Lua script files on the Wild Pockets file server. The game contains references to these script files.
Lua, like most programming languages, has global variables. Lua globals are generally used by the script to store whatever it wishes to store. You can write a game using multiple source files, in this case, each source file is loaded in its own scope. Each scope contains global variables.
Wild Pockets has a built-in saved-game mechanism, which creates a saved-game file on the Wild Pockets file server. The saved-game file contains a record of the scene objects and the filename of the main program script file. However, the lua global variables are not saved.
SceneObjects are the renderable objects that move around your virtual world. A scene object has the following attributes:
Every scene object has a position, rotation, and scale. Together, these are called the object's "transform."
All coordinates in Wild Pockets are measured relative to the "world origin," an invisible stationary marker in the center of the pocket. Or to put it differently, we use absolute world coordinates wherever possible.
In general, we like to think of the X-axis as pointing "east," the Y-axis as pointing "north," and the Z-axis as pointing "up." The engine has these assumptions wired into all default values. For instance, the default gravity pulls in the negative-Z direction (ie, down). As another example, the camera naturally orients itself upright – ie, with the top of the screen in the positive-Z direction – unless you force it to do otherwise.
When a scene object is created, it is associated with a 3D model. The model is created in Max or Maya and exported into a file on the Wild Pockets file server. The command to create the object requires that you specify the file server filename of the 3D model.
The 3D model consists of some number of individual meshes. Each mesh consists of a set of polygons, all of which share the same texture. There exist commands to override the texture of a mesh, thereby changing its appearance. You can also hide or unhide an individual mesh. When you alter the texture or visibility of a mesh, you do so on a per-scene-object basis. That is, if two scene objects use the same 3D model, and you alter the texture of one, you haven’t altered the texture of the other.
Wild Pockets uses a skeletal animation system. The virtual skeleton is an interconnected system of virtual bones. It looks and functions a lot like a real-world skeleton. Each bone is a rigid object. The polygons are "rigged" to the bones, meaning they move when the underlying bone moves. A polygon which is near a joint, such as an elbow, may be rigged to multiple bones, in which case the polygon may move when either bone moves. Commands exist in the lua script to move the entire model, and also to move individual bones.
In Wild Pockets, the word "Animation" is used to mean prerecorded movements. Animations are created in Max or Maya and exported to a file on the file server. Be clear on this terminology. There are three ways to generate movement: using the physics engine, using the lua script, and by recording movements in Max or Maya. Of these three, only the recorded ones are called "animations." The other two are just called "movement." Every scene object has a list of animations that it is currently playing.
By default, when you put a scene object into the world, the physics system will immediately begin operating on it. The object will fall to the ground, roll around and eventually come to a stop. If it bumps into something along the way, it will ricochet and impart inertia to the object it collided with.
You can set the anchored flag on an object, which alters the way the physics system affects it. Anchoring an object causes it to become immobile, though it will still collide with other objects.
A scene object may have a particle system attached to it. Particle systems are created by writing little programs in a language called 'ParticleScript.' This feature is covered more thouroughly in Chapter 14
If desired, you can write a script containing a lua class definition, and then you can cause a SceneObject to become an instance of that class by setting the SceneObject's classname. Once you do this, the SceneObject gains a data table where the Lua code can store instance variables.
In addition, the SceneObject has a persistent property storage area where you may write any data you wish. This persistent property storage is recorded in saved game files.
This is far from being a complete list of every piece of data in a Wild Pockets game. For instance, there are lights, there's a camera, and there's a clock, none of which were mentioned. If we were to try to give you a truly comprehensive list at this stage, it would be overwhelming. Suffice to say that the data structures listed above are the big ones - the key elements of a Wild Pockets game.
The Wild Pockets API reference, which is part of the development environment, is indeed comprehensive and includes every piece of data that can be accessed. To access this list, open the builder and select the help question mark on the right side of the screen.
The system contains a timer that in many ways governs the operation of the entire system. The timer calls the Lua functions that you write, it invokes the physics engine at regular intervals, it drives the renderer at regular intervals, it even polls the keyboard and the mouse. Since the timer is at the core of the system, it is useful to understand exactly how the timer works.
The timer can be used to make events occur at scheduled times. The function Timer.later will schedule a function to be called after a specified amount of time. This program is an example:
function onSceneStart()
Timer.later(printHello,1)
Timer.later(printGoodbye,3)
end
function printHello()
print("Hello")
end
function printGoodbye()
print("Goodbye")
end
This program waits until 1 second from the start has elapsed, prints "Hello", then waits until 3 seconds after the start have elapsed, and prints "Goodbye."
The function Timer.every(task, delay) is a variant of Timer.later that schedules a function to be called over and over at the specified rate.
There is an alternate way to achieve the same thing: create a background job. The method Coroutines.schedule creates a background job. Here is some sample code:
function onSceneStart()
Coroutines.schedule(printHelloGoodbye)
end
function printHelloGoodbye()
Timer.sleep(1)
print("Hello")
Timer.sleep(2)
print("Goodbye")
end
The method Coroutines.schedule creates the background job, which starts running immediately after onSceneStart. The background job puts itself to sleep for 1 second, prints hello, puts itself to sleep for another 2 seconds, then prints goodbye. It then exits. See Chapter 4 for information explaining background jobs in greater detail.
This code is actually using the same Timer queue as the first version: the Timer.sleep command uses the Timer queue to wake the background job up at the specified time.
Often, you want to schedule activities to occur once per frame. There is a special notation for this: use a time-value of zero in either Timer.later, Timer.every, or Timer.sleep. This will cause the task to be delayed until just before the next render.
There are several activities that take place before the timer starts running. These are the program initialization activities. These include:
The plugin always begins by loading a saved game, this is true even if you want to start building a new game. To make it possible to start with a blank slate, we have provided an empty save game file "Wild Pockets Team/Blank Scene" a saved game with no scene objects or scripts in it. You start by loading this empty saved game, and then begin adding objects. When you are ready, you save it again on the Wild Pockets file server, under a new filename.
You might wonder why we require the use of saved-game files for simple games, like Pac Man, which don’t appear to need them. In that case, the saved game might very well contain just the filename of the Pac Man script, and nothing else. The saved game file can, in some cases, just be a reference to the game's code. On the other hand, even in a simple game like Pac Man, it might be useful to save some objects in the saved game file. For instance, you could set up all the dots, walls, and power pellets using the level editing tools, and then save the game. When the player loads the game, the walls, dots, and power pellets are already in place.
Wild Pockets is actually a discrete event simulation. What that means is:
Since the Timer clock is governed entirely by math, and not by any actual real-world clock, the simulation is deterministic. Since it is in no way governed by a real clock, in theory, it is possible for the simulation to run hundreds of times faster than real time. To prevent this, a throttling mechanism in place to slow the discrete event simulator down to at most real-time.
But regardless of the throttling mechanism, the simulation behaves deterministically. This makes it much easier to write games with predictable behavior.
The system schedules a render to occur once every 1/60th second. In other words, the game is expected to run at exactly 60 FPS. If the video card isn't capable of keeping up with this, then frames will be silently dropped. What this means is that the task that is supposed to render a frame will skip a frame, to lighten the load on the video card. When a frame is dropped, that fact is hidden from the Lua code. In other words, the Lua code is allowed to believe that a render just occurred, as scheduled, at exactly 60 FPS.
Presenting the illusion of a fixed 60 FPS frame rate makes it much easier for the Lua programmer to write predictable games - in particular, you don't have to worry about writing frame-rate independent behavior, which is always tricky.
The physics engine is actually a Timer task. Every 1/240th of a second, the Timer runs the physics update function. The physics update process calculates forces on all the objects, and updates their positions and velocities. See Chapter 8 for more infomration on the Physics Engine.
It is possible to fire off a task that runs 'in the background,' continuously taking care of some responsibility in the game. The utility of this is best demonstrated with an example.
Suppose that a particular game contains two characters, Alice and Bob. Bob is supposed to rotate continuously to face Alice, no matter where Alice goes. Fortunately, there is already a command that rotates Bob:
bob:setLookAt(alice)
If we use this command inside 'onSceneStart', Bob will rotate only once, at the start of the game. That's not what we want: we need him to rotate every frame. To make this occur, we can launch a background job whose responsibility is to keep rotating him:
function onSceneStart()
Coroutines.schedule(watch, bob, alice)
end
function watch(person, target)
while true do
person:setLookAt(target)
Timer.sleep(0)
end
end
The word 'Coroutines' is a very academic word for "Background Job." Coroutines.schedule launches a new background job that calls the function watch(bob,alice). The watch-routine causes the person to look at the target, then it sleeps for one frame, and repeats. The routine is coded so that you can use it to cause any object to watch any other object.
Timer.later can be used to do anything that background jobs can do. However, background jobs often produce much more readable code. Consider this example:
function printOneTwoThree()
while true do
print("One")
Timer.sleep(1)
print("Two")
Timer.sleep(1)
print("Three")
Timer.sleep(1)
end
end
If launched as a background job, this function will print "One," "Two," "Three," "One," "Two," "Three," and so forth, with a one-second pause between each print. You can accomplish the same thing using Timer.later:
function printOne()
print("One")
Timer.later(printTwo, 1)
end
function printTwo()
print("Two")
Timer.later(printThree, 1)
end
function printThree()
print("Three")
Timer.later(printOne, 1)
end
As you can see, doing it this way requires us to chop the function up into lots of little pieces, and makes the code a bit harder to understand.
When a background job is scheduled, it starts running "as soon as possible." That basically means, as soon as the engine is done with whatever other thing it is doing. For example, if the background job is created during scene initialization, the job will run as soon as scene initialization is done. If the background job is created while handling a mouse click, the background job will run as soon as the mouse handler is done.
This is implemented as follows: Coroutines.schedule pushes the coroutine onto the front of the timer queue, which means it will be the next thing executed after the timer is done with the current task. As a result, if you schedule two background jobs, the most-recently scheduled runs first.
There are two ways to kill a background job. One is to design the subroutine that runs in the background so that it exits on some condition. For example, you could rewrite the 'watch' routine like this:
function watch(person, target)
while (person:getProperty("stoplooking") == nil) do
person:setLookAt(target)
Timer.sleep(0)
end
end
If you start this routine in the background, and then later set the "stoplooking" property of the person, the while loop will terminate, the subroutine will return, and the background job will be done.
The other way to kill a background job is forcefully. To do this, you need the background job's "handle." In order to obtain that handle, you need to use slightly different code to start the background job:
local jobhandle = Coroutines.schedule(func, arg1, arg2...)
Now that you have a job handle, you can use it to kill the background job:
Coroutines.kill(jobhandle)
The subroutine will then stop running. You can also obtain job handles using 'Coroutines.running.'
This needs to be repeated: when a background job is running, it locks out all other engine functions - including other background jobs, and other foreground jobs. It is only when the background job calls Timer.sleep that other aspects of the engine have a chance to run.
This is important for two reasons. First, background jobs seem a lot like threads in other engines, but because they automatically lock out everyone else, there is no need to manually write locking code. You do not really need critical sections when every block of code not containing Timer.sleep is implicitly atomic.
Second, it is important to realize that a background job really can freeze the whole engine by going into an infinite loop. It really is necessary for a background job to sleep on a frequent basis, and in fact, not to run any more than it really needs to.
There are two primary ways for a background job to sleep. One is to call Timer.sleep. The other is to call Coroutines.yield, which causes the background job to sleep indefinitely until explicitly awakened. If a background job uses yield, then the only way to get it to wake back up is to use Coroutines.schedule(jobhandle). The jobhandle can be obtained from Coroutines.schedule, as shown earlier, or by using the method Coroutines.running.
When Wild Pockets calls one of your event handlers, it automatically creates a background job to run the event handler. To put it differently, all event handlers are executed by background jobs. Because of this, it is legal for an event handler to call Timer.sleep.
There are actually several cases the engine automatically creates a background job to run your code. These include:
Any of these can use Timer.sleep.
Be careful - many things are not included. For example, object constructors are not run by background jobs. Therefore, object constructors cannot use Timer.sleep. Any attempt to do so will result in this strange Lua error:
attempt to yield across metamethod/C-call boundary
Wild Pockets has an "event system" whose primary purpose is to notify the program of keyboard keys, mouse clicks, and mouse movement. The event system works as follows: every event has a name, such as "keyPress-leftMouse". To respond to events, you must create an EventMap that connects event names to event handling functions. Here is a simple example:
local eventmap = EventMap.new()
eventmap:setEvent("keyPress-left", handleLeftArrow)
eventmap:setEvent("keyPress-right", handleRightArrow)
EventHandler.push(eventmap)
function handleLeftArrow()
print("You pressed the left arrow key.")
end
function handleRightArrow()
print("You pressed the right arrow key.")
end
The EventMap is basically a table of event names and functions. The command EventHandler.push adds the EventMap to the "event stack." When an event occurs (such as the user pressing a mouse button), the system looks at all the EventMaps in the event stack, starting with the most-recently added EventMap. If the EventMap has an entry for the relevant event name, then the function is called, and event handling is done. If not, the next EventMap in the stack is checked until finally an EventMap responds to the event.
Here is a list of all the major event categories:
Event handling functions may receive additional arguments, depending on the event type. This table shows the arguments for each event type:
The shiftFlag and ctrlFlag are attached to key-related events to indicate what state the keyboard shift and ctrl keys were in when the event was sent. Note that the keyDown event doesn't contain these values yet. You can get the same information by calling EventHandler.getKeyDown("shift") and the like. Here's some example code that utilizes these extra arguments:
function onSceneStart()
local evt = EventMap.new()
EventMap:setEvent("keyPress-enter", handleEnterKey)
EventHandler.push(evt)
end
function handleEnterKey(shiftFlag, ctrlFlag)
print("Somebody pressed the Enter key.")
print("Shift key down: ", shiftFlag)
print("Control key down: ", ctrlFlag)
end
You can just ask whether or not a given key is pressed by calling EventHandler.keyIsDown(key). The argument to this function is a key-name as you might find in a keyPress event. It is possible to get the mouse position without using events by calling Mouse:getX, Mouse:getY, or Mouse:getXY (which returns two values). It is also possible to find out what object in the scene the mouse is pointing at by calling Mouse:getTopObject.
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.
There are three ways to move an object:
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.
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.
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.
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:
car:setPosition(0,1,0,car)
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:
car:setHeading(3,car)
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:
| Move forward | obj:setPosition(0,distance,0,obj) | |
| Move backward | obj:setPosition(0,-distance,0,obj) | |
| Strafe right | obj:setPosition(distance,0,0,obj) | |
| Strafe left | obj:setPosition(-distance,0,0,obj) | |
| Turn left | obj:setHeading(degrees,0,0,obj) | |
| Turn right | obj:setHeading(-degrees,0,0,obj) |
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:
obj:setUpright()
If an object is supposed to stay upright, force it to stay upright.
obj:setZ(0)
If an object is supposed to stay at ground level, force it to stay there.
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.
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 box")
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.
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 box")
l = SceneManager.getObject("large box")
Coroutines.schedule(stayClose, l, s)
Then, drag the small box around and watch the large box 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.
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:
path:setTarget(object1) path:go()
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.
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))
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.
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.
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.
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.
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:
obj:pathToPosition(1,2,3):go()
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:
obj:pathToPosition(1,2,3):duration(5):go()
Quick Path methods have a set of standardized rules that they all follow:
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.
Wild Pockets uses the Lua language to control your game. It is not practical to include the Lua manual inside the Wild Pockets manual - you will need to go read the Lua manual separately to learn the Lua language.
However, we have a slightly customized version of Lua. Although you will not see any major differences between the Lua in Wild Pockets and traditional Lua, you will see some subtle differences. This chapter enumerates those differences.
Before we begin, we should mention that the Lua manual refers to a module system that is built-in to Lua. This module system has been stripped out, and has been replaced with one that retrieves source files from the Wild Pockets file server.
Every game has a main module - a Lua script file which is loaded when the game is launched. The saved game file contains the filename of the main module. In the development environment, open the global scene properties dialog, and look in the script properties. There, you will find the filename of the main module. You can point this at any lua script you have written.
This main module will automatically be "imported" (loaded) when the game starts.
To create script files in the first place, open the development environment. Select the menu item "Open Scripts Directory" from the file menu. This will open an explorer folder. Any file with a lua extension that you put in this directory will be automatically uploaded by the development environment to your account on the Wild Pockets file server. Any changes you make will be automatically uploaded as well.
If you make changes to files in this directory when the development environment is not running, the changes will not be uploaded until the next time you launch the development environment.
When you put a script file into the scripts directory on your machine, it must have the extension "lua," or it will not be uploaded. However, the lua extension is stripped from the filename when it is uploaded. Traditionally, filenames on the Wild Pockets file server don't have extensions.
All filenames on the Wild Pockets file server have usernames. The username becomes part of the filename when a file is uploaded.
For example, if Josh puts a file called "myGame.lua" into his scripts directory, then when the file is uploaded, its name becomes "josh/myGame".
It is intended that your modules should contain only function definitions and class definitions. It is possible to put other commands that are not inside a function into a module, but this may not always be supported.
The first thing that happens when a module is loaded is that the function and class definitions inside the module are processed, top-to-bottom. This has the effect of building up a scope full of functions and classes.
The second thing that happens is that the system looks inside the module for a definition of function _init. If present, this function is called: this is the module constructor.
Finally, if the module is the main module, then the function onSceneStart is called within the module.
In addition to the one lua script file that is automatically imported at game initialization time, you can manually import additional modules using the "import" statement:
import("josh/robotCode")
This will download the Lua script file "josh/robotCode" from the Wild Pockets server, and load it.
When loading multiple modules, each module exists within its own scope. By default, a module cannot see the functions and classes defined in any other module. In fact, a scope is a lua table. When you import a module, this scope table is automatically created. All the defined functions and classes are stored inside this scope table.
The import function actually returns the scope table for the module it imported. Using the scope table, you can retrieve the functions, classes, and global variables from that scope. Here's an example:
robotModule = import("josh/robotCode")
robotModule.killRobots() -- call the function 'killRobots' defined in the module
C programmers might think it's normal to put the import statement at the top of your file, where the C preprocessor include-directives normally go. But that's not what you're supposed to do. Import-statements are executable statements, they go inside functions. Often, it makes sense to put them inside the _init function:
myModule = import("rharke/mymodule") -- wrong
function _init()
myModule = import("rharke/mymodule") -- right
end
It is also possible to put them right inside another function, such as this one:
function calculateCosts(data)
local mathLibrary = import("josh/mathLibrary")
local results = mathLibrary.analyze(data)
...
end
This approach has the advantage that the file will only be imported if the calculateCosts function gets run.
If you import a file twice, it will not be loaded twice. The first time, it will be loaded. The second time, the import statement will just return the same scope table that it returned the first time.
You can use the import-statement to import the main module. This will return you the scope table of the main module. Of course, since the main module was already imported, this is a duplicate import, and will not trigger the main module to be loaded again.
It is possible for two modules to import each other, so that each can call functions that are defined in the other. However, there's a small caveat.
Normally, the import statement doesn't return until the module is loaded and the initialization function is finished. But what if module A imports module B, and then module B turns around and imports module A? In that case, B cannot wait for A to finish initializing, because A is already waiting for B to finish initializing. The deadlock is broken by having the second import statement return before the initialization function is finished. Usually, this is not an issue.
There are three scope levels in Wild Pockets:
Local variables are created when you use the local keyword inside a function.
These are only visible within the one function:
function xyz() local a = 3 -- this is visible only inside function xyz print(a) end
Global variables are created when you assign a variable without using the local keyword. Here, the word 'global' is somewhat misleading - it is true that these variables are visible to all the functions in the module, but they're only visible to functions in the same module:
function wxy()
b = 3 -- this is visible to all functions in this module
print(b)
end
The last level, super-global, is for variables that are visible throughout the system. To assign a super-global, you have to use the table 'GLOBAL'. But to read one, you can just read it like any other variable:
function vwy()
GLOBAL["c"] = 3 --- this is visible anywhere.
print(c)
end
There is a precedence ordering: local variables override global variables, and global variables override super-global variables.
Wild Pockets provides a means for you to define your own classes. Classes work similarly to classes in other object-oriented languages- you can create a class template that describes a set of functionality, and then create instances of that class. Additionally, you can set the class of a SceneObject, so that the object takes on the behavior described by your class. You define a class by calling the “new” method on the global Class:
MyClass = Class.new()
To create an instance of the class, use the new operator on the class:
local instance = MyClass.new()
When an instance is created, a table is created to store data for the instance. You can access fields of the instance using the usual dot-notation:
local instance = MyClass.new() instance.field1 = "hello" instance.field2 = "goodbye" print(instance.field1) -- prints hello print(instance.field2) -- prints goodbye
Fields are not declared. You simply store data to create a field, as shown above.
You can add instance methods to your class using this notation:
function MyClass:method1(arg1, arg2, ...)
print(self.field1)
print(self.field2)
end
This method actually has three formal parameters: self, arg1, arg2. The self parameter is hidden, it is meant to hold the handle of the instance. Using the instance handle, you can access the fields of the instance, as shown above. Note the colon in the method declaration. The colon is what instructs Lua to add a hidden parameter, self, to hold the instance handle. To call this method, you need to use a colon in the method call as well:
local instance = MyClass.new()
instance:method1("apple", "banana")
The colon in the method-call instructs lua to pass the instance handle prior to the other parameters. In other words, this method call is actually passing three actual parameters: instance, "apple", "banana". These three actual parameters correspond to the three formal parameters self, arg1, arg2. Note to C++ programmers: the self argument is directly analogous to this in C++. In C++, you can evaluate the expression this->m_instanceVariable to access an instance variable, but you can also just access the instance variable directly, using the expression m_instanceVariable. The latter syntax does not work in Lua: in lua, you have to use the instance handle explicitly. If you're in a method, the hidden parameter self usually contains the instance handle, so you would say self.instanceVar.
A class method differs from an instance method as follows: to invoke a class method, you don't need an instance. For example, the method MyClass.new is a class method. Class methods are defined as follows:
function MyClass.method2(arg1, arg2, ...)
etcetera...
end
Note the dot instead of a colon in the method declaration. The method call must also use a dot instead of a colon:
MyClass.method2("hello", "goodbye")
So as you can see, class methods are invoked by naming the class explicitly. Since no specific instance of the class is involved, no instance is passed to the method, and the method doesn't have a self parameter to hold an instance handle. You actually can invoke these methods using an instance handle, but it acts the same as if you had used the class name directly. This call is functionally equivalent to the one above:
local instance = MyClass.new()
instance.method2("hello", "goodbye")
You can associate a class with a SceneObject. If you do, the SceneObject will have all the methods of the class, and all the methods of SceneObject, combined. To set the class of an object programmatically, use setScriptClass, which takes a lua script filename and a classname:
obj:setScriptClass("josh/RobotCode","TankBot")
The lua script filename will be imported using the module system. Then, the module will be searched for the specified classname. In the example above, the module "josh/RobotCode" will be imported. This module should contain the command
TankBot = Class.new()
A SceneObject whose class has been set acts like a hybrid between a SceneObject and an object of the class. You can access fields on it, just as you would for any instance of the class:
local obj = SceneManager.getObject("robot")
obj:setScriptClass("josh/robotCode", "TankBot")
obj.hitPoints = 20
obj.attackStrength = 5
It also will support all methods of the class, and all methods of SceneObject, combined. If the class contains a method whose name is the same as a method of SceneObject, then the class method overrides the SceneObject method. If you do override a SceneObject method, WildPockets built-in methods never call your overriddes. Only your code is affected by your overrides. For example, you can override setPosition if you wish, but this won't alter the behavior of built-in mechanisms like the PathManager.
You can also set the class of an object in the builder. Select the object, open the properties panel, and select the script properties. From there, you can edit the script module filename and script classname. When the game starts, the SceneObject will automatically be made into an instance of the class.
It is possible to specify the module and class of a SceneObject in the model exporter. In this case, when you call SceneManager.createObject, the SceneObject will automatically be made into an instance of the class.
If you wish, you can add an instance method _init to the class. This method is called 'the constructor.' This is called whenever the instance comes into existence. There is a secondary constructor called onStart which is only called when a SceneObject comes into existence with a class already assigned. To clarify, here is a table of all the possible ways an instance can come into existence:
| Way to Create Instance | _init | onStart | ||
| Instance created using new | YES | NO | ||
| SceneObject given a class using setScriptMethod | YES | NO | ||
| SceneManager.createObject using a model with a Class | YES | YES | ||
| Object created in builder and given class in builder | YES | YES |
In addition, there is an optional onDelete method which is called if a SceneObject with a class is deleted.
You may be familiar with the Wild Pockets Timer functionality, which lets you set up code to run after a delay. Passing instance-methods to the timer is a little tricky. This won't work:
Timer.later(object1:method1, 10) --- doesn't work.
This seems intuitively correct, it would seem that you're asking the timer to invoke the method object1:method1 10 seconds in the future. But it doesn't work. Remember, a method is really just a function with a hidden parameter, self. When you call a method, you're really just calling a function and passing it an extra, hidden parameter. So we need the timer to call method1, and we also need it to pass object1 as a hidden parameter. That's too much to ask, though: the timer will call method1 if we ask it to, but it won't pass in object1 for us: the timer doesn't have a mechanism to pass parameters at our request. Here's the solution:
Timer.later(function () object1:method1() end, 10) --- This works
Here, we've created a tiny function whose sole behavior is to invoke object1:method1(). We've passed that function to the timer.
Classes on SceneObjects call for some extensions to the way timers work. For example, it is frequently desirable to have a function that is called periodically while an object exists, but to have that timer automatically be terminated when the object is destroyed. This is achieved by providing you two sets of timer functionality:
So for example:
-- doMethod1 is called every 10 seconds, forever Timer.every(function() self:doMethod1() end, 10) -- doMethod2 is called every 10 seconds, until object destroyed self:every(function() self:doMethod2() end, 10)
A SceneObject that has a class can have click-handling methods: onClick, onLeftClick, onMiddleClick, and onRightClick. If these methods exist, they will get invoked whenever you click on the SceneObject with the class. These methods are invoked by an EventMap which is at the bottom of the EventHandler stack. Therefore, if you put any other mouse-click-catching handler on the EventHandler stack, it will override these methods.
We have added a lot of useful classes to the stock Lua interpreter. The following is just a summary - the actual documentation for these functions, classes, and methods can be found in the API reference, which can be found inside the development environment.
We have added the following functions to the Lua math library:
We have added the following functions to the Lua string library:
We have added the following functions to the Lua table library:
We have added classes designed to help record positional and rotational data. These include:
Finally, there is a class Matrix which we ask that you not use - basically, we feel that if you need to use math which is that complex, then we're doing something wrong. However, we've included it just in case somebody needs it for something we didn't expect.
Lua has a built-in class coroutine which does not include any sort of scheduling support. We have replaced this class with our own version, Coroutines, which is integrated with the system timer module.
We have added a module system that allows you to load script files from the Wild Pockets file server. This is described in a previous chapter.
We have added a module system that allows you to define your own classes. If you wish, you can still use Lua metamethods to create your own class system.
Most modern programming languages provide a full library of abstract data types like hash tables, binary trees, stacks, queues, priority queues, and the like. Lua is one of the few languages in which the library is missing these fundamental structures (only hash tables and stacks are provided). This is one of the larger flaws in Lua.
We plan to remedy this by supplementing Lua with these core types. Currently, we have only implemented one: the double-ended queue (Deque). This can efficiently mimic a stack or a queue as well.
We have removed a few things from the Lua interpreter, mostly for safety. These include:
The physics system is a rigid-body simulator supporting joints and collision detection. A "physics step" does several things: it calculates forces on objects, it applies forces to the objects updating their velocities, and it detects whether or not objects have interpenetrated. When the lua script and the physics system are both manipulating an object, they take turns. The following chapters describe the physics system in more detail.
By default, when you put a scene object into the world, the physics system will immediately begin operating on it. The object will fall to the ground, roll around and eventually come to a stop. If it bumps into something along the way, it will ricochet and impart inertia to the object it collided with.
The Lua script and the physics subsystem are both capable of moving objects around. In effect, they take turns doing this: the physics system gets a chance to move the objects a tiny amount, then the script can examine the objects and move them a little.
This coeexistence is governed by the game's timer queue. As mentioned in a previous chapter, the Lua script can schedule tasks to be performed at specific times. The physics system uses this same mechanism to request physics updates at a rate of exactly 240 updates per second. It is in-between these steps that the Lua script can examine the objects and alter them, if desired.
It would theoretically be possible for the physics system to check whether or not two objects have collided by comparing their polygons. However, polygon comparisons are very expensive and slow. Instead, Wild Pockets uses "collision spheres" and "collision boxes." When the 3D modeler creates the model, he adds a set of spheres and boxes to the model that, when taken together, roughly approximate its shape. The collision volumes are considered a part of the 3D model. The physics system treats the object as if it were this set of boxes and spheres. It entirely ignores the polygons.
One of the side effects of using collision volumes is that an object can be modeled with collision volumes that don’t match its shape very well. In this case, you might get some odd movement out of the object. To see the collision volumes for objects in the scene, you can use the following function:
SceneManager.showCollisions(true)
It is also possible to model an object that has no collision volumes at all. In that case, the object is effectively insubstantial. Such objects will be created with physics disabled by default. If you reenable physics, they will fall right through the floor.
Sometimes, you write a game that really has nothing whatsoever to do with physics forces. For example, if you were to write a game of space invaders, physics forces wouldn't be very helpful - in fact, it would be a real nuisance to keep the space invaders from falling under the force of gravity. In these situations, it may be best to simply turn off different parts of the physics system. There are two parts of the physics system that can be turned off: physics force calculation, and collision detection.
Physics force calculation allows an object to have accumulated forces acting on it every frame, these can come from applyForce calls from the scripting layer, collisions, and gravity. Also simply updating an objects position because of an initial velocity is done via the physics engine. If the Path system, or a seperate method of object interaction using setPositions is being used, the physics engine force calculation is not needed. To shut it off use:
SceneObject:setAnchored(true)
Objects that are anchored still execute collision detection and can still affect the physics of other objects, but are not themselves affected by physics. Anchoring is also useful for scenery and for heavy, solid objects that shouldn't be pushed by other objects, such as a stone wall with a tennis ball bouncing off of it. When an object is anchored, the following functions will have no effect on it:
Collision detection is simply the detection of weather a collision is happening. Disabling collision can be very useful in games where objects aren't directly interacting with each other. When writing a game like majhong where multiple tiles may be constantly touching each other, but with no need to affect each other, the collision detection of the physics engine can drag down the speed of the game. Disabling collision detection will allow the objects to move around freely but not move other tiles. To disable collision detection use:
SceneObject:setCollidable(false)
With collision detection disabled on an object, it will not be able to collide with any other object in the scene and will simply pass through the other object without firing a collision handler or applying any force to either object. However, non-collidable objects are still governed by forces such as gravity, or calls from the scripting layer to apply different forces.
To completely remove an object from the physics system, use both
Turning off physics doesn't stop you from moving an object procedurally:
When two objects collide, it may be desirable to run some Lua code. For example, when a ball hits a wall, you might want to play a "boing" sound. To make things happen when objects collide, you will need a collision handler.
Note: if SceneObject:setCollidable(false) is set on either object, neither the collision handler on the object with setCollidable(false) or the object it's hitting will call their collision handler. setCollidable(false) effectively drops out all collision checking from that object, so it is as if the object doesn't exist in the collision system at all. If you would like objects to pass through each other, but still call collision handlers when they do, see
Before we tell you how collision handlers work, we need to explain what collisions actually are: two objects that have very slightly penetrated each other. As soon as the two objects interpenetrate, the physics system starts applying forces to shove the two objects apart. But it may take a little time for the forces to cause the two objects to separate. Because of this, collisions actually have three stages:
This is important: you need to remember that collision is not an instantaneous process - it can take quite a bit of time, in computer terms.
A collision handler is a Lua function that gets called when two objects collide. It takes three arguments: the first object (the object which has the collision handler set), the second object (the object against which it is colliding), and the stage, as described above. The stage is a string which can be "start", "continue", or "end".
The collision handler must be attached to a particular object using setCollisionHandler. Here is a simple example, which causes a ball to play a sound when it collides with something:
function boingCollisionFunction(obj1, obj2, stage)
if (stage == "start") then
Sound.new("alice/boing"):play()
end
end
ball:setCollisionHandler(boingCollisionFunction)
In a typical collision, the collision handling function will get called many times: once at the start of the collision, several times during the continuation of the collision, and once at the end of the collision. But we only want the 'boing' sound to get played once, so we check the 'stage' parameter. We only play the sound at the start of the collision.
When two objects collide, if both objects have collision handlers, then both collision handlers will get called. It is not predictable what order the handlers will get called in.
Sometimes, the two collision handlers may produce twice the effect you desire. For example, consider a billiards table where each ball has a collision handler that makes a "click" sound. When two balls collide, the two handlers will get called, and you'll get two clicks --- not what you want.
In this case, the solution is to use a rule to disable one of the two clicks. A good rule in this case would be for the collision handler of the higher numbered ball to make the click-sound, and the collision handler for the lower-numbered ball to stay silent:
function billiardBallCollisionHandler(obj1, obj2, stage)
if (stage == "start") then
if (obj1.ballNumber > obj2.ballNumber) then
Sound.new("alice/click"):play()
end
end
end
In fact, as a convenience feature, all SceneObjects can be compared against each other with <, <=, >, >=, and == operators and will return mathematically consistent answers (i.e. if objectA > objectB is true, objectB < objectA is also true). This feature can be useful in writing collision handlers similar to the one above.
When two objects interpenetrate, the physics engine immediately applies forces to push them apart. If a collision handler calls the method SceneObject:passThrough, this will suppress these separation forces. Effectively, this will cause the two objects to pass through each other like ghosts.
Joints are connectors between two scene objects that can keep the objects attached to each other. You can use joints to bind two or more objects together so that when one is moved, the other moves with it.
To create a joint using the Joint tool in the builder, click on the first object and drag to the second object, releasing the mouse button with the mouse hovering over the desired second object. This creates a joint between the two objects. you can also create a joint in script using Joint.new and then attach it between two objects using:
myJoint:setObject1(myFirstObject) myJoint:setObject2(mySecondObject)
Note that it is possible to attach a joint to only one object. If this is done, the object becomes anchored to the scene itself at the joint's position. This is useful for doing things like anchoring an object to the ground or to a point in empty space.
Joints also have a position and rotation for their pivots, which is specified in world coordinate space.
myJoint:setPosition(desiredPosition) myJoint:setRotation(axisAlignment)
While all joints have a position, only certain joints (such as hinge and universal) respect rotation. The rotation of the joint determines where the joint's free axes are aligned; hinge joints have one free axis, and the rotation goes from the global positive-y axis to the orientation of the joint's free axis. For universal joints, the rotation goes from the positive-y and positive-z global axes to the two free axes of the universal joint.
Wild Pockets supports the following types of joints (support for more types is coming soon):
| Ball | Allows two objects to rotate about a pivot point arbitrarily in all three axes of rotation. The distance to the pivot point is kept fixed. | |
| Universal | Allows two objects to rotate about a pivot point arbitrarily in two axes, but a third axis is kept fixed. You can imagine this joint as built with a cross-shaped connector. This type of joint is useful for building a bent linkage, where two bars are at an angle to each other but rotating one bar rotates the other bar. | |
| Hinge | Allows two objects to rotate around one specific axis of freedom. | |
| Anchor | Keeps two objects fixed to each other in terms of position and orientation. |
You can select the type of joint in the builder, or by using the Joint:setType command. Note that changing from one joint to another can lose some information if you try to change back; for example, ball joints do not constrain on any axis and therefore have no concept of "joint rotation," so changing from a hinge to a ball and back to a hinge is likely to lose the axis of rotation on the hinge.
Joints must be enabled to actually effect either object they are attached to, otherwise they will just stay at their current position in world space and effectively do nothing. After attaching a joint to two objects and setting its type, you must call:
Joint:setEnabled(true)
This will start the joint acting on its attached objects. If you would like a joint to stop acting on its attached objects, simply call the same function, but set enabled to FALSE. This will again, leave the joint in its current position until you setEnabled(true) again. If you are done using a joint, simply call:
delete(joint)
You can create joints that break when more than a certain amount of force is applied to the joint. To make a joint breakable, use the following command:
myJoint:setBreakForce(desiredBreakForce)
The break force is specified in newtons; specifying 0 denotes the joint should be unbreakable. A reasonable break force to test setBreakForce is around 4000 newtons.
You can also specify a minimum and maximum deflection angle for a joint; the joint will be restricted from travelling outside the angle ranges.
myJoint:setRange(min,max)
The "min" and "max" deflections are relative to the joint's initial position. Note that disabling and re-enabling a joint resets its initial position when the joint is enabled.
There are two different APIs for controlling the camera: class Camera, and class CameraBoom. You must choose one or the other. Do not try to use both, if you do, the results will be undefined.
Class CameraBoom has the same non-physics movement-methods as class SceneObject. All the commands that manipulate the position or rotation of a SceneObject work equally well on the one object of class CameraBoom. If you use class CameraBoom to control the camera, you will be using commands like setPosition, setHeading, setHPR, and the like, and you can also use Path objects to manipulate the CameraBoom.
In order to use class CameraBoom, you must first completely disable class Camera. The code to do this is as follows:
Camera.setMode("manual") -- totally disable class Camera
Attempts to call methods of class Camera after disabling it will produce undefined results.
Once you have disabled the Camera class, you must do all camera manipulation and access via methods of the CameraBoom. To obtain the CameraBoom, use this:
local cameraboom = SceneManager.getCameraBoom()
The methods of CameraBoom are identical to the methods of SceneObject, and they will not be discussed further here. We suggest that for simpler methods to do more standard camera manipulation, use Class Camera.
The rest of this chapter is about using class Camera. If you are using class Camera, you should not obtain a handle to the CameraBoom. Use one class or the other, not both.
The Camera class in Wild Pockets contains various simple movement functions. The basic concept behind the camera is that it has an eye and a focus. The eye is where the camera is and the focus is what the camera is looking at.
Camera.setEye(vec(0,5,5)) Camera.setFocus(vec(0,0,0))
The Camera class includes smooth movement. When you set the eye or focus the camera will animate towards its new position. If you do not want this behavior you can call the following functions.
Camera.setExactEye(vec(0,5,5)) Camera.setExactFocus(vec(0,0,0))
This will instantly teleport the camera to the new location. With these basic controls you can program the camera to do specifically what your game requires. If you need even more control, see Advanced Camera Control below.
Note: Class Camera assumes the camera is always upright and only gives you access to pitch and heading.
The Camera class has a couple built in modes that make the most common camera behavior easier.
The first mode is Sky mode. This mode has the camera orbiting around a single focus. This is useful for a birds eye view while focusing on an object or place. The builder uses this camera by default.
Camera.setMode("Sky")
Camera.setEye(vec(0,5,5))
Camera.setFocus(vec(0,0,0))
Once you've called setMode and positioned the eye and focus there are many simple functions that make moving the camera easy.
Camera.moveForward(3) Camera.moveBackward(3) Camera.moveLeft(3) Camera.moveRight(3) Camera.moveUp(3) Camera.moveDown(3)
The move functions will slide the camera in the direction specified by the amount that is passed to them. One thing to note is that these functions are designed to not move you closer or farther from you focus, but instead slide both your eye and focus.
Camera.zoomIn(3) Camera.zoomOut(3)
The Camera zoom functions will bring your eye closer to your focus.
Camera.rotateRight(10) Camera.rotateLeft(10) Camera.rotateUp(10) Camera.rotateDown(10)
The rotate functions will orbit your camera around the focus. One example of this is:
Timer.every(
function()
Camera.rotateLeft(1)
end
,0.01)
Where rotateLeft is called every 100 milliseconds, this will cause the camera to slowly orbit around the point specified in setFocus.
One other set of really useful functions for Sky mode are:
Camera.beginRotate(100) Camera.endRotate()
When beginRotate is called the Camera class pushes an EventMap that tracks the mouse and orbits the camera based on mouse movement. endRotate pops this eventMap and halts this behavior. An example of how this can be used is below:
cameraMap = EventMap.new()
cameraMap:setEvent("keyPress-rightMouse",function() Camera.beginRotate(100) end)
cameraMap:setEvent("keyRelease-rightMouse",function() Camera.endRotate end)
EventHandler.push(cameraMap)
This example will wait for the right mouse, then have the Camera track the mouse and orbit as long as the right mouse is held down. Combine this with the previous functions and you have the camera controls that are used in the builder.
fpskeymap = EventMap.new()
fpskeymap:setEvent("scrollWheel",function(d) Camera.zoomIn(d/100) end)
fpskeymap:setEvent("keyPress-rightMouse",function() Camera.beginRotate(150) end)
fpskeymap:setEvent("keyRelease-rightMouse",function() Camera.endRotate() end)
fpskeymap:setEvent("keyDown-left",function() Camera.moveLeft(0.2) end)
fpskeymap:setEvent("keyDown-right",function() Camera.moveRight(0.2) end)
fpskeymap:setEvent("keyDown-up",function() Camera.moveForward(0.2) end)
fpskeymap:setEvent("keyDown-down",function() Camera.moveBackward(0.2) end)
fpskeymap:setEvent("keyDown-shift-up",function() Camera.moveUp(0.2) end)
fpskeymap:setEvent("keyDown-shift-down",function() Camera.moveDown(0.2) end)
EventHandler:push(fpskeymap)
Another useful mode is 3rd. When using this mode the camera will follow an object while maintianing the constraints you specify.
Camera.setMode("3rd")
Camera.attachObject(foo)
If you have a SceneObject foo, you can use the above commands to have the camera always keep foo in the center of its vision. If you just use these commands the cameras eye will not move, but only turn towards the object. If you would like the camera to stay within a certain distance of the object you can call the following.
Camera.setFollowDistance(10)
Now the camera will move to always stay 10 meters away from the object. If you want to constrain the heading or the pitch at which the camera stays you can call the following.
Camera.setFollowHeading(90) Camera.setFollowPitch(30)
This will tell the camera to always maintain a global heading of 90 degrees and a pitch of 30 degrees. Passing nil to any of the above 3 functions will remove their constraint.
It is of note that some of the functions that worked for Sky mode will work here too. The rotate and zoom functions will rotate and zoom around the object. The move functions will not do anything in 3rd mode though.
In order to make 3rd mode a lot simpler there is a single function that will set up everything for you.
Camera.follow(foo,10,nil,30)
The above function will automatically put the camera in 3rd Mode and tell it to follow the object foo at a distance of 10, with no constraint to heading and to maintain a 30 degree pitch.
The only other function of interest when using this mode is setSpeed.
Camera.setSpeed(0.3)
This will control how fast the camera animates to its new location. If your object is outrunning the camera then a higher speed will help the camera keep up.
Sorry, this is coming shortly.
In Wild Pockets, the word "Animation" is used to mean prerecorded movements. Animations are created in Max or Maya and exported to a file on the file server. There are three ways to generate movement: using the physics engine, using the lua script, and by recording movements in Max or Maya. Of these three, only the recorded ones are called "animations." The other two are just called "movement."
Wild Pockets uses a bone-based animation system. This means that in addition to creating polygons, the artist creates "bone" objects. The polgyons are "rigged" to the bones, meaning that when the bone is moved, the polygons that are rigged to that bone follow along. In Max, you must use either Skin or Physique to rig your objects to the bones. In Maya you skin the model to the joints with a Smooth Bind.
When exporting an animation from Max or Maya, only bone-movements are recorded. That means that if you want to animate an object with Max or Maya, it must have a bone! Even if all you want to do is animate a cardboard box falling off a shelf, you must put one bone inside the box, rig the box to the bone, and manipulate the box by manipulating the bone. If you find a model in the Wild Pockets engine and you aren't sure if it has any bones, you can see all of the bones in the models in the scene using the following command:
SceneManager.showBones(true)
In many models, bones are interconnected (ie, the forearm-bone is connected to the upperarm-bone), but they don't have to be. For example, if you wanted to create an animation of a car exploding, you would split the car mesh into a dozen or so chunks, and put a bone inside each chunk. By sending each bone hurtling in a different direction, the car will fragment. In this case, you would want the bones to be completely unconnected to each other.
Commands exist in the lua script to move the entire model, and also to move individual bones; see the documentation of class SceneObject for more information on bone manipulation commands.
An animation file specifies where bones should move. If you were to decode an animation file, you would see commands a little like this:
In this, the strings "right-elbow" and "left-knee" are the names of bones in the character’s skeleton. The animation file tells these bones where to go. There are a few technicalities, but generally, that’s the essence of what an animation file is.
An animation can affect all of a model’s bones, or only some of them – for example, a wave animation could be designed to affect only a model’s arm bones. A scene object can be commanded to play an animation resource. Multiple animations can play at once – for example, you could play a walk and a wave animation at the same time.
If two animations affect the arm of the model, then one must be given precedence over the other using a priority system. Note: priority system not implemented yet.
Once you have exported an animation file, you can import it into the scene with the following command:
myAnimation = Animation.load(filename)
Once the animation is loaded, it can be applied to a scene object.
To play an animation on a scene object, use the following command:
mySceneObject:playAnimation(myAnimation,options)
The animation will begin playing immediately after this command is run. The "options" argument is a table containing a set of fields that can modify the playback of the animation:
The properties of the animation that are controlled by the options table can also be modified while the animation is playing. See the documentation of class SceneObject for more information on the functions that modify the properties.
In addition to using an animation to move a model through time, a single frame of animation can be used to pose a model with the following command:
mySceneObject:pose(myAnimation,desiredTime,boneNames)
This command positions the bones in the model based upon a specific snapshot of the specified animation. The desiredTime value is the time (in seconds) to snapshot within the animation. Additionally, boneNames is an array of strings that correspond to the names of the bones in the scene object; if boneNames is specified, only the bones with names matching the strings in the array will be moved, and all other bones will be left alone.
It should also be noted that animations only affect the visible geometry of a scene object. Collision geometry and physical properties aren't tied to the skeleton and so aren't modified by either posing or animation.
Animation blending allows multiple animations to be played at the same time with different weights. This can be very useful when many animations will need to be played on the same object at either the same time, or with seamless transitions between them.
Animation blending centers around the weights on a given channel playing on a scene object. If a scene object only has one animation playing on it, it doesn't matter what weight it has, it will play the full animation as it has nothing to blend against (an exception is a weight of 0, which is special cased out to not playing). If two objects are weighted equally, they will both be blended equally against each other, so two animations playing with weights of 1.0 and 1.0 will act exactly the same as weights of 0.1, and 0.1. Animation weights can be changed fluidly mid animation by calling:
mySceneObject:animationFade(weight, time, channel)
This command provides a way to linearly change weights from the current weight to a new weight over a period of time. For the best results blending from one animation to another, use this function in pair with itself, or playAnimation so that while one channel is fading in, another channel is fading out to 0.
As mentioned before, animation blending has to have something to blend against. Specifically for the case where there is no idle animation to blend all other animations against, this function returns a 0.1 second long looping animation of a scene objects default pose:
mySceneObject:animationDefault()
While in general, animations and the models that will use them go hand-in-hand, the engine is flexible enough to apply an animation to any model. When doing this, bones in the animation that aren't in the model are ignored, and bones that aren't mentioned by the animation won't be manipulated by the animation. Whether it looks good depends on whether the model has a skeleton which is "sufficiently similar" to the one for which the animation was intended.
Wild Pockets provides a set of commands for building GUIs. There are really three possible levels of complexity:
You need to learn how to do each of these in order, starting with the simplest, and working your way up to the level you need.
Here is a simple program that puts a score in the corner of the window:
function onSceneStart()
currentScore = 235
GuiManager.call("paintScore", paintScore, 1)
end
function paintScore()
local scorebox = Rect.new("",0,0,200,25)
scorebox:drawText(nil, {}, "Score: " .. currentScore .. " points")
end
Every frame, the GUI is erased and rebuilt from scratch. The way this works is that you must write a function like 'paintScore' above whose job is to repaint the GUI from scratch. You then use GuiManager.call to tell the GUI subsystem to call your paint-function. Every frame, the this function will get called to rebuild the GUI.
The actual process of drawing the GUI consists of two steps: construct objects of class 'Rect' (rectangle) to lay out the positions of the gui elements. The coordinates are in pixels: (0,0) is the upper-left corner. After constructing the rectangles, you fill those rectangles with pictures and text. In the code above, we start by constructing a rectangle that will hold the score. Then, using the method 'Rect.drawText', we paint the desired string.
Because the GUI is rebuilt from scratch every frame, it tends to update itself naturally. For example, if some other part of the program were to change the contents of the variable 'currentScore', the GUI would immediately reflect that.
The method 'Rect.drawPict' is used to fill a rectangle with a picture. The simplest cases are these:
rect:drawPict(nil, {background="user/zebra.jpg", stretch = false})
rect:drawPict(nil, {background="user/zebra.jpg", stretch = true})
In the first case, the picture of a zebra is tiled across the rectangle. In the second case, the picture of the zebra is stretched to fill the rectangle exactly once.
The first parameter to drawPict is a clipping rectangle. These aren't used that often, so the parameter is usually nil. If you supply a clipping rectangle, then this acts like a cropping tool outside of which nothing is drawn.
If you wish, you can also put something that looks like a picture frame around the zebra. To do so, create a PNG image that looks like a picture frame with an alpha-transparent center. Then, use the foreground keyword to supply the frame:
rect:drawPict(nil, {background="user/zebra.jpg", stretch=true, foreground="user/picframe.png"})
This will draw a picture of a zebra with a frame.
The 'foreground' and 'background' keywords are somewhat badly named - background is really "the image that fills the rectangle", and foreground is really "the picture frame that decorates the edges of the rectangle." This is an important distinction: many people make the mistake of trying to use 'foreground' to draw a picture. But foreground can't do that - foreground is to decorate the edges, not to fill the rectangle. To draw a simple picture, use 'background,' not 'foreground.'
The foreground (picture frame) image must have some alpha transparent areas. Before rendering, the alpha-transparent portions of the frame are analyzed. Some of the alpha-transparent areas may be judged to be on the "inside" of the frame, and others may be judged to be on the "outside" of the frame. (This analysis uses a flood-fill which starts at the outside edge and stops when it gets to an opaque pixel). Alpha transparent areas on the inside of the frame will show the zebra. Alpha transparent areas on the outside of the frame will show whatever was on the screen before the pict was rendered.
If the picture frame does not fit the rectangle exactly, it is expanded using a quadrant-based algorithm. The PNG is chopped into four quadrants: upper-left, upper-right, lower-left, and lower right. The four corners of the PNG are placed into the four corners of the screen Rect. This may leave gaps in the middle. The gaps are filled by stretching the middle pixels of the PNG. This quadrant-based algorithm is ideal for picture frames. It really is not very useful for anything else. The "foreground" keyword is solely intended to enable you to put a picture frame around an image. Note that the 'stretch' keyword only affects the background image. The foreground image is always processed using the quadrant-based algorithm.
To get more complex effects, you can use multiple 'drawPict' calls to layer images on top of each other.
The table which is passed to drawPict may contain a number of other options in addition to 'foreground', 'background', and 'stretch'. See the API reference for a full list.
To display text, build a rectangle, then call drawText. Here is a common case:
rect:drawText(nil, {}, "Now is the time for all good men")
The first parameter to drawText is a clipping rectangle. These aren't used that often, so the parameter is usually nil. If you supply a clipping rectangle, then this acts like a cropping tool outside of which nothing is drawn.
The table can contain a large variety of options. Here is an example of passing options:
rect:drawText(nil, { font="josh/verdana", size = 12, wrap = false }, "Now is the time for all good men")
The font must name a truetype font-file that has been uploaded to the Wild Pockets server. The font-file can be uploaded using a menu item in the development environment, in the file menu.
There are a large number of options to drawText. These can be found in the API reference in the plugin.
Objects of class Rect contain an ID string field. This ID string must be properly initialized if you want your GUI to accept mouse clicks. The ID string is what makes it possible to identify which rectangle the user clicked on. It is standard Wild Pockets practice to name your rectangles using a directory-tree-like naming system. Consider, for example, a dialog box with a message, an OK button, and a cancel button. It would be in keeping with standard practice to name the rectangles as follows:
| Rectangle Purpose | Rectangle ID String | |
| Entire Screen | screen | |
| The Dialog Box | screen/dialogbox | |
| The OK Button | screen/dialogbox/ok | |
| The Cancel Button | screen/dialogbox/cancel | |
| The Message | screen/dialogbox/message |
Note that these are logical, not spatial relationships. The ok button does not technically have to be physically inside the dialog box, it is just conceptually a part of the dialog box. There are a great many constructors that return rectangles. With the exception of Rect.new, all of these constructors require you to pass in a parent rectangle. These constructors concatenate the ID of the parent, a slash, and a sub-ID specified in the arguments to produce the resulting rectangle's ID. Because of this, it is very easy to construct rectangles with the standard naming conventions. Sometimes, a rectangle doesn't have any particular "parent." In that case, you should use the screen rectangle as the parent:
local screen = Rect.screen()
So here's an updated version of the paintScore function on the previous page. The rectangle is now properly named:
function paintScore()
local screen = Rect.screen()
local scorebox = screen:sub("scorebox",0,0,200,25)
scorebox:drawText(nil, {}, "Score: " .. currentScore .. " points")
end
The function Rect.new has been replaced with Rect:sub. These functions differ only in how they initialize the ID string of the rectangle. In Rect.new, the rectangle gets the exact ID string that you pass in. In Rect:sub, the ID is created using the Wild Pockets standard: by concatenating the parent ID, a slash, and the specified sub-ID ("scorebox"). Having a valid ID string in this rectangle will make it possible to detect the presence of the mouse.
To detect the mouse, the first step is to activate a rectangle for mouse input using the 'Rect:activateRegion' method:
function paintScore()
local screen = Rect.screen()
local scorebox = screen:sub("scorebox",0,0,200,25)
scorebox:drawText(nil, {}, "Score: " .. currentScore .. " points")
scorebox:activateRegion()
end
This causes the rectangle to be recorded in a list of input-regions. This list, like the GUI as a whole, is rebuilt once per frame. Now that our rectangle is part of the list of input regions, we can check whether it contains the mouse:
local mx,my = Mouse.getXY() local region = GuiManager.find(mx,my)
The variable region will now contain the ID string of the region containing the mouse. This is subtly different from simply asking whether or not the rectangle contains the mouse using Rect.contains. If you have two active regions that overlap, GuiManager.find will choose the one which is on top.
In addition to simply checking whether the mouse is inside a region, you can set up click handlers. As with the rest of the GUI, the handler tables are rebuilt once per frame, so they need to be set up inside the same paintScore function:
function paintScore()
local screen = Rect.screen()
local scorebox = screen:sub("scorebox",0,0,200,25)
scorebox:drawText(nil, {}, "Score: " .. currentScore .. " points")
scorebox:leftMouseDown(scoreDown)
end
function scoreDown()
print("You clicked on the score!")
end
Now clicking on the score rectangle will cause the scoreDown function to get called. In addition to leftMouseDown, you will find leftMouseClick, which doesn't call the handler until you have clicked the mouse down and then released it. Of course, rightMouseDown and rightMouseClick both exist as well. Functions that set up handlers call Rect:activateRegion implicitly.
When you click on a GUI element that is activated using leftMouseDown or the like, the GUI eats the mouse-down event and the corresponding mouse-up event. When this occurs, the event never gets to the EventHandler/EventMap system.
There are a great many functions in classes GuiManager and Rect to help detect the activity of the mouse. These include such functions as Rect:leftClickInProgress, Rect:hasMouse, and the like. See the documentation for these two classes.
In Wild Pockets, GUI widgets like buttons, scrollbars, menus, and the like are simply subroutines that use the simpler primitives to draw a complex object. Consider, for example, this subroutine:
function Rect:drawSimpleButton(clip, label)
if (self:leftClickInProgress()) then
self:drawPict(clip, { background="josh/buttonDownImage" } )
elseif (self:hasMouse()) then
self:drawPict(clip, { background="josh/buttonHoverImage" } )
else
self:drawPict(clip, { background="josh/buttonStandardImage" } )
end
self:drawText(clip, { halign="center" }, label)
self:leftMouseClick(fn)
end
So this is just a drawing routine like drawPict and drawText - the only difference is, it draws one of three different pictures depending on what the mouse is doing, and it also sets up a mouse handler.
This is a very important concept: our GUI system has no "widget" objects. All we have are subroutines like drawSimpleButton above that draw several drawPict/drawText elements all at once, and which may set up some mouse handlers as well.
Some widgets have state. For example, a check-box widget has a single on/off bit. A scrollbar widget has state that indicates how high/low the thumb is in the bar. When designing a routine that draws a checkbox or a scrollbar, you will need someplace to store the checkbox state, or the thumb position. For this, you will need Rect:userdata.
The function Rect:userdata will return a Lua table where you can put anything you want. The data is specifically associated with a particular rectangle.
The interesting thing about userdata is that your GUI may be discarded and recreated every frame, but the userdata is not discarded. Instead, the system manages to carry over userdata from the previous frame.
The way this works is as follows: the userdata is cached according to the rectangle's ID string. When the GUI is rebuilt, if you construct a rectangle whose ID string is the same as the previous frame, the userdata will be retrieved from the previous frame. This is one reason why it's important to use the predictable Rect naming conventions that are standard for Wild Pockets GUIs.
You must fetch the userdata every frame, or it will be discarded. This is how garbage collection of userdata works: if you stop drawing a given GUI element, its userdata will dissappear.
Here is how you would use userdata to implement a checkbox widget:
function Rect:drawSimpleCheckBox(clip)
local userdata = self:userdata()
if (userdata.__CHECKED) then
self:drawPict(clip, {background="josh/checkBoxChecked"})
else
self:drawPict(clip, {background="josh/checkBoxUnchecked"})
end
self:leftMouseClick(toggleCheckBox)
end
function toggleCheckBox(rect)
local userdata = rect:userdata()
userdata.__CHECKED = not userdata.__CHECKED
end
The checkbox state is stored in the userdata table. The routine that draws the checkbox draws either the checked or unchecked version, depending on the state stored in the userdata table. A mouse click in the region will call the toggle routine, which reverses the value stored in the table.
To implement GUI elements that accept keyboard input, you will need to use the GUI system's keyboard focus handling.
At any time, the focus points at a particular rectangle (or it can be nil). To enable a given rectangle to receive the keyboard focus, use Rect:enableFocus. This takes a pointer to a keyboard-event handling function. If you type when a rectangle has focus, the events will go to that rectangle's event-handling function.
The GUI system will use a series of heuristics to guide the focus to the right rectangle. The heuristics are:
It usually isn't necessary to understand these perfectly, suffice to say that these heuristics generally direct focus to the right place. You can force focus to a particular rectangle using GuiManager.setFocus.
Class Rect includes many 'draw' functions that draw all kinds of prefab widgets, including buttons, checkboxes, radio buttons, scroll bars, drop-down menus, and so forth. These are documented in the API reference for class Rect.
The functions drawPict and drawText both take tables full of options. If you recall:
rect:drawPict(nil, { option1=value1, option2=value2, ... })
rect:drawText(nil, { option1=value1, option2=value2, ... }, message)
These tables full of options are called "Gui Styles."
Parsing these tables can be a little expensive, so if your paint routine is taking up too much CPU time, you can precompile these into actual GuiStyle objects:
local style = GuiStyle.new({ background="josh/tiger" })
rect:drawPict(nil, style)
Using GuiStyle objects may be a little faster than using raw tables. Both the original table form and the compiled GuiStyle form are considered "Gui Styles."
A skin is a table containing a collection of styles and skins. This is a skin:
local style1 = { background="josh/tiger" }
local style2 = { background="josh/lion" }
local skin = { tigerStyle = style1, lionStyle = style2 }
The purpose of skin objects is that you may have a routine, like drawButton, that may draw several different images depending on circumstances. The skin allows you to configure the routine to cause it to draw any images you choose.
Many of our higher-level functions that draw widgets (ie, our button-drawing routines, our menu-drawing routines, and so forth) expect skins as parameters. Not only do they require skins, but they require that those skins contain particular styles. For example, our button-drawing routine is documented as follows:
Rect:drawButton(clip, skin, label, callback)
Draws a button, and arranges a callback to occur if the button
is clicked.
* Uses skin[.buttonDefault].down
* Uses skin[.buttonDefault].over
* Uses skin[.buttonDefault].normal
* Uses skin[.buttonDefault].text (optional)
* Uses skin[.buttonDefault].overlay (optional)
As you can see from the documentation, the skin must contain the fields "down", "over", and "normal". Here is a valid call:
local downStyle = { background="josh/downImage" }
local overStyle = { background="josh/overImage" }
local normalStyle = { background="josh/normalImage" }
local skin = { down=downStyle, over=overStyle, normal=normalStyle }
rect:drawButton(nil, skin, "OK", myCallback)
The GUI Manager provides a "standard skin" which contains all the required fields for any of our supplied widgets, but you can create your own skins to completely alter the appearance of these widgets.
In addition to SceneObjects, the Wild Pockets engine supports another type of 3D object: the Gizmo. Like SceneObjects, Gizmos are drawn in the 3D space. However, unlike SceneObjects, Gizmos never interact with other things in the scene; they are purely visual with no body. They are also much simpler than SceneObjects, allowing only sphere, box, and line shapes to be drawn. Gizmos are primarily intended for creating user interface elements, such as selection boxes, directional indicators, and visual connections between related things in a scene.
To use Gizmos, you must first enable the Gizmo system by doing the following in your onSceneStart method:
Gizmo.setActive(true)
Once Gizmos are activated, you can create some by using the Gizmo.create method. You can create Gizmos that are spheres, boxes, and lines. Regardless of what type you create, they all use the same method.
Here is an example of creating a Gizmo. Here, we simply create a yellow box that sits near the origin:
myBox = Gizmo.create( "box", {
position=vec(0,0,0),
extents=vec(20,20,2),
color=color(1,1,0,1)
} )
The first argument simply states that we want to create a box gizmo. The second argument is a table that sets the default values of the gizmo: position is the origin of the world, and color is yellow. The 'extents' option is unique to box gizmos: Extents tells the box what its height, width, and depth should be.
Once we have created the gizmo, we are returned a Gizmo object, which acts very much like a table. We can change the properties to change the gizmo's behavior; it will immediately change in the 3D view to reflect its new properties. For example, the following code would turn the box red and position it 5 meters above the scene's origin:
myBox.color=color(1,0,0,1) myBox.position=vec(0,0,5)
Gizmos are automatically shown when created; you can hide them by calling their hide() method (myBox:hide()) and hide all the Gizmos by calling Gizmo.hideAll().
As a more complicated example, here is a gizmo that always floats 5 meters above an object referenced by the variable "selectedObject":
myMarker = Gizmo.create( "sphere", {
position=function () return selectedObject:getPosition() + vec(0,0,5) end,
radius=2,
color=color(0,1,0,1)
} )
When you set a function as one of a Gizmo's properties, that function is evaluated every frame and the value the function returns is used as the value of the property. In this example, instead of setting position to a specific value, we set it to a function that calculates the position based on the position of selectedObject.
You can also set a SceneObject to be the value of a property. When this is done, the corresponding property of the SceneObject is read every frame and used as the value of the property. For example, here is how to create a purple line gizmo that connects the centers of two objects referenced by object1 and object2:
connectingLine = Gizmo.create( "line", {
color=color(1,0,1,1),
endpoint1=object1,
endpoint2=object2
} )
Since endpoint1 and endpoint2 are position properties for a line, the endpoints are decided by calling object1:getPosition() and object2:getPosition() every frame. So the line always connects the two objects, regardless of where they are.
Here are all the properties that Gizmos understand:
shared by all Gizmos
box Gizmos
sphere Gizmos
line Gizmos
To load and play a sound, use this code:
s = Sound.new("username/filename")
s:play()
In general, with short sounds like laser zaps, the easiest way to program it is to simply construct a new Sound object each time the laser fires, invoke the play method, and then don't even bother saving a pointer to the sound object. The sound will finish playing, and will then get garbage collected. The system is designed for this sort of programming to be efficient.
Another approach would be to construct just one copy of the 'zap' sound at the beginning of the program, and then invoke the 'play' method each time the laser fires. However, this produces a subtly different effect. If there's only one sound object, there can only be one instance of the sound playing at any given moment. If you were to play the 'zap' sound but then invoke 'play' again before it finishes, you would cut off the first playback and initiate a second one. It might sound like 'za-zap.' On the other hand, if you had two separate copies of the sound, then the two copies would play simulataneously.
By default, sounds play at maximum volume - ie, the volume with which they were recorded. You can only adjust the volume downward. One way to do this is to adjust using the decibel scale:
s:setVolumeDecibels(-10)
The decibel scale is a little hard to get used to - it may be easier to use a scale ranging from 0 to 1:
s:setVolume(0.5)
Where 0.0 is entirely silent, and 1.0 is maximum volume. This scale corresponds to decibel gain using the formula gain_in_decibels = -((1-n)*(1-n)*40.0), with the special case that 0.0 turns the sound off entirely.
You can query the duration of a sound:
local len = s:getLength()
You can also query the status of a sound using
local status = s:status()
The possible status values are 'playing', 'idle', 'downloading', and 'error: error message'. If the sound is not already downloaded, then using the 'play' function will block until the sound finishes downloading. If you do not wish to block, then verify that it is done downloading before playing it.
We have not yet had time to implement sound looping. We know this is an important feature and it is coming soon. As a stopgap measure, you can start a sound, then monitor whether or not it is playing. If not, you can start it again. This isn't perfectly seamless, but it's the best that is currently possible.
The WildPockets particle system is a work in progress. It is designed to be very powerful, but it is not entirely complete right now.
Particle systems are created by writing a tiny program in "ParticleScript." To those familiar with GPU shader languages, ParticleScript will look rather familiar. By providing a scripting language to control the behavior of individual particles, you gain the maximum flexibility and programmability. It would be too expensive to execute an interpreted script for each particle, so ParticleScript is designed as follows: the script is executed once per frame, not once per particle. Each command in the language implicitly affects all the particles, or a selected subset of the particles.
The following chapters describe the ParticleScript language and the Particle API. Please note that our Lua interpreter will not directly interpret your ParticleScript. Chapters 14A-14C will describe how to use ParticleScript, and Chapter 14D will explain how to integrate it into your Lua code.
This chapter is about the ParticleScript language.
The first step in a ParticleScript program is to declare particle attributes. Here is a typical set of attribute-declarations:
varying float3 position varying float3 velocity varying float4 color
This means that every particle has a position and velocity, both of which are float3. The keyword 'varying' means that different particles might have different positions - in other words, it means that the particles don't all have the same position.
The words 'position' and 'velocity' are just attribute names, not keywords. The system does not assign any particular meaning to these words. There is no built-in physics calculation in ParticleScript.
The opposite of 'varying' is 'uniform'. For example:
varying float3 position varying float3 velocity uniform float3 gravity
This means that all particles individually have position and velocity. There is also a gravity force which all particles share - ie, the gravity force is the same for all particles. Uniform attributes can be given initial values:
varying float3 position varying float3 velocity uniform float3 gravity = [0, 0, -9.8]
Attributes can be of type float1, float2, float3, or float4. There are no other attribute types. Constants in ParticleScript can also be float1, float2, float3, or float4. Constants are written as: an open bracket, a series of floating-point literals separated by commas, and a close-bracket. Constants of type float1 can omit the brackets.
After the attribute declarations come the executable statements. The most common of the executable statements is the assignment statement:
position = position + velocity * 0.01
At any given time, a subset of the particles are selected. Assignment statements implicitly apply to every selected particle. If there are no commands to select particles, then by default, every particle is selected. So if the statement above were the only executable statement in the particle system, it would implicitly be done to every particle.
Assignment statements cannot assign to uniform attributes. Uniforms can only be set at initialization time, or in the Lua code.
It is possible to write an assignment statement that operates on a subset of the particles using the select operator. Here is an example:
select (position.z < 0) color = [0,0,1,1]
The first command will select all particles whose Z-coordinate is less than zero. The second statement will set the color attribute (which must be declared float4) to blue. The second statement, an assignment statement, will implicitly affect only the currently-selected particles.
The select command above contains a boolean expression: (position.z < 0). Boolean expressions return "selection sets." The select command requires that the right-hand side be an expression that returns a selection set.
It is possible to store a selection set for later use. The following code does the same thing as the previous code:
selection lowparticles lowparticles = (position.z < 0) select lowparticles color = [0,0,1,1]
Now you have given a name, lowparticles, to the set of particles whose Z-coordinate is less than zero. You can select this group of particles again using "select lowparticles."
There is a built-in named selection set "all". If you say "select all," you will select all particles.
To create new particles, you need to use one of the spawning operators. A common operator is spawnsteady, which spawns particles at a steady, fixed rate, specified in particles per second. The particles that are created have zeros in all their varying attributes. They must be initialized.
All spawning operators implicitly create a named selection set called "new", which contains a list of all the particles that were just spawned. This selection set makes it possible to initialize the new particles.
Here is some sample code that creates new particles and initializes them:
spawnsteady 5.0 select new position = [0,0,0] velocity = rand.unitvec
The first statement causes particles to be spawned, at a rate of 5.0 particles per second. It also implicitly sets the selection set 'new' to the new particles. The second statement selects the new particles. The two assignment statements only affect the new particles - they initialize the position to zero, and the velocity to a random unit vector.
The delete command requires you to pass in a selection set. It removes the selected particles from the system:
delete (position.z < 0)
In the example above, the boolean expression returns a selection set containing all particles whose Z-coordinate is less than zero. The delete command deletes them. This is a common command: it gets rid of particles that have fallen through the floor.
After a delete operator, all named selection sets are zeroed out (excepting 'all', which still references all particles).
To render particles, use one of the rendering commands. A typical example is renderorbs, which renders blurry round balls at specified locations:
select all renderorbs pos:position color:[1,0,0,1] size:0.1
Rendering commands cannot render a subset of the particles. For clarity, you must 'select all' before rendering.
Rendering commands usually use keyword-style parameters, explained below.
Some commands, especially the rendering commands, take lots of parameters. To make these commands easier to understand, the parameters are passed "keyword-style". Each parameter consists of a keyword, a colon, and an expression. In addition, some commands follow the keyword-parameters with keyword-flags: single words that alter the behavior of the command. The "renderorbs" command is a good example:
varying float3 position varying float4 color ... renderorbs pos:position renderorbs pos:position color:color renderorbs color:color pos:position renderorbs pos:position add
The renderorbs command takes three keyword-parameters: "pos," "color," and "size." It also accepts the keyword-flag "add". As you can see, the keyword-parameters can be specified in any order. The flags can also be specified in any order, though they must follow the keyword-parameters.
Some expressions expect a value of a given type. If you pass in an expression of a different type, automatic conversion will take place.
When converting to a smaller type, the conversion involves truncation: the extra values are discarded. For example, converting [1,2,3,4] to float2 yields [1,2].
When converting a float1 to a larger type, the conversion involves replication. For example, converting [7] to float4 yields [7,7,7,7].
When converting anything other than float1 to a larger type, the conversion involves padding with zero. For example, converting [5,6] to float4 yields [5,6,0,0]. However, float1 is an exception.
The four elements of a float4 are called X,Y,Z,W in that order. It is possible to rearrange the values using a "swizzling operator." The swizzling operator consists of a dot followed by the letters X,Y,Z,W - not necessarily in that order. The swizzling operator extracts the named portions, in the named order, and constructs a new value from those:
# extracts three copies of Z from the constant - therefore, sets position to 3,3,3 position = [1,2,3,4].zzz # extracts W,Y,Z from the constant - therefore, sets position to 4,3,2 position = [1,2,3,4].wyz
Swizzling can be combined with automatic conversion. For example, let's say I do this:
varying float4 attr1 attr1 = [1,2,3,4].z
In this example, the swizzle operator takes the input [1,2,3,4] and returns the value [3]. Then, this value [3] is automatically converted to float4 to assign it to attr1. The automatic conversion produces [3,3,3,3].
Particle systems can contain multiple 'groups' of particles. Groups are almost completely disjoint - it is almost as if you had two separate particle systems. Particles in different groups have different attributes, different commands, and different selection sets.
To create a group, use the group name command. All commands that follow the group-declaration implicitly apply to particles in that group only.
Currently, groups are truly disjoint: there are not currently any commands that communicate between two groups. However, there will eventually be commands that pass data from one group to another.
This section is a list of all the commands in particle script.
varying
Creates a varying attribute. Examples:
varying float3 position varying float4 color
uniform
Creates a uniform attribute. Examples:
uniform float3 gravity = [0,0,-9.8] uniform float1 spawnrate = 5
selection
Creates a named selection set. Examples:
selection lowparticles
select
Selects a subset of the particles. The right hand side must evaluate to a selection set: this can either be a named selection set or a boolean expression.
select lowparticles select new select (position.z < 0.0)
assignment
The assignment statement is the only command that doesn't start with a keyword. Instead, it starts with an attribute or named selection set. The right hand side must be a float-expression (in the case of an attribute) or a boolean expression (if the left hand side is a named selection set).
position = position + velocity lowparticles = (position.z < 0)
delete
Deletes a subset of the particles. The right hand side must evaluate to a selection set: this can either be a named selection set or a boolean expression.
delete (position.z < 0) delete all
spawnsteady
Spawn particles at a steady rate. The right hand side is a spawn-rate. The particle attributes are filled with zeros. The named selection set 'new' is set to the new particles.
spawnsteady 5.0
spawnuntil
Spawn particles at a steady rate until a specified limit is reached. The parameters are spawn-rate and maximum-total-particles. The particle attributes are filled with zeros. The named selection set 'new' is set to the new particles.
spawnuntil 20.0 100
renderpoints
Renders particles as points using GL_POINTS. Keyword parameters: pos, color. Keyword flags: blend, add. The blend flag enables regular blending, the add flag enables additive blending (you can't specify both).
renderpoints pos:position renderpoints color:clr pos:position add
renderlines
Renders particles as line segments using GL_LINES. Keyword parameters: begin, end, begincolor, endcolor, color. Keyword flags: blend, add. The begin and end parameters specify the two endpoints of the line. The begincolor and endcolor specified the colors of the two endpoints. The color, if present, sets both begincolor and endcolor. The blend flag enables regular blending, the add flag enables additive blending (you can't specify both).
renderlines begin:position1 end:position2 begincolor:color1 endcolor:color2 renderlines color:[1,0,0,1] begin:position1 end:position2 add
renderorbs
Renders particles as hazy, blurry orbs. Keyword parameters: pos, color, size. Keyword flags: blend, add. The blend flag enables regular blending, the add flag enables additive blending (you can't specify both).
renderorbs pos:position1 size:0.2 renderorbs color:clr pos:position add
renderbillboards
Renders particles as textured square billboards. Keyword parameters: pos, color, rotate, size, and texture. Keyword flags: blend, add. The blend flag enables regular blending, the add flag enables additive blending (you can't specify both).
renderbillboards texture:"username/image" size:0.2
group
The group command delineates disjoint groups of particles. A group command should be followed by attributes, selection sets, executable commands, and rendering commands for that group.
group fire ... group smoke ...
What's Missing
Many important commands are obviously missing: Ways to spawn particles at non-steady rates is one. Another clear gap is the the inability for different groups to interact with each other. The particle system is a work in progress, these expansions are coming soon.
ParticleScript has a number of built-in operators and functions. These are listed here:
math operators
The operators + - * / are all supplied. These operate componentwise. The type returned is equal to the type you pass in - ie, if you multiply float3, it returns float3. If the inputs are not of the same type, it converts the smaller one to the larger.
swizzle operators
A period followed by the letters X,Y,Z,W is a swizzle. This rearranges the components, and possibly changes the number of components in the input value.
comparison operators
Comparison operators like > < >= <= == work on values of type float1. If you pass in a value that is not float1, only the first component will be used. These operators return selection sets. Boolean operators like 'and' and 'or' are not yet implemented.
new
New is a built-in expression that returns a selection set containing the most-recently-spawned particles.
all
All is a built-in expression that returns a selection set containing all particles.
host.position
The expression host.position returns the current position of the SceneObject to which this particle system is applied.
host.forward, etc
The expressions host.right, host.left, host.forward, host.backward, host.up, and host.down return the six axis vectors of the SceneObject to which this particle system is applied.
rand.unitvec
Returns a random float3 unit vector, evenly distributed on the unit circle.
rand.negpos
Returns a random float4 vector, with each component between -1 and 1.
rand.zeroone
Returns a random float4 vector, with each component between 0 and 1.
age
Returns the age of the particle, in seconds.
time.frame
Returns the amount of time elapsed in this rendering frame. Note that particles are one of the few aspects of Wild Pockets that do not run at a fixed framerate.
time.curr
Returns the real-time clock of the current frame.
time.prev
Returns the real-time clock of the previous frame.
trajectory(...)
Computes the position of an inertial particle, given four parameters: initial position, initial velocity, gravity, and time elapsed. Uses a closed-form equation.
undulate(...)
Returns a smooth waveform that rises and falls randomly. The waveform was constructed by generating short samples of sinusoidal waveforms at random frequencies and overlapping these short samples at random offsets. The undulating waveform is good for modeling many physical phenomena, such as the way a spark drifts away from a camp-fire in a semi-random walk. The return value is float4, each component undulates independently. The first parameter is time (offset within the waveform), the second is a random seed.
lerp(...)
Interpolation. Takes three parameters: A, B, fraction. If fraction is zero, returns A. If fraction is 1, returns B. Otherwise, interpolates.
circle(...)
Computes the position of a particle that is rotating in a unit circle around the Z-axis at a rate of 1 revolution per second. The input is an amount of time elapsed.
normalize(...)
Given a float3, returns a normalized version (a vector of unit length).
What's Missing?
Clearly, this is a very skeletal function list. There should be dozens, if not hundreds of useful functions. This will be expanded in time.
The command to set a particle system on a SceneObject is setParticleSystem, which takes a string. The string should be a ParticleScript program. Here's an example:
obj:setParticleSystem([[ # Controllable parameters. uniform float3 gravity = [0,0,-10] uniform float1 spawnrate = 50 uniform float3 randinit = [1,1,0.2] uniform float3 constinit = [0,0,3] # Particle state. varying float3 position varying float3 initpos varying float3 initvel # deletion and spawning, and initialization delete position.z < 0.0 spawnsteady spawnrate select new initpos = host.position initvel = randinit * rand.negpos + constinit # Particle update code. select all position = trajectory(initpos, initvel, gravity, age) # rendering renderorbs pos:position color:[1,0,0,0.4] size:0.2 renderorbs pos:position color:[1,0.6,0.6,1.0] size:0.1 add ]])
The scene object will immediately begin generating particles. You can remove the particle system by calling SceneObject:clearParticleSystem().
The Lua code can reach into the particle system and adjust the values of uniform attributes. To do this, use the methods SceneObject:particles, which returns the particle system handle, and the method ParticleSystem:setUniformValue:
obj:particles():setUniformValue("Main.spawnrate", 1.0)
When using commands like setUniformValue, you must refer to attributes using their extended name, which consists of the group name, a dot, and the attribute name. The default group name, if not specified in the particle system code, is "Main."
Removing a particle system from a scene object using clearParticleSystem will instantly cause all the particles to disappear. Sometimes, you don't want the particles to vanish in a blink, sometimes you want them to dissipate in a more natural way. One way to achieve this is to have a uniform spawnrate variable in your particle system. To get rid of the particle system, set the spawn rate to zero. Then, give the particle system time for the particles to go away on their own. Finally, several seconds later, clear the particle system.
Wild Pockets provides a file server, where all models, textures, scripts, and other assets are stored. Every Wild Pockets account comes with a folder on the file server. The file server provides several advantageous and unusual features, described in this section.
When an asset is stored the file server, it has a pathname. Pathnames always start with a Wild Pockets username. For example, if your username is "Bob", and you upload a texture called "Brick.jpg" to your server folder, the pathname of that asset is "Bob/Brick.jpg". When loading an asset from a game, you must provide the full path:
object:setTexture("Bob/Brick.jpg")
Pathnames in Wild Pockets are similar to pathnames in Unix or Windows. Directories are separated by forward slashes. The legal characters allowed in a filename are somewhat restricted: letters, numbers, spaces, dashes, underscores, and a few other punctuation marks. For the most part, all filenames must have valid extensions (however, there are legacy files on the file server that were created before this rule was put into place).
Your game can load assets that belong to other users, if they have granted you permission. To do this, just put the correct paths into your scripts:
SceneManager.createObject("Alice/Dragon.geom")
SceneManager.createObject("Steve/Lizard.geom")
SceneManager.createObject("Fred/Snake.geom")
Or, use a drag-and-drop tool such as the library or the file browser to find the asset and drag it into your game.
The development environment contains a menu item: Folders/Open Server Folder. This opens up a file browser that shows you the contents of your folder on the server. The file browser functions much like any other operating system's file browser. You can view the contents of your directory, sort it by name or type, and move from directory to directory. The file browser has a right-click menu that allows you to delete, rename, and do other manipulations to files.
When you upload a file to the server, the server gives it a version number 0. The next time you upload that same file, it gives it the version number 1. If you then delete the file, you have actually created a blank version - version 2 is empty. If you upload the file again, you have now created version 3. The server remembers all versions indefinitely. (The file browser only shows you the latest version.)
Storing old versions is very useful for backups: we plan to provide a tool that shows your files as they were at a particular time. This will allow you to recover from any disaster. This tool is not implemented yet, until it is written, we are happy to help our users restore files manually. We can restore to any time or date.
In addition to backups, versioning is particularly useful for ensuring that games don't break. When you test your game and the game loads an asset, the development environment records what version of the asset was used. If you then choose to publish the game, the published game will continue to use the same version of the asset that you saw in testing mode -- even if a more recent version of the asset is written to the file server. This means that assets can be updated on the file server without any risk of published games that use those assets breaking. Version locking is described in greater detail in a chapter of its own.
The file server will only store files in certain formats: you cannot upload files unless they are one of the recognized file formats understood by Wild Pockets. Any file that is not in a valid format will be ignored by the file-uploading tool. You must use the correct extension, or Wild Pockets will ignore the file. Here are the understood formats:
| File Type | File Purpose |
| JPG | Texture image in JPG format. |
| PNG | Texture image in PNG format. |
| LUA | Lua Script File |
| MP3 | Sound file in MP3 format |
| GEOM | 3D Model |
| ANIM | Animation file |
| SCENE | Scene File (the main file of a game) |
| TTF | Truetype font file |
| TXTR | Texture image in legacy TXTR format (obsolete) |
The development environment creates a folder on your hard drive whose purpose is to mirror portions of the file server. Usually, you would mirror your own directory, but you can mirror other directories as well if you have been given access.
To view the local mirror of your server folder, click Folders/Open Local Folder. The directory will open using your operating system's standard file browser. The local folder is meant to contain identical files to those in your server folder.
When using Wild Pockets, scripts are created by using your programmer's editor to save them into the local folder. Then, a synchronization tool we provide is used to upload the script to the server folder. Models are created the other way around: the exporter writes to the server folder. Then, the synchronization tool is used to download the file to the local folder.
Running the sync tool causes all newly-created files, regardless of which side they were created on (server or local), to be copied to the other side. If you edit a file and update it, then the sychronization tool is smart enough to find the edit and propagate the modification to the other side. No matter what you do to either the server or local folder, the sync tool will find the alteration and propagate it to the other side.
If you modify either the server or local folder, and forget to run the sync tool, then the server folder and local folders will differ. This is no problem: the development environment loads assets from both the local and server folders. It is smart enough to find the most-recent version, regardless of whether it was altered on the server side or the local side.
The upshot is this: you can edit files in the local folder, or on the server folder, or both, and things will continue to "just work." You can run the synchronization tool whenever you feel like it, and things will "just work."
To invoke the sync tool, click Folders/Sync [my username]. This will attempt to mirror the entire contents of your server folder to your local folder, and vice-versa.
The sync tool will scan your local folder and your server folder to see what has been altered. It is smart enough to find alterations on either side. When it is done scanning, it will present you with a list of files that have been altered. For each alteration, it will present you with the necessary action to propagate the change: upload the local copy, download the server copy, or delete both copies.
The listing produced by the synchronization tool looks like this:
| Sync Action | Filename |
| Upload to Server | Bob/Brick.jpg |
| Download to Local | Bob/Intro.scene |
| Delete | Bob/Obsolete.jpg |
The sync tool will always try to propagate your edits. But there are some situations where you might not want to stop it from doing that. For example, suppose that the sync tool tells you that you edited a file locally, and the modifications need to be uploaded. You examine the file and realize that sure enough, you did edit it, but it was an accident. In that case, you can ask the sync tool to download the file instead of upload it - effectively overwriting your unintentional modifications. To do this, click the download-button next to the file name. This will force the sync tool to download the file instead of uploading it.
There is one situation where the sync tool doesn't know what to do: if you alter a file in the server folder (say, using the file explorer), and alter the same file in the local folder. In that case, you have edits on both sides, and it doesn't know which set of edits to propagate. In that case, the sync tool will show you that there is a conflict, and will ask you what you want to do. You answer by clicking the upload-button, download-button, or delete-button next to the file name.
When you hit the "OK" button, the sync tool will initiate uploads and downloads. It will take a little time for all the transfers to complete. The sync tool will show you its progress.
Normally, you just synchronize your own folder. But on occasion, you may want to sync files that belong to other users to your own hard drive. You can only do this if you have been granted permission. To do this, use the Folders/Sync Other... menu item, and type in the name of the folder you wish to synchronize.
Sometimes, syncing somebody else's folder is a part of your development process. In that case, you may wish to add a menu item to the Folders menu to allow you to synchronize that folder without having to type its name in over and over. To do this, use the Folders/Configure File Sync configuration panel.
If your own folder gets very large, it may take quite a bit of time to sync the whole thing. In that case, it may be desirable to sync just a subfolder. You can do this using Sync Other or Configure File Sync.
Other users can access your files if you grant them permission. There are several kinds of rights you can grant:
Permission grants can be controlled via the file browser (right click a file or folder), or via the gallery publishing page. Every file has an access control list. A typical access control list might look like this:
| Person | Permission Grants |
| David | Read, Use, Preview, Play |
| Everyone | Preview |
Here, you have granted David all rights except the ability to alter the file (no write access). The token "Everyone" is a special word that grants rights to all Wild Pockets users. In this case, the access control list grants everyone preview-only access.
If you grant permissions on a folder, then you have granted rights to all the files inside the folder. For example, if you're an easygoing free software developer, you might decide to grant Read access of your main folder to everyone. If you do that, everyone on the system will be able to experiment with, use, and download your files.
If you grant permissions on a folder, and also to a file in that folder, the rights are cumulative. For example, if you grant read-access to the entire folder, and write access to just one file, then that file has both read and write access. This is important: you cannot remove access by creating grants: you can only add access by creating grants.
When running your program in test mode, you will see an exclamation-point icon on the right side of the display. This opens the debugging console. This icon will not be present when you run the game normally.
The debugging console is there to show you any error-messages that may occur when your program is running. It also shows the output from any print-statements that you put into your code.
If you manage to lock the engine hard (for example, by writing a Lua infinite loop), you will not be able to use the debugging console. However, you can still view the output from your program by bringing up the engine debug log.
In Microsoft Windows, the engine debug log can be accessed by right-clicking on the Wild Pockets tray icon in the windows notification area in the lower-right corner of the taskbar. This brings up a pop-up menu. Select "Engine debug log" in this menu to bring up the log. This doesn't let you enter commands, but at least it lets you see the output of your program.
If your program generates an error, the debugging console should show a traceback. This tells you not just the subroutine that generated the error, but also all the subroutines that called the one that generated the error.
It is possible to use tracebacks deliberately, with the help of the assert function. The 'assert' function takes two arguments, the first of which is a boolean expression and the second of which is an error to report of the assertion is false:
assert(n <= 5,"N is not supposed to be larger than 5!")
If the assertion is false, it will generate a traceback showing you how the program got to the offending state. A similar function to assert is the error(message) function, which generates a traceback every time it is run.
The development environment contains a menu item Debug/Begin Test. This launches your game in "test mode." Running your game in test mode is similar to, but not exactly the same as running your game in production mode. Production mode is used when you embed your game on a webpage, or publish it in the gallery. Here are the key differences between test mode and production mode:
The following sections will describe all the useful testing, debugging, and performance tuning features provided by the Wild Pockets system.
The debugger is a pop-up window that can appear when your program is running in test mode. When the debugger pops up, your program freezes. Your program remains frozen until the debugger is dismissed.
There are several ways to pop up the debugger. One is to simply hit the "pause-break" key on the keyboard. Another is to set a breakpoint using the breakpoints dialog in the development environment. Another is to insert a call to Debugger.breakpoint directly into your code.
You can also pop up the debugger in the development environment itself (by pressing pause-break). This is not very useful, but it does make it easy to familiarize yourself with the debugging window.
The debugging window contains the following major parts:
The call stack shows where your program is stopped. The topmost line in the call stack is the currently-executing function. The next line is the function that called the currently-executing function, and so forth.
You will notice that sometimes, some of the lines in the call stack may be labeled "SYS". This is because you may at times be executing not only your own Lua code, but also Lua code that came with Wild Pockets itself. The "SYS" lines represent lua modules that came with Wild Pockets. Likewise, you may at times see a line labeled "System Kernel." This, too, is Lua code that comes with Wild Pockets.
Sometimes the function names in the call stack may be wrong. This is because Lua unfortunately does not record the function name inside the function declaration - it only records the name from the call site. Consider this code:
function foo() print("hi") end
bar = foo
bar()
Here, the function was declared as "foo," but we then copied the function into a variable "bar", and the call-site is invoking "bar." If you break, the debugger will show "bar", the name from the call-site. We hope, in the future, to modify the Lua system to fix this. Fortunately, the filename and line number are always correct. You can use this to find the real function.
The last line in the call stack is always "Debugging Scope."
Some visual debuggers will actually show you the code and put a little arrow indicating where the program is stopped. Unfortunately, we haven't implemented that yet. We hope to provide this functionality soon.
The debug console inside the debugger is different from the standard debug console in that it evaluates expressions inside the current scope. For example, let's say your program contains this code:
function foo()
local a = 3
Debugger.breakpoint()
end
When Debugger.breakpoint is executed, the debugger pops up. The program is now stopped inside function foo. You can now type this into the debug console:
print(a)
And it will print "3" - the value of the local variable inside function foo.
The there are often radio buttons next to the lines of the call stack. By clicking one of these radio buttons, you change what scope the debugging console is operating in. By clicking on the bottommost radio button (labeled 'Debugging Scope'), the debugging console becomes a regular debugging console that operates in its own global scope.
Underneath the debug console is a button labeled locals. Clicking this causes all the local variables of the currently-selected scope to be printed into the debug console.
In addition to being able to print out values, you can also modify them. For example, if you were running function foo (above), and if you were to then type this into the debug console:
a=3
This would modify the value of the local variable 'a'.
We have noticed a bug in the local variable access: local variables defined inside if-statements, while-loops, and the like are not detected by the debugger. Only locals declared at function scope are seen. We hope to fix this weakness in a future release of the debugger.
You can type any expression at all into the debug console, and it will try to evaluate it. For example, if your game contains a function killRobots, you may very well be able to use it directly from inside the debug console to kill the robots in your game. However, there are some serious caveats.
The first thing you need to remember is that your program is in the middle of executing some subroutine. Your program may not be designed to handle arbitrary modifications in the middle of a subroutine - there is no guarantee that suddenly removing all the robots won't confuse the heck out of your program.
Second, remember that your program is frozen in time. If you execute a command like "Timer.later" inside the debug console, later will never arrive (unless you close the debugger).
Basically, most code will work in the debug console, but if you do very intrusive alterations of the game state, it may have unexpected repercussions.
The import statement works inside the debug console. With the help of the import statement, you can call functions inside any module.
For example, let's say your game contains a module bob/robots.lua, and this module contains a function killRobots. Simply typing this won't work:
killRobots()
Because 'killRobots' is defined only inside the module, and you're not inside that module. However, this code will work:
import("bob/robots.lua").killRobots()
The import pulls up the scope for that module, and the dot-operator pulls the necessary function out of that scope.
In the future, we will probably include a drop-down in the debugging window to make it easier to get at the scopes of your many modules.
When you're done examining your program's state, you can click the "cont" button (which stands for 'continue') to simply resume execution. If you do so, the debugger will disappear and the program will continue executing until such time as somebody presses pause-break again or another breakpoint is hit.
It is also possible to single-step:
Some visual debuggers will actually show you the code and put a little arrow indicating where the program is stopped. This is particularly useful when single-stepping, because you can watch the execution proceed through the code. Unfortunately, we haven't implemented that yet. We hope to provide this functionality soon.
The breakpoint window can be accessed in two places: in the development environment in the "debug" window, or in the debugger itself inside the "edit" menu. Either way, it brings up the same breakpoints dialog.
The breakpoints window requires you to type in a filename and a line number. After typing them in, click the add-button. The breakpoint is added to the list. A small check-box can be used to remove the breakpoint from the list.
Once set, the breakpoint stays set no matter what game you are testing or editing. Breakpoints remain set even if you leave the development environment and come back, or if you switch to editing a different game. The only way to get rid of a breakpoint is to go into the breakpoint editor and remove it.
You can also ask your game to break when test-mode starts, or whenever there is an error thrown by any lua function.
When breakpoints are set, Lua execution is significantly slower.
It may amuse you to learn that the most common cause of poor performance in Wild Pockets games is too many print-statements. The debug console is not designed to handle thousands of prints-per-second, and programmers often fail to realize just how many megabytes of debugging data they're pouring into that hapless little debug console.
Many inexperienced developers, seeing a performance problem, will immediately blame the renderer. But in reality, any subsystem can bog down: the renderer, the collision detection, the animation code, the lua execution. Even trivial subsystems like the debug console or the GUI renderer can bog down if you push them to do what they weren't designed to do. The renderer is no more likely to be the problem than any of the others. Blaming the wrong subsystem can waste a tremendous amount of time "optimizing" the subsystem that's already blazing-fast, while ignoring the subsystem that's running slow.
If the problem subsystem does in fact turn out to be the renderer, the cause needs to be narrowed further. Often, people will try reducing polygon counts, and will be suprised to find this has little effect. The reason for this is that modern video cards can handle an awful lot of polygons without breaking a sweat. But those same video cards may choke if you give them too many textures - combining textures may be far more effective than reducing polygons. Textures, polygons, models, meshes, state changes - too many of any of these will cause problems.
Long story short, it is absolutely essential to isolate the cause of a problem before attempting repairs.
For this, we provide the profiling and performance monitoring tools.
Most of the profiling tools can be accessed by popping up the debugger (see the previous section), and then clicking on the Profiling menu.
The menu items labeled "report" will cause a report to be printed into the debugging console. The same report also appears inside the program's debug output, which can be accessed by right-clicking on the Wild Pockets icon in the Windows notification area (usually, the lower-right corner of your screen). The debug output is often more readable than the debug console, since it uses a fixed-width font.
One of the main reasons games can slow down is that they're using more memory than your computer has. To determine if this is the case, use the operating system task manager. Look for "WildPocketsEngine.exe" in the report, and see how much memory it is using. If the number seems unreasonable, it is time to look at the memory usage report. Bring up the debugger, and select Profiling/Memory Usage Report. It will print a report into the debug output.
The first line in the report is "Total of Tracked Memory." Every time the Wild Pockets engine calls "malloc", it increments a total, and every time it calls "free," it decrements the total. This is the "tracked memory."
Microsoft Windows stores memory in "heaps." There are two big heaps - the malloc heap, and the general heap. Microsoft windows provides a means to query how much data is being stored in the malloc and general heaps. The tracked memory is allocated with "malloc," so it is included in the malloc heap. The total reported by the malloc heap is usually slightly larger than the total of tracked memory. The general heap is usually pretty small.
You will notice that the two heaps (malloc + general) are usually not as large as the number you see in the task manager. This is because drivers (especially the OpenGL driver) usually do not use the malloc or general heaps. So the memory they use is not reported anywhere but in the task manager. If you see a big discrepancy between the heap sizes and the task manager size, then there's probably a lot of memory being used by the OpenGL driver - usually, to store textures.
The remaining lines of the memory usage report break down what the tracked memory is being used for.
The big memory risks are: lua data structures, textures, 3D models, and driver-related memory which is again usually textures.
The timers report shows how much execution time is being taken by each subsystem of Wild Pockets. The counters report is simply a list of counts of various things that might affect execution time - number of polygons, number of models, and so forth. The two are combined into the timers and counters report.
Wild Pockets is a multithreaded engine: the renderer renders while the engine simulates. Unfortunately, multithreading can make it hard to get a reliable performance measurement. When an engine is multithreading, the operating system may decide to simply stop executing one thread for a while. This delay shows up in the performance report, but it's not the fault of the module that was executing - it only reflects the operating system's decision to split its effort between the various threads. Because of this, it is best to turn off multithreading while using the timers and counters report. To do so, select Profiling/Turn Profiling mode On.
After you turn on profiling mode, you need to exit the debugger and let the program run for a few seconds in profiling (non-multithreaded) mode. Then, you can reenter the debugger and get a report.
To obtain a report, click Profiling/Print Timers and Counters. This prints both the timers report and the counters report to the debug console and the debug output. It is much better to use the debug output, since the debug output uses a fixed-width font and the columns line up properly.
The timers report breaks the engine into several major categories:
Physics. Physics includes all the calculations done by the physics engine. Collision detection is the amount of time needed to determine what is touching what. Once this is known, the analysis phase determines what collision handlers need to be called. The collision analysis and handlers category includes both the time for the analysis and the time needed to execute the lua collision handlers. The world update step is time spend adding up forces and calculating inertial movement. Normally, the physics engine should disable objects that aren't moving - this will reduce the time taken for world update (but not collision detection). Failure to disable can cause slow world-update.
Render. Render is a big category including all parts of both the GUI and 3D renderers. Many lines in this category are trivial tasks that should always take close to zero time. Those that can be more expensive include the following. Clear buffers is the amount of time it takes to clear the screen before rendering each frame. Culling is the amount of time needed to decide what to render and what not to. GUI rendering can be expensive, especially if you make the mistake of tiling a tiny image. Render objects is a misleading name, it only queues objects for rendering, so it is almost always close to zero. Send Vertex Arrays is the actual time needed to send the objects to the video card for rendering. glFinish is the most interesting thing here: this measures the amount of time spent by the renderer waiting for the GPU to finish its job, in the event that the GPU couldn't render as fast as the CPU was asking things to be rendered.
GuiManager. If your game has a substantially complex GUI with buttons, menus, and so forth, it may take significant CPU time to process the GUI every frame.
Timer. Most lua code is triggered, in one way or another, by the timer. This is true if the lua code was scheduled with Timer.later, Timer.every, or Timer.sleep. This category includes time taken to execute such lua code. If this number is large, it generally means your Lua code is taking a lot of time to execute.
The counters report shows many things that can affect the performance of the engine:
Apply State. The renderer, before rendering a model, must set the current color to the model's color, it must set the current texture to the model's texture, and so forth. But if it renders two models in a row with the same texture, it doesn't have to set the current texture prior to rendering the second model - the texture is already set. State changes are expensive - especially texture changes. Try to keep the number of texture changes under 100, and the number of other state changes under 300.
Models sorted by State / Models sorted by Depth. The engine tries hard to minimize the number of state changes. It does so by sorting the models into groups with the same texture and color (sorting by state). However, when models are transparent, they cannot be sorted by state, instead, they must be sorted back-to-front. This can lead to much more state changing. If you see lots of models sorted by depth, it means you have lots of transparency. If this is unintentional, fix it, you will get a performance boost.
Buffer Update Counter. When a model is animated, the vertex positions must be recalculated every frame. This is called a "buffer update." Animation can be expensive. This time will show up in the timers report under "Vertex Arrays: Store in Main Memory."
Model Counter. The total number of models in the scene. This has a signficant impact on renderer performance, even if the models are low-poly, because there is per-model overhead. Try to keep the number of models under 100.
Model Mesh Counter. The total number of meshes in the scene. A model with three textures contains three meshes. So the meshes can far exceed the number of models. This is the single biggest determinant of engine performance: mesh setup time is significant. Try to keep the number of meshes under 300.
Vertex Counter. People often talk about 'polygons', but in fact, they are speaking of number of unique vertices. The vertex counter tells you this. The index counter tells you how many times each vertex was used, which is less important. Try to keep the number of vertices under 50,000.
Timer Tasks. The total number of tasks scheduled through the timer module. Usually this is quite small and not a problem, but bugs in your code can cause this number to skyrocket - if you see a big number here, you may have a bug.
When you build a website, and you link to an image on somebody else's web server, they could potentially break your website by removing the image from their web server. Likewise, when you build a Wild Pockets game, and you refer to an asset in somebody else's folder, they could potentially break your game by removing the asset. Version locking is a mechanism to make sure this doesn't occur.
Removing an asset isn't the only way to break a game. Simply replacing an asset with a more recent version could break a game, if the new version isn't compatible with the old one. You don't even have to link to somebody else's folder to put yourself at risk. Even referencing your own assets can cause problems, if you intend to continue tweaking and improving those assets.
Version locking is designed to solve problems associated with asset and script updates unexpectedly causing a game to break.
When you upload a file to the file server for the first time, the file server assigns the file a version number of 0. If you upload a new version, that version has a version number of 1. If you then delete the file, the version sequence continues: version 2 is an null file. If you recreate the file, that is now version 3. The version sequence never clears and never resets, the server saves all versions, indefinitely.
The key idea behind version locking is that when your game is in the gallery, it does not necessarily use the latest version of every asset. Instead, it uses the last-known-good version. To make this possible, the scene file contains a table that lists, for every asset, the last known good version. This is called the version lock table.
Having a table of last-known-good versions makes it possible to run the game in a safe mode where it always uses known good asset versions.
Open the builder, then save a new game in your server folder. This is essential, you must save the game in your folder - it cannot build a version lock table unless you have write-access to the game. Your game initially has a blank version lock table, which you can see by opening the version lock editor. Do so by clicking in the File menu of the development environment.
The version lock table is built primarily by testing your game. To do so, click Debug/Begin Test. Each time you test your game, the development environment attempts to update the entries in the version lock table, and add new entries. The update process works as follows:
Because the development/testing environment is constantly updating the entries in the table to the latest version, you pretty much always see the latest version of every asset during the testing process. The only exception is when an entry in the table has the freeze checkbox next to it. This prevents the entry from getting updated. If nothing in the table is frozen (as is commonly the case), then testing works as if there were no version locking at all, always showing the latest version of everything.
When you publish your game to the gallery, or send it to a friend, the game runs in a different mode: production mode. Production mode differs from testing mode in that it does not attempt to update the table. Instead, it uses whatever versions are already recorded in the table.
The net effect is this: when you run the game in test mode, it uses the latest version of everything. When you run the game in production mode, it uses the same versions you last used during testing. Or to put it differently, the production game will always look exactly as it did during testing - even if the assets are being altered or deleted by their authors.
Suppose that you use a friend's dragon model in your game, and it looks great. But one day, you're testing your game and you see that your friend has recolored the dragon, and you like the old colors better. The system doesn't know that this update is undesired. You must tell it, by manually tweaking the version lock table.
Go into the version lock table, and find the entry for the dragon. Click the "freeze" button to prevent the system from updating the dragon to the latest version. Then, dial back the version number by clicking on it and selecting a different version. After dialing back the version, test the game again - if the dragon looks right, you're done. If not, you may need to play with the version number some more until you find the version that was good.
Sometimes you really do want your game to use the latest version no matter what. For example, suppose that you intend to deliberately publish new textures every week, just to keep your game looking fresh. In that case, version locking is going to get in your way - it's going to keep the textures locked down to how they looked the last time you tested the game.
To disable version locking for an asset, go into the version lock editor and find the asset you intend to update frequently. Click the disable-icon next to the asset. This will grey out the line in the version lock editor. The result will be that the game will use the latest version no matter what, even if the latest version is broken.
The file server assigns a version number to everything. When an asset is stored in your local folder, it doesn't have a version number yet. If a file has no version number, it is impossible to put the correct version number into the version lock table.
However, a file which is in your local folder will probably get uploaded eventually. At that time, it will be assigned a version number. The version locking system knows this and waits for that moment. As soon as the file is uploaded (and hence, assigned a version number), the version lock system inserts the correct version number into the table.
This process can fail under an extremely obscure set of circumstances. The sync tool leaves behind records showing what was uploaded, and what version number was assigned. These records are used by the version lock system to update the version lock table. If those records are deleted or overwritten before the version lock table can be updated, the version lock table can be left not knowing what the correct version number should be. It is very hard to get into this state. If you manage it, the solution is to test the game a second time. Testing the game a second time will clear up the problem.
Of course, during normal development you tend to test your game over and over, so this is not often an issue.
The file server saves old versions of every file. This is true for scene files as well. That has interesting consequences.
Suppose you're in the development environment, and you click File/Save As, and type in a filename: "mygame.scene". You have now created a file on the file server: mygame.scene version 0. If you then click File/Save, you have written another file to the file server: mygame.scene version 1. If you click File/Save again, you have written yet another file to the file server, mygame.scene version 2. The file server now contains three versions of your game.
Suppose that mygame.scene version 2 contains this version lock table:
12, dragon.jpg
27, lizard.jpg
And, suppose you continue development of your game - you make a lot of improvements to dragon.jpg, yielding dragon.jpg version 18. You test your game, and sure enough, the new dragon texture looks much better. So you save your game again. You have now created mygame.scene version 3, which contains this version lock table:
18, dragon.jpg
27, lizard.jpg
But remember, the file server saves everything, so mygame.scene version 2 is still being stored on the file server, and it still has this version lock table in it:
12, dragon.jpg
27, lizard.jpg
If you launch mygame.scene version 2, it will use version 12 of dragon.jpg. If you launch mygame.scene version 3, it will use version 18 of dragon.jpg.
The point is this: the version lock table captures a snapshot in time in the development of your game. Each time you save your scene, you save another snapshot. Those snapshots are all stored, in the version history of your scene file. You can launch your game not just in its current incarnation, but in any past incarnation.
Currently, 3D models and animations for Wild Pockets can be created using either Autodesk 3D Studio Max, or Autodesk Maya. In the future, we intend to support a large range of modeling programs: XSI, Lightwave, Sketchup. However, at this stage, only these two are supported.
This chapter is about how to model in 3D Studio Max for Wild Pockets with the best possible results. This tutorial will explain how to install the exporter, model efficiently for Wild Pockets, create collision volumes, and other important topics.
There are two files that must be on your computer to make the exporter work properly. One is a Max plugin with a DLO extension, the other is the executable file CURL.EXE. You must choose the right plugin for your version of Max. The plugin DLO contains the Max version number in its filename. For example, the right plugin for Max 2008 is the DLO with 2008 in its filename. If you're using a 64-bit version of Max, make sure to download an x64 version of the DLO. The DLO file must be dropped into the Max plugins directory (which is normally located at 'Program Files/Autodesk/3D Studio Max/plugins'). The program CURL.EXE should be dropped into your windows directory.
Most Max export plugins show up in the File/Export menu. The Wild Pockets exporter is an exception! The Wild Pockets exporter is a helper object. You must add the export helper to your scene. Go to the creation tab in Max, and select the helpers category. In the drop-down menu, a new item should show up entitled Exporters. Select this and then left-click the WildPockets button. With this highlighted, left-click anywhere in your scene to create the export helper, which should look like a green wireframe. This is the WildPockets exporter; it does not change size, and its location does not matter, so once you place the object, you can ignore it until you are ready to actually export your meshes. You should only put one of them in your scene.
Once you place the exporter in your scene, you can continue to work with it by selecting it and going to the Modify Tab. The exporter can subdivide your Max file into multiple art assets. For example, if you model a scene with a tree and a house, the exporter can export the tree into one file, and the house into a different file. You must configure which meshes go into which files. To make this possible, the export helper modify panel contains these buttons:
In addition, the export helper modify panel contains a big "Export Now" button. When you push this, the exporter will create all the art asset files in the list. These files are created directly inside your Wild Pockets library, not on your hard drive. After adding your meshes to the exporter, they will be listed within the exporter according to their filename and the type. If you do not want to be asked whether or not writing over a file is okay, check the 'Overwrite Existing File' box.
To tell it to create an art asset, click the 'add' button in the export helper modify panel. This brings up the 'add' dialog. The add dialog contains many questions. Most of these questions can be ignored at first, but some have to be answered. Here are the questions you must answer:
In addition, you may be interested in these additional questions, but normally, you can just leave them at their default values:
Now that you have made the exporter, you need to make sure you models are ready. When it comes to modeling resolution, there are no true limits, but it is suggested that none of your models should be over 3,000 triangles. This will help the game run smoothly. It is important to get the scale right! One Max unit translates to one Wild Pockets meter. A Human should be modeled about two meters high: ie, two Max units. This is going to look a little funny, because the default Max grid is too big for this to look right. Adjust the Max grid if it helps you to remember. Next, placing the object correctly on the coordinate grid is very important. In general, the object should always be neatly on top of the coordinate origin. If it's an object that has a "front", the front should be facing toward the +Y axis. For example, if it's a character, the character should face the +Y axis.
Usually, the physics system does a reasonable job of selecting a center-of-mass for your object. However, if you don't like its choice, you can take matters into your own hands. To control the center of mass, put a point helper in your scene. Position this point helper where you want the center of mass to be. You must check the following boxes in the point helper:
If you do not check this exact combination, the point helper will be ignored.
The Wild Pockets exporter interprets Atmospheric Apparatuses as collision volumes. Atmospheric Apparatuses can be found under the Helpers category where the exporter was originally. Here, you should see two important types: BoxGizmo and SphereGizmo. DO NOT USE CylGizmo! It does not do anything. Place these collision objects so that they "cover" the overall shape of your model. It is fine for them to overlap. You should use at most four or five volumes per object. Of course, it will usually not be possible to exactly replicate the shape of the model using a few volumes. Just approximate as closely as you can. Note, spherical collision volumes are limited: they must stay perfect spheres! Turning them into hemispheres by setting the hemisphere flag doesn't work. Also, turning them into ellipses by scaling them nonuniformly doesn't work. Only true spheres can be exported. You can rotate and scale collision boxes as needed. You should name your collision volumes appropriately so you do not get them confused with other objects’ collision volumes that may exist in your scene.
Wild Pockets allows two different kinds of materials: Standard Materials, and Multi/Sub-Object materials. Nothing else is understood, the exporter will complain if you use anything else. The material editor options are ignored except the filenames of the Diffuse Map, the Glossiness Map, and the Normal Map. The diffuse map can be JPG or PNG. The glossiness map must be a black/white JPG. The normal map must be a colored JPG - it will look bluish if it's a proper normal map. If you supply both a normal map and a glossiness map, they must be the same size. Texture sizes must be powers of two: ie, 4, 8, 16, 32, 64, 128, 256, 512, or 1024. They don't have to be square: for example, you can use a texture that's 512x64.
When you hit the Export Now button, the exporter will attempt to upload your models to the Wild Pockets file server. At this time, you will see several black windows with numbers and words flashing by rapidly. This is supposed to happen. After the export, a dialog will appear telling you whether or not the export was a success.
This chapter explains how to use the Wild Pockets Exporter for Maya. It will explain how to install the exporter, model efficiently for Wild Pockets, create collision volumes, and other important topics.
There are two files that must be on your computer to make the exporter work properly. One is the MEL Script that is the exporter itself, and the other is the executable file CURL.EXE.
The MEL script can be placed anywhere on your system. You can invoke it by dragging it into the Maya window. The program curl.exe should be dropped into your Windows directory (probably C:\Windows).
Now that you have made the exporter, you need to make sure you models are ready. When it comes to modeling resolution, there are no true limits, but it is suggested that none of your models should be over 3,000 triangles. This will help the game run smoothly.
It is important to get the scale right! One Maya unit translates to one Wild Pockets meter. A Human should be modeled about two meters high: ie, two Maya units. Adjust the Maya grid if it helps you to remember.
Next, placing the object correctly on the coordinate grid is very important. In general, the object should always be neatly on top of the coordinate origin. If it's an object that has a "front", the front should be facing toward the -Z axis. For example, if it's a character, the character should face the -Z axis.
The exporter window contains a big "EXPORT SCENE" button. When you push this, the exporter will ask you to log in (if you are not already logged in) and then ask you for a name for the file. It will then create the file directly inside your Wild Pockets library, not on your hard drive. After the export, a dialog will appear telling you whether or not the export was a success. During the export, it may appear as if Maya has stopped responding. Don't worry, it more than likely hasn't. Be patient, and your export will finish.
Usually, the physics system does a reasonable job of selecting a center-of-mass for your object. However, if you don't like its choice, you can take matters into your own hands.
To control the center of mass, click the "Create Center of Mass" button. This will create a locator. Position this locator where you want the center of mass to be.
If you want further control, you may check the "Use Center of Mass axes for inertia calculations" box. This will enable you to use the axes of the locator to describe the object's moment of inertia. For further information, see the Help button in the top-right corner of the Exporter.
The Wild Pockets exporter allows you two add two kinds of collision volumes: boxes and spheres. To add these to your scene, press their corresponding buttons on the Exporter window. Note that regardless of how you organize your Hypergraph, only collision volumes that you create using these buttons will be used as collision. Anything else will be rendered.
Place these collision objects so that they "cover" the overall shape of your model. It is fine for them to overlap. You should use at most four or five volumes per object. Of course, it will usually not be possible to exactly replicate the shape of the model using a few volumes. Just approximate as closely as you can.
Note, spherical collision volumes are limited: they must stay perfect spheres! Turning them into ellipses by scaling them nonuniformly doesn't work. Only true spheres can be exported. You can rotate and scale collision boxes as needed. For further information, see the Help button in the top-right corner of the Exporter.
Wild Pockets supports three kinds of textures: diffuse maps, gloss maps, and norrmal maps. They should be attached to lamberts if you're not using a gloss map or phongs if you are. The diffuse map can be jpg or png, and should be attached to the lambert or phong's color. The gloss map must be a black/white jpg, and should be attached to the phong's specularColor. The normal map must be a colored jpg (it will look bluish if it's a proper normal map), and should be attached to the lambert or phong's normalCamera. If you supply both a normal map and a glossiness map, they must be the same size.
Texture sizes must be powers of two: ie, 4, 8, 16, 32, 64, 128, 256, 512, or 1024. They don't have to be square: for example, you can use a texture that's 512x64.
To export an animation, hit the "EXPORT ANIMATION" button, which will act like the "EXPORT SCENE" button, but will upload the animation for the file instead. With the "All Frames in Timeline" button selected, it will export the whole animation as one file. With the "Range of Frames" button selected, you may input a starting frame and an ending frame. The "EXPORT ANIMATION" button will then export only that range of frames, in case you want to store multiple animations in the same file. For further information, see the Help button in the top-right corner of the Exporter.
The Exporter will allow you to set a default script class for your object, so that when it is added to the scene it will automatically be of that class. Before you export, put the name of the script containing the class in the "Script containing class" field and the class name itself in the "Default class name" field. Then, when you export, the Scene Object will be set to this script class by default. Ask your scripter for details, and for further information, see the Help button in the top-right corner of the Exporter.
Keywords will enable you to add search terms to your model or animation, so that it can be more easily found in the library. For whichever you are exporting, click on the corresponding "Add/Edit Keywords" button to bring up the keywords for that model or animation. To add a keyword, type it into the box and click "Add Keyword." To remove a keyword, select it and click "Remove Selected Keyword." When you have created your keyword list, click "OK." When you export your art, it will be exported with these keywords, which you can then use to search the Wild Pockets library for them. As always, for further information, see the Help button in the top-right corner of the Exporter.
The purpose of the payment system is to allow you to make money selling games and in-game content.
To use the payment system, players must sign up for an account with Wild Pockets. The player uses his credit card (or another payment method) to purchase "coins." The player then enters your game. Your game must initially run in a feature-limited mode. You can limit access to any feature you like - for example, you could deny access to a dungeon, or to a cool costume, or to god-mode. If you want to sell your game as a whole, you could deny access to everything but the title screen. Your game must use the payment system API to offer the player a chance to unlock some restricted feature, for a price. If the player agrees, the purchase is recorded, and coins are transferred from the player's account to your developer account, from which they can be cashed out. Your game can then use the payment system API to detect that the user has paid, and it can unlock the desired feature.
When you wall off a feature of your game as a paid feature, you must give that feature a name, which must be a string. For example, if your game contained three hats which must be individually paid for, you might give each hat a name: "Large Fedora", "Cute Beret", and "Wizard Hat."
Your customers will see these strings! They show up when the customer views his bill. Your customer might complain if he sees a charge that he doesn't understand, so be sure the ID string is self-explanatory.
These feature ID strings are used throughout the payment system API. They appear in the price-list file which you will upload to the server. They appear when you visit your statistics web-page that displays your revenues. They also appear in the Lua commands that verify whether or not the item has been paid for.
Purchase records are stored on the Wild Pockets servers. The records look much like this:
Item Paid Until
--------------------- --------------------
Wizard Hat Sep 9, 2010
Cute Beret Jan 1, 9999
The payment API is essentially a means to create and query these payment records.
The paid-until date is used to implement subscription services. It shows the date that your subscription expires and must be renewed by making another payment. Some purchases never expire - ie, the customer has purchased permanent access to the content. In that case, the paid-until date will be set to Jan 1, 9999.
You will need to upload a price list to our servers. This is an XML file that you upload as “yourusername/pricelist”. It must be named pricelist. Each user account has a single pricelist for everything that they are selling. Upload this XML document the same way you would a texture or sound, through the builder's upload file menu. The file must consist of a 'pricelist' block containing multiple 'item' blocks, like this:
<pricelist>
<item key="yourusername/Wizard Hat" type="subscription" >
<offer price="200" days="5" />
<offer price="300" days="10" />
</item>
<item key="yourusername/Cute Beret" type="onetime" >
<offer price="300" />
</item>
<item key="yourusername/Large Fedora" type="subscription" >
<offer price="200" months="1" />
</item>
</pricelist>
Each item is priced in "coins." Coins are nominally worth 1/1000 American Dollar. However, there are certain factors that adjust the value of a coin, which will be explained later.
You can sell items on a one-time basis (your payment buys permanent access to the content), or on a subscription basis (your payment buys temporary access to the content). In the case of a subscription purchase, the pricelist file must specify how many days of access you get for your payment.
If today were midnight on Jan 1, 2000, and you were to pay for the Wizard Hat above for 5 days, the purchase would expire at midnight on Jan 6, 2000 (exactly 5 days from today). If you were to then pay again, the expiration date would be moved to Jan 11, 2000 (exactly 5 days after that). Each payment advances the expiration date by the amount specified in the pricelist file.
Instead of 'days', you may specify 'months'. In that case, the expiration is always the same day of the month - regardless of how many days there are in a month. If today were Jan 18, 2000, and you were to pay 200 coins for the Large Fedora (in the pricelist above), the expiration would be Feb 18, 2000. If you were to pay another 200 coins, the expiration would be advanced to Mar 18, 2000. The exception is if there is no such day in the month ahead. If you were to buy the Fedora on Jan 31, the expiration would be Feb 28. The next expiration after that would be Mar 28, and so forth.
The player cannot make any purchases unless he is logged into his Wild Pockets account. To put it differently, the system doesn't know if the user has purchased anything unless the user identifies himself. You can determine whether or not the user is logged in by using the function Player.getUsername. If it returns nil, the user is still anonymous.
If you try to use payment system functions when the user is not logged in, the payment system will automatically pop up the login dialog. To the player, this unexpected login prompt can seem "out of the blue." It is very disconcerting. The spontaneous popup is a useful development tool, but in a polished game, you should hand-hold the user. Explain to him why he should log in ("so you can store your high scores and achievements, and access cool features!"), then encourage him to click a login button. When he clicks the button, call Player.requestLogin. This will pop up the exact same login dialog, but since the user specifically asked for it, it not disconcerting. Your payment system code should always verify that the user is logged in. If not, the payment system code should not be used, and the login encouragement should be displayed instead.
When you use Player.requestLogin to pop up the login dialog, you must supply a callback function. When the user clicks the "ok" or "cancel" button on the login dialog, the callback will be invoked. The callback takes parameters that allow you to determine what happened in the login dialog. See the API reference for Player.requestLogin for more information.
The login status is stored in web-browser cookies. Therefore, being logged in is browser-wide state. If the user is running two Wild Pockets games at the same time in the same browser, they are either both logged in to the same account, or both logged out. The Wild Pockets website uses the same cookies. If the user visits the Wild Pockets website, and logs in via the website, he is logged in within the game as well.
Because of this, the user's login status can change "spontaneously." It's not actually spontaneous: the user is actively using another browser window. But as far as the game can tell, the return value of Player.getUsername changes without warning. Games need to take this into account.
SAY SOMETHING ABOUT PAYMENT SYSTEM SINGLE-ACCOUNT RULE.
Once the user is logged in, you can check to see whether or not he has paid for a particular item. To determine whether or not he has paid for the Wizard Hat, use code like this:
Player.hasPurchased("yourusername/Wizard Hat", whCallback)
...
function whCallback(paid, errormessage)
if paid then
unlock wizard hat
else
if errormessage then
display the error message
end
end
end
First, you call Player.hasPurchased, passing in the Feature ID string and a callback function. Because it takes time to query our servers, it may take as much as a second for the engine to retrieve the necessary information. When the information is available, the callback will be invoked. The callback takes two parameters - a paid-flag, and an error message string. The error message is usually nil - if not, the most likely error is that the internet might be down. Most errors will start with the following strings:
"LoginError" : The player has refused or can not log in
"PriceListError" : The pricelist file has an error or does not match
"InvalidInputError" : One of the passed parameters was invalid
"UnknownError" : There was a problem communicating with the server, possibly the nework was down
If there is no error, then the paid-flag might be true, in which case the Wizard Hat has been paid for.
Avoid making the following mistake: if you call 'hasPurchased' every frame, then each time you call it, you will schedule a callback to be invoked. It is possible to schedule literally hundreds of callbacks in a very short time. You probably don't want this - you probably just want to call the function once.
If the user hasn't paid for a given item, you can offer him the opportunity to buy it. Use the function Player.offerOneTimePurchase. This functions take the same parameters as Player.hasPurchased, and the callback is identical. The difference is that these offer-functions will pop up dialogs that offer the player a chance to buy the item, whereas Player.hasPurchased doesn't make any offers. To offer a subscription purchase use the function Player.offerSubscriptionPurchase. In addition to the first argument which is the item name, this function takes two more arguments , days and months. These arguments determine which offer from the pricelist the player is given. If you specify a number of days that is not in the pricelist, an error will be returned. You should specify days or months, leaving the one that you're not using nil. For example the function below will offer the player the chance to purchase a 5 day subscription for the amount specified in the pricelist file.
Player.offerSubscriptionPurchase("yourusername/Wizard Hat",5,nil,callbackFn)
Once again, it is disconcerting to pop up a dialog unless the player is expecting it. You shouldn't call either Player.offerOneTimePurchase unless you first present the user with a message and a button (ie, "click here to buy a wizard hat for your character"). Once the user clicks a purchase-button, he is going to be expecting the purchase-popup.
Game content is sold in 'coins.' Coins are nominally worth 1/1000 American Dollar. Currently, there is no minimum amount that you can charge. In time, we may implement a minimum, but it is going to remain very low - the minimum is not likely to exceed one penny. The overhead for these tiny sales is low - there is no problem selling things cheaply.
When you have charged for something in a game, a new page will appear on the Wild Pockets website: the "developer account" page. From this page, you can view your sales and your revenues. Periodically, Wild Pockets will write a check and mail it to you. This "cash-out" will be shown on your developer account page. To make cash-out possible, you will need to talk to the Wild Pockets staff and notify them of your mailing address.
When you charge for something, you charge the player in "coins." Nominally, a coin is worth 1/1000 of an American Dollar. However, because of expenses and incentives, you will not receive the full amount for each coin that you charge. There are two primary kinds of deductions:
All of this is spelled out in detail in the Wild Pockets terms of service.
It is worth it to spend a moment on practices that can avoid conflict and misunderstandings with the customer. Mainly, conflicts arise because of confusion - particularly, when the player buys one thing, but thinks he's buying something else. It's best to take great care to avoid this situation. It can be very frustrating to work hard, deliver a good game, and still have customers accuse you of trying to cheat them.
There are a lot of potentially bad situations. One is that the player might think he's buying an RPG, when in fact, the game is a platformer. Another is that the customer might find something on his bill that he doesn't remember buying. Another risk is that the player might think he's buying all the hats, when in fact, he's only buying one. It's worth it to go over your purchase-messages and item descriptions with a fine-tooth comb to avoid these situations. Here are some guidelines that might help:
Wild Pockets games can store data in a central database on the Wild Pockets servers. The database is a key-value storage system that allows games to record data and retrieve it later. The database can be used for numerous purposes:
There are many other potential uses. It is important to remember that quantity must be kept reasonably limited - it is OK to store a few kilobytes per developer per user, but not more than that. The system may in time use a quota system to enforce this limitation.
Each player has his own space in the database. Whenever a player stores a record, it goes into that player's space. Players cannot overwrite or alter each other's spaces. Because of this, a player must be logged in to store anything in the database. In effect, my password protects access to my own private space in the database.
Most queries only touch my own personal space. However, a few queries may span player boundaries. For example, the queries to fetch a high-score table may scan all the players.
Because the data is segregated by player ID, the Lua class for accessing the database is called class 'Player'.
Data records always contain these fields:
Key Fields:
Player - name of the player who was playing.
Developer - name of the developer who wrote the game.
Game - name of the game that stored the data.
Key - a string of the game's choosing.
Value Fields:
Date - date that this record was stored.
Private - a flag, determines whether other players can see this record.
Value - either a number or a string, of the game's choosing.
The Lua functions that create a data record are as follows:
Player.putPlayerStringData(game,private,key,value,callback)
Player.putPlayerNumberData(game,private,key,value,callback)
The Key and Value parameters are what this is all about. The Key parameter is a string that lets you classify what kind of data it is. Key might be "score", or "lives remaining", or the like. You can choose any string you desire. The Value parameter is the data that is being stored - again, you can store anything you desire. The Value parameter must be a Lua string if you are using putPlayerStringData, or a Lua number if you are using putPlayerNumberData.
The Game parameter allows two pockets to share data. For example, you might have two pockets: "HackDungeon" and "HackDungeon Character Editor." They might both store data records in the central database under the game-name "HackDungeon" so that they can share data. Only pockets created by the same developer can share data in this way. You can pass in nil for game, in which case it will use the pocket's file name as the game name.
The Private parameter, if true, prevents other players from reading the current player's data. Use this most of the time: players expect privacy most of the time. Remember, they may be more sensitive than you are. For example, some players won't want anyone to know that they're playing a female character in HackDungeon. The exception is if you deliberately want to broadcast data to the player base as a whole. For example, if you want to implement a high-score table, the score-record would need to be marked public. Other than that, you should mark everything private.
The callback parameter is a function that takes a single parameter 'errormessage'. If the storage is successful, the callback will be invoked with a nil error message. If the storage fails (which it might, if the internet is temporarily down), then the callback will be passed an error message string. Remember, these functions require communication over the internet, which can be slow, so it may take some time before the callback gets invoked.
As you can see, the player name is not specified in the call. It implicitly records the userID of the currently logged-in player. The functions won't work at all if the player is not logged in. See the documentation of the Payment system for information about how to get the player logged in.
The developer name and date are not specified in the Lua methods - these are stored automatically.
When a record is stored, the system first checks to see if a previous record exists with the same Player, Developer, Game, and Key. If so, the old record is replaced. Otherwise, a new record is created.
The game name and key must be strings of printable ascii characters. That includes letters, numbers, punctuation marks, and space. It does not include tabs, newlines, carriage returns, or any other control character. It also does not include unicode characters.
The value field is 8-bit clean. Any Lua string can be stored in the value field, it will be retrieved uncorrupted.
After storing a data record, you may retrieve it again using these functions:
Player.getPlayerStringData(game,user,key,callback)
Player.getPlayerNumberData(game,user,key,callback)
As you can see, this differs from storage in that you can specify the name of another user to retrieve data from. If you do not specify a user (ie, pass in nil), it will use the currently logged-in user. It is possible to read another user's data - it's just not possible to write to another user's data. You can only retrieve data from another user if it was stored without the private flag. Most data should be stored private, only data which is intended to be displayed on web-pages should be marked public.
These functions don't return the data. This is important: it takes time to retrieve data over the internet. These functions cannot return the data, they don't have it yet. All they can do is send off the query. When the reply finally comes back, the callback gets invoked. The callback for these functions takes two parameters: the retrieved data, and an errormessage. If the errormessage is nil, it means the retrieval was successful.
The following function is intended to make it feasible to display high score tables:
Player.getTopPlayerNumberData(game,key,number,callback)
For example, if you were to call getTopPlayerNumberData(nil,"score",10,callback), it would scan all players to find all record marked "score" and marked with the current game. It would return the 10 largest, in the form of a table. See the API reference for more information.
Data which is marked private will be ignored by this function. It will return the top N public records.
Additional queries are likely to be coming soon - including ones that allow you to retrieve the most recent piece of information stored with a given key.
We are implementing an achievement system that will allow games to more easily associate achievements with in-game events, and post those achievements for public viewing.
Because Wild Pockets games run on the customer's computer, they can be hacked. It is possible for a player with sufficient hacking tools to store anything he desires into the database. Remember, though, that data is stored on a per-player basis. A player has to log in to alter his space in the database. Therefore, a hacker can only corrupt his own portion of the database. He cannot corrupt the database as a whole.
Still, having a user with fake data could cause problems. For example, a high-score list can be generated by scanning the database for the 10 highest scores. That might include the hacker's score, which of course, might have been faked. To solve this problem, it may at times be necessary to detect that a player has hacked his own portion of the database, and ban that player. Once his portion of the database has been stripped out, the system should return to normal.
We are planning a 'ban' mechanism that will make it possible to strip hackers out of the database. However, banning is not implemented yet.
When you perform multiple put-operations at the same time, they could get performed in any order. The system makes no guarantee that they will be performed in the order that you "expect" them to.
For example, let's say you execute this code:
Player.putPlayerStringData(nil, true, "weapon", "sword", callback)
Player.putPlayerStringData(nil, true, "armor", "chainmail", callback)
In this example, the armor might get recorded to the database before the weapon. If this is a problem for your game, you can use the callbacks to guarantee that one write is finished before starting the next.
Things get even more complicated when you remember that it is possible for the player to run two copies of your game at the same time (usually, by opening two browser windows). This can make ordering issues even more complicated. For example, suppose the second copy of the game tries to execute this code:
Player.putPlayerStringData(nil, true, "weapon", "arrow", callback)
Player.putPlayerStringData(nil, true, "armor", "leather", callback)
The four puts could occur in any order, you could even end up with a mix in the end (ie, database says weapon:arrow and armor:chainmail).
The easiest way to avoid ordering issues is that in cases where you need atomicity, put everything into a single key, like this:
Player.putPlayerStringData(nil, true, "equipment", "weapon:sword armor:chainmail", callback)
Wild Pockets is being continually upgraded and improved. Most of the time, adding features and fixing bugs makes everyone happier. But sometimes, we wish to make changes that could cause problems for developers.
Here is an example: when Wild Pockets was first conceived, we weren't sure what units of length, force, and velocity to use. By failing to think it through thoroughly, we ended up with some units that didn't correspond to any reasonable standard of measurement. We call this crazy system of units "legacy units." Several games were written using these legacy units. It was an unfortunate situation.
At some point, we realized that we should have used the metric system (Meters, Newtons, Kilograms). Although it was obvious to us that the metric system would be better, it was also clear that switching from legacy units to the metric system would have caused existing games to stop working.
We consider this forbidden: one of our prime directives is "never break a game that works." A developer should be able to write a game, finish it, walk away, and then come back ten years later and the game should still be playable.
But of course, we still need to be able to make improvements to Wild Pockets, and the metric system would have been a big improvement.
To resolve the conflicting goals, we created the "engine version" system.
The first publicly announced engine version was version 1.3, which was soon followed by version 1.4. Since then, we have released additional versions.
The first thing to know about engine versions is: we never throw out a version. If you want to make a game that uses engine version 1.3, you can. (It's not advised, that's a very old and buggy version, but you can do it.)
The second thing to know about engine versions is that every game in Wild Pockets is tied to a particular engine version. If a game was created using engine version 1.4, for instance, then that game will always run using engine version 1.4. Engine versions are "sticky."
When your game is running, or when editing your game in the builder, look in the lower right corner of the Windows taskbar: you will see the "WP" icon. You can click on the WP icon and select "About Wild Pockets." This will show you what engine version you are using.
If you created a game using engine version 1.4, and we release version 1.5, then your game remains tied to engine version 1.4. Although this is advantageous in that it means your game still works as well as it ever did, it's disadvantageous in that you can't take advantage of the features of the new version.
If you want access to the new features, then you can manually switch your game over to the new version, by issuing a command in the builder. Load your game in the builder, and open the debug console. In the debug console, type this command:
Simulation.setEngineVersion("1.5")
Notice that the parameter is a string, not a floating point number.
Save the game using the builder's file menu. Now that you've saved it, your game will use engine version 1.5.
Of course, once you switch engine versions, your game may not work any more. You may need to alter it to be compatible with engine 1.5. If this turns out to be more work than is practical, you can switch back to 1.4, using the same command.
In Wild Pockets, you don't create games from scratch. You always start by opening somebody else's game, editing it, and saving it as your own. When you do this, your game has the same engine version as the game you built upon.
Many Wild Pockets developers don't quite realize that they built their game on "somebody else's game," because they started with what appeared to be a "blank slate." But in fact, when you open the builder, the scene isn't actually completely blank - there are two blue cubes and a cloudy sky. Those cubes were loaded from a saved game file whose filename is ""Wild Pockets Team/default scene". Like every other Wild Pockets game, the default scene has an engine version. When you build a game on top of the default scene, you inherit the default scene's engine version.
When we release a new engine version, we update the engine version of the default scene. That way, if you build on top of the default scene, you're using the latest engine version. But if you build your game on top of anyone else's game, you need to check what engine version they used.
Sometimes, we upgrade an existing version without changing the version number. For example, if we were to discover a bug in the 1.4 engine, we might release a bugfix. The bugfixed engine would still be version 1.4. Because of this, engine versions have a "minor" version number: 1.4.12, 1.4.13, 1.4.14, etc. When selecting an engine version, you cannot specify the minor version number.
We are strict about the following rule: when we upgrade an engine in-place, we are very careful not to break any existing games. We never make API modifications by altering an engine in-place. If we wish to make API or design changes, then those have to wait until we release a new engine version.
When we release a new version, the old versions become deprecated. You can use them anyway! They will never be deleted.
However, when an engine is deprecated, it means we're going to put less effort into maintaining it. If a bug is found in engine version 1.4, we may fix it, if it's reasonably feasible to do so. But we may not. We rarely add features to old engine versions. The older the version is, the less likely we are to put effort into it.
Security vulnerabilities are a different story. If we discover that an old engine version allows some sort of exploit, it will be fixed.
We provide a version history page that you can use to see the major changes in each engine version.
Wild Pockets provides developers with an analytics page to see how popular your games are. You can access this page from:
After entering the name of the game that you would like to track you'll be presented with a page where you can graph 1 or 2 statistics at a time. These statistics include:
Each of these different stats can be looked at in terms of Unique views or Total views. Unique simply means that if the same user looks at the page twice it only counts as once. In addition you can specify a statistic to divide by. One example is to view "Game Starts" per "Page Loads" in order to see the percent of people who actually started your game after they loaded the page. Another example is to view "Minutes In Game" per "Game Starts" to see the average amount of time spent in game.
Wild Pockets also allows you to track specific points in your game. At any point in your lua code you can call:
Player.recordCheckpoint("Killed Ogre")
Now on the analytics page you can select Custom and type in "Killed Ogre" to view how many players reached that point in your game. In addition you can specify a number in your recordCheckpoint call. For example if the player spends 32 minutes playing your first level, when he does beat it you can record:
Player.recordCheckpoint("Time Spent In Level 1",32)
Now you can view the total time spent in level one from the analytics page. One nifty thing you could do is to graph "Time Spent In Level 1" per "Game Started" in order to see the average amount of time your players are spending on the first level.