Realtimeイントロ

Photon Realtimeは、我々のマルチプレイヤーゲームおよび高レベルネットワークソリューションの基盤レイヤーです。マッチングや高速通信などの問題をスケーラブルなアプローチで解消します。ゲームや、我々のより具体的なマルチプレイヤーソリューションであるPUNやQuantumで使用されています。

Unityエンジンを使用して開発する場合には、FusionまたはQuantumの使用を検討してください。これらは、ゲームステートやシミュレーションを正確に同期するのに役立ちます。

Photon RealtimeにはAPI、ソフトウェアツール、サービスの包括的なフレームワークも含まれており、クライアントとサーバーの相互作用を定義づけています。

本ドキュメントはクライアント側のRealtime APIマニュアルとして作成されていますが、関係する構造についての概要説明でもあります。

クライアントとサーバー

Photon Realtime APIは、ゲームやアプリで直接使用されるクライアント側のソフトウェアです。これがあると、アプリでConnectJoinRandomRoomRaiseEventを行えるようになります。

主要なプラットフォームやエンジンで複数言語の使用が可能(SDKダウンロードページ)で、iOSやAndroid、ウェブ、コンソールやスタンドアロンなどに関わりなくほぼ全てのクライアント同士でやりとりできます。


Photon Realtimeクライアントは全員、認証と地域振分け(ネームサーバー)、マッチメイキング(マスターサーバー)、ゲームプレイ(ゲームサーバー)の3つの別タスクに分けられた専用サーバーシーケンスに接続します。これらのサーバーはRealtime APIに処理されるので下位にかける必要はありませんが、背景を知っておくのも良いかと思います。

Photon Cloudは完全なマネージドサービスで、Photon Realtimeクライアントの世界中のホスティングを提供します。無料のPhotonアカウントでは義務なしのアクセスができ、Photon Realtimeの使用に慣れる間Photon Cloudをご使用いただくことを推奨しています。もちろん、独自のPhotonサーバーを使用したければ後から移行することもできます。

コードサンプル

以下は、Realtime APIがどのように使用されているのか理解するためのコードサンプルです。完全な使用ガイドではなく概要としてとらえてください。

Photon Cloudを使用してサーバーのセットアップを省きましょう。コード内にAppIdの設定が必要です。

無料サインアップの後にRealtimeダッシュボードからAppIDを取得してください。

接続

Photon Cloudに接続

T以下のコードは、AppIDに入力するとPhoton Cloudに接続するコードです。

C#

using System;
using System.Collections.Generic;
using System.Threading;
using Photon.Realtime;


class GameClass : IConnectionCallbacks
{
    private readonly LoadBalancingClient client = new LoadBalancingClient();
    private bool quit;

    ~GameClass()
    {
        this.client.Disconnect();
        this.client.RemoveCallbackTarget(this);
    }

    public void StartClient()
    {
        this.client.AddCallbackTarget(this);
        this.client.StateChanged += this.OnStateChange;

        this.client.ConnectUsingSettings(new AppSettings() { AppIdRealtime = "<your appid>", FixedRegion = "eu" });

        Thread t = new Thread(this.Loop);
        t.Start();

        Console.WriteLine("Running until key pressed.");
        Console.ReadKey();
        this.quit = true;
    }

    private void Loop(object state)
    {
        while (!this.quit)
        {
            this.client.Service();
            Thread.Sleep(33);
        }
    }

    private void OnStateChange(ClientState arg1, ClientState arg2)
    {
        Console.WriteLine(arg1 + " -> " + arg2);
    }

    #region IConnectionCallbacks

    public void OnConnectedToMaster()
    {
        Console.WriteLine("OnConnectedToMaster Server: " + this.client.LoadBalancingPeer.ServerIpAddress);
    }

    public void OnConnected()
    {
    }

    public void OnDisconnected(DisconnectCause cause)
    {
    }

    public void OnRegionListReceived(RegionHandler regionHandler)
    {
    }

    public void OnCustomAuthenticationResponse(Dictionary<string, object> data)
    {
    }

    public void OnCustomAuthenticationFailed(string debugMessage)
    {
    }

    #endregion
}

C++

class SampleNetworkLogic
{
public:
    SampleNetworkLogic(const ExitGames::Common::JString& appID, const ExitGames::Common::JString& appVersion);
    void connect(void);
    void disconnect(void);
    void run(void);
private:
    ExitGames::LoadBalancing::Client mLoadBalancingClient;
    Listener mListener; // your implementation of the ExitGames::LoadBalancing::Listener interface
    ExitGames::Common::Logger mLogger; // accessed by EGLOG()
};

SampleNetworkLogic::SampleNetworkLogic(const ExitGames::Common::JString& appID, const ExitGames::Common::JString& appVersion)
    : mLoadBalancingClient(mListener, appID, appVersion)
{
}

void SampleNetworkLogic::connect(void)
{
    // connect() is asynchronous - the actual result arrives in the Listener::connectReturn() or the Listener::connectionErrorReturn() callback
    if(!mLoadBalancingClient.connect())
        EGLOG(ExitGames::Common::DebugLevel::ERRORS, L"Could not connect.");
}

int main(void)
{
    static const ExitGames::Common::JString appID = L"<no-app-id>"; // set your app id here
    static const ExitGames::Common::JString appVersion = L"1.0";

    SampleNetworkLogic networkLogic(appID, appVersion);

    networkLogic.connect();

セルフホスティングのPhoton Serverに接続

セルフホスティングのPhoton Serverに接続するには、AppIdまたはリージョンは必要ありません。 必要なのは、クライアントの接続方法で使用するサーバーのアドレスとポートだけです。

Photon CloudとPhoton Serverの違いについてはこちら。特に、"v4.0.29.11263以来の重要な変更"です。

Serviceの呼び出し

LoadBalancing APIはゲームロジックと最適に統合するよう構築されています。 受信メッセージの処理のタイミングや、送信頻度を任意で設定できます。 内部的には、ゲームがLoadBalancingClient.Service()を呼ぶまで、送受信ともにバッファされます。

多くの場合、ゲームはアップデートを計算して画面をリフレッシュするゲームループを使用しています。 1秒ごとにService()を10~20回呼んでください。 Service()を呼ばないかぎり、"ネットワークプログレス"はまったく処理されません。

Service は2つのタスクをカバーします:

  • 受信したイベントとデータが実行されます。 これは、更新を処理できるときに行われます。 通常、シーケンスの順序はそのまま維持されるため、クライアントが受信するすべてのものがキューに入れられ、順序付けされます。 Serviceは、利用可能なメッセージを1つずつ渡すため、内部的にDispatchIncomingCommandsを呼び出します。

  • クライアントの送信データがサーバーに送信されます。 これには、サーバーへの接続を維持するために重要な確認応答(バックグラウンドで作成)が含まれます。 Service はこのタスクを行うために内部的にSendOutgoingCommandsを呼び出します。 SendOutgoingCommands呼び出しの頻度を制御すると、クライアントが生成したデータを送信するために使用するパッケージの数が制御されます。

    C#

    void GameLoop()
    {
        while (!shouldExit)
        {
            this.loadBalancingClient.Service();
            Thread.Sleep(100); // wait for few frames/milliseconds
        }
    }
    

    C++

    void SampleNetworkLogic::run(void)
    {
        mLoadBalancingClient.service(); // needs to be called regularly!
    }
    
    int main(void)
    {
        static const ExitGames::Common::JString appID = L"<no-app-id>"; // set your app id here
        static const ExitGames::Common::JString appVersion = L"1.0";
    
        SampleNetworkLogic networkLogic(appID, appVersion);
    
        networkLogic.connect();
    
        while(!shouldExit)
        {
            networkLogic.run();
            SLEEP(100);
        }
    

切断

アプリケーションが終了する際や、ユーザーがログアウトする際には、忘れずに切断するようにしてください。

C#

using System.Collections.Generic;
using Photon.Realtime;

public class MyClient : IConnectionCallbacks
{
    private LoadBalancingClient loadBalancingClient;

    public MyClient()
    {
        this.loadBalancingClient = new LoadBalancingClient();
        this.SubscribeToCallbacks();
    }

    ~MyClient()
    {
        this.Disconnect();
        this.UnsubscribeFromCallbacks();
    }

    private void SubscribeToCallbacks()
    {
        this.loadBalancingClient.AddCallbackTarget(this);
    }

    private void UnsubscribeFromCallbacks()
    {
        this.loadBalancingClient.RemoveCallbackTarget(this);
    }

    void Disconnect()
    {
        if (this.loadBalancingClient.IsConnected)
        {
            this.loadBalancingClient.Disconnect();
        }
    }

    void IConnectionCallbacks.OnDisconnected(DisconnectCause cause)
    {
        switch (cause)
        {
            // ...

C++

void SampleNetworkLogic::disconnect(void)
{
    mLoadBalancingClient.disconnect(); // disconnect() is asynchronous - the actual result arrives in the Listener::disconnectReturn() callback
}

int main(void)
{
    static const ExitGames::Common::JString appID = L"<no-app-id>"; // set your app id here
    static const ExitGames::Common::JString appVersion = L"1.0";

    SampleNetworkLogic networkLogic(appID, appVersion);

    networkLogic.connect();

    while(!shouldExit)
    {
        networkLogic.run();
        SLEEP(100);
    }

    networkLogic.disconnect();
}

マッチメイキング

ゲームの作成

新しいルームやゲームを作成するには、接続されたロードバランシングクライアントで"ルームの作成"操作をおこなってください

C#

using System.Collections.Generic;
using Photon.Realtime;

// we add IMatchmakingCallbacks interface implementation
public class MyClient : IConnectionCallbacks, IMatchmakingCallbacks
{
    private LoadBalancingClient loadBalancingClient;

    public MyClient()
    {
        this.loadBalancingClient = new LoadBalancingClient();
        this.SubscribeToCallbacks();
    }

    ~MyClient()
    {
        this.UnsubscribeFromCallbacks();
    }

    private void SubscribeToCallbacks()
    {
        this.loadBalancingClient.AddCallbackTarget(this);
    }

    private void UnsubscribeFromCallbacks()
    {
        this.loadBalancingClient.RemoveCallbackTarget(this);
    }

    void MyCreateRoom(string roomName, byte maxPlayers)
    {
      EnterRoomParams enterRoomParams = new EnterRoomParams();
      enterRoomParams.RoomName = roomName;
      enterRoomParams.RoomOptions = new RoomOptions();
      enterRoomParams.RoomOptions.MaxPlayers = maxPlayers;
      this.loadBalancingClient.OpCreateRoom(enterRoomParams);
    }

    void IMatchmakingCallbacks.OnJoinedRoom()
    {

    }

    // ...

C++

void createRoom(const ExitGames::Common::JString& roomName, nByte maxPlayers)
{
    mLoadBalancingClient.opCreateRoom(roomName, ExitGames::LoadBalancing::RoomOptions().setMaxPlayers(maxPlayers));
}

これによって、ルームの名前とルームで許可されるプレイヤーの人数が設定されます。 クライアントは、自動的に新しいルームに入室します。 "ルームの作成"操作をすると、まだそのルームが存在していない場合には、参加者がいなくてもルームが作成されます。 最後のプレイヤーが退室するまで、ルームは存在します。

ルーム作成時に、"カスタムルームプロパティ"を定義して共通の値を設定することができます。 カスタムルームプロパティを使用すると、マップ名、レベル、ラウンドの時間などを保存できます。 カスタムプロパティのキーは文字列でなければなりません。 もちろん、これらの値はルーム内での設定や変更も可能です。

任意の配列を"ロビー用のカスタムルームプロパティ"として設定し、 その配列にそれらのカスタムプロパティの名前をつけて設定することで、15個のカスタムプロパティをロビーに表示するよう選択できます。 ロビーに表示するプロパティは、マッチメイキングやランダムマッチメイキングのフィルタとして使用することが可能です。

マッチメイキングの詳細については、こちらを参照してください 。

ゲームの検索

クライアントは名前を指定するか、またはPhotonにマッチングのリクエストをしてゲームに参加します。

ルームの検索方法

  • Random: ランダムにプレイヤーをマッチングします。ルームを満員にするか、またはプレイヤーを均等に配分するかを選択できます。
  • Parameterized: フィルター として ロビープロパティ を定義して、ランダムマッチングをカスタマイズします。
  • Private: 名前を把握している、非表示のルームに参加します。
  • Listing: 選択可能なルームをロビーに表示し、プレイヤーが手動で選択したり、参加できるようにします。

C#

using System.Collections.Generic;
using ExitGames.Client.Photon;
using Photon.Realtime;

// we add IMatchmakingCallbacks interface implementation
public class MyClient : IConnectionCallbacks, IMatchmakingCallbacks
{
    private LoadBalancingClient loadBalancingClient;

    public MyClient()
    {
        this.loadBalancingClient = new LoadBalancingClient();
        this.SubscribeToCallbacks();
    }

    ~MyClient()
    {
        this.UnsubscribeFromCallbacks();
    }

    private void SubscribeToCallbacks()
    {
        this.loadBalancingClient.AddCallbackTarget(this);
    }

    private void UnsubscribeFromCallbacks()
    {
        this.loadBalancingClient.RemoveCallbackTarget(this);
    }

    void JoinRandomRoomByMap(byte maxPlayers, int mapIndex)
    {
        // join random rooms easily, filtering for specific room properties, if needed
        Hashtable expectedCustomRoomProperties = new Hashtable();

        // custom props can have any name but the key must be string
        expectedCustomRoomProperties["map"] = mapIndex;

        // joining a random room with the map we selected before
        this.loadBalancingClient.OpJoinRandomRoom(
                new OpJoinRandomRoomParams
                {
                    ExpectedCustomRoomProperties = expectedCustomRoomProperties,
                    ExpectedMaxPlayers = maxPlayers
                }
            );
    }

    void IMatchmakingCallbacks.OnJoinRoomFailed(short returnCode, string message)
    {
        if (returnCode == ErrorCode.NoRandomMatchFound)
        {
            // no match found, try another filter or create a room
        }
        // ...

C++

// join random rooms easily, filtering for specific room properties, if needed
ExitGames::Common::Hashtable expectedCustomRoomProperties;

// custom props can have any name but the key must be string
expectedCustomRoomProperties.put(L"map", 1);

// joining a random room with the map we selected before
mLoadBalancingClient.opJoinRandomRoom(expectedCustomRoomProperties);

ゲームの持続

Photon Realtimeでは、ルームのデータを簡単に保管およびローディングできます。 外部のWebサーバーとPhoton Cloudを接続するには、 Webhooks をセットアップする必要があります。

セットアップが完了すると、ルームステートは自動的に保存されるようになります。ルームに再参加するには:

C#

this.loadBalancingClient.OpReJoinRoom(savedRoomName);

C++

mLoadBalancingClient.opJoinRoom(savedRoomName, true);

この機能によって、非同期マッチメイキングとゲームプレイが可能になります。

詳細な手順は、ルーム持続性ガイドを参照してください。

ゲームプレイ

イベントの送信

1つのクライアント上で起こることは、すべて同じルームにいる全員を更新するイベントとして送信することができます。

位置、現在のターン、またはステートの値でプレイヤーを更新します。 Photonはその更新を、出来る限り速く送信します(信頼性は任意で設定可能です)。

  • Send messages/events: 他のプレイヤーにすべてのタイプのデータを送信
  • Player/Room properties: Photonは、後から参加したプレイヤーも含めて、これらのデータを更新および同期します。

C#

byte eventCode = 1; // make up event codes at will
Hashtable evData = new Hashtable(); // put your data into a key-value hashtable
this.loadBalancingClient.OpRaiseEvent(eventCode, evData, RaiseEventOptions.Default, SendOptions.SendReliable);

C++

nByte eventCode = 1; // use distinct event codes to distinguish between different types of events (for example 'move', 'shoot', etc.)
ExitGames::Common::Hashtable evData; // organize your payload data in any way you like as long as it is supported by Photons serialization
bool sendReliable = false; // send something reliable if it has to arrive everywhere
mLoadBalancingClient.opRaiseEvent(sendReliable, evData, eventCode);

イベントコードは200以下にしてください。 各コードはイベントの型と、受信者が想定できる内容を定義します。

上記例のイベントデータはHashtableです。 byte[]や、Photonのシリアル化(stringfloat[]など)で対応しているすべてのデータ型を使用できます。 詳細情報はPhotonでシリアル化を参照してください。

イベントの受信

イベントが送信されると、ハンドラーが呼ばれます。以下に例を示します。

C#

using System.Collections.Generic;
using ExitGames.Client.Photon;
using Photon.Realtime;

// we add IOnEventCallback interface implementation
public class MyClient : IConnectionCallbacks, IMatchmakingCallabacks, IOnEventCallback
{
    private LoadBalancingClient loadBalancingClient;

    public MyClient()
    {
        this.loadBalancingClient = new LoadBalancingClient();
        this.SubscribeToCallbacks();
    }

    ~MyClient()
    {
        this.UnsubscribeFromCallbacks();
    }

    private void SubscribeToCallbacks()
    {
        this.loadBalancingClient.AddCallbackTarget(this);
    }

    private void UnsubscribeFromCallbacks()
    {
        this.loadBalancingClient.RemoveCallbackTarget(this);
    }

    void IOnEventCallback.OnEvent(EventData photonEvent)
    {
        // we have defined two event codes, let's determine what to do
        switch (photonEvent.Code)
        {
            case 1:
                // do something
                break;
            case 2:
                // do something else
                break;
        }
    }

    // ...

C++

void NetworkLogic::customEventAction(int playerNr, nByte eventCode, const ExitGames::Common::Object& eventContent)
{
    // logging the string representation of the eventContent can be really useful for debugging, but use with care: for big events this might get expensive
    EGLOG(ExitGames::Common::DebugLevel::ALL, L"an event of type %d from player Nr %d with the following content has just arrived: %ls", eventCode, playerNr, eventContent.toString(true).cstr());

    switch(eventCode)
    {
    case 1:
        {
            // you can access the content as a copy (might be a bit expensive for really big data constructs)
            ExitGames::Common::Hashtable content = ExitGames::Common::ValueObject<ExitGames::Common::Hashtable>(eventContent).getDataCopy();
            // or you access it by address (it will become invalid as soon as this function returns, so (any part of the) data that you need to continue having access to later on needs to be copied)
            ExitGames::Common::Hashtable* pContent = ExitGames::Common::ValueObject<ExitGames::Common::Hashtable>(eventContent).getDataAddress();
        }
        break;
    case 2:
        {
            // of course the payload does not need to be a Hashtable - how about just sending around for example a plain 64bit integer?
            long long content = ExitGames::Common::ValueObject<long long>(eventContent).getDataCopy();
        }
        break;
    case 3:
        {
            // or an array of floats?
            float* pContent = ExitGames::Common::ValueObject<float*>(eventContent).getDataCopy();
            float** ppContent = ExitGames::Common::ValueObject<float*>(eventContent).getDataAddress();
            short contentElementCount = *ExitGames::Common::ValueObject<float*>(eventContent).getSizes();
            // when calling getDataCopy() on Objects that hold an array as payload, then you must deallocate the copy of the array yourself using deallocateArray()!
            ExitGames::Common::MemoryManagement::deallocateArray(pContent);
        }
        break;
    default:
        {
            // have a look at demo_typeSupport inside the C++ client SDKs for example code on how to send and receive more fancy data types
        }
        break;
    @}
}

各イベントは、クライアントが定義して送信したコードとデータを送信します。 アプリケーションは、渡されたコードによってどのコンテンツを期待すべきかを把握します(上記参照)。

デフォルトのイベントコードの最新のリストは、SDK内のイベントコード定数を確認してください。たとえば、C#の場合はExitGames.Client.Photon.LoadBalancing.EventCode内にあります。

カスタムサーバーまたはオーソリテーティブサーバーのロジック

オーソリテーティブロジックを加えなくても、Photon Cloud製品は幅広いタイプのゲームに対応しています。

  • ファーストパーソンシューター
  • レーシングゲーム
  • マインクラフトタイプのゲーム
  • カジュアルなリアルタイムゲーム
  • 非同期および同期ゲーム
  • ...

独自のカスタムロジックを実装するには、Photon Serverまたは プラグイン を使用してください。

Back to top