KENTEM TechBlog

建設業のDXを実現するKENTEMの技術ブログです。

Photon EngineのRealtimeを使ってみる【②実践編】

こんにちは🐊 新卒フロントエンドのK.Sです。
前回はPhotonの概要とAppIDの作成まで行いました。今回は、簡易的に複数人でマッチングが行えるwebアプリを作成していこうと思います。

開発環境

vite + React + Typescriptで開発していきます。
ここではviteの環境構築の説明は省きます。

SDKの準備

まずはPhoton公式サイトからSDKをダウンロードします。

解凍するとlibフォルダの中に以下の二つが入っています。

  • photon.js
  • photon.d.ts

photon.jsをプロジェクトのsrc/public、photon.d.tsをsrc/typesに配置します。

以下をindex.htmlに書き加えます。

<head>
  <script src="/photon.js"></script>
</head>

型定義ファイルから型情報を参照できるようにします。 photon.d.tsで定義されているPhoton namespaceを参照します。

/// <reference path="./types/photon.d.ts" />

declare global {
  interface Window {
    Photon: typeof Photon;
  }
}

export {};

グローバル変数からPhotonを取得します。

const Photon = window.Photon;

PhotonからLoadBalancingClientを生成します。 このクラスは、Photon Cloud と通信し、部屋の入退室やイベント送受信を担うクラスとなります。

appIdは前回作成したAppIDを設定しましょう。
appVersionは自由に設定できますが、appVersionが異なるユーザー同士ではappIdが同じでもマッチングができませんのでご注意ください。

const appId = "ここにAppIDを挿入";
const appVersion = "1.0";

const photonClient = new Photon.LoadBalancing.LoadBalancingClient(
  Photon.ConnectionProtocol.Wss,
  appId,
  appVersion
);

これでサーバーに接続する準備が整いました!

Photon Cloudに接続する

以下でPhoton Cloudに接続します。
前回説明しましたが、まずはこれで玄関口となるネームサーバーへ接続し、設定したリージョンに応じてマスターサーバーへ振り分けられます。 マスターサーバーには「ロビー」があり、プレイヤーはロビーとルームを行き来する形となります。

const region = "jp";
photonClient.connectToRegionMaster(region);

次にcreateRoomでルームを作成し入室します。 ひとつのAppIDにつきCCU(同時接続ユーザー数)は無料枠だと20人までです。 今回はひとつのルームに4人までに設定しました。

const roomName = "room1";
photonClient.createRoom(roomName, {
  maxPlayers: 4,
});

他のプレイヤーはjoinRoomで入室します。

photonClient.joinRoom(roomName)

また、退出するときはleaveRoomを使用します。

photonClient.leaveRoom();

これでサーバー接続からルーム入室まで完了しました!

プレイヤーやルームなどの情報を取得する

ここまでできたら複数のプレイヤーで実際に入室を試してみたいですが、その前に様々な情報を表示できると便利そうです。LoadBalancingClientの主要な関数と変数をまとめるので、必要に応じて使用しましょう。

利用可能なルームのリストを取得

ロビーでルームの一覧を表示したいときに便利そうです。
返ってくるRoom[]に含まれる情報は次の「現在のルーム情報を取得」で確認できます。

photonClient.availableRooms(): Photon.LoadBalancing.Room[]

現在のルーム情報を取得

入室中のルーム情報を取得できます。

// 現在のルームオブジェクト
photonClient.myRoom(): Photon.LoadBalancing.Room

// ルーム情報
const room = photonClient.myRoom();
room.name: string              // 部屋の名前(一意のID)
room.isOpen: boolean           // 部屋が新規参加を受け付けているか(false = 満員または開始済み)
room.isVisible: boolean        // 部屋がロビーのリストに表示されるか(false = 非表示・プライベート)
room.maxPlayers: number        // 部屋の最大プレイヤー数(0 = 無制限)
room.masterClientId: number    // マスタークライアントのactorNr(ホストプレイヤー)
room.playerCount: number       // 現在の部屋のプレイヤー数
room.roomTTL: number          // 部屋の生存時間(ミリ秒)全員退出後、この時間で部屋が削除される
room.playerTTL: number        // プレイヤーの再接続猶予時間(ミリ秒)切断後、この時間内なら復帰可能
room.expectedUsers: string[]  // 参加予定のユーザーID配列(招待制マッチなどで使用)

アクター情報を取得

アクターとはルームに入室しているプレイヤーのことです。 また、マスタークライアントとは簡単に言うとルームに必ずただ一人存在するホストのプレイヤーのようなものです。 公式の用語サイトには、以下のように書かれています。

マスタークライアントはルームにいるひとりのクライアントによってのみ実行されるロジックの処理をつかさどるように作られています(例:全員の準備が完了したらマッチを開始する)。 Realtime - 用語集 | Photon Engine

// 自分のアクター
photonClient.myActor(): Photon.LoadBalancing.Actor

// 部屋内の全アクター
photonClient.myRoomActors(): { [actorNr: number]: Actor }
photonClient.myRoomActorsArray(): Actor[]

// Actorの情報
const actor = photonClient.myActor();
actor.actorNr: number         // プレイヤーの一意な番号(部屋内でのID、1から始まる連番)
actor.name: string            // プレイヤーの表示名(ニックネーム)
actor.isLocal: boolean        // 自分自身かどうか(true = 自分、false = 他プレイヤー)
actor.isMasterClient: boolean // マスタークライアント(ホスト)かどうか(true = ホスト)

サーバー時間を取得

ローカルではなくサーバー時間を使用することで、全員が統一された時間基準を持つことができます。 また、時刻の改ざん防止にもなります。

// サーバー時間
photonClient.getServerTimeMs(): number

ロビー統計を取得

ロビーの概要情報を取得できます。

const stats = photonClient.getLobbyStats()
stats.PeerCount;    // 現在のプレイヤー数
stats.GameCount;    // 現在の部屋数
stats.MasterPeerCount;  // マスターサーバーのプレイヤー数

コールバック関数

Photon Realtimeではコールバック関数での制御が基本になります。
いくつか用意されているのですが、よく使用するのは以下です。
状態変化した時に処理されます。

// 状態変化のコールバック
onStateChange(state: number)

作成したコードが以下です。実際にアプリを作成して、サーバーに接続するとわかるのですが、こちらでconsole.log()をしなくてもPhoton側がブラウザの開発者ツールのコンソールに状態遷移先を表示してくれます。 (9はわからなかったです🫠)

enum PhotonState {
  Uninitialized = 0,
  ConnectingToNameServer = 1,
  ConnectedToNameServer = 2,
  ConnectingToMasterserver = 3,
  ConnectedToMaster = 4,
  JoinedLobby = 5,
  ConnectingToGameserver = 6,
  ConnectedToGameserver = 7,
  Joined = 8,
  Disconnected = 10,
}

photonClient.onStateChange = (state: number) => {
    switch (state) {
      case PhotonState.ConnectingToNameServer:
        console.log("ネームサーバー接続中...");
        break;
      case PhotonState.ConnectedToNameServer:
        console.log("ネームサーバー接続完了");
        break;
      case PhotonState.ConnectingToMasterserver:
        console.log("マスターサーバー接続中...");
        break;
      case PhotonState.ConnectedToMaster:
        console.log("マスターサーバー接続完了");
        break;
      case PhotonState.JoinedLobby:
        console.log("ロビー接続完了");
        break;
      case PhotonState.ConnectingToGameserver:
        console.log("ゲームサーバー接続中...");
        break;
      case PhotonState.ConnectedToGameserver:
        console.log("ゲームサーバー接続完了");
        break;
      case PhotonState.Joined:
        console.log("ルーム入室完了");
        break;
      case PhotonState.Disconnected:
        console.log("切断");
        break;
    }
  };

他の関数については以下が参考になるかもしれません。

doc-api.photonengine.com

情報を共有する

ここまで、マルチプレイ対戦に必要な要素の一つ「接続」を行いました。
ここから「同期」を行っていきたいと思います。

Photon Realtimeでは同期をraiseEventという関数で行います。 raiseEventはルーム内の他のプレイヤーにカスタムイベントを送信する関数です。

raiseEvent(eventCode, data, options?)

eventCodeはイベントの種類を識別する番号で、0~199が使用できます。dataは送信するデータ(オブジェクト、配列、文字列、数値など)です。
optionsでは主に以下が設定できます。

  • options.receivers (number):イベントを送る対象を選択する
// 自分以外に送信(デフォルト)
photonClient.raiseEvent(1, data, { 
  receivers: 0  // ReceiverGroup.Others
});

// 自分を含む全員に送信
photonClient.raiseEvent(1, data, { 
  receivers: 1  // ReceiverGroup.All
});

// マスタークライアントのみに送信
photonClient.raiseEvent(1, data, { 
  receivers: 2  // ReceiverGroup.MasterClient
});
  • options.targetActors (number[]):特定のプレイヤーにのみ送信する
// プレイヤー3と5にだけ送信
photonClient.raiseEvent(1, { message: 'Secret message' }, {
  targetActors: [3, 5]
});
  • options.cache (number):イベントをサーバーにキャッシュする
// ゲーム開始イベントをキャッシュ(後から参加者も受信)
photonClient.raiseEvent(EVENT_GAME_START, {
  startTime: Date.now(),
  mapId: 'forest'
}, {
  cache: 4  // AddToRoomCache
});

例えば以下のように他のプレイヤーに自分の座標を送信します。

const EVENT_MOVE = 2;

const movePlayer = (x: number, y: number) => {
  photonClient.raiseEvent(EVENT_MOVE, {
    x: x,
    y: y,
    rotation: player.rotation
  }, {
    receivers: 0  // Others - 自分以外
  });
}

他のプレイヤーはonEventというコールバック関数で受信できます。

photonClient.onEvent = (code, content, actorNr) => {
  if (code === EVENT_MOVE) {
    updateOtherPlayer(actorNr, content.x, content.y, content.rotation);
  }
};

基本的には、eventCodeを送信データごとに分けて同期し合う実装となります。
シンプルなのでアプリに合わせて独自で実装する部分が多くなりそうですね💦

まとめ

これまで紹介した関数を使用して、サーバーに接続しルームの入退出ができるwebアプリを作成してみました。


作成した感想ですが、JavaScript(TypeScript)でRealtimeを使った情報が少ないのでSDKの導入に時間がかかりました。
また、PUN2やfusionに比べ同期処理に関する便利な関数が少ないので、本格的にマルチプレイゲームを開発するのは難しそうです。やはり基本はUnity向けなのかもしれません!
ここまで読んでいただきありがとうございました!
またどこかでお会いしましょう🐊🐊

おわりに

KENTEMでは、様々な拠点でエンジニアを大募集しています! 建設×ITにご興味頂いた方は、是非下記のリンクからご応募ください。 recruit.kentem.jp career.kentem.jp