这个网络版五子棋游戏是今年四月初写的。当时觉得自己应该学一些网络编程的东西。而我课程设计的题目已经定了———做一个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画出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+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;
}
将相对坐标转化成本地像素坐标,绘制棋子,然后本人落子。
没有考虑很多,实现“上传下达”的功能就好了。
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;
}
重点来了,我开头就说要学网络编程的。最后简单介绍一下C#中Socket编程。当然,C#也提供了更高级别的封装如TcpClient,TcpListener。以及更高性能的异步套接字:SocketAsyncEventArgs。
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);
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 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);
当然实际编程的时候会遇到好多问题,比如: