Lập trình game cờ vua - Bài 7 - Tạo và lập trình cho quân cờ (Phần 2)
Back To BlogsBài viết này sẽ hướng dẫn các bạn các bước tiếp theo tạo và lập trình các quân cờ
1. Viết Logic gợi ý đường đi cho quân cờ
Trước khi gợi ý đường đi cho quân cờ thì mình cần phải nâng cấp logic cho hàm của ô cờ.
Trong class ChessBoardCell mình update cho hàm OnMouseDown() thành như sau:
private void OnMouseDown() { if (_currentChessPiece != null) { _boardManager.GameManager.OnClickOnCell?.Invoke(_cellPos); } }
Logic này sẽ là chỉ khi ô cờ có quân cờ thì mới cho click vào.
Tiếp theo trong GameManager mình viết thêm 1 trường và 1 property để lưu lại quân cờ đang được chọn.
private ChessPiece _selectedChessPiece; public ChessPiece SelectedChessPiece => _selectedChessPiece;
Ngoài ra mình sẽ update action OnClickOnCell và hàm OnPlayerClickCell để có thể set được quân cờ hiện tại mà người chơi đang chọn và gọi callback để hiện ra nước đi gợi ý của quân cờ.
public Action<Vector2, ChessPiece, Action<ChessPiece>> OnClickOnCell; .... private void OnPlayerClickOnCell(Vector2 currentChessPiecePos, ChessPiece chessPiece, Action<ChessPiece> suggestWaysForMove) { _cellSelectedObj.SetActive(true); _cellSelectedObj.transform.position = currentChessPiecePos; _selectedChessPiece = chessPiece; suggestWaysForMove?.Invoke(chessPiece); }
Tiếp theo mình sẽ update cho BoardManager để bàn cờ có thể gọi tới các ô cờ cần để hiển thị chỉ dẫn. Mình update như sau:
.... private List<ChessBoardCell> _onSuggestCells; .... internal void SuggestWayForChessPiece(ChessPiece chessPiece) { List<(int, int)> suggestWays = chessPiece.GetWays(); foreach (var item in suggestWays) { var cell = _allCells[item.Item1, item.Item2]; cell.ShowCellCanMove(); _onSuggestCells.Add(cell); } } ....
Tiếp theo mình lại vào class BoardChessCell thêm 1 hàm:
internal void ShowCellCanMove() { _cellCanMove.SetActive(true); }
Logic thay đổi trên là khi có vị trí của các ô cờ cần hiển thị chỉ dẫn thì BoardManager sẽ gọi tới ô cờ với vị trí tương ứng để hiện ra object _cellCanMove tương ứng.
Tuy nhiên muốn hiển thị ra gợi ý thì chúng ta cần phải có input là vị trí các ô cờ cần hiển thị gợi ý, mà để lấy gợi ý chúng ta cần nâng cấp hàm GetWays của quân cờ, mà mỗi quân cờ thì sẽ di chuyển khác nhau nên ta sẽ cần viết ở trong từng script cho từng loại quân cờ.
Logic Di chuyển cho từng quân cờ
Trước tiên mình sẽ update cho class ChessPiece. Thêm các field và property sau:
protected Vector2Int CurrentPosition; public ChessSkin Skin => _skin; public Vector2Int Position => CurrentPosition;
Thêm hàm start và hàm SetPosition để set vị trí ban đầu khi quân cờ được khởi tạo:
.... private void Start() { SetPosition(new Vector2Int((int)transform.position.x, (int) transform.position.y)); } .... protected void SetPosition(Vector2Int newPosition) { CurrentPosition = newPosition; } ....
Tiếp theo mình thêm hàm kiểm tra xem vị trí quân cờ có còn trong bàn cờ không, dùng để kiểm tra khi tìm các bước đi của quân cờ có thể gợi ý.
protected bool IsInsideBoard(int x, int y) { return x >= 0 && x < 8 && y >= 0 && y < 8; }
Tiếp theo mình cần update cho ô cờ thêm hàm kiểm tra ô cờ ở vị trí gợi ý đang trống hay có quân cờ khác và quân cờ khác đó có phải kẻ địch không, mình update trong class ChessBoardCell như sau:
.... internal bool IsEmpty() => _currentChessPiece == null; internal bool HasEnemy(ChessSkin chessSkin) { if(IsEmpty()) return false; ChessPiece piece = _currentChessPiece; return piece.Skin != chessSkin; } ....
Tiếp theo mình sẽ update cho quân tốt. Trong script Pawn mình sẽ update như sau:
.... private bool _isFirstMove = true; .... public override List<(int, int)> GetWays() { List<(int, int)> moves = new List<(int, int)>(); int direction = Skin == ChessSkin.White ? 1 : -1; // White moves up, Black moves down // One step forward int forwardY = CurrentPosition.y + direction; if (IsInsideBoard(CurrentPosition.x, forwardY) && BoardManager.AllCells[CurrentPosition.x, forwardY].IsEmpty()) { moves.Add((CurrentPosition.x, forwardY)); if (_isFirstMove) { int twoStepsY = forwardY + 1 * direction; if (BoardManager.AllCells[CurrentPosition.x, twoStepsY].IsEmpty()) moves.Add((CurrentPosition.x, twoStepsY)); } } // Diagonal captures foreach (int dx in new[] { -1, 1 }) { int diagonalX = CurrentPosition.x + dx; int diagonalY = CurrentPosition.y + direction; if (IsInsideBoard(diagonalX, diagonalY) && BoardManager.AllCells[diagonalX, diagonalY].HasEnemy(Skin)) { moves.Add((diagonalX, diagonalY)); } } return moves; } ....
Trong class mình thêm trường isFirstMove và set bằng true, vì trong cờ vua quân tốt đi bước đầu có thể tiến 2 bước nên mình đặt 1 biến để check ở đây.
Hàm gợi ý có logic đơn giản là kiểm tra màu của quân cờ, nếu là trắng thì y + 1 còn đen thì là y – 1 để tiến lên.
Tiếp theo kiểm tra xem bước đi còn trong bàn cờ không nếu còn thì sẽ thêm vị trí đó vào list move được tạo ở trên, kiểm tra tiếp nếu là lần đi đầu thì sẽ cho đi thêm 1 ô, cuối cùng là kiểm tra xem 2 ô chéo trước mặt có kẻ địch không, nếu có thì cũng cho phép di chuyển chéo để ăn quân cờ của đối phương sau đó trả về list move để gợi ý đường đi cho người chơi.
Lúc này bạn tiếp tục tạo prefab quân cờ cho từng quân cờ, cách tạo cũng giống như tạo Cell prefab, các bạn tạo 2D object square sau đó duplicate ra 6 object đổi tên từng quân cờ tương ứng, sau đó kéo từng sprite vào quân cờ tương ứng.
Sau khi update xong mình ra editor và chơi thử. Và sẽ gặp lỗi.
Lỗi ở dòng 46 BoardManager
Kiểm tra lại là do mình chưa khởi tạo list _onSuggestCells nên mình sẽ update lại class này thêm 1 dòng khởi tạo list trong hàm InitBoard()
public void InitBoard(GameManager gameManager) { this.GameManager = gameManager; _onSuggestCells = new List<ChessBoardCell>(30); CreateBoard(); }
Ngoài ra thì object CanMove vẫn chưa set layer nên mình sẽ vô prefab Cell để update object CanMove layer lên 1
Sau đó mình chạy game và kết quả là hiển thị như ý muốn.
Tuy nhiên khi mình bấm thử các quân khác, lại lỗi:
Bây giờ mình cần phải reset lại các chỉ dẫn này khi gọi quân cờ khác, mình vô BoardManager để update thêm logic này. Nhưng để ẩn thì vẫn phải vào class ChessBoardCell để update thêm hàm ấn. Trong class ChessBoardCell mình thêm hàm:
internal void HideCellCanMove() { _cellCanMove.SetActive(false); }
Trong class BoardManager mình thêm hàm:
internal void ResetAllBoardCell() { if (_onSuggestCells.Count == 0) return; foreach (var cell in _onSuggestCells) { cell.HideCellCanMove(); } _onSuggestCells.Clear(); }
Tiếp theo mình sẽ gọi ResetAllBoardCell mỗi khi người chơi click ở trong hàm OnMouseDown của class ChessBoardCell
private void OnMouseDown() { if(_currentChessPiece != null) { _boardManager.ResetAllBoardCell(); _boardManager.GameManager.OnClickOnChessPiece?.Invoke(_cellPos, _currentChessPiece, _boardManager.SuggestWayForChessPiece); } }
Kết quả đã được như mong muốn, tiếp theo thì mình sẽ làm gợi ý tương tự cho các quân cờ xe, mã, tượng, hậu và vua.
Trước tiên để tiện lấy các ô cờ để tiện việc kiểm tra vị trí mình thêm hàm cho BoardManager:
public ChessBoardCell GetCell(int x, int y) { return this._allCells[x, y]; }
Với hàm trên mình chỉ cần truyền x, y tương ứng mình sẽ lấy được ô cờ tương ứng.
Tiếp tục công việc với các quân cờ khác trong game.
Trong class Rook:
Vector2Int[] directions = { new Vector2Int(1, 0), new Vector2Int(-1, 0), new Vector2Int(0, 1), new Vector2Int(0, -1) }; public override List<(int, int)> GetWays() { List <(int, int)> validMoves = new List<(int,int)>(); foreach (var direction in directions) { Vector2Int currentPos = CurrentPosition; while (true) { currentPos += direction; if (!IsInsideBoard(currentPos.x, currentPos.y)) break; var cell = BoardManager.GetCell(currentPos.x, currentPos.y); if (cell.HasEnemy(Skin)) { validMoves.Add((currentPos.x, currentPos.y)); break; } else if (cell.IsEmpty()) { validMoves.Add((currentPos.x, currentPos.y)); } else break; } } return validMoves; }
Logic ở đây là mình sẽ tạo 1 mảng cho quân xe chứa các hướng mà quân xe có thể đi, sau đó chạy 1 vòng for để kiểm tra các ô tiếp theo, theo hướng trong mảng có thể đi hay không, cách kiểm tra có thể đi hay không thì giống như quân tốt, sau đó thì trả về list các ô có thể đi logic cũng như quân tốt.
Trong class Knight:
Vector2Int[] directions = { new Vector2Int(2, 1), new Vector2Int(2, -1), // Two right, one up/down new Vector2Int(-2, 1), new Vector2Int(-2, -1), // Two left, one up/down new Vector2Int(1, 2), new Vector2Int(1, -2), // One right, two up/down new Vector2Int(-1, 2), new Vector2Int(-1, -2) // One left, two up/down }; public override List<(int, int)> GetWays() { List<(int, int)> validMoves = new List<(int, int)>(); foreach (var move in directions) { Vector2Int newPosition = CurrentPosition + move; if (IsInsideBoard(newPosition.x, newPosition.y)) { var cell = BoardManager.GetCell(newPosition.x, newPosition.y); if(cell.HasEnemy(Skin) || cell.IsEmpty()) { validMoves.Add((newPosition.x, newPosition.y)); } } } return validMoves; }
Trong class Bishop:
Vector2Int[] directions = { 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 override List<(int, int)> GetWays() { List <(int, int)> validMoves = new List<(int,int)>(); foreach (var direction in directions) { Vector2Int newPosition = CurrentPosition; while (true) { newPosition += direction; if (!IsInsideBoard(newPosition.x, newPosition.y)) break; var cell = BoardManager.GetCell(newPosition.x, newPosition.y); if (cell.HasEnemy(Skin)) { validMoves.Add((newPosition.x, newPosition.y)); break; } else if (cell.IsEmpty()) { validMoves.Add((newPosition.x, newPosition.y)); } else break; } } return validMoves; }
Trong class Queen:
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 override List<(int, int)> GetWays() { List<(int, int)> validMoves = new List<(int, int)>(); foreach (var direction in directions) { Vector2Int currentPos = CurrentPosition; while (true) { { currentPos += direction; if (!IsInsideBoard(currentPos.x, currentPos.y)) break; var cell = BoardManager.GetCell(currentPos.x, currentPos.y); if (cell.HasEnemy(Skin)) { validMoves.Add((currentPos.x, currentPos.y)); break; } else if (cell.IsEmpty()) { validMoves.Add((currentPos.x, currentPos.y)); } else break; } } } return validMoves; }
Trong class King:
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 override List<(int, int)> GetWays() { List<(int, int)> validMoves = new List<(int, int)>(); foreach (var direction in directions) { Vector2Int targetPos = CurrentPosition + direction; if (IsInsideBoard(targetPos.x, targetPos.y)) { var cell = BoardManager.GetCell(targetPos.x, targetPos.y); if (cell.HasEnemy(Skin) || cell.IsEmpty()) { validMoves.Add((targetPos.x, targetPos.y)); } } } return validMoves; }
Sau khi xong mình ra ngoài editor và play lại, kết quả là ngoài quân tốt thì chỉ có quân mã có gợi ý vì quân mã có thể nhảy qua quân tốt còn các quân khác thì bị chặn lại.
Để khắc phục vấn đề những con bị chặn có thể chọn được thì mình sẽ hẹn các bạn vào phần sau.
Tác giả: Nguyễn Minh Thuận & Nguyễn Văn Khánh