Creating 3D Chess in Unity

Original author: Brian Broom
  • Transfer
  • Tutorial
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. They are not only interesting to play, the 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 find out. how to implement the following:

  • How to choose a moveable figure
  • How to determine allowed moves
  • How to change players
  • How to recognize a state of victory

By the end of this tutorial, we will create a multifunctional game of chess, which can be used as a basis for the development of other board games.

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

Getting to work


Download project materials for this tutorial. To get started, open the project blank in Unity.

Chess is often implemented as a simple 2D game. However, in our version in 3D, we will imitate a player sitting at a table and playing with his friend. In addition, 3D is cool.

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

  • Prefabs : it contains a board, individual pieces and squares-indicators for the selection of cells, which we will use in the process of choosing a move.
  • Materials : here are materials for a chessboard, pieces and cells.
  • Scripts : contains components that are already attached to objects in the hierarchy.
  • Board : controls the visual display of shapes. This component is also responsible for highlighting individual shapes.
  • Geometry.cs : helper class that manages the transformations between writing rows / columns and points Vector3.
  • Player.cs : controls the player’s pieces as well as the pieces taken by the player. In addition, it contains the direction of movement of the pieces, for which the direction is important, for example, for pawns.
  • Piece.cs : A base class that defines enumerations for all instances of shapes. Also contains the logic for determining the valid moves in the game.
  • GameManager.cs : stores game logic, such as valid moves, the original location of the pieces at the beginning of the game, and more. This is a singleton, so it is convenient for other classes to call it.

GameManagercontains a 2D array piecesstoring the position of the pieces on the board. Explore AddPiece, PieceAtGridand GridForPieceto understand how it works.

Turn on Play mode to look at the board and see ready-to-play pieces.


Moving Shapes


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

To determine which cell the player has moved the mouse to, you can use Ray Tracking / Raycasting . If you don’t know how ray tracking works in Unity, read our tutorial Introduction to Unity Scripting or the popular Bomberman tutorial .

After the player selects a piece, we must generate valid cells that the piece can move to. Then you need to choose one of them. To process this functionality, we will add two new scripts. TileSelectorhelp you choose a moving figure, and MoveSelectorwill allow you to choose a place to move.

Both components have the same basic methods:

  • Start: for initial setup.
  • EnterState: Performs setup for the current shape activation.
  • Update: Performs ray tracking while moving the mouse.
  • ExitState: resets the current state and calls the EnterStatenext state.

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

Cell selection


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

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

Select cell selection


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

public GameObject tileHighlightPrefab;
private GameObject tileHighlight;

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

Next, add to the Startfollowing lines:

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

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

Note: it is useful to refer to the coordinates by row and column, which take the form Vector2Intand to which we refer as GridPoint. Vector2Inthas two integer values: x and y. When we need to position an object in a scene, we need a point Vector3. Vector3has three floating point values: x, y, and z.

Geometry.cs are helper methods for the following transformations:

  • GridPoint(int col, int row): gives us GridPointfor a given column and row.
  • PointFromGrid(Vector2Int gridPoint): Converts GridPointto a real point in the scene Vector3.
  • GridFromPoint(Vector3 point): gives us GridPointfor the x and z values ​​of this 3D point, and the y value is ignored.

Next we will add EnterState:

public void EnterState()
{
    enabled = true;
}

This allows you to turn on the component again when it is time to select another shape.

Next, add to the Updatefollowing:

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 Rayfrom the camera, passing through the mouse pointer and further to infinity.

Physics.Raycastchecks if this beam 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 pieces will overlap.

If the beam crosses the collider, then RaycastHitdetails are recorded in it, including the intersection point. Using the auxiliary method, we turn this intersection point into GridPoint, and then use this method to set the position of the selected cell.

Since the mouse pointer is above the board, we also include the selected cell to display it.

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

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


Figure selection


To select a shape, we need to check whether the mouse button is pressed. Add this check to the block if, immediately 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, it GameManagerpasses us a figure at the current position. We need to check whether this piece belongs to the current player, because players should not be able to move enemy pieces.

Note: in such complex games it is useful to clearly define the boundaries of responsibility of components. Boarddeals only with the display and selection of shapes. GameManagerkeeps track of the GridPointposition values 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.


Choosing a shape, we must learn to move it to a new cell.

Move point selection


At this stage, he TileSelectorcompleted all his work. It is time for another component: MoveSelector.

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

Control transfer


The first thing we need to achieve is to learn how to transfer control from a TileSelectorcomponent MoveSelector. You can use for this ExitState. Add the following method to TileSelector.cs :

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

It hides the cell overlay and disables the component TileSelector. In Unity, you cannot call the method of Updatedisabled components. Since we want to call the method of the Updatenew component now, by disabling the old component, it will not bother us.

We call this method by adding the following line Updateimmediately after Опорной точки 1:

ExitState(selectedPiece);

Now open MoveSelectorand 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 moving and attacking, as well as an instance of the selection cell and the figure selected in the previous step.

Then add the following configuration code to Start :

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

This component should initially be in the off state, because we need to execute first TileSelector. Then we load the selection overlay in the same way as we did before.

Move the figure


Next, add the method EnterState:

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

When this method is called, it saves the moved shape and turns on itself.

Add the following lines to the Updatecomponent method MoveSelector:

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, it is Updatesimilar to the method from TileSelectorand uses the same check Raycastto find out which cell the mouse is over. However, this time, when we click the mouse button, we call GameManagerto move the figure to a new cell.

