前言

    这个网络版五子棋游戏是今年四月初写的。当时觉得自己应该学一些网络编程的东西。而我课程设计的题目已经定了———做一个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<leftBorder||p.Y<topBorder||p.X>(lineNumber-1)*distance+leftBorder|| p.Y > (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;}

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

服务器设计

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

  • 消息转发
  • 控制用户数量
  • 维护房间列表信息
  • 维护用户列表信息
  1. 比如,玩家断开连接:要及时从玩家列表清理,更新列表,并发送给在线的玩家。
  2. 比如,玩家退出房间:查找到该房间,更新房间信息,发送给在线玩家

举个栗子

    相对于客户端而言,服务端的代码量少很多,除了通用的代码,大概四百行左右。

某玩家退出某房间

    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;}
  1. 根据玩家名称,从房间列表该找到房间。
  2. r.QuitRoom§: 判断该玩家是不是房主,是:将另一名玩家提升为房主;finally:从房间中清除该玩家。
  3. 若房间玩家全部退出,删除该房间
  4. 发送新的房间列表信息给所有玩家。

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);
  1. 新建Socket实例:指定使用IPv4,流传输,TCP协议。

  2. 绑定到本机,4396端口

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

  4. 将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);}}
  1. 从IAsyncResult中获取到mainSocket,并结束异步操作。这是较为经典的异步编程模型写法。

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

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

  4. 开始接收该Socket的消息

     clientSession.Socket.BeginReceive(receiveBuffer, 0, DefaultBufferSize, SocketFlags.None, new AsyncCallback(ReceiveData), clientSession.Socket);BeginReceive函数有多种重载形式,看看说明不难理解。
    
  5. 服务端继续监听连接

     Server.BeginAccept(new AsyncCallback(AcceptConn), Server);
    

客户端Socket

  1. 连接

     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);
    
  2. 发送

     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);}
    
  3. 接收

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

结尾

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

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

C#版网络对战五子棋以及Socket通信相关推荐

  1. java双人对战五子棋(socket通信)

    学习java的时候一直想要做出一个像样的小游戏,所以就动手做了一个远程联网对战的java五子棋小游戏.这个程序我前前后后也是改动了几次,这次发出来的是最终版本了,虽然还是有很多不足,但本人已经没有精力 ...

  2. 网络对战五子棋(来一起PK鸭)

    网络对战五子棋(来一起PK鸭) 一.本地调用和RPC调用的区别 首先了解一下RPC~ RPC主要是解决了两个问题: 解决了分布式系统中,服务之间的调用问题 尤其是在远程调用的时候,可以让调用者感受不到 ...

  3. 【181018】VC++ 网络对战五子棋游戏(服务端+用户端)

    VC++ 网络对战五子棋游戏(服务端+客户端),编译后先开启服务器端,服务端管理着各个用户之间的数据传递,用户端是多个的.就像游戏大厅一样.用户登录了后服务端将向用户端发送当前所有在线玩家列表数据.由 ...

  4. 基于TCP的网络实时聊天室(socket通信案例)

    开门见山 一.数据结构Map 二.保证线程安全 三.群聊核心方法 四.聊天室具体设计 0.用户登录服务器 1.查看当前上线用户 2.群聊 3.私信 4.退出当前聊天状态 5.离线 6.查看帮助 五.聊 ...

  5. Java进阶:基于TCP的网络实时聊天室(socket通信案例)

    目录 开门见山 一.数据结构Map 二.保证线程安全 三.群聊核心方法 四.聊天室具体设计 0.用户登录服务器 1.查看当前上线用户 2.群聊 3.私信 4.退出当前聊天状态 5.离线 6.查看帮助 ...

  6. Android网络开发(一、Socket通信HTTP通信)

    一.Socket通信   Socket:即套接字,其本身并不是一种通信协议,它是封装了TCP/IP.UDP 协议的API实现.在创建Socket对象后,底层会完成TCP/IP的三次握手等(UDP协议对 ...

  7. java aio socket_java核心学习(三十三) 网络编程---AIO实现异步Socket通信

    AIO需要操作系统的支持,在linux内核2.6版本中加入了对真正异步IO的支持,java从jdk1.7开始支持AIO 核心类有AsynchronousSocketChannel .Asynchron ...

  8. 【网络编程知识】使用Socket通信,做一个简单的多人聊天室

  9. 嵌入式课程设计:socket通信模拟服务器客户端实现文件传送(基于c++语言)

            本次课程设计时间大概是两天半,磕磕碰碰,一路上遇到了很多的问题,也不断的查资料,想办法解决各种拦路的BUG,最后做出来的效果自己还是挺满意的,能够实现多台电脑之间模拟服务端,客户端,进 ...

最新文章

  1. python建站与java建站有何不同_详解模板建站和定制建站的不同之处
  2. Android Fragment中嵌套Fragment,不显示view
  3. ×××:关于促进云计算创新发展 培育信息产业新业态的意见
  4. 【春华秋实】.NET Core之只是多看了你一眼
  5. 计算机接口教程,运用接口实现计算机各组件信息
  6. VirtualBox安装教程及使用(Windows)
  7. CCS 报警告 #10247-D
  8. PDF技术(三)-Java实现图片转PDF文件
  9. ERROR StatusLogger No log4j2 configuration file found. Using default configuration解决方式
  10. ubuntu 20.04 ROS 环境下 使用 velodyne
  11. HTML5制作坦克大战游戏+Canvas绘制基础图形——学习笔记一
  12. vue 中嵌入iframe页面
  13. 2017 码云最火开源项目 TOP 50
  14. C++ Reference: Standard C++ Library reference: C Library: cmath: cosh
  15. Elman神经网络预测的Matlab实现
  16. 【Matlab】系统的响应分析
  17. 用于 LLM 应用开发的 LangChain 中文版
  18. 量子力学奇妙之旅-从相对论下薛定谔方程到量子场论
  19. Android工程师成长之路
  20. 污染源在线监控_污染源在线监控数采仪

热门文章

  1. 黑帽python_seo黑白帽:关键词排名官网,Python 黑帽SEO
  2. php内容管理系统 admini,网站内容管理系统 BageCms
  3. Android蓝牙Ble基本操作-(连接2)
  4. 京东商城关键词SEO优化总结
  5. dia 导出大分辨率高清png图
  6. [一个程序员的人文素养系列]这世界如露水般短暂俳句摘抄
  7. 【NOIP2013模拟10.23】君と彼女の恋
  8. python俄罗斯方块编程思路_python游戏开发之俄罗斯方块(一):简版
  9. win32关键点(一)
  10. 条码打印机碳带装反了会怎么样