プレイ「マルチプレイヤーオンラインゲームの開発」パート3:クライアントとサーバーの相互作用





パート1:アーキテクチャ

パート2:プロトコル

パート4:3Dへの移行



第三部では、少し遅れました。 しかし、彼らは決して遅くないほうが良いと言うように...



それで、会話を続けます。



ステートメントの3番目の部分では、プロトコルを実装し、ネットワークを介して対話するサーバーとクライアントを作成します。 そして(OMG!)戦車が乗ります!

カットの下で、あなたが長い間望んでいたが、尋ねることを恐れていたもの...







特に風変わりなことに、この記事のコード全体が「SuperPuperMegaFiguishingFasteningIntelligent solution to all issues」のタイトルのふりをしているわけではないことを思い出してください。 コードは、主要なポイントなどを示すように設計されています。 それはいくつかの場所ではく、最適ではありませんが、本質の本質を伝えます。



前回の記事以降、多くのイベントが発生しました。 その1つは、IDEAでScalaの開発に切り替えたことです。 理由は単純です-NetBeansのプラグインは完全にくだらないです...したがって、bitbucketのプロジェクトはNetBeansからIDEAに変更されているので、心配しないでください。 IDEAの第一印象はあまりポジティブではありませんが、このサボテンを噛んでみます。



パート3。 アクション1:アーキテクチャの問題は何ですか?





アーキテクチャーがあることを思い出してください...







彼女は岩の中の俳優によく横たわっています。 接続を受け入れる1つのプロセス(GameServer)があり、接続が確立された後、処理のためにチャネルをアクター(ClientHandler)に渡すことがわかります。 したがって、各クライアントに対してアクターが作成され、クライアントとの通信を担当します。 クライアントにメッセージを送信するには、アクターに送信するだけで、アクターはクライアントにメッセージを送信して回答を受け入れます。 実際、岩の中の俳優は非常に興味深いものです。 それらは、ほとんどすべてのくしゃみに対して何万人も作成できます。 岩の上にアクターの別の実装、Akkaプロジェクトがあります。 彼ははるかに洗練されています。 実際のプロジェクトでは、彼を見ることは理にかなっています。



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





まず、Playerプレーヤークラスを作成します。 プレイヤーIDとその座標を保存します。

class Player(idd: Int, xx: Int, yy: Int) { var id = idd var x = xx var y = yy }
      
      







