3 - オーソリテーティブな動作

<< 前章へ

Contents

本章では、エンティティおよびゲーム内の移動の管理について触れます。Boltにおける control のコンセプトがどのようなものか、そしてどのようにオーソリテーティブな移動が処理されるかについて学びましょう。

サーバーとクライアントの差異を隠す

エンティティの管理について学ぶ前に、Boltおよびマルチプレイヤーで一般的に多く生じることについて説明します。

問題: サーバーをゲームにおける単なる一個のプレイヤーとしたい場合、サーバーがそれ自体への繋がりとして存在していない点をどう処理すべきか、というのがここでの問題です。

サーバーに接続する各クライアントは、BoltConnectionオブジェクトにより表され、各クライアント上でサーバーは単一のBoltConnectionオブジェクトとして表されます。 しかし、サーバー自体の"サーバープレイヤー"に対して何かする場合、それを参照するのは困難です。これは、サーバー自体を表すオブジェクトが存在しないためです。

これを解決するには、シンプルな抽象を作成する必要があります。これにより、特定の接続ではなく Player オブジェクトを扱うことができるようになります。このプレイヤーオブジェクトにおいて、接続の有無を隠すことができ、残りのコードではそれを考慮する必要がなくなります。 新しいフォルダ tutorial/scripts/Player と、2つの新しいC#ファイルを作成してください:TutorialPlayerObject.csTutorialPlayerObjectRegistry.cs です。 TutorialPlayerObject クラスで開始します。

using UnityEngine;

public class TutorialPlayerObject
{
  public BoltEntity character;
  public BoltConnection connection;
}

これは標準のC#クラスでUnityのMonoBehaviourクラスから継承されて いません。 これはとても重要です。 また、これにはcharacterconnectionという2つのフィールドが含まれています。 characterフィールドには、世界でのプレイヤーのキャラクターを表す、インスタンスが作成されたオブジェクトが含まれます。 connectionフィールドには、それが存在する場合に このプレイヤーに対する接続が含まれます。これは、サーバープレイヤーオブジェクトに対するサーバー上でnullとなります。

また、2つのプロパティを追加します。これにより、connectionフィールドを直接的に扱うことなく、これがクライアントなのかサーバープレイヤーオブジェクトなのかを確認することができます。

using UnityEngine;

public class TutorialPlayerObject
{

    public BoltEntity character;
    public BoltConnection connection;

    public bool IsServer
    {
        get { return connection == null; }
    }

    public bool IsClient
    {
        get { return connection != null; }
    }
}

IsServerおよびIsClientは、その接続がnullかどうかを確認します。これにより、そのプレイヤーがサーバーとクライアントのどちらを表しているのかを知ることができます。TutorialPlayerObjectにさらに機能を追加する前に、TutorialPlayerObjectRegistryクラスを開きましょう。 このクラスを用いて、TutorialPlayerObjectクラスのインスタンスを管理します。

このクラス全体において唯一標準のC#コードでないのが、BoltConnectionにおけるUserDataプロパティへのアクセスです。 このプロパティは、接続とペアリングしたいその他のオブジェクトまたはデータのあらゆるタイプに固執することができる場所です。 今回の場合、作成したTutorialPlayerObjectを、従属する接続とペアリングさせます(従属する場合)。

このクラスの残りの部分には、以下のコメントやコードを読む上でBoltに特別なものが多少含まれていますが、それについてここで掘り下げることはしません。

using System.Collections.Generic;

public static class TutorialPlayerObjectRegistry
{
    // keeps a list of all the players
    static List<TutorialPlayerObject> players = new List<TutorialPlayerObject>();

    // create a player for a connection
    // note: connection can be null
    static TutorialPlayerObject CreatePlayer(BoltConnection connection)
    {
        TutorialPlayerObject player;

        // create a new player object, assign the connection property
        // of the object to the connection was passed in
        player = new TutorialPlayerObject();
        player.connection = connection;

        // if we have a connection, assign this player
        // as the user data for the connection so that we
        // always have an easy way to get the player object
        // for a connection
        if (player.connection != null)
        {
            player.connection.UserData = player;
        }

        // add to list of all players
        players.Add(player);

        return player;
    }

    // this simply returns the 'players' list cast to
    // an IEnumerable<T> so that we hide the ability
    // to modify the player list from the outside.
    public static IEnumerable<TutorialPlayerObject> AllPlayers
    {
        get { return players; }
    }