Finally, add a method ExitStateto 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();
    selector.EnterState();
}

We disable this component and hide the cell selection overlay. Since the shape has been moved, we can clear this value and ask GameManagerto deselect the shape. Then we call EnterStateout TileSelectorto start the process from the very beginning.

In the Board editor, select and drag the prefabs overlaying the cells from the prefabs folder into the slots MoveSelector:

  • Move Location Prefab must beSelection-Blue
  • Tile Highlight Prefab should be Selection-Yellow.
  • Attack Location Prefab should be Selection-Red.


Colors can be changed using material settings.

Launch Play mode and try moving the shapes.


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

Determine allowed moves


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

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

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

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

Generate a list of valid cells


The general strategy is to take the selected piece and request a GameManagerlist of valid cells (i.e. moves). GameManagerwill use a subclass of the shape to generate a list of possible cells. Then it will filter the occupied or off-board positions.

This filtered list is passed back to MoveSelector, which highlights the valid moves and awaits player selection.

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

Open Pawn.cs in Pieces , and change MoveLocationsit to look like this:

public override List MoveLocations(Vector2Int gridPoint) 
{
    var locations = new List();
    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 to record positions. Then he creates a position, which is a cell one step forward.

Since white and black pawns move in different directions, the object Playercontains a value that determines the direction of movement of the pawn. 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 forward by one cell, they are not able to take the opponent’s figure on this cell; they take figures only diagonally forward. Before adding the front cell as a valid position, we need to check if another figure occupies this place. If not, we can add the front cell to the list.

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

Until we check whether it belongs to the player or his opponent, and we will deal with this later.

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

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 component of the Piecegame figure, as well as its current position.

Next, we ask the GameManagerlist of positions for this figure and filter out invalid values.

RemoveAllIs a useful function using the callback expression (callback mechanism) . This method scans each value in the list, pass it to the expression as tile. If this expression is equal true, then the value is removed from the list.

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

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

private List moveLocations;
private List locationHighlights;

The first one contains a list of values GridPointfor move positions; the second one contains a list of overlay cells showing whether the player can move to this position.

Add the EnterStatefollowing lines to the end of the method :

moveLocations = GameManager.instance.MovesForPiece(movingPiece);
locationHighlights = new List();
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 valid positions from GameManagerand creates an empty list for storing overlay cell objects. Then it loops through each position in the list. If there is already a figure in the current position, then it should be the opponent’s figure, because the player’s figures are already filtered out.

The opponent’s position is assigned an attack overlay, and the remaining position is assigned an overlay of the move.

Progress


Add this code below Опорной точкой 2, inside the construct ifthat checks the mouse button:

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

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

Finally, add the code to MoveSelector.cs at the end ExitState:

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

At this stage, the player has chosen the move, so we can delete all the objects of the overlays.


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 to move other figures.

Next player


If only one side can move, then this is not very much like a game. So far, we have to fix it!

So that both players can play, we need to decide how to switch between the players and where to add the code.

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

Switching itself is quite simple to implement. There GameManagerare variables in the 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. ExitStateis MoveSelectorcalled after moving the selected shape, so it seems like it's best to do the switching 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 interchange 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 move on to MoveSelector.cs and add to the ExitStatefollowing code, right before the call EnterState:

GameManager.instance.NextPlayer();

That's all! ExitStateand EnterStatealready take care of their own cleaning.

Launch 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 it GameManagerchecks which figure is in the target position. This figure is added to the list of taken figures for the current player. Then it is removed from the board's cell record GameManager, and GameObjectdestroyed, which removes it from the scene.

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

In the method, Updatefind the comment Опорная точка 3and replace it with the following construction:

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

The previous design ifchecked if the figure was in the target position. Since the player’s pieces were filtered out at the stage of generating the moves, the opponent’s piece must be on the cell containing the piece.

After deleting an 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 - prepare for death!

Game completion


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

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

In the CapturePieceAtscript method of GameManager.cs add the following lines before deleting the taken shape:

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

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

Destroy is not just for classes GameObject; you can 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 is 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 is the 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 rules of movement.

To add moves to some other pieces, you can use tricks from Pawn. Shapes moving one square in different directions, such as a king and a horse, are configured in the same way. Try to implement these move rules yourself.

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

Multi-cell moves


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

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

Open Bishop.cs and replace with the MoveLocationsfollowing code:

public override List MoveLocations(Vector2Int gridPoint)
{
    List locations = new List();
    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 of the directions. There is a second cycle for each direction, generating a sufficient number of new positions to move the piece out of the board. Since the list of positions will filter the 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 there is, then we stop the inner cycle to go in the next direction.

breakadded because a standing figure will block further movement. I repeat, lower in the chain we will remove the positions with the player’s figures by a filter so that their presence no longer bothers us.

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

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

That's all! Launch Play mode and try playing.


We move the queen


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

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

In Queen.cs, replace with the MoveLocationsfollowing code:

public override List MoveLocations(Vector2Int gridPoint)
{
    List locations = new List();
    List directions = new List(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 differs here is the transformation of an array of directions into List.

The advantage Listis that we can add directions from another array by creating one Listwith all directions. Otherwise, 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 is working correctly.


Where to go next?


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

There are special rules that we have not implemented here, for example, the first move of a pawn into two squares instead of one, castling and some others.

The general pattern is to add GameManagervariables and methods that track these situations and their ability to move the shape. If they are possible, then you need to add the 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.

Also popular now: