Tech

Making A One Script Game

Techblog_header

Posted by Mark Alexander on 9 September 2016

Over at the GMC we have been having a "One Script Game" jam, which is essentially a mini competition to create a game using only one script. There are a few other limitations too, like you can only use enums and local variables (so no global nor instance variables), and you can't use any other resource from the GMS resource tree, which means no sprites or sounds, etc... either! I don't normally participate in game Jams as my free time is limited, but this one attracted me as I love a good challenge. So, in this tech blog I'm going to discuss the game I made and explain a few of the coding and design decisions...

Choosing A Game

The first thing was to choose a game to make. Given how little time I actually had - I reckoned a couple of evenings worth of work at most - it had to be something simple. Immediately I thought of making a Pong clone, but almost as fast as choosing it I discarded it, since it would require an AI for the opponent and I really didn't fancy going that far! My next choice was Sokoban, since I've already made a sokoban game and had a good idea of what would be required. However, rui-rosario (who is running the jam) had already made an OSG called "Pushy" which is a sokoban clone and even included the source code in the topic... After thinking of other classic games I could remake (like Pac Man or Q-bert) I decided upon one that was really simple but well known: Asteroids!.

OSG Asteroids

This game sat well with the idea of the Jam, since it uses simple vertex graphics (no sprites required, yay!) and has very simple gameplay: One player ship, a few asteroids and some bullets. Obviously I would need a bit more than that, as to be a game it has to have a title screen, game-over conditions, levels, etc... but all that was "fluff" that should be easy enough to add at the end after I had the general gameplay worked out.

Perpetuating Variables

The main limitation of the OSG Jam is that you can't use instance variables nor global variables. This meant that to perpetuate values from one frame to the next would require saving out the current values at the end of every frame and loading them in again at the start. I set about doing some simple tests to see how this works and was actually quite disappointed by the results. My initial test was simply a script that moved a rectangle around the screen using the arrow keys, with the "player" position being saved to a file every frame like this:

  • Open file
  • Read values and set some local vars to hold them
  • Update player position adding to the loaded values based on keys pressed
  • Write the updated position to the ini file
  • Close the ini

Sadly, when I checked this in the debugger, I saw that it was slooooooow. Opening, updating and writing to the ini file was fine, but calling ini_close() was just eating up my FPS, so I also tried text files, secure ds_maps and buffers as alternative save options. Here are the heartbreaking results (the baseline was using instance variables to hold position which worked out at about 2500 - 3000 fps):

Okay, so maybe 800fps sounds a lot, or even 1000fps if I use buffers, but I wasn't sure just how much power the rest of the game code would require and so I wanted to maximise my FPS as much as possible right from the start. In the OSG Jam topic a "hack" was discussed where you would use a buffer or a data structure to perpetuate values across frames and this seemed an interesting approach. Basically all data structures and all buffers have an "ID" value assigned to them, and if they are created at the start of the game and no previous buffers or data structures exist then this ID value will always be zero (to be sure I checked this with Mike Dailly and he confirmed it was the case). This means that I could create a data structure or buffer on the first game frame, and then access it every following frame as long as I used (0) as it's ID value.

The basic code structure I came up with was this:

enum map{
    ID = 0
    }
if !ds_exists(map.ID, ds_type_map) {   
    var m = ds_map_create();
    // populate the map with the required base values
    m[? "player_x"] = 1024 / 2;
    m[? "player_y"] = 768 / 2;
    m[? "player_spd"] = 0;
    m[? "player_dir"] = 90;
    }
else {
    // Game will go here
    }

That's the general idea (I went with using a DS map, as they are easy to use and like that you can "name" values). First I check to see if the map with ID (0) exists and if it doesn't I then create it. Since the map will not exist for that very first frame, it means that I can wrap all of my game initialisation code into that very first "if" check, and then put everything else into the "else" part.

Drawing

The only way to draw anything in GMS is to use the Draw Event, so this script has to go into there and not in the Step Event. However, what can I draw? As mentioned above, the game Asteroids is a vector game, with all the graphics being made up of simple lines, which means I can do the same in my OSG version.

