网络中国象棋小游戏的实现

迁移自: https://www.cnblogs.com/dashublog/p/3304360.html

效果图 01

开学了,去图书馆借了几本书,没有找到想要的C++网络编程,倒是找到了几本LINUX的书,以及一本《Visual C#经典游戏编程开发》。翻了翻发现里面有个可以联网对弈的中国象棋游戏,一直写ASP.NET的网页,也想试着写写Winform的图形程序了,而且可以了解一下.NET的网络编程相关的内容。

象棋方面,先从网上找了张棋盘图片,在PS里把棋子一一扣了出来作为素材。编写对应的棋子类和棋盘类,前者实现棋子身份的识别和棋子图片的绘制,后者实现棋盘绘制、走棋规则、棋谱生成等功能。

网络通信的协议使用的是UDP协议,发送的信息规则是:“命令|参数1|参数2……”,主要命令:join表示用户想要联机,处于接受对方连接的状态;conn表示收到对方联机命令,已准备好开始游戏,对方可以开局了;new表示其中一方提出重新开始游戏;move|id|x|y表示移动棋盘上编号为id的棋子到x,y处;succ|赢方代号 表示此局已分出胜负;exit表示退出游戏。

游戏开始后创建了一个线程,在read方法中创建UdpClient对象,循环侦听指定端口中由指定IP主机传来的信息。主要代码是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
udpClient = new UdpClient(Convert.ToInt32(txt_port.Text));
int id, x, y;
while(readFlag == true)
{
try
{
byte[] data = udpClient.Receive(ref remote);
string strData = Encoding.UTF8.GetString(data);
string[] a = strData.Split('|'); // 分割命令与参数
switch(a[0]) // 判断命令
{
case "join":
case "conn":
//......
}
}
catch
{
break;
}
}

游戏过程中发送数据则是通过send方法,创建UdpClient网络服务对象,向指定IP的主机发送消息到设定的端口号。完成后发送后,关闭UDP网络服务。主要代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
UdpClient sendUdp = new UdpClient();
UPAddress remoteIP;
try {
remoteIP = IPAddress.Parse(txt_IP.Text);
}
catch {
MessageBox.Show("请输入正确的IP地址!", "错误");
return;
}
remote = new IPEndPoint(remoteIP, Convert.ToInt32(txt_remoteport.Text));
byte[] buffer = Encoding.UTF8.GetBytes(str);
sendUdp.Send(buffer, buffer.Length, remote);
sendUdp.Close();

原书中的代码有一些小问题,所以做了点改动和简化。不过基本的思路还是和原书上是一样的。

通信过程方面,每次联机都需要双方填写好对方的IP、端口号以及自己的端口号。而且,由于没有明显的游戏过程标记,所以造成了一些通信问题,导致在不正确的阶段收到对方错误的请求时游戏出现意外的情况。

于是在完成后,就开始了对原程序的改造。考虑到内网用户连接外网的问题,就将游戏中的UDP协议通信改为了使用Socket通过TCP协议进行游戏通信。添加游戏状态state字段,包括以下几个主要状态:

1
2
3
4
public const short WAITING_CLIENT = 1;  // 作为主机,等待客户端连接
public const short SERVING = 2; // 作为主机,客户端已连接,游戏中
public const short WAITING_SERVER = 3; // 作为从机,等待主机接受连接
public const short CONNECTING = 4; // 作为从机,连接到主机,游戏中

游戏的命令和监听流程也做了对应的改动。

主机建立游戏等待客户端连接的过程主要包括:监听端口、建立连接、开始监听消息,这三个步骤。首先创建线程,在线程中执行acceptClientConnect监听端口等待连接,检测到连接后建立连接,转入Read监听阶段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
private void WaitClient()
{
// 启动一个线程来接受请求
thread = new Thread(acceptClientConnect);
thread.Start();
SetState(WAITING_CLIENT);
}

// 接受请求
private void acceptClientConnect()
{
IPEndPoint localEndPoint = new IPEndPoint(IPAddress.Any, DEFAULT_PORT);
listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
toolStripStatusLabel1.Text = "正在等待其他玩家连接……";
try
{
if (timeoutThread != null)
{
timeoutThread.Abort();
}
timeoutThread = new Thread(WaiteTimeOut);
timeoutThread.Start();
listener.Bind(localEndPoint);
listener.Listen(1);
connection = listener.Accept();
listener.Close();
listener = null;
Read();
}
catch
{
toolStripStatusLabel1.Text = "连接中断,游戏终止。";
SetState(FREE);
}
}

private void WaiteTimeOut()
{
Thread.Sleep(30000);
if (connection == null || !connection.Connected)
{
if (listener != null)
{
SetState(FREE);
toolStripStatusLabel1.Text = "连接超时,无玩家连接游戏。";
}
}
timeoutThread = null;
}

预定的监听超时时间是30秒,30秒后没有连接时,转到FREE状态并终止监听。终止的实现思路是在主机本身创建一个TCP协议的Socket对象,向自身端口发送终止连接的消息,listener建立连接后在Read中接受到此消息停止阻塞,结束游戏彻底转为FREE状态,线程结束。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
private void SetState(int newState)     // 支线程
{
state = newState;
switch (state)
{
case FREE:
btnCreate.Text = "创建游戏";
btnCreate.Enabled = true;
btnJoin.Text = "加入游戏";
btnJoin.Enabled = true;
if (listener != null)
{
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
socket.Connect(IPAddress.Parse("127.0.0.1"), DEFAULT_PORT);
if (socket.Connected)
{
socket.Send(Encoding.UTF8.GetBytes("end"));
}
}
if (connection != null)
{
connection.Close();
connection = null;
}
break;
case WAITING_CLIENT:
btnCreate.Text = "关闭游戏";
btnJoin.Enabled = false;
break;
case SERVING:
break;
case WAITING_SERVER:
btnJoin.Text = "断开游戏";
btnCreate.Enabled = false;
break;
case CONNECTING:
break;
}
}

接受消息的Read方法和发送消息的Send方法方面,原本的UdpClient收发消息改为通过创建的connection(Socket)连接来通信。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
private void Read()
{
int t, x1, y1, x2, y2;
while (state != FREE)
{
try
{
byte[] data = new byte[1024];
int len = connection.Receive(data);
string msg = Encoding.UTF8.GetString(data);
string[] a = msg.Split('|');
switch (a[0])
{
case "end": // 终止listener的监听
if (state != WAITING_CLIENT) continue;
connection.Close();
connection = null;
SetState(FREE);
return;
case "join": // 客户端请求加入游戏
break;
case "acce": // 服务端接受加入的请求
break;
case "exit": // 对方方退出游戏
break;
case "new": // 对方方提出重新开局
break;
case "succ": // 一方获胜
break;
case "lose": // 一方认输
break;
case "move": // 对方移动棋子
break;
}
}
catch
{
break;
}
}
}

private void Send(string msg)
{
if (connection != null)
{
byte[] buffer = Encoding.UTF8.GetBytes(msg);
connection.Send(buffer);
}
}

具体游戏效果截图:

  • 红方先行(自己视角):
    效果图 01
  • 红方先行(对手视角):
    效果图 02
  • 等待对手行棋:
    效果图 03
  • 轮到自己操作:
    效果图 04

项目工程下载:

点我下载整个项目的压缩包(883K)

点我下载整个项目(UDP版)的压缩包(881K)