Bà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 Script base cho quân cờ và tạo prefab quân cờ
Tiếp tục vào folder script, tạo folder ChessPiece, tạo script ChessPieces, đây là script base mình sẽ để là abstract class.
Và cũng trong script này mình sẽ xác định trước 2 logic là phương thức GetWays sẽ trả về các vị trí gợi ý mà quân cờ này có thể đi để sau này có thể làm hiển thị gợi ý ô cờ mà quân cờ có thể đi trong game, phương thức Move sẽ là phương thức để di chuyển quân cờ.
Ngoài ra cũng sẽ tạo 2 trường là white skin và black skin để gán Sprite trắng đen tương ứng cho quân cờ tùy vào quân trắng hay đen.
using System.Collections; using System.Collections.Generic; using UnityEngine; public abstract class ChessPiece : MonoBehaviour { [SerializeField] protected Sprite WhiteSkin; [SerializeField] protected Sprite BlackSkin; public abstract List<(int, int)> GetWays(); public abstract void Move(Vector2Int target); }
Tiếp theo mình sẽ tạo prefab các quân cờ
Trước tiên mình sẽ tạo folder ChessPiece trong folder Sprites, và import sprite các quân cờ vào.
Cài đặt bước đầu cho các sprite quân cờ, mình sẽ để sprite mode từ single sang multiple, việc chuyển sprite mode tại vì khi mình import vào là 1 tấm hình có các quân cờ ở trong chứ không phải từng con cờ riêng lẻ, nên mình cần phải cắt ra để có từng sprite quân cờ riêng lẻ. Sau đó mình chọn Sprite Editor.
Sau khi vào Sprite Editor sẽ có giao diện như sau:
Sau đó mình chọn Slice và ấn vào nút slice vì mặc định editor đã là automatic nên sẽ không cần mình phải cắt thủ công.
Mình chọn apply và thoát ra ngoài, lúc này các bạn có thể thấy rằng các quân cờ đã được tách ra như thế này.
Ta sẽ làm tương tự với quân cờ trắng.
Lúc này các bạn thấy rằng quân cờ chưa được rõ lắm, có vẻ hơi mờ, các bạn hãy chọn vô sprite lại và vào inspector của sprite chọn Filter Mode, chuyển từ Bilinear sang Point no filter.
Lúc này các sprite của các bạn sẽ trông rõ ràng hơn vì đã tắt filter.
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.
Các bạn không cần tạo 12 prefab quân cờ, mà chỉ cần 6 vì mình sẽ đổi màu quân cờ bằng script ở run time nên chỉ cần 6 prefab của từng loại quân cờ là đủ.
Các bạn cũng nên tạo folder con đặt tên ChessPieces trong folder Prefabs nhé.
2. Viết Logic cho khởi tạo quân cờ
Trong phần này mình sẽ tạo 1 game object ngoài scene là ChessSpawnManager và 1 script trong folder Managers là ChessSpawnManager.cs, tạo thêm 2 enum 1 để phân loại cờ, 1 để phân loại phe trắng hay đen.
public enum ChessName { None, Pawn, Knight, Bishop, Rook, Queen, King } public enum ChessSkin { None, White, Black }
Ngoài ra mình cũng sẽ giới thiệu thêm cho các bạn 1 class khác được Unity cũng cấp là ScriptableObject, các bạn có thể sử dụng ScriptableObject để lưu trữ dữ liệu, để làm config cho game, để chi tiết hơn các bạn có thể tham khảo link này: Unity - Manual: ScriptableObject và link này: Unity - Scripting API: ScriptableObject.
Mình sẽ tạo 1 script khác là ChessPieceSO để lưu dữ liệu về các quân cờ trong game và đặt trong folder SOs trong folder Scripts
[CreateAssetMenu(fileName = "ChessPieces", menuName = "ScriptableObjects/ChessPieces")] public class ChessPieceSO : ScriptableObject { [SerializeField] private List<ChessPieceModel> _chessPieces; internal ChessPiece GetChessByType(ChessName chessName) { foreach (var piece in _chessPieces) { if (piece.ChessPieceName == chessName) { return piece.ChessPiece; } } return null; } } [Serializable] public class ChessPieceModel { public ChessName ChessPieceName; public ChessPiece ChessPiece; }
Trong script này mình sẽ viết thêm class ChessPieceModel có 2 trường dữ liệu ChessPieceName và ChessPiece để lưu các dữ liệu về loại quân cờ và prefab quân cờ.
Trong class ChessPieceSO thì mình sẽ tạo 1 list cho ChessPieceModel và 1 hàm để lấy được prefab quân cờ khi truyền vào loại quân cờ.
Tiếp theo mình sẽ update script ChessPiece, thêm 2 function InitChessPiece và SetSkin để khởi tạo quân cờ và set màu của quân cờ, mình sẽ update script ChessPiece như sau:
public abstract class ChessPiece : MonoBehaviour { [SerializeField] protected Sprite WhiteSkin; [SerializeField] protected Sprite BlackSkin; protected BoardManager BoardManager; private ChessSkin _skin; public abstract List<(int, int)> GetWays(); public abstract void Move(Vector2Int target); internal void InitChessPiece(ChessSkin chessSkin, BoardManager boardManager) { SetSkin(chessSkin); SetBoardData(boardManager); } private void SetSkin(ChessSkin chessSkin) { _skin = chessSkin; var renderer = GetComponent<SpriteRenderer>(); switch (chessSkin) { case ChessSkin.White: renderer.sprite = WhiteSkin; break; case ChessSkin.Black: renderer.sprite = BlackSkin; break; default: renderer.sprite = WhiteSkin; break; } } private void SetBoardData(BoardManager boardManager) { BoardManager = boardManager; } }
Trong script này mình sẽ update thêm các trường BoardManager và _skin, với BoardManager thì mình cần để có thể viết các logic về sau, còn skin để lưu dữ liệu về màu của quân cờ.
Ngoài ra mình sẽ viết thêm hàm InitChessPiece với tham số truyền vào là màu quân cờ và GameManager, với hàm InitChessPiece vì tất cả quân cờ mình đều dùng chung 1 cách init data nên là mình sẽ viết chung vào script này.
Còn hàm SetSkin mình sẽ truyền vào dữ liệu màu của quân cờ để chương trình biết rằng quân cờ màu gì để lấy sprite tương ứng sau đó hiển thị ra ngoài game.
Cuối cùng là hàm SetBoardData mình chỉ dùng để gán giá trị BoardManager vào với trường dữ liệu đã tạo.
Tiếp theo mình update cho script ChessBoardCell như sau:
public class ChessBoardCell : MonoBehaviour { [SerializeField] private GameObject _cellCanMove; private Vector2 _cellPos; private BoardManager _boardManager; private ChessPiece _currentChessPiece; public void Setup(Vector2 position, BoardManager boardManager) { _cellPos = position; _boardManager = boardManager; FillColor(this.GetComponent<SpriteRenderer>(), (int) position.x, (int) position.y); } private void FillColor(SpriteRenderer cell, int x, int y) { bool oddY = (y % 2 != 0) ? true : false; bool oddX = (x % 2 != 0) ? true : false; if((oddY && oddX)||(!oddY && !oddX)) { cell.color = new Color32(120, 79, 72, 255); return; } } internal void SetChessPiece(ChessPiece piece) { _currentChessPiece = piece; } private void OnMouseDown() { _boardManager.GameManager.OnClickOnCell?.Invoke(_cellPos); } }
Mình thêm 1 trường dữ liệu ChessPiece để lưu quân cờ hiện tại đang ở trên ô cờ, ngoài ra mình thêm 1 hàm để làm việc gán giá trị quân cờ vào SetChessPiece với tham số truyền vào là quân cờ.
Ngoài ra trong script BoardManager mình cần tạo thêm 1 property để lấy giá trị ô cờ phục vụ cho việc khởi tạo các quân cờ nên mình tạo property AllCell:
public ChessBoardCell[,] AllCells { get => _allCells; }
Cuối cùng là mình sẽ viết script cho ChessSpawnManager:
public class ChessSpawnManager : MonoBehaviour { [SerializeField] private ChessPieceSO _chessPiece; private BoardManager _boardManager; public void InitChessSpawner(BoardManager board) { _boardManager = board; InitChessPieces(_boardManager.AllCells); } private void InitChessPieces(ChessBoardCell[,] allCells) { // Place Pawns var pawn = _chessPiece.GetChessByType(ChessName.Pawn); InitPawns(allCells, pawn); //// Place Rooks var rook = _chessPiece.GetChessByType(ChessName.Rook); var whiteRookCellsPos = new (int, int)[] { (0, 0), (7, 0) }; var blackRookCellsPos = new (int, int)[] { (0, 7), (7, 7) }; InitGeneralChessPiece(whiteRookCellsPos, rook, ChessSkin.White); InitGeneralChessPiece(blackRookCellsPos, rook, ChessSkin.Black); //// Place Knights var knight = _chessPiece.GetChessByType(ChessName.Knight); var whiteKnightCellsPos = new (int, int)[] { (1, 0), (6, 0) }; var blackKnightCellsPos = new (int, int)[] { (1, 7), (6, 7) }; InitGeneralChessPiece(whiteKnightCellsPos, knight, ChessSkin.White); InitGeneralChessPiece(blackKnightCellsPos, knight, ChessSkin.Black); //// Place Bishops var bishop = _chessPiece.GetChessByType(ChessName.Bishop); var whiteBishopCellsPos = new (int, int)[] { (2, 0), (5, 0) }; var blackBishopCellsPos = new (int, int)[] { (2, 7), (5, 7) }; InitGeneralChessPiece(whiteBishopCellsPos, bishop, ChessSkin.White); InitGeneralChessPiece(blackBishopCellsPos, bishop, ChessSkin.Black); //// Place Queens var queen = _chessPiece.GetChessByType(ChessName.Queen); InitGeneralChessPiece(new (int, int)[] { (3, 0) }, queen, ChessSkin.White); InitGeneralChessPiece(new (int, int)[] { (3, 7) }, queen, ChessSkin.Black); //// Place Kings var king = _chessPiece.GetChessByType(ChessName.King); InitGeneralChessPiece(new (int, int)[] { (4, 0) }, king, ChessSkin.White); InitGeneralChessPiece(new (int, int)[] { (4, 7) }, king, ChessSkin.Black); } private void InitPawns(ChessBoardCell[,] allCells, ChessPiece pawn) { for (int i = 0; i < 8; i++) { var whitePawn = Instantiate(pawn, allCells[i, 1].transform); whitePawn.GetComponent<ChessPiece>().InitChessPiece(ChessSkin.White, _boardManager); allCells[i, 1].SetChessPiece(whitePawn); // White pawns var blackPawn = Instantiate(pawn, allCells[i, 6].transform); blackPawn.GetComponent<ChessPiece>().InitChessPiece(ChessSkin.Black, _boardManager); allCells[i, 6].SetChessPiece(blackPawn); // Black pawns } } 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); chessPiece.GetComponent<ChessPiece>().InitChessPiece(skin, _boardManager); cell.SetChessPiece(chessPiece); // White pawns } } }
Logic của script này là mình sẽ tạo 1 trường chứa scriptableobject đã tạo chứa dữ liệu của các loại quân cờ trong game, và 1 trường sau có thể gọi tới BoardManager.
Hàm InitChessSpawner mình để public để GameManager sẽ gọi tới hàm này và truyền vào tham số BoardManager để set cho trường đã tạo ở trên.
Trong hàm InitChessSpawner đồng thời gọi tới hàm InitChessPieces với tham số truyền vào là các ô cờ trên bàn cờ.
Mình sẽ lấy ra từng prefab cho từng loại quân cờ trong scriptableobject đã tạo, và đối với từng loại quân cờ mình sẽ tạo ra từng mảng vị trí tương ứng cho quân đen và quân trắng, sau đó mình gọi hàm InitGeneralChessPiece đối với các quân tướng như xe mã tượng hậu vua, còn với quân tốt mình có viết hàm riêng cho nó là InitPawn, vì cách tạo quân tốt có chút khác so với các quân kia là quân tốt sẽ được tạo và dàn hàng hết vị trí thứ 1 và vị trí thứ 6 của bàn cờ, nên mình chỉ việc dùng 1 hàm for để chạy từ vị trí x0 – x7 của bàn cờ sau đó tạo quân tốt ở vị trí đó.
Tiếp theo mình vào class GameManager, cũng tạo 1 trường cho ChessSpawnManager và gọi ChessSpawnerManager.InitChessSpawner().
public class GameManager : MonoBehaviour { .... [SerializeField] private ChessSpawnManager _chessSpawnManager; .... private void InitGame() { _board.InitBoard(this); _chessSpawnManager.InitChessSpawner(_board); .... } ... }
Sau đó ra editor kéo ref cho ChessSpawnManager và GameManager.
Tuy nhiên lúc này chúng ta chưa thể play để thấy kết quả vì chúng ta chưa tạo scriptableobject và kéo vào cho ChessSpawnManager.
Các bạn hãy tạo 1 folder đặt tên là SOs hay ScriptableObjects tùy các bạn mình sẽ để SOs (Tạo ngoài folder Scripts vì đây mình sẽ tạo object assets lưu data chứ không phải scripts)
Tiếp theo các bạn chuột phải chọn ScriptableObject/ChessPieceSO, 1 object để chứa data sẽ được tạo, và các bạn hãy kéo nó vào trong trường ChessPieceSO của ChessSpawnManager.
Lúc này các bạn lại vào ChessPieceSO để kéo dữ liệu của các quân cờ vào. À nhưng chúng ta chỉ mới tạo prefab mà chưa kéo script cho các prefab quân cờ, mà mỗi quân cờ sẽ cần có class tương ứng để thực hiện hành vi tương ứng nên mình cần tạo script cho từng loại quân cờ trong game.
3. Tạo script cho từng loại con cờ trong game
Vì có tất cả là 6 loại con cờ, mình sẽ tạo tương ứng số script cho game lần lượt là Pawn, Rook, Knight, Bishop, Queen và King.
Tiếp theo mình sẽ vào từng script và cho các class này kế thừa lại class ChessPiece, sau đó override lại các hàm abstract như thế này:
public class Pawn : ChessPiece { public override List<(int, int)> GetWays() { throw new System.NotImplementedException(); } public override void Move(Vector2Int target) { throw new System.NotImplementedException(); } }
Tiếp theo mình sẽ kéo các script này vô prefab quân cờ tương ứng. Đồng thời kéo sprite tương ứng vào các trường skin.
Làm cho các quân cờ khác cũng tương tự.
Sau đó mình sẽ kéo các prefab này trong ChessPieceSO như sau:
Sau đó các bạn run game, lúc này cũng sẽ chưa thấy các quân cờ, các bạn vô prefab và chỉ layer in order lên là 2.
Các quân cờ có vẻ hơi nhỏ, mình sẽ vô prefab và scale lên tầm 3. Sau đó chạy lại game:
Tác giả: Nguyễn Minh Thuận & Nguyễn Văn Khánh