Classic Asteroids

I could have created a sprite from a surface and stored its ID in the DS map, or I could have saved it to disk and loaded it each frame (probably very slow), but drawing basic forms in GMS is actually very fast, so I went with creating an array of points around the player position and using lengthdir_x/y along with the stored DS map value for direction to set the draw angle. This approach worked really well and gave the player ship an "authentic" look! The code is something like this:

var p_x = map.ID[? "player_x"];
var p_y = map.ID[? "player_y"];
var p_d = map.ID[? "player_dir"];
// Keyboard checks here where I update the p_x/p_y and p_d variables
px[0] = p_x + lengthdir_x(16, p_d);
py[0] = p_y + lengthdir_y(16, p_d);
px[1] = p_x + lengthdir_x(16, p_d + 135);
py[1] = p_y + lengthdir_y(16, p_d + 135);
px[2] = p_x + lengthdir_x(8, p_d + 180);
py[2] = p_y + lengthdir_y(8, p_d + 180);
px[3] = p_x + lengthdir_x(16, p_d - 135);
py[3] = p_y + lengthdir_y(16, p_d - 135);
draw_line(px[0], py[0], px[1], py[1]);
draw_line(px[1], py[1], px[2], py[2]);
draw_line(px[2], py[2], px[3], py[3]);
draw_line(px[3], py[3], px[0], py[0]);

This looks great, and is very fast to draw so I would use this same approach for drawing the asteroids too. Talking of which...

Asteroids

