📜 ⬆️ ⬇️

Creating 3D Chess in Unity

image

Not every successful game must be dedicated to shooting aliens or saving the world. The history of board games, and in particular chess, dates back thousands of years. It is not only interesting to play in them - the very idea of ​​porting a board game from the real world to a video game is fascinating.

In this tutorial, we will create a 3D chess game on Unity. In the process, you will learn. how to implement the following:


By the end of this tutorial, we will create a multipurpose chess game that can be used as a basis for developing other board games.

Note: you need to know Unity and the C # language. If you want to increase your skill in C #, then you can start with a series of video courses Beginning C # with Unity Screencast .

Getting Started


Download project materials for this tutorial. To get started, open a project stub in Unity.
')
Chess is often realized as a simple 2D game. However, in our 3D version we will imitate a player sitting at a table and playing with his friend. In addition, 3D is awesome.

Open the Main scene from the Scenes folder. You will see the Board object, which is the game board, and the object for the GameManager . Scripts are already attached to these objects.


GameManager contains a 2D array of pieces that stores the positions of pieces on the board. Explore AddPiece , PieceAtGrid and GridForPiece to understand how it works.

Turn on Play mode to look at the board and see the figures ready for the game.


Moving shapes


First of all, we need to determine which piece to move.

You can determine which cell the player hoisted the mouse by using raycasting / raycasting . If you do not know how ray tracking in Unity works, then read our tutorial Introduction to Unity scripting or a popular tomial about the game Bomberman .

After the player chooses a figure, we must generate admissible cells to which the figure can move. Then you need to choose one of them. To handle this functionality, we will add two new scripts. TileSelector will help you choose a moving shape, and MoveSelector will allow you to choose a place to move.

Both components have the same basic methods:


This is the simplest implementation of the state machine pattern. If you need more states, you can make it more formal. However, this will add complexity.

Cell selection


Select Board from the hierarchy. Then click on the Add Component button in the Inspector window. Type TileSelector in the field and click New Script . Finally, click Create and Add to attach the script.

Note: when creating new scripts, take time to move them to the appropriate folder to maintain order in the Assets folder.

Select the selected cell


Double-click TileSelector.cs to open it, and add the following variables inside the class definition:

 public GameObject tileHighlightPrefab; private GameObject tileHighlight; 

In these variables is stored a transparent overlay pointing to the cell under the mouse cursor. The prefab is assigned in edit mode and the component tracks the selection and moves with it.

Next, add the following lines to Start :

 Vector2Int gridPoint = Geometry.GridPoint(0, 0); Vector3 point = Geometry.PointFromGrid(gridPoint); tileHighlight = Instantiate(tileHighlightPrefab, point, Quaternion.identity, gameObject.transform); tileHighlight.SetActive(false); 

Start gets the source row and column for the selected cell, turns them to a point and creates a game object from the prefab. This object is initially deactivated, so it will not be visible until it is needed.

Note: It is useful to refer to the coordinates for the row and column, which take the form Vector2Int and are Vector2Int to as GridPoint . Vector2Int has two integer values: x and y. When we need to place an object in a scene, we need a Vector3 point. Vector3 has three floating point values: x, y, and z.

Geometry.cs are helper methods for the following transformations:


Next we will add EnterState :

 public void EnterState() { enabled = true; } 

This allows you to re-enable the component when it is time to select another shape.

Next, add the following to Update :

 Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(ray, out hit)) { Vector3 point = hit.point; Vector2Int gridPoint = Geometry.GridFromPoint(point); tileHighlight.SetActive(true); tileHighlight.transform.position = Geometry.PointFromGrid(gridPoint); } else { tileHighlight.SetActive(false); } 

Here we create a ray of Ray from the camera, passing through the mouse pointer and on, to infinity.

Physics.Raycast checks if this ray intersects with any physical colliders of the system. Since the board is the only object with a collider, we do not need to worry that the figures will overlap each other.

If the beam intersects the collider, then RaycastHit recorded in RaycastHit , including the intersection point. Using an auxiliary method, we turn this intersection point into a GridPoint , and then use this method to set the position of the selected cell.

Since the mouse pointer is over the board, we also turn on the selected cell so that it is displayed.

