序文
このオンライン バックギャモン ゲームは、今年 4 月上旬に作成されました。その時、私はネットワークプログラミングについて何かを学ばなければならないと感じました. そして、私のコース設計のトピックは、すべてを行うことです。では、ブラザー フェイがバックギャモンのオンライン版を作るのを手伝ってください。
ソースコード: https://pan.baidu.com/s/1oLYgg-PykBkCtT0MtKI_xQ
インターフェイスは WinForm で、GDI 描画を使用してチェス盤とチェスの駒の描画を完了し、チェスの駒の座標は定義された式によって計算されます。以前は人間とコンピューターのバージョンのバックギャモンをやっていたので、ゲーム ロジックの最も重要な部分に多くの時間を費やすことはありませんでした。このプログラムは 1 週間以上行われています。
しかし、今となっては当時のコードが若すぎたようで、まずコース設計が中間点検を間近に控えており、時間があまりないこと、第二に、レベルとビジョンが非常に高くないことです。
例えば:
- メッセージ オブジェクトのシリアル化。当時はJSONのシリアライズがあることを知らなかったので、ToString()メソッドを書いて、それを受け取った相手は文字列をパースして分割し、実体を再構築しました。
- メッセージ処理。さまざまな種類のメッセージを処理するために、Switch で多くのロジック コードを記述します。
- そしてバグ等が多い。
ソースコード: https://pan.baidu.com/s/1oLYgg-PykBkCtT0MtKI_xQ
デザイン
プレイヤー vs マンマシン vs マンマシン vs プレイヤー A の違いは、プレイヤー 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 描画
- ゲームロジック
- ログインして家を建てる
- 参加開始
- もう一度終わる
- チャット情報
- 終了
-_
私は最も基本的な栗の 1 つを与えることにしました--ゲーム ロジックにおけるプレイヤーの動き。
ラジ
ゲームルームに入ったら、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;
}
相対座標をローカル ピクセル座標に変換し、チェスの駒を描画してから、駒を自分で配置します。
サーバー設計
「アップロードとリリース」機能を実現するだけで、あまり考えませんでした。
- メッセージ転送
- ユーザー数を制御する
- ルームリスト情報を維持する
- ユーザーリスト情報を維持する
- たとえば、プレイヤーが切断された場合: 時間内にプレイヤー リストから削除し、リストを更新して、オンライン プレイヤーに送信する必要があります。
- たとえば、プレーヤーが部屋を出る: 部屋を見つけ、部屋の情報を更新し、オンライン プレーヤーに送信します。
栗をあげる
クライアント側と比較すると、サーバー側のコード量は、一般的なコードを除いて 400 行程度とはるかに少なくなります。
プレーヤーが部屋を出る
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§: プレーヤーがルームの所有者であるかどうかを判断します, はい: 別のプレーヤーをルームの所有者に昇格させます; 最後に: ルームからプレーヤーをクリアします.
- ルーム内のすべてのプレイヤーが退出した場合、ルームを削除します
- すべてのプレイヤーに新しいルーム リスト情報を送信します。
Socket通信
ここが要点ですが、冒頭でネットワークプログラミングを学びたいと言いました。最後に、C# でのソケット プログラミングについて簡単に紹介します。もちろん、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);
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 イベントがトリガーされ、クライアントに入ることができないことが通知されます。
-
サーバーがいっぱいでない場合は、着信ソケット接続をカプセル化し、プレーヤー コレクションに追加します。
-
このソケットからメッセージの受信を開始します
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);
終わり
もちろん、実際のプログラミングでは、次のような多くの問題に遭遇します。
- ソケット接続の正常切断と異常切断の問題。
- イベント ドリブン モデルでは、イベント リスナーは直接参照されなくなり、パブリッシャーにはまだ参照があり、ガベージ コレクターはそれをリサイクルできません。複数の画面にイベント リスナーがあると、混乱が生じます。待って。