連載
» 2014年01月30日 18時00分 公開

Androidでリアルタイムマルチプレーゲームを開発するにはAndroidで動く携帯Javaアプリ作成入門(48)(2/3 ページ)

[緒方聡,株式会社イーフロー]

友達から招待されたゲームに参加する

 以下では、友達から招待されたゲームに参加する方法を解説していきます。

 友達を招待する場合と同様、以下の手順でGameClientを取得し、Intentを生成した後、Activityを起動します。

    intent = getGamesClient().getInvitationInboxIntent();
    startActivityForResult(intent, RC_INVITATION_INBOX);

 「GameClient#getInvitationInboxIntent()」メソッドでIntentを生成します。これを任意のリクエストコードとともに「startActivityForResult(Intent, int)」メソッドに渡すことで、以下のような画面を開けます。

 拒否またはプレーをタップすることで、ゲームを招待してくれたプレーヤーに対して通知します。拒否する場合はこの画面で拒否通知を送ってくれます。プレーをタップした場合にホストプレーヤーの部屋に参加する処理は以下のように行います。

    private void handleInvitationInboxResult(int response, Intent data) {
        // キャンセルの場合は何もしない
        if (response != Activity.RESULT_OK) {
            return;
        }
        Invitation inv =
            data.getExtras().getParcelable(GamesClient.EXTRA_INVITATION);
        // 招待を受ける
        acceptInviteToRoom(inv.getInvitationId());
    }
    void acceptInviteToRoom(String invId) {
        // 招待を受ける際の一連の処理
        RoomConfig.Builder roomConfigBuilder = RoomConfig.builder(this);
        roomConfigBuilder.setInvitationIdToAccept(invId)
                .setMessageReceivedListener(this)
                .setRoomStatusUpdateListener(this);
        getGamesClient().joinRoom(roomConfigBuilder.build());
    }

 画面復帰した際のIntentからInvitationを取得し、そこからさらにInvitation IDを文字列で取得、それをRoomConfig.Builderにセットし、「GameClient#joinRoom(RoomConfig)」を呼び出す、という流れです。

 部屋に参加できた場合は、「RoomUpdateListener#onRoomJoined(int, Room)」メソッドが呼び出されます。

    @Override
    public void onJoinedRoom(int statusCode, Room room) {
        // 失敗したら何もしない
        if (statusCode != GamesClient.STATUS_OK) {
            return;
        }
        // プレーヤー参加待ち画面を表示
        showWaitingRoom(room);
    }

 プレーヤー参加待ち画面は友達を招待する場合と同様で、他の参加者を待ちます。

友達からの招待を検知する

 能動的にゲームに招待されたかどうかを確認するには前述の方法で良いのですが、多くの場合はユーザーに気付いてもらうために通知をする方がよいでしょう。

 Google Play Game ServicesのマルチプレーAPIにもこうした仕組みが用意されており、ButtonClickerにはメイン画面上部にポップアップするように実装されています。

 以下、この機能について解説します。

 ButtonClickerではOnInvitationReceivedListenerインターフェースを以下のような形で実装しています。

    @Override
    public void onInvitationReceived(Invitation invitation) {
        // 招待IDをフィールドに保持しておく
        mIncomingInvitationId = invitation.getInvitationId();
        // ポップアップするテキストフィールドに招待者の名前を表示
        ((TextView) findViewById(R.id.incoming_invitation_text)).setText(
                invitation.getInviter().getDisplayName() + " " +
                        getString(R.string.is_inviting_you));
        // ポップアップを表示
        switchToScreen(mCurScreen);
    }

 ButtonClickerでは、FrameLayoutに複数のLinearLayoutを入れ子にして、必要に応じて「switchToScreen(int)」メソッドで表示・非表示を切り替えています。FrameLayoutの特性を利用したシンプルで効果的なUI技法です。

 ポップアップされたAcceptボタンをタップした際の処理は以下のように実装されています。

    case R.id.button_accept_popup_invitation:
        acceptInviteToRoom(mIncomingInvitationId);
        mIncomingInvitationId = null;
        break;

 「acceptInviteToRoom(String)」メソッドの内容は前述した通りで、実装を有効に再利用しています。

 ローカルサービスを併用して、アプリがバックグラウンドでも招待を検知してNotificationで通知するなど、ますます便利になりそうです。

メッセージを送信する

 マルチプレーゲームでは、全プレーヤーでゲームの状態を同期する必要があります。そのために、メッセージの送受信を行う必要があります。

 Google Play Game Servicesのマルチプレーは、サーバー集中型ではなく分散型です。つまり、他の全てのプレーヤーに自分の状態を送信する必要があり、他の全てのプレーヤーからの状態が送信されてきます。それらを整合性が取れるようにマージしてゲームの状態を最新に保つのは各クライアントの仕事です。

 ゲーム開始時に部屋を作るという点からピンと来る方もいるかと思いますが、Google Play Game ServicesはMMORPGなどの大規模なマルチプレーには向いていません。

 これを踏まえて、メッセージ送信方法について説明していきます。

 メッセージ送信は大きく分けて2つあります。確実に届けたいメッセージ送信と、届かなくても支障のないメッセージ送信です。ButtonClickerの実装で説明します。

    void broadcastScore(boolean finalScore) {
        // 最終スコアかどうかを最初のバイトに保持
        mMsgBuf[0] = (byte) (finalScore ? 'F' : 'U');
        // 次のバイトにスコアを保持
        mMsgBuf[1] = (byte) mScore;
        // 全参加者に送信
        for (Participant p : mParticipants) {
            if (p.getParticipantId().equals(mMyId))
                continue;
            if (p.getStatus() != Participant.STATUS_JOINED)
                continue;
            if (finalScore) {
                // 最終スコアなら確実に送信する
                getGamesClient().sendReliableRealTimeMessage(
                        null, mMsgBuf, mRoomId,
                        p.getParticipantId());
            } else {
                // 途中のスコアは確実でなくてもよい
                getGamesClient().sendUnreliableRealTimeMessage(
                        mMsgBuf, mRoomId,
                        p.getParticipantId());
            }
        }
    }

 「GameClient#sendReliableRealTimeMessage(...)」メソッドは確実に届けたい送信、「GameClient#sendUnreliableRealTimeMessage(...)」メソッドは確実に届かなくても支障のない送信です。

 「確実に届けたい送信」という意味は、確実に届くということではなく、送信が必ず試みられるということであり、ネットワークの状態などの理由で届かない可能性ももちろんあります。

 「確実に届かなくても支障のない送信」は、送信間隔が短すぎて負荷が高いなどの理由で送信処理がスキップされる可能性があるということです。つまり、差分送信だと状態の整合性が取れなくなります。

 ButtonClickerでは、ゲーム開始と最終スコアは「sendReliableRealTimeMessage(...)」メソッドで確実に、途中スコアは「sendUnreliableRealTimeMessage(...)」メソッドで届かなくても支障のない送信を行っています。

 ここまで読んで、「全部sendReliableRealTimeMessage(...)メソッドで送信すれば、全部のメッセージが確実に届くので、その方が良いんじゃないの?」と考える人もいるのではないかと思います。

 Javadocには特に説明はありませんが、おそらく全てのメッセージ送信がこの方式で行われてしまうと、タイムラグが発生したり、最悪Google Play Servicesのクラウドサーバーがダウンしてしまう可能性があります。できる限り「sendUnreliableRealTimeMessage(...)」メソッドを利用してリアルタイム性を確保しつつ、クラウドサーバーの負荷を減らすようにしたいものです。

 「sendReliableRealTimeMessage(...)」メソッドは、参加者IDを文字列で指定し、必要であれば送信結果を受け取るコールバックを渡します。

 「sendUnreliableRealTimeMessage(...)」メソッドはコールバックは指定できず、代わりに参加者IDを複数指定したり、「sendUnreliableRealTimeMessageToAll(...)」メソッドで全員に送信したりできます。

 送信するメッセージはバイト配列なので、実質どのような内容でも送信可能です。ButtonClickerは2バイトでまとめていますが、ゲームが複雑になると全状態を都度送信するのはオーバーヘッドが大きくなるので、相手の状態を問い合わせ、問い合わせられた状態を返す独自プロトコルを設計するのが良さそうです。

Copyright © ITmedia, Inc. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。