同期とは
プレイヤーが画面に登場するようになりましたが、移動をすると実行するプログラムの間での位置がずれてしまいます。これを解決しましょう。
NetworkViewコンポーネント
各キャラクターの位置や姿勢、アニメは、全ての参加者の画面上で同じように表示する必要があります。これを「同期させる」といいます。Unityには同期処理をサポートするNetworkViewというコンポーネントがあります。このコンポーネントのObservedプロパティでどのようなデータを同期させるかを指定することができます。
初期状態はTransformになっているので、場所や回転などの姿勢情報が同期します。RigidbodyやAnimationをObservedプロパティにドラッグ&ドロップすればそれらを同期します。この方法で同期できるのは、1つのNetworkViewにつき1種類のデータです。Transform、Rigidbody、Animationの全てを同期させようとした場合、3つのNetworkViewを追加する必要があります。また、それ以外のデータの同期ができません。
Observed用のスクリプトを作成して、それを設定することで上記の3つのデータをまとめて同期させたり、それ以外のデータを送受信することも可能になります。
姿勢の同期
まずは姿勢だけを同期させてみましょう。
- [Project]ビューから[Player]プレハブを選択します。
- [Inspector]ビューの[Add Component]を押して、[Miscellaneous]>[Network View]を選択します。
- [File]メニューから[Build & Run]を選択して、実行ファイルをリビルドします。
プログラムが起動したら、これまでの手順でサーバーとクライアントを起動して、動かしてみましょう。キャラクターのうちの片方が、2つのウィンドウ間で同期して動きます。
しかし、現状では以下の問題があります。
- もう一方のプレイヤーが同期していない
- 動きがカクカク
これらを解決していきましょう。
自分のプレイヤーのみを操作できるようにする
マルチプレイヤーのゲームでは、プレイヤー本人が操作するキャラクターと、ネットワーク先のプレイヤーが操作するキャラクターの動かし方が違います。自分のキャラクターは自分の指示に従って動き、他のプレイヤーのキャラクターはネットワークから送信されてくるデータに従って動かします。
自分が生成したキャラクターかどうかは、NetworkViewのインスタンスが持つisMineプロパティがtrueかどうかで判断できます。isMineがtrueの時はキー操作を反映させて、falseのときは他のPCから送られてくるデータに従って移動させるようにすればよいのです。
自分だけ操作できるようにしましょう。
private NetworkView netView = null;
- Start()関数内に以下のコードを追加します。
netView = GetComponent<NetworkView>();
- Update()関数の最初に以下のコードを追加します。
if (!netView.isMine) { return; }
- 上書き保存をして、Unityに切り替えます。
- [File]>[Build & Run]で実行ファイルをリビルドして起動後、2つのウィンドウで動作を確認してみましょう。
操作すると、先ほどまで両方のキャラクターが動いていたのが、有効なウィンドウのキャラクターのみが動くようになったはずです。
他のプレイヤーにぶつかると、ぶつかった相手のプレイヤーが動きはじめて動作が不安定になります。これはRigidbodyの同期ができていないからです。動きを滑らかにする過程で修正します。
動きを滑らかにする
動きがカクカクするのは、初期状態ではNetworkViewの同期が1秒間に15回しか実行されないからです。頻繁に姿勢データを通信すると、ネットワーク上にデータがあふれてパンクする可能性があるため、回数が少なめになっています。
これを解決する方法は以下の2通りが考えられます。
- 通信回数を増やす
- 動きの補間をして滑らかに動かす
最初の方法は、Network.sendRateというプロパティーの値を書き換えることで対応できるので簡単に試せます。[Edit]メニュー>[Project Settings]>[Network]>[Sendrate]の値を60などに変更して動かしてみてください。
実際に試してみるとあまり綺麗に動いてくれません。格闘ゲームなどのタイミングがシビアなゲームでは、通信回数を増やして重要なデータを素早くやり取りする必要がありますが、今回は多少同期が遅れてもそれほど影響はなさそうです。滑らかに動かす方を重視するので、効果の薄い前者ではなく、後者の方法で対応することにします。
通信する内容ややりとりをカスタマイズするには、NetworkViewコンポーネントのObservedプロパティを利用します。最初はTransformが指定されていて、姿勢情報が直接送受信されています。ここをオリジナルのプログラムに変更することで、お互いに同期させるデータを自由に設定することができます。詳しくはUnity - マニュアル: ネットワークビューを参照してください。
参考図書のp339のList 18-7を参考に、キャラクタの同期用のクラスを作成しました。
- Sendrateを60にしていたら、15に戻します。
- Unityの[Project]ビューの[Create]から[C# Script]を選択します。
- 作成したスクリプトの名前を「CharaSynchronizer」に変更します。
- 作成したスクリプトを、[Project]ビューの[Player]プレハブにドラッグ&ドロップします。
- [Project]ビューの[Player]プレハブを選択して、[Inspector]ビューの[Chara Synchronizer(Script)]をドラッグして、[Observed]欄にドロップします。
- 作成したスクリプトをダブルクリックしてエディターで開きます。
- 以下のようにコードを入力します。
using UnityEngine; using System.Collections; public class CharaSynchronizer : MonoBehaviour { // 受信した位置 Vector3 position; // 受信した回転 Quaternion rotation; // Rigidbodyのインスタンス Rigidbody myRigidbody; // NetworkViewのインスタンス NetworkView netView; // Use this for initialization void Start () { myRigidbody = GetComponent<Rigidbody>(); netView = GetComponent<NetworkView>(); position = transform.position; rotation = transform.rotation; } // Update is called once per frame void FixedUpdate () { // 自分の制御キャラクター以外のとき、positionとrotationを反映させる if (!netView.isMine) { // データは1/Network.sendRate間隔で送信されてくる。このうちの経過時間分が内分する値 float t = Time.fixedDeltaTime * Network.sendRate; // 移動先から速度を逆算 Vector3 move = (Vector3.Lerp(transform.position, position, t)-transform.position)/Time.fixedDeltaTime; // 速度を設定 myRigidbody.velocity = move; // 回転 transform.rotation = Quaternion.Slerp(transform.rotation, rotation, t); } } void OnSerializeNetworkView(BitStream stream, NetworkMessageInfo info) { if (stream.isWriting) { Vector3 pos = transform.position; Quaternion rot = transform.rotation; // 送信 stream.Serialize(ref pos); stream.Serialize(ref rot); } else { // 受信 stream.Serialize(ref position); stream.Serialize(ref rotation); } } }
- 参考図書との違いは以下の通りです。
- 移動を座標の代入から、Rigidbodyへの速度設定に修正
- ゲームフラグは削除
- 補間処理の係数を、5の固定値からNetwork.sendRateに変更
以上ができたら、上書き保存をして、[File]>[Build & Run]でプログラムをリビルドして試してみてください。動きの補間とRigidbodyの同期を行ったので、2つの問題が同時に解消されました。
ここでやったこと
同期データの通信
NetworkViewコンポーネントが付加されていると、データの送受信時にOnSerializeNetworkView()関数がUnityから呼ばれます。引数には、データを送受信するためのBitStreamとネットワークの情報が渡されます。データの送受信ではBitStreamを利用します。
NetworkViewは、送信時と受信時の2通りのタイミングで呼ばれます。BitStreamのisWritingフラグがtrueの時は送信なので、他のプレイヤーに自分の情報を送信するプログラムを書きます。isWritingフラグがfalse(isReadingがtrueでも可)の時は受信なので、他のプレイヤーから届いた情報を受け取るプログラムを書きます。
送受信とも、BitStreamのSerialize()関数で行います。送信時はSerializeに参照渡ししたオブジェクトのデータを送信し、受信時はSerializeに参照渡ししたオブジェクトにデータを受け取ります。Serialize()関数で送受信できる変数は以下の通りです。
- bool
- char
- short
- int
- float
- Quaternion
- Vector3
- NetworkPlayer
- NetworkViewID
(Unity Script Reference – BitStream.Serializeより)
charは1byteだそうです。日本語などのマルチバイト文字はそのままでは送れません。文字列は対応していません。文字列は常時送受信する必要はないので、後述のRPCを利用しましょう。
滑らか移動
今回の設定では、データを受信するのは1秒間に15回程度です。そのまま移動させた場合はカクカクな動きになります。そこで、受信した座標にそのまま移動させず、FixedUpdate()関数内で受信した座標を目的地として、現在座標から少しずつ近づけることで滑らかに移動するように見せています。Vector3のLerp関数と、QuaternionのSlerp関数は、内分点を計算する関数です。1/15秒で次のデータが届くので、1回の動きを(経過時間/(1/15))だけ動かすことで、それらしく動かしています。
この方法は、他のプレイヤーは1/15秒前の位置を目指して動きますので、2つの画面を見比べると通信先のキャラクターがワンテンポ遅れて動きます。また、内分点の移動は、受信直後は移動量が大きく、目的地に近づくにつれて移動量が減る性質があるので、少し動きがガタガタします。通信回数を増やすとこれらの症状は軽減します。Network.sendRateを30にするとかなり綺麗に動きます。送受信するデータ量やネットワークの状況に合わせて、送信回数を調整するとよいでしょう。
動きの遅れと確実性
他のプレイヤーは1/15秒程度、動きが遅れますが、これはどうにかできないのでしょうか。
データが全プレイヤーに行き渡ったことを確認してから画面の更新を行えば、全てのプレイヤーの画面を完全に一致させることは可能です。しかし、ネットワークはデータが届くのに時間がかかったり、途中で失われる可能性があります。完全に同期させるには時間がかかるため、更新回数が減ってしまい、滑らかな動きが犠牲になります。
ゲームの中には、多少ズレてても大勢に影響しないデータと、全員が確実に共有しなくてはならない情報があります。ネットワークゲームをデザインする時にはこのことを理解して割り切ることが必要です。
例えばキャラクターの位置は、多少違っていても滑らかに動作することが優先される場合が多いです。プレイヤーにとって、自分のキャラクターの動きさえ正しければ、他のキャラクターの動きが不自然であってもそれほど気になりません。このような場合は、ある程度正確性は犠牲にして、滑らかに動くように考えてよいでしょう。
一方で、アイテムを取得したとか、ダメージを受けたような情報は、全プレイヤー間で確実に同期させる必要があるでしょう。このようなやり取りは、判断する担当者を決め(基本的にはゲームサーバー)、後述するRPCを利用して状態を共有するのが便利です。