The basic player movement was easy enough to code, just some keyboard checks and then storing the final position in the DS map, but the game also needs asteroids, and lots of them! I had to work out some way to dynamically add and remove an arbitrary number of asteroids to the base DS map, and it had to be efficient. I felt that adding and deleting multiple map entries every frame (I'd need at least 3 values per asteroid) would be quite hard to manage, so instead I decided to uses nested data structures.

I started by creating a DS List and storing that in the main game map. This list would be held in memory the same as the map, and I can access it every frame by simply accessing the map key. I then went one step further and for every asteroid I create a new DS map! This map would be used to hold any relevant information (position, speed and direction to start with) and would then be assigned a position within the list. So now I have:

DS Maps, with lists and maps

There are loads of benefits to using this method. The first one is that I can use ds_list_size to easily find out if there are any asteroids in the room currently, making the "level complete" state really easy to detect. Lists are also really easy to iterate through, with a simple for loop along with the list size being all I need. Finally, lists are really easy to add and remove items from.

So, I had my asteroid DS list and then I added to that list a series of DS maps - one for each asteroid - containing the x/y position, the direction, the speed, the image angle and the size. These last two were added so that I could have the asteroids spinning independently of the direction of travel, and so that I could have three different sized asteroids: small, medium and large. Before going on with this approach however, I set up three enums for the asteroids. I needed to store the shape of the asteroid somehow and rather than clutter up the asteroid DS maps with loads of position data I simply set up an enum with the point data for the asteroid outline and then used the "size" value of the DS map to choose which enum to use for drawing.

// Enum for the small asteroid
enum a_small{
    len1 = 9,       ang1 = 110,
    len2 = 8,       ang2 = 140,
    len3 = 8,       ang3 = 194,
    len4 = 6,       ang4 = 225,
    len5 = 9,       ang5 = 250,
    len6 = 9,       ang6 = 315,
    len7 = 8,       ang7 = 7,
    len8 = 7,       ang8 = 56,
    rad = 8
    }

// Drawing the asteroid
for (var i = 0; i < ds_list_size(a_list); i++;) {
    var ax, ay, rr;
    var a = ds_list_find_value(a_list, i);
    var tx = a[? "xpos"];
    var ty = a[? "ypos"];
    var aa = a[? "ang"];
    switch (a[? "size"]){
        case 0:
        ax[0] = tx + lengthdir_x(a_small.len1, a_small.ang1 + aa);
        ay[0] = ty + lengthdir_y(a_small.len1, a_small.ang1 + aa);
        ax[1] = tx + lengthdir_x(a_small.len2, a_small.ang2 + aa);
        ay[1] = ty + lengthdir_y(a_small.len2, a_small.ang2 + aa);
        ax[2] = tx + lengthdir_x(a_small.len3, a_small.ang3 + aa);
        ay[2] = ty + lengthdir_y(a_small.len3, a_small.ang3 + aa);
        ax[3] = tx + lengthdir_x(a_small.len4, a_small.ang4 + aa);
        ay[3] = ty + lengthdir_y(a_small.len4, a_small.ang4 + aa);
        ax[4] = tx + lengthdir_x(a_small.len5, a_small.ang5 + aa);
        ay[4] = ty + lengthdir_y(a_small.len5, a_small.ang5 + aa);
        ax[5] = tx + lengthdir_x(a_small.len6, a_small.ang6 + aa);
        ay[5] = ty + lengthdir_y(a_small.len6, a_small.ang6 + aa);
        ax[6] = tx + lengthdir_x(a_small.len7, a_small.ang7 + aa);
        ay[6] = ty + lengthdir_y(a_small.len7, a_small.ang7 + aa);
        ax[7] = tx + lengthdir_x(a_small.len8, a_small.ang8 + aa);
        ay[7] = ty + lengthdir_y(a_small.len8, a_small.ang8 + aa);
        draw_line(ax[0], ay[0], ax[1], ay[1]);
        draw_line(ax[1], ay[1], ax[2], ay[2]);
        draw_line(ax[2], ay[2], ax[3], ay[3]);
        draw_line(ax[3], ay[3], ax[4], ay[4]);
        draw_line(ax[4], ay[4], ax[5], ay[5]);
        draw_line(ax[5], ay[5], ax[6], ay[6]);
        draw_line(ax[6], ay[6], ax[7], ay[7]);
        draw_line(ax[7], ay[7], ax[0], ay[0]);
        break;
        // The rest of the cases here...
        } 
    }

The workflow for the asteroids was now as follows:

  • Initialize the asteroid list and add some asteroid maps to it in the first game frame
  • In every consecutive game frame:
    • Loop through the asteroid list
    • For each iteration, get the DS map ID for the asteroid
    • Use the DS map data to update the asteroid position, angle, etc...
    • Draw the asteroids using the map data and the size enum
    • save the new position etc... to the map

This worked well and the asteroids were drawn as required, and they also moved and rotated. The next thing to deal with was the collisions...

Collisions

In the game I would be required to check for two types of collisions: collision with the player and an asteroid, and collision with a bullet and an asteroid. Considering this I quickly added in the bullets to the game (I made a bullet DS list, just as I did for the asteroids and added a DS map for each bullet into the list), and then looked at the code to see where would be the best place to check for collisions. In the end, I went with checking them in the loop that I had for the asteroids, so that each asteroid will update its position, draw itself and then check for a collision with either the player or a bullet (collisions are checked using the rectangle_in_rectangle function, which is really fast). If one is found, then it removes itself from the asteroid DS list (destroying its map) and also removes the bullet from the bullet DS list or changes the game state to the "end game" state if it's collided with the player.

However... This wasn't nearly as simple as it sounded to me when I first thought about it! Basically, if I remove an entry from the DS list while the list is being iterated through, then the final iterations of the list will return "undefined" since the list is now shorter that it was when we got its size to begin the loop. This might seem obvious with hindsight, but it caused me a bit of a headache to start with as I wasn't sure why I was getting errors. Anyway, to solve this I used - you've guessed it - another DS list!

The idea is that when an asteroid is "destroyed" the list position (the "i" value in the iteration loop) is stored in a temporary list. Once the asteroid list has been dealt with, this temporary list is then iterated through and the asteroids flagged as destroyed are removed from the main list along with their DS maps:

if ds_list_size(temp_list) > 0{
    for (var i = 0; i < ds_list_size(temp_list); i++;){
        var val = temp_list[| 0];
        var asteroid = a_list[| val];
        switch (ds_map_find_value(asteroid, "type")) {
            /// Various things here to create effects, like spawn smaller asteroids and give score, etc...
            }
        ds_map_destroy(asteroid);
        ds_list_delete(a_list, val);
        ds_list_delete(temp_list, 0);
        }
    }
ds_list_destroy(temp_list);

The result of this was that I could get dozens of asteroids and bullets on screen at any one time, and the FPS never dips below 500, even when the number of asteroids is really high.

Lots of asteroids

Game States

As mentioned at the start, I wanted the game to have a "proper" title screen and game over screen, which means I need to have a way to create and change the game state. Again, I created an enum to hold different "state" values and in the initial script I write the state "game_intro" into the game DS map. Now all I have to do is create a switch to deal with the different states that this could be:

switch(map.ID[? "game_state"]){
    case state.intro:
        break;
    case state.game:
        // All the existing game code goes here
        break;
    case state.game_over:
        break;
    }

The great thing about this is that I can add in any extra states that I require, for example in the final game code I have a couple of extra "transition" states for fading out the game over screen and for starting the game/changing level, and since we are using local variables only, I don't need to worry about using variables from any other section. Most of my games have their menus done in a single object and a single event anyway, so it was really easy to add in the main menu and the game over screen to this.

Fluff

After about 12 - 13 hours work I had what I considered the game in a "finished" state, ie: it had title screen and game over and the main game was bug free (afaik...). However it was missing the "fluff" that makes people enjoy a game. It had no explosions, no sound effects, no nice transitions or anything else that could be considered the icing on the cake. Which meant that it was time to have some real fun and see what I could squeeze into the game framework I had created.

Now, I'd love to write here about how I generated audio for the game and explain how I came up with the the code I used, but I'm not going to. I have to confess to simply copy/pasting the manual code for audio buffers (here) and then tweaking the values until I got something I wanted. I know, I know, I wrote that code, but it was still a bit of a cheat... So, I created an audio buffer using the code from the manual and then stored the ID in the main game map for use later.

Another thing I added which gives the game a lot of visual action is particles. I added in a particle system and a couple of particle types to the main game map, which meant I could then create particles at any time by simply referencing this map. I use particles for the explosions and for the background effects to give a "space" feeling. Note too that I have disabled any background drawing, which means that the back-buffer is not being cleared each step. This gives rise to a great "trail" effect and if you draw a black rectangle with a low alpha over the top then you get a really cheap but effective way to add action and motion to a scene. When combined with the particles, it enabled me to use the particle update functions to create a lovely transition between levels and on game end:

Particle transition effect

This kind of effect is really easy to achieve, with code something like this:

 repeat(100){
    if irandom(29) == 0 var c = map.ID[? "accent_colour"] else var c = merge_colour(c_white, c_black, random(0.75));
    part_particles_create_colour(map.ID[? "p_sys"], rm.width / 2, rm.height / 2, map.ID[? "p1"], c, 1);
    part_system_update(map.ID[? "p_sys"]);
    part_system_drawit(map.ID[? "p_sys"]);
    }

You can see that I reference a new main map key "accent_colour". This was another piece of visual "fluff" that I added. Basically I found the whole back/white monochrome thing a bit tiring on the eyes, and so I took a leaf from the Downwell school of game design and made certain things tinted a colour. This meant I could also offer the player an option to change this colour, which I felt was a lovely touch!

Summary

And that's about it. One single, self-contained, 1166 line script to be added to the draw event of an object and then placed in a room to get a full game. The hardest part by far was coming up with the initial method for perpetuating variables and then thinking up a way of dealing with collisions (and making it bug free and fast). The solution of using a general DS map along with a couple lists was certainly the best choice I could have made as it kept the the game fast and fairly structured. Are there things I'd change? Yes, a few... The code is certainly a bit sloppy and I can think of things that could be done to make it nicer, and I'm itching to add in some kind of enemy AI controlled ship that will attack the player after the first couple of levels. However those things are outside of my timescale and will have to wait till another time...

If you want to see what the final result is actually like, then you can play the game over on GameJolt, and when the OSG Jam is finally finished on the forums I'll post the source code along with it so you can have a laugh (or a cry) at how I made the whole thing.

Back to Top