So you want to build a video game? Cool! I’m not really a professional game developer myself, but I have made a few games here and there for fun. The architecture and patterns used for building games are a bit different from, say, developing a website or back-end service.

In this post, we will walk through the core concepts that you, as a programmer, would need to know before you dive into creating your own game. These patterns are so essential to building a game, that if you left any programmer with the task of developing a game for the first time, they would probably come up with the same patterns themselves… eventually.

The state machine

A game is actually just one giant state machine. Picture a game of snake: Its “state” refers to the position of each block of the snake, along with the position of the next morsel of food. Now, the next state is calculated based on the current state, and the user input.

This calculation of the next state, happening many times a second, more or less makes a video game.

In an ideal world, we would just program the game like it was an ideal state machine and be done with it. However, games are visual, and need pictures and sounds to fully immerse the user. These pictures and sounds are commonly referred to as “assets”, They are too heavy to be loaded from disk and displayed in real time.

This is where the all-too-familiar loading screen comes in. The purpose of this screen is not solely to annoy you. During this time, the different images and audio clips that the game needs are loaded into memory, from where they can be fetched and displayed in real time while the game is being played.

Initialization

Before the game begins, it’s initial state is decided. For our game of snake, this would mean deciding the initial position of the body segments of the snake, as well as the position of the first morsel of food. If we expressed the snakes state as a JSON object, it would look like this:

{ "snake" : { "body" : [ { "x" : 5 , "y" : 5 } , { "x" : 6 , "y" : 5 } , { "x" : 7 , "y" : 5 } ] , "direction" : "UP" , } , "food" : { "x" : 2 , "y" : 10 } }

The state represents the current frame in a game:

If the game is savable, then this would be the point where the previous saved state is fetched and loaded.

Although games seem continuous to the player, they are actually a bunch of static frames, that are rendered based on the games current state. The frames are refreshed at a high rate (30-60 frames per second) to give the appearance of continuity.

As mentioned before, the next state is calculated based on the current state, and the input. This calculation is done for every frame, and is called the update loop. The update loop is probably the most important part in understanding game development, and is also the part that will contain the bulk of the logic that makes the game work.

For our snake game, let’s take a look at the simplified version of the update loop that we would see in a typical game. I have used javascript to illustrate this, and have only covered the actions of movement and eating food, leaving out the conditions for dying or ending the game:

const state = { snake : { body : [ { x : 5 , y : 5 } , { x : 6 , y : 5 } , { x : 7 , y : 5 } ] , direction : 'UP' } , food : { x : 2 , y : 10 } } const input = { upKeyPressed : false , downKeyPressed : false , leftKeyPressed : false , rightKeyPressed : false } function update ( ) { if ( state . snake . direction == 'UP' || state . snake . direction == 'DOWN' ) { if ( input . leftKeyPressed ) { state . snake . direction = 'LEFT' } else if ( input . rightKeyPressed ) { state . snake . direction = 'RIGHT' } } if ( state . snake . direction == 'LEFT' || state . snake . direction == 'RIGHT' ) { if ( input . upKeyPressed ) { state . snake . direction = 'UP' } else if ( input . downKeyPressed ) { state . snake . direction = 'DOWN' } } const head = state . snake . body [ 0 ] const nextPos = { x : head . x , y : head . y } switch ( state . snake . direction ) { case 'UP' : nextPos . y -= 1 break case 'DOWN' : nextPos . y += 1 break case 'LEFT' : nextPos . x -= 1 break case 'RIGHT' : nextPos . x += 1 break } if ( head . x === state . food . x && head . y === state . food . y ) { state . snake . body . push ( { x : 0 , y : 0 } ) } for ( let i = state . snake . body . length - 1 ; i >= 0 ; i -- ) { let nextSegment = i === 0 ? nextPos : state . snake . body [ i - 1 ] state . snake . body [ i ] . x = nextSegment . x state . snake . body [ i ] . y = nextSegment . y } }

After the update() function is run, the state of the snakes body would change from

[ { x : 5 , y : 5 } , { x : 6 , y : 5 } , { x : 7 , y : 5 } ]

to

[ { x : 5 , y : 4 } , { x : 5 , y : 5 } , { x : 6 , y : 5 } ]

Rendering

Ok, so we have the state object, which lets us know the state of everything in the game. The update loop controls how this state changes. Now, all that’s left is to actually draw the game on screen from the state. This is referred to as “rendering”, or “painting”. This stage normally takes place just after the update function has completed. Now that we have the updated state, we can paint the corresponding frame.

Scaling

The resolution of the screen that the game is rendered to is often different from the in-game co-ordinate system. In our example, the coordinate system should be scaled to the pixels on screen. If the resolution of our assets (the picture of the snake head, body, and food) is 20x20, and the resolution of the area we are rendering to is 300x300, then that would make our game coordinate system 15 (300/20) blocks wide and 15 blocks tall.

The code required for painting differs based on the platform. The code for painting for an android game would be quite different from, say, a game made in Unity. For the sake of completeness, let’s see the code that renders our snake game on to an HTML5 Canvas:

const render = ( ) => { ctx . clearRect ( 0 , 0 , boardWidth , boardHeight ) ctx . drawImage ( imgFood , state . food . x * blockSize , state . food . y * blockSize ) const head = state . snake . body [ 0 ] ctx . drawImage ( imgHead , head . x * blockSize , head . y * blockSize ) for ( let i = 1 ; i < state . snake . body . length ; i ++ ) { ctx . drawImage ( imgBody , state . snake . body [ i ] . x * blockSize , state . snake . body [ i ] . y * blockSize ) } }

This is a simplified version of the actual rendering code. Here, we skipped the rotation of the head based on the direction of the snake (Which you can see in the full source code).

Optimizations

Games are one of the most resource intensive applications that run on a regular computer. For 3D games, this means rendering many models and shapes at a rate fast enough to simulate motion. For web based games, this involves downloading many heavy assets to be displayed in game. There are a lot of optimizations that happen behind the scenes.

One such optimization used for 2D games is using sprite sheets. A sprite sheet is a single image that contains all the images used in-game. For our snake game, we used 3 20x20 images for the head, body, and food. Instead of downloading all 3 separately when the game was loading, we could download a single 60x20 image containing all the images within it. This reduces the number of HTTP requests required, and also makes better use of image compression when possible.

Finishing up

In this post, we went through the basic concepts of game development, and the lifecycle that a game goes through. But, as with everything, the devil is in the details. There’s a bit more that we need to do to make the game work as expected. This is dependent on the platform or framework we’re using. For our web-based snake game, we need to:

Wait for the assets to load before starting the game loop Run the update and render function periodically, and the desired fps (frames per second) rate. Update the input state when we get a keydown event from the keyboard. Calculate the blockSize based on the height and width of the canvas, and the height and width of our assets.

You can view the complete source code for this game on Github, and play the actual game here.