プレイ「マルチプレイヤーオンラインゲームの開発」パート2:これはひどい言葉「プロトコル」です。



パート1:アーキテクチャ
パート3:クライアントとサーバーの相互作用
パート4:3Dへの移行

そのため、マルチプレイヤーゲームを作成し続けます。
今日は、データ転送プロトコルの作成を検討します。
また、TCPサーバーブランクを作成し、それに応じてクライアントを作成します。



パート2 ステップ1:データ転送プロトコル。



念のために、特に退屈な読者には、地震、SC、またはそのようなものを開発していないことを思い出させます。 主なタスクは、動作するアプリケーションの例を使用して、一般的な原則とアプローチを示すことです。

外観上、非常に恐ろしく複雑な「データ転送プロトコルの作成」に聞こえますが、悪魔は彼が描かれているほどひどくはありません...

ゲーム開発のプロトコルとして一般的に理解されているものは何ですか? いいえ、これは自転車ソケットの独自の実装ではありません...また、独自のhttpプロトコルではありません...一般に、これはサーバーとクライアント間の伝送のためにデータをパックする方法です。 たとえば、マップ上の戦車の位置に関する情報を保存するDataオブジェクトがあります。 この情報をサーバーに転送する方法は? 標準のシリアル化オプションがあります。 多くのプログラミング言語は、シリアル化/逆シリアル化プロセスを非常に単純にします。 ほんの数行のコード。 そして、それは動作します...しかし、いつものように落とし穴があります。 たとえば、シリアル化/逆シリアル化の速度、受信したオブジェクトのサイズ...、およびクライアントの速度をまだ無視できる場合、サーバーは各クライアントからのメッセージを解析する必要があるため、サーバーをタイトにする必要があります...誰もネットワークに余分な負荷を必要としません。 別のポイントがあります、これはクロスプラットフォームです。 ここでは、サーバーはscala上にあり、クライアントはフラッシュ上にあります。 ですから、オブジェクトを直接交換することはできません。

したがって、このプロセスにはさまざまな実装があります。 グローバルには、バイナリとテキストの2つのタイプに分類できます。 XMLやJSONなどのテキスト形式。 彼らと働くことは非常に簡単です。 XMLはおそらく最も一般的なオプションです。 しかし、リアルタイムゲームには適していません。 XMLの解析のオーバーヘッドが非常に大きく、大量の送信データ(XMLマークアップで多くのスペースを占有する)が実際にその使用に終止符を打ちます。 JSONはより軽量で、マークアップはXMLよりもはるかに少ないスペースを使用しますが、それでもバイナリプロトコルと比較することはできません。 ターンベースのゲームには適していますが、非常に適しています。

バイナリプロトコルに関しては、XMLやJSONのようなマークアップはありません。これにより、送信されるデータの量とメッセージの解析時間が大幅に削減されます。 バイナリプロトコルの例は、AMF、protobufです。 Protobufは一般的に非常に優れています。 Googleがそのサービスで開発および使用しました。 多くの言語で実装されています。 そして、一般的な場合、私はそれまたはそのアナログを使用することをお勧めします。

しかし、私たちはここで勉強しており、比較的単純なプロジェクトがあります。 したがって、私たちは自分の快適な自転車を作ります。

通常、サーバーとクライアント間で転送する必要があると想像してみましょう。

クライアントからサーバーへのメッセージ
1.認証(ログイン)
2.移動時のタンクの座標(x、y)
3.ヒットの有無をサーバー上で計算するための、ショットの座標(x、y)、プレーヤーが発射した場所
4.チームはサーバー上のプレーヤーの数を要求します
5.チームを終了する

サーバーからクライアントへのメッセージ
1.確認の承認
2.敵の戦車とその状態(生きている/殺された)の座標(x、y、s)
3.チームは「ヒット」します
4.プレイヤーの数
5.リクエストに応じたクライアントの切断

要約すると、すべての送信データを7バイトに収めることができます。



これは、移動時および射撃時のタンクの座標を含むメイン送信パケットです。 他のデータを転送する必要がある場合、これらの7バイトにそれらを配置します。 たとえば、X、Y座標の代わりにアクティブユーザーの数を転送します。

その結果、非常に単純化されたバージョンではありますが、非常にコンパクトで高速になりましたが、私たちの目的には非常に適しています。 結局のところ、送信データはほとんどありません。

将来的には、プロトコルを改善します。 しかし、それは次の部分になります。

パート2 アクション2:TCPサーバー。



現時点では、サーバースタブを作成する必要があります。 タスクは次のとおりです。
1.顧客をつなぐ
2.接続されたクライアントにIDを割り当てる

それだけです。この段階では他に何も必要ないので、コードは非常に単純です。
ここでscalaアプリケーションを作成しました

def main(args: Array[ String ]): Unit =
{
val port = 7777

try
{
val listener = new ServerSocket(port)
var numClients = 1

println( "Listening on port " + port)

while ( true )
{
new ClientHandler(listener.accept(), numClients).start()
numClients += 1
}

listener.close()
}
catch
{
case e: IOException =>
System.err.println( "Could not listen on port: " + port + "." )
System.exit(-1)
}
}


* This source code was highlighted with Source Code Highlighter .
def main(args: Array[ String ]): Unit =
{
val port = 7777

try
{
val listener = new ServerSocket(port)
var numClients = 1

println( "Listening on port " + port)

while ( true )
{
new ClientHandler(listener.accept(), numClients).start()
numClients += 1
}

listener.close()
}
catch
{
case e: IOException =>
System.err.println( "Could not listen on port: " + port + "." )
System.exit(-1)
}
}


* This source code was highlighted with Source Code Highlighter .


接続ハンドラー。

