When I first started programming, I think, like many, I wanted to make games. But before me there were many architectural issues that I did not know how to solve, I did not even hear about double buffering, but I wanted to get the result as soon as possible. Therefore, recently I decided to write a project in which it will be possible to write simple games without any problems. Games in this project can be created by GameBoy type, that is: Tetris, Snake, etc. But you can click the mouse in it too.
Link to the project in GitHub .
In this article I want to make out the creation of a snake.
The first thing you need to start is to create your own game class and inherit from the base Game class.
')
class Snake : Game
it has already implemented the playing field and the events that occur during the transition of the game from one state to another. In essence, all we need to do is declare event handling.
public Snake() : base() { OnPreview += BasePreview; OnNewGame += Snake_OnNewGame; OnUpdateGame += Snake_OnUpdateGame; OnGameOver += DrawScore; }
For the OnPreview and OnGameOver events, there are already ready-made stubs in the Game class, they can be not implemented. It remains only to initialize a new game and handle update events.
private GameBlock head; private List<GameBlock> body; private GameBlock eat; private void Snake_OnNewGame() { head = new GameBlock() { X = 10, Y = 10, Vector = Vector.Up, Color = GameColor.Green }; body = new List<GameBlock>(); body.Add( head ); body.Add( new GameBlock() { X = 10, Y = 11, Vector = Vector.Up, Color = GameColor.Black } ); body.Add( new GameBlock() { X = 10, Y = 12, Vector = Vector.Up, Color = GameColor.Black } ); CreateEat(); DrawField(); }
To draw a field, you can work with it directly, or you can use the ready-made GameBlock class. It implements such things as position, direction of movement, and color.
In this function, we declared the snake body, create the first piece of food and display what is happening on the field.
private void CreateEat() { var emptyBlocks = new List<GameBlock>(); for( int i = 0; i < MainForm.FIELD_SIZE; i++ ) for( int j = 0; j < MainForm.FIELD_SIZE; j++ ) if( CheckEmptyBlock( i, j ) ) emptyBlocks.Add(new GameBlock() { X = i, Y = j, Color = GameColor.Red } ); if (emptyBlocks.Count > 0) eat = emptyBlocks[random.Next( emptyBlocks.Count )]; }
To create food, we get a list of empty blocks and use randomizer (which is already announced in the Game) to select a random one. In case the snake took up the whole field it is worth checking the size of the list.
Actually, the function of checking empty cells:
private bool CheckEmptyBlock(int x, int y) => !( x < 0 || y < 0 || x == MainForm.FIELD_SIZE || y == MainForm.FIELD_SIZE ) && !body.Exists( a => a.Equals( new GameBlock() { X = x, Y = y } ) );
Field drawing looks like this:
private void DrawField() { Field.Clear( GameColor.White ); Field.DrawGameBlock( eat ); Field.DrawGameBlocks( body ); WriteScore(); }
As it is not difficult to guess, the field is cleared with white color and food with a snake is displayed. WriteScore is another standard feature for displaying an account on a special status bar.
So go to the update event of the game, which occurs at intervals of 300 ms.
private void Snake_OnUpdateGame( Controller controller ) { ControlMove( controller.GameKey ); if( CheckGameOver() ) GameOver(); else SnakeMove(); }
Four things happen in it: changes in direction, checking for the end of the game, calling the end of the game event and moving the snake in case everything is in order.
private void ControlMove( GameKey key ) { switch( key ) { case GameKey.Left: head.Vector = head.Vector == Vector.Right ? Vector.Right : Vector.Left; break; case GameKey.Right: head.Vector = head.Vector == Vector.Left ? Vector.Left : Vector.Right; break; case GameKey.Up: head.Vector = head.Vector == Vector.Down ? Vector.Down : Vector.Up; break; case GameKey.Down: head.Vector = head.Vector == Vector.Up ? Vector.Up : Vector.Down; break; default: break; } }
To change the direction of movement in the snake, we need to change the vector in her head. Therefore, in motion control there is a check for the case of inversion of a vector, so that the snake does not start climbing on itself.
private bool CheckGameOver() { switch( head.Vector ) { case Vector.Up: return !CheckEmptyBlock( head.X, head.Y - 1 ); case Vector.Down: return !CheckEmptyBlock( head.X, head.Y + 1 ); case Vector.Left: return !CheckEmptyBlock( head.X - 1, head.Y ); case Vector.Right: return !CheckEmptyBlock( head.X + 1, head.Y ); default: throw new NotImplementedException(); } }
To check the end of the game, it is enough to check whether the block in the direction is free or not. As you might guess, the food in the checkout is ignored.
It remains to disassemble the snake movement function:
private void SnakeMove() { var temp = body.Last().Copy(); foreach( var block in body ) block.Move(); for( int i = body.Count - 1; i > 0; i-- ) body[i].Vector = body[i - 1].Vector; if( head.Equals( eat ) ) { score++; body.Add( temp ); CreateEat(); } DrawField(); }
The end of the tail is copied in order in case the food was reached to add it as a snake build-up. Moving blocks is not difficult, because in the class of the block this function is already implemented. Then there is a distribution of vectors by the movement of the snake and checking for intersection with food. If the food is found the account is incremented, the snake grows and a new food is created. In order for our game to appear in the list of games, it needs to be added to the form initialization:
List<Game> games = new List<Game>(); games.Add( new Snake() ); games.Add( new Tetris() ); games.Add( new Life() ); Application.Run( new MainForm( games ) );
That's all. The entire code of the game took only 102 lines. As you can see from the example, tetris and game life have already been added to the project. Below you can see the result.
Game selection menu
Game process
End of the game