    // finds the server player by checking the
    // .IsServer property for every player object.
    public static TutorialPlayerObject ServerPlayer
    {
        get { return players.Find(player => player.IsServer); }
    }

    // utility function which creates a server player
    public static TutorialPlayerObject CreateServerPlayer()
    {
        return CreatePlayer(null);
    }

    // utility that creates a client player object.
    public static TutorialPlayerObject CreateClientPlayer(BoltConnection connection)
    {
        return CreatePlayer(connection);
    }

    // utility function which lets us pass in a
    // BoltConnection object (even a null) and have
    // it return the proper player object for it.
    public static TutorialPlayerObject GetTutorialPlayer(BoltConnection connection)
    {
        if (connection == null)
        {
            return ServerPlayer;
        }

        return (TutorialPlayerObject) connection.UserData;
    }
}

TutorialServerCallbacks.csファイルを開き、ファイル内のクラスを更新します。

  1. Remove the two calls to BoltNetwork.Instantiate;
  2. UnityのAwake機能を実装して、その内部のTutorialPlayerObjectRegistry.CreateServerPlayer を呼び出します。これにより、このコールバックオブジェクトがアクティブになるとき、常にサーバープレイヤーが作成されます。 3.また、TutorialServerCallbacksにおいて、Bolt.GlobalEventListenerから継承されているConnectedというメソッドをオーバーライドしてください。 その中で、TutorialPlayerObjectRegistry.CreateClientPlayerを呼び出し、その接続引数内で渡してください。
using UnityEngine;

[BoltGlobalBehaviour(BoltNetworkModes.Server, "Level2")]
public class TutorialServerCallbacks : Bolt.GlobalEventListener
{
    void Awake()
    {
        TutorialPlayerObjectRegistry.CreateServerPlayer();
    }

    public override void Connected(BoltConnection connection)
    {
        TutorialPlayerObjectRegistry.CreateClientPlayer(connection);
    }

    public override void SceneLoadLocalDone(string map)
    {
    }

    public override void SceneLoadRemoteDone(BoltConnection connection)
    {
    }
}

いよいよそれぞれのプレイヤーに対応したキャラクターをスポーンさせ、それぞれに適切に管理を割り当てます。TutorialPlayerObjectクラスを再度開き、2つのメソッドを追加してください。

  1. Spawn:キャラクターをスポーンします;
  2. RandomPosition: スポーンする場所をゲーム内でランダムに選びます
using UnityEngine;
using System.Collections.Generic;

public class TutorialPlayerObject
{
    // ...

    public void Spawn()
    {
        if (!character)
        {
            character = BoltNetwork.Instantiate(BoltPrefabs.TutorialPlayer, RandomPosition(), Quaternion.identity);


            if (IsServer)
            {
                character.TakeControl();
            }
            else
            {
                character.AssignControl(connection);
            }
        }

        // teleport entity to a random spawn position
        character.transform.position = RandomPosition();
    }

    Vector3 RandomPosition()
    {
        float x = Random.Range(-32f, +32f);
        float z = Random.Range(-32f, +32f);
        return new Vector3(x, 32f, z);
    }
}

Spawnでは、キャラクターが居るかどうかが最初に確認され、キャラクターが存在 しない 場合、BoltNetwork.Instantiateを呼び出してキャラクターが作成されます。それから、サーバーかどうかが確認され、管理を行うか手放すか、適切なメソッドを呼び出します。 キャラクターオブジェクトについてtransform.positionのプロパティを設定します。これにより、ゲーム内でプレイヤーをランダムな位置に移動させることができます。

ゲームを開始する前にあと2つするべきことがあります。 まず、tutorial/Scripts/Callbacks から、TutorialPlayerCallbacksクラスを開いてください。 その後、ControlOfEntityGainedというコールバックをオーバーライドしてください。これは、エンティティの管理を得た際に告知を行うものです。 このコールバックについての詳細は、 APIページを参照してください。

using Bolt.AdvancedTutorial;
using UnityEngine;

[BoltGlobalBehaviour("Level2")]
public class TutorialPlayerCallbacks : Bolt.GlobalEventListener
{
    public override void SceneLoadLocalDone(string map)
    {
        // this just instantiates our player camera,
        // the Instantiate() method is supplied by the BoltSingletonPrefab<T> class
        PlayerCamera.Instantiate();
    }

