【Unity】MiniGame编辑器小游戏(十六)中国象棋局域网对战【Chinese Chess】(下)

更新日期:2026年5月20日。

项目源码:获取项目源码

索引

中国象棋【Chinese Chess】

本篇的目标是开发一个中国象棋【Chinese Chess】小游戏,可以与你的好同(ji)事(you)进行局域网对战。

一、游戏最终效果

Unity编辑器小游戏:中国象棋局域网对战

二、玩法简介

本游戏的玩法与标准中国象棋的规则一致,当轮到己方走棋时,通过鼠标左键点击选择棋子,然后点击棋盘位置移动棋子,当达到获胜或失败条件时,游戏结束。

三、正式开始

内容接上篇:【Unity】MiniGame编辑器小游戏(十五)中国象棋局域网对战【Chinese Chess】(上)

四、游戏棋局

1.开始游戏

房主成员玩家之间使用Socket(TCP协议)进行双向通信,在上文中双方已经建立了连接,那么接下来便可以按照事先约定的通信消息格式进行通信了。

①.开始游戏消息

开始游戏消息的格式如下(长度3字节):

第1字节 第2字节 第3字节
命令码:1 对手阵营 回车符(结束符)

注意:为了达到极简通信,消息内容中的信息大多数都使用一个字节表示(极大的降低通信带宽),且所有消息包均使用回车符结束(以避免粘包)。

成员玩家连接房主成功后,房主会发送开始游戏消息,然后双方会近乎同时进入游戏棋局:

csharp 复制代码
        /// <summary>
        /// 发送开始游戏消息(房主 → 成员)
        /// 开始游戏消息格式:命令[1byte]=1, 成员阵营[1byte], 结束符[1byte]=回车
        /// </summary>
        /// <param name="memberCamp">成员阵营</param>
        private async void SendMessage_StartGame(PieceCamp memberCamp)
        {
            byte[] data = new byte[3];
            data[0] = 1;
            data[1] = (byte)memberCamp;
            data[2] = 13;

            try
            {
                await _connectSocket.SendAsync(data, SocketFlags.None).ConfigureAwait(true);
            }
            catch (Exception)
            {
            	//对手断线,判定为对手弃子
                if (State == GameState.Playing)
                {
                    EndGame(true, "对手已弃子,恭喜你获胜!");
                }
            }
        }

成员玩家接收到开始游戏消息后,便会调用StartGame方法进入游戏棋局:

csharp 复制代码
        /// <summary>
        /// 接收开始游戏消息
        /// </summary>
        private void ReceiveMessage_StartGame(byte[] data)
        {
        	//此处接收到的消息包已自动剔除了结尾的回车符
            if (data.Length == 2 && data[0] == 1)
            {
                //开始游戏(参数为自身阵营)
                StartGame((PieceCamp)data[1]);
            }
        }

成员玩家在何处接收消息?还记得在上文中,双方连接成功后,启动了一个接收消息循环吗?

csharp 复制代码
        //启动接收消息循环
        ReceiveMessage();

这是一个异步的循环方法,除非停止循环条件被激活,否则他将持续接收消息:

csharp 复制代码
        /// <summary>
        /// 接收消息
        /// </summary>
        private async void ReceiveMessage()
        {
            _isReceiving = true;
            while (_isReceiving)
            {
                if (IsConnected)
                {
                    int count = 0;
                    try
                    {
                        count = await _connectSocket.ReceiveAsync(_receiveBuffer, SocketFlags.None).ConfigureAwait(true);
                    }
                    catch (Exception)
                    {
                    	//对手断线,判定为对手弃子
                        if (State == GameState.Playing)
                        {
                            EndGame(true, "对手已弃子,恭喜你获胜!");
                        }
                    }

                    for (int i = 0; i < count; i++)
                    {
                        byte b = _receiveBuffer[i];
                        //发现回车符,收到一个完整数据包
                        if (b == 13)
                        {
                            byte command = _receiveData.Count > 0 ? _receiveData[0] : (byte)0;
                            switch (command)
                            {
                            	//命令码:1,为开始游戏消息
                                case 1:
                                    ReceiveMessage_StartGame(_receiveData.ToArray());
                                    break;
                                default:
                                    break;
                            }
                            _receiveData.Clear();
                        }
                        else
                        {
                            _receiveData.Add(b);
                        }
                    }
                }
                else
                {
                    return;
                }
            }
        }
②.创建棋盘与棋子

进入游戏棋局后,会创建棋盘棋子

