【Cocos Creator 3.8】ProjectileShooter の実装:アタッチするだけで「入力または自動タイマーで弾を発射」できる汎用スクリプト
このガイドでは、任意のノードにアタッチするだけで「スペースキーやマウスクリックなどの入力」または「一定間隔のタイマー」によって、指定した弾プレハブ(弾シーン)をそのノードの位置から発射できる汎用コンポーネント ProjectileShooter を実装します。
プレイヤーキャラ・敵キャラ・タレットなど、発射位置となるノードに直接このコンポーネントを付けるだけで動作し、他のカスタムスクリプトへの依存は一切ありません。
コンポーネントの設計方針
要件整理
- このコンポーネントをアタッチしたノードの位置から、弾プレハブをインスタンス化して発射する。
- 発射トリガーは以下の2種類をサポート:
- 入力トリガー:キーボードキー、マウス左クリック、タッチなど。
- 自動トリガー:一定間隔のタイマーで自動発射。
- 弾は「方向」と「初速(速度)」を指定して飛ばす。
- 弾の見た目や当たり判定は弾プレハブ側に任せる(このコンポーネントはあくまで「発射」だけを担当)。
- 弾プレハブには
RigidBody2Dが付いていれば物理速度で飛ばし、無ければVec3の位置更新で直進させる「簡易ムーブ」機能を内蔵する。 - 外部の GameManager 等には一切依存せず、すべてインスペクタのプロパティで設定可能にする。
インスペクタで設定可能なプロパティ
ProjectileShooter に定義するプロパティと役割は以下の通りです。
- projectilePrefab: Prefab
- 発射する弾のプレハブ(弾シーン)。必須。
- ここに指定したプレハブがインスタンス化されて飛んでいきます。
- shootParent: Node | null
- 生成した弾をぶら下げる親ノード。
- 未指定の場合は、このコンポーネントが付いているノードの親(
this.node.parent)を使用。 - シーン直下に出したい場合は、Canvas やルートノードを指定します。
- shootDirection: Vec3
- 弾を飛ばす方向ベクトル。
- 例:右方向なら (1, 0, 0)、上方向なら (0, 1, 0)。
- ゼロベクトルの場合はワールド空間の +X 方向 (1, 0, 0) を自動使用。
- projectileSpeed: number
- 弾の速度(単位:ユニット/秒)。
- 2D 物理(
RigidBody2D)がある場合はその線形速度として設定。 - なければ簡易ムーブの速度として使用。
- lifeTime: number
- 弾が自動で破棄されるまでの秒数。
- 0 以下の場合は自動破棄しない(弾側で処理)。
- fireOnKey: boolean
- キーボード入力で発射するかどうか。
- fireKeyCode: KeyCode
- 発射に使用するキー。
- デフォルトは
KeyCode.SPACE。
- fireOnMouse: boolean
- マウス左クリック / タッチで発射するかどうか。
- PC では左クリック、モバイルではタップが対象。
- autoFire: boolean
- タイマーで自動発射を行うかどうか。
- fireInterval: number
- 自動発射の間隔(秒)。
autoFireが true のときのみ使用。
- initialDelay: number
- 自動発射開始までの遅延時間(秒)。
- maxProjectiles: number
- 同時に存在できる弾の最大数(簡易リミッター)。
- 0 以下なら無制限。
- 制限に達した場合、新しい弾は生成されません(警告ログを出力)。
- useNodeRotation: boolean
- 発射方向にこのノードの回転を反映するかどうか。
- 2D ゲームでノードの向きに合わせて撃ちたいときに便利。
- alignProjectileRotation: boolean
- 弾の回転を発射方向に合わせるかどうか。
- 2D で弾の向きを飛ぶ方向に向けたい場合に使用。
- debugLog: boolean
- 発射時やエラー時にログを出すかどうか。
TypeScriptコードの実装
import {
_decorator,
Component,
Node,
Prefab,
instantiate,
Vec3,
math,
RigidBody2D,
ERigidBody2DType,
v3,
KeyCode,
input,
Input,
EventKeyboard,
EventMouse,
EventTouch,
systemEvent,
SystemEventType,
director,
} from 'cc';
const { ccclass, property } = _decorator;
@ccclass('ProjectileShooter')
export class ProjectileShooter extends Component {
@property({
type: Prefab,
tooltip: '発射する弾のプレハブ(必須)。ここで指定した Prefab がインスタンス化されます。'
})
public projectilePrefab: Prefab | null = null;
@property({
type: Node,
tooltip: '生成した弾をぶら下げる親ノード。未指定の場合は this.node.parent を使用します。'
})
public shootParent: Node | null = null;
@property({
tooltip: '発射方向ベクトル。例: 右(1,0,0)、上(0,1,0)。ゼロベクトルの場合は自動的に (1,0,0) を使用します。'
})
public shootDirection: Vec3 = v3(1, 0, 0);
@property({
tooltip: '弾の速度(ユニット/秒)。RigidBody2D があれば線形速度、なければ簡易ムーブ用速度として使用されます。'
})
public projectileSpeed: number = 800;
@property({
tooltip: '弾が自動で破棄されるまでの秒数。0 以下なら自動破棄しません。'
})
public lifeTime: number = 3.0;
@property({
tooltip: 'キーボード入力で発射するかどうか。'
})
public fireOnKey: boolean = true;
@property({
type: KeyCode,
tooltip: '発射に使うキー。デフォルトは SPACE。'
})
public fireKeyCode: KeyCode = KeyCode.SPACE;
@property({
tooltip: 'マウス左クリック / タッチで発射するかどうか。'
})
public fireOnMouse: boolean = false;
@property({
tooltip: 'タイマーで自動発射するかどうか。'
})
public autoFire: boolean = false;
@property({
tooltip: '自動発射の間隔(秒)。autoFire が true のときのみ使用されます。'
})
public fireInterval: number = 0.5;
@property({
tooltip: '自動発射開始までの遅延時間(秒)。'
})
public initialDelay: number = 0.0;
@property({
tooltip: '同時に存在できる弾の最大数。0 以下なら無制限。制限に達すると新規発射はキャンセルされます。'
})
public maxProjectiles: number = 0;
@property({
tooltip: '発射方向にこのノードの回転を反映するかどうか(2D で向きに合わせて撃ちたいときなど)。'
})
public useNodeRotation: boolean = false;
@property({
tooltip: '弾の回転を発射方向に合わせるかどうか。'
})
public alignProjectileRotation: boolean = false;
@property({
tooltip: '発射やエラー時にデバッグログを出力するかどうか。'
})
public debugLog: boolean = false;
// 内部状態
private _timeSinceStart: number = 0;
private _timeSinceLastShot: number = 0;
private _currentProjectileCount: number = 0;
private _inputRegistered: boolean = false;
onLoad() {
// defensive: projectilePrefab が未設定の場合は警告
if (!this.projectilePrefab) {
console.warn('[ProjectileShooter] projectilePrefab が設定されていません。このコンポーネントは発射できません。', this.node.name);
}
// 入力イベント登録
this.registerInput();
}
onEnable() {
// disable → enable の切り替え時にも入力を登録し直す
this.registerInput();
}
onDisable() {
this.unregisterInput();
}
onDestroy() {
this.unregisterInput();
}
start() {
// 自動発射用のタイマー初期化
this._timeSinceStart = 0;
this._timeSinceLastShot = 0;
}
update(deltaTime: number) {
// 自動発射のみここで処理
if (!this.autoFire) {
return;
}
if (!this.projectilePrefab) {
return;
}
this._timeSinceStart += deltaTime;
this._timeSinceLastShot += deltaTime;
if (this._timeSinceStart < this.initialDelay) {
return;
}
if (this._timeSinceLastShot >= this.fireInterval) {
this.tryShoot();
}
}
/**
* 入力イベントの登録
*/
private registerInput() {
if (this._inputRegistered) {
return;
}
// キーボード
if (this.fireOnKey) {
input.on(Input.EventType.KEY_DOWN, this.onKeyDown, this);
}
// マウス / タッチ
if (this.fireOnMouse) {
input.on(Input.EventType.MOUSE_DOWN, this.onMouseDown, this);
input.on(Input.EventType.TOUCH_START, this.onTouchStart, this);
}
this._inputRegistered = true;
}
/**
* 入力イベントの解除
*/
private unregisterInput() {
if (!this._inputRegistered) {
return;
}
input.off(Input.EventType.KEY_DOWN, this.onKeyDown, this);
input.off(Input.EventType.MOUSE_DOWN, this.onMouseDown, this);
input.off(Input.EventType.TOUCH_START, this.onTouchStart, this);
this._inputRegistered = false;
}
private onKeyDown(event: EventKeyboard) {
if (!this.fireOnKey) {
return;
}
if (event.keyCode === this.fireKeyCode) {
this.tryShoot();
}
}
private onMouseDown(event: EventMouse) {
if (!this.fireOnMouse) {
return;
}
if (event.getButton() === EventMouse.BUTTON_LEFT) {
this.tryShoot();
}
}
private onTouchStart(event: EventTouch) {
if (!this.fireOnMouse) {
return;
}
this.tryShoot();
}
/**
* 弾発射の試行。条件を満たしていれば shoot() を呼ぶ。
*/
private tryShoot() {
if (!this.projectilePrefab) {
console.warn('[ProjectileShooter] projectilePrefab が設定されていないため発射できません。', this.node.name);
return;
}
// 同時弾数制限
if (this.maxProjectiles > 0 && this._currentProjectileCount >= this.maxProjectiles) {
if (this.debugLog) {
console.warn('[ProjectileShooter] maxProjectiles に達しているため発射をキャンセルしました。', this.node.name);
}
return;
}
this.shoot();
// 自動発射のインターバルリセット
this._timeSinceLastShot = 0;
}
/**
* 実際に弾を生成して飛ばす。
*/
private shoot() {
if (!this.projectilePrefab) {
return;
}
const parent = this.shootParent || this.node.parent;
if (!parent) {
console.warn('[ProjectileShooter] 親ノードが見つからないため、弾をシーンに追加できません。', this.node.name);
return;
}
// 弾インスタンス生成
const projectile = instantiate(this.projectilePrefab);
parent.addChild(projectile);
// 発射位置 = このノードのワールド座標
const worldPos = this.node.worldPosition.clone();
projectile.setWorldPosition(worldPos);
// 発射方向の決定
let dir = this.shootDirection.clone();
if (dir.lengthSqr() === 0) {
dir.set(1, 0, 0); // デフォルトは +X 方向
}
// ノードの回転を反映する場合
if (this.useNodeRotation) {
// 3D でも 2D でも Quaternion を使って回転を適用
const quat = this.node.worldRotation;
dir = Vec3.transformQuat(new Vec3(), dir, quat);
}
dir.normalize();
// 弾の回転を方向に合わせる場合(簡易 2D 対応)
if (this.alignProjectileRotation) {
// 2D 前提: Z 回転を使う
const angleRad = Math.atan2(dir.y, dir.x);
const angleDeg = math.toDegree(angleRad);
projectile.setRotationFromEuler(0, 0, angleDeg);
}
// 速度を適用
const rb2d = projectile.getComponent(RigidBody2D);
if (rb2d) {
// 2D 物理ボディがある場合は線形速度を設定
const velocity = new math.Vec2(dir.x * this.projectileSpeed, dir.y * this.projectileSpeed);
rb2d.linearVelocity = velocity;
} else {
// 物理ボディが無い場合は簡易ムーブ用コンポーネントをアタッチ
projectile.addComponent(SimpleProjectileMover).init(dir, this.projectileSpeed);
}
// ライフタイムが設定されている場合は自動破棄
if (this.lifeTime > 0) {
// 弾が破棄されたらカウンタを減らすため、コールバックを仕込む
this.scheduleOnce(() => {
if (projectile.isValid) {
projectile.destroy();
}
}, this.lifeTime);
}
// 弾カウンタ更新
this._currentProjectileCount++;
// 弾が destroy されたときにカウンタを減らす
projectile.on(Node.EventType.NODE_DESTROYED, () => {
this._currentProjectileCount = Math.max(0, this._currentProjectileCount - 1);
});
if (this.debugLog) {
console.log(`[ProjectileShooter] Projectile fired from ${this.node.name} at ` +
`(${worldPos.x.toFixed(2)}, ${worldPos.y.toFixed(2)}, ${worldPos.z.toFixed(2)}) ` +
`dir=(${dir.x.toFixed(2)}, ${dir.y.toFixed(2)}, ${dir.z.toFixed(2)})`);
}
}
}
/**
* RigidBody2D が存在しない弾に対して、単純な直進運動を付与するための内部コンポーネント。
* ProjectileShooter からのみ使用される前提で、外部から直接触る必要はありません。
*/
@ccclass('SimpleProjectileMover')
class SimpleProjectileMover extends Component {
private _direction: Vec3 = v3(1, 0, 0);
private _speed: number = 0;
public init(direction: Vec3, speed: number) {
this._direction.set(direction);
this._direction.normalize();
this._speed = speed;
}
update(deltaTime: number) {
if (this._speed === 0) {
return;
}
const move = this._direction.clone().multiplyScalar(this._speed * deltaTime);
this.node.translate(move, Node.NodeSpace.WORLD);
}
}
コードのポイント解説
- onLoad / onEnable / onDisable / onDestroy
onLoadでprojectilePrefabの未設定チェックと入力イベント登録を行います。onEnableでも再度registerInput()を呼び、無効化→有効化時にも入力が復活するようにしています。onDisable/onDestroyで必ず入力イベントを解除し、二重登録やリークを防ぎます。
- update
autoFireが true の場合のみ処理します。initialDelay経過後、fireIntervalごとにtryShoot()を呼びます。
- tryShoot
- プレハブ未設定時や
maxProjectiles制限に達した場合は発射を中止し、必要に応じてログを出します。 - 条件を満たした場合のみ
shoot()を呼び出します。
- プレハブ未設定時や
- shoot
- 発射元ノードのワールド座標に弾を生成し、
shootDirectionとuseNodeRotationに基づいて方向ベクトルを決定します。 alignProjectileRotationが true の場合、2D 前提で Z 回転を方向に合わせます。- 弾に
RigidBody2Dがあれば線形速度をセットし、なければ内部のSimpleProjectileMoverを追加して直進させます。 lifeTimeが正ならscheduleOnceで自動破棄します。- 弾破棄時にカウンタを減らすため、
NODE_DESTROYEDイベントで_currentProjectileCountを管理します。
- 発射元ノードのワールド座標に弾を生成し、
- SimpleProjectileMover
RigidBody2Dを持たない弾に対して、updateで毎フレーム位置を移動させるだけの軽量コンポーネントです。- 外部依存を避けるため、このファイル内に閉じています。
使用手順と動作確認
1. ProjectileShooter.ts を作成する
- Assets パネルで右クリック → Create → TypeScript を選択し、ファイル名を
ProjectileShooter.tsにします。 - 自動生成された内容をすべて削除し、本記事の
ProjectileShooterコード全体を貼り付けて保存します。
2. 弾プレハブ(Projectile)を用意する
ここでは 2D のシンプルな弾を例にします。
- Hierarchy で右クリック → Create → 2D Object → Sprite を選択し、名前を
Bulletに変更します。 - Inspector の Sprite コンポーネントで、お好みの弾画像(小さな丸など)を指定します。
- 物理挙動を使いたい場合:
- Add Component → Physics 2D → RigidBody2D を追加します。
- Body Type を
Dynamicに設定します。 - Add Component → Physics 2D → CircleCollider2D など、適切なコライダーを追加します。
- Assets パネルに
Bulletノードをドラッグ&ドロップしてプレハブ化します(例:Assets/Prefabs/Bullet.prefab)。 - Hierarchy 上の元の
Bulletノードはテスト用に不要であれば削除して構いません。
3. 発射元ノードを作成し ProjectileShooter をアタッチ
プレイヤーキャラや砲台ノードに付けても良いですが、ここではテスト用に単純なノードを作ります。
- Hierarchy で右クリック → Create → 2D Object → Sprite を選択し、名前を
Shooterに変更します。 - Inspector の Add Component → Custom → ProjectileShooter を選択し、コンポーネントを追加します。
- ProjectileShooter コンポーネントの各プロパティを設定します:
- Projectile Prefab:先ほど作成した
Bulletプレハブをドラッグ&ドロップ。 - Shoot Parent:空欄のままでも OK(この場合
Shooterの親ノードに弾が生成されます)。Canvas 直下に出したい場合は Canvas ノードを指定します。 - Shoot Direction:
(1, 0, 0)(右方向)に設定。 - Projectile Speed:
800など、少し大きめの値を入れます。 - Life Time:
3(3秒後に自動破棄)。 - Fire On Key:チェック ON。
- Fire Key Code:
SPACEのままで OK。 - Fire On Mouse:必要なら ON(左クリックやタップで撃ちたい場合)。
- Auto Fire:まずは OFF(手動発射テスト)。
- Fire Interval:
0.2(自動発射時用)。 - Initial Delay:
0。 - Max Projectiles:
0(無制限)でテストし、必要に応じて制限をかけます。 - Use Node Rotation:2D で向きに合わせて撃ちたい場合は ON。
- Align Projectile Rotation:弾の見た目を飛ぶ方向に向けたい場合は ON。
- Debug Log:挙動確認したい場合は ON にしてコンソールを確認します。
- Projectile Prefab:先ほど作成した
4. 手動発射の動作確認(スペースキー / クリック)
- 上記のように Fire On Key = ON, Fire Key Code = SPACE に設定し、Auto Fire = OFF にしておきます。
- エディタ右上の ▶︎ Play ボタンでプレビューを開始します。
- ゲームビュー上でフォーカスを当てた状態で、キーボードの スペースキー を押します。
Shooterの位置から、右方向に向かって弾が飛んでいけば成功です。- マウスクリックでも発射したい場合は、Fire On Mouse = ON にして、ゲームビュー上で左クリック(またはタップ)してみてください。
5. 自動発射の動作確認(タイマー)
Shooterノードの ProjectileShooter 設定を以下のように変更します:- Auto Fire:ON
- Fire Interval:
0.2(1秒間に約5発) - Initial Delay:
1.0(1秒後から撃ち始める) - Fire On Key / Fire On Mouse:任意(ON のままでも併用可能)
- プレビューを開始すると、1秒後から自動的に一定間隔で弾が発射され続けます。
- Max Projectiles を
20などに設定すると、画面内の弾数が増えすぎるのを防げます。
6. ノードの向きに合わせて発射方向を変える(2D 回転対応)
プレイヤーが回転するゲームなどでは、ノードの回転に応じて発射方向を変えたいケースがあります。
Shooterノードの ProjectileShooter で:- Shoot Direction:
(1, 0, 0)(ノードの右方向を基準にする)。 - Use Node Rotation:ON。
- Align Projectile Rotation:ON(弾の見た目も向きを合わせたい場合)。
- Shoot Direction:
- ゲーム中に
Shooterノードの Z 回転を変える(エディタ上でアニメーションを付ける、または別スクリプトで回転させる)と、その向きに沿って弾が飛ぶようになります。
まとめ
ProjectileShooter コンポーネントは、
- 弾プレハブ(見た目・当たり判定・物理の有無)
- 発射トリガー(キー入力 / マウス・タッチ / 自動タイマー)
- 発射方向・速度・ライフタイム
- 同時弾数制限やデバッグログ
といった要素をすべてインスペクタから完結して設定できるようにした、完全に独立した汎用コンポーネントです。
プレイヤー、敵、タレット、トラップなど「何かを定期的に(または入力で)飛ばしたい」場面で、このスクリプトをそのままアタッチして設定するだけで済むため、
- 発射ロジックを毎回書き直す手間の削減
- プレハブ単位での再利用性向上
- プロトタイピングの高速化
といったメリットがあります。
弾側の挙動(ヒット時の処理、エフェクト、ダメージ計算など)は別途プレハブにスクリプトを追加して拡張できますが、ProjectileShooter 自体はそれらに一切依存していないため、どんなゲームにもそのまま流用しやすい構成になっています。
必要に応じて、次のようなバリエーションを追加していくこともできます。
- 複数方向への同時発射(拡散弾)
- チャージショット(押しっぱなし時間で速度や弾種を変える)
- オブジェクトプールを用いた弾の再利用
まずは今回の ProjectileShooter をベースに、あなたのゲームに合わせた発射パターンを組み立ててみてください。