    public override void ControlOfEntityGained(BoltEntity entity)
    {
        // this tells the player camera to look at the entity we are controlling
        PlayerCamera.instance.SetTarget(entity);
    }
}

最後に、TutorialServerCallbacksに戻り、シーンの読み込みが完了したらSpawnメソッドを呼び出してください。この動作はサーバー上に存在する([BoltGlobalBehaviour(BoltNetworkModes.Host, "Level2")]属性の優待である)ため、サーバー自体への SceneLoadLocalDoneと、クライアントへのSceneLoadRemoteDoneを確認する必要があります。

using UnityEngine;

[BoltGlobalBehaviour(BoltNetworkModes.Server, "Level2")]
public class TutorialServerCallbacks : Bolt.GlobalEventListener
{
    void Awake()
    {
        TutorialPlayerObjectRegistry.CreateServerPlayer();
    }

    public override void Connected(BoltConnection connection)
    {
        TutorialPlayerObjectRegistry.CreateClientPlayer(connection);
    }

    public override void SceneLoadLocalDone(string map)
    {
        TutorialPlayerObjectRegistry.ServerPlayer.Spawn();
    }

    public override void SceneLoadRemoteDone(BoltConnection connection)
    {
        TutorialPlayerObjectRegistry.GetTutorialPlayer(connection).Spawn();
    }
}

Bolt Scenes ウィンドウに進み、Play As Server をクリックすると以下のような画面が表示されます。

Game running with our Tutorial Player
Tutorial Playerで実行中のゲーム

ここまでで、キャラクターをスポーンし、そのコントロールの割り当てを行い、カメラがそれを見ている状態となっています。 次にするべき点は、キャラクターを動かし管理することです。 個別のクライアントを作成し、エディター内で開始しているサーバーに接続することもできます。クライアントが正しくスポーンされ、サーバーのようにキャラクターを割り当てられていることを確認できます。

注: カメラのコードの仕様上、キャラクターの周りでカメラを回転させることはできません。カメラはキャラクターが動かない限りは静止したままとなり、この挙動は意図されたものです。

Back To Top

動作

このセクションでは、多くのユーザーから質問が寄せられる点について説明します:サーバーによってまだ管理および確認されるクライアント上の、瞬間的な移動のクライアント側での予測をともなうオーソリテーティブな動作です。また、 Bolt はこれを完全な透明性の中で行い、移動コードの観点から クライアント である点と サーバー である点の差異を排除します。

Photon Boltではこの概念を実現するのに コマンド を使用します。commandは、コントローラーからサーバーへ制御の情報(入力)を表す一連のデータです。サーバーがcommandを実行し、結果を計算し、ネットワーク上で結果を複製します。 コマンド についての詳細は、 専用ページを参照してください。

新しい Command の作成から開始します。Bolt Assets ウィンドウ (Bolt/Assets)で右クリックし、New Command を選択します。

Creating a new Command
新しいコマンドの作成

Coomand の設定を行い、プレイヤー動作に必要な inputsresults を与えます。  Bolt Assets ウィンドウでコマンドをクリックして選択すると、Bolt Editor ウィンドウが表示され、左上に New Property ボタンの代わりに New InputNew Result という2つのボタンが表示されます。 コマンドにデータを追加していく前に、実際に InputResult が表すものについて詳細を見てみましょう。

Input は一般に、あるプレイヤーからのプレイヤー入力を示します。 例えば、移動の "Forward""Backward"、マウス回転の "YRotation""XRotation" などがそれに当たります。 また、"SelectedWeapon" などのように、より抽象的なものであることもあります。

Result は、その Input をオブジェクトに適用した結果の状態を示します。ここでの共通のプロパティとして、位置速度 に対する値があるほか、isGrounded などの異なる状態タイプに対するフラグもあります。

