更新日期:2026年5月20日。
项目源码:获取项目源码
索引
- [中国象棋【Chinese Chess】](#中国象棋【Chinese Chess】)
中国象棋【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;
}
绘制棋盘比较简单,麻烦的地方就是士的路径是斜线,这里通过BeginRotateArea和EndRotateArea方法来构建一个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】。