csharp 复制代码
        private void OnEnter_PlayingState()
        {
        	//创建棋盘
            _chessBoard = new ChessBoard(this);

			//创建32枚棋子
            _pieces.Add(new Piece(this, 1, _chessBoard, PieceCamp.Red, PieceType.Shuai, new Vector2Int(4, 0)));
            _pieces.Add(new Piece(this, 2, _chessBoard, PieceCamp.Red, PieceType.Shi, new Vector2Int(3, 0)));
            _pieces.Add(new Piece(this, 3, _chessBoard, PieceCamp.Red, PieceType.Shi, new Vector2Int(5, 0)));
            _pieces.Add(new Piece(this, 4, _chessBoard, PieceCamp.Red, PieceType.Xiang, new Vector2Int(2, 0)));
            _pieces.Add(new Piece(this, 5, _chessBoard, PieceCamp.Red, PieceType.Xiang, new Vector2Int(6, 0)));
            _pieces.Add(new Piece(this, 6, _chessBoard, PieceCamp.Red, PieceType.Ma, new Vector2Int(1, 0)));
            _pieces.Add(new Piece(this, 7, _chessBoard, PieceCamp.Red, PieceType.Ma, new Vector2Int(7, 0)));
            _pieces.Add(new Piece(this, 8, _chessBoard, PieceCamp.Red, PieceType.Che, new Vector2Int(0, 0)));
            _pieces.Add(new Piece(this, 9, _chessBoard, PieceCamp.Red, PieceType.Che, new Vector2Int(8, 0)));
            _pieces.Add(new Piece(this, 10, _chessBoard, PieceCamp.Red, PieceType.Pao, new Vector2Int(1, 2)));
            _pieces.Add(new Piece(this, 11, _chessBoard, PieceCamp.Red, PieceType.Pao, new Vector2Int(7, 2)));
            _pieces.Add(new Piece(this, 12, _chessBoard, PieceCamp.Red, PieceType.Bing, new Vector2Int(0, 3)));
            _pieces.Add(new Piece(this, 13, _chessBoard, PieceCamp.Red, PieceType.Bing, new Vector2Int(2, 3)));
            _pieces.Add(new Piece(this, 14, _chessBoard, PieceCamp.Red, PieceType.Bing, new Vector2Int(4, 3)));
            _pieces.Add(new Piece(this, 15, _chessBoard, PieceCamp.Red, PieceType.Bing, new Vector2Int(6, 3)));
            _pieces.Add(new Piece(this, 16, _chessBoard, PieceCamp.Red, PieceType.Bing, new Vector2Int(8, 3)));

            _pieces.Add(new Piece(this, 17, _chessBoard, PieceCamp.Black, PieceType.Shuai, new Vector2Int(4, 9)));
            _pieces.Add(new Piece(this, 18, _chessBoard, PieceCamp.Black, PieceType.Shi, new Vector2Int(3, 9)));
            _pieces.Add(new Piece(this, 19, _chessBoard, PieceCamp.Black, PieceType.Shi, new Vector2Int(5, 9)));
            _pieces.Add(new Piece(this, 20, _chessBoard, PieceCamp.Black, PieceType.Xiang, new Vector2Int(2, 9)));
            _pieces.Add(new Piece(this, 21, _chessBoard, PieceCamp.Black, PieceType.Xiang, new Vector2Int(6, 9)));
            _pieces.Add(new Piece(this, 22, _chessBoard, PieceCamp.Black, PieceType.Ma, new Vector2Int(1, 9)));
            _pieces.Add(new Piece(this, 23, _chessBoard, PieceCamp.Black, PieceType.Ma, new Vector2Int(7, 9)));
            _pieces.Add(new Piece(this, 24, _chessBoard, PieceCamp.Black, PieceType.Che, new Vector2Int(0, 9)));
            _pieces.Add(new Piece(this, 25, _chessBoard, PieceCamp.Black, PieceType.Che, new Vector2Int(8, 9)));
            _pieces.Add(new Piece(this, 26, _chessBoard, PieceCamp.Black, PieceType.Pao, new Vector2Int(1, 7)));
            _pieces.Add(new Piece(this, 27, _chessBoard, PieceCamp.Black, PieceType.Pao, new Vector2Int(7, 7)));
            _pieces.Add(new Piece(this, 28, _chessBoard, PieceCamp.Black, PieceType.Bing, new Vector2Int(0, 6)));
            _pieces.Add(new Piece(this, 29, _chessBoard, PieceCamp.Black, PieceType.Bing, new Vector2Int(2, 6)));
            _pieces.Add(new Piece(this, 30, _chessBoard, PieceCamp.Black, PieceType.Bing, new Vector2Int(4, 6)));
            _pieces.Add(new Piece(this, 31, _chessBoard, PieceCamp.Black, PieceType.Bing, new Vector2Int(6, 6)));
            _pieces.Add(new Piece(this, 32, _chessBoard, PieceCamp.Black, PieceType.Bing, new Vector2Int(8, 6)));
        }

2.棋盘

棋盘类ChessBoard继承至MiniGameObject,所以new一个该类对象便会自动加入到游戏窗口的绘制列表,然后只需在OnGUI方法中绘制棋盘即可:

