C#版网络对战五子棋以及Socket通信 |
您所在的位置:网站首页 › 双人对战游戏(完整版) › C#版网络对战五子棋以及Socket通信 |
前言
这个网络版五子棋游戏是今年四月初写的。当时觉得自己应该学一些网络编程的东西。而我课程设计的题目已经定了———做一个Everything。 那就帮我斐哥做个网络版的五子棋吧。 源码:https://pan.baidu.com/s/1oLYgg-PykBkCtT0MtKI_xQ 界面是WinForm的,使用GDI绘图来完成棋盘与棋子的绘制,落子坐标通过定义的公式来计算。我原先做过人机对战版的五子棋,因此游戏逻辑这个最重要的部分并没有花很多时间。这个程序一个多星期就搞的差不多了。 源码: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; }将相对坐标转化成本地像素坐标,绘制棋子,然后本人落子。 服务器设计没有考虑很多,实现“上传下达”的功能就好了。 消息转发控制用户数量维护房间列表信息维护用户列表信息 比如,玩家断开连接:要及时从玩家列表清理,更新列表,并发送给在线的玩家。比如,玩家退出房间:查找到该房间,更新房间信息,发送给在线玩家![]() 重点来了,我开头就说要学网络编程的。最后简单介绍一下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 |