Android で実行するネットワーク対応の○×ゲームを作成する

PHP、XML、Android 開発キットを使用し、ネットワーク対応でマルチプレイヤーの○×ゲームを作成する

この記事では、フロントエンドとして Android のネイティブ・アプリケーションを使用し、マルチプレイヤーでネットワーク対応の○×ゲームのバックエンドを作成します。

Jack D. Herrington, Senior Software Engineer, Fortify Software, Inc.

Photo of Jack HerringtonJack Herrington はサンフランシスコ湾岸地域に住む技術者、著作者、講演者です。彼の最新の活動や記事を http://jackherrington.com で知ることができます。



2011年 9月 30日

ネットワーク対応でマルチプレイヤーの○×ゲーム

よく使われる頭文字語

  • API: Application Programming Interface
  • HTTP: HyperText Transfer Protocol
  • IP: Internet Protocol
  • SDK: Software Development Kit
  • SQL: Structured Query Language
  • UI: User Interface
  • XML: Extensible Markup Language

カジュアル・ゲームは非常に人気があり、それによる収益も非常に大きいですが、その理由は容易に理解することができます。光速並みの反射神経を持つ小学生たちを相手に、オンラインでファーストパーソン・シューティング・ゲームをプレイすることに、すべての年齢層の誰もが興味を持っているわけではありません。考えたり戦略を練ったりする時間があるゲームや、互いに協力しながら勝つことが目標のゲームでプレイする方が面白い場合があります。

開発者の観点から見た場合、カジュアル・ゲームの素晴らしい点は、グラフィックスを多用するファーストパーソン・シューティング・ゲームやスポーツ・ゲームよりも作成が容易なことです。そのため、1 人の開発者、または複数の開発者からなる 1 つのグループでも、今までにない新しいゲームを容易に作り出すことができます。

この記事では、ネットワーク対応でマルチプレイヤーのカジュアル・ゲームとして、○×ゲームを作成するための基本事項について説明します。このゲームのサーバーは MySQL と PHP による Web アプリケーション・サーバーであり、XML インターフェースを持っています。フロントエンドは Android ネイティブ・アプリケーションであり、Android フォンで動作します。


バックエンドを作成する

バックエンドの作成は、2 つのテーブルを持つ単純な MySQL データベースを作成することから始めます。リスト 1 は、このデータベースのスキーマを示しています。

リスト 1. db.sql
DROP TABLE IF EXISTS games;
CREATE TABLE games(
     id INT NOT NULL AUTO_INCREMENT,
     primary key ( id ) );

DROP TABLE IF EXISTS moves;
CREATE TABLE moves(
     id INT NOT NULL AUTO_INCREMENT,
     game INT NOT NULL,
     x INT NOT NULL,
     y INT NOT NULL,
     color INT NOT NULL,
     primary key ( id ) );

2 つのテーブルのうち、最初に作成しているのは games テーブルです。このテーブルにはゲームの一意の ID のみが含まれています。本番のアプリケーションでは、おそらく users テーブルと、両プレイヤーのユーザー ID が含まれる games テーブルを持つことになるはずです。ただしここでは単純にするために、その方法を使わず、ゲーム・データの保存、クライアントとサーバーとの間の通信、フロントエンドの作成のための基本に集中することにします。

2 番目のテーブルは moves テーブルです。このテーブルには、指定されたゲームに対する個々の指し手が含まれており、そのため 5 つの列があります。最初の列は指し手に対する一意の ID です。2 番目の列は、その指し手が適用されるゲームの ID です。その次に、指し手の x 位置と y 位置を示す列が来ます。ゲームの格子が 3 X 3 であれば、x 位置の場合も y 位置の場合も 0 と 2 の間の値になるはずです。最後のフィールドは指し手の「色」であり、×または○を示す整数です。

データベースを作成するためには、まず mysqladmin を使用してデータベースを作成した後、mysql コマンドを使って db.sql スクリプトを実行します。その例を以下に示します。

% mysqladmin --user=root --password=foo create ttt
% mysql --user=root --password=foo ttt < db.sql

このステップにより、○×ゲームのスキーマを持つ ttt という新しいデータベースが作成されます。

これでスキーマを作成できたので、ゲームの開始手段を作成しなければなりません。そのためには start.php というスクリプトを使います (リスト 2)。

リスト 2. start.php
<?php
header( 'Content-Type:text/xml' );

$dd = new PDO('mysql:host=localhost;dbname=ttt', 'root', '');
$sql = 'INSERT INTO games VALUES ( 0 )';
$sth = $dd->prepare($sql);
$sth->execute( array() );
$qid = $dd->lastInsertId();

$doc = new DOMDocument();
$r = $doc->createElement( "game" );
$r->setAttribute( 'id', $qid );
$doc->appendChild( $r );

print $doc->saveXML();
?>

このスクリプトはデータベースに接続されると起動され、games テーブルに対して INSERT 文を実行し、生成された ID を取得します。さらに、XML 文書を作成し、取得した ID を game タグに追加し、その XML をエクスポートします。

以下のスクリプトを実行し、データベースの中にあるゲームを取得する必要があります。なぜなら、単純な Android アプリケーションにはゲームを作成するためのインターフェースが無いからです。そのためのコードを以下に示します。

$ php start.php
<?xml version="1.0"?>
<game id="1"/>
$

これで最初のゲームができました。ゲームの一覧を表示するためにはリスト 3 の games.php スクリプトを使用します。

リスト 3. games.php
<?php
header( 'Content-Type:text/xml' );

$dbh = new PDO('mysql:host=localhost;dbname=ttt', 'root', '');

$sql = 'SELECT * FROM games';

$q = $dbh->prepare( $sql );
$q->execute( array() );

$doc = new DOMDocument();
$r = $doc->createElement( "games" );
$doc->appendChild( $r );

foreach ( $q->fetchAll() as $row) {
  $e = $doc->createElement( "game" );
  $e->setAttribute( 'id', $row['id'] );
  $r->appendChild( $e );
}

print $doc->saveXML();
?>

このスクリプトは start.php スクリプトと同じように、データベースに接続されると起動され、games テーブルに対してクエリーを実行し、どんなゲームがあるかを調べます。さらに、新しい XML 文書を作成して games タグを追加し、利用可能な各ゲームに game タグを追加します。

このスクリプトをコマンドラインから実行すると、以下のようなものが表示されるはずです。

$ php games.php
<?xml version="1.0"?>
<games><game id="1"/></games>
$

このスクリプトは Web ブラウザーから実行することもでき、その場合も同じ出力が表示されます。

素晴らしい結果です。これで games API を作成できたので、今度は指し手を処理するサーバー・コードを作成します。このコードではまず、show_moves というヘルパー・スクリプトを作成します。このスクリプトは指定されたゲームに対する現在の指し手を取得し、それらの指し手を XML としてエクスポートします。このヘルパー関数の PHP コードをリスト 4 に示します。

リスト 4. show_moves.php
<?php
function show_moves( $dbh, $game ) {
  $sql = 'SELECT * FROM moves WHERE game=?';

  $q = $dbh->prepare( $sql );
  $q->execute( array( $game ) );

  $doc = new DOMDocument();
  $r = $doc->createElement( "moves" );
  $doc->appendChild( $r );

  foreach ( $q->fetchAll() as $row) {
    $e = $doc->createElement( "move" );
    $e->setAttribute( 'x', $row['x'] );
    $e->setAttribute( 'y', $row['y'] );
    $e->setAttribute( 'color', $row['color'] );
    $r->appendChild( $e );
  }

  print $doc->saveXML();
}
?>

このスクリプトはデータベースのハンドルとゲームの ID を引数に取り、SQL を実行して指し手の一覧を取得します。そして指定のゲームに対する指し手を含む XML 文書を作成します。

このヘルパー関数を作成した理由は、この関数を 2 つのスクリプトで使用するからです。第 1 のスクリプトは、指定されたゲームの現在の指し手を返す moves.php スクリプトです。このスクリプトをリスト 5 に示します。

リスト 5. moves.php
<?php
require_once( 'show_moves.php' );

header( 'Content-Type:text/xml' );

$dbh = new PDO('mysql:host=localhost;dbname=ttt', 'root', '');

show_moves( $dbh, $_REQUEST['game'] );
?>

ヘルパー関数のコードが含まれるこの単純なスクリプトでは、データベースに接続し、指定されたゲームの ID を使用して show_moves 関数を呼び出します。このコードをテストするためには、コマンドラインから curl コマンドを使用してサーバー上のスクリプトを呼び出します。

$ curl "http://localhost/ttt/moves.php?game=1"
<?xml version="1.0"?>
<moves/>
$

残念ながら、まだ指し手がないため、出力はあまり面白くありません。そこで、サーバー API に最後のスクリプトを追加する必要があります。リスト 6 は move.php スクリプトを示しています。

リスト 6. move.php
<?php
require_once( 'show_moves.php' );

header( 'Content-Type:text/xml' );

$dbh = new PDO('mysql:host=localhost;dbname=ttt', 'root', '');
$sql = 'DELETE FROM moves WHERE game=? AND x=? AND y=?';
$sth = $dbh->prepare($sql);
$sth->execute( array(
  $_REQUEST['game'],
  $_REQUEST['x'],
  $_REQUEST['y']
) );

$sql = 'INSERT INTO moves VALUES ( 0, ?, ?, ?, ? )';
$sth = $dbh->prepare($sql);
$sth->execute( array(
  $_REQUEST['game'],
  $_REQUEST['x'],
  $_REQUEST['y'],
  $_REQUEST['color']
) );

show_moves( $dbh, $_REQUEST['game'] );
?>

このスクリプトでは、まずヘルパー関数を読み込んでデータベースに接続し、2 つの SQL 文を実行します。1 つ目の SQL 文は、送られてくる指し手と衝突する可能性のある指し手をすべて削除します。2 つ目の SQL 文は指定された指し手に対する新しい行を moves テーブルに挿入します。続いてこのスクリプトは指し手の一覧をクライアントに返します。このステップにより、クライアントは指し手ごとに 2 つのリクエストを実行する必要がなくなります。帯域幅の使用にかかるコストは安くはないため、リクエストを集約できる場合には集約する必要があります。

これらのすべてが適切に動作するかどうかをテストするために、以下のように指してみます。

$ curl "http://localhost/ttt/move.php?game=1&x=1&y=2&color=1"
<?xml version="1.0"?>
<moves><move x="1" y="2" color="1"/></moves>

ゲーム・サーバーのコードが完成すると、このネットワーク対応のマルチプレイヤー・ゲームに対する Android のフロントエンドを作成することができます。


Android のフロントエンドを作成する

まず、Android SDK と、Android プラットフォームのいくつかのバージョン、そして最後に Eclipse と Android Eclipse プラグインをインストールします。幸い、これらの方法はすべて Android のサイトに詳しく説明されています (「参考文献」のリンクを参照)。開発環境のセットアップ方法を詳細に説明しようとすると、この記事全体よりもさらに多くのスペースが必要になります。

開発環境のセットアップを終えたら、Eclipse を起動して新しい Android プロジェクトを開始します。すると図 1 のようなものが表示されるはずです。

図 1. Eclipse で Android アプリケーションを作成する
「New Android Project (新規 Android プロジェクト)」ウィザードのスクリーン・キャプチャーとして、サンプル・プロジェクトの詳細が表示されています。

図 1 は Android アプリケーション用のプロジェクト・ウィザードを示しています。プロジェクト名を入力して「Create new project in workspace (ワークスペース内に新規プロジェクトを作成)」ラジオ・ボタンを選択し、UI 要素のあるコードの場所を指定します。「Build Target (ビルド・ターゲット)」チェックリストで Android プラットフォームを選択します。ここでは、Android 2.3.1 を使用します。この記事のコードは非常に単純なので、どれでも皆さんがお好みのバージョンを使用することができます。何もプラットフォームが表示されていない場合には、Android SDK のセットアップの説明にあるとおり、対象のプラットフォームをダウンロードしてインストールする必要があります。これらのプラットフォームをすべてダウンロードしようとすると、非常に長い時間がかかることに注意してください。

Properties (プロパティー)」セクションにアプリケーション名とパッケージ名を入力します。ここではそれぞれのフィールドに、「Tic Tac Toe」(訳注: Tic Tac Toe は○×ゲームを表す語です) と「com.jherrington.tictactoe」を入力しました。続いて「Create Activity (アクティビティーを作成)」チェックボックスにチェックを入れ、アクティビティー名を入力します。ここではアクティビティー名として「TicTacToeActivity」と入力しました。

Finish (完了)」をクリックすると、図 2 のような新規プロジェクトが表示されます。

図 2. TicTacToe プロジェクトのファイル
新規 TicTacToe プロジェクトのファイルとフォルダーを示すスクリーン・キャプチャー

図 2 は Android アプリケーションのための最上位レベルのディレクトリーとファイルを示しています (ディレクトリーには src、gen、Android 2.3.1、assets、res があり、ファイルには AndroidManifest.xml、default.properties、proguard.cfg があります)。重要な項目は以下のとおりです。

  • リソースを含む res ディレクトリー
  • Java ソースを持つ src ディレクトリー
  • アプリケーションの履歴情報を含むマニフェスト・ファイル

最初にマニフェスト・ファイルを編集します。このファイルの大部分は既に適切な状態になっていますが、アプリケーションがインターネット経由でリクエストを行えるように、インターネットへのアクセス許可を追加する必要があります。リスト 7 は完成したマニフェスト・ファイルを示しています。

リスト 7. AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      android:versionCode="1"
      android:versionName="1.0" package="com.jherrington.tictactoe">

    <uses-permission
        android:name="android.permission.INTERNET" />

    <uses-sdk android:minSdkVersion="5" />

    <application android:icon="@drawable/icon" android:label="@string/app_name">
        <activity android:name="TicTacToeActivity"
                  android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

    </application>
</manifest>

唯一の変更は、ファイルの先頭の方に uses-permission タグを追加したことです。

続いて UI のデザインを行います。そのためには res/layout ディレクトリーの中にある layout.xml ファイルを少し変更します。このファイルの新しい内容をリスト 8 に示します。

リスト 8. layout.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
    <LinearLayout android:layout_height="wrap_content" 
                android:layout_width="match_parent" android:id="@+id/linearLayout1">
        <Button android:text="Play X" android:id="@+id/playx" 
                android:layout_width="wrap_content" 
                android:layout_height="wrap_content"></Button>
        <Button android:text="Play O" android:id="@+id/playo" 
                android:layout_width="wrap_content" 
                android:layout_height="wrap_content"></Button>
    </LinearLayout> 
    <com.jherrington.tictactoe.BoardView android:id="@+id/bview" 
                android:layout_width="wrap_content" 
                android:layout_height="wrap_content"
    ></com.jherrington.tictactoe.BoardView>
</LinearLayout>

これは単純なレイアウトです。画面上部には 2 つのボタンがあり、これらのボタンが横方向のリニア・レイアウトにラップされています。この 2 つのボタンは、プレイする色をユーザーが指定するための×ボタンと○ボタンです。

このコードの残りの部分は、○×ゲームのボードを表す BoardView クラスです。BoardView クラスのコードはリスト 11 に示してあります。

レイアウトを用意できたら、今度はアプリケーションのための Java コードを作成します。このコーディングはリスト 9 の TicTacToeActivity クラスの作成から始まります。アクティビティーは Android アプリケーションの基本ビルディング・ブロックです。各アプリケーションには、そのアプリケーションのさまざまな状態を表現する 1 つ以上のアクティビティーがあります。アプリケーションをナビゲートしていくとアクティビティーのスタックが作成され、電話の「戻る」ボタンを使用すると、それらのアクティビティーをスタックからポップすることができます。○×ゲーム・アプリケーションには 1 つのアクティビティーしかありません。

リスト 9. TicTacToeActivity.java
package com.jherrington.tictactoe;

import java.util.Timer;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup.LayoutParams;
import android.widget.Button;
import android.widget.Gallery;
import android.widget.LinearLayout;

public class TicTacToeActivity extends Activity implements OnClickListener {
  @Override
    public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.main);

      Button playx = (Button)this.findViewById(R.id.playx);
      playx.setOnClickListener( this );

      Button playo = (Button)this.findViewById(R.id.playo);
      playo.setOnClickListener( this );

      Timer timer = new Timer();
      UpdateTimer ut = new UpdateTimer();
      ut.boardView = (BoardView)this.findViewById(R.id.bview);
      timer.schedule( ut, 200, 200 );
    }

    public void onClick(View v) {
      BoardView board = (BoardView)this.findViewById(R.id.bview);
      if ( v.getId() == R.id.playx ) {
        board.setColor( 2 );
      }
      if ( v.getId() == R.id.playo ) {
        board.setColor( 1 );
      }
    }
}

このアクティビティーには 2 つのメソッドがあります。その第一は onCreate メソッドです。このメソッドはユーザー・インターフェースを作成し、×ボタンと○ボタンに onClick ハンドラーを接続し、更新タイマーを起動します。更新タイマーは 200 ミリ秒ごとにゲームの状態を更新するために使われます。この機能により、どちらのプレイヤーも手番がわかるようになっています。

onClick ハンドラーは、ユーザーが×ボタンをクリックしたのか○ボタンをクリックしたのかに応じて、ボードの色を設定します。

リスト 10 の GameService クラスはシングルトン・クラスであり、指定されたゲームの現在の状態とゲーム・サーバーを表します。

リスト 10. GameService.java
package com.jherrington.tictactoe;

import java.util.ArrayList;
import java.util.List;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

import android.util.Log;

public class GameService {
  private static GameService _instance = new GameService();

  public int[][] positions = new int[][] { 
      { 0, 0, 0 },
      { 0, 0, 0 },
      { 0, 0, 0 }
  };

  public static GameService getInstance() {
    return _instance;
  }

  private void updatePositions( Document doc ) {
    for( int x = 0; x < 3; x++ ) {
      for( int y = 0; y < 3; y++ ) {
        positions[x][y] = 0;
      }
    }
    doc.getDocumentElement().normalize();
    NodeList items = doc.getElementsByTagName("move");
    for (int i=0;i<items.getLength();i++){
      Element me = (Element)items.item(i);
      int x = Integer.parseInt( me.getAttribute("x") );
      int y = Integer.parseInt( me.getAttribute("y") );
      int color = Integer.parseInt( me.getAttribute("color") );
      positions[x][y] = color;
    }
  }

  public void startGame( int game ) {
    HttpClient httpclient = new DefaultHttpClient();
    HttpPost httppost = new HttpPost("http://10.0.2.2/ttt/moves.php");

    try {
      List<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>(2);
      nameValuePairs.add(new BasicNameValuePair("game", Integer.toString(game)));
      httppost.setEntity(new UrlEncodedFormEntity(nameValuePairs));

      HttpResponse response = httpclient.execute(httppost);
      DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
      DocumentBuilder db = dbf.newDocumentBuilder();
      updatePositions( db.parse(response.getEntity().getContent()) );
    } catch (Exception e) {
      Log.v("ioexception", e.toString());
    }
  }

  public void setPosition( int game, int x, int y, int color ) {
    HttpClient httpclient = new DefaultHttpClient();
    HttpPost httppost = new HttpPost("http://10.0.2.2/ttt/move.php");

    positions[x][y] = color;

    try {
      List<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>(2);
      nameValuePairs.add(new BasicNameValuePair("game", Integer.toString(game)));
      nameValuePairs.add(new BasicNameValuePair("x", Integer.toString(x)));
      nameValuePairs.add(new BasicNameValuePair("y", Integer.toString(y)));
      nameValuePairs.add(new BasicNameValuePair("color", Integer.toString(color)));
      httppost.setEntity(new UrlEncodedFormEntity(nameValuePairs));

      HttpResponse response = httpclient.execute(httppost);
      DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
      DocumentBuilder db = dbf.newDocumentBuilder();
      updatePositions( db.parse(response.getEntity().getContent()) );
    } catch (Exception e) {
      Log.v("ioexception", e.toString());
    }
  }
}

このコードはアプリケーションの中で最も興味深いコードの1 つです。まず、updatePositions メソッドでサーバーから返された XML を引数に取り、その XML 内にある move 要素を探し、現在の一連の move によって positions 配列を更新します。positions 配列にはボード上の各位置に対する値が含まれています。ゼロは空のスペースを表し、1 は「○」を、2 は「×」を表します。

それ以外の 2 つの関数 (startGame と setPosition) はサーバーとの通信のためのものです。startGame メソッドはサーバーに対して現在の一連の指し手を要求し、その差し手の情報で位置の一覧を更新します。setPosition メソッドはサーバーに指し手を送信します。そのために setPosition メソッドは HTTP の POST リクエストを作成し、名前と値のペアの配列を使用して POST データを設定します (名前と値のペアはエンコードされて送信されます)。そして setPosition メソッドはレスポンスとして返される XML を構文解析し、位置の一覧を更新します。

詳しく見てみると、サーバーへの接続に使用される IP は非常に興味深いものです。この IP は「localhost」や「127.0.0.1」ではなく、「10.0.2.2」です。この「10.0.2.2」はエミュレーターが実行されているマシンのエイリアスです。Android フォンそのものは UNIX システムなので、localhost にはAndroid フォン独自のサービスがあります。これは素晴らしいと思いませんか? Android フォンそれ自体は、実際には電話というよりも、手のひらサイズの本格的なコンピューターであり、そのコンピューターにたまたま電話が組み込まれているだけであるということが明確にわかる機会はそれほど頻繁にはありません。

ところで、どこまで説明したでしょうか?アプリケーションのメイン・コンポーネントとしてのアクティビティーがあり、UI レイアウトが設定され、サーバーへの接続のための Java コードがあります。今度はゲーム・ボードを描画する必要があります。そのためにはリスト 11 の BoardView クラスを使用します。

リスト 11. BoardView.java
package com.jherrington.tictactoe;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

public class BoardView extends View {
  private int _color = 1;
  
  public void setColor( int c ) {
    _color = c;
  }
  
  public BoardView(Context context) {
      super(context);
      GameService.getInstance().startGame(0);
  }
  
  public BoardView(Context context, AttributeSet attrs) {
      super(context,attrs);
      GameService.getInstance().startGame(0);
  }
  
  public BoardView(Context context, AttributeSet attrs, int defStyle) {
      super(context,attrs,defStyle);
      GameService.getInstance().startGame(0);
  }

  public boolean onTouchEvent( MotionEvent event ) {
    if ( event.getAction() != MotionEvent.ACTION_UP )
      return true;
    int offsetX = getOffsetX();
    int offsetY = getOffsetY();
    int lineSize = getLineSize();
    for( int x = 0; x < 3; x++ ) {
      for( int y = 0; y < 3; y++ ) {
        Rect r = new Rect( ( offsetX + ( x * lineSize ) ),
            ( offsetY + ( y * lineSize ) ),
            ( ( offsetX + ( x * lineSize ) ) + lineSize ),
            ( ( offsetY + ( y * lineSize ) ) + lineSize ) );
        if ( r.contains( (int)event.getX(), (int)event.getY() ) ) {
          GameService.getInstance().setPosition(0, x, y, _color);
          invalidate();
          return true;
        }
      }
    }
    return true;
  }

  private int getSize() {
    return (int) ( (float) 
    ( ( getWidth() < getHeight() ) ? getWidth() : getHeight() ) * 0.8 );
  }

  private int getOffsetX() {
    return ( getWidth() / 2 ) - ( getSize( ) / 2 );
  }
  
  private int getOffsetY() {
    return ( getHeight() / 2 ) - ( getSize() / 2 );
  }
  
  private int getLineSize() {
    return ( getSize() / 3 );
  }

  protected void onDraw(Canvas canvas) {
    Paint paint = new Paint();
    paint.setAntiAlias(true);
    paint.setColor(Color.BLACK);
    canvas.drawRect(0,0,canvas.getWidth(),canvas.getHeight(), paint);
    
    int size = getSize();
    int offsetX = getOffsetX();
    int offsetY = getOffsetY();
    int lineSize = getLineSize();
    
    paint.setColor(Color.DKGRAY);
    paint.setStrokeWidth( 5 );
    for( int col = 0; col < 2; col++ ) {
      int cx = offsetX + ( ( col + 1 ) * lineSize );
      canvas.drawLine(cx, offsetY, cx, offsetY + size, paint);
    }
    for( int row = 0; row < 2; row++ ) {
      int cy = offsetY + ( ( row + 1 ) * lineSize );
      canvas.drawLine(offsetX, cy, offsetX + size, cy, paint);
    }
    int inset = (int) ( (float)lineSize * 0.1 );
    
    paint.setColor(Color.WHITE);
    paint.setStyle(Paint.Style.STROKE); 
    paint.setStrokeWidth( 10 );
    for( int x = 0; x < 3; x++ ) {
      for( int y = 0; y < 3; y++ ) {
        Rect r = new Rect( ( offsetX + ( x * lineSize ) ) + inset,
            ( offsetY + ( y * lineSize ) ) + inset,
            ( ( offsetX + ( x * lineSize ) ) + lineSize ) - inset,
            ( ( offsetY + ( y * lineSize ) ) + lineSize ) - inset );
        if ( GameService.getInstance().positions[ x ][ y ] == 1 ) {
          canvas.drawCircle( ( r.right + r.left ) / 2, 
                  ( r.bottom + r.top ) / 2, 
                  ( r.right - r.left ) / 2, paint);
        }
        if ( GameService.getInstance().positions[ x ][ y ] == 2 ) {
          canvas.drawLine( r.left, r.top, r.right, r.bottom, paint);
          canvas.drawLine( r.left, r.bottom, r.right, r.top, paint);
        }
      }
    }
  }

}

