Posted at

Unityでオンラインマルチプレイなゲームを作りたい その10 敵の同期

前回の記事でプレイヤーが発射する弾の処理の作成を行いました。

今回は敵を作っていこうかと思います。


今回の目標

敵を実装します。


敵の管理/所有権

各ルームメンバー各々でMonobitNetwork.Instantiateを行なったり同期のRPCを飛ばしたりしているような実装だと、バグが出やすく、頭も混乱します。

そのため、敵の管理/所有権をルームメンバーの誰か一人に担ってもらう形で実装するのがベストです。


取りあえずホストに責任を押し付けてしまう

MUN技術情報ページ/ホストチェック

ルーム内で共有されるもの(共通のスコア/敵/マップ内に散らばってるアイテムなどなど)はホスト側で処理を行い、処理の結果を他のプレイヤーに共有する方法がベターなようです。

ホストチェック/MonobitEngine.MonobitNetwork.isHost プロパティ

MonobitEngine.MonobitNetwork.isHost

これで自身がホストかどうかを確認し、自身がホストだったときに処理を行うようにすることが出来ます。

では、これを踏まえて敵を実装していきます。


Enemy.cs

/// <summary>敵の状態</summary>

public enum State
{
Active = 0,
Deactive = 1,
Unrivaled = 2,
Dead = 3
}

/// <summary>敵</summary>
public class Enemy : MonobitEngine.MonoBehaviour
{
/// <summary>敵の位置などを操作するリジッドボディ</summary>
private Rigidbody2D m_Rigidbody;

/// <summary>移動量</summary>
private Vector2 m_MovementAmount;

/// <summary>移動速度</summary>
private float m_Velocity;

/// <summary>向き</summary>
private float m_Angle;

/// <summary>体力最大値</summary>
private int m_MaxHealthPoint = 10;

/// <summary>体力</summary>
private int m_HealthPoint = 0;

/// <summary>状態</summary>
private State m_State;

/// <summary>弾の発射間隔</summary>
private float m_ShotInterval = 1.0f;

/// <summary>弾の発射間隔カウント</summary>
private float m_ShotIntervalCount = 0.0f;

/// <summary>弾の方向数</summary>
private int m_ShotWay = 1;

/// <summary>弾の方向間隔</summary>
private float m_ShotAngleRange = 0.0f;

/// <summary>撃破時に与えるスコア</summary>
private int m_Score;

// Start is called before the first frame update
private void Start()
{
m_Rigidbody = GetComponent<Rigidbody2D>();

m_HealthPoint = m_MaxHealthPoint;

m_State = State.Deactive;
}

// Update is called once per frame
private void Update()
{
if (!MonobitNetwork.isHost){ return; }

switch(m_State)
{
case State.Active:
UpdateTarget();
Move();
Shot();

break;

case State.Deactive:
break;

default:
break;
}
}

/// <summary>移動</summary>
private void Move()
{
Vector3 playerPos = GameObject.FindWithTag("Player").transform.localPosition;

float x = (playerPos .x - m_Rigidbody.position.x) - Vector2.zero.x;
float y = (playerPos .y - m_Rigidbody.position.y) - Vector2.zero.y;

m_Angle = Mathf.Atan2(y, x) * Mathf.Rad2Deg;

m_MovementAmount.x = Mathf.Cos(angle);
m_MovementAmount.y = Mathf.Sin(angle);

m_Rigidbody.position += m_MovementAmount * m_Velocity * Time.deltaTim;
}

/// <summary>弾の発射</summary>
private void Shot()
{
m_ShotIntervalCount += Time.deltaTime;

if (m_ShotIntervalCount < m_ShotInterval) { return; }

m_ShotIntervalCount = 0.0f;

float baseAngle = m_Angle - (m_ShotAngleRange * 0.5f);

float angleAmount = (m_ShotWay <= 1) ? m_ShotAngleRange : m_ShotAngleRange / (m_ShotWay - 1);

for (int i = 0; i < m_ShotWay; ++i)
{
monobitView.RPC("RecvShot", MonobitTargets.All, transform.localPosition, baseAngle + (angleAmount * i));
}
}

/// <summary>弾を発射するRPC</summary>
/// <param name="pos">弾の発射位置</param>
/// <param name="angle">弾の発射方向</param>
[MunRPC]
private void RecvShot(Vector3 pos, float angle)
{
Debug.Log("Shot Enemy Bullet!");
}

/// <summary></summary>
/// <param name="stream">MonobitAnimatorViewの送信データ、または受信データのいずれかを提供するパラメータ</param>
/// <param name="info">特定のメッセージやRPCの送受信、または更新に関する「送信者、対象オブジェクト、タイムスタンプ」などの情報を保有するパラメータ</param>
public void OnMonobitSerializeView(MonobitEngine.MonobitStream stream, MonobitEngine.MonobitMessageInfo info)
{
if (stream.isWriting)
{
stream.Enqueue(m_HealthPoint);
stream.Enqueue(m_MaxHealthPoint);

stream.Enqueue((int)m_State);
}
else
{
m_HealthPoint = (int)stream.Dequeue();
m_MaxHealthPoint = (int)stream.Dequeue();

m_State = (State)stream.Dequeue();
}
}
}


