ノート: UNet は非推奨となり、今後 Unity から削除される予定です。新しいシステムが開発中です。詳細は ブログ と FAQ を参照してください。 |
Unity では、通常、Instantiate()
を使って新しいゲームオブジェクトを「スポーン」(spawn、生成) します。 ただし、Multiplayer 高レベル API では、「スポーン」という言葉は、より特殊な意味を持ちます。サーバーに権限のある HLAPI のモデルでは、ゲームオブジェクトをサーバーで「スポーン」するということは、つまり、ゲームオブジェクトがサーバーに接続するクライアント上で作成され、そのスポーンシステムで管理されるという意味です。
ゲームオブジェクトがこのシステムで生成されると、ゲームオブジェクトがサーバー上で変更されるたびに状態の更新がクライアントに送信されます。Unity がサーバー上のゲームオブジェクトを破棄すると、クライアント上のゲームオブジェクトも破棄されます。サーバーは生成されたゲームオブジェクトを他のネットワーク化されたゲームオブジェクトと一緒に管理します。そのため、もし他のクライアントが後からゲームに参加しても、サーバーはそのクライアント上にゲームオブジェクトを生成することができます。これらの生成されたゲームオブジェクトはそれぞれ、サーバーとクライアント上で、同じ netId という一意のネットワークインスタンス ID を持っています。この一意のネットワークインスタンス ID は、ネットワーク全体に設定されるメッセージをゲームオブジェクトにルーティングし、ゲームオブジェクトを識別するために使用されます。
サーバーが Network Identity コンポーネントを使ってゲームオブジェクトをスポーンする場合、クライアント上で生成されるゲームオブジェクトは同じ「状態」を持ちます。つまり、サーバー上のゲームオブジェクトとまったく同じものが作成されます。これらのゲームオブジェクトは同じ Transform、移動状態、[NetworkTransform](../ScriptReference/Networking.NetworkTransform.html と SyncVars が使用されている場合は、同期された変数を持ちます。したがって、Unity がそれらを作成する場合、クライアントのゲームオブジェクトは常に最新です。このことは、ゲームオブジェクトが誤った初期位置で生成され、その後、状態の更新が行われたときに正しい位置に再度出現する、というような問題を回避します。
Network Manager は登録したプレハブからのみゲームオブジェクトを生成し同期できます。そのため、ゲーム中にスポーンしたい場合は、Network Manager を使って特定のゲームオブジェクトのプレハブを登録します。Network Manager は Network Identity コンポーネントが設定されたゲームオブジェクトプレハブしか受け付けません。そのため、Network Manager に登録する前に、必ずプレハブに Network Identity コンポーネントを加える必要があります。
エディターの Network Manager にプレハブを加えるには、Network Manager ゲームオブジェクトを選択し、インスペクターで Network Manager コンポーネントへ移動します。 Spawn Info の横の三角をクリックし開きます。それから、Registered Spawnable Prefabs の下のプラス (+) ボタンをクリックします。プレハブをリストに加えるには、空のフィールドにドラッグアンドドロップします。
より精通しているユーザーは、プレハブの登録とゲームオブジェクトのスポーンを Network Manager コンポーネントを使わずに行いたい場合があるかもしれません。
Network Manager コンポーネントを使わずにゲームオブジェクトをスポーンするには、プレハブの登録をスクリプトを通して独自に処理します。ClientScene.RegisterPrefab メソッドを使用して、Network Manager にプレハブを登録します。
using UnityEngine;
using UnityEngine.Networking;
public class MyNetworkManager : MonoBehaviour
{
public GameObject treePrefab;
NetworkClient myClient;
// クライアントを作成してサーバーポートに接続します
public void ClientConnect() {
ClientScene.RegisterPrefab(treePrefab);
myClient = new NetworkClient();
myClient.RegisterHandler(MsgType.Connect, OnClientConnect);
myClient.Connect("127.0.0.1", 4444);
}
void OnClientConnect(NetworkMessage msg) {
Debug.Log("Connected to server: " + msg.conn);
}
}
この例では、ネットワーク マネージャーとして機能する空のゲームオブジェクトを作成し、次に MyNetworkManager
スクリプト (上記) を作成してそのゲームオブジェクトにアタッチします。Network Identity コンポーネントを設定したプレハブを作成し、インスペクターの MyNetworkManager コンポーネントの treePrefab フィールドにドラッグします。これにより、サーバーが木のゲームオブジェクトを生成するときに、クライアント上にも同じ種類のゲームオブジェクトが作成されます。
プレハブを登録すると必ず、アセットはシーンとともにロードされるので、アセットを作成するためのフリーズやロード時間がなくなります。
ただし、スクリプトが機能するには、サーバーのコードも追加する必要があります。以下を MyNetworkManager スクリプトに加えます。
public void ServerListen() {
NetworkServer.RegisterHandler(MsgType.Connect, OnServerConnect);
NetworkServer.RegisterHandler(MsgType.Ready, OnClientReady);
if (NetworkServer.Listen(4444))
Debug.Log("Server started listening on port 4444");
}
// クライアントが 2-3 の木をスポーンする準備が完了しているとき
void OnClientReady(NetworkMessage msg) {
Debug.Log("Client is ready to start: " + msg.conn);
NetworkServer.SetClientReady(msg.conn);
SpawnTrees();
}
void SpawnTrees() {
int x = 0;
for (int i = 0; i < 5; ++i) {
var treeGo = Instantiate(treePrefab, new Vector3(x++, 0, 0), Quaternion.identity);
NetworkServer.Spawn(treeGo);
}
}
void OnServerConnect(NetworkMessage msg) {
Debug.Log("New client connected: " + msg.conn);
}
サーバーにはゲームオブジェクトがスポーンで生成され、アセット ID がスポーンのメッセージで送信されているので、サーバーはなにも登録する必要はありません。クライアントはゲームオブジェクトをルックアップする必要があるので、ゲームオブジェクトはクライアントで登録される必要があります。
独自のネットワークマネージャーを作成する場合、サーバーでスポーンコマンドを呼び出す前に、クライアントが状態の更新を受信する準備が完了していることが重要です。そうでないと、スポーンのコマンドは送信されません。Unity のビルトインの Network Manager コンポーネントを使用する場合は、これは自動的に行われます。
オブジェクトプールや動的に作成したアセットなど、より高度な使用方法のために、ClientScene.RegisterSpawnHandler メソッドを利用できます。このメソッドを使うと、コールバック関数をクライアント側のスポーンのために登録することができます。この例を見るには、カスタムのスポーン関数 を参照してください。
ゲームオブジェクトが同期された変数などのネットワークの状態を持つ場合は、その状態はスポーンメッセージで同期されます。以下の例では、このスクリプトは木のプレハブにアタッチされます。
using UnityEngine;
using UnityEngine.Networking;
class Tree : NetworkBehaviour {
[SyncVar]
public int numLeaves;
public override void OnStartClient() {
Debug.Log("Tree spawned with leaf count " + numLeaves);
}
}
このスクリプトをアタッチして、numLeaves
変数を変えたり SpawnTrees
関数を変更したりして、それがクライアントに正確に反映されているかを確認できます。
void SpawnTrees() {
int x = 0;
for (int i = 0; i < 5; ++i) {
var treeGo = Instantiate(treePrefab, new Vector3(x++, 0, 0), Quaternion.identity);
var tree = treeGo.GetComponent<Tree>();
tree.numLeaves = Random.Range(10,200);
Debug.Log("Spawning leaf with leaf count " + tree.numLeaves);
NetworkServer.Spawn(treeGo);
}
}
Tree
スクリプトを以前に作成した treePrefab
スクリプトにアタッチし、実際に動かしてみます。
NetworkIdentity はスポーン可能なプレハブのルートゲームオブジェクトに置く必要があります。これがないと、Network Manager はプレハブを加えることができません。
NetworkBehaviour スクリプトは、子のゲームオブジェクトではなく、同じ NetworkIdentity のゲームオブジェクトに設定されなければなりません。
ゲームオブジェクトを生成するために発生する内部操作の実際のフローは以下の通りです。
スポーン可能な Network Identity を持つプレハブが登録されます。
サーバー上でプレハブからゲームオブジェクトがインスタンス化されます。
ゲームのコードによってインスタンスに初期値が設定されます (ここで適用される 3D 物理の力は、即座には有効になりません)。
NetworkServer.Spawn()
がインスタンスで呼び出されます。
サーバー上のインスタンスの SyncVars の状態は、 Network Behaviour コンポーネントの OnSerialize()
を呼び出すことによって収集されます。
SyncVar データを含む MsgType.ObjectSpawn
タイプのネットワークメッセージが接続したクライアントに送信されます。
OnStartServer()
がサーバー上のインスタンスで呼び出され、isServer
が true
に設定されます。
クライアントは ObjectSpawn
メッセージを受信し、登録されたプレハブから新しいインスタンス を作成します。
Network Behaviour コンポーネントの OnDeserialize() を呼び出すことによって、SyncVar データはクライアント上の新しいインスタンス に適用されます。
OnStartClient()
が各クライアント上のインスタンスで呼び出され、isClient
が true
に設定されます。
ゲーム中の SyncVar 値の変化は、自動的にクライアントと同期されます。これは、ゲームが終了するまで継続します。
NetworkServer.Destroy()
がサーバー上のインスタンスで呼び出されます。
MsgType.ObjectDestroy
タイプのネットワークメッセージがクライアントに送信されます。
NetworkServer.Destroy()
がサーバー上のインスタンスで呼び出され、インスタンスが破棄されます。
HLAPI のプレイヤーゲームオブジェクトは、ノンプレイヤーゲームオブジェクトと少し異なる挙動をします。Network Manager を使ったプレイヤーゲームオブジェクトのスポーンのフローは以下の通りです。
NetworkIdentity
を持つプレハブが PlayerPrefab
として登録されます。
クライアントがサーバーに接続します。
クライアントが AddPlayer()
を呼び出し、MsgType.AddPlayer
タイプのネットワークメッセージがサーバーに送信されます。
サーバーはメッセージを受信し NetworkManager.OnServerAddPlayer()
を呼び出します。
サーバー上でプレイヤープレハブからゲームオブジェクトがインスタンス化されます。
NetworkManager.AddPlayerForConnection()
がサーバー上の新しいプレイヤーインスタンスで呼び出されます。
プレイヤーインスタンスが生成されます。このプレイヤーインスタンスのために NetworkServer.Spawn()
を呼び出す必要はありません。通常のスポーンのように、スポーンメッセージがすべてのクライアントに送信されます。
MsgType.Owner
タイプのネットワークメッセージが、プレイヤーが加わったクライアントのみに送信されます。
サーバーに接続してメッセージを送ったクライアントがネットワークメッセージを受信します。
OnStartLocalPlayer()
がメッセージを受信したクライアント上のプレイヤーインスタンスで呼び出され、isLocalPlayer
が true
に設定されます。
OnStartLocalPlayer()
は OnStartClient()
の後に呼び出されることに注意してください。なぜなら、OnStartLocalPlayer() は、プレイヤーゲームオブジェクトの生成後、サーバーから所有権に関するメッセージが届いた場合にのみ発生するからです。そのため、isLocalPlayer
は OnStartClient()
内に設定されていないのです。
OnStartLocalPlayer
はクライアントのローカルプレイヤーゲームオブジェクトのためだけに呼び出されるため、ローカルプレイヤーだけに必要な初期化をするのに好都合です。OnStartLocalPlayer には入力処理やプレイヤーゲームオブジェクトのカメラトラッキングを可能にすることを加えることができます。
ゲームオブジェクトを生成してそのゲームオブジェクトの権限を特定のクライアントに割り当てるには、権限を付与されるべきクライアントの NetworkConnection
を引数として取るNetworkServer.SpawnWithClientAuthority を使用します。
ゲームオブジェクトに対して権限を持つクライアントのプロパティー hasAuthority
は true に設定され、そのクライアント上でOnStartAuthority()
が呼び出されます。そのクライアントはゲームオブジェクトのためのコマンドを発信できます。他のクライアント (及びホスト) では、hasAuthority
は false です。
クライアント権限によってスポーンされたオブジェクトは、その NetworkIdentity
に LocalPlayerAuthority
が設定されていなければなりません。
例えば、上の木のスポーンの例では、以下のように変更してクライアント権限を持たせることができます。 (ここでは、クライアントの接続を得るために NetworkConnection GameObject を渡す必要があります)。
void SpawnTrees(NetworkConnection conn) {
int x = 0;
for (int i = 0; i < 5; ++i)
{
var treeGo = Instantiate(treePrefab, new Vector3(x++, 0, 0), Quaternion.identity);
var tree = treeGo.GetComponent<Tree>();
tree.numLeaves = Random.Range(10,200);
Debug.Log("Spawning leaf with leaf count " + tree.numLeaves);
NetworkServer.SpawnWithClientAuthority(treeGo, conn);
}
}
Tree スクリプトはサーバーにコマンドを送信できるように変更されました。
public override void OnStartAuthority() {
CmdMessageFromTree("Tree with " + numLeaves + " reporting in");
}
[Command]
void CmdMessageFromTree(string msg) {
Debug.Log("Client sent a tree message: " + msg);
}
CmdMessageFromTree
の呼び出しを単に OnStartClient
に加えることはできません。なぜなら、その時点では権限はまだ設定されていないため、呼び出しが失敗するからです。