Creating the Levels

The Mediécross levels make heavy use of instances. Each level is built from a project library of items and several items are combined into scenes to create new items.

Creating the Visuals


Creating levels was done in a very ordered manner. By first creating parts, setting them up in a closed context before integrating them into a larger scene used as the level.

Creating the Level Parts

François started by modeling and texturing the base assets making a level. These assets were created using 3DS Max and imported into GameStart by using the FBX exchange format.


The first assets, well before the game was to become Metrocross inspired.

Recycling a Maximum of What's Available

As the project developed new assets were created and variants of the previously created ones were made. Several new assets were also created by combining existing assets parts. Obviously because we are lazy but also because of the strict time constraint of the compo.


The final assets library as seen in the GameStart project explorer.

All of the available level parts are full-blown scenes and we took a strong advantage of this fact by defining collision, physics properties and eventually scripts directly in the item scenes. Once the assets setup was done creating a level was merely a game of drag'n'drop, moving assets around and adjusting things.

Feel free to open and look how the level part scenes are setup. The tree_square_0.nms is an example of ingenious assets recycling.

Creating a Level

As explained earlier, once the assets were setup, creating the level was as easy as it gets. The GameStart tools really helped here. Once a few levels were done, it was possible to work on a new level while having several other finished levels open to copy/paste sections from.

Here is a video of the creation of a new level from parts of the old ones.

Creating a level in a matter of minutes.

Creating the Logic


The level uses a script to track the elapsed amount of time since the race start but also to display the various UI elements such as the score, life count and time but also handles the transition between the race start and the race itself.

Using the Script Coroutine

To do all this a very convenient feature of Squirrel, the coroutine (or thread/cothread), is used. Coroutines basically let you suspend a function execution at any point and resume execution from that point later on. This is a very efficient way to program sequences of events without having to maintain a large state machine.

Let's have a look at what a Mediécross level does:

  1. Display the level name and play the start level jingle.
  2. Switch to the game camera and display a countdown to the start of race.
  3. Start the race and let the game run.
  4. Detect when the time runs out or the Knight reaches the level end trigger.
  5. Display a game over screen if the time ran out.
  6. Add time left to the score and proceed to the next level otherwise.

Wouldn't it be nice if we could just write that code just like that? Well, that's exactly what coroutines let us do. Let's consider the first point of the previous list as implemented in the final coroutine code:


                          
  1. //-------------------------------------------------------------------------------------
  2. // Display the level name and rotate around the knight character.
  3. //-------------------------------------------------------------------------------------
  4.  
  5. // Start the music!
  6. MixerSoundStart(g_mixer, sfx_intro)
  7.  
  8. // Program the UI to fade in in 1 second, pause for 3 seconds then fade out in 0.15 seconds.
  9. UISetCommandList(ui, "globalfade 1, 0; nop 3; globalfade 0.15, 1;")
  10.  
  11. // As long as the UI command list is not done, we wait...
  12. while (!UIIsCommandListDone(ui))
  13. suspend("Game")
  14.  
  15. //-------------------------------------------------------------------------------------

                        

Hopefully this script sample is sufficiently explicit, note that I am omitting the setup code for this but you can find it all in the LevelLogic function of the level.nut script. Let's dig in this small code sample further.

Points of Interest

There are a few things to mention. First, the UISetCommandList call uses the ACE system. In a nutshell, ACE is a lightweight script language meant to simplify things like fades and simple motions. As the comment says this call programs the UI system to fade-in in 1 second, pause for 3 seconds then fade out in 0.15 seconds.

The second thing of interest concerns the suspension of execution for the coroutine. The execution is blocked as long as the UI system is not done executing its ACE command list. What is happening is that each call to suspend will resume on the while condition test. If that test fails the coroutine will suspend again right away. When the test condition is met the program will continue its execution right past the while loop.

Lastly, notice the parameter passed to the suspend function. This will passed as a return value to the calling function (the one that wakes up the coroutine on each frame) and is used to pass the level state to the higher control level. As long as we return the "Game" string the calling function will loop and wake up the coroutine periodically. But when a game over or the end of the level is reached that return value string will be changed and the calling function will take the appropriate action... Loading the next level if the current one was completed or loading the title screen if time ran out.

Note that a coroutine does not execute in the context of its caller. What this means is that if you create or wake a coroutine from a class instance, that coroutine cannot access the instance members directly. So unless you clutter the global space (by using global variables to store your game states) the parameter passed to the suspend call is a critical mechanism to communicate with the calling context.

Note: If you really need to, you can always pass the instance to the coroutine while creating or waking it up, this way it will be able to access its content indirectly.

Maximizing Perfomance


The method used here to build the level is obviously very efficient in term of flexibility and allows very quick cycles of design/test. There's one catch however. All these objects we create without taking care are:

  1. Fully dynamic actors of the scene and,
  2. separate display lists because of that.

Obviously the level background, the slowing tiles and the slope items won't move and do not need to be disjoint display lists cluttering the item lists of the scene.

GameStart lets us solve this problem in two ways. The first flexible way is through the use of a scene builtin script that will merge a specific group of items under a single item (and one geometry). The second way is by manually performing the same operation from the GameStart interface by using the Merge Selection... command under the Scene/Tools menu.

We went for the first way even if it considerably slows down the loading process as it allowed us to make fast adjustment to comments on the level design and the loading was still fast enough. For a more ambitious project however, no doubt that the second approach would be better.