Move()Shot()の中身はプレイヤーの位置を取得し追尾 + 弾の発射を行うようにしています。他は前々回のプレイヤーとあんまり変わらないですね。

注目ポイントはUpdate()内の頭でやってるif文です。

if (!MonobitNetwork.isHost){ return; }でホストだけに処理を行わせ、他のプレイヤーに出ている敵に関してはmonobitView.RPCOnMonobitSerializeViewで同期させるようにします。

後はこれをホストのみが生成するようにしてあげます。


EnemyGeneator.cs

/// <summary>エネミーを生成するためのもの</summary>

public class EnemyGeneator: MonobitEngine.MonoBehaviour
{
// Start is called before the first frame update
private void Start()
{
}

// Update is called once per frame
private void Update()
{
if (Input.GetKeyDown(KeyCode.Space)) { Generate(); }
}

/// <summary>エネミーを生成する</summary>
public void Generate()
{
if (!MonobitNetwork.isHost) { return; }

float x = UnityEngine.Randome.Range(0.0f, 96.0f);
float y = UnityEngine.Randome.Range(0.0f, 96.0f);

MonobitNetwork.Instantiate("Enemy", new Vector3(x, y, 0.0f), Quaternion.identity, 0);
}
}


Enemy.csと同じくMonobitNetwork.isHostGenerate()の最初に置き、ホストのみが操作できるようにしています。

ホストがスペースキーを押すことでネットワーク越しに敵を生成するようにしていますが、Generateの引数に出現位置を設け、出現の条件に見合ったときにこのGenerate()を呼んであげるだけでそれなりに扱えます。


敵の削除

Unityでオンラインマルチプレイなゲームを作りたい その8 プレイヤーの同期/5.プレイヤーオブジェクトの削除

で少し触れてたのですが、MonobitNetwork.Destroy()を使ってオブジェクトの削除を行います。

ホストが作成したオブジェクトなので、ホストだけで削除を行うようにする必要があります。

ということで、ホストでエネミーを削除できるようにします。


EnemyGeneator.cs

/// <summary>エネミーを生成するためのもの</summary>

public class EnemyGeneator: MonobitEngine.MonoBehaviour
{
/// <summary>エネミーを管理するためのもの</summary>
private List<GameObjet> m_Enemies = new List<GameObjet>();

// Start is called before the first frame update
private void Start()
{
}

// Update is called once per frame
private void Update()
{
if (Input.GetKeyDown(KeyCode.Space)) { Generate(); }
}

/// <summary>エネミーを生成する</summary>
public void Generate()
{
if (!MonobitNetwork.isHost) { return; }

float x = UnityEngine.Randome.Range(0.0f, 96.0f);
float y = UnityEngine.Randome.Range(0.0f, 96.0f);
GameObject enemyObj = MonobitNetwork.Instantiate("Enemy", new Vector3(x, y, 0.0f), Quaternion.identity, 0);

m_Enemies.Add(enemyObj);
}

/// <summary>エネミーを削除する</summary>
/// <param name="enemyObj">削除したいオブジェクト</param>
public void Destroy(GameObject enemyObj)
{
if (!MonobitNetwork.isHost || !m_Enemies.Contains(enemyObj)) { return; }

MonobitNetwork.Destroy(enemyObj);

m_Enemies.Remove(enemyObj);
}
}


