C#版网络对战五子棋以及Socket通信

您所在的位置:网站首页 双人对战游戏(完整版) C#版网络对战五子棋以及Socket通信

C#版网络对战五子棋以及Socket通信

#C#版网络对战五子棋以及Socket通信| 来源: 网络整理| 查看: 265

前言

    这个网络版五子棋游戏是今年四月初写的。当时觉得自己应该学一些网络编程的东西。而我课程设计的题目已经定了———做一个Everything。 那就帮我斐哥做个网络版的五子棋吧。

源码:https://pan.baidu.com/s/1oLYgg-PykBkCtT0MtKI_xQ

    界面是WinForm的,使用GDI绘图来完成棋盘与棋子的绘制,落子坐标通过定义的公式来计算。我原先做过人机对战版的五子棋,因此游戏逻辑这个最重要的部分并没有花很多时间。这个程序一个多星期就搞的差不多了。 在这里插入图片描述 在这里插入图片描述 不过现在看来,当时的代码太青涩了,一是课程设计马上就要中期检查没太多时间,二是水平和眼界确实不高。 比如:

消息对象的序列化。那时候不知道有JSON序列化,所以自己就写了个ToString()方法,对方收到之后,解析出字符串,再Split,重建实体。消息处理。在Switch里写大量的逻辑代码,来处理不同类型的消息。以及大量Bug等。

源码:https://pan.baidu.com/s/1oLYgg-PykBkCtT0MtKI_xQ

设计

    玩家对战与人机对战的区别其实就是将玩家A的操作发送给玩家B,玩家B那边的界面渲染。我将游戏里的操作指令封装为了枚举类型。

public enum MsgType { LuoZi=0,//玩家落子 Connect=1,//玩家上线 Quit=2,//玩家退出房间 IsWin=3,//是否胜利 CreateRoom=4,//创建房间 JoinRoom=5,//加入房间 UserList=6,//请求|发送玩家列表 RoomList,//请求|发送房间列表 Other,//其他 Start,//开始游戏 Exit,//玩家连接断开 OtherName,//忘了干嘛的了 Restart,//重新开始游戏 Msg//聊天 }

消息对象:

public class MessagePackage { public MsgType msgType; public string data; public string senderIP = ""; public string senderName = ""; public string sendTime; public MessagePackage() { } public MessagePackage(string msg) { string[] msgs = msg.Split('|'); msgType = (MsgType)int.Parse(msgs[0]); data = msgs[1]; senderIP = msgs[2]; senderName = msgs[3]; sendTime = msgs[4]; } public MessagePackage(MsgType msg, string data, string senderIP, string senderName, string sendTime) { this.msgType = msg; this.data = data; this.senderIP = senderIP; this.senderName = senderName; this.sendTime = sendTime; } public string ConvertToString() { string msg = ((int)msgType).ToString() + "|" + data + "|" + senderIP + "|" + senderName + "|" + sendTime; return msg; } } 客户端逻辑 GDI绘制游戏逻辑登录建房加入开始结束重来聊天信息退出 -在这里插入图片描述 在这里插入图片描述 在这里插入图片描述     我决定举一个最基本的栗子———游戏逻辑中的玩家落子。 落子

    进入游戏房间后,我会用GDI画出15*15的棋盘。使用过GDI的朋友都知道,它是根据像素为单位的,这样做是不简单的。

    比如你想将棋子落在棋盘上(7,7)这个点上,那就需要用GDI来画一个白色的棋子在那个位置上。GDI提供的绘圆方法是什么呢?FillEllipse,你需要指定一个长方形,包括这个长方形左上角的横纵坐标,以及它的长和宽,以及填充的颜色。这个方法才能为你画出这个长方形里最大的那个圆,或是椭圆。

private bool GraphicsPiece(Point upleft, Color c) { Graphics g = this.panel1.CreateGraphics(); if (upleft.X != -1 || upleft.Y != -1) { g.FillEllipse(new SolidBrush(c), upleft.X, upleft.Y, CheckerBoard.chessPiecesSize, CheckerBoard.chessPiecesSize); return true; } return false; }

