Lập trình game cờ vua - Bài 11 - Tối ưu và hoàn thiện game

Chúng mình đã cùng các bạn tạo ra được bàn cờ. Bài viết này sẽ hướng dẫn các bạn các bước cuối cùng để hoàn thiện trò chơiTối ưu hóa Về tối ưu hóa cho game, chúng ta có thể xem xét các vấn đề sau: Tối ưu hóa hiệu suất đồ họa, Tối ưu hóa ánh sáng, Tối ưu hóa vật lý, ...Build Game Bây giờ mình sẽ hướng dẫn các bạn build game ra file exe cho windows. Đầu tiên các bạn vào File/Build Settings. Tiếp theo mình sẽ nói sơ qua về giao diện build: (1) Đầu tiên là scenes in build, phần này mình có giới thiệu với các bạn vào phần trước rồi, đây là nơi quản lý scenes trong game, trong đây các bạn muốn thêm scene thì có thể mở scene đó xong chọn Add Open Scenes, và cũng có thể tùy chọn build scene nào thì chỉ cần tick vào ô vuông bên cạnh scene đó, còn nếu không cần build scene đó thì không tick vào là được. (2) Đây là khu vực chọn platform để build, ở đây mình đang chọn windows, tuy nhiên nếu muốn các bạn có thể ra ngoài cài thêm plugin để chuyển qua nền tảng khác như Android, Ios, WebGl, vân vân. (3) Đây là khu vực cài đặt trước khi build, ở Target Platform thì mình đang build Windows nên mặc định và cũng chỉ có 1 lựa chọn là Windows, tiếp theo là kiến trúc vi xử lý (CPU), ở đây máy mình là Intel 64 bit nên mặc định cũng là intel 64 bit, ngoài ra còn các option khác thì hiện tại các bạn chưa cần để ý. (4) Player settings, trong này sẽ có thêm các cài đặt liên quan tới player, nếu có cơ hội mình sẽ giới thiệu các bạn sau. (5) Phần này là 2 lựa chọn build khác nhau, nút Build đầu tiên là đê build ra file exe, còn Build And Run thì vừa build ra file exe, và còn tự động mở game sau khi build xong, bây giờ mình sẽ chọn Build bình thường. Mình nhấn vào nút Select Folder. Sau đó Unity sẽ build game ra, giờ thì mình chỉ cần chờ. Sau đó các bạn chọn file Chess2D.exe và xong các bạn đã có thể chơi. Vậy là chuỗi bài hướng dẫn lập trình game Chess2D của mình tới đây là kết thúc, qua chuỗi bài này mong rằng đã có thể cho các bạn nắm được cơ bản về Unity và cách viết 1 tựa game đơn giản như game cờ vua 2D, cũng từ đó có thể giúp các bạn có thể từng bước nắm rõ hơn về việc lập trình game và phát triển game. Github Project: https://github.com/ngminhthuan/Chess-2D-Unity.gitTác giả: Nguyễn Minh Thuận & Nguyễn Văn Khánh

Lập trình game cờ vua - Bài 10 - Tạo giao diện người dùng (UI) cho game