csharp 复制代码
            public override void OnGUI()
            {
                base.OnGUI();

                GUI.color = Color.white;

                //绘制棋盘横线
                Rect rect = Rect.zero;
                for (int i = 0; i < 10; i++)
                {
                    rect.Set(LeftUpCoordinates.x, LeftUpCoordinates.y + GridSize * i - 1, Width, 2);
                    GUI.Box(rect, "", "WhiteBackground");
                }

                //绘制棋盘纵线
                float columnHeight = GridSize * 4;
                for (int i = 0; i < 9; i++)
                {
                    //绘制上方纵线
                    bool isBorder = (i == 0 || i == 8);
                    rect.Set(LeftUpCoordinates.x + GridSize * i - 1, LeftUpCoordinates.y, 2, isBorder ? (columnHeight + GridSize) : columnHeight);
                    GUI.Box(rect, "", "WhiteBackground");

                    //绘制下方纵线
                    rect.Set(LeftUpCoordinates.x + GridSize * i - 1, LeftUpCoordinates.y + GridSize * 5, 2, columnHeight);
                    GUI.Box(rect, "", "WhiteBackground");
                }

                //绘制上方九宫格斜线(士的路线)
                gameWindow.BeginRotateArea(UpNineGridCenter, 45);
                rect.Set(UpNineGridCenter.x - GridDiagonalSize, UpNineGridCenter.y - 1, GridDiagonalSize * 2, 2);
                GUI.Box(rect, "", "WhiteBackground");
                gameWindow.EndRotateArea();

                gameWindow.BeginRotateArea(UpNineGridCenter, -45);
                rect.Set(UpNineGridCenter.x - GridDiagonalSize, UpNineGridCenter.y - 1, GridDiagonalSize * 2, 2);
                GUI.Box(rect, "", "WhiteBackground");
                gameWindow.EndRotateArea();

                //绘制下方九宫格斜线(士的路线)
                gameWindow.BeginRotateArea(DownNineGridCenter, 45);
                rect.Set(DownNineGridCenter.x - GridDiagonalSize, DownNineGridCenter.y - 1, GridDiagonalSize * 2, 2);
                GUI.Box(rect, "", "WhiteBackground");
                gameWindow.EndRotateArea();

                gameWindow.BeginRotateArea(DownNineGridCenter, -45);
                rect.Set(DownNineGridCenter.x - GridDiagonalSize, DownNineGridCenter.y - 1, GridDiagonalSize * 2, 2);
                GUI.Box(rect, "", "WhiteBackground");
                gameWindow.EndRotateArea();

                //绘制上方炮位置的标记
                OnSignGUI(RightUpCoordinates + new Vector2(-GridSize, GridSize * 2), true, true);
                OnSignGUI(LeftUpCoordinates + new Vector2(GridSize, GridSize * 2), true, true);

                //绘制下方炮位置的标记
                OnSignGUI(RightDownCoordinates + new Vector2(-GridSize, -GridSize * 2), true, true);
                OnSignGUI(LeftDownCoordinates + new Vector2(GridSize, -GridSize * 2), true, true);

                //绘制上方兵位置的标记
                OnSignGUI(RightUpCoordinates + new Vector2(0, GridSize * 3), true, false);
                OnSignGUI(RightUpCoordinates + new Vector2(-GridSize * 2, GridSize * 3), true, true);
                OnSignGUI(RightUpCoordinates + new Vector2(-GridSize * 4, GridSize * 3), true, true);
                OnSignGUI(RightUpCoordinates + new Vector2(-GridSize * 6, GridSize * 3), true, true);
                OnSignGUI(RightUpCoordinates + new Vector2(-GridSize * 8, GridSize * 3), false, true);

                //绘制下方兵位置的标记
                OnSignGUI(RightDownCoordinates + new Vector2(0, -GridSize * 3), true, false);
                OnSignGUI(RightDownCoordinates + new Vector2(-GridSize * 2, -GridSize * 3), true, true);
                OnSignGUI(RightDownCoordinates + new Vector2(-GridSize * 4, -GridSize * 3), true, true);
                OnSignGUI(RightDownCoordinates + new Vector2(-GridSize * 6, -GridSize * 3), true, true);
                OnSignGUI(RightDownCoordinates + new Vector2(-GridSize * 8, -GridSize * 3), false, true);

                //绘制楚河、汉界
                rect.Set(LeftUpCoordinates.x + GridSize, LeftUpCoordinates.y + GridSize * 4, GridSize * 2, GridSize);
                GUI.Label(rect, "楚 河", _wordGS);
                rect.x = LeftUpCoordinates.x + GridSize * 5;
                GUI.Label(rect, "汉 界", _wordGS);

                GUI.color = Color.clear;

                //绘制所有棋子位置(不可见,但可点击)
                for (int i = 0; i < Poss.Count; i++)
                {
                    ChessBoardPos pos = Poss[i];
                    if (GUI.Button(pos.Transform, ""))
                    {
                        if (_gameWindow.PlayCamp == _gameWindow.SelfCamp && !_gameWindow.IsMovingPiece)
                        {
                            //点击棋子
                            if (pos.Piece != null)
                            {
                                //点击自身棋子:选中该棋子
                                if (pos.Piece.Camp == _gameWindow.SelfCamp)
                                {
                                    _gameWindow.CurrentSelectPiece = pos.Piece;
                                }
                                //点击对手棋子:尝试走棋
                                else
                                {
                                    _gameWindow.TryMovePiece(pos);
                                }
                            }
                            //点击空位置:尝试走棋
                            else
                            {
                                _gameWindow.TryMovePiece(pos);
                            }
                        }
                    }
                }

                GUI.color = Color.white;
            }