以上に留意して、コマンドに入力を追加してみましょう。 以下の 入力 プロパティが必要です。

  1. Rename it to TutorialPlayerCommand;
  2. Set the Correction Interpolation to value 30;
  3. Add the Input:
    • Forward - Bool: 前進キーを押し続けているかどうか。
    • Backward - Bool: 後退キーを押し続けているかどうか。
    • Left - Bool: 左キーを押し続けているかどうか。
    • Right - Bool: 右キーを押し続けているかどうか。
    • Jump - Bool: ジャンプキーを押したかどうか。
    • Yaw - Float: Y軸上の現在の回転。
    • Pitch - Float: X軸上の現在の回転。
  4. Add the Results:

    • Position - Vector3.
    • Velocity - Vector3.
    • IsGrounded - Bool: 地面に接しているかどうか。
    • JumpFrames - Integer: ジャンプを適用するのに残っている"フレーム"数を示しています。これから用いるキャラクター動力に固有なものです。
  5. 名前を TutorialPlayerCommand に変更します。;

  6. Correction Interpolationの値を30に設定します。;
  7. Input を追加します:
    • Forward [Bool]: 前進キーを押し続けているかどうか。;
    • Backward [Bool]: 後退キーを押し続けているかどうか。;
    • Left [Bool]: 左キーを押し続けているかどうか。;
    • Right [Bool]: 右キーを押し続けているかどうか。;
    • Jump [Bool]: ジャンプキーを押したかどうか。;
    • Yaw [Float]: Y軸上の現在の回転。;
    • Pitch [Float]: X軸上の現在の回転。
  8. Add the Results:
    • Position [Vector3]: 入力を適用した後のキャラクターの最終位置。;
    • Velocity [Vector3]: 入力を適用した後のキャラクターの現在の速度;
    • IsGrounded [Bool]: 地面についているかどうか;
    • JumpFrames [Integer]: 少し変わっていますが、ジャンプ力を適用するのにいくつの「フレーム」を残したかを表す数字です。具体的な詳細キャラクターモーターです。 

Command 詳細の作成を完了すると、以下のようになります:

*TutorialPlayerCommand* with all Inputs and Results
TutorialPlayerCommand のすべてのInputとResult

コマンドが完了しました。後に追加をおこないますが、現在のところ、動作を機能させるのに必要なことはこれで全てです。Bolt/Compile Assemblyから再度コンパイルを行ってください。これにより、Boltは作成した新しいコマンドによって内部データを更新します。

次の設定はキャラクターの動力です。Boltには、必要なすべての機能をサポートする作動中のキャラクター動力が既に存在します。 Assets/samples/AdvancedTutorial/scripts/Player/PlayerMotor.csで確認してください。スクリプトや TutorialPlayer プレハブを検索し、動力のコピーをプレハブに添付してください。

Attach the *PlayerMotor* to the *TutorialPlayer* prefab
TutorialPlayer プレハブに PlayerMotor を添付

Player Motor コンポーネントと、自動的に追加される Character Controller コンポーネントで数点、設定調整をおこなう必要があります。 1. Step Offset0.5に設定; 2. Center(0, 1, 0)に設定; 3. Height2.2に設定; 4. Layer MaskTerrainに設定.

*TutorialPlayer* configuration
TutorialPlayer の設定

インプットを受信し、モーターを送信するため、スクリプトが必要になりました。controller が必要です。 tutorial/Scripts/Player フォルダに、TutorialPlayerController.cs という新しいスクリプトを作成します。

*TutorialPlayerController* script location
TutorialPlayerController スクリプトの場所

新しいクラスはBolt.EntityBehaviour<ITutorialPlayerState>から継承されているため、TutorialPlayerStateのアセット上のデータに対して直接的かつ静的なアクセスが得られます。 Bolt.EntityBehaviourBolt.GlobalEventListenerに似ていますが、各Bolt Entities上でゲームコードとBolt SDKの間でインターフェイスとして使用するものです。 generic引数としてITutorialPlayerState状態を使用することで、Boltにこのクラスが管理する State の種類を伝えています。

using Bolt;
using Bolt.AdvancedTutorial;
using UnityEngine;

public class TutorialPlayerController : Bolt.EntityBehaviour<ITutorialPlayerState>
{
}

The player controller is a large class that needs to be written, please follow the small pieces of code, one at time. We will start by adding fields for our inputs, motor and also a constant to the TutorialPlayerController class:

using Bolt;
using Bolt.AdvancedTutorial;
using UnityEngine;

public class TutorialPlayerController : Bolt.EntityBehaviour<ITutorialPlayerState>
{
    const float MOUSE_SENSITIVITY = 2f;

    bool _forward;
    bool _backward;
    bool _left;
    bool _right;
    bool _jump;

    float _yaw;
    float _pitch;

    PlayerMotor _motor;

    // ...
}

それから標準的なUnityのAwakeメソッドを定義し、その中で動力を参照してください。

  // ...

  void Awake()
  {
      _motor = GetComponent<PlayerMotor>();
  }

  // ...