    重点就是这个长方形的左上角坐标怎么得到?我们知道鼠标点击事件中,参数Args带给我们的是一个以像素为单位的,相对与绘图区的位置。而且你不能指望用户正好点在棋盘的那个点上,他可能点在(7,7)上面一点,或是下面一点。因此我们就需要对鼠标点击的坐标值就行处理,将其转化相对的表现形式(7,7)。

将像素坐标转化成相对坐标: public static Piece ConvertPointToCoordinates(Point p,int flag) { int x, y; Piece qi; if (p.X (lineNumber - 1) * distance + topBorder) { qi= new Piece(-1,-1,flag); } else { float i = ((float)p.X - leftBorder) / distance; float j= ((float)p.Y - topBorder) / distance; x = Convert.ToInt32(i); y = Convert.ToInt32(j); if (GameControl.ChessPieces[x, y] != 0) { qi = new Piece(-1, -1, flag); } else { qi = new Piece(x, y,flag); } } return qi; } 将相对坐标转化成像素坐标: public static Point ConvertCoordinatesToPoint(Piece p) { int x, y; x = p.X * distance + leftBorder - chessPiecesSize / 2; y = p.Y * distance + topBorder - chessPiecesSize / 2; return new Point(x, y); } 落子:绘制本地棋子并将相对坐标发送给服务器;如果取得胜利,则发送胜利消息给服务器,服务器根据房间信息,查找到对手玩家,发送消息给对手玩家。 Piece p = CheckerBoard.ConvertPointToCoordinates(new Point(e.X, e.Y), 1); if (p.X != -1) { Point point = CheckerBoard.ConvertCoordinatesToPoint(p); if (Program.gc.AddPiece(p)) { GraphicsPiece(point, myColor); MessageBox.Show("黑棋获胜"); return; } else { GraphicsPiece(point, myColor); p = Program.gc.MachineChoose(); point = CheckerBoard.ConvertCoordinatesToPoint(p); if (Program.gc.AddPiece(p)) { GraphicsPiece(point, otherColor); turnFlag = true; MessageBox.Show("白棋获胜"); return; } GraphicsPiece(point, otherColor); lbmyscore.Text = (0 - Program.gc.GetScore()).ToString(); lbhisscore.Text = Program.gc.GetScore().ToString(); turnFlag = true; } } 对方收到落子消息后 case MsgType.LuoZi: { string[] qi = mp.data.Split(','); int x = int.Parse(qi[0]); int y = int.Parse(qi[1]); Piece p = new Piece(x, y, 3 - flag); Point point = CheckerBoard.ConvertCoordinatesToPoint(p); if (Program.gc.AddPiece(p)) { GraphicsPiece(point, otherColor); start = false; btnStart.Enabled = true; MessageBox.Show("对方获胜"); } else { GraphicsPiece(point, otherColor); turnFlag = true; } break; }

    将相对坐标转化成本地像素坐标,绘制棋子,然后本人落子。

服务器设计

    没有考虑很多,实现“上传下达”的功能就好了。

消息转发控制用户数量维护房间列表信息维护用户列表信息 比如,玩家断开连接:要及时从玩家列表清理,更新列表,并发送给在线的玩家。比如,玩家退出房间:查找到该房间,更新房间信息,发送给在线玩家 在这里插入图片描述 举个栗子     相对于客户端而言,服务端的代码量少很多,除了通用的代码,大概四百行左右。 某玩家退出某房间 case MsgType.Quit: { GameRoom r = SearchRoomBySenderName(mp.senderName); GamePlayer p = SearchUserByName(mp.senderName); r.QuitRoom(p); if (r.PlayerNumber == 0) roomList.Remove(r); else { mp = new MessagePackage(MsgType.Quit, "", "", "", ""); tcpServer.Send(r.RoomMaster.Session, mp.ConvertToString()); } mp = new MessagePackage(MsgType.RoomList, GetRoomList(), "", "", DateTime.Now.ToString()); foreach (Session session in tcpServer.SessionTable.Values) { tcpServer.Send(session, mp.ConvertToString()); } break; } 根据玩家名称,从房间列表该找到房间。r.QuitRoom§: 判断该玩家是不是房主,是:将另一名玩家提升为房主;finally:从房间中清除该玩家。若房间玩家全部退出,删除该房间发送新的房间列表信息给所有玩家。

在这里插入图片描述

Socket通信

