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 .
Vector3
points.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.TileSelector
will help you choose a moving shape, and MoveSelector
will allow you to choose a place to move.Start
: for initial setup.EnterState
: performs customization for the current shape activation.Update
: performs ray tracking as you move the mouse.ExitState
: resets the current state and calls EnterState
next state.Note: when creating new scripts, take time to move them to the appropriate folder to maintain order in the Assets folder.
public GameObject tileHighlightPrefab; private GameObject tileHighlight;
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.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.GridPoint(int col, int row)
: gives us a GridPoint
for a given column and row.PointFromGrid(Vector2Int gridPoint)
: converts a GridPoint
to a real point of the Vector3
scene.GridFromPoint(Vector3 point)
: gives us a GridPoint
for the x and z values ​​of this 3D point, and the y value is ignored.EnterState
: public void EnterState() { enabled = true; }
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); }
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.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.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 } }
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 theGridPoint
positions of the shapes. It also contains helper methods that answer questions about where the pieces are and to which player they belong.
TileSelector
did all its work. It is time for another component: MoveSelector
.TileSelector
. As before, select the Board
object in the hierarchy, add a new component to it and name it MoveSelector
.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); }
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.Update
immediately after 1
: ExitState(selectedPiece);
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;
this.enabled = false; tileHighlight = Instantiate(tileHighlightPrefab, Geometry.PointFromGrid(new Vector2Int(0, 0)), Quaternion.identity, gameObject.transform); tileHighlight.SetActive(false);
TileSelector
. Then we load the selection overlay just as we did before.EnterState
method: public void EnterState(GameObject piece) { movingPiece = piece; this.enabled = true; }
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); }
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.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(); }
GameManager
to remove the selection from the shape. Then we call EnterState
from TileSelector
to start the process from the beginning.MoveSelector
slots:Selection-Blue
Selection-Yellow
.Selection-Red
.EnterState
component of the MoveSelector
. Here we generate overlay cells showing where the player can go, so this is the most reasonable.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.MoveSelector
, which highlights the allowed moves and waits for the player to choose.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; }
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.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; }
Piece
component of the game piece, as well as its current position.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. private List<Vector2Int> moveLocations; private List<GameObject> locationHighlights;
GridPoint
values ​​for move positions; the second contains a list of overlay cells indicating whether the player can move to this position.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); }
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.2
, inside the if
construct that checks the mouse button: if (!moveLocations.Contains(gridPoint)) { return; }
ExitState
in ExitState
: foreach (GameObject highlight in locationHighlights) { Destroy(highlight); }
GameManager
is responsible for all the rules of the game, it is most reasonable to insert the switching code into it.GameManager
has variables for current and other players, so we just need to swap these values.ExitState
in MoveSelector
is called after moving the selected shape, so it seems that it is best to perform a switch here. public void NextPlayer() { Player tempPlayer = currentPlayer; currentPlayer = otherPlayer; otherPlayer = tempPlayer; }
ExitState
, just before the EnterState
call: GameManager.instance.NextPlayer();
ExitState
and EnterState
already take care of their own cleaning.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); }
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.Update
method, locate the comment 3
and replace it with the following construction: else { GameManager.instance.CapturePieceAt(gridPoint); GameManager.instance.Move(movingPiece, gridPoint); }
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.TileSelector
and MoveSelector
from the board.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>()); }
ExitState
and EnterState
turn on one of them, so the game will continue.GameObject
; You can also use it to remove a component attached to an object.Piece
and its individual subclasses are a great tool for encapsulating special relocation rules.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.Piece
are 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.MoveLocations
following 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; }
foreach
goes 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.GridPoint
for 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.break
added 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.
MoveLocations
following 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; }
List
.List
is that we can add directions from another array, creating one List
with all directions. The rest of the method is the same as in the elephant code.GameManager
variables 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
.Source: https://habr.com/ru/post/359356/
All Articles