ネットワーク燃料争奪ゲーム

  1. 参考文献
    この内容は小高著、「Javaネットワーク入門」の解説(ゼミ資料)です。クライアントソフトはJbuilderに移植しレイアウトを変更しました。

  2. 内容
    海上にランダムに置かれる燃料を船で移動して取得し、その個数を競います。参加する人は、サーバーに名前を指定してログインをします。これで、自分の名前の船が表示されます。船をボタンで4方向に移動します。最初に燃料近くの場所に到着した船が燃料を獲得します。

  3. 手法
    1. サーバー・クライアント方式
      ここでは、TCP/IPを用いたサーバークライアント方式でゲームを行います。ゲームを実行するには、まず、サーバーソフトを立ち上げます。次に、ゲームに参加する人はクライアントソフトを立ち上げ、サーバーにログインします。サーバーはログインした名前を登録し参加者のデータを登録します。
       各クライアントは定期的にサーバーから新しい情報を取り出し画面に表示します。新規参加者のデータは、新規登録後にサーバーから情報を取得すると、見ることができます。クライアントの操作はすべてサーバーに送られ、そこで処理した結果をサーバーから取り出し、描画します。
       サーバーにログアウトコマンドを送ると、サーバーを登録を抹消します。

    2. サーバー

      1. ゲームの進行
        このゲームはサーバー側ですべての情報を制御します。サーバーはすべての船の位置と燃料の位置を知っており、クライアント側の要求でこの情報を送ります。クライアントは船の移動要求を送り、サーバーは船の状態と燃料の状態を更新します。

      2. ネットワーク
        サーバーはポート番号2000でクライアントの接続を待ちます。接続の要求があると、ソケットを作成しそのソケットの受信処理を行う専用のスレッドを作成します。したがって、接続したクライアントの数のスレッドが並列処理を行います。

      3. サーバーソケット
        サーバーではServerSocketクラスを利用します。
         ServerSocket serverSocket;
        でソケットを定義し、
         serverSocket = new ServerSocket(DEFAULT_PORT);
        で生成します。クライアントから、接続要求が到着したら
         Socket cs = serverSocket.accept();
        で、ソケットcsを生成します。次に、クライアントの処理を行うclientProcクラスのスレッドを生成し
         Thread ct = new Thread(new clientProc(cs));
        スレッドctを起動します。
          ct.start();
        各スレッドは接続したクライアントからの要求に基づき、処理を行います。接続した各クライアントに専用のスレッドで対応しますから、clientProc(cs)は特定のクライアントに対応する処理を行えばよいことになります。ここでは、単純な(モードレスな)処理ですから、スレッドの処理は必要ありませんが、複雑なサービス処理をするには、スレッドで対応するのは処理を簡単にできます。

      4. サーバーのネット処理:clientProc
        クライアントへのサービスを行います。コンストラクタでソケットsに対するストリーム入出力を開きます。
        public clientProc(Socket s) throws IOException {
          this.s = s;
          in = new BufferedReader(new InputStreamReader(s.getInputStream()));
          out = new PrintWriter(s.getOutputStream());
         }
        スレッドのRun()で、クライアントからのメッセージを受け取り
         String line = in.readLine();
        それを処理し、必要ならoutに応答を返します。in.readLine();で、ソケットにメッセージを受信するまで、このスレッドはsleep(休止)します。もし、スレッドで受信しないと、全体が休止してしまいます。

    3. クライアント

      1. 接続要求
        クライアントは、ログインボタンでサーバーにサーバーの名前とポート番号2000を指定してソケットserverを作成します。
         server = new Socket(host, port);
        ソケットから、入出力クラスを生成します。
         in = new BufferedReader(new InputStreamReader(server.getInputStream()));
         out = new PrintWriter(server.getOutputStream());
        次に、自分の名前(name)を付加してログインコマンドをサーバーに送ります。
         out.println("login " + name);

      2. 表示
        クライアントは定期的にサーバーに現在の状態の送信を要求し(statコマンド)、そのデータをもとにすべての船の状態と燃料の位置を表示します。これで、自分の船の位置を確認できます。

      3. 移動処理
        クライアントは移動ボタンでサーバーに移動情報を出します。サーバーは各船の移動要求に基づき位置データや、確保した燃料の数を更新します。また、ログアウトボタンでログアウト処理を行います。

    4. コマンド(データ)形式
      サーバーとクライアントで交換するコマンド(データ形式)は以下のようです。

      1. login、logoutコマンド
        クライアントがサーバーにおくります
         login 名前
         logout
        サーバーは応答を返しません。

      2. statコマンド
        クライアントはサーバーにstatコマンドを送ります。
         stat
        サーバーは船の位置と捕獲した燃料の数、および、燃料の位置情報を返します。
        ship_info
        <name> <x> <y> <確保した燃料の数>
        .....
        energy_info
        <x> <y>
        .....

      3. 移動コマンド
        クライアントからのボタン操作で、次のコマンドをサーバーに送ります。
        left、right、up、down

  4. サーバープロジェクト
    1. 構成法
      原版は、JDKを用いて作成されています。したがって、必要ならUmiServer.javaをエディタで修正し、javacコマンドでコンパイルします。

    2. UmiServerクラス
      1. 変数
        Vector connections; 接続したソケットを記録する
        Vector energy_v;  燃料の位置情報を記録する
        Hashtable userTable ;クライアント情報を記録する

      2. main
        サーバーソケットを作成し、燃料を作成するスレッドetを生成します。また、以下のように、serverSocket.accept()でクライアントからのログインメッセージを受け、各クライアントに対してサービスを行うclientProcクラスのスレッドctを生成し、起動します。
        while (true) {// 無限ループ
            try {
                    Socket cs = serverSocket.accept();
                    addConnection(cs);// コネクションを登録します
                    // クライアント処理スレッドを作成します
                    Thread ct = new Thread(new clientProc(cs));
                    ct.start();
                 }catch (IOException e){
                        System.err.println("client socket or accept error.");
                 }
            }

      3. addConnection(Socket s)、deleteConnection(Socket s)
        ログインやログアウトの処理を行います。

      4. loginUser()
        名前と船の位置をHashtable()に記録します。Hashtableは名前で検索することができます。
         public static void loginUser(String name){
             if (userTable == null){// 登録用テーブルがなければ作成します
                 userTable = new Hashtable();
             }
             if (random == null){// 乱数の準備をします
                 random = new Random();
             }
             // 船の初期位置を乱数で決定します
             int ix = Math.abs(random.nextInt()) % 256;
             int iy = Math.abs(random.nextInt()) % 256;
        
             // クライアントの名前や船の位置をハッシュ表userTablweに登録します
             userTable.put(name, new Ship(ix, iy));
             // サーバ側の画面にもクライアントの名前を表示します
             System.out.println("login:" + name);
             System.out.flush();
         }

      5. left,right,up,down
        ハッシュ機能を利用して名前から船の情報を取り出し、位置を更新します。また、calculationで燃料の獲得処理をします。

      6. calculation
        燃料に到達している船を調べ、pointを更新します。

      7. statInfo
        船と燃料の情報をクライアントに送ります。

    3. ClientProcクラス:スレッド
      1. 変数
        BufferedReader in; ソケットからメッセージを受信するストリーム
        PrintWriter out;ソケットからメッセージを送信するストリーム

      2. run
        クライアントからのコマンドを受け取り、対応する処理を実行します。nameが空の場合、UmiServer.loginUser(name);でログイン処理を行います。それ以外の場合、受信したコマンドにより、対応する処理を行います。
        public void run(){
          try {
              //LOGOUTコマンド受信まで繰り返します
              while (true) {
                  // クライアントからの入力を読み取ります
                  String line = in.readLine();
                  // nameが空の場合にはLOGINコマンドのみを受け付けます
                  if (name == null){
                      StringTokenizer st = new StringTokenizer(line);
                      String cmd = st.nextToken();
                      if ("login".equalsIgnoreCase(cmd)){
                          name = st.nextToken();
                          UmiServer.loginUser(name);
                      }else{
                          // LOGINコマンド以外は,すべて無視します
                      }
                  }else{
                  // nameが空でない場合はログイン済みですから,コマンドを受け付けます
                      StringTokenizer st = new StringTokenizer(line);
                      String cmd = st.nextToken();// コマンドの取り出し
                      // コマンドを調べ,対応する処理を行います
                      if ("STAT".equalsIgnoreCase(cmd)){
                          UmiServer.statInfo(out);
                      } else if ("UP".equalsIgnoreCase(cmd)){
                          UmiServer.up(name);
                      ....
                      } else if ("LOGOUT".equalsIgnoreCase(cmd)){
                          UmiServer.logoutUser(name);
                          break;
                      }
                  }
              }
              // 登録情報を削除し,接続を切断します.
              UmiServer.deleteConnection(s);
              s.close();
          }catch (IOException e){
              try {
                  s.close();
              }catch (IOException e2){}
            }
          }
        }

    4. shipクラス
      1. 変数
        位置、x,yと確保した燃料の数point
      2. reft,right,up,down
        船を10単位進めます

  5. クライアントプロジェクト

    1. Jbuilder
      Jbiulderに移植しました。ネットワークを利用しているため、アプレットでは動作しません。(アプレットではネット環境が制限されます。)そこで、アップレットをスタンドアロンにするmain()を組み込み、アプリケーションとして動作可能な構成にします。

    2. UmiClientクラス
      1. レイアウト
        login、logoutボタンをフレームの下に配置し、イベント処理を行うメソッドを定義します。
        また、4方向に移動するボタンを配置し、イベント処理を行うメソッドを定義します。
        中央の250*250に海を表示する領域を確保します。

      2. run
        スレッドで、500ms間隔で再描画を行います。

      3. repaint
        再描画で呼び出されます。statコマンドを送り、その応答メッセージで船と燃料を描画します。
          public void repaint(){
          // サーバにstatコマンドを送付し,盤面の様子などの情報を得ます
          if( !dlg.OKflag) return;
          out.println("stat");
          out.flush();
          //System.out.println("repaint");
          try {
              String line = in.readLine();// サーバからの入力の読み込み
              Graphics g = getGraphics();// Canvas cに海の様子を描きます
        
              // 海の描画(単なる青い四角形です)
              g.setColor(Color.blue);
              g.fillRect(0, 0, 256, 256);
              //ship_infoから始まる船の情報の先頭行を探します
              while (!"ship_info".equalsIgnoreCase(line))
                  line = in.readLine();
        
              // 船の情報ship_infoの表示
              // ship_infoはピリオドのみの行で終了です
              line = in.readLine();
              while (!".".equals(line)){
                  StringTokenizer st = new StringTokenizer(line);
                  // 名前を読み取ります
                  String obj_name = st.nextToken().trim();
                  // 自分の船は赤(red)で表示し,他人の船は緑(green)で表示します
                  if (obj_name.equals(name))//自分の船
                      g.setColor(Color.red);
                  else // 他人の船
                      g.setColor(Color.green);
                  // 船の位置座標を読み取ります
                  int x = Integer.parseInt(st.nextToken()) ;
                  int y = Integer.parseInt(st.nextToken()) ;
                  // 船を表示します
                  g.fillOval(x - 10, 256 - y - 10, 20, 20);
                  // 得点を船の右下に表示します
                  g.drawString(st.nextToken(),x+10,256-y+10) ;
                  // 名前を船の右上に表示します
                  g.drawString(obj_name,x+10,256-y-10) ;
                  // 次の一行を読み取ります
                  line = in.readLine();
              }
        
              // energy_infoから始まる,燃料タンクの情報を待ち受けます
           ....
          }catch (Exception e){
              e.printStackTrace();
              System.exit(1);
          }

      4. LogIn_actionPerformed(ActionEvent e)
        ログインをするためのloginDlgクラスのダイアログを可視にします。実際のログイン処理は、loginDlgのOKボタンの処理で実行します。

      5. LogOut_actionPerformed
        logoutボタンで起動します。ログアウトコマンドを送ります。

    3. loginDlgクラス
      1. ログインダイアログ
        ホスト名と利用者名を入力するTextFieldを配置します。また、OKボタンを配置します。

      2. コンストラクタ
        コンストラクタで親のオブジェクト(this)を受け取り、parentに記録します。

      3. OKボタン
        OKボタンでparentを利用して、親で定義されたソケットに、in,outストリームを設定し、サーバーにログインコマンドを送ります。OKflagをtrueにし、repaintで船と燃料の表示を開始し、このダイアログを不可視とします。

  6. 実行
    1. 実行環境
      適当なサーバーマシンがない場合でも、ネットワーク機能が組み込まれたPCなら、一台でサーバーと複数のクライアントを立ち上げ、実験ができます。この場合、サーバーマシンとしてlocalhost(127.0.0.1)を指定します。

    2. 実行コマンド
      まず、コマンドプロンプトを起動し、次のようにサーバーを起動します。
       G:\java\netGame\server>java UmiServer

       次にクライアントを起動します。Jbuilderから「実行」メニューで実行することは可能ですが、アプレットとして、ブラウザからは実行できません(サーバーとの接続に失敗します)。
       コマンドで実行する場合、サーバーを起動したコマンドプロンプトは実行中ですから利用できません。別のコマンドプロンプトを利用します。クライアントはumiclientフォルダのApplet1.classにありますから
       G:\java\netGamei\client>java umiclient/Applet1
      で起動します。

       クライアントを起動したら、ログインボタンを押しログインダイアログを出します。hostにlocalhost、名前に適当な名前を設定し、OKボタンを押します。ログインが成功すると、海の画面が表示されますから、4方向のボタンで船を移動し、燃料を獲得します。終了するにはログアウトボタンを押します。サーバーを終了するには、サーバーのコマンドラインでcntrl-Cを押します。
       同じようにして、一台のPCで好きな数のクライアントを立ち上げることができます。ネットワークに接続されている場合、複数のPCでクライアントを立ち上げることが可能です。この場合、ログイン画面でhostにサーバーのIPまたは公開されている名前を指定します。

    3. 実行画面
      1. サーバー
        ログイン、ログアウトをするとメッセージが表示されます。ウインドウは表示されません。
        G:\java\sample\netGameUmi\server>java UmiServer
        login:mito
        logout:mito
         
      2. クライアント画面
        Loginボタンでログインします。右のログインウインドウが開きます。hostにlocalhaost、nameに適当な名前を記入し、okボタンを押します。緑の大きな○が船、小さな○が燃料です。方向のボタンをクリックして船を移動します。燃料に到着すると、獲得数が表示されます。これだけのプログラムです。

           

    4. ダウンロード
      このプロジェクトをダウンロードできます。次の行をクリックして、netgame.exeファイルを適当なフォルダに保存します。
      ダウンロード開始
      このファイルは自己解凍型の圧縮ファイルです。このファイルを実行すると指定したフォルダに必要なファイルが生成されます。