【Cocos Creator 3.8】Summoner(召喚士)の実装:アタッチするだけで定期的に敵シーンを自動生成する汎用スクリプト
このガイドでは、任意のノードにアタッチするだけで「指定した敵シーンを一定間隔で自動インスタンス化して出現させる」汎用コンポーネント Summoner を実装します。
敵の出現ポイントにこのコンポーネントを付けておけば、ゲーム全体の管理クラスやシングルトンを用意しなくても、インスペクタでシーンアセットと出現間隔を設定するだけで簡単に「召喚ポイント」を量産できます。
目次
コンポーネントの設計方針
実現したい機能
- 指定した プレハブシーン(Prefab) を一定間隔でインスタンス化する。
- 生成位置は、このコンポーネントがアタッチされたノードの位置を基準とする。
- 最大同時出現数(上限)を超えないように制御できる。
- ゲーム開始時にすぐ召喚を始めるか、遅延してから開始するかを選べる。
- ランダムな出現間隔(最小〜最大)を設定できる。
- 召喚された敵が破棄されたときにカウントを減らし、上限管理が正しく動くようにする。
- 外部スクリプトに一切依存しない(GameManager 等は不要)。
外部依存をなくすための設計アプローチ
- 敵のプレハブは
@property(Prefab)で直接インスペクタから指定する。 - 生成先の親ノード(コンテナ)も
@property(Node)で指定可能にし、未設定の場合は自分自身の親ノードに生成する。 - 召喚ロジックは
Summonerコンポーネント内で完結させ、schedule/unscheduleを使ってタイマー管理する。 - 敵の破棄検知も
Summoner内で完結させるため、生成したノードに対して 自前のコールバック を紐づける(外部コンポーネントに依存しない)。
インスペクタで設定可能なプロパティ
- enemyPrefab: Prefab
召喚する敵のプレハブ。
必須。未設定の場合はエラーログを出して召喚を行わない。 - spawnParent: Node | null
召喚した敵をぶら下げる親ノード。
未設定の場合は Summoner がアタッチされたノードの親 を使用する。
どこにもぶら下げられない場合(親がいない)は、エラーログを出す。 - autoStart: boolean
trueの場合、start()で自動的に召喚を開始する。
falseの場合は、外部からstartSummon()を呼び出したときのみ召喚が始まる。 - initialDelay: number
召喚を開始するまでの遅延時間(秒)。
例:1.5にすると、ゲーム開始から 1.5 秒後に最初の召喚が行われる。 - minInterval: number
召喚間隔の最小値(秒)。
次の召喚までの時間を[minInterval, maxInterval]の範囲でランダムに決定する。 - maxInterval: number
召喚間隔の最大値(秒)。
minInterval > maxIntervalの場合は、自動的に入れ替えて安全な値に補正する。 - maxAlive: number
同時に存在できる召喚済み敵の最大数。
0 以下を指定した場合は「上限なし」とみなす。 - randomOffsetRadius: number
召喚位置のランダム半径(ワールド座標)。
0 の場合は完全に同じ位置に出現。
例:2.0にすると、召喚者の周囲 2 ユニット以内のランダムな位置に出現。 - alignToSummonerRotation: boolean
trueの場合、召喚された敵の回転を召喚者ノードの回転に合わせる。 - debugLog: boolean
trueにすると、召喚開始・停止・生成・上限到達などの情報をconsole.logに出力する。
TypeScriptコードの実装
import { _decorator, Component, Node, Prefab, instantiate, Vec3, math, director } from 'cc';
const { ccclass, property } = _decorator;
/**
* Summoner
* 指定した Prefab を一定間隔でインスタンス化して出現させる汎用コンポーネント。
* 他のカスタムスクリプトには一切依存しません。
*/
@ccclass('Summoner')
export class Summoner extends Component {
@property({
type: Prefab,
tooltip: '召喚する敵のプレハブ。未設定の場合は召喚されません。'
})
public enemyPrefab: Prefab | null = null;
@property({
type: Node,
tooltip: '召喚された敵をぶら下げる親ノード。未設定の場合はこのノードの親ノードを使用します。'
})
public spawnParent: Node | null = null;
@property({
tooltip: 'true の場合、ゲーム開始と同時に召喚を開始します。'
})
public autoStart: boolean = true;
@property({
tooltip: '召喚を開始するまでの遅延時間(秒)。'
})
public initialDelay: number = 0.0;
@property({
tooltip: '召喚間隔の最小値(秒)。maxInterval と合わせてランダムな間隔を決定します。'
})
public minInterval: number = 2.0;
@property({
tooltip: '召喚間隔の最大値(秒)。minInterval より小さい場合は自動で入れ替えます。'
})
public maxInterval: number = 4.0;
@property({
tooltip: '同時に存在できる召喚済み敵の最大数。0 以下の場合は上限なし。'
})
public maxAlive: number = 5;
@property({
tooltip: '召喚位置のランダム半径。0 の場合は完全に同じ位置に出現します。'
})
public randomOffsetRadius: number = 0.0;
@property({
tooltip: 'true の場合、召喚された敵の回転を召喚者ノードの回転に合わせます。'
})
public alignToSummonerRotation: boolean = true;
@property({
tooltip: 'true にすると召喚処理のログをコンソールに出力します。'
})
public debugLog: boolean = false;
/** 現在生存している召喚済み敵の数 */
private _aliveCount: number = 0;
/** 現在召喚がアクティブかどうか */
private _isSummoning: boolean = false;
/** 次の召喚までの残り時間(秒) */
private _timeToNextSpawn: number = 0;
onLoad() {
// minInterval と maxInterval の関係を補正
if (this.minInterval <= 0) {
this.minInterval = 0.1;
}
if (this.maxInterval <= 0) {
this.maxInterval = this.minInterval;
}
if (this.minInterval > this.maxInterval) {
const tmp = this.minInterval;
this.minInterval = this.maxInterval;
this.maxInterval = tmp;
}
if (!this.enemyPrefab) {
console.error('[Summoner] enemyPrefab が設定されていません。このコンポーネントは動作しません。', this.node);
}
// spawnParent が未設定の場合は、このノードの親を使う
if (!this.spawnParent) {
this.spawnParent = this.node.parent;
}
if (!this.spawnParent) {
console.error('[Summoner] spawnParent が設定されておらず、このノードにも親がありません。生成先がないため召喚できません。', this.node);
}
}
start() {
if (this.autoStart) {
this.startSummon();
}
}
update(deltaTime: number) {
if (!this._isSummoning) {
return;
}
if (!this.enemyPrefab) {
// 毎フレームエラーを出すと煩雑なので、一度だけエラーを出したい場合はフラグ管理するのもあり。
return;
}
// 上限チェック(maxAlive <= 0 の場合は上限なし)
if (this.maxAlive > 0 && this._aliveCount >= this.maxAlive) {
return;
}
this._timeToNextSpawn -= deltaTime;
if (this._timeToNextSpawn <= 0) {
this.spawnOne();
// 次回の召喚時間を再設定
this._timeToNextSpawn = this.getRandomInterval();
}
}
/**
* 召喚を開始する(外部からも呼び出し可能)。
*/
public startSummon(): void {
if (this._isSummoning) {
return;
}
if (!this.enemyPrefab) {
console.error('[Summoner] enemyPrefab が設定されていないため、召喚を開始できません。', this.node);
return;
}
if (!this.spawnParent) {
console.error('[Summoner] spawnParent が設定されていないため、召喚を開始できません。', this.node);
return;
}
this._isSummoning = true;
this._timeToNextSpawn = this.initialDelay > 0 ? this.initialDelay : this.getRandomInterval();
if (this.debugLog) {
console.log('[Summoner] 召喚を開始しました。初回まで', this._timeToNextSpawn.toFixed(2), '秒', this.node.name);
}
}
/**
* 召喚を停止する(外部からも呼び出し可能)。
*/
public stopSummon(): void {
if (!this._isSummoning) {
return;
}
this._isSummoning = false;
if (this.debugLog) {
console.log('[Summoner] 召喚を停止しました。', this.node.name);
}
}
/**
* ランダムな召喚間隔を返す。
*/
private getRandomInterval(): number {
if (this.minInterval === this.maxInterval) {
return this.minInterval;
}
return math.lerp(this.minInterval, this.maxInterval, Math.random());
}
/**
* 敵を 1 体召喚する。
*/
private spawnOne(): void {
if (!this.enemyPrefab || !this.spawnParent) {
return;
}
// 生成位置を決定
const worldPos = new Vec3();
this.node.getWorldPosition(worldPos);
if (this.randomOffsetRadius > 0) {
const angle = Math.random() * Math.PI * 2;
const radius = Math.random() * this.randomOffsetRadius;
worldPos.x += Math.cos(angle) * radius;
worldPos.y += Math.sin(angle) * radius;
}
// プレハブからインスタンスを生成
const enemyNode = instantiate(this.enemyPrefab);
// 親ノードに追加
this.spawnParent.addChild(enemyNode);
// ワールド座標を設定
enemyNode.setWorldPosition(worldPos);
// 回転を揃えるオプション
if (this.alignToSummonerRotation) {
const worldRot = this.node.getWorldRotation();
enemyNode.setWorldRotation(worldRot);
}
this._aliveCount++;
if (this.debugLog) {
console.log('[Summoner] 敵を召喚しました。現在の生存数:', this._aliveCount, 'ノード名:', enemyNode.name);
}
// 敵ノードが破棄されたときにカウントを減らすためのフック
// onDestroy は Component にしかないため、一時的な内部コンポーネントを追加して管理する。
const tracker = enemyNode.addComponent(SummonedTracker);
tracker.ownerSummoner = this;
}
/**
* SummonedTracker から呼ばれるコールバック。
* 召喚された敵が破棄されたときに生存数を減らす。
*/
public notifySummonedDestroyed(): void {
this._aliveCount = Math.max(0, this._aliveCount - 1);
if (this.debugLog) {
console.log('[Summoner] 召喚された敵が破棄されました。現在の生存数:', this._aliveCount);
}
}
onDisable() {
// ノードが無効化されたら召喚を止める
this.stopSummon();
}
onDestroy() {
// ノード破棄時も安全のため停止
this.stopSummon();
}
}
/**
* SummonedTracker
* Summoner が生成した敵ノードに自動的に付与される内部用コンポーネント。
* 敵ノードが破棄されたときに Summoner に通知してカウントを調整する。
*
* ※外部から直接使う必要はありません。
*/
@ccclass('SummonedTracker')
class SummonedTracker extends Component {
public ownerSummoner: Summoner | null = null;
onDestroy() {
if (this.ownerSummoner) {
this.ownerSummoner.notifySummonedDestroyed();
}
}
}
コードの主要ポイント解説
- onLoad
minIntervalとmaxIntervalの値を補正し、異常値(負数や逆転)を防いでいます。enemyPrefabが設定されていない場合や、spawnParentを決定できない場合にconsole.errorでエラーログを出します。
- start
autoStartがtrueのときだけstartSummon()を呼び出し、自動的に召喚を開始します。
- update(deltaTime)
_isSummoningがtrueのときだけ動作します。maxAliveで同時出現数の上限をチェックし、上限に達していれば召喚をスキップします。_timeToNextSpawnを減算し、0 以下になったらspawnOne()を呼び出して敵を生成し、次の間隔を再設定します。
- spawnOne()
- 自分自身のワールド座標を基準に、
randomOffsetRadiusでランダムな位置を計算します。 instantiate(enemyPrefab)で敵ノードを生成し、spawnParentにaddChildします。alignToSummonerRotationがtrueの場合、召喚者と同じ回転を設定します。- 生成した敵ノードに
SummonedTrackerを付与し、敵が破棄されたときにnotifySummonedDestroyed()が呼ばれるようにしています。
- 自分自身のワールド座標を基準に、
- SummonedTracker
- このコンポーネントは
Summoner内部専用です。ユーザーが意識して設定する必要はありません。 - 敵ノードが
destroy()されたときにonDestroy()が呼ばれ、紐づいているSummonerに「1 体減った」ことを通知します。
- このコンポーネントは
使用手順と動作確認
1. 敵プレハブの準備
- Hierarchy パネルで右クリック → Create → 2D Object → Sprite などで、敵用のノードを作成します。
- 必要であれば、敵用のスクリプトやアニメーション、Collider などを追加してください(任意)。
- 作成した敵ノードを Assets パネル にドラッグ&ドロップして Prefab にします。
例:EnemyBasic.prefab
2. Summoner.ts スクリプトの作成
- Assets パネルで右クリック → Create → TypeScript を選択し、ファイル名を
Summoner.tsにします。 - 自動生成されたコードをすべて削除し、本記事の TypeScriptコードの実装 セクションのコードを丸ごと貼り付けて保存します。
3. 召喚ポイント用ノードの作成とアタッチ
- Hierarchy パネルで右クリック → Create → Empty Node を選択し、ノード名を
EnemySpawnPointなどに変更します。 - このノードを敵を出現させたい位置に移動させます。
EnemySpawnPointノードを選択し、Inspector の Add Component ボタンをクリックします。- Custom → Summoner を選択してコンポーネントを追加します。
4. Summoner のプロパティ設定
- Enemy Prefab に、先ほど作成した
EnemyBasic.prefabをドラッグ&ドロップします。 - Spawn Parent は、特にこだわりがなければ空欄のままで構いません。
空欄の場合、EnemySpawnPointの親ノードに敵がぶら下がります。
敵だけをまとめて管理したい場合は、Hierarchy でEnemiesノードを作成し、そこをSpawn Parentに指定してください。 - Auto Start: とりあえず
trueのままにしておきます。 - Initial Delay: 例として
1.0秒に設定すると、ゲーム開始から 1 秒後に最初の敵が出現します。 - Min Interval: 例として
2.0秒。 - Max Interval: 例として
4.0秒。
この場合、2〜4 秒のランダムな間隔で敵が次々と出現します。 - Max Alive: 例として
5に設定します。
シーン上に 5 体以上の敵が存在する場合は、新たな敵は生成されません。
敵をdestroy()するなどして減らすと、再び召喚が再開されます。 - Random Offset Radius: 例として
1.5に設定します。
召喚ポイントの周囲 1.5 ユニット以内のランダムな位置に敵が出現するようになります。 - Align To Summoner Rotation: 敵の向きを召喚ポイントの向きに合わせたい場合は
trueのままにします。 - Debug Log: 動作確認のために
trueにしておくと、コンソールに召喚ログが表示されてわかりやすいです。
5. プレビューで動作確認
- メインカメラが敵出現位置を映していることを確認します。
- エディタ右上の Play ボタン(ブラウザプレビュー)をクリックします。
- ゲーム開始から
Initial Delay秒後に、EnemySpawnPointの周囲に敵が出現し始めることを確認してください。 Debug Logをtrueにしている場合、ブラウザのコンソールに
[Summoner] 敵を召喚しました。現在の生存数: ...
といったログが出力されているはずです。- 敵ノードを何らかの方法で
destroy()すると、生存数が減り、再び召喚が行われることも確認してみてください。
6. 応用的な使い方の例
- 複数の召喚ポイントを配置する
EnemySpawnPointノードを複製して、マップのあちこちに配置します。- それぞれの
SummonerでInitial DelayやMin/Max Intervalを変えることで、場所ごとに違うテンポで敵が湧くようにできます。
- 敵の種類を変える
- 別の敵プレハブ(例:
EnemyFast.prefab)を作成し、別のSummonerのEnemy Prefabに設定します。 - 1 つのマップに「遅い雑魚」「素早い雑魚」などを混在させることが簡単にできます。
- 別の敵プレハブ(例:
- スクリプトから召喚の開始/停止を制御する
autoStart = falseにしておき、他のコンポーネントからstartSummon()/stopSummon()を呼び出すことで、特定のイベント(ボス出現後など)に合わせて召喚を開始・停止できます。- このときも Summoner 自体は外部に依存していない ため、どのようなゲーム構造にも組み込みやすくなっています。
まとめ
この Summoner コンポーネントは、
- 敵プレハブと出現間隔、上限数をインスペクタで指定するだけで使える。
- 外部の GameManager やシングルトンに一切依存せず、このスクリプト単体で完結している。
- 召喚位置や向き、ランダムオフセット、ログ出力など、実運用に必要なオプションを備えている。
という特徴を持つ、汎用性の高い召喚ポイント実装です。
マップ上に複数配置するだけで「このエリアでは 2〜4 秒おきに 3 体まで」「こっちのエリアでは 5〜8 秒おきに 10 体まで」といったパターンを簡単に作れるため、ステージ設計やバランス調整の効率が大きく向上します。
このコンポーネントをベースに、例えば「時間経過で召喚速度が上がる」「プレイヤーの距離に応じて召喚をオン/オフする」といった拡張も、すべて Summoner 内部のロジック追加だけで完結させることができます。
まずは本記事のコードをそのまま導入して、シンプルな「雑魚敵の湧きポイント」として活用してみてください。
