3 - オーソリテーティブな動作
本章では、エンティティおよびゲーム内の移動の管理について触れます。Boltにおける control のコンセプトがどのようなものか、そしてどのようにオーソリテーティブな移動が処理されるかについて学びましょう。
サーバーとクライアントの差異を隠す
エンティティの管理について学ぶ前に、Boltおよびマルチプレイヤーで一般的に多く生じることについて説明します。
問題: サーバーをゲームにおける単なる一個のプレイヤーとしたい場合、サーバーがそれ自体への繋がりとして存在していない点をどう処理すべきか、というのがここでの問題です。
サーバーに接続する各クライアントは、BoltConnection
オブジェクトにより表され、各クライアント上でサーバーは単一のBoltConnection
オブジェクトとして表されます。
しかし、サーバー自体の"サーバープレイヤー"に対して何かする場合、それを参照するのは困難です。これは、サーバー自体を表すオブジェクトが存在しないためです。
これを解決するには、シンプルな抽象を作成する必要があります。これにより、特定の接続ではなく Player オブジェクトを扱うことができるようになります。このプレイヤーオブジェクトにおいて、接続の有無を隠すことができ、残りのコードではそれを考慮する必要がなくなります。
新しいフォルダ tutorial/scripts/Player と、2つの新しいC#ファイルを作成してください:TutorialPlayerObject.cs と TutorialPlayerObjectRegistry.cs です。
TutorialPlayerObject
クラスで開始します。
C#
using UnityEngine;
public class TutorialPlayerObject
{
public BoltEntity character;
public BoltConnection connection;
}
これは標準のC#クラスでUnityのMonoBehaviour
クラスから継承されて いません。
これはとても重要です。
また、これにはcharacter
と connection
という2つのフィールドが含まれています。
character
フィールドには、ワールドでのプレイヤーのキャラクターを表す、インスタンスが作成されたオブジェクトが含まれます。
connection
フィールドには、それが存在する場合に このプレイヤーに対する接続が含まれます。これは、サーバープレイヤーオブジェクトに対するサーバー上でnullとなります。
また、2つのプロパティを追加します。これにより、connection
フィールドを直接的に扱うことなく、これがクライアントなのかサーバープレイヤーオブジェクトなのかを確認することができます。
C#
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に特別なものが多少含まれていますが、それについてここで掘り下げることはしません。
C#
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
ファイルを開き、ファイル内のクラスを更新します。
- Remove the two calls to
BoltNetwork.Instantiate
; - Unityの
Awake
機能を実装して、その内部のTutorialPlayerObjectRegistry.CreateServerPlayer
を呼び出します。これにより、このコールバックオブジェクトがアクティブになるとき、常にサーバープレイヤーが作成されます。
3.また、TutorialServerCallbacks
において、Bolt.GlobalEventListener
から継承されているConnected
というメソッドをオーバーライドしてください。
その中で、TutorialPlayerObjectRegistry.CreateClientPlayer
を呼び出し、その接続引数内で渡してください。
C#
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つのメソッドを追加してください。
Spawn
:キャラクターをスポーンします;RandomPosition
: スポーンする場所をゲーム内でランダムに選びます
C#
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ページを参照してください。
C#
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
を確認する必要があります。
C#
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 をクリックすると以下のような画面が表示されます。
ここまでで、キャラクターをスポーンし、そのコントロールの割り当てを行い、カメラがそれを見ている状態となっています。
次にするべき点は、キャラクターを動かし管理することです。
個別のクライアントを作成し、エディター内で開始しているサーバーに接続することもできます。クライアントが正しくスポーンされ、サーバーのようにキャラクターを割り当てられていることを確認できます。
注: カメラのコードの仕様上、キャラクターの周りでカメラを回転させることはできません。カメラはキャラクターが動かない限りは静止したままとなり、この挙動は意図されたものです。
動作
このセクションでは、多くのユーザーから質問が寄せられる点について説明します:サーバーによってまだ管理および確認されるクライアント上の、瞬間的な移動のクライアント側での予測をともなうオーソリテーティブな動作です。また、 Bolt はこれを完全な透明性の中で行い、移動コードの観点から クライアント である点と サーバー である点の差異を排除します。
Photon Boltではこの概念を実現するのに コマンド を使用します。command
は、コントローラーからサーバーへ制御の情報(入力)を表す一連のデータです。サーバーがcommand
を実行し、結果を計算し、ネットワーク上で結果を複製します。 コマンド についての詳細は、 専用ページを参照してください。
新しい Command の作成から開始します。Bolt Assets ウィンドウ (Bolt/Assets
)で右クリックし、New Command を選択します。
Coomand の設定を行い、プレイヤー動作に必要な inputs と results を与えます。
Bolt Assets ウィンドウでコマンドをクリックして選択すると、Bolt Editor ウィンドウが表示され、左上に New Property ボタンの代わりに New Input と New Result という2つのボタンが表示されます。
コマンドにデータを追加していく前に、実際に Input と Result が表すものについて詳細を見てみましょう。
Input は一般に、あるプレイヤーからのプレイヤー入力を示します。
例えば、移動の "Forward" や "Backward"、マウス回転の "YRotation" や "XRotation" などがそれに当たります。
また、"SelectedWeapon" などのように、より抽象的なものであることもあります。
Result は、その Input をオブジェクトに適用した結果の状態を示します。ここでの共通のプロパティとして、位置 と 速度 に対する値があるほか、isGrounded などの異なる状態タイプに対するフラグもあります。
以上に留意して、コマンドに入力を追加してみましょう。
以下の 入力 プロパティが必要です。
- Rename it to TutorialPlayerCommand;
- Set the
Correction Interpolation
to value30
; - Add the Input:
- Forward - Bool: 前進キーを押し続けているかどうか。
- Backward - Bool: 後退キーを押し続けているかどうか。
- Left - Bool: 左キーを押し続けているかどうか。
- Right - Bool: 右キーを押し続けているかどうか。
- Jump - Bool: ジャンプキーを押したかどうか。
- Yaw - Float: Y軸上の現在の回転。
- Pitch - Float: X軸上の現在の回転。
- Add the Results:
- Position - Vector3.
- Velocity - Vector3.
- IsGrounded - Bool: 地面に接しているかどうか。
- JumpFrames - Integer: ジャンプを適用するのに残っている"フレーム"数を示しています。これから用いるキャラクター動力に固有なものです。
- 名前を TutorialPlayerCommand に変更します。;
Correction Interpolation
の値を30
に設定します。;- Input を追加します:
- Forward [Bool]: 前進キーを押し続けているかどうか。;
- Backward [Bool]: 後退キーを押し続けているかどうか。;
- Left [Bool]: 左キーを押し続けているかどうか。;
- Right [Bool]: 右キーを押し続けているかどうか。;
- Jump [Bool]: ジャンプキーを押したかどうか。;
- Yaw [Float]: Y軸上の現在の回転。;
- Pitch [Float]: X軸上の現在の回転。
- Add the Results:
- Position [Vector3]: 入力を適用した後のキャラクターの最終位置。;
- Velocity [Vector3]: 入力を適用した後のキャラクターの現在の速度;
- IsGrounded [Bool]: 地面についているかどうか;
- JumpFrames [Integer]: 少し変わっていますが、ジャンプ力を適用するのにいくつの「フレーム」を残したかを表す数字です。具体的な詳細キャラクターモーターです。
Command 詳細の作成を完了すると、以下のようになります:
コマンドが完了しました。後に追加をおこないますが、現在のところ、動作を機能させるのに必要なことはこれで全てです。Bolt/Compile Assembly
から再度コンパイルを行ってください。これにより、Boltは作成した新しいコマンドによって内部データを更新します。
次の設定はキャラクターの動力です。Boltには、必要なすべての機能をサポートする作動中のキャラクター動力が既に存在します。
Assets/samples/AdvancedTutorial/scripts/Player/PlayerMotor.cs
で確認してください。スクリプトや TutorialPlayer プレハブを検索し、動力のコピーをプレハブに添付してください。
Player Motor コンポーネントと、自動的に追加される Character Controller コンポーネントで数点、設定調整をおこなう必要があります。
- Step Offset を
0.5
に設定; - Center を
(0, 1, 0)
に設定; - Height を
2.2
に設定; - Layer Mask を
Terrain
に設定.
インプットを受信し、モーターを送信するため、スクリプトが必要になりました。controller が必要です。
tutorial/Scripts/Player フォルダに、TutorialPlayerController.cs という新しいスクリプトを作成します。
新しいクラスはBolt.EntityBehaviour<ITutorialPlayerState>
から継承されているため、TutorialPlayerState
のアセット上のデータに対して直接的かつ静的なアクセスが得られます。
Bolt.EntityBehaviour
はBolt.GlobalEventListener
に似ていますが、各Bolt Entities
上でゲームコードとBolt SDKの間でインターフェイスとして使用するものです。
generic
引数としてITutorialPlayerState
状態を使用することで、Boltにこのクラスが管理する State の種類を伝えています。
C#
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:
C#
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
メソッドを定義し、その中で動力を参照してください。
C#
// ...
void Awake()
{
_motor = GetComponent<PlayerMotor>();
}
// ...
また、エンティティの変換についてBoltに認識させる必要があります。Bolt.EntityBehaviour
ベースクラス (API)により提供されている'Attached'メソッドをオーバーライドしてください。このメソッド内でstate.Transform
にアクセスし、SetTransforms
メソッドを呼び出し、ゲームオブジェクトの変換を行ってください。
Entity
変換を同期する場合、推奨されるアプローチとなります。
C#
// ...
public override void Attached()
{
// This couples the Transform property of the State with the GameObject Transform
state.SetTransforms(state.Transform, transform);
}
// ...
次にPollKeysを扱います。PollKeysは、ローカルのプレイヤーによる入力データのバッファに用いられます。これには、私たちのボタンやマウスの移動も全て含まれます。
C#
// ...
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を渡し、マウスの移動も読み込むことができるようにします。
C#
// ...
void Update()
{
PollKeys(true);
}
// ...
いよいよ、Boltに特有な点に触れていきましょう。まず、SimulateController
というメソッドをオーバーライドします。これは、エンティティの 管理 を割り当てられたコンピュータ上でのみ呼び出されます。
始めに、PollKeysを再度呼び出します。また、マウスのデータを読み込まないようにするにはfalseを渡します。
これは、マウスのデータをここで再度読み込むと、マウスの移動が重複するためです。
次に、TutorialPlayerCommand アセットから、TutorialPlayerCommand.Create()
を呼び出し、Boltがコンパイルしたコマンド入力のインスタンスを作成してください。ローカルの変数からの入力データを全て、入力にコピーしてください。
最後に、エンティティ上にてQueueInput
を呼び出してください。これにより、サーバーとクライアントの両方に入力が送信され処理されます。また、Boltがクライアント側の予測を行いながら、サーバー上の権限を保持し続けることもできます。
C#
// ...
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はコマンドの正しい結果をオーナーからコントローラに適用します。
C#
// ...
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
に対する完全なコードを参照してください。
C#
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つのクライアントが接続していることがわかります。
これで第三章は終了です。ここまでで、以下のようにゲームが動くようになりました。
Back to top