Destroy()に削除したいエネミーのオブジェクトを引数に入れてあげればネットワーク越しに削除されます。

Destroy()の頭でホスト以外は操作させないようにもしています。

削除するオブジェクトを指定する必要があるので、エネミーのオブジェクトを管理するためにListを追加しています。


ホストマイグレーション

この実装のままだとホストが落ちた場合、現状だとホストが生成したオブジェクトは全て削除されてしまいます。

ホストが意図せず居なくなってしまってもゲームを続けられるようにオブジェクトを新しいホストで処理を行えるようにしないといけません。

API リファレンス(MUN クライアント)/Instantiate

ホストが消えても何とかできそうな機能がありました。

// prefabName:生成したいプレハブ名

// position:生成する位置
// rotation:生成した際の姿勢
// group:グループ(とりあえず0でいい)
// isDontDestroyOnLoad:シーンの変更時に削除を行わないようにする(シーン変更後もオブジェクトが必要であればtrueにしてくdさい)
// isMine:オブジェクトの所有権に自分を指定する(isMineで判定を行う場合はtrueにするんですかね...?試してないのでちょっと分からないです)
// isDontDestroyOnRoom:所有者がルーム退室を行っても削除されないようにする(ここがキモ、これをtrueにすることで所有者が消えてもオブジェクトは残ります)
MonobitNetwork.Instantiate("Prefab name", Vector3.zero, Quaternion.identity, 0, null, false, false, true);

最後の引数でオブジェクトの所有者が居なくなっても削除されないようにすることができます。

ということで、これを敵を生成するソースに取り入れます。


EnemyGeneator.cs

/// <summary>エネミーを生成するためのもの</summary>

public class EnemyGeneator: MonobitEngine.MonoBehaviour
{
/// <summary>エネミーを管理するためのもの</summary>
private List<GameObjet> m_Enemies = new List<GameObjet>();

// Start is called before the first frame update
private void Start()
{
}

// Update is called once per frame
private void Update()
{
if (Input.GetKeyDown(KeyCode.Space)) { Generate(); }
}

/// <summary>エネミーを生成する</summary>
public void Generate()
{
if (!MonobitNetwork.isHost) { return; }

float x = UnityEngine.Randome.Range(0.0f, 96.0f);
float y = UnityEngine.Randome.Range(0.0f, 96.0f);
GameObject enemyObj = MonobitNetwork.Instantiate("Enemy", new Vector3(x, y, 0.0f), Quaternion.identity, 0, null, false, false, true);

m_Enemies.Add(enemyObj);
}

/// <summary>エネミーを削除する</summary>
/// <param name="enemyObj">削除したいオブジェクト</param>
public void Destroy(GameObject enemyObj)
{
if (!MonobitNetwork.isHost || !m_Enemies.Contains(enemyObj)) { return; }

MonobitNetwork.Destroy(enemyObj);

m_Enemies.Remove(enemyObj);
}
}


Instantiateを変えただけです。

オブジェクトの所有者の設定はされていませんが、ホストのみがオブジェクトの処理を行うように前述で実装しているので、特に他に行うことはありません。

ホストが落ちた際に、自動的に同ルーム内のプレイヤーが新しくホストになるため、ホストの切り替える際の処理も意識しなくて良さそうです。

これで、敵の実装が行えました。

次回は敵の弾について書いていこうと思うんですが、弾の実装自体はほぼプレイヤーの弾と変わらないため、少しステップアップした実装法を記載しようかなと思います。


参考

http://www.monobitengine.com/doc/mun/contents/FeatureClient/IsHost.htm

http://www.monobitengine.com/doc/mun/contents/FeatureClient/IsHost.htm#MonobitEngine.MonobitNetwork.isHost%20%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3

https://qiita.com/koyuki2007/items/e7d16e020539f38c6e7c#5%E3%83%97%E3%83%AC%E3%82%A4%E3%83%A4%E3%83%BC%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%AE%E5%89%8A%E9%99%A4

http://www.monobitengine.com/doc/doxygen/client/html/class_monobit_engine_base_1_1_monobit_network.html#a84633670a91ec31d3e91f1eca0942566