【Cocos Creator 3.8】AmmoCounter の実装:アタッチするだけで「残弾管理・発射可否判定・リロード処理」を実現する汎用スクリプト
このガイドでは、任意のノードにアタッチするだけで「弾数の管理」「弾切れ時の発射制限」「リロード処理(自動・手動)」を完結できる AmmoCounter コンポーネントを実装します。
弾を発射するロジック自体(弾プレハブを生成するなど)は別のスクリプトに任せつつ、「今撃てるか?」「リロード中か?」「残弾はいくつか?」という状態管理をこのコンポーネント1つで完結させます。外部のシングルトンや GameManager への依存は一切ありません。
コンポーネントの設計方針
機能要件の整理
- 残弾数(現在弾数)を管理する。
- 最大装弾数を超えないようにする。
- 残弾が 0 のときは「発射不可」と判断できるようにする。
- リロード機能を提供する。
- 手動リロード(メソッド呼び出し)に対応。
- オプションで自動リロード(弾切れになったら一定時間後に自動でリロード)も可能。
- リロード中は発射不可にする。
- 外部スクリプトから簡単に状態を参照・操作できるようにする。
canShoot()で発射可否を判定。tryConsumeAmmo()で「撃つときの弾消費」を1メソッドで完結。startReload()で手動リロード開始。getCurrentAmmo(),getMaxAmmo()で残弾・最大弾数を取得。
このコンポーネント自体は UI 表示やサウンド再生などには一切依存せず、「弾の数と状態」だけを管理します。UI やエフェクトは別スクリプト側で、必要に応じて AmmoCounter の public API を参照する形を想定しています。
インスペクタで設定可能なプロパティ
AmmoCounter は完全に外部依存を排除し、すべての設定をインスペクタで調整できるようにします。
maxAmmo: number- 「最大装弾数」。
- 弾倉に入る最大の弾数。
- 1 以上の値を推奨。0 以下の場合は警告を出し、1 に補正します。
startWithFullAmmo: boolean- 開始時に最大弾数でスタートするかどうか。
- ON の場合:
currentAmmo = maxAmmoで開始。 - OFF の場合:
initialAmmoの値で開始。
initialAmmo: number- 開始時の弾数(
startWithFullAmmoが OFF のときのみ有効)。 - 0 以上
maxAmmo以下に自動クランプ。 - 例えば、ゲーム開始時は 0 発で、プレイヤーがアイテムを取るまで撃てない…といった設計が可能。
- 開始時の弾数(
autoReloadOnEmpty: boolean- 弾切れになったときに、自動でリロードを開始するかどうか。
- ON の場合:
currentAmmoが 0 になった瞬間にリロード処理へ移行。 - OFF の場合:手動で
startReload()を呼ぶ必要があります。
reloadDuration: number- リロードにかかる時間(秒)。
- 0 の場合は即時リロード(フレームをまたがずに即座に満タンになる)。
- 0 より小さい値が設定された場合は警告を出し、0 に補正します。
allowShootDuringReload: boolean- リロード中でも発射を許可するか。
- 通常は OFF を推奨(リロード中は撃てない状態にする)。
- ON の場合、リロード中でも
canShoot()が true になり得ます(残弾があれば)。
logDebugInfo: boolean- 弾消費やリロード開始・完了などのデバッグログをコンソールに出すかどうか。
- 開発中は ON、本番ビルドでは OFF を推奨。
これらのプロパティにより、
- 普通の FPS 風リロード(弾切れで自動リロード + リロード中は発射不可)
- ショットガン風の「手動リロードのみ」スタイル
- リロード時間ゼロで「弾数だけ制限する」シンプルな挙動
などを柔軟に表現できます。
TypeScriptコードの実装
以下が完成版の AmmoCounter.ts です。
import { _decorator, Component, clamp, warn, log } from 'cc';
const { ccclass, property } = _decorator;
/**
* AmmoCounter
* 弾数管理・発射可否判定・リロード処理を行う汎用コンポーネント。
* 他のノードやカスタムスクリプトには依存しません。
*
* 主な利用メソッド:
* - canShoot(): boolean
* - tryConsumeAmmo(amount?: number): boolean
* - startReload(force?: boolean): boolean
* - getCurrentAmmo(): number
* - getMaxAmmo(): number
* - isReloadingNow(): boolean
*/
@ccclass('AmmoCounter')
export class AmmoCounter extends Component {
@property({
tooltip: '最大装弾数(弾倉に入る弾の最大数)。1以上を推奨します。'
})
public maxAmmo: number = 10;
@property({
tooltip: '開始時に最大弾数でスタートするかどうか。ONなら currentAmmo = maxAmmo になります。'
})
public startWithFullAmmo: boolean = true;
@property({
tooltip: '開始時の弾数。startWithFullAmmo が OFF のときのみ有効です。0〜maxAmmo に自動クランプされます。'
})
public initialAmmo: number = 0;
@property({
tooltip: '弾切れ時に自動的にリロードを開始するかどうか。'
})
public autoReloadOnEmpty: boolean = true;
@property({
tooltip: 'リロードにかかる時間(秒)。0の場合は即時リロード。負の値は0に補正されます。'
})
public reloadDuration: number = 1.5;
@property({
tooltip: 'リロード中でも発射を許可するかどうか。通常はOFF(false)を推奨します。'
})
public allowShootDuringReload: boolean = false;
@property({
tooltip: '弾消費やリロード開始/完了などのデバッグログをコンソールに出力します。'
})
public logDebugInfo: boolean = false;
// ==== 内部状態 ====
/** 現在の残弾数 */
private _currentAmmo: number = 0;
/** 現在リロード中かどうか */
private _isReloading: boolean = false;
/** リロードが完了する時刻(秒)。リロード中のみ有効。 */
private _reloadEndTime: number = 0;
/** 経過時間(秒)を管理するための内部タイマー */
private _elapsedTime: number = 0;
// ==== ライフサイクル ====
onLoad() {
// プロパティのバリデーションと初期化
if (this.maxAmmo <= 0) {
warn('[AmmoCounter] maxAmmo が 1 以下です。1 に補正します。');
this.maxAmmo = 1;
}
if (this.reloadDuration < 0) {
warn('[AmmoCounter] reloadDuration が負の値です。0 に補正します。');
this.reloadDuration = 0;
}
if (this.startWithFullAmmo) {
this._currentAmmo = this.maxAmmo;
} else {
// initialAmmo を 0〜maxAmmo にクランプ
this._currentAmmo = clamp(this.initialAmmo, 0, this.maxAmmo);
}
this._isReloading = false;
this._reloadEndTime = 0;
this._elapsedTime = 0;
if (this.logDebugInfo) {
log(`[AmmoCounter] onLoad: currentAmmo=${this._currentAmmo}, maxAmmo=${this.maxAmmo}`);
}
}
update(deltaTime: number) {
// 経過時間を加算(リロード完了判定に使用)
this._elapsedTime += deltaTime;
// リロード中で、かつリロード完了時刻を過ぎていればリロード完了処理
if (this._isReloading && this._elapsedTime >= this._reloadEndTime) {
this._finishReload();
}
}
// ==== パブリックAPI(外部スクリプトから利用する想定) ====
/**
* 現在の残弾数を返します。
*/
public getCurrentAmmo(): number {
return this._currentAmmo;
}
/**
* 最大装弾数を返します。
*/
public getMaxAmmo(): number {
return this.maxAmmo;
}
/**
* 現在リロード中かどうかを返します。
*/
public isReloadingNow(): boolean {
return this._isReloading;
}
/**
* 現在発射可能かどうかを返します。
* - 残弾が1以上
* - リロード中でない、または allowShootDuringReload が true
*/
public canShoot(): boolean {
if (this._currentAmmo <= 0) {
return false;
}
if (!this.allowShootDuringReload && this._isReloading) {
return false;
}
return true;
}
/**
* 弾を消費しつつ、発射処理が可能かどうかを判定します。
* - amount: 消費する弾数(デフォルト1)
* - 戻り値: 消費に成功した場合 true(このタイミングで実際の発射処理を行う)
*
* 使用例:
* if (ammoCounter.tryConsumeAmmo()) {
* // 弾を1発消費できたので、ここで弾を発射する処理を書く
* }
*/
public tryConsumeAmmo(amount: number = 1): boolean {
if (amount <= 0) {
warn('[AmmoCounter] tryConsumeAmmo: amount は 1 以上を指定してください。');
return false;
}
if (!this.canShoot()) {
// 弾切れ or リロード中(かつ allowShootDuringReload=false)
if (this.logDebugInfo) {
log('[AmmoCounter] tryConsumeAmmo: 発射不可状態です。');
}
// 弾切れかつ自動リロードONで、まだリロード中でないならリロード開始
if (this._currentAmmo <= 0 && this.autoReloadOnEmpty && !this._isReloading) {
this.startReload();
}
return false;
}
if (this._currentAmmo < amount) {
// 要求弾数が残弾を超えている場合は発射不可
if (this.logDebugInfo) {
log(`[AmmoCounter] tryConsumeAmmo: 残弾不足 current=${this._currentAmmo}, requested=${amount}`);
}
return false;
}
this._currentAmmo -= amount;
if (this.logDebugInfo) {
log(`[AmmoCounter] 弾を ${amount} 発消費しました。残弾=${this._currentAmmo}/${this.maxAmmo}`);
}
// 弾を使い切った瞬間に自動リロードを開始
if (this._currentAmmo <= 0 && this.autoReloadOnEmpty && !this._isReloading) {
this.startReload();
}
return true;
}
/**
* リロードを開始します。
* - force が true の場合、既にリロード中でも強制的にリロードをやり直します。
* - 戻り値: リロードを開始/再開始できた場合 true、何も起きなかった場合 false。
*/
public startReload(force: boolean = false): boolean {
if (this._currentAmmo >= this.maxAmmo && !force) {
// すでに満タンならリロード不要
if (this.logDebugInfo) {
log('[AmmoCounter] startReload: 弾が満タンのためリロード不要です。');
}
return false;
}
if (this._isReloading && !force) {
// すでにリロード中なら何もしない
if (this.logDebugInfo) {
log('[AmmoCounter] startReload: すでにリロード中です。');
}
return false;
}
if (this.reloadDuration <= 0) {
// 即時リロード
this._currentAmmo = this.maxAmmo;
this._isReloading = false;
this._reloadEndTime = 0;
if (this.logDebugInfo) {
log(`[AmmoCounter] 即時リロード完了。残弾=${this._currentAmmo}/${this.maxAmmo}`);
}
return true;
}
// 時間のかかるリロードを開始
this._isReloading = true;
this._reloadEndTime = this._elapsedTime + this.reloadDuration;
if (this.logDebugInfo) {
log(`[AmmoCounter] リロード開始。完了まで ${this.reloadDuration.toFixed(2)} 秒`);
}
return true;
}
// ==== 内部処理 ====
/**
* リロード完了処理(内部用)。
*/
private _finishReload(): void {
this._isReloading = false;
this._currentAmmo = this.maxAmmo;
this._reloadEndTime = 0;
if (this.logDebugInfo) {
log(`[AmmoCounter] リロード完了。残弾=${this._currentAmmo}/${this.maxAmmo}`);
}
}
}
コードのポイント解説
- onLoad
maxAmmoとreloadDurationの値をバリデーションし、不正な値を補正します。startWithFullAmmoが ON の場合は最大弾数でスタート、OFF の場合はinitialAmmoを 0〜maxAmmoにクランプして設定します。
- update
- 内部タイマー
_elapsedTimeを加算し、リロード中であれば_reloadEndTimeを超えたタイミングで_finishReload()を呼び出します。 - これにより、ゲーム全体の時間管理に依存せず、このコンポーネント単体でリロード時間を管理できます。
- 内部タイマー
- canShoot()
- 残弾が 1 以上であり、かつ「リロード中でない」または
allowShootDuringReloadが true のときにのみ true を返します。 - UI やアニメーション側で「撃てる状態かどうか」をチェックするのに便利です。
- 残弾が 1 以上であり、かつ「リロード中でない」または
- tryConsumeAmmo()
- 「撃てるか?」と「弾を消費する」をセットで行うメソッドです。
- 成功したときだけ true を返すので、そのタイミングで弾プレハブの生成などを行えば、ロジックが明確になります。
- 弾切れかつ
autoReloadOnEmptyが ON の場合、ここで自動的にstartReload()を呼びます。
- startReload()
- 手動リロード用のメソッドです。R キーなどの入力に応じて呼び出すことを想定しています。
reloadDurationが 0 以下なら即時リロード、それ以外なら内部タイマーで完了時刻を管理します。force引数を true にすると、「満タンでもリロードし直したい」「リロード中だがやり直したい」といったケースに対応できます。
使用手順と動作確認
1. スクリプトファイルの作成
- エディタの Assets パネルで、任意のフォルダ(例:
assets/scripts)を右クリックします。 - Create → TypeScript を選択し、ファイル名を
AmmoCounter.tsとします。 - 自動生成されたファイルをダブルクリックして開き、内容をすべて削除して、上記の
AmmoCounterコードをそのまま貼り付けて保存します。
2. テスト用ノードの作成
- Hierarchy パネルで右クリックし、Create → 3D Object → Node などからテスト用ノードを作成します。
- 名前は
PlayerGunやTestGunなど、分かりやすいものにするとよいでしょう。
- 名前は
このノードは見た目を持っている必要はなく、「弾数管理のロジックを持つオブジェクト」として機能します。
3. AmmoCounter コンポーネントのアタッチ
- 作成したノード(例:
PlayerGun)を選択します。 - Inspector パネルの下部にある Add Component ボタンをクリックします。
- Custom カテゴリから AmmoCounter を選択してアタッチします。
4. プロパティの設定例
Inspector で AmmoCounter の各プロパティを設定します。まずは分かりやすい FPS 風の設定例から。
- 例1:一般的な自動リロード付きの銃
maxAmmo: 30startWithFullAmmo: ONinitialAmmo: 0(この例では無視されます)autoReloadOnEmpty: ONreloadDuration: 2.0(2秒でリロード完了)allowShootDuringReload: OFFlogDebugInfo: ON(動作確認中はON推奨)
- 例2:手動リロードのみのショットガン風
maxAmmo: 8startWithFullAmmo: ONautoReloadOnEmpty: OFF(弾切れでも自動リロードしない)reloadDuration: 1.0allowShootDuringReload: OFFlogDebugInfo: お好みで
- 例3:リロード時間ゼロで「弾制限のみ」
maxAmmo: 5reloadDuration: 0- リロードは即時完了するので、
startReload()を呼べばその場で弾が満タンになります。
5. 簡単な動作確認(デバッグログで確認)
スクリプト単体の挙動を確認するために、簡単なテストスクリプトを追加しても良いですが、ここでは logDebugInfo のログだけで確認する方法を紹介します。
AmmoCounterの logDebugInfo を ON にします。- ゲームを再生(Play ボタン)します。
- コンソール(Console パネル)に以下のようなログが出ることを確認します。
[AmmoCounter] onLoad: currentAmmo=30, maxAmmo=30など。
より実用的なテストとして、「スペースキーで撃つ」「R キーでリロードする」簡易スクリプトを同じノードに追加することもできます(このガイドでは AmmoCounter 自体は他スクリプトに依存しないので、テストスクリプトは任意です)。
// 参考: 簡易テスト用スクリプト例(任意)
// このスクリプトは AmmoCounter に依存しますが、AmmoCounter 自体は完全に独立しています。
import { _decorator, Component, input, Input, EventKeyboard, KeyCode } from 'cc';
import { AmmoCounter } from './AmmoCounter';
const { ccclass } = _decorator;
@ccclass('AmmoTestController')
export class AmmoTestController extends Component {
private ammo!: AmmoCounter;
onLoad() {
this.ammo = this.getComponent(AmmoCounter)!;
input.on(Input.EventType.KEY_DOWN, this.onKeyDown, this);
}
onDestroy() {
input.off(Input.EventType.KEY_DOWN, this.onKeyDown, this);
}
private onKeyDown(event: EventKeyboard) {
if (event.keyCode === KeyCode.SPACE) {
if (this.ammo.tryConsumeAmmo()) {
console.log('Bang! 残弾:', this.ammo.getCurrentAmmo());
} else {
console.log('撃てません。残弾:', this.ammo.getCurrentAmmo(), 'リロード中:', this.ammo.isReloadingNow());
}
} else if (event.keyCode === KeyCode.KEY_R) {
this.ammo.startReload();
}
}
}
このテストスクリプトを同じノードに付けておけば、
- スペースキー:撃つ(弾を消費)
- R キー:リロード開始
という挙動をコンソールログで簡単に確認できます。
まとめ
この AmmoCounter コンポーネントを使うことで、
- 「今撃てるか?」「残弾はいくつか?」「リロード中か?」といった状態管理を 1 コンポーネントに集約できる。
- 弾の見た目や発射処理、UI やサウンドなどはそれぞれ独立したスクリプトに分離しやすくなる。
- 外部の GameManager やシングルトンに依存しないため、どのノードにも簡単に再利用できる。
- インスペクタのプロパティを変えるだけで、銃ごとの弾数・リロード時間・自動リロードの有無などを簡単に調整できる。
このような「状態管理専用の汎用コンポーネント」を積み重ねていくと、プロジェクト全体の見通しが良くなり、機能の差し替えや再利用が非常にしやすくなります。
今回の AmmoCounter をベースに、例えば「リロード完了時にイベントを飛ばす」「残弾が閾値以下になったら警告を出す」など、プロジェクトに合わせた拡張も容易に行えるはずです。
まずはこのコンポーネントをゲーム内の武器ノードにアタッチし、発射ロジック側から tryConsumeAmmo() と startReload() を呼ぶ構成にしてみてください。弾数まわりのロジックがすっきり整理されるのを実感できるはずです。




