作为一名长期从事C#开发的程序员,我一直对网络游戏开发充满兴趣。最近花了两周时间,用C#实现了一个完整的联网象棋对战系统。这个项目不仅涵盖了TCP网络通信、多线程处理等核心技术点,还涉及到了游戏规则验证、状态同步等游戏开发特有的挑战。
系统采用经典的C/S架构,包含一个独立运行的服务器程序和多个客户端程序。服务器负责维护游戏状态、转发玩家指令,客户端则处理用户界面交互和网络通信。整个系统从零开始搭建,代码量约2000行,实现了基本的象棋对战功能。
提示:在开始编码前,建议先绘制系统架构图,明确各模块的职责边界。这能有效避免后期出现职责不清的问题。
系统采用三层架构设计:
这种分层设计使得各模块职责清晰,便于后期维护和扩展。比如要更换UI框架为WPF时,只需重写表现层即可。
考虑到象棋对战的实时性要求,我们选择了TCP协议而非UDP,主要基于以下考虑:
自定义的简单文本协议格式如下:
code复制指令类型:参数1,参数2,...
例如移动棋子的指令:
code复制MOVE:1,2,3,4
表示将棋子从(1,2)移动到(3,4)。
服务器采用多线程模型处理并发连接:
这种模型在并发量不大时(象棋对战通常2人一组)性能足够,且实现简单。
服务器核心类是ChessServer,主要职责包括:
csharp复制public class ChessServer {
private TcpListener _listener;
private ConcurrentDictionary<string, ClientHandler> _clients = new();
public void Start(string ip, int port) {
_listener = new TcpListener(IPAddress.Parse(ip), port);
_listener.Start();
Console.WriteLine("服务器已启动...");
while (true) {
var client = _listener.AcceptTcpClient();
var handler = new ClientHandler(client, this);
_clients[client.Client.RemoteEndPoint.ToString()] = handler;
new Thread(handler.Handle).Start();
}
}
public void Broadcast(string message, ClientHandler exclude) {
foreach (var client in _clients.Values) {
if (client != exclude) client.Send(message);
}
}
}
注意:在实际项目中,建议使用线程池而非直接创建线程,避免频繁创建销毁线程的开销。
客户端核心是ChessForm类,主要功能包括:
csharp复制public partial class ChessForm : Form {
private TcpClient _client;
private NetworkStream _stream;
private ChessBoardControl _board;
private void ConnectToServer() {
_client = new TcpClient();
_client.Connect("127.0.0.1", 8888);
_stream = _client.GetStream();
new Thread(() => {
byte[] buffer = new byte[1024];
while (true) {
int bytesRead = _stream.Read(buffer, 0, buffer.Length);
string msg = Encoding.UTF8.GetString(buffer, 0, bytesRead);
this.Invoke((MethodInvoker)delegate {
HandleServerMessage(msg);
});
}
}).Start();
}
private void HandleServerMessage(string msg) {
if (msg.StartsWith("MOVE:")) {
string[] data = msg.Substring(5).Split(',');
Point from = new Point(int.Parse(data[0]), int.Parse(data[1]));
Point to = new Point(int.Parse(data[2]), int.Parse(data[3]));
_board.MovePiece(from, to);
}
}
}
为了保证两个客户端的棋盘状态一致,服务器维护了权威的游戏状态:
csharp复制public class ChessBoardState {
private int[,] _board = new int[9,10]; // 9列10行
private object _lock = new object();
public void UpdatePosition(int x, int y, int piece) {
lock(_lock) {
_board[x, y] = piece;
}
}
public int GetPiece(int x, int y) {
lock(_lock) {
return _board[x, y];
}
}
}
任何棋子移动都需要经过服务器验证和转发,确保两个客户端看到的棋盘状态完全一致。
象棋规则的核心是各种棋子的移动规则验证。我们实现了ChessRuleValidator类来封装这些规则:
csharp复制public class ChessRuleValidator {
public bool IsValidMove(int fromX, int fromY, int toX, int toY, int[,] board) {
int piece = board[fromX, fromY];
if (piece == 0) return false;
switch (piece) {
case 1: // 车
return IsStraightPathClear(fromX, fromY, toX, toY, board);
case 2: // 马
return IsHorseMoveValid(fromX, fromY, toX, toY, board);
case 3: // 炮
return IsCannonMoveValid(fromX, fromY, toX, toY, board);
// 其他棋子规则...
default:
return false;
}
}
private bool IsStraightPathClear(int x1, int y1, int x2, int y2, int[,] board) {
if (x1 != x2 && y1 != y2) return false;
if (x1 == x2) { // 垂直移动
int step = y1 < y2 ? 1 : -1;
for (int y = y1 + step; y != y2; y += step) {
if (board[x1, y] != 0) return false;
}
} else { // 水平移动
int step = x1 < x2 ? 1 : -1;
for (int x = x1 + step; x != x2; x += step) {
if (board[x, y1] != 0) return false;
}
}
return true;
}
}
象棋中有一些特殊规则需要特别注意:
这些规则都需要在验证逻辑中特殊处理:
csharp复制private bool IsHorseMoveValid(int fromX, int fromY, int toX, int toY, int[,] board) {
int dx = Math.Abs(toX - fromX);
int dy = Math.Abs(toY - fromY);
if (!((dx == 1 && dy == 2) || (dx == 2 && dy == 1))) {
return false;
}
// 检查马脚
if (dx == 2) {
int blockX = fromX + (toX > fromX ? 1 : -1);
if (board[blockX, fromY] != 0) return false;
} else {
int blockY = fromY + (toY > fromY ? 1 : -1);
if (board[fromX, blockY] != 0) return false;
}
return true;
}
为了检测连接是否断开,我们实现了简单的心跳机制:
csharp复制// 服务器端
private void Handle() {
DateTime lastHeartbeat = DateTime.Now;
while (true) {
if ((DateTime.Now - lastHeartbeat).TotalSeconds > 30) {
break; // 超时断开
}
int bytesRead = _stream.Read(buffer, 0, buffer.Length);
if (bytesRead == 0) break;
string msg = Encoding.UTF8.GetString(buffer, 0, bytesRead);
if (msg == "HEARTBEAT") {
lastHeartbeat = DateTime.Now;
continue;
}
ProcessCommand(msg);
}
_server.RemoveClient(this);
}
客户端需要处理网络中断的情况:
csharp复制private void Reconnect() {
while (!_client.Connected) {
try {
_client = new TcpClient();
_client.Connect("127.0.0.1", 8888);
_stream = _client.GetStream();
break;
} catch {
Thread.Sleep(3000);
}
}
}
为了避免UI线程阻塞,使用消息队列处理网络消息:
csharp复制public class MessageQueue {
private ConcurrentQueue<string> _queue = new();
private AutoResetEvent _signal = new(false);
public void Enqueue(string msg) {
_queue.Enqueue(msg);
_signal.Set();
}
public string Dequeue() {
_signal.WaitOne();
return _queue.TryDequeue(out var msg) ? msg : null;
}
}
问题现象:两个客户端显示的棋盘状态不一致
解决方案:
问题现象:移动棋子后响应慢
优化方案:
问题现象:偶尔出现棋子位置错乱
解决方案:
ConcurrentDictionaryInvoke回到UI线程执行实现观战模式的关键点:
csharp复制public class SpectatorHandler : ClientHandler {
public override void Handle() {
// 发送初始棋盘状态
Send(_server.GetFullBoardState());
// 只接收不处理指令
while (true) {
string msg = Receive();
if (msg.StartsWith("MOVE:")) {
UpdateBoardView(msg);
}
}
}
}
实现回放功能需要:
csharp复制public class GameRecorder {
private List<string> _moves = new List<string>();
public void RecordMove(string move) {
_moves.Add(move);
}
public void SaveToFile(string path) {
File.WriteAllLines(path, _moves);
}
public void ReplayFromFile(string path) {
var moves = File.ReadAllLines(path);
foreach (var move in moves) {
ExecuteMove(move);
Thread.Sleep(500); // 控制回放速度
}
}
}
基于Minimax算法实现简单AI:
csharp复制public class ChessAI {
public Move FindBestMove(int[,] board, int depth) {
Move bestMove = null;
int bestValue = int.MinValue;
foreach (var move in GenerateAllMoves(board)) {
MakeMove(move);
int value = Minimax(depth - 1, int.MinValue, int.MaxValue, false);
UndoMove(move);
if (value > bestValue) {
bestValue = value;
bestMove = move;
}
}
return bestMove;
}
private int Minimax(int depth, int alpha, int beta, bool maximizingPlayer) {
if (depth == 0 || IsGameOver()) {
return EvaluateBoard();
}
if (maximizingPlayer) {
int value = int.MinValue;
foreach (var move in GenerateAllMoves()) {
MakeMove(move);
value = Math.Max(value, Minimax(depth - 1, alpha, beta, false));
UndoMove(move);
alpha = Math.Max(alpha, value);
if (alpha >= beta) break;
}
return value;
} else {
int value = int.MaxValue;
foreach (var move in GenerateAllMoves()) {
MakeMove(move);
value = Math.Min(value, Minimax(depth - 1, alpha, beta, true));
UndoMove(move);
beta = Math.Min(beta, value);
if (beta <= alpha) break;
}
return value;
}
}
}
频繁创建销毁网络缓冲区会影响性能,可以使用对象池:
csharp复制public class BufferPool {
private ConcurrentQueue<byte[]> _pool = new();
private int _bufferSize;
public BufferPool(int bufferSize, int initialCount) {
_bufferSize = bufferSize;
for (int i = 0; i < initialCount; i++) {
_pool.Enqueue(new byte[bufferSize]);
}
}
public byte[] Rent() {
return _pool.TryDequeue(out var buffer) ? buffer : new byte[_bufferSize];
}
public void Return(byte[] buffer) {
if (buffer.Length == _bufferSize) {
_pool.Enqueue(buffer);
}
}
}
使用二进制协议替代文本协议可以减少数据量:
csharp复制public class BinaryProtocol {
public byte[] SerializeMove(int fromX, int fromY, int toX, int toY) {
byte[] data = new byte[5];
data[0] = 0x01; // 移动指令
data[1] = (byte)fromX;
data[2] = (byte)fromY;
data[3] = (byte)toX;
data[4] = (byte)toY;
return data;
}
public (int fromX, int fromY, int toX, int toY) DeserializeMove(byte[] data) {
if (data[0] != 0x01 || data.Length < 5) {
throw new ArgumentException("Invalid move data");
}
return (data[1], data[2], data[3], data[4]);
}
}
对于棋盘状态,使用更紧凑的数据结构:
csharp复制public class CompactChessBoard {
private byte[] _data = new byte[45]; // 9x10 / 2 (每个byte存储两个棋子)
public int this[int x, int y] {
get {
int index = (y * 9 + x) / 2;
return (x % 2 == 0) ? (_data[index] >> 4) : (_data[index] & 0x0F);
}
set {
int index = (y * 9 + x) / 2;
if (x % 2 == 0) {
_data[index] = (byte)((_data[index] & 0x0F) | (value << 4));
} else {
_data[index] = (byte)((_data[index] & 0xF0) | value);
}
}
}
}
建议的服务器部署步骤:
powershell复制nssm install ChessServer "C:\path\to\ChessServer.exe"
nssm set ChessServer AppParameters "--port 8888 --maxplayers 100"
nssm start ChessServer
完善的日志系统有助于问题排查:
csharp复制public static class Logger {
private static readonly object _lock = new object();
public static void Log(string message, [CallerMemberName] string caller = "") {
lock (_lock) {
string logEntry = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss} [{caller}] {message}";
File.AppendAllText("server.log", logEntry + Environment.NewLine);
Console.WriteLine(logEntry);
}
}
public static void LogError(Exception ex, [CallerMemberName] string caller = "") {
Log($"ERROR: {ex.Message}\n{ex.StackTrace}", caller);
}
}
使用PerformanceCounter监控关键指标:
csharp复制public class ServerMonitor {
private PerformanceCounter _cpuCounter;
private PerformanceCounter _memCounter;
public ServerMonitor() {
_cpuCounter = new PerformanceCounter(
"Processor", "% Processor Time", "_Total");
_memCounter = new PerformanceCounter(
"Memory", "Available MBytes");
}
public float GetCpuUsage() {
return _cpuCounter.NextValue();
}
public float GetAvailableMemory() {
return _memCounter.NextValue();
}
public void LogMetrics() {
Logger.Log($"CPU Usage: {GetCpuUsage()}%, Available Memory: {GetAvailableMemory()}MB");
}
}
在实际开发过程中,我总结了以下几点经验:
先设计后编码:在开始编码前,花时间设计好协议格式和接口定义,这能节省大量后期调整的时间。
重视日志系统:完善的日志能在出现问题时快速定位原因,建议从一开始就建立良好的日志习惯。
防御性编程:网络程序要处理各种异常情况,如连接中断、数据错误等,代码中要有充分的错误处理。
性能考量:即使是小型网络游戏,也要注意性能问题,特别是锁的使用要谨慎,避免死锁和性能瓶颈。
测试驱动:先编写测试用例再实现功能,这能保证代码质量,特别是对于象棋规则这类复杂逻辑。
这个项目让我对C#网络编程有了更深的理解,特别是在多线程同步、网络通信协议设计等方面积累了不少实战经验。后续我计划继续完善这个项目,添加更多功能如棋谱保存、AI对战等。