In part one of this tutorial we created a LimpetGE script that worked. Now to convert it into the game. The game consists of making your way to the end of a corridor wihout being "squished" by spheres going the other way. We have created the corridor, now for the spheres.
The sphere structures is a bit simpler than the "Corridor" definition, however, as we do not want all spheres the same, we are creating four sphere structures. Each one will be used to create spheres of their specific color:
There are some differences from the corridor. First of all, rather than create a color "template", we are coloring each sphere a different color.
An array of four colors. However we are not creating that to create a texture collage as in the corridor, but each one to create s specific sphere structure.
The above creates an "array" of structures in the "structs" variable. Looking at:
In the "new LStructureDef(....)" constructor, the arguments are:
Four structure definitions are created here, and the function returns an array consisting of them.
Collision detection processing is performed in the main game engine with resource available to it, unlike graphics rendering which is done on processors in the graphics card. The resource available in the engine for LimpetGE is javascript, and in effect a single thread of it. Therefore collision detection cannot use too many cycles up.
LimpetGE performs collision detection using sparse arrays. The scene is divided up into, say, one meter sized cubes, or in the case of this game, five meter sized cubes, and what can be collided with is placed, and if neccessary tracked, in these. The collision tests take any object, that may have moved, be it in these arrays or not, and performing an algorithm to see if that object has "collided" with any in the arrays.
The first type of object here is "Static" ones. These use the "AABB" method, that is there is a "block" of a width, height and length, that if a moving object wonders into, or near it, it has "collided" with it. These in LimpetGE can be large, but need to be static. Once placed they cannot be moved. The "lScene.lSetup()" routine creates the entries in the sparse array for these. Although they cannot be moved, they can be switched "off" by either making them invisible, or setting the "obj.ignore" property for them to boolean "true", then setting it to "false" to re-enable it. The "Static" collision types are useful for walls, doors, floors, ceilings etc.
Static types are created by defining "collision: LSTATIC" in the "args" argument of the "LStructureDef" class constructor. Then, while creating the components on that, a collision "block" is created that fits around that component. This can be changed by:
The other type, "Dynamic" objects, use the "distance" method to detect collisions. That is if a moving object gets to less than a certain distance to the object's center a collision has occured. This in effect makes that object spherical. The advantage to these is that they can move during the game, or can be created dynamically while the game is played. These are useful for NPCs, bullets, flying things and the like.
These are created by specifying the "collision: LDYNAMIC" in a entry in the "args" argument of the "LStructureDef" constructor. You also specify a "distance: 1.23" there too, where 1.23 can be any number representing the radius around the object in which it is said to have been collided with.
Random numbers are used in the game. There are a couple of LimpetGE classes that deal with these. The "Squish" game uses both:
The "LPRNG" class instance creates a series of random number integers in a given scope, and the "LPRNGD" creates one for doubles. They work by calling the "next(scope)" on the instance. For instance:
The Sphere class is daunting at first, but I will go through it...
.The constructor takes an argument "color", which in this case is an integer between 0 and 3. This will decide which of the sphere structures will be used to create the sphere, which controls the color of it.
A "new LWObject(....)" is created and assigned to the "obj" property. This takes the arguments:
Next the "obj.mkvisible(false)" function makes the object invisible. We want spheres to appear where and when the game demands rather than when it is created. Therefore they are created invisible. When required, the "obj.mkvisible(true)" function call makes it visible again.
The "lScene.lPlace()" places it in the top level of the game hierarchy. As it is not visible yet, we use place it at the origin by using the identity transformation matrix ("mat4.create()")
The velocity of the sphere is stored in the "velocity", and is set when the sphere is to appear.
On success of the level, the player is treated to a show of the spheres flying in all directions. The "endx", "endy", and "endz" properties represent the speed this sphere will go in each dimension when this occurs.
In order for a sphere is to "appear", A "start" method is created:
When "starting" or "instigating" the sphere, it first randomly sets the "velocity" property between 5.0 and 10.0
It then moves the object to the end of the corridor, randomly placing it laterally, so it is not touching the side walls, though a slightly grater chance it is at the edge of the corridor. This is called by the game objects "obj.moveHere(x, y, z)" method. This, on this object, sets it to the scene co-ordinates (x, y, z).
Note - it in fact sets it relative to it's initial position relative to it's parent. For objects placed using the "lScene.lPlace(...)" method this is the origin, so for spheres it is the scene's X, Y, Z coordinates.
After placing it at the top of the corridor, it needs to make sure it is not colliding with anything. Or more exactly, make sure it has not appeared in the same place as another one. To do that it performs a collision test.
It first sets a "collision" variable to "false", then a closure function that sets it to "true".
Then a loop is performes. Which:
The "obj.mkvisible(true)" method makes the thing visible.
The "obj.procpos()" method processes the position in the hierarchy. It needs to be called for nay object after it has been moved, but before it is drawn. It is called by the programmer because that is the best way to optimise that functionality.
Spheres are dynamic and they move. To control this the "move" method is created, and performed on each sphere on each frame:
The "sphere.move(....)" method takes an argument, "delta". This is the value passed to the "lScene.lLoop(...)" method for the frame, which is where this is called from.
The fist thing done is to see if the sphere is visible. The is done by seeing if the "obj.isvisible" property is the boolean "true" (rather than "false" for invisible). if it is not, it quits. Nothing needs to be done
It then adjusts "delta" so it matches that particular sphere's velocity. This means it will move by that in this method.
It then moves the sphere up the Z axis, at the appropriate velocity using the "obj.moveAbs(...)" method which we covered in the "sphere.start()" method.
As these are spheres, they roll, so next we rotate the sphere around the X axis by the appropriate amount. Although you cannot see this for solid colored spheres, you will be able to when textures are in place on the spheres. The rotation is done by the "obj.rotate(x, y, z)" which rotates the object around the "Z" axis, then the "Y" then the "X" axis (in that order) by the appropriate number of radians. Note the rotation is done in the reverse order to which the arguments are supplied.
The sphere has just moved. It is now it's responsibility to see if it has hit anything. The following deals with that...
First the variable "histsphere" is set to null. The crux of what happens next is the "lScene.lCAllPointDetect(....)" method. This runs the Collision Detection algorithms for both the dynamic and static sparse arrays. The argumnts are:
Within the closure function itself, the argument it takes is the object that has been collided with. If it is the camera (the control of the object is an instance of LCamera), then the "lScene.ishit" property is set to true. This implies the camera has been hit, so the player has been squished. The "lScene.lLoop(...)" method will pick that up and detect the player has been "squished".
If on the other hand the object is a sphere (the control of the object hit is an instance of the class "Sphere"), that sphere's game object is stored in the "hitsphere" variable. After the collision test has completed, if a sphere has been hit, the current sphere is "moved back" so they remain separate, and bearing in mind the spheres move in one direction a sphere can only collide with another one from "behind", velocities are compared and if it is going faster than the one it hit, the velocities are swapped, making the one in front faster, and the current one slower.
The method:
If a sphere reaches the "beginning" of the corridor, it has served it's purpose, and "dies". This is handled by the "sphere.die()" routine. All it does is make the sphere invisible by using the "this.obj.mkvisible(false)" method. that is all it needs to do.
To include the spheres, and levels, in the game, additions need to be made. First, I will go through additions to the "g_playgame()" and "g_playlevel" functions. (New code in green bold)
As levels are included in the game, the level number needs to be stored somewhere, and it is stored in the "g_level" global variable. As stated, I like to place these near the top of the file.
In the "g_playgame()" function, I need to create and store thos sphere structures in a similar manner to I processed the corridor structure. This is done by executing the function "sphereStructures()" and store the returning structure definition ("LStructureDef" instance), or in this case the array of structures, in the global "g_structures" object.
The reason why "g_playlevel()" is separate to "g_playgame()" is that the time consuming "lInit()" function, and the creation of structures happen once, but a new "Scene" is created for each level. The first level is called when the game is first run (from the "g_playgame()" function called on the HTML "BODY" tag "onload" attribute, and subsequently from the "lScene.lRestart()" property function explained below.
After the "Scene" has been created, the sphere objects are. When they are created they take an argument, between 0 to 3 inclusive. This is the structure that sphere uses to create the game object, which here determines the color. They are also created invisible, and therefore not included in the collision algorithms. When they are "activated" they are made visible, placing them in the collision array. Then when finished with made invisible again. Although LimpetGE does support "in-game" creation of dynamic game objects, there is no way of performing "in-game" removal. The best you can do is make them invisible. The reason for this is that they are referenced from the hierarchy tree and the rendering list, and it would take a fair bit of resource to dynamically remove them.
For that reason all spheres are created at the start, and "switched on" (made visible) and "switched off" (made invisible) when required, thus enabling thousands of spheres to roll passed the player without cluttering the game object arrays and objects.
The "lScene.lSetTitle(...)" sets the title at the top of the screen the supplied argument. This is used in this game to display what level the player is on.
To include the spheres in the game, additions need to be made the the "Scene" class. First, additions to the "lScene" constructor: (new code in green bold)
Going through the new code:
The "lScene.spheres" property contain an array, that will hold all the spheres objects.
The "lScene.sidx" property holds an index to the above, to decide which is "instigated" next (see below)
The "lScene.ishit" is set to true if the player gets squished. Already covered in the "sphere.move(...)" method.
The "lScene.nextsphere" is the number of seconds (usually less than 1) when the next sphere is instigated.
The "lScene.lRestart" is a property that holds a function (not a method as it is not on the javascript object's constructor/class). It sstores the function that is performed when the game ends by the "lScene.lLoop(...)" method returning boolan "false". The function sets up here does the following:
The "lScene.endtime" is set to 5.0, this is the number of seconds left to display the celebration or commiseration animation when the game ends.
The "this.isend" property is initialised to the boolean "false", and set to "true" when the game ends
These additions are:
Again, focusing on the new code.
The "lScene.isend" is a boolean that is set to "true" if the game ends, successfully for the player or not. If this is set, and the game is still running, it means we are in the five second celebration or commiseration animations at the end. Which one is determined by the "lScene.ishit" property, which is set if the player has been squished.
If "lScene.isend" is set just the animations are done. The function returns there and the rest of the loop is not executed.
New spheres need to appear at the end of the corridor as the game progresses. This snippet deals with that. The "lScene.nextsphere" property contains the time in seconds (usually less than one) until the next sphere is to appear. If it is greater than zero this decrements, if it has reached zero it makes a sphere appear using the "lScene.makesphere()" property, then resets the timer. This is between zero and one second, getting more rapid as the level (stored in the global variable "g_level") increases.
In the "_seecam" "closure" function for collision detection:
This sees if the player has run into a sphere, and if so, set the "lScene.ishit" property to "true". This is picked up in the loop later to indicate the player has been squished.
The spheres need to move. There can be up to two hundred of them (see below). This is done by calling the "sphere.move(...)" method on each sphere in turn.
What to do if the game finished? Celebrate if successful, or die if squished. Both the methods set the "lScene.isend" property to "true".
There are more methods created on the "Scene" class, that are called by the "lLoop" method now.
This handles creation of a new sphere:
This method simply takes the next "sphere" in the array of spheres, indexes by the "lScene.sidx". It then makes that "live" using the "sphere.start()" method, then increases the "sidx" property, resetting it to zero if it gets to 200.
This means that if a sphere has a slow velocity, then it may be "restarted" before it reaches the end of the corridor. If that happens the sphere will simply disappear from where it was. This is relatively rare though, and does not have a big impact on the game, and the code and resource required for that - which would increase cycles between frames - makes it non-advantageous to fix.
The "lScene.die()" method is called when the player is "squished":
The "lScene.lMessage(...) displays a message at the top of the screen. The arguments for this are:
The game uses this mechanism to tell the player what has happened,
The next thing it does is turn the light red. This is the sunk=light, so everything will appear red.
The final thing is set the "lScene.isend" property to "true", which, in the above "lScene.lLoop(...)" method, srops the game play and displays the end animations instead.
This is called after the player is squished, and controls the "commiseration" animation. This consists of everything stopping, going red, and dimming to blackness over the next five seconds:
When called by the "lScene.lLoop(...)" method shown above it is called using "return this.dieing(delta);". In other words, what this returns the "lScene.lLoop(...)" method returns, and if that returns "true" the game continues, and if "false" the game ends.
The first thing this method does is detriment the "lScene.endtime" function. The is set to 5.0 in the constructor. If this ends up being zero the game finishes. The effect of this is to end the game five seconds while repeatedly calling this.
If incomplete, the "directionalLighColor" (sunlight) and the "ambientLight" (light in the shadow) is reduced depending how much time is keft in the "lScene.endtime" property, remembering that the "directionalLightColor" has been turned red at this point.
This is called when the player "Makes it" by reaching the end of the corridor:
This method first uses the "lScene.lMessage(...)" method to tell the player of the success, and does so in the "lightgreen" color instead of the default "red". It then sets the "lScene.isend" property to "true" so the animations are called.
The celebration animation consists in an "explosion" of all spheres going in all directions. This is set up here. First the camera is moved outside the corridor so it cannot be seen, then all the spheres are mmade visible, and placed in front of the camera using their "sphere.endx", "sphere.endy" and "sphere.endz" properties. These were created for each sphere in the "Sphere" class's constructor using a random number between 10.0 and 20.0. This has the effect of displaying the spheres randomly in front of the camera.
The "obj.mkvisible(true)" is required here because the spheres may be invisible at this point, and they all need to be visible for the "celebration" animation.
the "obj.procpos()" also required because that needs to process the position of the spheres for the LimpetGE rendering.
The "celebrating" animation consists of the spheres shooting out in all directions in a kind of sphere "explosion":
The mechanism for timing this (to 5.0 seconds) is the same as the "lScene.dieing(...)" method above.
The animation is controlled by moving the object, using the "obj.move(x, y, z)" method, by the afore mentioned "sphere.endx", "sphere.endy" and "sphere.endz" velocities. The "obj.move(x, y, z)" method moves the object relative to it's own space by the amount supplied along each axis.
The "obj.procpos()" is required to process the position of the sphere prior to rendering.
That is it. If you have managed to do all that you have created your first game.
The full script to date can be viewed here. It is an elementary game, but still a game! Here is a link to an HTML page where you can play it!.
Continue to Part three of the tutorial.