LimpetGE is a WebGL game engine library written in Javascript to run in HTML5 (ECMA) compatible browesers, using WebGL. It is designed to facilitate the rapid development of simple WebGL games. It is not a shader writer. Also it has some 3D modelling routines incorporated in it, but simple ones.
This tutorial, with the next, goes through the process of creating a simple game.
At time of writing, the prject is hosted at https://github.com/eddymac/limpetge.
To install dowload the files to a development directory. These should contain the following directory tree:
To "do" this tutorial, it is assumed you will create a direcory "tutorial" (or whatever) and work in there.
Before actual coding though, I will go through the overall concepts of LimpetGE.
LimpetGE is a game engine library. It is not a "3D" magic thing, or shader writer. There are however standard shaders, and appropriate interfaces, in the "shaders" directory. (This tutorial will use one of those - "ShaderSimple".)
The library handles creation and placement of game objects, collision detection helpers, sound helpers, WebGL based key input helpers and the such.
For game objects, the "structure" hierarchy is:
Once created, Game objects are placed into a "Game object hierarchy":
In the running of the game, game objects are manipulated and tested on as per described.
The game this tutorial showcases is one called "Squish". It is a very simple game where the player tries to get to the end of a corridor avoiding a whole lot of giant spheres rolling the other way, so as not to be "squished". No sound or anything included here as I did not ant to clutter this first tutorial.
A completed version of this game is included in the examples.
As this is a WebGL game it needs to exist on an HTML page. The one used is:
This is not a HTML tutorial, but to go through respective parts:
In the LimpetGE library, global variables are used, as are properties on the programmer written "Scene" class. The names of these all start with one of:
The "regex" for this is: _?[lL][A-Z][a-zA_Z_0-9]+
If the programmer avoids variable names that follow the above criteria there will be no clashes, and it is easier to read. Also, is easy to see what is a library supplied variable/property/method easier, especially in this tutorial.!
Now that is out the way - to go to the meat of the game.
First things first, the top line should read:
If not, you deserve everything you are going to get.
As performance is an issue, it is best to import all names from the library, and the shaders, individually. At first this looks like a icomplex mess, but it is a simple matter of copy and paste to implement.
Looking at the "Structural Concept" tree above, we will start by the structure classes. We will not create the instances at this point, just the classses.
Let us do the "corridor" which the player advances through The structure is created within a function that returns an instance of it. This may seem daunting at first, but I will go through it line by line.
Going through the function:
An array named "colors" is created with four colors. The colors are "vec4" type, or an array of four floating numbers, consisting of red, green, blue and alpha values. These values are between 0.0 and 1.0.
Create a new instance of the structure definition. Arguments are:
The way LimpetGE deals with colors is it creates a small texture of the colors, consisting of one pixel high and as many colors there are pixels wide, each pixel being the appropriate color. The shader then uses "UV" coordinate matching of that texture to assign the appropriate color to the appropriate surface.
What the "lTextureColor" funcion does is that it converts the arguments given to an appropriate "LTextureControl" instance, which in turn controls the "UV" coordinates in the shader buffer itself. The arguments are:
What the above code snippet does is to assign each color in the "colors" array defined above to instances that can be used for surfaces in the component creation below.
Now the structure base and other data has been created, we can start adding components.
For the "Squish" game, a unit of "1" in size represents one meter (ish).
First - the corridor. This has two walls (left and right), a floor and ceiling. Each needs to be added.
The walls of the corridor:
This adds the left wall, then the right. Going through the first line (adding the left wall)..
"addBlock" is a method of a "LStructureDef" class instance that adds a "block". This is like a cube, brick, plank or similar, that has a width, height an depth and right-angled corners, and straight edges and surfaces. Like all components, it takes one argument, a javascript object consisting of a number of named controlling arguments, most of those optional. The ones used, with explanations, are:
Digression - the "lIndArray" function.
What this does is take an array of array pairs, and converts each pair into a [key, value] entry into an object. So:
is the same as:
Therefore, the first line:
In effect it tells LimpetGE to render a brown plane 20 meters to the left.
The second line does the same thing, except to the wall on the right.
These two lines create a green floor 2 meters below the origin, and a cyan ceiling 3 meters above. I will leave it up to the reader to wok out the mechanics. There is a new optional argument though:
In order to see advancement along the corridor, the above places white lines 20 centimeters wide every 10 meters along it. It doe this by rendering the top of a white blocks 2 millimeters thick and 20 cetimetrs wide 1 millimetr above the floor, and the same 1 millimeter inside the side walls. Again, not including these in the Collision Detection Mechanism (corners: null).
To include a finish line. A line is created one meter thick at the end using the same method.
That concludes building the components for the corridor. The function then returns the instance when called.
The Corridor structure has been defined. Now for some game objects.
For objects to exist they need to have a scene to exist in. We will start with a simple one.
The "Scene" is programmer defined, but it needs to be derived from "LBase". When an instance of this is created, a reference to that instance is automatically copied to the global "lScene" variable. While it is doing that, it also creates a camera object (an "LCamera" instance) and copies that into the global variable "lCamera".
More than one scene or camera can exist at the same time, but cannot be active simultaneously. This is advanced stuff though and beyond this tutorial. For now, we will just have one scene.
To go through the above:
The constructor. It takes the argument "args", which is a Javascript object with optional named arguments, and passes this to the "LBase" class constructor. Other arguments can be included here of course if the programmer requires it.
The "lInput" object is like a static class, and it handles the keyboard input during the game. The "press(..)" method of this takes a javascript key code of a key as the argument(The "https://keycode.info" web site is useful for determining this), and returns a "LKey" class instance, which has a property "val" which is the boolean "true" when the key is pressed, or "false" when not.
The "lInput.usekeys()" method activates this.
The "lLoop" function is a virtual function in "LBase" so needs to be included in the "Scene" class. It is called once per frame. It takes the argument "delta" which is a floating point of the number of seconds (not milliseconds) since the last time this was called. I have found this to be a number near 0.013.
Going through the function:
The above looks at the keyboard entry, and sets the "x" and "z" variables to how much the camera has moved depending on what key is pressed. When going forward, the camera move at 5 meters a second (a good run). When back or side at 2.5 meters a second, and if moving sideways it slows how fast forward or back it is going by a factor of 0.7.
The "moveFlat(....)" method of the lCamera object moves the camera along it's x or z axis. As it does not rotae at all, this is always towards the "back" of the scene.
It is the responsibility of what has moved to see if it has hit anything. So next the collision detection:
A closure function is used here. The main part of the above is the "lCAllPointDetect(....)" method - (defined in "LBase"). This takes three arguments:
The closure function itself takes one argument, which is the instance of the object that has been collided with. In the above it looks at the "control" property of that, and if it is "Corridor" (explained next in this tutorial) then it has hit a wall If it has then it needs to move back.
It would be wrong for the "move back" to occur in the closure, as that may be called more than once. LimpetGE employs ray tracing, so if it gets laggy, that scenario may occur.
In the above, if the camera tries to move before the start of the corridor it stops it. lCamera.z is the Camera's scene Z coordinates. Then if the camera reaches the end of the corridor, it exits the game ("lLoop" function returns the boolean "false"), otherwise the game continues ("lLoop" function returns boolean "true").
Note: I could have simply tested if the "lCamera.x" property was less than -20 or greater than 20 for wall collision detection, but the reasons here for not are:
We have created the means for creating a "Corridor" structure. To create the game object based on it:
This is a class with no methods. It is created by "new Corridor()". This sets the propery "obj" to a Game object. The constructor of this has:
The "lScene.lPlace(...)" method places the object in the scene at the top level of the hierarchy. The arguments for this are:
To put the whole together...
The "g_structures" variable is a global one, and in fact, I would normally place it near the top of the jsavascript file.
The "g_playgame()" function is called by the "onload" function of the HTML file. and runs the thing.
The "lInit()" function initialises the WebGL context (lGl variable), and compiles the shaders. The inclusion of shaders is explained below. This should only be called once at the start of everything.
Next to place the instance of LStuctureDef that defines the Corridor into "g_structures.Corridor". This is not necessary per-se, but defining structures can be resource intensive and I have got into the habbit of just doing so once, and storing it in a global variable.
The "g_playlevel()" plays a level. In this initial example this happens only once, so I could have included all "g_playlevel"'s code here. Hoever, anything more complex that would not be right.
In "g_playlevel()":
The "new Scene({....})" constructor. This creates a new "Scene", and places a copy of the instance in the global valriable "lScene", so it does not need to asign it to anything here.
As for the constructor's named arguments suplied here:
The lScene.lDefaultMessage property is the default message displayed in the HTML element Id'd "lTMessage".
The "lScene.ambientLight" is a "vec3" array defining the ambient lighting (the light in the shade) used by the shader (explained below)
The "lScene.directionalLightColor" is a "vec3" array defining the directional light color (sunlight) used by the shader.
The "new Corridor()" statement creates a new corridor as defined above. The constructor places it in the game object hierarchy (using "lScene.lPlace(...)"), so again, needs no assignment here.
The "lCamera.moveHere(....)" method moves the camera to the supplied X, Y and Z co-ordinates. The camera is already pointing "down" the Z axis so it does not need to be rotated.
The "lScene.lSetup()" function needs to be called after all objects are created. It primarily sets up the Static Collision Detection Sparse Array and positions everything in their initial positions.
The "lScene.lMain()" function runs the game.
Finally, in order for "g_playgame" to be seen by the "onload" property of the body, it needs to exist on the "window" object....
This uses a standard shader "ShaderSimple". To implement this:
lExtendarray(lShader_objects, [ ShaderSimple ]);
That should implement the shader proggram, as long as the script is referenced in a "<script... >" tag in the HTML.
Once done you are ready to run the first iteration of this. The full script to date can be viewed here. It is not a game yet, but you can run something. Here is a link to an HTML page where you can run the script.
Continue to Part two of the tutorial.