ここで行われていることの大部分は onTouchEvent メソッドと onDraw メソッドの中で行われています。onTouchEvent メソッドはユーザーがゲーム・ボード上の特定のセルに触れると応答し、onDraw メソッドは Android のペイント・メカニズムを使用してゲーム・ボードを描画します。

onTouchEvent メソッドはサイズ関数を使用して、各セル位置の四角形を判断します。次にその四角形の contains メソッドを使用して、ユーザーがそのセル内でクリックしたかどうかを判断します。クリックした場合には、ゲーム・サービスに対するリクエストを起動し、その指し手が実行されます。

onDraw 関数はサイズ関数を使用することで、ゲーム・ボード上の線と、指された×と○の両方を描画します。ゲーム・ボード上の各正方形の領域の現在の状態を保持する positions 配列には、GameServer シングルトンが使用されます。

最後に、UpdateTimer クラスが必要です。このクラスはゲーム・サービスを使用して、最新の値によってボードの positions を更新します。リスト 12 は、このタイマーのコードを示しています。

リスト 12. UpdateTimer.java
package com.jherrington.tictactoe;

import java.util.TimerTask;

public class UpdateTimer extends TimerTask {
  public BoardView boardView;
  
  @Override
  public void run() {
    GameService.getInstance().startGame( 0 );
    boardView.post(new Runnable(){ public void run(){ boardView.invalidate(); } });
  }
}

アプリケーションが起動すると TicTacToeActivity クラスによって初期化されるこのタイマーは、ポーリング・メカニズムを使用しています。このメカニズムは、クライアントとサーバーとの間の通信手段としては、あまり効率的とは言えませんが、極めて単純かつ信頼性の高いメカニズムです。最も効率的な方法は、HTTP プロトコルのバージョン 1.1 を使用して接続を開いたまま維持し、指し手が実行されたときにサーバーからクライアントに更新情報を送信する方法です。しかし、この方法ははるかに複雑で、クライアントとサーバーの両方が HTTP 1.1 プロトコルをサポートする必要があります。さらに、接続数が増加したときのスケーラビリティーの問題もあります。この方法についてはこの記事では説明しません。今回のような単純なデモ用のゲームであれば、ポーリング・メカニズムでまったく問題ありません。

コーディングが完了したら、アプリケーションをテストします。これはつまりエミュレーターを起動するということです。エミュレーターが起動されると、図 3 のような画面が表示されるはずです。

図 3. Android エミュレーターを起動する
Android エミュレーターを起動した状態のスクリーン・キャプチャー

これはエミュレーターが素晴らしい「A N D R O I D」インターフェースをロードしているところです。ロードが終わると、図 4 の起動画面が表示されます。

図 4. エミュレーターが起動され、準備が整った状態
エミュレーターに時刻、日付、バッテリー残量メーター、音量アイコンとロック・アイコンが表示された状態のスクリーン・キャプチャー

Android フォンの操作をするには、ロック・アイコンを右にスライドします。このアクションによってホーム画面が表示され、通常はデバッグ対象のアプリケーションが起動されます。この記事の場合には、このアクションによって図 5 に示すゲームの画面が表示されます。

図 5. 指し手が実行される前のゲーム
新たにゲームを開始するための「Play X」ボタンと「Play O」ボタンが表示された○×ゲームのスクリーン・キャプチャー

サーバーの状態により、指し手が表示される場合と表示されない場合があります。この図の場合、ゲームはまだ空です。「Play X」ボタンと「Play O」ボタンが画面上部にあり、○×ゲームのボードが画面中央に表示されています。この状態で、「Play X」をクリックしてから真ん中の正方形をクリックすると、図 6 のような表示になります。

図 6. 当然、真ん中の正方形に×が表示されます
格子の真ん中の正方形に×が表示された状態の○×ゲームのスクリーン・キャプチャー

図 6 は、真ん中の正方形に×が入力された状態のゲーム・ボード画面です。サーバーが接続されたかどうかを確認するために、サーバー上の moves.php スクリプトに対して curl コマンドを実行し、ゲームの最新の指し手の一覧を取得します。

○が正常に動作するかどうかをテストするために、「Play O」をクリックするのに続いて角の正方形をクリックします (図 7)。

図 7. 角の正方形に○を置く
真ん中の正方形と右上の正方形に、それぞれ×と○が表示された状態の○×ゲームのスクリーン・キャプチャー

×でプレイすることも○でプレイすることもできます。このアプリケーションはサーバーに接続し、共有の場所にゲームの状態を保持します。また、更新タイマーがあるため、各ユーザーは手番がわかるようになっています。

まとめ

このゲームは完全なゲームでしょうか?そうとは言えません。勝ったかどうかのチェックがなく、既に指し手が実行されている位置の指し手をプレイヤーが上書きできてしまい、どちらの番であるかのチェックもありません。しかし、技術的な基本部分は用意できています。ゲーム・サーバーにはプレイヤー間で共有される状態が格納され、モバイル機器で実行されるネイティブ・グラフィカル・アプリケーションはゲーム・サーバーに接続してゲームへのインターフェースを提供します。このゲームを皆さん独自のゲームの出発点として使用し、好みに合わせて作り上げることができます。手軽で楽しいゲームにとどめることだけは忘れないでください。皆さんが次期 Words With Friends やマルチプレイヤーの Angry Birds を作成することになるかもしれません。

参考文献

学ぶために

  • Eclipse: この記事で Android アプリケーションを作成するために使用した IDE について学んでください。このサイトには Eclipse のダウンロードやプラグインもあります。
  • PHP Development Tools for Eclipse: PHP のための IDE が必要な場合、Eclipse プロジェクトには PHP のための拡張機能があり、またそれ以外にも、ほとんどすべてのものに対して Eclipse プラグインがあります。
  • Android マーケット: Android で実行するネットワーク対応のマルチプレイヤー・カジュアル・ゲームを作成したら、それを Android マーケットにアップロードしてください。そしてこの記事のコメント・セクションを通じて、それを私達に知らせてください。
  • PHP のサイト: このサイトは PHP の資料のサイトとして最高です。
  • W3C のサイト: さまざまな標準のための素晴らしいサイトにアクセスしてください。特にこの記事に関連しているのは XML 標準です。
  • 著者の Jack Herrington が developerWorks に寄稿した他の記事 (2005年3月から現在まで): Ajax、JSON、PHP、XML、その他の技術が解説されています。
  • New to XML: XML を学ぶために必要なリソースを入手することができます。
  • developerWorks の XML ゾーン: DTD、スキーマ、XSLT など、XML の領域でのスキルを磨くためのリソースが豊富に用意されています。XML 技術文書一覧に用意された、さまざまな技術記事やヒント、チュートリアル、技術標準、IBM Redbooks を見てください。
  • IBM XML certification: XML および関連技術において IBM 認定技術者になる方法を参照してください。
  • developerWorks の Technical events and webcasts: これらのセッションで最新情報を入手してください。
  • developerWorks on Twitter: 今すぐ Twitter に参加して developerWorks のツイートをフォローしてください。
  • developerWorks podcasts: ソフトウェア開発者のための興味深いインタビューや議論を聞くことができます。
  • developerWorks On demand demos: 初心者のための製品インストール方法やセットアップのデモから、上級開発者のための高度な機能に至るまで、多様な話題が解説されています。

製品や技術を入手するために

議論するために

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=XML, Open source, Web development
ArticleID=758658
ArticleTitle=Android で実行するネットワーク対応の○×ゲームを作成する
publish-date=09302011