ヴァンサバ風ゲームの作り方#4|一番近い敵を狙って自動で攻撃しよう

スライムちゃんの周りに集まったコゲクッキーへ、計量スプーンが自動で飛んでいく様子

前回で敵がわいてくるようになりましたが、こちらは逃げることしか出来ませんでした。いよいよ反撃です!ヴァンサバ風のゲームでは、プレイヤーは移動に集中して、攻撃は武器が勝手にやってくれるのがルール。今回はヴァンサバ風サバイバーゲームのシリーズの4回目として、一番近い敵を自動で見つけて、計量スプーンを投げる仕組みを作ってみたいと思います!

目次

今回作るもの

  • 1秒に1本、計量スプーンが飛んでいく
  • 飛ぶ先は一番近い敵。敵がいないときは投げない
  • 当たった敵はダメージを受けて、HPが0になったら倒れる
  • スプーンも敵と同じようにObjectPoolで使い回す

作る・直すスクリプトは3つです。

スクリプト役割付ける場所
Spoon(新しく作る)まっすぐ飛んで、当たった敵にダメージを与えるスプーンのプレハブ
SpoonThrower(新しく作る)一番近い敵を探して、一定間隔で投げるPlayer
Enemy(直す)HPを持って、ダメージを受ける敵のプレハブ

前回の続きから

  • Unityのバージョンは6.3 LTS
    • 記事作成時点では6000.3.22f1で確認しています
  • #3の状態:画面の外から敵が出てきて追いかけてくる。敵はEnemyレイヤーに入っている

当たり判定のイベント(OnTriggerEnter2Dなど)は6-10 当たり判定のイベントとタグで紹介しています。

計量スプーンのプレハブを作ろう

  • ProjectウインドのSurvivorsLike/Spritesから、spoonをHierarchyにドラッグ&ドロップ
  • 名前を「Spoon」にリネーム
  • Add Component>Rigidbody2D
    • Gravity Scale:0
    • Constraints>Freeze Rotation:Z にチェック
  • Add Component>Circle Collider 2D
    • Is Trigger:チェックを入れる
    • Radius:0.15
Spoonのプレハブ。Sprite Renderer(spoon)、Rigidbody 2D(Gravity Scale 0)、Circle Collider 2D(Is Trigger、Radius 0.15)

スクリプトを書いてから、プレハブにします(後でまとめてやります)。

敵にHPを持たせよう

まずは殴られる側から。#3で作ったEnemyに、HPとダメージの処理を足します。

using UnityEngine;
using UnityEngine.Pool;

// こげたお菓子の敵。スライムちゃんを追いかけ、遠くに離れたらプールに戻る
[RequireComponent(typeof(Rigidbody2D))]
public class Enemy : MonoBehaviour
{
    [SerializeField] private float moveSpeed = 1.5f;      // 1秒に進むマス数
    [SerializeField] private float despawnDistance = 22f; // これより離れたら引っ込む
    [SerializeField] private float keepDistance = 0.5f;   // これ以上は近づかない
    [SerializeField] private int maxHp = 10;              // 体力

    private Rigidbody2D body;
    private Transform target;
    private IObjectPool<Enemy> pool;
    private int hp;

    private void Awake()
    {
        body = GetComponent<Rigidbody2D>();
    }

    // 出てくるときに、EnemySpawnerから呼んでもらう
    public void Spawn(Vector2 position, Transform newTarget, IObjectPool<Enemy> newPool)
    {
        transform.position = position;
        target = newTarget;
        pool = newPool;
        hp = maxHp; // 使い回すので、体力を戻しておく
    }

    // 攻撃が当たったときに、武器のほうから呼んでもらう
    public void TakeDamage(int damage)
    {
        if (hp <= 0) // もう倒れている(同じ瞬間に2発当たったとき用)
        {
            return;
        }

        hp -= damage;
        if (hp <= 0)
        {
            pool.Release(this);
        }
    }

    private void FixedUpdate()
    {
        if (target == null)
        {
            return;
        }

        Vector2 toTarget = (Vector2)target.position - body.position;
        float distance = toTarget.magnitude;

        // 真上に乗ってしまわないよう、一定の距離まで近づいたら止まる
        body.linearVelocity = distance > keepDistance ? toTarget.normalized * moveSpeed : Vector2.zero;

        if (distance > despawnDistance)
        {
            pool.Release(this);
        }
    }
}

敵を直すポイントは3つです。

