はじめに
ネットワークゲームでは、ネットワーク上の各マシン上で共有されているゲームオブジェクトの位置が常にシンクロ(同期)していないと困りますが、それをきっちり作るのに意外と苦労しています、、、。最初はシンクロしているようでも、時間がたつと段々ズレてしまったりして。
そこで便利なのがAnySyncというUnity用アセットです(残念ながら有料。15ドル)
これは、ネットワーク上のマシン間でゲームオブジェクトのPosition, Rotationなどを共有するためのアセットです。UNET、Photonなど幅広い種類のライブラリと一緒に使うことができます。ただ、使用の際には少しコーディングが必要なので、その方法について解説します。なお、今回はLAN内P2P(peer to peer)に限定してお話するのでご注意ください。
準備
UnityのネットワークライブラリはUNET(2018.4LTSを最後に提供終了予定)が
最も身近ですが、Izmさんのこちらの記事を参考にした結果、UNETと近い感覚で使えて動作が安定している Mirror という無料アセットを使うことにしました。
Unityプロジェクトを作ったら、MirrorとAnySyncをダウンロード、インポートしてください。
シンクロさせるゲームオブジェクトの設定
今回は、2つのPCで一方にHost,もう一方にClientを実行し、Host側でスペースキーを押すとBallというプレハブがインスタンス化され、それをHost側で動かすとClient側でも動くようにします。
まず、メニューからGameObject/3D Object/Sphereを選んでゲームオブジェクトを作り、Ballという名前を付けてください。
次に、BallにNetwork Identityコンポーネントをアタッチします。Server Only,Local Player Authorityのチェックは不要です。更に、Sync Bufferもアタッチします。これには、シンクロさせたい位置情報などが保存されます。
それでは、シンクロに関する処理のためのスクリプトBallSync.csを準備しましょう。AnySyncのサンプルプロジェクトに付属のものを基にしています。準備できたらBallにアタッチしてください。
using UnityEngine;
using Mirror;
public class BallSync : NetworkBehaviour
{
private const float MinimumSendInterval = 0.05f;
private float _timeSinceLastSync;
private Vector3 _lastSentPosition;
private bool _idle;
private SyncBuffer BallSyncBuffer;
private void Awake()
{
BallSyncBuffer = GetComponent<SyncBuffer>();
}
private void Update()
{
if (hasAuthority)
{
_timeSinceLastSync += Time.deltaTime;
if (_timeSinceLastSync >= MinimumSendInterval)
{
if (_lastSentPosition != transform.position)
_idle = false;
if (!_idle)
{
CmdSync(_timeSinceLastSync, transform.position);
if (_lastSentPosition == transform.position)
_idle = true;
_lastSentPosition = transform.position;
_timeSinceLastSync = 0f;
}
}
}
else
{
if (BallSyncBuffer.HasKeyframes)
{
BallSyncBuffer.UpdatePlayback(Time.deltaTime);
transform.position = BallSyncBuffer.Position;
}
}
}
[Command]
private void CmdSync(float interpolationTime, Vector2 position)
{
BallSyncBuffer.AddKeyframe(interpolationTime, position);
RpcSync(interpolationTime, position);
}
[ClientRpc]
private void RpcSync(float interpolationTime, Vector2 position)
{
if (isLocalPlayer || isServer)
return;
BallSyncBuffer.AddKeyframe(interpolationTime, position);
}
}
この中で、[Command]アトリビュートのついているCmdSyncはホスト側で、[ClientRpc]アトリビュートのついているRpcSyncはクライアント側で実行されます。
Update関数内はhasAuthorityによって分岐しています。hasAuthorityがTrue、つまりAuthorityを持っている場合(今回はホスト側)はCmdSyncを実行して現在の位置情報をSync Bufferに送ります。更にその中でRpcSyncを実行して、クライアント側のSyncBufferにも位置情報を送ります。これでホスト、クライアント両方で同じ位置情報がバッファーに保存されます。hasAuthorityがFalseの場合(今回はクライアント側)はSync Bufferに保存されているデータを読み取ってクライアント側のBallの位置として代入しています。
これでBallの準備はできたので、Ballをプロジェクトウィンドウにドラッグして、プレハブ化してください。ヒエラルキー内のBallは消しましょう。
他のゲームオブジェクトの設定
次に、BallSpawnerという空のゲームオブジェクトを作ってください。Network Identityコンポーネントをアタッチして、Server Onlyにチェックをいれます。これにより、BallSpawnerはホスト側だけで動作するようになります。
次に、スクリプト BallSpawner.csを作ってBallSpawnerオブジェクトにアタッチしてください。
using UnityEngine;
using Mirror;
public class BallSpawner : NetworkBehaviour {
public GameObject Ball;
void Update () {
if (Input.GetKeyDown(KeyCode.Space))
{
Fire();
}
}
void Fire()
{
var go = Instantiate(Ball);
NetworkServer.Spawn(go);
}
}
インスペクタ上で変数BallにBallプレハブをアサインするのを忘れずに。
これで、スペースキーを押すとホスト側でBallがインスタンス化されて、さらに、NetworkServer.Spawn(go)によりクライアント側でもBallオブジェクトが生成されます。
最後に、NetworkManagerという空のゲームオブジェクトを作ってください。そちらに、Network Managerコンポーネント、Network Manager HUDコンポーネントをアタッチしてください。さらに、Network Managerコンポーネントの中のSpawn Info中のRegistered Spawnable PrefabsにBallプレハブをアサインしてください。
完成
これで完成です。スタンドアロンとしてビルドして、エディターと一緒に実行してみましょう。
この画面になったら、Editor側ではLAN Host, ビルドしたアプリ側ではLAN Clientをクリックしてください。次に、スペースバーを押せばBallが出てきて、シーンビューか何かでBallを移動させれば、Client(スタンドアロンアプリ)側でも同じように移動するはずです。