Finally, select Board from the hierarchy and in the Project window click on Prefabs . Then drag the Selection-Yellow prefab into the Tile Highlight slot of the Prefab component of the Tile Selector board.

Now if you start the Play mode, you will see a yellow selection box that follows the mouse pointer.


Figure selection


To select a shape, we need to check if the mouse button is down. Add this check to the if block, right after the place where we turn on the cell selection:

 if (Input.GetMouseButtonDown(0)) { GameObject selectedPiece = GameManager.instance.PieceAtGrid(gridPoint); if(GameManager.instance.DoesPieceBelongToCurrentPlayer(selectedPiece)) { GameManager.instance.SelectPiece(selectedPiece); //   1:      ExitState } } 

If the mouse button is pressed, the GameManager sends us a figure in the current position. We need to check whether this piece belongs to the current player, because players should not be able to move the opponent’s pieces.

Note: in such complex games, it is useful to clearly define the limits of responsibility of the components. Board deals only with the display and selection of figures. GameManager tracks the values ​​of the GridPoint positions of the shapes. It also contains helper methods that answer questions about where the pieces are and to which player they belong.

Launch Play mode and select a shape.


Having chosen a figure, we must learn to move it to a new cell.

Selection point


At this point, TileSelector did all its work. It is time for another component: MoveSelector .

This component is similar to TileSelector . As before, select the Board object in the hierarchy, add a new component to it and name it MoveSelector .

Transfer of control


The first thing we need to achieve is to learn how to transfer control from TileSelector to the TileSelector component. To do this, you can use ExitState . Add the following method to TileSelector.cs :

 private void ExitState(GameObject movingPiece) { this.enabled = false; tileHighlight.SetActive(false); MoveSelector move = GetComponent<MoveSelector>(); move.EnterState(movingPiece); } 

It hides the cell overlay and disables the TileSelector component. In Unity, you cannot call the Update method of disabled components. Since we now want to call the Update method of the new component, then by disabling the old component, it will not disturb us.

Call this method by adding the following line to Update immediately after 1 :

 ExitState(selectedPiece); 

Now open the MoveSelector and add these instance variables at the top of the class:

 public GameObject moveLocationPrefab; public GameObject tileHighlightPrefab; public GameObject attackLocationPrefab; private GameObject tileHighlight; private GameObject movingPiece; 

They contain mouse selection, cell overlays for movement and attack, as well as an instance of the selection cell and the shape selected in the previous step.

Then add the following setup code to Start :

 this.enabled = false; tileHighlight = Instantiate(tileHighlightPrefab, Geometry.PointFromGrid(new Vector2Int(0, 0)), Quaternion.identity, gameObject.transform); tileHighlight.SetActive(false); 

This component must initially be in a disabled state, because we first need to run a TileSelector . Then we load the selection overlay just as we did before.

Move the shape


Next, add the EnterState method:

 public void EnterState(GameObject piece) { movingPiece = piece; this.enabled = true; } 

When calling this method, it saves the moving shape and includes itself.

Add the following lines to the Update method of the MoveSelector component:

 Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(ray, out hit)) { Vector3 point = hit.point; Vector2Int gridPoint = Geometry.GridFromPoint(point); tileHighlight.SetActive(true); tileHighlight.transform.position = Geometry.PointFromGrid(gridPoint); if (Input.GetMouseButtonDown(0)) { //   2:     if (GameManager.instance.PieceAtGrid(gridPoint) == null) { GameManager.instance.Move(movingPiece, gridPoint); } //   3:        ExitState(); } } else { tileHighlight.SetActive(false); } 

In this case, Update similar to the method from TileSelector and uses the same Raycast check to find out which cell a mouse is on. However, this time, when you click the mouse button, we call GameManager to move the shape to a new square.

Finally, add an ExitState method to reset everything and prepare for the next move:

 private void ExitState() { this.enabled = false; tileHighlight.SetActive(false); GameManager.instance.DeselectPiece(movingPiece); movingPiece = null; TileSelector selector = GetComponent<TileSelector>(); selector.EnterState(); } 

We disable this component and hide the cell selection overlay. As the shape is moved, we can clear this value and ask the GameManager to remove the selection from the shape. Then we call EnterState from TileSelector to start the process from the beginning.

Select in the Board editor and drag the prefabs overlay the cells from the prefabs folder into the MoveSelector slots:



Colors can be changed by setting the materials.

Start the Play mode and try to move the shapes.


You will notice that you can move the shapes to any empty cell. It would be a very strange chess game! In the next step, we will make the pieces move according to the rules of the game.

Determine the allowed moves


In chess, each piece has its own permissible moves. Some can move in any direction, others can move to a certain number of cells, and others can only move in one direction. How do we keep track of all these options?

One way is to create an abstract base class that describes all the shapes, followed by creating separate subclasses that override the cell generation method for the turn.

We need to answer one more question: where should we generate a list of moves?

It would be logical to generate them in the EnterState component of the MoveSelector . Here we generate overlay cells showing where the player can go, so this is the most reasonable.

We generate the list of admissible cells


The overall strategy is to take the selected shape and request from GameManager list of allowable cells (i.e. moves). GameManager will use a shape subclass to generate a list of possible cells. It will then filter out positions that are occupied or outside the board.

This filtered list is passed back to the MoveSelector , which highlights the allowed moves and waits for the player to choose.

The pawn has the simplest move, so it’s more logical to start with it.

Open Pawn.cs in Pieces , and modify MoveLocations to look like this:

 public override List MoveLocations(Vector2Int gridPoint) { var locations = new List<Vector2Int>(); int forwardDirection = GameManager.instance.currentPlayer.forward; Vector2Int forward = new Vector2Int(gridPoint.x, gridPoint.y + forwardDirection); if (GameManager.instance.PieceAtGrid(forward) == false) { locations.Add(forward); } Vector2Int forwardRight = new Vector2Int(gridPoint.x + 1, gridPoint.y + forwardDirection); if (GameManager.instance.PieceAtGrid(forwardRight)) { locations.Add(forwardRight); } Vector2Int forwardLeft = new Vector2Int(gridPoint.x - 1, gridPoint.y + forwardDirection); if (GameManager.instance.PieceAtGrid(forwardLeft)) { locations.Add(forwardLeft); } return locations; } 

Here we perform several actions:

First, this code creates an empty list for recording positions. Then he creates a position that represents a cell one step forward.

Since the white and black pawns move in different directions, the Player object contains a value that determines the direction of the pawn's movement. For the first player this value is +1, for the opponent it is -1.

Pawns move in a special way and have several special rules. Although they can move one cell forward, they are not able to take on an opponent figure on that cell; they take the figures only diagonally forward. Before adding the front cell as a valid position, we need to check whether another piece takes this place. If not, we can add the front cell to the list.

In the case of taking cells, we also need to check if there is a figure in this position. If there is, then we can take it.

For the time being we will not check whether it belongs to the player or his opponent, but we will deal with it later.

In the GameManager.cs script , add this method immediately after the Move method:

 public List MovesForPiece(GameObject pieceObject) { Piece piece = pieceObject.GetComponent(); Vector2Int gridPoint = GridForPiece(pieceObject); var locations = piece.MoveLocations(gridPoint); //      locations.RemoveAll(tile => tile.x < 0 || tile.x > 7 || tile.y < 0 || tile.y > 7); //      locations.RemoveAll(tile => FriendlyPieceAt(tile)); return locations; } 

Here we get the Piece component of the game piece, as well as its current position.

Next, we ask GameManager list of positions for this shape and filter out invalid values.

RemoveAll is a useful function that uses a callback expression (callback mechanism) . This method looks at each value in the list, passing it to the expression as a tile . If this expression is true , then the value is removed from the list.

This first expression removes positions with x or y values ​​that would place the piece outside the board. The second filter is similar, but removes all positions in which there are player pieces.

At the top of the MoveSelector.cs script class, add the following instance variables:

 private List<Vector2Int> moveLocations; private List<GameObject> locationHighlights; 

The first stores a list of GridPoint values ​​for move positions; the second contains a list of overlay cells indicating whether the player can move to this position.

Add the following lines to the end of the EnterState method:

 moveLocations = GameManager.instance.MovesForPiece(movingPiece); locationHighlights = new List<GameObject>(); foreach (Vector2Int loc in moveLocations) { GameObject highlight; if (GameManager.instance.PieceAtGrid(loc)) { highlight = Instantiate(attackLocationPrefab, Geometry.PointFromGrid(loc), Quaternion.identity, gameObject.transform); } else { highlight = Instantiate(moveLocationPrefab, Geometry.PointFromGrid(loc), Quaternion.identity, gameObject.transform); } locationHighlights.Add(highlight); } 

This part performs several actions:

First, it receives a list of allowable positions from GameManager and creates an empty list for storing overlay cell objects. It then loops through each position in the list. If there is already a figure in the current position, then it must be an opponent figure, because the player’s figures have already been filtered.

The enemy positions are assigned the overlay of the attack, and the remaining positions the overlay of the course.

Performing a turn


Add this code under 2 , inside the if construct that checks the mouse button:

 if (!moveLocations.Contains(gridPoint)) { return; } 

If a player clicks on a cell that is not a valid move, then exit the function.

Finally, add the code to the end of ExitState in ExitState :

 foreach (GameObject highlight in locationHighlights) { Destroy(highlight); } 

At this stage, the player chose a move, so we can delete all the overlay objects.


Wow! I had to write a lot of code just to make the pawn move. Having finished with all the difficult work, it will be easier for us to learn how to move other figures.

Next player


If only one side can move, this is not very similar to the game. Until we fix it!

In order for both players to play, we need to decide how to switch between players and where to add the code.

Since GameManager is responsible for all the rules of the game, it is most reasonable to insert the switching code into it.

Switching itself is quite simple to implement. GameManager has variables for current and other players, so we just need to swap these values.

The difficulty is where do we call for a replacement?

The player’s turn ends when he finishes moving the piece. ExitState in MoveSelector is called after moving the selected shape, so it seems that it is best to perform a switch here.

Add the following method to the end of the GameManager.cs script class :

 public void NextPlayer() { Player tempPlayer = currentPlayer; currentPlayer = otherPlayer; otherPlayer = tempPlayer; } 

To reverse the two values, a third variable is needed, used as an intermediary; otherwise, we will overwrite one of the values ​​before it is copied.

Let's go to MoveSelector.cs and add the following code to ExitState , just before the EnterState call:

 GameManager.instance.NextPlayer(); 

That's all! ExitState and EnterState already take care of their own cleaning.

Start the Play mode and you will see that now the figures are moving on both sides. We are already getting closer to the real game.


Taking figures


Taking pieces is an important part of chess. Since all the rules of the game are in GameManager , open it and add the following method:

 public void CapturePieceAt(Vector2Int gridPoint) { GameObject pieceToCapture = PieceAtGrid(gridPoint); currentPlayer.capturedPieces.Add(pieceToCapture); pieces[gridPoint.x, gridPoint.y] = null; Destroy(pieceToCapture); } 

Here, GameManager checks which piece is in the target position. This figure is added to the list of taken pieces for the current player. Then it is removed from the cell record of the GameManager board, and the GameObject destroyed, which removes it from the scene.

To take a figure, you need to stand on top of it. Therefore, the code for calling this action must be in MoveSelector.cs .

In the Update method, locate the comment 3 and replace it with the following construction:

 else { GameManager.instance.CapturePieceAt(gridPoint); GameManager.instance.Move(movingPiece, gridPoint); } 

The previous if construct checks if there is a figure in the target position. Since at the stage of generating moves, the player’s figures were filtered, then the opponent’s figure should be on the cage containing the figure.

After removing the opponent's piece, the selected piece can make a move.

Click on Play and move the pawns until you can take one of them.


I am the queen, you took my pawn - get ready for death!

Game completion


The chess game ends when a player takes the opponent's king. When taking a piece, we check whether it is king. If yes, then the game is over.

But how do we stop the game? One way is to remove TileSelector and MoveSelector from the board.

In the CapturePieceAt method of the CapturePieceAt script , add the following lines before deleting the taken shape:

 if (pieceToCapture.GetComponent<Piece>().type == PieceType.King) { Debug.Log(currentPlayer.name + " wins!"); Destroy(board.GetComponent<TileSelector>()); Destroy(board.GetComponent<MoveSelector>()); } 

Disabling these components will not be enough. The following challenges ExitStateand EnterStateturn on one of them, so the game will continue.

Destroy is not only used for classes GameObject; You can also use it to remove a component attached to an object.

Click on Play. Move the pawn and take the opponent's king. You will see that a victory message will be displayed in the Unity console.

As an additional task, you can add UI elements to display the “Game Over” message or go to the menu screen.


Now it is time to get a serious weapon and set in motion more powerful figures!

Special moves


Pieceand its individual subclasses are a great tool for encapsulating special relocation rules.

To add moves to some other pieces, you can use tricks from Pawn. Figures moving one cell in different directions, such as the king and knight, are configured in the same way. Try to implement these rules of moves independently.

Look at the finished project code if you need a hint.

Cell moves


A more difficult case are the pieces that can move several cells in the same direction, namely the bishop, rook and queen. It's easier to show an elephant, so let's start with it.

There Pieceare pre-prepared lists of directions in which the bishop and the rook can move from the starting point. These are all directions from the current position of the shape.

Open Bishop.cs and replace with the MoveLocationsfollowing code:

 public override List<Vector2Int> MoveLocations(Vector2Int gridPoint) { List<Vector2Int> locations = new List<Vector2Int>(); foreach (Vector2Int dir in BishopDirections) { for (int i = 1; i < 8; i++) { Vector2Int nextGridPoint = new Vector2Int(gridPoint.x + i * dir.x, gridPoint.y + i * dir.y); locations.Add(nextGridPoint); if (GameManager.instance.PieceAtGrid(nextGridPoint)) { break; } } } return locations; } 

The cycle foreachgoes around each direction. For each direction, there is a second cycle that generates a sufficient number of new positions to move the piece off the board. Since the position list will filter out positions outside the board, we just need so many of them so that we don’t miss any cells.

At each stage, we will generate GridPointfor the position and add it to the list. Then check if there is a figure in this position. If so, stop the inner loop to go in the next direction.

breakadded because the standing figure will block further movement. Again, down the chain, we remove the filter position with the player's figures, so that we no longer interfere with their presence.

Note: if you need to distinguish the direction forward from the direction backwards, or left from the right, then you need to take into account that the black and white pieces move in opposite directions.

In chess, this is important only for pawns, but in other games such distinction may be necessary.

That's all! Start the Play mode and try to play.


Move the queen


The queen is the strongest piece, so it is best to finish on him.

The queen move is a combination of the bishop and the rook; The base class contains an array of directions for each shape. It will be useful if you can combine these two figures.

In Queen.cs, replace with the MoveLocationsfollowing code:

 public override List<Vector2Int> MoveLocations(Vector2Int gridPoint) { List<Vector2Int> locations = new List<Vector2Int>(); List<Vector2Int> directions = new List<Vector2Int>(BishopDirections); directions.AddRange(RookDirections); foreach (Vector2Int dir in directions) { for (int i = 1; i < 8; i++) { Vector2Int nextGridPoint = new Vector2Int(gridPoint.x + i * dir.x, gridPoint.y + i * dir.y); locations.Add(nextGridPoint); if (GameManager.instance.PieceAtGrid(nextGridPoint)) { break; } } } return locations; } 

The only thing that is different here is the transformation of an array of directions into List.

The advantage Listis that we can add directions from another array, creating one Listwith all directions. The rest of the method is the same as in the elephant code.

Click on Play again and take the pawns out of the way to make sure everything works correctly.


Where to go next?


At this stage we can do several things, for example, to complete the movements of the king, the knight and the rook. If you have problems at any of the stages, then study the finished project in the project materials .

There are special rules that we did not implement here, for example, the first move of a pawn to two cells instead of one, castling and some others.

The general pattern is to add to the GameManagervariables and methods that track these situations and their possibility when moving the figure. If they are possible, then you need to add corresponding positions in this figure MoveLocations.

You can also make visual improvements to the game. For example, the pieces can move to a new position smoothly, and the camera rotates to show the board from the point of view of the second player in his turn.

Source: https://habr.com/ru/post/359356/


All Articles