Enemyのポイント
  • keepDistance(重ならないように止まる)
    • #3では敵どうしの当たり判定を切ったので、そのままだと全員がプレイヤーの真上にぴったり重なります
    • 重なると「一番近い敵はどっちの方向か」が計算出来なくなり、武器が飛ばなくなります
    • 少し手前で止めておくと、プレイヤーを囲むように集まってくれます
  • hp = maxHp;をSpawnに入れる
    • 使い回すオブジェクトは、出すときに必ず状態を戻す。これを忘れると、2回目に出てきた敵が体力0のままになります
  • if (hp <= 0) return;
    • 同じ瞬間に2発当たると、pool.Releaseが2回呼ばれてしまいます
    • ObjectPoolは同じものを2回戻すとエラーになるので、倒れた後は何もしないようにしています

計量スプーンを作ろう(Spoon)

  • SurvivorsLike/Scriptsに「Spoon」を作る
using UnityEngine;
using UnityEngine.Pool;

// 投げた計量スプーン。まっすぐ飛んで、当たった敵にダメージを与える
[RequireComponent(typeof(Rigidbody2D))]
public class Spoon : MonoBehaviour
{
    [SerializeField] private float speed = 8f;  // 1秒に進むマス数
    [SerializeField] private int damage = 10;   // 当たったときに減らす体力
    [SerializeField] private float life = 3f;   // 何秒で消えるか

    private Rigidbody2D body;
    private IObjectPool<Spoon> pool;
    private float timer;
    private bool used; // 1回使ったら、それ以上は当たらない

    private void Awake()
    {
        body = GetComponent<Rigidbody2D>();
    }

    // 投げるときに、SpoonThrowerから呼んでもらう
    public void Throw(Vector2 position, Vector2 direction, IObjectPool<Spoon> newPool)
    {
        transform.position = position;
        transform.right = direction; // 飛ぶ向きに回転させる
        body.linearVelocity = direction * speed;
        pool = newPool;
        timer = 0f;
        used = false;
    }

    private void Update()
    {
        if (used)
        {
            return;
        }

        timer += Time.deltaTime;
        if (timer >= life)
        {
            used = true;
            pool.Release(this); // 当たらないまま飛んでいったら引っ込める
        }
    }

    private void OnTriggerEnter2D(Collider2D other)
    {
        if (used)
        {
            return;
        }
        if (!other.TryGetComponent(out Enemy enemy))
        {
            return;
        }

        used = true;
        enemy.TakeDamage(damage);
        pool.Release(this);
    }
}
Spoonのポイント
  • transform.right = direction;
    • スプーンの絵は右を向いているので、transform.rightに進む向きを入れると、絵がその向きに回ります
  • TryGetComponent(out Enemy enemy)
    • 当たった相手が敵かどうかを調べています。壁やプレイヤーに当たっても何も起きません
    • タグで判定する方法もありますが、どうせEnemyを取り出すので、取り出せたかどうかで判定するほうが短く書けます
  • usedのフラグ
    • 敵と敵の間をすり抜けるときに2体同時に当たったり、当たった瞬間に寿命が来たりすると、二重にプールへ戻してしまいます
  • life(3秒で消える)
    • 敵がいない方向に飛んでいったスプーンを、いつまでも飛ばし続けないための保険です

一番近い敵を探して投げよう(SpoonThrower)

  • SurvivorsLike/Scriptsに「SpoonThrower」を作る
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Pool;

// 一定間隔で、一番近い敵に計量スプーンを投げる
public class SpoonThrower : MonoBehaviour
{
    [SerializeField] private Spoon spoonPrefab;
    [SerializeField] private float interval = 1f;      // 何秒に1本投げるか
    [SerializeField] private float searchRadius = 10f; // 何マス以内の敵を狙うか
    [SerializeField] private LayerMask enemyLayer;

    private ObjectPool<Spoon> pool;
    private ContactFilter2D filter;
    private readonly List<Collider2D> found = new List<Collider2D>();
    private float timer;

    private void Awake()
    {
        filter = new ContactFilter2D
        {
            useLayerMask = true,
            layerMask = enemyLayer,
            useTriggers = true, // 敵の当たり判定はIs Triggerなので、これが必要
        };

        pool = new ObjectPool<Spoon>(
            createFunc: () => Instantiate(spoonPrefab, transform.parent),
            actionOnGet: spoon => spoon.gameObject.SetActive(true),
            actionOnRelease: spoon => spoon.gameObject.SetActive(false),
            actionOnDestroy: spoon => { if (spoon != null) Destroy(spoon.gameObject); },
            defaultCapacity: 20,
            maxSize: 200);
    }