绘制棋盘比较简单,麻烦的地方就是的路径是斜线,这里通过BeginRotateAreaEndRotateArea方法来构建一个UI的旋转区域。

3.棋子位置

棋盘中的所有可落子点均由一个ChessBoardPos对象来描述,在创建棋盘时,所有位置便已跟随创建:

csharp 复制代码
        /// <summary>
        /// 棋盘上的棋子位置
        /// </summary>
        public class ChessBoardPos
        {
            /// <summary>
            /// 位置索引(红方视角:右下角[0,0],左上角[8,9])
            /// </summary>
            public Vector2Int Index { get; private set; }
            /// <summary>
            /// 位置(UI界面的真实位置坐标)
            /// </summary>
            public Rect Transform { get; private set; }
            /// <summary>
            /// 当前位置上停留的棋子
            /// </summary>
            public Piece Piece { get; set; }

            public ChessBoardPos(ChessBoard board, Vector2Int pos)
            {
                Index = pos;
                //棋盘是否需要翻转(当为黑方阵营时)
                if (board.IsFlip)
                {
                	//翻转后,位置索引【Index】所代表的真实坐标【Transform】将改变
                    Transform = new Rect(board.LeftUpCoordinates.x + board.GridSize * pos.x - board.PieceSizeHalf, board.LeftUpCoordinates.y + board.GridSize * pos.y - board.PieceSizeHalf, board.PieceSize, board.PieceSize);
                }
                else
                {
                    Transform = new Rect(board.RightDownCoordinates.x - board.GridSize * pos.x - board.PieceSizeHalf, board.RightDownCoordinates.y - board.GridSize * pos.y - board.PieceSizeHalf, board.PieceSize, board.PieceSize);
                }
                Piece = null;
            }
        }

在棋盘的OnGUI方法中已然看到了棋子位置的交互逻辑:

csharp 复制代码
                //绘制所有棋子位置(不可见,但可点击)
                for (int i = 0; i < Poss.Count; i++)
                {
                    ChessBoardPos pos = Poss[i];
                    if (GUI.Button(pos.Transform, ""))
                    {
                    	//当为自身走棋时,且没有棋子正在移动中
                        if (_gameWindow.PlayCamp == _gameWindow.SelfCamp && !_gameWindow.IsMovingPiece)
                        {
                            //点击棋子
                            if (pos.Piece != null)
                            {
                                //点击自身棋子:选中该棋子
                                if (pos.Piece.Camp == _gameWindow.SelfCamp)
                                {
                                    _gameWindow.CurrentSelectPiece = pos.Piece;
                                }
                                //点击对手棋子:尝试走棋
                                else
                                {
                                    _gameWindow.TryMovePiece(pos);
                                }
                            }
                            //点击空位置:尝试走棋
                            else
                            {
                                _gameWindow.TryMovePiece(pos);
                            }
                        }
                    }
                }

4.棋子

然后是棋子类Piece,其同样继承至MiniGameObject