    重点来了,我开头就说要学网络编程的。最后简单介绍一下C#中Socket编程。当然,C#也提供了更高级别的封装如TcpClient,TcpListener。以及更高性能的异步套接字:SocketAsyncEventArgs。

服务端Socket mainSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); mainSocket.Bind(new IPEndPoint(IPAddress.Any, 4396)); mainSocket.Listen(5); mainSocket.BeginAccept(new AsyncCallback(AcceptConn), mainSocket);

新建Socket实例:指定使用IPv4,流传输,TCP协议。

绑定到本机,4396端口

开始监听,连接队列最大为5

将AcceptConn函数注册为连接回调函数。回调函数必须接收一个类型为IAsyncResult的参数。

mainSocket.BeginAccept(new AsyncCallback(AcceptConn), mainSocket); BeginAccept会阻塞当前线程。当有连接进入后,将mainSocket封装为作为IAsyncResult对象,作为参数传递给AcceptConn。 连接回调函数AcceptConn的用法 protected virtual void AcceptConn(IAsyncResult iar) { Socket Server = (Socket)iar.AsyncState; Socket client = Server.EndAccept(iar); if (clientCount == maxClient) { ServerFull?.Invoke(this, new NetEventArgs(new Session(client))); } else { Session clientSession = new Session(client); sessionTable.Add(clientSession.SessionId, clientSession); clientCount++; clientSession.Socket.BeginReceive(receiveBuffer, 0, DefaultBufferSize, SocketFlags.None, new AsyncCallback(ReceiveData), clientSession.Socket); ClientConn?.Invoke(this, new NetEventArgs(clientSession)); Server.BeginAccept(new AsyncCallback(AcceptConn), Server); } }

从IAsyncResult中获取到mainSocket,并结束异步操作。这是较为经典的异步编程模型写法。

服务器满,触发ServerFull事件,通知客户端无法进入。

服务器未满,将接入的socket连接进行封装,加入到玩家集合中

开始接收该Socket的消息

clientSession.Socket.BeginReceive(receiveBuffer, 0, DefaultBufferSize, SocketFlags.None, new AsyncCallback(ReceiveData), clientSession.Socket); BeginReceive函数有多种重载形式,看看说明不难理解。

服务端继续监听连接

Server.BeginAccept(new AsyncCallback(AcceptConn), Server); 客户端Socket

连接

Socket newSoc = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); IPEndPoint remoteEP = new IPEndPoint(IPAddress.Parse(ip), port); newSoc.BeginConnect(remoteEP, new AsyncCallback(Connected), newSoc);

发送

public virtual void Send(string datagram) { if (datagram.Length == 0) { return; } if (!isConnected) { throw (new ApplicationException("没有连接服务器,不能发送数据")); } //获得报文的编码字节 byte[] data = coder.GetEncodingBytes(datagram); session.Socket.BeginSend(data, 0, data.Length, SocketFlags.None,new AsyncCallback(SendDataEnd), session.Socket); }

接收

session.Socket.BeginReceive(receiveBuffer, 0, DefaultBufferSize, SocketFlags.None, new AsyncCallback(RecvData), socket); 结尾

当然实际编程的时候会遇到好多问题,比如:

Socket连接正常断开和异常断开的问题。事件驱动模型中,事件侦听程序不再直接引用,发布程序仍会有引用存在,垃圾回收器就不能对其进行回收。当多个界面都存在事件侦听操作时,会发生混乱。等。


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3