    private void Update()
    {
        timer += Time.deltaTime;
        if (timer < interval)
        {
            return;
        }
        timer -= interval;

        Transform nearest = FindNearestEnemy();
        if (nearest == null) // 近くに敵がいないときは投げない
        {
            return;
        }

        Vector2 toEnemy = (Vector2)nearest.position - (Vector2)transform.position;
        if (toEnemy.sqrMagnitude < 0.0001f) // 真上にいるときは向きが決められない
        {
            return;
        }

        Vector2 direction = toEnemy.normalized;
        Spoon spoon = pool.Get();
        spoon.Throw(transform.position, direction, pool);
    }

    // まわりの敵を集めて、一番近いものを返す
    private Transform FindNearestEnemy()
    {
        Physics2D.OverlapCircle(transform.position, searchRadius, filter, found);

        Transform nearest = null;
        float nearestDistance = float.MaxValue;
        foreach (Collider2D collider in found)
        {
            float distance = ((Vector2)collider.transform.position - (Vector2)transform.position).sqrMagnitude;
            if (distance < nearestDistance)
            {
                nearestDistance = distance;
                nearest = collider.transform;
            }
        }
        return nearest;
    }
}

useTriggers = trueを忘れると、敵が1体も見つかりません

#3で敵の当たり判定をIs Triggerにしたので、そのままだと探すときに素通りしてしまいます。「投げているはずなのに何も起きない」ときは、まずここを疑ってください。

SpoonThrowerのポイント
  • Physics2D.OverlapCircle
    • 自分を中心にした円の中にある当たり判定を、まとめて集めてくれます
    • LayerMaskでEnemyレイヤーだけを探すようにしているので、壁や自分は引っかかりません
  • sqrMagnitudeで比べる
    • 距離を比べるだけなら、ルートの計算(magnitude)を省いても結果は同じです。毎フレーム何十体も比べるので、こういう所を削ります
  • List<Collider2D>を使い回す
    • foundをフィールドに持っておくと、毎回リストを作り直さずに済みます(#3のゴミの話と同じ考え方)

プレハブにして、設定しよう

スプーンをプレハブにする

  • HierarchyのSpoonに、Add Component>Spoon
  • SpoonをSurvivorsLike/Prefabsフォルダにドラッグ&ドロップしてプレハブにする
  • Hierarchyに残っているSpoonは削除する

Playerに投げる係を付ける

  • HierarchyでPlayerを選び、Add Component>SpoonThrower
  • Inspectorで設定する
    • Spoon Prefab:作ったスプーンのプレハブをドラッグ
    • Interval:1
    • Search Radius:10
    • Enemy Layer:Enemy(ドロップダウンからEnemyだけを選ぶ)
PlayerのSpoon Thrower。Spoon Prefab・Interval 1・Search Radius 10・Enemy LayerがEnemy

動かして確認!

再生すると、敵が近づいてきたところで、スライムちゃんから計量スプーンが飛んでいきます。

#4の動作確認

  • 1秒に1本、スプーンが一番近い敵に向かって飛んでいく
  • 当たった敵が消える(コゲクッキーはHP10、スプーンのダメージ10なので1発)
  • 敵が近くにいないときは投げない
  • 当たらなかったスプーンは、3秒で消える
フェアリー

スプーンが敵に吸い込まれるように飛んでいけば成功!向きもちゃんと回っているか見てみてね

飛んでいる2本の計量スプーン。どちらも進む向きに回転している

強さの決め方

今回の数字は、1体の敵を1発で倒せるように決めています。

項目値理由
投げる間隔1秒序盤は1秒に1体しか湧かないので、ちょうど追いつく
ダメージ10コゲクッキーのHPと同じ。1発で倒せる
スプーンの速さ8敵(1.5)よりずっと速く、プレイヤー(5)よりも速い
狙う範囲10マス画面の端(横9マス)より少し広い

序盤が気持ちよく、後半は数で押されて足りなくなる――この「だんだん追いつかなくなる感じ」がこのジャンルの面白さです。#7で強化を選べるようにして、プレイヤーが自分で追いつく形にします。

スライム

やっと反撃出来ました!でも1秒に1本だと、そのうち押し切られそうです・・・

フェアリー

それでいいの!強くなる余地を残しておくのが、この手のゲームのコツだよ

あとがき

今回は一番近い敵を自動で狙って、計量スプーンを投げるところまで作りました。移動しているだけで敵が倒れていく、あの気持ちよさの土台が出来ましたね!次回は武器のデータをScriptableObjectにまとめて、2つ目の武器「ホイッパー」を追加します。武器が増えても散らからない作りにしていきましょう。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

目次