Tiếp phần trước hôm nay mình sẽ hướng dẫn các bạn xử lý lượt chơi và kết thúc ván cờ.1. Giới thiệu UI trong game UI là User Interface tức là giao diện người dùng, ở trong game giao diện người dùng là 1 phần rất quan trọng, UI trong game là hệ thống các yếu tố đồ họa và tương tác mà người chơi sử dụng để điều khiển hoặc nhận thông tin từ trò chơi. UI giúp người chơi hiểu rõ trạng thái hiện tại của game, thực hiện các hành động, và điều hướng qua các menu hoặc tùy chọn khác nhau. Hẳn các bạn có từng nghe về UI và UX, tuy nhiên 2 thứ này là 2 thứ riêng biệt, như mình nói ở trên UI là hệ thống hiển thị giao diện người dùng, giúp người chơi tương tác với game, còn UX thì là User Experience là về trải nghiệm của người chơi trong game từ cách chơi, cách thiết kế làm sao để thuận tiện cho người chơi thì đó là UX. Đó là 1 vài điều cơ bản về UI và UX, vì mình là 1 game dev nên chỉ giải thích sơ như vậy thôi, còn nếu ai có hứng thú với Game Design thì có thể lên mạng tìm hiểu thêm. 2. Canvas trong Unity Canvas trong Unity là 1 thành phần dùng để UI có thể được bố trí và hiển thị, và tất cả các UI element khác được tạo thì đều phải có Canvas đính kèm thì mới có thể hiển thị ra được. Nếu 1 đối tượng UI được tạo thì Canvas cũng sẽ được tự động tạo ra nếu trong scene chưa có đối tượng Canvas nào. Link tham khảo: Canvas | Unity UI | 1.0.0 3. UI element và các component hay dùng trong Unity Về các thành phần UI element có trong Unity thì về cơ bản mình sẽ có 1 bài loại sau đây: Ngoài ra đối với với UI element các bạn nên tải thêm package TMP Pro trong package manager về để sử dụng, vì các UI element trong pack này sẽ tốt hơn trong việc format chữ, kiểm soát layout trong UI,... Link tham khảo: TextMesh Pro Documentation | Unity UI | 2.0.0 4. Tạo Giao diện Scene Menu Ban đầu khi bắt đầu dự án, mình có tạo 2 scene, 1 là Game, 2 là Menu, scene Game mình đã làm bữa giờ, bây giờ mình sẽ chuyển scene Start để tạo Main Menu UI. Các bạn vào folder scene, chọn vào scene Start, nếu chưa các bạn chuột phải, Create/Scene và đặt tên là Start. - Trong scene Start, mình chọn chuột phải UI/Image, và Unity sẽ tự động tạo ra Canvas như này, cũng như thêm 1 object là Event System (Event System là hệ thống có sẵn trong Unity, hỗ trợ chúng ta trong việc tương tác với UI, link tham khảo: Event System | Unity UI | 1.0.0 và Unity - Manual: Event System ). Đặt tên gameobject Image mới tạo là GameMenu. Tiếp theo mình muốn tấm hình này sẽ bao phủ toàn bộ màn hình, mình chọn vào Anchor Presets ở góc trên bên trái của component RectTransform, sau đó mình nhấn nút ALt trên bàn phím và chọn lựa chọn sau đây, tấm hình sẽ tự động được đặt full màn hình: Kết quả là: Tiếp theo mình sẽ trang trí 1 chút cho game, mình sẽ thêm 1 cái title cho game, cũng như 1 tấm hình trang trí cho game cờ vua, cách thêm cũng tương tự như cách các bạn tạo object GameMenu tuy nhiên mình sẽ tạo nó là con của GameMenu, và title mình sẽ tạo Text – Textmeshpro chứ không phải hình: Mình đặt tên là GameTitleTxt, tiếp theo mình điền title game là CHESS 2D và đổi màu chữ thành màu đen, đặt size chữ cỡ 180 cho dễ nhìn, mình cũng căn lại lề cho title như sau: Tiếp theo mình cũng làm như GameMenu object nhưng mình chọn top center và set y position là -400: Tiếp theo mình thêm 1 empty gameobject đặt tên là GameIconContainer. Sau đó mình thêm 2 tấm hình làm gameobject con như sau: Sau đó ở black icon và white icon mình kéo 2 quân vua vào để trang trí như sau: Cái này thì tùy các bạn, vì đây chỉ là trang trí nên có thể làm như mình hoặc các bạn tự thiết kế theo ý các bạn. Tiếp theo các bạn tạo 1 button – textmeshpro theo cách tương tự, và làm con của GameMenu luôn sau đó các bạn cũng có thể tùy chọn size sao cho hợp mắt các bạn, hoặc cũng có thể đổi màu sao tùy các bạn, sao cho cuối cùng thì sẽ có 1 cái nút để có thể cho người chơi nhấn để vào game như sau: Tiếp theo mình sẽ viết logic để có thể chuyển scene, các bạn vào Script, tạo 1 folder con là UI, tạo script đặt tên là MainMenu, và gán script vừa tạo vào gameobject GameMenu. Sau đó các bạn vào File/Build Settings, các bạn chọn Add Open Scene để add scene Start và Game. Các bạn kéo scene Start lên trên như này: Sau đó các bạn Ctrl + S, sau đó vào script MainMenu vừa tạo. Các bạn đơn giản tạo 1 hàm như sau: public class MainMenu : MonoBehaviour { public void OnClickPlayBtn() { SceneManager.LoadScene(1); // Scene(1); } } Hàm này đơn giản là dùng SceneManager (Được Unity cung cấp sẵn để quản lý scene) rồi gọi hàm LoadScene sau đó truyền scene index vô với index ở đây là số đằng sau tên scene ở trong Build Setting lúc nãy các bạn thêm scene vào, ở đây ta có scene 1 tương đương GameScene. Sau đó các bạn vô lại editor, vào nút play, các bạn kéo xuống component Button, chọn dấu + chỗ OnClick, sau đó kéo GameMenu vào object như sau: Tiếp theo các bạn làm như sau: Các bạn kéo object GameMenu vào phần object, Cũng như bấm vào chỗ drop list, sẽ xổ xuống các lựa chọn, các bạn chọn MainMenu/OnClickPlayBtn, lúc này các bạn thành công cài đặt chức năng cho nút, lúc này các bạn play game từ đây, bấm play, game sẽ tự động chuyển scene vào trong Game scene/ 5. Tạo Giao diện scene Game Trong scen này mình cũng làm tương tự, tuy nhiên lúc này mình sẽ tạo object UI cha không phải là 1 tấm hình mà chỉ đơn giản là 1 gameobject trống thôi, mình làm như sau: Sau đó mình sẽ tạo ra 2 nút ở góc trên bên trái để có thể reload lại game, hoặc thoát ra main menu như sau, về sprite thì mình có cung cấp từ những bài đầu, các bạn có thể dùng sprite của mình, hoặc có thể tải trên mạng về các sprite mà các bạn thích. Sau đó mình thêm 2 cái text để hiển thị cho lượt chơi đang ở bên nào, vì hiện tại game của chúng ta chuyển lượt nhưng không có bất kỳ thứ gì cho người chơi biết rằng đã đổi lượt, mình thêm như sau: Tiếp theo mình làm luôn 1 cái popup nhỏ sẽ được gọi lên khi có 1 bên thắng như sau: Với popup này thì mình sẽ làm logic cho nó hiển thị người thắng, và 2 nút dùng để reload game hoặc thoát ra main menu như 2 nút đã làm ở trên, sau đó mình tắt popup này trên scene, vì mình sẽ gọi nó ở trong code sau. Tiếp theo các bạn lại vào folder Scripts/UI tạo script mang tên MainGameUI. public class MainGameUI : MonoBehaviour { [SerializeField] private TMP_Text _whiteTurnTxt; [SerializeField] private TMP_Text _blackTurnTxt; [SerializeField] private TMP_Text _winningTxt; [SerializeField] private GameObject _endGamePanel; public void ShowEndGameUI(ChessSkin chessSkin) { _endGamePanel.gameObject.SetActive(true); switch (chessSkin) { case ChessSkin.None: case ChessSkin.White: _winningTxt.text = "Black win!"; break; case ChessSkin.Black: _winningTxt.text = "White win!"; break; } } public void ChangeTurnTitle(ChessSkin turn) { switch (turn) { case ChessSkin.None: case ChessSkin.White: _whiteTurnTxt.gameObject.SetActive(true); _blackTurnTxt.gameObject.SetActive(false); break; case ChessSkin.Black: _whiteTurnTxt.gameObject.SetActive(false); _blackTurnTxt.gameObject.SetActive(true); break; } } public void OnClickRePlayGame() { SceneManager.LoadScene(1); } public void OnClickExitToMenu() { SceneManager.LoadScene(0); } } Trong class này mình tạo 4 trường để chứa các object cần thay đổi hoặc bật tắt như text hiển thị lượt chơi và text thông báo thắng thua cũng như là popup end game. Tiếp theo mình lại vào hàm Occupied trong ChessBoardCell gọi hàm KillOpponentKing này Tiếp theo mình thêm function có logic hiển thị end game, truyền vào parameter ở đây mình sẽ gọi khi quân vua bị ăn, nên là khi truyền vào chess skin sẽ là màu của bên thua, nên sẽ hiển thị bên ngược lại là thắng, và bật popup này lên. Tiếp theo là function chuyển lượt chơi, mình chỉ cần truyền vào thông tin lượt chơi tiếp theo là màu quân cờ, sau đó UI sẽ hiển thị text lượt chơi tương ứng. Tiếp theo là 2 hàm tương ứng cho 2 nút bên ngoài UI, là reload scene, mình cũng gọi SceneManager.LoadScene(1) để load lại scene Game, và LoadScene(0) để trở về scene Menu. Sau đó mình sẽ vào GameManager, để có thể gọi tới UI khi cần. ..... [SerializeField] private MainGameUI _mainGameUI; ..... Tiếp theo mình update cho hàm OnOnePlayerMoved(): private void OnOnePlayerMoved() { ..... _mainGameUI.ChangeTurnTitle(_currentGameTurn);..... } Gọi hàm ChangeTurnTitle() và truyển parameter vào. Tiếp theo mình update hàm EndGame, thay logic dùng debug.log lần trước bằng logic gọi tới hàm ShowEndGameUI(): private void EndGame(ChessSkin chessSkin) { _mainGameUI.ShowEndGameUI(chessSkin); OnClickOnCell -= OnPlayerClickOnCell; OnMoved -= OnOnePlayerMoved; } Đã xong, giờ các bạn hãy ra editor và kéo script MainGameUI kéo vào MainGamePanel, sau đó kéo các thành phần cần thiết vào như sau: Tiếp theo mình kéo ref MainGamePanel vào GameManager: Tắt object text BlackTurn vì mới vào lượt chơi ban đầu sẽ là của quân trắng: Sau đó mình nhấn play: Lượt Trắng: Lượt Đen: Đã xử lý xong lượt chơi, tiếp theo mình sẽ gán chức năng cho các nút như cách làm ban nãy với nút Play ở MainMenu, nút reload sẽ gán cho hàm OnClickRePlayGame() còn nút X sẽ gán cho hàm OnClickExitToMenu(). Làm tương tự với popup EndGame, sau đó mình sẽ play game lại. Tác giả: Nguyễn Minh Thuận & Nguyễn Văn Khánh

Lập trình game cờ vua - Bài 9 - Xử lý lượt chơi và Kết thúc ván cờ

Tiếp phần trước hôm nay mình sẽ hướng dẫn các bạn xử lý lượt chơi và kết thúc ván cờ.1. Xử lý lượt chơi Để xử lý lượt chơi, mình sẽ đưa logic này vào GameManager. .... private ChessPiece _selectedChessPiece; public Action OnMoved; .... private void InitGame() { .... OnMoved += OnOnePlayerMoved; .... } private void StartGame() { _currentGameTurn = ChessSkin.White; } .... private void EndGame(ChessSkin chessSkin) { .... OnMoved -= OnOnePlayerMoved; } private void OnPlayerClickOnCell(Vector2 currentChessPiecePos, ChessPiece chessPiece, Action<ChessPiece> suggestWaysForMove) { if (!LegitToSelect(chessPiece)) { _cellSelectedObj.SetActive(false); return; } .... } private bool LegitToSelect(ChessPiece chessPiece) { if (chessPiece.Skin != _currentGameTurn) return false; if (chessPiece.Skin != _currentGameTurn) return false; return true; } private void OnOnePlayerMoved() { _currentGameTurn = _currentGameTurn == ChessSkin.White ? ChessSkin.Black : ChessSkin.White; _selectedChessPiece = null; _cellSelectedObj.SetActive(false); } Ở logic update này mình tạo thêm biến chứa lượt chơi hiện tại là của quân trắng hay đen, và trong hàm OnPlayerClickCell và kiểm tra xem xem nếu chọn quân cờ đen nhưng nếu đang ở lượt bên trắng hoặc ngược lại thì sẽ không cho chọn. Tiếp theo là hàm OnOnePlayedMoved là hàm này mình sẽ dùng để xử lý sau khi người chơi đi 1 bước sẽ chuyển lượt qua cho người khác. Tiếp theo mình sẽ gọi action OnMoved từ hàm ClickToMoveChessPieceTo trong hàm ChessBoardCell khi người chơi click vào ô cờ và di chuyển quân cờ. private void ClickToMoveChessPieceTo() { if (_cellCanMove.activeSelf) { ..... _boardManager.GameManager.OnMoved?.Invoke(); } } Lúc này khi ra editor play thử các bạn sẽ thấy quân trắng đi trước quân đen đi sau, và cứ lần lượt đổi lượt, vậy là logic xử lý lượt chơi đến đây là xong, mình sẽ tiếp tục hướng dẫn các bạn logic kết thúc trò chơi. Logic Kết thúc ván cờ Chơi thì sẽ có thắng thua, mà đối với cờ vua, mất vua là thua, nên mình sẽ xử lý logic này bằng cách kiểm tra khi mà vua 1 bên mất thì bên kia sẽ thắng. Trước tiên mình cần phải update logic ăn quân trước. Mình sẽ update logic này trong script ChessBoardCell ..... private void OnMouseDown() { if (_currentChessPiece != null) { if (CanSelected()) { _boardManager.ResetAllBoardCell(); _boardManager.GameManager.OnClickOnCell?.Invoke(_cellPos, _currentChessPiece, _boardManager.SuggestWayForChessPiece); } } ClickToMoveChessPieceTo(); } private bool CanSelected() { var selectedChessPiece = _boardManager.GameManager.SelectedChessPiece; if (selectedChessPiece != null) { if (_currentChessPiece.Skin != selectedChessPiece.Skin) { return false; } } return true; } private void ClickToMoveChessPieceTo() { if (_cellCanMove.activeSelf) { _cellCanMove.SetActive(false); var chessPiece = _boardManager.GameManager.SelectedChessPiece; var selectedChessPieceTransform = chessPiece.transform; Occupied(chessPiece); _boardManager.RemoveChessPieceFromCell(chessPiece); _currentChessPiece = chessPiece; chessPiece.Move(new Vector2Int((int)_cellPos.x, (int)_cellPos.y)); selectedChessPieceTransform.parent = this.gameObject.transform; _boardManager.GameManager.OnMoved?.Invoke(); _boardManager.ResetAllBoardCell(); } } private void Occupied(ChessPiece newChessPiece) { if (_currentChessPiece != null) { if (newChessPiece.Skin != _currentChessPiece.Skin) { Destroy(_currentChessPiece.gameObject); } } } ..... Logic trong đây là khi quân cờ mới được đặt xuống ô cờ, mình sẽ kiểm tra 1 lần nữa là quân trong ô cờ đó khác màu với quân vừa được đặt xuống, mình sẽ gọi hàm Destroy để hủy gameobject quân cờ của ô cờ hiện tại, sau đó tiếp tục set quân cờ mới đặt tới vào ô cờ hiện tại. Tiếp theo mình update trong script GameManager thêm như sau: .... private void EndGame(ChessSkin chessSkin) { if (chessSkin == ChessSkin.White) Debug.Log("Black win"); else Debug.Log("White win"); .... } ..... public void KillOpponentKing(ChessPiece chessPiece) { var king = chessPiece.GetComponent<King>(); if (king != null) { EndGame(king.Skin); } } Logic ở đây là khi có 1 quân cờ bị ăn thì sẽ gọi tới hàm KillOpponentKing để kiểm tra xem có phải quân bị ăn là quân vua không, nếu phải thì gọi tới hàm EndGame, và truyền vào đây màu của quân vua, nếu là vua màu trắng thì bên thắng là bên đen sẽ log ra màn hình console, nếu vua màu đen thì là bên thắng là bên trắng. Tiếp theo mình lại vào hàm Occupied trong ChessBoardCell gọi hàm KillOpponentKing này private void Occupied(ChessPiece newChessPiece) { if (_currentChessPiece != null) { if (newChessPiece.Skin != _currentChessPiece.Skin) { _boardManager.GameManager.KillOpponentKing(_currentChessPiece); Destroy(_currentChessPiece.gameObject); } } } Sau khi hoàn thành mình sẽ lại vào editor play game và thử cho 1 bên ăn vua bên kia. Sau khi quân trắng ăn được vua bên đen thì log đã log ra thông báo bên trắng đã thắng. Tác giả: Nguyễn Minh Thuận & Nguyễn Văn Khánh

Lập trình game cờ vua - Bài 8 - Tạo và lập trình cho quân cờ (Phần 3)

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: 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: Sau đó ra ngoài và chơi thử, kết quả ta nhận được là: 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: Đố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. Kéo ref BoardManager vào CastlingControl Sau đó mình chạy game, và kết quả là: 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: Tác giả: Nguyễn Minh Thuận & Nguyễn Văn Khánh

Lập trình game cờ vua - Bài 7 - Tạo và lập trình cho quân cờ (Phần 2)

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 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

Lập trình game cờ vua - Bài 6 - Tạo và lập trình cho quân cờ (Phần 1)

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

Lập trình game cờ vua - Bài 5 - Tạo và lập trình bàn cờ vua (Phần 2)

Chúng mình đã cùng các bạn tạo ra được bàn cờ. 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 ô cờViết Script logic cho ô cờ Mình sẽ vào folder script, tạo folder ChessBoard, tạo script ChessBoardCell. using System.Collections; using System.Collections.Generic; using UnityEngine; public class ChessBoardCell : MonoBehaviour { [SerializeField] private GameObject _cellCanMove; private Vector2 _cellPos; private BoardManager _boardManager; 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; } } private void OnMouseDown() { } } Trong script này sẽ có 3 trường, 1 trường là gameobject _cellCanMove, 1 trường vector2 cellPos, còn lại là BoardManager. Tiếp theo là phương thức cài đặt cho ô cờ, trong phương thức này mình sẽ truyền parameter vị trí ô cờ, BoardManager vào các trường tương ứng, cũng như là gọi tới hàm tô màu ô cờ để giúp bàn cờ có màu caro xen kẽ thay vì 1 màu như hiện tại. Trong hàm FillColor mình sẽ có logic như sau, mình sẽ tính ra ô đang ở x chẵn hay lẻ và y chẵn hay lẻ, logic của ô màu đen đó là ô nằm ở cùng x y chẵn hoặc x y lẻ sẽ là màu đen, còn ô mà x y không cùng chẵn hoặc không cùng lẻ sẽ là màu trắng. Ngoài ra mình còn sử dụng thêm phương thức OnMouseDown() cho script này để gọi tới khi mà người chơi nhấn lên trên ô cờ. Mình sẽ import asset bên ngoài vào là sprite crosshair (mình có đính kèm các sprite sẽ dùng trong game ở dưới cuối bài), đầu tiên mình vẫn tạo folder con trong folder sprite đặt tên là Board, sau đó kéo sprite từ bên ngoài vào. Sau đó mình sẽ tạo 1 gameobject square trong prefab Cell đặt tên là CanMove, và sprite vừa import vào. Vì sprite nhỏ hơn ô cờ nên mình quyết định resize gameobject này và đổi màu sang màu xanh (các bạn có thể đổi màu qua bất kỳ màu nào các bạn muốn). Ẩn gameobject CanMove đi vì nó sẽ chỉ hiện khi chúng ta nhấn vào, nên không cần thiết lúc nào cũng hiện, kéo script ChessBoardCell vào gameobject Cell, cũng như kéo ref của object CanMove vào trường trong script ChessBoardCell. Đã xong giờ thì Ctrl+S để lưu prefab lại và vào lại script BoardManager để gọi hàm Setup() cell trong hàm CreateBoard(). Tiếp theo kéo prefab vừa tạo vào trường cellPrefab trong Inspector BoardManager. Lúc này các bạn có thể xóa Cell trên scene. Tiếp theo mình sẽ gọi hàm tạo bàn cờ từ GameManager. using System.Collections; using System.Collections.Generic; using UnityEngine; public class BoardManager : MonoBehaviour { //Creating a reference field for the cell prefab in the Unity Editor [SerializeField] private GameObject _cellPrefab; private ChessBoardCell[,] _allCells = new ChessBoardCell[8, 8]; // (1) private List<ChessBoardCell> _onSuggestCells; internal GameManager GameManager; public ChessBoardCell[,] AllCells { get => _allCells;} public void InitBoard(GameManager gameManager) { this.GameManager = gameManager; _onSuggestCells = new List<ChessBoardCell>(30); CreateBoard(); } private void CreateBoard() { //Creating the all the cells for (int x = 0; x < 8; x++) { for (int y = 0; y < 8; y++) { //Instantiating a new cell from the Cell Prefab GameObject newCell = Instantiate(_cellPrefab, transform); //Setting the position of the Cell newCell.transform.position = new Vector2(x, y); ////Setting up the Cells (2) _allCells[x, y] = newCell.GetComponent<ChessBoardCell>(); _allCells[x, y].Setup(new Vector2Int(x, y), this); } } } } (1) Mình tạo thêm 1 mảng 2 chiều của ChessBoardCell để dễ quản lý các ô cờ trong bàn cờ, khi cần chỉ cần vị trí của ô cờ để gọi ra. (2) Thêm logic thêm ô cờ vào trong mảng đã tạo, và gọi tới ô cờ mới thêm hàm Setup truyền vào vị trí của ô cờ và BoardManager. Viết Script logic chọn ô cờ Trước tiên các bạn vào prefab Cell trước đó, và thêm vào component BoxCollider2D. Các bạn chỉ cần thêm vào là đc, giải thích sơ qua thì trong hệ thống physics của Unity sẽ có collider và trigger để xử lý va chạm, thì trong Unity 2d và 3d sẽ có các component collider khác nhau, tùy vào trường hợp và hình dáng của 2d object hoặc 3d object mình sẽ chọn loại collider khác nhau, trong trường hợp này thì đây là hình vuông, nên mính sẽ chọn box collider 2d, và đối với hàm OnMouseDown muốn được gọi vào thì mình bắt buộc phải có box collider này. Các bạn có thể tham khảo thêm về Collider tại link này: Unity - Manual: Colliders và link này Unity - Scripting API: Collider Tiếp theo mình sẽ tạo ra 1 object là Select sẽ hiện ra khi người chơi click vào ô cờ sẽ hiện ra để cho người chơi biết rằng mình đã click vào ô cờ đó. Với object này mình cũng sẽ tạo như object CanMove trong prefab Cell và dùng sprite crosshair nhưng không đổi màu. Tiếp theo mình sẽ vào script GameManager thêm trường cho cellSelectedObject để có thể gọi tới cell selected object khi người chơi click vào 1 ô, và mình cũng tạo thêm action gán action này với hàm đặt vị trí cho cellSelectedObject để khi ô cờ được nhấn thì sẽ gọi qua action này tới và gọi tới hàm đặt vị trí này. using System.Collections; using System.Collections.Generic; using UnityEngine; public class GameManager: MonoBehaviour { //Creating a reference field for the cell prefab in the Unity Editor [SerializeField] private BoardManager_board; private GameObject _cellSelectedObj; public Action<Vector2> OnClickOnCell; private void InitGame() { _board.InitBoard(this); OnClickOnChessPiece += OnPlayerClickOnChessPiece; StartGame(); } private void StartGame() { } private void EndGame() { } private void OnPlayerClickOnChessPiece(Vector2 currentChessPiecePos, ChessPiece chessPiece, Action<ChessPiece> suggestWaysForMove) { _cellSelectedObj.SetActive(true); _cellSelectedObj.transform.position = currentChessPiecePos; } } Mình sẽ assign action với hàm OnPlayerClickOnCell, trong hàm này sẽ truyền vào vị trí của ô cờ được người chơi nhấn vào, đồng thời set active cho cellSelectedObj và đặt vị trí cellSelectedObj bằng với vị trí ô cờ được nhấn Tiếp theo mình sẽ vào script ChessBoardCell để update hàm OnMouseDown() như sau: private void OnMouseDown() { _boardManager.GameManager.OnClickOnCell?.Invoke(_cellPos); } Mình gọi tới GameManager thông qua boardManager để gọi tới action OnClickOnCell và truyền tham số cellPos vào. Giờ mình sẽ ra ngoài scene và kéo ref gameobject Select vào GameManager và chạy thử game. Kết quả là không có gì xảy ra, tại vì Select object đang nằm ở dưới Hierachy, trong game 2D và UI khi object cùng layer nằm ở trên thì sẽ được hiển thị đè lên object cùng layer nằm ở dưới. Nên lúc này mình có 2 hướng giải quyết, 1 là mình sẽ thêm 1 layer khác cho Select object, 2 là mình sẽ thay đổi order in layer của Select object, mình sẽ chọn thay đổi order in layer là 1, còn cell thì mình vẫn để là 0. Sau đó mình ẩn Select object trước khi chạy game vì Select object sẽ được set active trong lúc chạy, và mình chạy game, và đây là kết quả. Giờ mình sẽ ra ngoài scene và kéo ref gameobject Select vào GameManager và chạy thử game. Tác giả: Nguyễn Minh Thuận & Nguyễn Văn Khánh

Lập trình game cờ vua - Bài 4 - Tạo và lập trình bàn cờ vua (Phần 1)

Như bài trước, chúng mình đã giới thiệu các bạn cách tải và quản lý mã nguồn. 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ờViết Script Quản lý Game (GameManager) Chúng ta sẽ sử dụng lại script GameManager đã tạo trước đó và thêm vào để chạy được. using System; using System.Collections; using System.Collections.Generic; using UnityEngine; public class GameManager : MonoBehaviour { void Awake() { InitGame(); } private void InitGame() { // khởi chạy tất cả dữ liệu StartGame(); } private void StartGame() { // người chơi có thể bắt đầu chơi tại đây } private void EndGame() { // xử lý việc kết thúc game ở đây } } Mình sẽ viết khung cho script GameManager, ban đầu sẽ có 4 phương thức, Awake (cũng giống như start tuy nhiên awake sẽ được khởi chạy trước start) mình sẽ cho game khởi tạo dữ liệu game trước khi bắt đầu chơi nên mình sẽ viết thêm hàm Init và cho gọi trong Awake. Trong hàm Init sẽ gọi cho hàm StartGame(), đảm bảo việc khởi tạo dữ liệu game sẽ được hoàn thành trước khi người chơi có thể chơi game. Hàm StartGame sẽ gọi xử lý các logic trong lúc chơi game. Hàm EndGame sẽ xử lý sau khi kết thúc trò chơi, vd như gọi UI lên để thông báo ai là người thắng, đưa ra lựa chọn chơi lại hay thoát game. Viết script quản lý bàn cờ Tiếp theo cũng như bước tạo script quản lý game, thì mình sẽ tạo 1 script quản lý bàn cờ tương tự như trên và 1 gameobject BoardManager Chúng ta sẽ sử dụng lại script GameManager đã tạo trước đó và thêm vào để chạy được. using System.Collections; using System.Collections.Generic; using UnityEngine; public class BoardManager : MonoBehaviour { //Creating a reference field for the cell prefab in the Unity Editor [SerializeField] private GameObject _cellPrefab; internal GameManager GameManager; public void InitBoard(GameManager gameManager) { this.GameManager = gameManager; CreateBoard(); } private void CreateBoard() { //Creating the all the cells for (int x = 0; x < 8; x++) { for (int y = 0; y < 8; y++) { //Instantiating a new cell from the Cell Prefab GameObject newCell = Instantiate(_cellPrefab, transform); newCell.transform.position = new Vector2(x, y); } } } } Ở script này mình sẽ khởi tạo các ô cờ trong bàn cờ, mình cũng sẽ cần truy cập vào GameManager nên sẽ tạo 2 trường là gameobject cellPrefab và GameManager Trong script này mình cho biến cellPrefab là private vì mình chỉ cần sử dụng biến này ở trong phạm vi class này, tuy nhiên nếu sử dụng private thì trên editor sẽ không hiển thị lên Inspector mà mình thì cần sẽ kéo prefab của ô cờ vào trong này. Prefab các bạn có thể hiểu đơn giản đây là 1 gameobject được các bạn cài đặt, gán các component vào sau đó được các bạn lưu lại để có thể tái sử dụng. Link tham khảo: (Unity - Manual: Prefabs)[https://docs.unity3d.com/Manual/Prefabs.html] Tiếp tục với đoạn code của mình, mình sẽ tạo hàm InitBoard truyền parameter GameManager vào để gán cho trường GameManager ở trên, và mình sẽ gọi tới hàm tạo bàn cờ CreateBoard(). Hàm tạo bàn cờ thì mình có thể hiểu đây là 1 mảng 2 chiều, vậy nên mình sẽ cho chạy 2 vòng for mỗi vòng sẽ chạy từ 0 tới 8, tại sao lại là 8, vì bàn cờ của chúng ta sẽ có kích thước 8x8. Tiếp theo mình sẽ ra editor gán script vào gameobject BoardManager, sau đó mình sẽ kéo reference gameobject BoardManager vào GameManager. Sau đó thì mình sẽ tạo 1 gameobject 2d Sprite/Square Đặt tên gameobject là Cell, sau đó chọn màu của gameobject ở trong component Sprite Renderer, ở đây các bạn có thể kéo màu bằng hệ rgb 255 hoặc dùng hệ hex, ở đây mình sẽ dùng hệ hex với mã màu (E2D5A1) cho ô cờ màu trắng. Sau đó bạn kéo gameobject Cell vào trong folder Prefabs, à trước tiên hãy tạo 1 folder con trong Prefabs đặt tên là BoardCell, sau đó kéo gameobject Cell vừa tạo vào, vậy là các bạn đã có prefab Cell. Tiếp theo kéo prefab vừa tạo vào trường cellPrefab trong Inspector BoardManager. Lúc này các bạn có thể xóa Cell trên scene. Tiếp theo mình sẽ gọi hàm tạo bàn cờ từ GameManager. using System.Collections; using System.Collections.Generic; using UnityEngine; public class GameManager : MonoBehaviour { [SerializeField] private BoardManager _board; private void Awake() { InitGame(); } private void InitGame() { _board.InitBoard(this); StartGame(); } } Lúc này mình sẽ thử play game bằng cách nhấn vào biểu tượng play trong editor Unity Nếu như bàn cờ bị lệch thì chúng ta sẽ chỉnh sửa thông số (Size, Position,...) của Camera sao cho Camera chính diện với bàn cờ. Tác giả: Nguyễn Minh Thuận & Nguyễn Văn Khánh

Lập trình game cờ vua - Bài 3 - Tìm kiếm Assets, Khởi tạo Scene và quản lý mã nguồn

Như bài trước, chúng mình đã giới thiệu các bạn tổng quan về Unity về cách tải và giao diện Unity Editor. Với tiếp nối, thì bọn mình sẽ đem đến các bạn, cách để tìm kiếm Assets, khởi tạo scene và quản lý mã nguồn. 1. Chuẩn bị Assets: a) Tải Assets:Các bạn có thể tìm kiếm hình ảnh các quân cờ và bàn cờ trên các nền tảng cung cấp Assets nổi tiếng như Unity Assets Store hoặc Itch.io để tải và sử dụng. Lưu ý là các bạn nên tìm kiếm các assets cung cấp bản quyền (Licence) sử dụng và sửa chửa nhé.b) Đưa Assets vào Project:Sau khi các bạn đã chuẩn bị xong các hình ảnh quân cờ và bàn cờ, các bạn có thể import vào Unity bằng cách như sau: Cách 1: Import thông qua Package Manager của Unity (Window -> Package Manager). Cách 2: Kéo folder chứa Assets bạn cần vào thẳng Project. Lưu ý là các bạn nên bỏ trong thư mục Assets và tạo sắp xếp thư mục như bài trước. 2. Tạo Scene và Khởi tạo Script: a) Tạo Scene:Định nghĩa Scene:Đầu tiên là tạo scene, vậy thì phải hiểu scene là gì, scene ở đây là một thành phần cơ bản trong hệ thống của Unity. Scene đại diện cho một không gian làm việc hoặc môi trường trong game. Nó là nơi bạn tổ chức các đối tượng (objects), ánh sáng, camera, âm thanh, và các yếu tố khác tạo nên trò chơi hoặc ứng dụng.Nói về scene thì mình cũng sẽ nói sơ qua 1 chút về các hệ trục tọa độ trong Unity, thì Unity về cơ bản sẽ có 4 hệ trục tọa độ: Trục thế giới (World Coordinate System): là hệ trục 3D của thế giới trong Unity 3d, đây cũng là trục cố định và áp dụng cho tất cả các gameobject có trong 1 scene. Trục tọa độ màn hình (Screen Coordinate System): là hệ trục 2D được tính bằng pixel, được tính từ (0, 0) góc dưới bên trái màn hình và tính tới (Screen.width, Sceen.height), hệ trục này sẽ thay đổi tùy vào tỷ lệ / độ phân giải của màn hình của thiết bị. Trục GUI (GUI Coordinate System): hệ trục này khá tương tự hệ trục tọa độ màn hình chỉ khác là điểm bắt đầu (0, 0) được tính từ góc trên bên trái màn hình. Trục viewport (Viewport Coordinate System): hệ trục này cũng khá giống 2 trục màn hình và GUI, tuy nhiên nó không bị phụ thuộc vào tỷ lệ / độ phân giải của màn hình thiết bị, mà nó sẽ là cố định, hệ này sẽ tính từ điểm dưới bên trái màn hình (0, 0) tới điểm trên bên phải màn hình (1, 1), vd điểm (0.5, 0.5) luôn luôn ở giữa màn hình cho dù là thiết bị có độ phân giải là bao nhiêu. Hướng dẫn tạo Scene: Các bạn từ thư mục scene đã tạo trước đó, chuột phải vào sau đó chọn Create, chọn Scene, 1 scene mới sẽ được tạo. Mặc định Unity khi khởi tạo sẽ có 1 scene trống, tuy nhiên vì mình tính sẽ làm 2 scene, 1 để làm main menu, 1 cho game nên mình sẽ tạo thêm 1 scene đặt tên là Start và Game. b) Khởi tạo script: Tạm bỏ qua scene Start, mình sẽ tập trung vào scene Game trước, trước tiên mình sẽ hướng dẫn tạo gameobject (Gameobject là một đối tượng cơ bản trong mọi Scene - có thể hiển thị hoặc không hiển thị nhưng ở trong scene thì đều là gameobject, và là thành phần trung tâm của hệ thống Unity. GameObject có thể xem là 1 hộp chứa dùng để tổ chức và quản lý các thành phần (components), giúp tạo ra các đối tượng trong game hoặc ứng dụng. Tạo gameobject: Các bạn cũng bấm chuột phải vào cửa sổ Hierachy, chọn xuống create empty vậy là xong, 1 object empty đã được tạo, thì với gameobject này sẽ không có bất kỳ thành phần (component) nào khác ngoài Transform. Transform là gì: Thì về cơ bản nó là một component bắt buộc gắn với mọi GameObject. Transform xác định vị trí (position), góc xoay (rotation), và kích thước (scale) của GameObject trong không gian 3D hoặc 2D. Tiếp đến mình vào folder Scripts, chuột phải chọn Create/Folder, đặt tên folder là Managers để chứa tất cả script quản lý của game ở đây. Tiếp theo trong folder Managers mình tiếp tục chuột phải chọn Create/Scripts đặt tên script mới tạo là GameManager. Mở script lên, mặc định thì class của chúng ta sẽ được kế thừa lớp Monobehavior của Unity, và sẽ có 2 hàm mặc định là Start() và Update() như sau. using System.Collections; using System.Collections.Generic; using UnityEngine; public class GameManager : MonoBehaviour { // Start is called before the first frame update void Start() { } // Update is called once per frame void Update() { } } Mình sẽ nói sơ qua về những thứ mới ở đây, đầu tiên là MonoBehavior nó là 1 lớp cơ bản trong Unity, nếu muốn gắn script vào gameobject chúng ta bắt buộc phải kế thừa MonoBehavior. Monobehavior sẽ cung cấp cho chúng ta các phương thức và tính năng cần thiết để tương tác với gameobject, cũng như là vòng đời, quản lý sự kiện và điều khiển hành vi của gameobject trong Unity. Về vòng đời của script các bạn có thể tham khảo: Unity - Manual: Order of execution for event functions Còn về 2 phương thức mặc định là Start và Update thì về cơ bản, khi 1 gameobject được khởi chạy, thì script có hàm Start được gắn vào gameobject sẽ khởi chạy 1 lần, còn update sẽ được gọi xuyên suốt chương trình cho đến khi gameobject bị phá hủy, hàm update sẽ được gọi mỗi 1 lần mỗi frame (vì vậy nên hàm này cũng nên sử dụng 1 cách có tính toán vì nếu lạm dụng có thể gây nặng chương trình ảnh hưởng tới game) Tác giả: Nguyễn Minh Thuận & Nguyễn Văn Khánh

Lập trình game cờ vua - Bài 2 - Tổng quan về Unity và thiết lập dự án

Trước khi chúng ta đến với phần thiết lập dự án trong Unity, hãy cùng tìm hiểu qua về Unity Engine nhé.Tổng quan về Unity: Unity là một game engine phổ biến được phát triển bởi Unity Technologies, cho phép tạo trò chơi 2D, 3D và các ứng dụng tương tác trên nhiều nền tảng như PC, di động, máy chơi game, VR/AR và web. Với ngôn ngữ lập trình C Sharp, Unity dễ học và sử dụng, hỗ trợ lập trình viên qua giao diện trực quan và tài nguyên phong phú. Cộng đồng đông đảo và tài nguyên như Unity Asset Store giúp tiết kiệm thời gian phát triển. Unity miễn phí cho các nhà phát triển nhỏ và cá nhân, và đã được sử dụng để phát triển nhiều game nổi tiếng như Among Us và Monument Valley. 1. Cài đặt Unity Hub: Truy cập vào website: https://unity.com/download để tải Unity Hub. 2. Cài đặt Unity Editor: Sau khi cài đặt Unity Hub đăng nhập vào Unity Hub bằng tài khoản Unity, chưa có thì có thể tạo. Tìm và cài đặt phiên bản Unity 2022.3.10f1 LTS trong link: https://unity.com/releases/editor/whats-new/2022.3.10#installs và nhấn vào Install, trình duyệt sẽ hỏi cài đặt qua unity hub chọn ok và phiên bản unity 2022 sẽ tự động được chuyển qua unity hub để cài đặt. Lưu ý có thể chọn các phiên bản Unity mới hơn hoặc cũ hơn cũng được, tùy vào các bạn, nhưng nên sử dụng các phiên bản được đánh dấu là LTS (Long Term Support) là các phiên bản được hỗ trợ lâu dài, các phiên bản này thường được đánh giá là ổn định và ít lỗi hơn so với các phiên bản không được đánh dấu là LTS. Sau khi chuyển hướng qua Unity Hub để cài đặt bản unity, chọn vào option Visual Studio Community để cài đặt Visual Studio, hoặc nếu trên máy đã có sẵn các IDE hỗ trợ code C Sharp .Net sẵn rồi thì có thể bỏ qua. Đối với các option platfoms có thể bỏ qua nếu chỉ build windows, còn nếu mục tiêu của bạn là phát triển trên các nền tảng khác thì có thể chọn nền tảng tương ứng. Dự án chess 2d mình sẽ làm trên windows và sẽ build windows nên mình sẽ không chọn gì ở phần này và chọn Continue. Sau khi cài đặt xong phiên bản Unity, các bạn chọn vào add new để tạo dự án mới, nhập tên dự án: Chess, có các lựa chọn là 2D để làm game 2d, 3D để làm game 3D, VR để làm game vr, ngoài ra còn các sample project mà Unity cho sẵn, các bạn có thể tạo để vọc vạch sau, còn trong phạm vi dự án là chess 2d nên mình sẽ chọn project 2D. Chọn đường dẫn lưu dự án, tiếp tới chọn organization chọn tới organization của bạn chọn xong thì bấm add, project sẽ được tạo và mở lên tự động. Tổng quan về giao diện Unity Engine Giao diện Unity Engine về cơ bản có các khu vực như trên: (1) Đây là khu vực thanh công cụ, ở đây các bạn có thể thấy các menu về file, edit, asset, vv của Unity. (2) Đây là Hierachy là khu vực chứa scene (1 cảnh / 1 màn chơi) đang load, các gameobjects (các đối tượng làm nên 1 game hoàn chỉnh) trong scene. (3) Đây là khu vực scene cũng là khu vực mà các bạn sẽ tương tác, kéo thả các gameobjects. (4)Đây là cửa sổ game, tại đây tất cả những gì camera render sẽ được hiển thị tại đây. (5) Inspector sẽ hiển thị mọi thông tin về gameobjects đang làm việc một cách chi tiết, kể cả những Components được đính kèm và những thuộc tính của nó. (6) Cửa sổ project, tại đây chúng ta có thể xem được tất cả các assets có trong dự án (assets là các tài nguyên trong dự án gồm hình ảnh, âm thanh, file script, animation, vv) (7) Cửa sổ console đây là cửa sổ sẽ hiển thị các thông tin được log ra trong quá trình phát triển game. Cấu trúc thư mục và dự án Về cơ bản chúng ta có thể bỏ tất cả mọi thứ vào cửa sổ project mà không cần tới việc cấu trúc thư mục, tuy nhiên khi chúng ta mở rộng dự án hoặc cần phải sửa đổi nâng cấp về sau, việc có quá nhiều tài nguyên bỏ vào dự án mà không có bất kỳ cấu trúc nào sẽ gây ra 1 sự lộn xộn và chúng ta sẽ rất khó để có thể tìm tới tài nguyên mà chúng ta cần, và rất khó để quản lý tài nguyên. Vì vậy chúng ta cần phải cấu trúc thư mục của dự án ngay từ đầu, vd trong dự án này mình sẽ chia thư mục như sau: Về cơ bản mình sẽ chia các folder để chứa theo loại tài nguyên, vd: Hình ảnh sẽ có folder riêng và trong folder hình ảnh sẽ có folder nhỏ hơn chứa hình ảnh cho UI, hình ảnh cho game play, vv. Script code sẽ có folder riêng là scripts và sẽ có các folder nhỏ hơn như folder cho managers chứa các script manager, folder cho quân cờ chứa các script xử lý logic cho các quân cờ, vv. Với các tài nguyên khác cũng dùng chung nguyên tắc để tạo thư mục. Link tham khảo: https://unity.com/how-to/organizing-your-project Bài tiếp theo chúng ta sẽ bắt đầu vào phát triển dự án và lập trình cho bàn cờ. Tác giả: Nguyễn Minh Thuận & Nguyễn Văn Khánh

Lập trình game cờ vua - Bài 1 - Tổng quan dự án Chess Offline 2D trên Unity

Tổng quan dự án Chess Offline Unity 2D Mục tiêu của dự án Dự án game Chess Offline Unity 2D không chỉ là cơ hội để bạn tạo ra một trò chơi cờ vua đơn giản mà còn giúp bạn: Làm quen với Unity Engine Hiểu cách hoạt động của game engine, cách sử dụng Game Objects, Components, và các công cụ trong Unity. Nâng cao kỹ năng lập trình Áp dụng các khái niệm OOP (Lập trình Hướng Đối Tượng), cấu trúc dữ liệu và thuật toán để giải quyết các bài toán logic trong game. Tìm hiểu quy trình phát triển game Từ việc thiết lập dự án, thiết kế đồ họa, đến lập trình logic và tối ưu hóa sản phẩm. Dự án dành cho ai? Người mới bắt đầu với Unity muốn tìm hiểu cách tạo một game 2D đơn giản. Lập trình viên muốn cải thiện kỹ năng lập trình và tư duy giải thuật thông qua việc triển khai luật chơi cờ vua. Danh sách bài viết về chủ đề dự án Chess Offline Unity 2D: Tổng quan dự án game Chess Offline Unity 2D Tổng quan về Unity và thiết lập dự án. Tìm kiếm Assets, Khởi tạo Scene và quản lý mã nguồn Tạo và lập trình bàn cờ vua (Phần 1) Tạo và lập trình bàn cờ vua (Phần 2) Tạo và lập trình cho quân cờ (Phần 1) Tạo và lập trình cho quân cờ (Phần 2) Tạo và lập trình cho quân cờ (Phần 3) Xử lý lượt chơi và logic kết thúc trận đấu Tạo giao diện người dùng (UI) cho game Tối ưu hóa và hoàn thiện game Tác giả: Nguyễn Minh Thuận & Nguyễn Văn Khánh