The purpose of this tutorial is to teach 2D game programming and C-language through examples. The author used to program games in the mid-1980s and was a game designer at MicroProse for a year in the 90s. Although much of that is not relevant to the programming of today's big 3D games, for small casual games it will serve as a useful introduction.
Games like snake where objects are moving over a 2D field can represent the game objects either in a 2D grid or as a single dimension array of objects. "Object" here meaning any game object, not an object as used in object-oriented programming.
The keys are move with W=up, A= left, S=down, D=right. Press Esc to quit the game, f to toggle frame rate (this isn't synchronized to the display so can be fast), tab key to toggle debug info and p to pause it. When it's paused the caption changes and the snake flashes,
In snake the main game objects are
- The snake
- Traps and fruit
For purposes of gameplay, an array of ints will hold every game object (or part for the snake). This can also help when rendering the objects into the screen buffer. I've designed the graphics for the game as follows:
- Horizontal Snake Body - 0
- Vertical Snake Body - 1
- Head in 4 x 90-degree rotations 2-5
- Tail in 4 x 90-degree rotations 6-9
- Curves for Directions Change. 10-13
- Apple - 14
- Strawberry - 15
- Banana - 16
- Trap - 17
- View the snake graphics file snake.gif
So, it makes sense to use these values in a grid type defined as blockWIDTH*HEIGHT. As there are only 256 locations in the grid I've chosen to store it in a single dimension array. Each coordinate on the 16 x16 grid is an integer 0-255. We've used ints so you could make the grid bigger. Everything is defined by #defines with WIDTH and HEIGHT both 16. As the snake graphics are 48 x 48 pixels (GRWIDTH and GRHEIGHT #defines) the window is initially defined as 17 x GRWIDTH and 17 x GRHEIGHT to be just slightly bigger than the grid.
This has benefits in game speed as using two indexes is always slower than one but it means instead of adding or subtracting 1 from the snake's Y coordinates to move vertically, you subtract WIDTH. Add 1 to move right. However being sneaky we've also defined a macro l(x,y) which converts the x and y coordinates at compile time.
What Is a Macro?
The first row is index 0-15, the 2nd 16-31 etc. If the snake is in the first column and moving left then the check to hit the wall, before moving left, must check if coordinate %WIDTH ==0 and for the right wall coordinate %WIDTH == WIDTH-1. The % is the C modulus operator (like clock arithmetic) and returns the remainder after division. 31 div 16 leaves a remainder of 15.
Managing the Snake
There are three blocks (int arrays) used in the game.
- snake, a ring buffer
- shape - Holds Snake graphic indexes
- dir - Holds the direction of every segment in the snake including head and tail.
At the game start, the snake is two segments long with a head and a tail. Both can point in 4 directions. For north the head is index 3, the tail is 7, for the east head is 4, the tail is 8, for the south head is 5 and the tail is 9, and for the west, the head is 6 and tail is 10. While the snake is two segments long the head and tail are always 180 degrees apart, but after the snake grows they can be 90 or 270 degrees.
The game starts with the head facing north at location 120 and the tail facing south at 136, roughly central. At a slight cost of some 1,600 bytes of storage, we can gain a discernible speed improvement in the game by holding the snake's locations in the snake ring buffer mentioned above.
What Is a Ring Buffer?
A ring buffer is a block of memory used for storing a queue that is a fixed size and must be big enough to hold all data. In this case, it's just for the snake. The data is pushed on the front of the queue and taken off the back. If the front of the queue hits the end of the block, then it wraps around. So long as the block is big enough, the front of the queue will never catch up with the back.
Every location of the snake (i.e., the single int coordinate) from the tail to the head (i.e., backwards) is stored in the ring buffer. This gives speed benefits because no matter how long the snake gets, only the head, tail and the first segment after the head (if it exists) need to be changed as it moves.
Storing it backwards is also beneficial because when the snake gets food, the snake will grow when it's next moved. This is done by moving the head one location in the ring buffer and changing the old head location to become a segment. The snake is made up of a head, 0-n segments), and then a tail.
When the snake eats food, the atefood variable is set to 1 and checked in the function DoSnakeMove()
Moving the Snake
We use two index variables, headindex and tailindex to point to the head and tail locations in the ring buffer. These start at 1 (headindex) and 0. So location 1 in the ring buffer holds the location (0-255) of the snake on the board. Location 0 holds the tail location. When the snake moves one location forward, both the tailindex and headindex are incremented by one, wrapping round to 0 when they reach 256. So now the location that was the head is where the tail is.
Even with a very long snake that is winding and convoluted in say 200 segments. only the headindex, segment next to the head and tailindex change each time it moves.
Note because of the way SDL works, we have to draw the entire snake every frame. Every element is drawn into the frame buffer then flipped so it's displayed. This has one advantage though in that we could draw the snake smoothly moving a few pixels, not an entire grid position.