Tiếp phần trước hôm nay mình sẽ hướng dẫn các bạn làm di chuyển cho toàn bộ quân trong bàn cờ của game Chess2D.

1. Viết Logic cho việc chọn ô cờ để di chuyển quân cờ

  • Trước tiên với logic di chuyển cho quân cờ, hiện tại chúng ta chỉ có thể click vào ô cờ có quân cờ, nên để di chuyển chúng ta cần có thể click thêm vào các ô có gợi ý di chuyển, nên mình sẽ vào class ChessBoardCell để update logic, mình sẽ thêm 1 hàm là:

private void ClickToMoveChessPieceTo()
{
     if (_cellCanMove.activeSelf)
     {
         _cellCanMove.SetActive(false);
 
         var chessPiece = _boardManager.GameManager.SelectedChessPiece;
         var selectedChessPieceTransform = chessPiece.transform;
 
         _currentChessPiece = chessPiece;
         chessPiece.Move(new Vector2Int((int)_cellPos.x, (int)_cellPos.y));
         selectedChessPieceTransform.parent = this.gameObject.transform;
 
         _boardManager.ResetAllBoardCell();
     }
}
  • Hàm này sẽ kiểm tra xem _cellCanMove có đang được active không, nếu có thì cho phép thực hiện hành vi di chuyển, cũng như tắt đi object _cellCanMove và reset lại toàn bộ cái ô cờ trên bàn để trả lại trạng thái bình thường, còn lại là lấy quân cờ hiện tại đang được chọn từ GameManager, cũng như transform của nó đặt _currentChessPiece của ô cờ hiện tại bằng quân cờ đang được chọn.

  • Sau đó gọi hàm move của quân cờ được chọn và set parent của quân cờ thành ô cờ này.

  • Tiếp đến để có thể chạy mình cần gọi hàm này trong hàm OnMouseDown:

OnMouseDown.png
  • Sau đó mình sẽ bắt đầu update cho từng class của các quân cờ hàm Move để có thể di chuyển.

  • Đầu tiên là quân tốt (Pawn), mình update hàm move như sau:

public override void Move(Vector2Int newPos)
{
     _isFirstMove = false;
 
     this.transform.position = new Vector3(newPos.x, newPos.y, transform.position.z);
     SetPosition(newPos);
}
  • Với hàm này logic khá đơn giản chỉ là set vị trí mới cho quân cờ, và đối với quân tốt, đã di chuyển rồi thì sẽ set _isFirstMove = false để những lần di chuyển sau chỉ có thể gợi ý đi 1 ô.

  • Đối với các quân cờ khác thì mình đơn giản update hàm như sau:

public override void Move(Vector2Int newPos)
{
     this.transform.position = new Vector3(newPos.x, newPos.y, transform.position.z);
     SetPosition(newPos);
}
  • Logic cũng giống quân tốt chỉ khác không check đi lần đầu.

  • Tuy nhiên lúc này chạy game, các bạn sẽ thấy chỉ di chuyển được các quân tốt và ngựa, vì khi di chuyển khỏi ô cờ, chúng ta chưa set lại biến cho ô cờ nên vẫn sẽ tính là ô cờ đó vẫn còn quân cờ ở trên, nên các quân ở sau không thể di chuyển.

  • Nên mình sẽ fix lỗi này bằng cách thêm 1 hàm remove current chess piece trong ô cờ:

internal void RemoveChessPiece()
  {![logicDone.png](/api/v1/media/18f5fe7265145b894a239f271e1c26119313caa355b6ad2a0931045fb4b3948d.png)
      _currentChessPiece = null;
  }
  • Sau đó mình vào class BoardManager để thêm 1 hàm:

 internal void RemoveChessPieceFromCell(ChessPiece chessPiece)
  {
      var cell = GetCell(chessPiece.Position.x, chessPiece.Position.y);
      cell.RemoveChessPiece();
  }
  • Hàm này sẽ truyền vào chessPiece và từ chessPiece lúc này sẽ lấy được giá trị x y và lấy ô cờ hiện tại chứa chessPiece chưa di chuyển và gọi hàm RemoveChessPiece

  • Tiếp theo update hàm ClickToMoveChessPieceTo trong ChessBoardCell thêm 1 dòng như sau:

RemoveChessPiece.png
  • Sau đó ra ngoài và chơi thử, kết quả ta nhận được là:

logicDone.png

2. Viết Logic Nhập thành

-

Đầu tiên mình cần tạo thêm 1 class để điều khiển việc nhập thành, đặt tên class là CastlingControl đặt chung trong folder Manager, cũng như sẽ tạo 1 gameobject trên scene để gán script này vào.

  • Tiếp theo với class King và Rook, vì logic nhập thành mình cần phải biết là quân vua và xe đã từng di chuyển hay chưa, nếu đã di chuyển thì coi như logic này không thể áp dụng, nên mình cần đặt biến check đã di chuyển hay chưa vào trong 2 class này, cũng như sẽ cần truyền control vào để đối tượng king và rook có thể nhập thành.

  • Trong Rook.cs:

public class Rook : ChessPiece
{
    private bool _hasMove;
    private CastlingControl _castling;

    public bool HasMove => _hasMove;

    Vector2Int[] directions = {
            new Vector2Int(1, 0),  
            new Vector2Int(-1, 0), 
            new Vector2Int(0, 1),  
            new Vector2Int(0, -1)  
        };

    public void SetCastling(CastlingControl castling)
    {
        _hasMove = false;
        _castling = castling;
    }
...
}
  • Trong King.cs:

public class King: ChessPiece
{
    private bool _hasMove;
    private CastlingControl _castling;

Vector2Int[] directions = {
        new Vector2Int(1, 0),   // Right
        new Vector2Int(-1, 0),  // Left
        new Vector2Int(0, 1),   // Up
        new Vector2Int(0, -1),  // Down
        new Vector2Int(1, 1),   // Top-right
        new Vector2Int(-1, 1),  // Top-left
        new Vector2Int(1, -1),  // Bottom-right
        new Vector2Int(-1, -1)  // Bottom-left
    };

public void SetCastling(CastlingControl castling)
{
    _hasMove = false;
    _castling = castling;
}
...
}
  • Tiếp theo mình vào class CastlingControl update như sau:

public class CastlingControl : MonoBehaviour
{
    [SerializeField] private BoardManager _boardManager;
    private GameManager _gameManager;
 
    private King _whiteKing;
    private King _blackKing;
 
    private List<Rook> _whiteRooks;
    private List<Rook> _blackRooks;
 
    Vector2Int[] castlingDirections = {
            new Vector2Int(-2, 0),  // Left
            new Vector2Int(2, 0),  // Right
        };
 
    public void InitControl(GameManager gameManager)
    {
        _gameManager = gameManager;
 
        _whiteRooks = new List<Rook>(2);
        _blackRooks = new List<Rook>(2);
    }
 
    public void SetKing(King king)
    {
        king.SetCastling(this);
        if (king.Skin == ChessSkin.White)
            _whiteKing = king;
        else
            _blackKing = king;
    }
 
    public void SetRook(Rook rook)
    {
        rook.SetCastling(this);
        if (rook.Skin == ChessSkin.White)
            _whiteRooks.Add(rook);
        else
            _blackRooks.Add(rook);
    }
 
    public List<(int, int)> GetCastlingSuggest(King king)
    {
        List<(int, int)> validMoves = new List<(int, int)>();
        var kingPos = king.Position;
 
        List<Rook> rooks = king.Skin == ChessSkin.White ? _whiteRooks : _blackRooks;
        int directionLeft = -1;
        int directionRight = 1;
 
        CheckCastlingDirection(kingPos, rooks[0], directionLeft, validMoves);
        CheckCastlingDirection(kingPos, rooks[1], directionRight, validMoves);
 
        return validMoves;
    }
 
    private void CheckCastlingDirection(Vector2Int kingPos, Rook rook, int direction, List<(int, int)> validMoves)
    {
        if (rook.HasMove) return;
 
        bool legitToCastling = true;
        int targetColumn = (direction == -1) ? 0 : 7;
 
        for (int i = kingPos.x + direction; i != targetColumn; i += direction)
        {
            var cell = _boardManager.GetCell(i, kingPos.y);
            if (!cell.IsEmpty())
            {
                legitToCastling = false;
                break;
            }
        }
 
        if (legitToCastling)
        {
            Vector2Int targetPos = kingPos + new Vector2Int(2 * direction, 0);
            validMoves.Add((targetPos.x, targetPos.y));
        }
    }
 
    public void MoveCastling(King king, int moveValue)
    {
        List<Rook> rooks = (king.Skin == ChessSkin.White) ? _whiteRooks : _blackRooks;
 
        int rookIndex = (moveValue > 0) ? 1 : 0;
        int rookOffset = (moveValue > 0) ? 1 : -1;
 
        Vector2Int rookTargetPos = new Vector2Int(king.Position.x + rookOffset, king.Position.y);
 
        _boardManager.RemoveChessPieceFromCell(rooks[rookIndex]);
        rooks[rookIndex].Move(rookTargetPos);
 
        var cell = _boardManager.GetCell(rookTargetPos.x, rookTargetPos.y);
        cell.SetChessPiece(rooks[rookIndex]);
    }
}
  • Logic trong class này đầu tiên vẫn sẽ cần gọi init từ GameManager, và SetKing và SetRook từ 2 phía quân cờ trắng và đen, hàm này sẽ được gọi khi quân cờ được tạo và xếp lên bàn cờ, tiếp theo giống như hàm gợi ý đường đi của quân cờ, thì cũng sẽ có 1 hàm tương tự gợi ý đường đi cho quân vua và kiểm tra các điều kiện nếu đủ điều kiện sẽ gợi ý cho phép quân vua thực hiệp phép nhập thành, cuối cùng là hàm di chuyển quân cờ vua hàm này cũng sẽ được gọi để thực thi hành vi nhập thành, và sẽ được gọi từ hàm di chuyển của quân vua.

  • Tiếp theo mình sẽ update trong class ChessSpawnManager

.....
private CastlingControl _castlingControl;
.....
  public void InitChessSpawner(BoardManager board, CastlingControl castlingControl)
  {
      _boardManager = board;
      _castlingControl = castlingControl;
      InitChessPieces(_boardManager.AllCells);
  }
.....
private void InitGeneralChessPiece((int, int)[] positions, ChessPiece generalChessPiece, ChessSkin skin)
{
     foreach (var pos in positions)
     {
         var cell = _boardManager.AllCells[pos.Item1, pos.Item2];
         var chessPiece = Instantiate(generalChessPiece, cell.transform);
         var chessPieceInstant = chessPiece.GetComponent<ChessPiece>();
 
         chessPieceInstant.InitChessPiece(skin, _boardManager);
         cell.SetChessPiece(chessPiece);
 
         if (chessPieceInstant is King)
             _castlingControl.SetKing((King)chessPieceInstant);
         if (chessPieceInstant is Rook)
             _castlingControl.SetRook((Rook)chessPieceInstant);
     }
}
.....
  • Tiếp theo mình sẽ update trong class GameManager

....
   [SerializeField] private CastlingControl _castingControl;
....
private void InitGame()
{
    .....
     _castingControl.InitControl(this);
     _chessSpawnManager.InitChessSpawner(_board, _castingControl);
     .....
}
.....
  • Tiếp theo mình sẽ update trong class King và Rook:

King.cs
....
public override List<(int, int)> GetWays()
{
.....
if (!_hasMove)
     validMoves.AddRange(_castling.GetCastlingSuggest(this));
....
}
public override void Move(Vector2Int newPos)
{
.....
if (!_hasMove)
{
     _hasMove = true;
     var moveValue = newPos.x - this.transform.position.x;
     if (Mathf.Abs(moveValue) > 1)
         _castling.MoveCastling(this, (int)moveValue);
}
.....
}
  • Trong hàm gợi ý bước đi, chúng ta sẽ check xem quân vua đã di chuyển chưa, nếu chưa thì sẽ gọi tới kiểm tra bước nhập thành nào có thể thực hiện vào hiện gợi ý, còn trong hàm di chuyển chúng ta cũng sẽ kiểm tra xem quân vua đã di chuyển chưa, nếu chưa chúng ta sẽ thực hiện hành vi nhập thành nếu bước đi có giá trị lớn hơn 1, vì quân vua trong bàn cờ chỉ đi được 1 bước, trừ khi nhập thành thì có thể đi 2 bước.

  • Tiếp theo mình sẽ update class Rook.cs:

....
  public override void Move(Vector2Int newPos)
  {
      if (!_hasMove)
          _hasMove = true;
....
}
  • Đối với quân xe thì mình chỉ cần đơn giản là check nếu chưa di chuyển mà thực hiện hành vi di chuyển thì sẽ đánh dấu là đã di chuyển.

  • Sau đó mình sẽ thử vào editor để kéo ref trên scene:

ref1.png
  • Đối với quân xe thì mình chỉ cần đơn giản là check nếu chưa di chuyển mà thực hiện hành vi di chuyển thì sẽ đánh dấu là đã di chuyển.

ref2.png
  • Kéo ref BoardManager vào CastlingControl

  • Sau đó mình chạy game, và kết quả là:

CastlKing.png

DoneCastling.png

Logic phong Hậu

-

Tiếp theo mình sẽ thêm logic phong hậu cho game, đầu tiên mình sẽ vào class Pawn.cs để update:

....
public Action<Vector2Int, Pawn> OnReachExchangeToQueenDestination;
....
  • Action này sẽ được gọi khi mà quân tốt chạm tới hàng cờ cuối của phía đối diện. Mình sẽ update trong hàm Move của quân tốt:

public override void Move(Vector2Int newPos)
{
.....
    if(newPos.y == 0 || newPos.y == 7)
    {
        OnReachExchangeToQueenDestination?.Invoke(newPos, this);
    }
}
  • Ở hàm này, logic là nếu quân tốt chạm y bằng 0 hoặc y = 7 thì quân tốt sẽ được phong hậu, vì quân tốt không đi lùi nên quân đen chắc chắn sẽ chạm vào y = 0 còn quân trắng chắn chắn sẽ chạm y = 7 mà không bị lỗi khi quân trắng chạm y = 0 và ngược lại.

  • Sau đó để phong hậu thì mình sẽ vào class ChessSpawnManager để viết logic phong hậu và update hàm tạo quân tốt để gán chức năng phong hậu, mình update như sau:

public void OnPawnExchangeToQueen(Vector2Int exchangePos, Pawn pawn)
{
     pawn.OnReachExchangeToQueenDestination -= OnPawnExchangeToQueen;
 
     var queen = _chessPiece.GetChessByType(ChessName.Queen);
     InitGeneralChessPiece(new (int, int)[] { (exchangePos.x, exchangePos.y) }, queen, pawn.Skin);
 
     Destroy(pawn.gameObject);
}
  • Hàm này có logic là truyền vào vị trí của quân cờ, và quân tốt được phong hậu, sau đó sẽ unassign action phong hậu của quân tốt, sau đó sẽ tạo ra quân hậu ngay tại vị trí đó.

  • Sau đó ở hàm tạo quân tốt mình sẽ update như sau để assign action phong hậu:

ChessPiece pawn)
{
    for (int i = 0; i < 8; i++)
    {
        var whitePawn = Instantiate(pawn, allCells[i, 1].transform);
        var whitePawnInstant = whitePawn.GetComponent<Pawn>();
        whitePawnInstant.OnReachExchangeToQueenDestination += OnPawnExchangeToQueen;
        whitePawnInstant.InitChessPiece(ChessSkin.White, _boardManager);
        allCells[i, 1].SetChessPiece(whitePawnInstant); 
 
        var blackPawn = Instantiate(pawn, allCells[i, 6].transform);
        var blackPawnInstant = blackPawn.GetComponent<Pawn>();
        blackPawnInstant.OnReachExchangeToQueenDestination += OnPawnExchangeToQueen;
        blackPawnInstant.InitChessPiece(ChessSkin.Black, _boardManager);
        allCells[i, 6].SetChessPiece(blackPawnInstant); 
    }
}
  • Vậy là xong, mình chỉ cần save script lại và ra ngoài play và test thử tính năng.

  • Tốt đã vào vị trí, và phong hậu:

pawnmoveEnd.png

PawnToQueen.png

Tác giả: Nguyễn Minh Thuận & Nguyễn Văn Khánh