Unity では、Instantiate() を使用して新しいゲームオブジェクトを作成することを “spawning” と呼ぶことがあります。ネットワーク “HLAPI” 上では、“spawn” という言葉は、特有の限定された意味を持ちます。ネットワーク HLAPI のコンテンツ(権限)サーバーモデルでは、サーバー上にオブジェクトを “spawn(生成)” するということは、そのオブジェクトがサーバーに接続されたクライアント上に作成され、Spawning System(オブジェクト生成システム)によって管理されることを意味します。Spawning System に取り込まれると、オブジェクトがサーバー上で変化するたびにクライアントに状態のアップデートが送られ、オブジェクトがサーバー上で破壊されるとクライアント上でも破壊されます。また、Spawn されたオブジェクトは、サーバーが管理しているネットワーク オブジェクトのセットにも加えられるので、後で新しいクライアントがゲームに参加した場合には、そのクライアント上にもオブジェクトが Spawn されます。これらのオブジェクトは “netId” と呼ばれる固有のネットワーク インスタンス ID を持っています。これはオブジェクトごとに、サーバー上でもクライアント上でも同じ ID になります。これは、オブジェクトにメッセージを送ったり、オブジェクトを識別するために使用されます。
NetworkIdentity オブジェクトがクライアント上に Spawn(生成)されるときサーバー上におけるそのオブジェクトの現在の状態と同じ状態で生成されます。これにはオブジェクトの変形状態、動きの状態と同期された変数も含まれます。つまり、クライアントオブジェクトは作成されるときは常に最新の状態です。このおかげで、オブジェクトが間違って開始位置に生成されてしまい更新パケットが到着してから正しい位置に現れる、といった問題が回避されます。
これは非常に役立ちますが、それではオブジェクトはどのようにクライアント上に作成されるのでしょうか。そして、もしオブジェクトが生成されてから新しいクライアントが接続するまでの間にオブジェクトが変化した場合はどうなるのでしょうか。新しいクライアント上には、どちらのバージョンのオブジェクトが生成されるのでしょうか。
オブジェクトがクライアント上で Spawn するとき、サーバー上で NetworkServer.Spawn にパスされたオブジェクトのプレハブからクライアント オブジェクトがインスタンス化されます。NetworkIdentity インスペクターのプレビューパネルには NetworkIdentity のアセット ID が表示されますが、この値によってプレハブが識別されることで、クライアントはオブジェクトを作成することができます。このシステムが効率よく機能するために、クライアントによって実行されなければならない登録ステップがあります。クライアントは ClientScene.RegisterPrefab を呼び出し、クライアントオブジェクトが作成される元になるアセットの情報をシステムに伝える必要があります。
Spawn プレハブの登録には、エディターの NetworkManager を使用するのが最も便利です。NetworkManager の “Spawn Info” セクションで、一切コードを書かずにプレハブを登録できます。これは NetworkClient の作成時にコードによって行うこともできます。コードで行う方法は以下のとおりです。
using UnityEngine;
using UnityEngine.Networking;
public class MyNetworkManager : MonoBehaviour
{
public GameObject alienPrefab;
NetworkClient myClient;
// Create a client and connect to the server port
public void SetupClient()
{
ClientScene.RegisterPrefab(alienPrefab);
myClient = new NetworkClient();
myClient.RegisterHandler(MsgType.Connect, OnConnected);
myClient.Connect("127.0.0.1", 4444);
}
}
この参考例では、ユーザーはプレハブアセットを MyNetworkManager スクリプトの alienPrefab スロットにドラッグすることになります。これは、サーバーにエイリアン オブジェクトが Spawn(生成)されたときに各クライアントにも同様のオブジェクトが作成されるようにするためです。このようにアセットを登録することで確実にアセットがシーンと一緒にロードされるようになり、アセットが作成されたときにそのロードに時間が掛からなくなります。オブジェクトのプールや、アセットが動的に作成された場合などの高度な使用方法としては、ClientScene.RegisterSpawnHandler があります。これは、クライアント側での Spawn(生成)のためにコールバック関数を登録できるようにするものです。
以下の簡単な例では、ランダムな数の葉をつくる1本の木を Spawn(生成)させています。
class Tree : NetworkBehaviour
{
[SyncVar]
public int numLeaves;
}
class MySpawner : NetworkBehaviour
{
public GameObject treePrefab;
public void Spawn()
{
GameObject tree = (GameObject)Instantiate(treePrefab, transform.position, transform.rotation);
tree.GetComponent<Tree>().numLeaves = Random.Range(10,200);
NetworkServer.Spawn(tree);
}
}
この参考例を完成させるには、プロジェクトに、Tree スクリプトと NetworkIdentity コンポーネントを持った木のプレハブアセットを持たせます。そしてシーン内の MySpawner インスタンスで treePrefab スロットに木のプレハブアセットを追加します。また、木のプレハブは Spawn 可能なオブジェクトとして登録される必要があります。これは NetworkManager UI か ClientScene.RegisterPrefab() をコードで使用して行います。
このコードが実行されると、クライアントに作成された木のオブジェクトはサーバーから送られた正しい numLeaves の値を持つようになります。
Spawning(生成)の処理手順は以下のとおりです。
ネットワーク HLAPI のプレイヤー オブジェクトは特殊な面を持っています。NetworkManager によるプレイヤーオブジェクトの Spawning (生成)の流れは以下のとおりです。
ここで留意すべきは、OnStartLocalPlayer() は OnStartClient() の後に呼び出される事です。なぜなら、プレイヤーオブジェクトが Spawn (生成)された後に所有者の メッセージがサーバーから届いたときのみ OnStartLocalPlayer が呼び出されるからです。したがって、isLocalPlayer は OnStartClient() では設定されていません。
OnStartLocalPlayer は「あなたの」プレイヤーに対してのみ呼び出されるので、ローカルプレイヤーだけに対して行われるべき初期化はここで実行されるのが理に適っています。これには入力処理の有効化、プレイヤーオブジェクトのカメラのトラッキングの有効化を含めることもあります。通常は、アクティブなカメラはローカルプレイヤーのみが持ちます。
オブジェクトを生成し、そのオブジェクトに特定のクライアントの権限を割り当てることができます。NetworkServer.SpawnWithClientAuthority で行い、これには引数として権限を作成するクライアントの NetworkConnection が必要です。
これらのオブジェクトでは、権限を持つクライアント上では hasAuthority プロパティーが true となり、OnStartAuthority() が呼び出されます。またクライアントでは権限を持つオブジェクトのために Commands を発行することもできます。ほかのクライアント(とホスト)では、hasAuthority は false となります。
クライアント権限を持つオブジェクトの生成は、オブジェクトの NetworkIdentity に LocalPlayerAuthority を持つ必要があります。
例えば、プレイヤーの生成とオブジェクトの制御を可能にするコードは以下になります。
[Command]
void CmdSpawn()
{
var go = (GameObject)Instantiate(
otherPrefab,
transform.position + new Vector3(0,1,0),
Quaternion.identity);
NetworkServer.SpawnWithClientAuthority(go, connectionToClient);
}