In this post, I’ll be covering the basics of building a simple chess game in Unity and C#.
The MVP of this project will be a fully playable 3D chess game for two players. Each character will only be able to make moves that comply with the rules. After slaying a king, the game is reset. To improve the game, menu scenes, statistics and a simple AI will be added later. The final project and its source code is available on my GitHub-profile.
Since this article became quite large, I split it into different parts:
- Loading the Assets - for programmers lacking 3d-modeling skills
- Setting up the Scene - to create a basic setting
- Drawing the Chessboard - to make testing easier
- Initializing the Characters - and move them to their default location
- Movement Control - to differentiate between selecting and moving characters
- Board Highlighting - to mark all fields to which can be moved
- Obey the Rules - to implement each characters movesets
Loading the Assets
I think it is quite common that programmers are bad designers. At least that’s the case for me, which is why I try to get the graphic elements for my chess game from the Asset Store. I found Arcane Cyber’s Classic Chess Set to look pretty nice and decided to give it a shot. This asset pack contains three different types of figures (metallic, marble & plastic), although the board is only available in the marble look.
Setting up the Scene
I start with creating a simple scene, which contains a camera and a light source, as well as an empty GameObject for the chessboard. The chessboards prefab is attached to this gameobject as a child element. Another child element is a simple plane, which I use for testing. For the time being, I deactivate the chess board itself to get a better overview.
As soon as the scene itself is finished, I start with the actual scripting. The parent element of the chess field receives a script called BoardManager, which manages the whole game.
Drawing the Chessboard
The BoardManager has several tasks, which I’ll be covering in the following. First of all, the manager loads all the characters at their starting positions. Then, possible movements are calculated and the observance of the alternating moves is checked.
I’ll start by drawing a chessboard manually. This helps to test the correct movements and positioning inside the squares of the chess board. The method is called in the scripts Start()-method.
This function generates an 8×8 grid, which symbolizes the chess board and is now used to simplify the positioning of the chess pieces. Before I will position the game pieces next, I want to add a function to hold the current mouse position on the chess board. This function will be implemented by displaying a cross on the labelled field.
The currently selected position will be identified by two integer values, which represent X and Y position, ranging from 0 to 7. These two variables are defined at class level and have a default value of -1, which indicates that no field on the chessboard is selected. This value should also be assumed if the mouse position is outside of the chess field. To update the selection, I move the call of DrawChessboard() from the Start()– to the Update()-method.
I use a Raycast to check if the mouse is inside the board. I also assign the chessboard GameObject its own layer called “ChessPlane”, which is checked for availability by the Raycast.
This method is also called in the Update()-method. As you can see in the snippet above, only the X and Y variables are set in the method, but no corresponding changes are made to the graphical user interface. I implement this functionality in the DrawChessBoard()-method to maintain a clear code structure through the related functional area. So, after drawing the board itself, I use the following code to display the current mouse position. By this instruction, two further lines are drawn, which are represented in the form of a cross on the chess board.
Initializing the Characters
Next, the chess pieces are initialized and positioned at their respective locations. For this purpose I need information about the different game pieces, so a prefab for each piece has to be created. The package from the Asset Store I used already has prebuilt assets, but you’ll want to adjust the scaling and rotation.
I would like to create a class for each type of character so that I can deduce the different possible movements later on. But this is not needed at the moment, so I create a superclass which describes each character and is later used as a parent class of the different characters.
I’ve already done some preliminary work here, which is why this class needs an explanation. Each figure has a position, described by two-dimensional coordinates (X and Y). In addition, each figure is either white or black and should have information about the positions to which it may move. The differentiation between white and black figures is realized by a boolean. Alternatively, an enumeration could be used here as well, which I think would be better with regard to readability, but the boolean offers the advantage of comparing this property with a corresponding property of the BoardManager to check the current active color and switching it with the != operator. I have provided the class with the keyword abstract, which prevents the use of the class itself, so only the subclasses can be used.
So much for it. Next, a class is created for each type of character, inherited from the superclass. Since these classes will only overwrite the possible movements, they do not contain any logic of their own at the moment. Remember to adjust the different prefabs after adding the script using the public property “isWhite” and set the check mark in the editor to the white figures.
To assign the different prefabs to BoardManager, I’ll now add a public list of the GameObject type and add the different prefabs to it in the editor. The order of the instances is not important, but I recommend that you build a basic logic to make the instantiation as easy to read as possible later on. In my case, I use the following order:
- King (White)
- Queen (White)
- Rook (White)
- Bishop (White)
- Knight (White)
- Pawn (White)
- King (Black)
- Queen (Black)
- Rook (Black)
- Bishop (Black)
- Knight (Black)
- Pawn (Black)
To instantiate the different figures correctly, different functionalities are needed. The figure must start on the correct field, but also take the right position (centered) within the field. To calculate the center of a field, I created a method GetTileCenter (). This requires the information about the size of a field, which I have stored in a constant called TILE_SIZE (which is equal to 1). I also calculate the center using an offset (TILE_OFFSET) of 0.5. The parameters correspond to the coordinates of the respective figure on the chess board, which are represented by X and Y (0–7).
This method is now called by another one, which is used to instantiate the figures. Here too, I have already implemented another functionality, which I must briefly explain. ChessFigurePositions is a two-dimensional array of the type GameObject, which holds information about the positions of figures on the board. When an entry of this is equal to null, that means that no figure is located on that position. ActiveFigures is list of the type GameObject, used to keep track of all characters that are currently alive.
With this method, it is now possible to instantiate game pieces, but there is no information on which position belongs to which figure. I create another method called SpawnAllChessFigures(), which handles this task. This is then called in the Start() method of the script and will later also be used to implement the functionality to reset the game. The only difficulty here lies in the correct positioning of the game pieces, which is determined by the parameters of the spawn function.
After all characters have been added to the board, they need to be able to change their position. I splt this functionality into two parts. First of all, it must be possible to select a character by clicking on it. Afterwards, the selected character must be able to move to another location (considering its allowed moves) as far as it is the figures colors turn.
To implement this functionality, I have created two new methods in the BoardManager: SelectChessFigure() and MoveChessFigure(). To keep track of the currently active color, I have created a class wide boolean-variable named isWhiteTurn, which can be compared to the isWhite property of the individual characters. The call of these two functions is in the Update()-method and listens for a mouse click.
I have created another boolean-variable inside the method, which is used to monitor whether the selected figure can move at all. If this is not the case, the figure should not be selectable, so that a different figure can be selected directly afterwards. Otherwise, the selection of a field to which the piece should moved follows. In case of no other possible movement a double click on a new figure would be required, which is circumvented by this implementation. So, the task of the nested for-loop is simply to check if the chosen figure is able to move.
If the figure is able to move, all available target fields should be highlighted. I have outsourced this functionality to another class, which I will discuss next. The array containing information on the available movements is also filled here. This calculation takes place in the different classes of the individual game pieces, which will also be discussed soon.
The MoveChessFigure()-Method is implemented quite simple. If there is already a different colored figure on the target field, it will be destroyed. If this figure happens to be a king, the EndGame-()-method is called, which resets the game. Afterwards, the board layout and position properties of the selected piece are updated.
Another detail that is visible in the implementation of the EndGame()-method is the access to the BoardHighlighting-class. This was implemented as a singleton, whereby only one instance of the class can exist, which is accessible from every area of the game.
The BoardHighlighting-class is used to emphasise possible target positions. I use a simple prefab in the form of a square plane, which is instantiated on the potential target fields. The possible fields are set by a parameter in form of a two-dimensional boolean array. The highlight objects are additionally managed in a list, which makes it easy to perform a reset between moves.
And that’s it! Almost. The only thing missing is the assignment of the possible moves to the different pieces, which will be covered next.
Obeying the Rules
The simplest character is also the most difficult to implement, so I’ll start with this one. Generally, the pawn can only move forward one field per turn. However, there are special regulations: if the pawn is located on its start field, then he’s allowed to move two fields forward. He can also not move forward if there is another character on that field. Finally, the pawn may only attack diagonally.
Because the direction in which the figures move is different, I implement this logic for both colors separately:
The next character I’m going to implement will be the bishop. The bishop can run any number of fields diagonally, as long as no other figures stand in the way.
The rook is implemented in a similar way to the bishop, but the movement here is straight (vertical and horizontal) instead of diagonal.
Another complicated looking figure is the knight. The knight moves two fields straight ahead in one direction, followed by a 90 degree turn and a further movement by one field in this direction. Alternatively, the movement of a single field can be done first, followed by the two-field-movement. Figures that are on the fields within the movement sequence are irrelevant. However, the knight has a positive side: the movements do not have to be implemented separately for both colors.
The possible movements of the king correspond to two fields in any direction, which I have implemented as follows:
And last but not least, the queen’s movements are still missing. These seem to be quite complex again, but here you can simply combine the already created movements of rook and bishop, which is why I don’t separately add a snippet of the source code for the queens implementation.
Now that’s it! The functionality of the MVP defined at the beginning is fully implemented. You can now try to implement a simple AI or whatever else you can think of!