class ClientHandler (socket : Socket, clientId : Int) extends Actor
{
def act
{
try
{
val out = new PrintWriter( socket.getOutputStream(), true )
val in = new BufferedReader( new InputStreamReader(socket.getInputStream()) )

print( "Client connected from " + socket.getInetAddress() + ":" + socket.getPort )
println( " assigning id " + clientId)

var inputLine = in .readLine()
while (inputLine != null )
{
println(clientId + ") " + inputLine)

inputLine = in .readLine()
}

socket.close()

println( "Client " + clientId + " exit" )
}
catch
{
case e: SocketException => System.err.println(e)

case e: IOException => System.err.println(e.getMessage)

case e => System.err.println( "Unknown error " + e)
}
}
}


* This source code was highlighted with Source Code Highlighter .
class ClientHandler (socket : Socket, clientId : Int) extends Actor
{
def act
{
try
{
val out = new PrintWriter( socket.getOutputStream(), true )
val in = new BufferedReader( new InputStreamReader(socket.getInputStream()) )

print( "Client connected from " + socket.getInetAddress() + ":" + socket.getPort )
println( " assigning id " + clientId)

var inputLine = in .readLine()
while (inputLine != null )
{
println(clientId + ") " + inputLine)

inputLine = in .readLine()
}

socket.close()

println( "Client " + clientId + " exit" )
}
catch
{
case e: SocketException => System.err.println(e)

case e: IOException => System.err.println(e.getMessage)

case e => System.err.println( "Unknown error " + e)
}
}
}


* This source code was highlighted with Source Code Highlighter .


コードは究極の真実ではありません。 最適化せずに、誰にとっても最も理解しやすい方法で書きますので、あまり蹴らないでください。建設的な批判がある場合は、kamentyへようこそ。

コードが不適切に説明されることを望みますか? 彼はとてもシンプルです。

これは単なる空白なので、ハードコードのポート番号などがあります。 次の部分では、サーバーを組み合わせてリファクタリングを実行します。

パート2 アクション3:クライアント。



プロトコルを実装して検証するには、クライアントの調達も必要です。
この段階で必要なタスクは非常に簡単です。
1.サーバーに接続します。
2.状態を表示します。
これで終わりです。今はもう必要ありません。

クライアントコードも非常に単純であり、コメントはしません。

public class Main extends Sprite
{
public var socket:Socket = new Socket();

public var host: String = "127.0.0.1" ;
public var port: int = 7777;

public var status:TextField = new TextField();

public function Main(): void
{
if (stage) init();
else addEventListener(Event.ADDED_TO_STAGE, init);
}

private function init(e:Event = null ): void
{
removeEventListener(Event.ADDED_TO_STAGE, init);
// entry point

status.text = "Player" ;
addChild( status );

socket.addEventListener(Event.CONNECT, socketConnectHandler);
socket.addEventListener(IOErrorEvent.IO_ERROR, ioErrorHandler);
socket.addEventListener(DataEvent.DATA, dataHandler);

socket.connect(host, port);
}


//
private function socketConnectHandler(e:Event): void
{
status.text = "Player - connectrd" ;
}

//
private function ioErrorHandler(e:IOErrorEvent): void
{
status.text = "Player - error" ;
}

//
private function dataHandler(e:DataEvent): void
{
switch (e.data)
{
//
}
}

//
public function sendMessage(val: String ): void {
if (val != "" && socket.connected)
{
//socket.writeBytes( val);
}
}

public function exitGame(): void
{
if (socket.connected)
{
socket.close();
}
}

}


* This source code was highlighted with Source Code Highlighter .
public class Main extends Sprite
{
public var socket:Socket = new Socket();

public var host: String = "127.0.0.1" ;
public var port: int = 7777;

public var status:TextField = new TextField();

public function Main(): void
{
if (stage) init();
else addEventListener(Event.ADDED_TO_STAGE, init);
}

private function init(e:Event = null ): void
{
removeEventListener(Event.ADDED_TO_STAGE, init);
// entry point

status.text = "Player" ;
addChild( status );

socket.addEventListener(Event.CONNECT, socketConnectHandler);
socket.addEventListener(IOErrorEvent.IO_ERROR, ioErrorHandler);
socket.addEventListener(DataEvent.DATA, dataHandler);

socket.connect(host, port);
}


//
private function socketConnectHandler(e:Event): void
{
status.text = "Player - connectrd" ;
}

//
private function ioErrorHandler(e:IOErrorEvent): void
{
status.text = "Player - error" ;
}

//
private function dataHandler(e:DataEvent): void
{
switch (e.data)
{
//
}
}

//
public function sendMessage(val: String ): void {
if (val != "" && socket.connected)
{
//socket.writeBytes( val);
}
}

public function exitGame(): void
{
if (socket.connected)
{
socket.close();
}
}

}


* This source code was highlighted with Source Code Highlighter .


これで、サーバーとクライアント間の最初の接続を確立する準備がすべて整いました。
サーバーを起動し、クライアントを起動し...そして出来上がり...

顧客の結果



クライアントがサーバーに正常に接続したことがわかります。

サーバー結果



サーバーがポート7777で起動し、クライアントから接続を受け取り、IDを与えたことがわかります。

今日は以上です。
次の部分では、プロトコルを実装し、結果として生じるクライアントサーバーの相互作用を実際にチェックします。

いつものように、すべてのソースはGithubで表示できます

更新しました。 コメントするとき、これは「5分で地震を書く方法」のチュートリアルではないことに注意してください...これは、ネットワークゲームの開発の単純なものから複雑なものまでの連続的なプレゼンテーションです。 最初のタスクは、ScalaサーバーとFlashクライアント間のネットワーク通信を行うことです。 そして、この相互作用に基づいてゲームを作ります。

Source: https://habr.com/ru/post/J123748/


All Articles