また、エンティティの変換についてBoltに認識させる必要があります。Bolt.EntityBehaviour ベースクラス (API)により提供されている'Attached'メソッドをオーバーライドしてください。このメソッド内でstate.Transformにアクセスし、SetTransformsメソッドを呼び出し、ゲームオブジェクトの変換を行ってください。 Entity変換を同期する場合、推奨されるアプローチとなります。

// ...
public override void Attached()
{
    // This couples the Transform property of the State with the GameObject Transform
    state.SetTransforms(state.Transform, transform);
}
// ...

次にPollKeysを扱います。PollKeysは、ローカルのプレイヤーによる入力データのバッファに用いられます。これには、私たちのボタンやマウスの移動も全て含まれます。

  // ...

  void PollKeys(bool mouse)
  {
    _forward = Input.GetKey(KeyCode.W);
    _backward = Input.GetKey(KeyCode.S);
    _left = Input.GetKey(KeyCode.A);
    _right = Input.GetKey(KeyCode.D);
    _jump = Input.GetKeyDown(KeyCode.Space);

    if (mouse)
    {
      _yaw += (Input.GetAxisRaw("Mouse X") * MOUSE_SENSITIVITY);
      _yaw %= 360f;

      _pitch += (-Input.GetAxisRaw("Mouse Y") * MOUSE_SENSITIVITY);
      _pitch = Mathf.Clamp(_pitch, -85f, +85f);
    }
  }

  // ...

これは非常に標準的なUnityの入力コードであるため、説明はそれほど必要ないでしょう。唯一特筆に値するのは、bool mouseパラメータです。これにより、マウスによる入力をポーリングするべきか否かが分かります。これについては後述します。

UnityはUpdate内のInputクラスの状態を更新するため、単純なUpdate 機能を定義します。たとえば、単にPollKeys 機能を呼ぶなどです 。PollKeys機能にtrueを渡し、マウスの移動も読み込むことができるようにします。

  // ...

  void Update()
  {
    PollKeys(true);
  }

  // ...

いよいよ、Boltに特有な点に触れていきましょう。まず、SimulateControllerというメソッドをオーバーライドします。これは、エンティティの 管理 を割り当てられたコンピュータ上でのみ呼び出されます。 始めに、PollKeysを再度呼び出します。また、マウスのデータを読み込まないようにするにはfalseを渡します。 これは、マウスのデータをここで再度読み込むと、マウスの移動が重複するためです。

次に、TutorialPlayerCommand アセットから、TutorialPlayerCommand.Create()を呼び出し、Boltがコンパイルしたコマンド入力のインスタンスを作成してください。ローカルの変数からの入力データを全て、入力にコピーしてください。

最後に、エンティティ上にてQueueInputを呼び出してください。これにより、サーバーとクライアントの両方に入力が送信され処理されます。また、Boltがクライアント側の予測を行いながら、サーバー上の権限を保持し続けることもできます。

// ...
public override void SimulateController()
{
    PollKeys(false);

    ITutorialPlayerCommandInput input = TutorialPlayerCommand.Create();

    input.Forward = _forward;
    input.Backward = _backward;
    input.Left = _left;
    input.Right = _right;
    input.Jump = _jump;
    input.Yaw = _yaw;
    input.Pitch = _pitch;

    entity.QueueInput(input);
}
// ...

最後に扱うメソッドは、Boltの中でも最も重要なものの1つです。オーソリテーティブな移動や管理に関するこの素晴らしいメソッドは、ExecuteCommandという名前です。これはエンティティのオーナーおよびコントローラーの両方で実行されます。

この機能への最初のパラメータは常にSimulateControllerからQueueInputによって送信された入力を含むコマンドです。2つ目のパラメータはresetStateです。これは コントローラ上でのみtrueとなる ことに注意してください。これはコントローラー(通常はクライアント)に、オーナー(通常サーバー)が接続を送信したことを伝えます。また、動力の状態をリセットしなくてはいけません。

機能のコード内で、resetStateがtrueになっているかを確認してください。trueになっている場合、コマンドを"実行"はせず、単に動力のローカルの状態をリセットしてください。もしtrueになっていなかったら、Moveを呼び出し、コマンドの入力を動力に適用します。これにより 渡されたコマンド上のResultプロパティにコピーされた 新規の状態が返されます。

動力の状態を渡されたコマンドに割り当てることは重要です。これにより、Boltはコマンドの正しい結果をオーナーからコントローラに適用します。