csharp 复制代码
        /// <summary>
        /// 棋子
        /// </summary>
        public class Piece : MiniGameObject
        {
            /// <summary>
            /// 棋子ID(一个byte,用于双方通信时定位到同一棋子)
            /// </summary>
            public byte ID { get; private set; }
            /// <summary>
            /// 棋子阵营
            /// </summary>
            public PieceCamp Camp { get; private set; }
            /// <summary>
            /// 棋子类型
            /// </summary>
            public PieceType Type { get; private set; }
            /// <summary>
            /// 棋子所在的位置
            /// </summary>
            public ChessBoardPos Pos
            {
                get
                {
                    return _pos;
                }
                set
                {
                    _pos = value;
                    transform = _pos != null ? _pos.Transform : new Rect(10000, 10000, 20, 20);
                }
            }
            /// <summary>
            /// 棋子的显示名称
            /// </summary>
            public string ShowName 
            {
                get
                {
                    return _typeName;
                }
            }

            private MiniGame_ChineseChess _gameWindow;
            private ChessBoard _board;
            private ChessBoardPos _pos;
            private string _typeName;
            private Color _typeColor;

            public Piece(MiniGameWindow window, byte id, ChessBoard board, PieceCamp camp, PieceType type, Vector2Int pos) : base(window, Rect.zero)
            {
            	//棋子无需碰撞效果
                collider = false;

                _gameWindow = window as MiniGame_ChineseChess;
                _board = board;
                switch (type)
                {
                    case PieceType.Shuai:
                        _typeName = camp == PieceCamp.Red ? "帅" : "将";
                        break;
                    case PieceType.Shi:
                        _typeName = "士";
                        break;
                    case PieceType.Xiang:
                        _typeName = "相";
                        break;
                    case PieceType.Ma:
                        _typeName = "马";
                        break;
                    case PieceType.Che:
                        _typeName = "车";
                        break;
                    case PieceType.Pao:
                        _typeName = "炮";
                        break;
                    case PieceType.Bing:
                        _typeName = "兵";
                        break;
                }
                _typeColor = camp == PieceCamp.Red ? Color.red : Color.black;

                ID = id;
                Camp = camp;
                Type = type;
                Pos = _board.GetChessBoardPos(pos);
                Pos.Piece = this;
            }
            public override void OnGUI()
            {
                base.OnGUI();

                GUI.backgroundColor = _typeColor;
                //当选中棋子时,绘制为黄色
                GUI.contentColor = _gameWindow.CurrentSelectPiece == this ? Color.yellow : Color.white;

                GUI.Button(transform, _typeName);

                GUI.backgroundColor = Color.white;
                GUI.contentColor = Color.white;
            }
        }

5.走棋逻辑

最核心的便是走棋逻辑了,通过上文代码可以看到,要触发走棋,会调用TryMovePiece方法尝试走棋:

csharp 复制代码
        /// <summary>
        /// 当前选中的棋子
        /// </summary>
        public Piece CurrentSelectPiece { get; private set; }

        /// <summary>
        /// 尝试向目标位置走棋(目标位置不存在己方棋子)
        /// </summary>
        /// <param name="pos">目标位置</param>
        private void TryMovePiece(ChessBoardPos pos)
        {
            //如果当前未选中棋子,走棋失败
            if (CurrentSelectPiece == null)
                return;

            if (IsCanMove(CurrentSelectPiece, pos))
            {
                //走棋开始
                MovePiece(CurrentSelectPiece, pos, (obj) =>
                {
                    //走棋结束,切换为对手走棋,并通知对手
                    PlayCamp = SelfCamp == PieceCamp.Red ? PieceCamp.Black : PieceCamp.Red;
                    SendMessage_MovePiece(obj);
                });
            }
        }
①.走棋条件判断

IsCanMove方法用于走棋条件判断,其返回true代表可以走棋:

csharp 复制代码
        /// <summary>
        /// 棋子是否能够抵达目标位置
        /// </summary>
        /// <param name="piece">棋子</param>
        /// <param name="pos">目标位置</param>
        private bool IsCanMove(Piece piece, ChessBoardPos pos)
        {
            bool isCanGoToPos = false;
            switch (piece.Type)
            {
                case PieceType.Shuai:
                    isCanGoToPos = IsCanMove_Shuai(piece, pos);
                    break;
                case PieceType.Shi:
                    isCanGoToPos = IsCanMove_Shi(piece, pos);
                    break;
                case PieceType.Xiang:
                    isCanGoToPos = IsCanMove_Xiang(piece, pos);
                    break;
                case PieceType.Ma:
                    isCanGoToPos = IsCanMove_Ma(piece, pos);
                    break;
                case PieceType.Che:
                    isCanGoToPos = IsCanMove_Che(piece, pos);
                    break;
                case PieceType.Pao:
                    isCanGoToPos = IsCanMove_Pao(piece, pos);
                    break;
                case PieceType.Bing:
                    isCanGoToPos = IsCanMove_Bing(piece, pos);
                    break;
                default:
                    isCanGoToPos = false;
                    break;
            }
            return isCanGoToPos;
        }
        private bool IsCanMove_Shuai(Piece piece, ChessBoardPos pos)
        {
            if (piece.Camp == PieceCamp.Red)
            {
                if (_chessBoard.IsInRedNineGrid(pos))
                {
                    if (_chessBoard.ManhattanDistance(piece.Pos, pos) == 1)
                        return true;
                }
                else if (_chessBoard.IsInBlackNineGrid(pos))
                {
                    if (pos.Piece != null && pos.Piece.Type == PieceType.Shuai && _chessBoard.PieceNumberBetweenTwoPos(piece.Pos, pos) == 0)
                        return true;
                }
            }
            else
            {
                if (_chessBoard.IsInBlackNineGrid(pos))
                {
                    if (_chessBoard.ManhattanDistance(piece.Pos, pos) == 1)
                        return true;
                }
                else if (_chessBoard.IsInRedNineGrid(pos))
                {
                    if (pos.Piece != null && pos.Piece.Type == PieceType.Shuai && _chessBoard.PieceNumberBetweenTwoPos(piece.Pos, pos) == 0)
                        return true;
                }
            }
            return false;
        }
        private bool IsCanMove_Shi(Piece piece, ChessBoardPos pos)
        {
            if (piece.Camp == PieceCamp.Red)
            {
                return _chessBoard.IsInRedNineGrid(pos) && _chessBoard.IsDiagonalOfOneGrid(piece.Pos, pos);
            }
            else
            {
                return _chessBoard.IsInBlackNineGrid(pos) && _chessBoard.IsDiagonalOfOneGrid(piece.Pos, pos);
            }
        }
        private bool IsCanMove_Xiang(Piece piece, ChessBoardPos pos)
        {
            bool isCan = false;
            if (piece.Camp == PieceCamp.Red)
            {
                isCan = _chessBoard.IsInRedRange(pos) && _chessBoard.IsDiagonalOfFourGrid(piece.Pos, pos);
            }
            else
            {
                isCan = _chessBoard.IsInBlackRange(pos) && _chessBoard.IsDiagonalOfFourGrid(piece.Pos, pos);
            }

            if (isCan)
            {
                ChessBoardPos centerPos = _chessBoard.GetChessBoardPos(new Vector2Int(piece.Pos.Index.x + (pos.Index.x - piece.Pos.Index.x) / 2, piece.Pos.Index.y + (pos.Index.y - piece.Pos.Index.y) / 2));
                if (centerPos != null && centerPos.Piece == null)
                {
                    return true;
                }
            }
            return false;
        }
        private bool IsCanMove_Ma(Piece piece, ChessBoardPos pos)
        {
            bool isCan = _chessBoard.IsDiagonalOfTwoGrid(piece.Pos, pos, out int direction);
            if (isCan)
            {
                ChessBoardPos obstaclePos = null;
                if (direction == 1) obstaclePos = _chessBoard.GetChessBoardPos(new Vector2Int(piece.Pos.Index.x, piece.Pos.Index.y + 1));
                else if (direction == 2) obstaclePos = _chessBoard.GetChessBoardPos(new Vector2Int(piece.Pos.Index.x, piece.Pos.Index.y - 1));
                else if (direction == 3) obstaclePos = _chessBoard.GetChessBoardPos(new Vector2Int(piece.Pos.Index.x + 1, piece.Pos.Index.y));
                else if (direction == 4) obstaclePos = _chessBoard.GetChessBoardPos(new Vector2Int(piece.Pos.Index.x - 1, piece.Pos.Index.y));

                if (obstaclePos != null && obstaclePos.Piece == null)
                {
                    return true;
                }
            }
            return false;
        }
        private bool IsCanMove_Che(Piece piece, ChessBoardPos pos)
        {
            int number = _chessBoard.PieceNumberBetweenTwoPos(piece.Pos, pos);
            return number == 0;
        }
        private bool IsCanMove_Pao(Piece piece, ChessBoardPos pos)
        {
            int number = _chessBoard.PieceNumberBetweenTwoPos(piece.Pos, pos);
            return (number == 0 && pos.Piece == null) || (number == 1 && pos.Piece != null);
        }
        private bool IsCanMove_Bing(Piece piece, ChessBoardPos pos)
        {
            if (piece.Camp == PieceCamp.Red)
            {
                if (_chessBoard.IsInRedRange(piece.Pos))
                {
                    return _chessBoard.ManhattanDistance(piece.Pos, pos) == 1 && pos.Index.y > piece.Pos.Index.y;
                }
                else
                {
                    return _chessBoard.ManhattanDistance(piece.Pos, pos) == 1 && pos.Index.y >= piece.Pos.Index.y;
                }
            }
            else
            {
                if (_chessBoard.IsInBlackRange(piece.Pos))
                {
                    return _chessBoard.ManhattanDistance(piece.Pos, pos) == 1 && pos.Index.y < piece.Pos.Index.y;
                }
                else
                {
                    return _chessBoard.ManhattanDistance(piece.Pos, pos) == 1 && pos.Index.y <= piece.Pos.Index.y;
                }
            }
        }

这里的走棋条件判断严格遵循中国象棋的规则,代码较多但比较简单所以不再细说,在另一篇文章中有细节描述,两者的判断规则一致:【Unity】中国象棋联网对战游戏开发(三)对弈流程(下)

②.开始走棋

当走棋条件判断通过后,便开始走棋,播放走棋动画:

csharp 复制代码
        /// <summary>
        /// 上一次移动的棋子
        /// </summary>
        public Piece LastMovePiece { get; private set; }
        /// <summary>
        /// 上一次移动棋子的旧位置
        /// </summary>
        public ChessBoardPos LastMovePiecePos { get; private set; }
        /// <summary>
        /// 上一次移动棋子的坐标法记录
        /// </summary>
        public string LastMoveRecord { get; private set; }

        /// <summary>
        /// 将一个棋子走到目标位置(目标位置不存在己方棋子)
        /// </summary>
        /// <param name="piece">棋子</param>
        /// <param name="pos">目标位置</param>
        /// <param name="onEnd">走棋结束回调</param>
        private void MovePiece(Piece piece, ChessBoardPos pos, Action<Piece> onEnd = null)
        {
        	//标记为走棋中,此时点击其他棋子不再交互
            IsMovingPiece = true;
            //DoMove:为一个MiniGameObject对象播放移动(无物理效果)动画
            piece.DoMove(pos.Transform.position, 0.5f, (obj) =>
            {
            	//记录上一次走棋的棋子、走棋位置、走棋的坐标法记录
                LastMovePiece = piece;
                LastMovePiecePos = piece.Pos;
                LastMoveRecord = GetMoveRecord(LastMovePiece, pos);

                //目标位置存在棋子(必定为对手棋子),则吃子
                if (pos.Piece != null)
                {
                    Piece opponent = pos.Piece;
                    opponent.activeSelf = false;
                    opponent.Pos = null;
                    pos.Piece = null;

                    if (opponent.Type == PieceType.Shuai)
                    {
                        //自身帅阵亡,判负
                        if (opponent.Camp == SelfCamp)
                        {
                            EndGame(false, $"我方【{opponent.ShowName}】阵亡,很遗憾你输了!");
                        }
                        //对手帅阵亡,判胜
                        else
                        {
                            EndGame(true, $"对手【{opponent.ShowName}】阵亡,恭喜你获胜!");
                        }
                    }

                    LastMoveRecord += $"、吃【{opponent.ShowName}】";
                }

                //当前棋子走到目标位置
                Piece self = obj as Piece;
                self.Pos.Piece = null;
                self.Pos = pos;
                pos.Piece = self;

                IsMovingPiece = false;
                onEnd?.Invoke(self);
            });
        }
③.走棋消息

走棋完毕后,会发送走棋消息给对手,走棋消息的格式如下(长度5字节):

第1字节 第2字节 第3字节 第4字节 第5字节
命令码:2 走棋棋子ID 走棋位置x坐标 走棋位置y坐标 回车符(结束符)
csharp 复制代码
        /// <summary>
        /// 发送走棋消息(房主 ←→ 成员)
        /// 走棋消息格式:命令[1byte]=2, 走棋棋子ID[1byte], 走棋位置x坐标[1byte], 走棋位置y坐标[1byte], 结束符[1byte]=回车
        /// </summary>
        /// <param name="piece">走棋棋子</param>
        private async void SendMessage_MovePiece(Piece piece)
        {
            byte[] data = new byte[5];
            data[0] = 2;
            data[1] = piece.ID;
            data[2] = (byte)piece.Pos.Index.x;
            data[3] = (byte)piece.Pos.Index.y;
            data[4] = 13;

            try
            {
                await _connectSocket.SendAsync(data, SocketFlags.None).ConfigureAwait(true);
            }
            catch (Exception)
            {
            	//对手断线,判定为对手弃子
                if (State == GameState.Playing)
                {
                    EndGame(true, "对手已弃子,恭喜你获胜!");
                }
            }
        }

对手接收到走棋消息后,会重现走棋过程(将走棋棋子移动到目标位置):

csharp 复制代码
        /// <summary>
        /// 接收走棋消息
        /// </summary>
        private void ReceiveMessage_MovePiece(byte[] data)
        {
            if (data.Length == 4 && data[0] == 2)
            {
                //收到对手走棋消息,切换为自己走棋
                PlayCamp = SelfCamp;
                //并在本地重现对手走棋过程
                Piece piece = _pieces.Find(p => p.ID == data[1]);
                ChessBoardPos pos = _chessBoard.GetChessBoardPos(new Vector2Int(data[2], data[3]));
                MovePiece(piece, pos);
            }
        }

6.倒计时

当切换走棋阵营后,比如自身切换到对手,对手切换到自身,都会重置走棋步时,这里我们不设计局时,步时为90秒,当任意方超时则直接判负:

csharp 复制代码
        /// <summary>
        /// 当前走棋阵营
        /// </summary>
        public PieceCamp PlayCamp 
        {
            get
            {
                return _playCamp;
            }
            private set
            {
                _playCamp = value;
                //切换走棋阵营,重置步时
                _playCountdown = 90;
                CurrentSelectPiece = null;
                IsMovingPiece = false;
            }
        }

这里引入一个新的属性RealDeltaTime,其位于基类MiniGameWindow中,他的作用完全等同于Time.deltaTime,只不过后者只能用于运行时:

csharp 复制代码
        /// <summary>
        /// 上一帧到当前帧所经过的真实时间(秒)
        /// </summary>
        public float RealDeltaTime { get; private set; }

那么在OnGamePlayingUpdate方法中使得倒计时每帧累减RealDeltaTime便可实现真实计时:

csharp 复制代码
        private float _playCountdown;
        private const float _repaintInterval = 1;
        private float _repaintTimer;
        
        protected override void OnGamePlayingUpdate()
        {
            base.OnGamePlayingUpdate();

            switch (State)
            {
                case GameState.Playing: OnGamePlayingUpdate_PlayingState(); break;
            }
        }

        private void OnGamePlayingUpdate_PlayingState()
        {
            if (_playCountdown > 0)
            {
                _playCountdown -= RealDeltaTime;
            }
            else
            {
                //自身超时,判负
                if (PlayCamp == SelfCamp)
                {
                    EndGame(false, "我方超时,很遗憾你输了!");
                }
                //对手超时,判胜
                else
                {
                    EndGame(true, "对手超时,恭喜你获胜!");
                }
            }

			//设定一个计时器,每1秒钟刷新一下界面,以跟随倒计时读秒,否则GUI界面在未收到输入时不会刷新
            _repaintTimer += RealDeltaTime;
            if (_repaintTimer >= _repaintInterval)
            {
                Repaint();
                _repaintTimer = 0;
            }
        }

7.显示上一步走棋记录

在走棋完毕后,当前走棋的信息会被存储为上一步走棋记录,并用青色文本显示,以便于查看:

csharp 复制代码
        private void OnOtherGUI_PlayingState()
        {
            Rect rect = new Rect(ViewportRect.x + ViewportRect.width + 5, ViewportRect.y + 5, 160, 20);
            string opponentCamp = SelfCamp == PieceCamp.Red ? "黑方" : "红方";
            GUI.Label(rect, $"{opponentCamp}:{OpponentName}");

            if (PlayCamp != SelfCamp)
            {
                GUI.color = Color.yellow;

                rect.y += 25;
                GUI.Label(rect, $"当前轮到{opponentCamp}走棋");

                rect.y += 25;
                GUI.Label(rect, $"倒计时:{_playCountdown:F0}秒");

                GUI.color = Color.white;
            }
            else
            {
                GUI.color = Color.cyan;

                rect.y += 25;
                //显示上一步走棋记录
                GUI.Label(rect, LastMoveRecord);

                GUI.color = Color.white;
            }

            rect.Set(ViewportRect.x + ViewportRect.width + 5, ViewportRect.y + ViewportRect.height - 75, 160, 20);
            string selfCamp = SelfCamp == PieceCamp.Red ? "红方" : "黑方";
            GUI.Label(rect, $"{selfCamp}:自己");

            if (PlayCamp == SelfCamp)
            {
                GUI.color = Color.yellow;

                rect.y += 25;
                GUI.Label(rect, $"当前轮到{selfCamp}走棋");

                rect.y += 25;
                GUI.Label(rect, $"倒计时:{_playCountdown:F0}秒");

                GUI.color = Color.white;
            }
            else
            {
                GUI.color = Color.cyan;

                rect.y += 25;
                //显示上一步走棋记录
                GUI.Label(rect, LastMoveRecord);

                GUI.color = Color.white;
            }
        }

8.中国象棋的坐标法记录

上一步走棋的记录会自动转换为标准的中国象棋的坐标法记录形式,比如如下:

如果该步存在吃子,还会特殊标记:

至此,一个简单的中国象棋小游戏就完成了,试玩效果如下:中国象棋【Chinese Chess】

相关推荐
Maddie_Mo2 小时前
Unity 联动 Trae AI 项目开发基础教学
人工智能·unity·游戏引擎
新手unity自用笔记14 小时前
unity简单新手上手动画系统讲解
unity·游戏引擎
伽蓝_游戏15 小时前
第二章:深入 Unity 资源导入管线 (Asset Import Pipeline)
游戏·unity·c#·游戏引擎·游戏程序
我寄人间雪满头丶20 小时前
Unity中对于数值游戏的大数显示
游戏·unity·游戏引擎
屋外雨大,惊蛰出没21 小时前
Vscode自动生成类图
ide·vscode·编辑器·类图绘制
游乐码21 小时前
unity基础 (三)坐标系
unity·游戏引擎
qq_2052790521 小时前
Unity 避免Text组件每行开头不是字符和空格,适配不同分辨率
unity·游戏引擎
游乐码21 小时前
Unity基础(二)游戏中的角度及三角函数
游戏·unity·游戏引擎
ONLYOFFICE21 小时前
ONLYOFFICE 文档9.4发布:许可证更新、电子表格的深色模式、水平分隔线、新幻灯片主题与切换等
编辑器·onlyoffice