最も簡単なプロトコルがありますが、これを実装するには、2つのクラスを作成する必要があります。 メッセージが保存されるパケットクラス。

 class Packet ( comm:Int, player: Player ) { val com = comm //  val id = player.id // id     val x = player.x val y = player.y }
      
      







およびクラスのエンコードおよびデコードメッセージ

 object Protocol { //  def encode( packet: Packet ): ByteBuffer = { val rez: ByteBuffer = ByteBuffer.allocate(16) rez.putInt(packet.com) rez.putInt(packet.id) rez.putInt(packet.x) rez.putInt(packet.y) rez } //  def decode( buffer: ByteBuffer ): Packet = { val com = buffer.getInt(0) val idd = buffer.getInt(4) val xx = buffer.getInt(8) val yy = buffer.getInt(12) val rez: Packet = new Packet( com, new Player(idd, xx, yy) ) rez } }
      
      





プロトコル実装の準備ができました。 ご覧のとおり、最も単純なケースでは大丈夫です。 しかし、実際のプロジェクトでは、自転車を発明する大きな理由がなければなりません。 既成の、実績のあるソリューションをより適切に使用します。



パート3。 ステップ3:サーバー、この単語の量...





ゲームのフレームを作成している間。 したがって、サーバーは純粋に名目上の作業を実行します。 クライアント接続を処理し、クライアント間の通信を提供します。 将来的には、それを改良します。



GameServerクラスを作成する

 object GameServer extends Runnable { var isActive = true var selector: Selector = null var numClients = 0 var port = 7778 //              var sessions = new HashMap[ SocketChannel, Actor ] var lock: AnyRef = new Object() //            def addPlayerMsg(player: Player) { lock.synchronized { ..... } } //   def init(portt: Int) { port = portt try { selector = Selector.open println( "Selector opened" ) } catch { case e => println( "Problems during Socket Selector init: " + e ) } } override def run() { //   bindSocket( "", port) //    while ( isActive ) { Loop() } } def Loop() { if ( selector.select > 0 ) { val it = selector.selectedKeys().iterator() while ( it.hasNext ) { val skey = it.next it.remove() if ( !skey.isValid ) { continue() } //  if ( skey.isAcceptable ) { val socket:ServerSocketChannel = skey.channel().asInstanceOf[ServerSocketChannel] try { numClients = numClients + 1 val channel = socket.accept channel.configureBlocking(false) channel.register(selector, SelectionKey.OP_READ) //      val player = new Player(numClients, numClients * 20, numClients * 20) val actor = new ClientHandler(player, channel) actor.start() //      val packet = new Packet( 0, player) actor.packets += packet //     sessions += channel -> actor println( "Accepted connection from:" + channel.socket().getInetAddress + ":" + channel.socket().getPort ) } catch { case e: Exception => println( "Game Loop Exception: " + e.getMessage ) } } //            else { val channel:SocketChannel = skey.channel.asInstanceOf[SocketChannel] val actor = sessions.get(channel).get.asInstanceOf[ClientHandler] if ( actor.packets.size > 0 ) { skey.interestOps(SelectionKey.OP_WRITE) } actor ! skey } } } } def close(remoteAddress:String, channel:SocketChannel) { channel.close(); println("Session close: " + remoteAddress); } //   def bindSocket(address: String, port: Int) { try { //   val hostAddr: InetAddress = null val isa = new InetSocketAddress(hostAddr, port) val serverChannel = ServerSocketChannel.open serverChannel.configureBlocking(false) serverChannel.socket.bind(isa) serverChannel.socket.setReuseAddress(true) serverChannel.register(selector, SelectionKey.OP_ACCEPT ) println( "Listening game on port: " + port ) } catch { case e: IOException => println("Could not listen on port: " + port + ".") System.exit(-1) case e => println("Unknown error " + e) System.exit(-1) } } }
      
      





サーバーは垂直であることがわかりました。 別のスレッドで起動されます。 彼はパンを求めません。 カスタマーサービスのみがここに表示されます。 クライアントの正しい接続/切断を処理する部品はありません。 セッション管理なし。 しかし、誰でも好きなようにこれを変更できます。



パート3。 第4幕:俳優はまだ俳優です。





次に、クライアント接続を処理するアクターを作成します。

 class ClientHandler(player: Player, chanel:SocketChannel) extends Actor { val player_id = player.id val channel = chanel val remoteAddress = channel.socket().getRemoteSocketAddress.toString var packets = new HashSet[ Packet ] def act() { loop { receive { //   case key: SelectionKey => { try { //   if (key.isReadable) { val buffer = ByteBuffer.allocate(16) channel.read(buffer) match { case -1 => close(remoteAddress, channel) case 0 => case x => processMessageRead(key, buffer) } } //   else if (key.isWritable) { packets.synchronized { for(packet <- packets) { processMessageWrite( Protocol.encode(packet) ) packets.remove(packet) } } if(packets.isEmpty) key.interestOps(SelectionKey.OP_READ) } } catch { case e: SocketException => println("ClientHandler SocketException error " + e.getMessage) case e: IOException => println("ClientHandler IOException error " + e.getMessage) case e => println("ClientHandler Unknown error " + e.getMessage) } } } } } //    def processMessageRead(key: SelectionKey, buffer: ByteBuffer) { if ( buffer.limit() == 0 ) { return } buffer.flip val protocol = Protocol.decode( buffer ) println( "Client : " + player.id + " - " + new Date + " - " + "com:" + protocol.com + " x:" + protocol.x + " y:" + protocol.y ) player.x = protocol.x player.y = protocol.y buffer.clear if (protocol.com == 0) { key.interestOps(SelectionKey.OP_WRITE) } else if (protocol.com == 1) { GameServer.addPlayerMsg(player) } } //    def processMessageWrite(buffer: ByteBuffer) { if ( buffer.limit() == 0 ) { return } buffer.flip channel.write(buffer) println( "Client write: " + player.id + " - " + new Date + " - " + buffer.array().mkString(":") ) buffer.clear } def close(remoteAddress: String, channel: SocketChannel) { channel.close() println("Session " + player.id + " close: " + remoteAddress) } }
      
      





俳優は棒のように簡単であることが判明しました。 彼はメッセージだけを受け入れて送信します。



負荷テストのために、かなり弱いラップトップ(1.3 GHz、AMD、WiFi 56Mbit)でサーバーを起動しました。 また、クライアントとして、指定された数のスレッドを起動し、一時停止することなく継続的にサーバーにパケットを送信するコンソールJavaアプリケーションを作成しました。 クライアントは、100スレッドでデスクトップ(3.6 GHz、4コア)で起動されました。

その結果、サーバーは1秒あたり約6000メッセージを消化しました。 これは一般的に悪くありません。 コンピューティングの負荷に応じて、実際のサーバーハードウェアでは、数千のクライアントを保持できます。



パート3。 5番目のアクション:クライアント...および他の誰ですか?





最後の部分のクライアントは変更されていません。 戦車の形のプレイヤーのグラフィカルな表示とプロトコルの実装のみを追加しました。



プレーヤーを説明するクラスを追加します

  public class Player extends MovieClip { public var Name:String = "Player"; public var id:int = 0; [Embed(source = '../../../../lib/tank.png')] public var _tank: Class; public var tank:Bitmap; public function Player() { width = 30; height = 30; tank = new _tank(); tank.width = 30; tank.height = 30; addChild(tank); } }
      
      





また、プロトコルを実装するメソッドを追加します。

  //  public function sendMessage(val:int):void { if (socket.connected) { var bytes:ByteArray = new ByteArray(); bytes.writeInt(val); bytes.writeInt(player.id); bytes.writeInt(player.x); bytes.writeInt(player.y); socket.writeBytes( bytes, 0, 16); socket.flush(); } } //    private function dataHandler(e:ProgressEvent):void { var bytes:ByteArray = new ByteArray(); socket.readBytes(bytes, 0, 16); var com:int = bytes.readInt(); var id:int = bytes.readInt(); var x:int = bytes.readInt(); var y:int = bytes.readInt(); switch (com) { //   case 0: … //  case 1: ... } }
      
      







これで、クライアントとサーバー間の基本的な相互作用の準備ができました。



クライアントの正しい接続/切断、クライアントの同期(移動中に戦車がひきつる)の問題は解決されません。 それはすべて次の部分で私たちを待っています...



PS多くのコード...パーツを削除して、メソッドの説明だけを残すことができますか?



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



All Articles