// ...
public override void ExecuteCommand(Command command, bool resetState)
{
    TutorialPlayerCommand cmd = (TutorialPlayerCommand)command;

    if (resetState)
    {
      // we got a correction from the server, reset (this only runs on the client)
        _motor.SetState(cmd.Result.Position, cmd.Result.Velocity, cmd.Result.IsGrounded, cmd.Result.JumpFrames);
    }
    else
    {
        // apply movement (this runs on both server and client)
        PlayerMotor.State motorState = _motor.Move(cmd.Input.Forward, cmd.Input.Backward, cmd.Input.Left, cmd.Input.Right, cmd.Input.Jump, cmd.Input.Yaw);

        // copy the motor state to the commands result (this gets sent back to the client)
        cmd.Result.Position = motorState.position;
        cmd.Result.Velocity = motorState.velocity;
        cmd.Result.IsGrounded = motorState.isGrounded;
        cmd.Result.JumpFrames = motorState.jumpFrames;
    }
}
// ...

TutorialPlayerController コンポーネントのコピーを TutorialPlayer プレハブに添付してください。Play As Server を押すと、ゲーム内を(アニメーションなしで)移動し、キャラクターを動かすことができます。 これについては、TutorialPlayerControllerに対する完全なコードを参照してください。

using Bolt;
using Bolt.AdvancedTutorial;
using UnityEngine;

public class TutorialPlayerController : Bolt.EntityBehaviour<ITutorialPlayerState>
{
    const float MOUSE_SENSITIVITY = 2f;

    bool _forward;
    bool _backward;
    bool _left;
    bool _right;
    bool _jump;

    float _yaw;
    float _pitch;

    PlayerMotor _motor;

    void Awake()
    {
        _motor = GetComponent<PlayerMotor>();
    }

    public override void Attached()
    {
        // This couples the Transform property of the State with the GameObject Transform
        state.SetTransforms(state.Transform, transform);
    }

    void PollKeys(bool mouse)
    {
        _forward = Input.GetKey(KeyCode.W);
        _backward = Input.GetKey(KeyCode.S);
        _left = Input.GetKey(KeyCode.A);
        _right = Input.GetKey(KeyCode.D);
        _jump = Input.GetKeyDown(KeyCode.Space);

        if (mouse)
        {
            _yaw += (Input.GetAxisRaw("Mouse X") * MOUSE_SENSITIVITY);
            _yaw %= 360f;

            _pitch += (-Input.GetAxisRaw("Mouse Y") * MOUSE_SENSITIVITY);
            _pitch = Mathf.Clamp(_pitch, -85f, +85f);
        }
    }

    void Update()
    {
        PollKeys(true);
    }

    public override void SimulateController()
    {
        PollKeys(false);

        ITutorialPlayerCommandInput input = TutorialPlayerCommand.Create();

        input.Forward = _forward;
        input.Backward = _backward;
        input.Left = _left;
        input.Right = _right;
        input.Jump = _jump;
        input.Yaw = _yaw;
        input.Pitch = _pitch;

        entity.QueueInput(input);
    }

    public override void ExecuteCommand(Command command, bool resetState)
    {
        TutorialPlayerCommand cmd = (TutorialPlayerCommand) command;

        if (resetState)
        {
            // we got a correction from the server, reset (this only runs on the client)
            _motor.SetState(cmd.Result.Position, cmd.Result.Velocity, cmd.Result.IsGrounded, cmd.Result.JumpFrames);
        }
        else
        {
            // apply movement (this runs on both server and client)
            PlayerMotor.State motorState = _motor.Move(cmd.Input.Forward, cmd.Input.Backward, cmd.Input.Left, cmd.Input.Right, cmd.Input.Jump, cmd.Input.Yaw);

            // copy the motor state to the commands result (this gets sent back to the client)
            cmd.Result.Position = motorState.position;
            cmd.Result.Velocity = motorState.velocity;
            cmd.Result.IsGrounded = motorState.isGrounded;
            cmd.Result.JumpFrames = motorState.jumpFrames;
        }
    }
}

以下は実行中のゲームの動画です。サーバーと、2つのクライアントが接続していることがわかります。

これで第三章は終了です。ここまでで、以下のようにゲームが動くようになりました。

Game running with two clients
2つのクライアントに接続して実行中のゲーム

次章へ>>

ドキュメントのトップへ戻る