【Unity】オンラインシューティングゲームを作る：敵をつくる

#はじめに
どうもnisokaです。
前回からオンラインシューティングゲームを作っています。
[前の記事：【Unity】オンラインシューティングゲームを作る：弾の処理](https://qiita.com/nisoka/items/a10cddec1e108dc41e58)

ここではウェーブ式に敵が出現する実装を紹介したいと思います。

環境は以下の通りです
*win10 64bit, Unity2018.3.3f1, Monobit Unity Networking2.0 v2.6*

#今回のトピック
- 敵をつくる
- 体力の同期をとる
- ウェーブの仕組みをつくる

#敵をつくる
まずは基底クラスEnemy・派生クラスMonster・弾クラスBulletをつくります。
弾クラスについては前回の記事で扱ったオブジェクトプールと組み合わせると良いです。
実装は以下のような感じです。

```C#:基底クラスEnemy.cs
using MonobitEngine;
public abstract class Enemy : MonobitEngine.MonoBehaviour
{
	// 体力
    int health;
    
    private void Awake()
    {
        // 最大体力を設定
        health = GetMaxHealth();
    }
    
    // ダメージを受けたとき呼ばれる関数
    void TakeDamage(int damage)
    {
        // ダメージ計算
        health = Mathf.Max(0, health - damage);
    }

    protected virtual void Update()
    {
        // 画面外に行ったら消す
        if (transform.position.x < Stage.bottomLeft.x - 1.0f)
        {
            Die();
        }
		// 体力が０以下だったら
        if (health <= 0)
        {
            Die();
        }
    }

    void OnTriggerEnter2D(Collider2D other)
    {
        if (other.transform.tag == "Bullet")
        {
            Bullet bullet = other.gameObject.GetComponent<Bullet>();
            if (bullet.isFromPlayer)
            {
                // ダメージを受ける
                TakeDamage(bullet.GetDamage());
                // 弾の情報をリセットする
                bullet.Uninit();
            }
        }
    }
    
	// 死亡時に呼ばれる関数
	protected virtual void Die(){}
	
	// 体力を取得する関数
    public abstract int GetMaxHealth();
}
```
```C#:派生クラスMonster.cs
using MonobitEngine;
public class Monster : Enemy
{	
    // 移動スピード
    public float speed = 2f;

    protected override void Update()
    {
    	// 移動
        transform.Translate(Vector2.left * speed * Time.deltaTime);
        base.Update();
    }

	// 体力を取得する関数
    public override int GetMaxHealth()
    {
        return 100;
    }
}
```
```C#:弾クラスBullet.cs
public class Bullet : MonoBehaviour
{
    // ダメージ値
    int damage = 100;
    // スピード
    float speed;
    // 方向
    Vector2 dir;
    // プレイヤーから発射フラグ
    public bool isFromPlayer;

    // 更新処理
    void Update()
    {
        // 移動
        transform.Translate(dir * Time.deltaTime * speed);
        // 画面外に出たら
        if(transform.position.x > Stage.topRight.x)
        {
            // 情報をリセット
            Uninit();
        }
    }

    // 初期処理
    public void Init(Vector3 position, Vector2 dir, float speed, Color color, float scale, bool isFromPlayer)
    {
        this.transform.position = position;
        this.dir = dir;
        this.speed = speed;
        GetComponent<SpriteRenderer>().color = color;
        transform.localScale *= scale;
        this.isFromPlayer = isFromPlayer;
    }

    // 終了処理
    public void Uninit()
    {
        this.transform.position = Vector3.zero;
        this.dir = Vector2.zero;
        this.speed = 0.0f;
        GetComponent<SpriteRenderer>().color = Color.white;
        transform.localScale = Vector3.one;
        this.isFromPlayer = false;
        // 未使用状態に戻す
        this.gameObject.SetActive(false);
    }

    // ダメージ値取得関数
    public int GetDamage()
    {
        return damage;
    }
}
```
#体力の同期をとる
以下を追記・変更する

```C#:Enemy.cs
// 弾との当たり判定
void OnTriggerEnter2D(Collider2D other)
{
    if(MonobitNetwork.isHost)
    {
        if (other.transform.tag == "Bullet")
        {
            Bullet bullet = other.gameObject.GetComponent<Bullet>();

            if (bullet.isFromPlayer)
            {
                TakeDamage(bullet.GetDamage());

                // 弾の情報をリセットする
                bullet.Uninit();
            }
        }
    }
}
```
ホストが`MonobitNetwork.Instantiate()`をするので、生成したオブジェクトの所有権はホストが持ち、**ホストは生成から破棄までの責任を担います。**そのため弾と敵の当たり判定はホストが行います。

```C#:Enemy.cs
// ダメージを受けたとき呼ばれる関数
void TakeDamage(int damage)
{
    // ダメージ計算
    health = Mathf.Max(0, health - damage);
    // ゲストへ同期
    monobitView.RPC("SyncHealth", MonobitTargets.Others, health);
}

// ゲストのHPを同期するRPC
[MunRPC]
void SyncHealth(int health)
{
    this.health = health;
}
```
`TakeDamage()`でホストがダメージ計算を行った直後に、他クライアント(ゲスト)に変更後の体力を通知して同期します。


```C#:Monster.cs
// 基底クラスではRPCを使えないので派生クラスで宣言
protected override void Die()
{
    monobitView.RPC("RecvDestroy", MonobitTargets.Host);
}

// 削除通知を受け取るRPC
[MunRPC]
void RecvDestroy()
{
    if (MonobitNetwork.isHost)
    {
        MonobitNetwork.Destroy(this.gameObject);
    }
}
```
**モノビットエンジンの仕様上、MonobitEngine.Monobehaviourを継承した基底クラスでRPC関数は使用できません。**
その場合は仮想関数を派生クラスに定義してRPC関数を宣言します。

#ウェーブの仕組みをつくる
5秒おきに敵を生成するウェーブの仕組みを作ります。
敵の生成はすべてホストが行います。
ゲームシーンを管理するクラスなどに組み込みます。

```C#:ゲーム管理クラスGameScene.cs
using MonobitEngine;
public class GameScene : MonobitEngine.MonoBehaviour
{
    // ウェーブ生成間隔時間
    public float waveTimeOut = 5f;
    private float waveTimeTrigger;
    
    // ゲーム開始フラグ
    public bool isGameStart = false;

    // 初期化処理
    private void Start()
    {
        // オンライン接続初期化処理
    }

    private void Update()
    {
        // ホストだったら
        if(MonobitNetwork.isHost)
        {
            // 同一ルームのプレイヤー数が２人なら
            if(MonobitNetwork.room.playerCount >= 2)
            {
                // ゲーム開始フラグ "オン"
                isGameStart = true;
            }

            // ゲームが開始したら
            if (isGameStart)
            {
                // ウェーブ生成リピート開始
                StartMonsterGeneration();
            }
        }
    }
    
    // 一定間隔でウェーブ生成を呼ぶ関数
    void StartMonsterGeneration()
    {
        if(Time.time > waveTimeTrigger)
        {
            // ウェーブを生成する
            GenerateWave();
            waveTimeTrigger = Time.time + waveTimeOut;
        }
    }

    // 敵を発生させるウェーブを生成する関数
    void GenerateWave()
    {
    	// 敵を生成する
        MonobitNetwork.Instantiate("Monster", pos, Quaternion.identity, 0);
    }
}
```

#最後に
最近Qiitaに投稿していて少しずつですが「いいね！」をいただけるようになりました。自分の記事でもどこかの誰かの役に立っていると思うとなんだか嬉しくなりました。

#参考リンク
- [参考：Unity Tutorial: Everwing (Part 1 - Player and Bullet)](https://www.youtube.com/watch?v=nFHzbXJH7k0)