
前回で敵がわいてくるようになりましたが、こちらは逃げることしか出来ませんでした。いよいよ反撃です!ヴァンサバ風のゲームでは、プレイヤーは移動に集中して、攻撃は武器が勝手にやってくれるのがルール。今回はヴァンサバ風サバイバーゲームのシリーズの4回目として、一番近い敵を自動で見つけて、計量スプーンを投げる仕組みを作ってみたいと思います!
- シリーズの目次:ヴァンサバ風ゲームの作り方
- 前回:#3 敵を出して追いかけさせよう
今回作るもの
- 1秒に1本、計量スプーンが飛んでいく
- 飛ぶ先は一番近い敵。敵がいないときは投げない
- 当たった敵はダメージを受けて、HPが0になったら倒れる
- スプーンも敵と同じようにObjectPoolで使い回す
作る・直すスクリプトは3つです。
| スクリプト | 役割 | 付ける場所 |
|---|---|---|
| Spoon(新しく作る) | まっすぐ飛んで、当たった敵にダメージを与える | スプーンのプレハブ |
| SpoonThrower(新しく作る) | 一番近い敵を探して、一定間隔で投げる | Player |
| Enemy(直す) | HPを持って、ダメージを受ける | 敵のプレハブ |
前回の続きから
- Unityのバージョンは6.3 LTS
- 記事作成時点では6000.3.22f1で確認しています
- #3の状態:画面の外から敵が出てきて追いかけてくる。敵は
Enemyレイヤーに入っている
計量スプーンのプレハブを作ろう
- 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

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

動かして確認!
再生すると、敵が近づいてきたところで、スライムちゃんから計量スプーンが飛んでいきます。
#4の動作確認
- 1秒に1本、スプーンが一番近い敵に向かって飛んでいく
- 当たった敵が消える(コゲクッキーはHP10、スプーンのダメージ10なので1発)
- 敵が近くにいないときは投げない
- 当たらなかったスプーンは、3秒で消える
フェアリースプーンが敵に吸い込まれるように飛んでいけば成功!向きもちゃんと回っているか見てみてね


強さの決め方
今回の数字は、1体の敵を1発で倒せるように決めています。
| 項目 | 値 | 理由 |
|---|---|---|
| 投げる間隔 | 1秒 | 序盤は1秒に1体しか湧かないので、ちょうど追いつく |
| ダメージ | 10 | コゲクッキーのHPと同じ。1発で倒せる |
| スプーンの速さ | 8 | 敵(1.5)よりずっと速く、プレイヤー(5)よりも速い |
| 狙う範囲 | 10マス | 画面の端(横9マス)より少し広い |
序盤が気持ちよく、後半は数で押されて足りなくなる――この「だんだん追いつかなくなる感じ」がこのジャンルの面白さです。#7で強化を選べるようにして、プレイヤーが自分で追いつく形にします。



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



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