【Cocos Creator 3.8】ComboCounterの実装:アタッチするだけで「連続ヒット数の計測&自動リセット」を実現する汎用スクリプト
このコンポーネントは、「攻撃が連続で当たった数(コンボ数)」をカウントし、一定時間攻撃が途切れると自動でリセットしてくれる汎用カウンタです。
攻撃判定やダメージ処理とは一切分離されており、「攻撃が当たったタイミングでメソッドを呼ぶだけ」でコンボ管理を実現できます。
スコア演出やコンボ表示UIなど、さまざまなゲームで再利用しやすいよう、どのノードにもアタッチして単体で完結する設計になっています。
コンポーネントの設計方針
1. 要件整理
- 攻撃がヒットするたびにコンボ数をインクリメントする。
- 最後にヒットしてから一定時間が経過したら、コンボ数を0にリセットする。
- 外部のGameManagerやUIコンポーネントに依存せず、このスクリプト単体で動作する。
- 攻撃判定側からは「ヒット時にメソッドを呼ぶ」だけで利用できるようにする。
- 必要に応じて、コンボ数やリセット発生を他のスクリプトから参照できるようにする(getterや簡易イベントコールバック)。
2. 外部依存をなくすためのアプローチ
- 攻撃判定(Colliderなど)をこのコンポーネントに含めない:
- どんな攻撃システムでも使えるように、
registerHit()のようなメソッドを公開し、ヒット時に呼んでもらうだけにする。
- どんな攻撃システムでも使えるように、
- UI更新なども一切行わない:
- テキスト表示やアニメーションなどは別コンポーネントに任せ、このコンポーネントは「コンボの状態管理」に専念する。
- Inspector から挙動を細かく調整できるようにする:
- コンボ継続時間(秒)
- コンボの最大値(上限)
- コンボが変化したときにログを出すかどうか
- コンボリセット時の自動ログ
3. インスペクタで設定可能なプロパティ設計
以下のプロパティを用意します。
comboTimeout(number)- 説明: 「最後のヒットから何秒間攻撃が無いとコンボをリセットするか」を秒数で指定。
- 例:
2.0と設定すると、最後のヒットから2秒経過でコンボが0に戻る。
maxCombo(number)- 説明: コンボ数の上限。0以下なら「上限なし」として扱う。
- 例:
999と設定すると、コンボは999で止まり、それ以上は増えない。
startFromOne(boolean)- 説明: 最初のヒットでコンボを1から始めるかどうか。
true: 初期値0 → 1, 2, 3, …false: 初期値0 → 0, 1, 2, 3, … (特殊なカウント方法をしたい場合用)
autoLogOnChange(boolean)- 説明: コンボ数が変化したときに
console.logでログを出すかどうか。 - デバッグ用。リリースでは
falseにする想定。
- 説明: コンボ数が変化したときに
autoLogOnReset(boolean)- 説明: コンボがタイムアウトなどでリセットされたときにログを出すかどうか。
resetOnDisable(boolean)- 説明: このコンポーネントのノードが
disableになったときにコンボをリセットするかどうか。 - シーン切り替えや一時停止などのタイミングで状態をクリアしたい場合に便利。
- 説明: このコンポーネントのノードが
4. 外部から利用するための公開メソッド
攻撃側のスクリプトから呼び出す想定のメソッドを用意します。
registerHit(): void- 攻撃がヒットしたタイミングで呼び出す。
- 内部でコンボ数を増加し、タイムアウト用のタイマーをリセットする。
resetCombo(): void- 強制的にコンボを0に戻す。
- 敵のガードや被弾など、任意の条件でコンボを切りたいときに使用できる。
getCurrentCombo(): number- 現在のコンボ数を取得する。
- UI表示など、他のコンポーネントから参照するときに使う。
getTimeSinceLastHit(): number- 最後のヒットから何秒経過したかを取得する。
- ゲージ演出などに利用可能。
なお、Cocos Creator の標準イベントシステム(EventTarget)を使ったイベント発火も検討できますが、外部スクリプトへの依存を増やさず、この記事では「シンプルなメソッドとプロパティのみ」で完結する形にしています。
TypeScriptコードの実装
以下が完成した ComboCounter.ts の全コードです。
import { _decorator, Component, game } from 'cc';
const { ccclass, property } = _decorator;
/**
* ComboCounter
*
* 連続ヒット数(コンボ)を管理する汎用コンポーネント。
*
* 使用方法:
* - 攻撃がヒットしたタイミングで、このコンポーネントの registerHit() を呼び出す。
* - コンボ数は getCurrentCombo() で取得可能。
* - 一定時間ヒットが無いと、自動的にコンボがリセットされる。
*/
@ccclass('ComboCounter')
export class ComboCounter extends Component {
@property({
tooltip: '最後のヒットから何秒間ヒットが無いとコンボをリセットするか(秒)。\n0 以下を指定すると、自動リセットは行われません。'
})
public comboTimeout: number = 2.0;
@property({
tooltip: 'コンボ数の上限。\n0 以下を指定すると上限なしとして扱います。'
})
public maxCombo: number = 0;
@property({
tooltip: '最初のヒットでコンボを 1 から開始するかどうか。\nON: 0 → 1 → 2 → 3...\nOFF: 0 → 0 → 1 → 2...(特殊なカウント用途向け)'
})
public startFromOne: boolean = true;
@property({
tooltip: 'コンボ数が変化したときにコンソールへログ出力するかどうか(デバッグ用)。'
})
public autoLogOnChange: boolean = false;
@property({
tooltip: 'コンボがリセットされたときにコンソールへログ出力するかどうか(デバッグ用)。'
})
public autoLogOnReset: boolean = false;
@property({
tooltip: 'このコンポーネントを持つノードが無効化されたときに、コンボを自動リセットするかどうか。'
})
public resetOnDisable: boolean = true;
// 現在のコンボ数
private _currentCombo: number = 0;
// 最後のヒットが発生したゲーム内時間(秒)
private _lastHitTime: number = 0;
// 前フレーム時点のコンボ数(変化検知用)
private _previousCombo: number = 0;
// タイムアウトによるリセットが有効かどうか
private get _timeoutEnabled(): boolean {
return this.comboTimeout > 0;
}
onLoad() {
// 初期状態の明示
this._currentCombo = 0;
this._previousCombo = 0;
this._lastHitTime = 0;
}
start() {
// 特に必須の標準コンポーネントはないため、ここでは何もしない。
// 他のスクリプトからは、start 以降に registerHit() を呼ぶようにしてください。
}
update(deltaTime: number) {
// 自動リセットが有効で、現在コンボが 1 以上のときだけ監視する
if (!this._timeoutEnabled || this._currentCombo <= 0) {
return;
}
const now = game.totalTime / 1000.0; // ミリ秒 → 秒
const elapsed = now - this._lastHitTime;
if (elapsed >= this.comboTimeout) {
this._resetInternal('timeout');
}
}
onEnable() {
// 有効化時に特別な処理は不要だが、
// 必要であればここで状態を初期化することもできる。
}
onDisable() {
// ノードが無効化されたときの挙動
if (this.resetOnDisable) {
this._resetInternal('disabled');
}
}
/**
* 攻撃がヒットしたときに呼び出してください。
* コンボ数が増加し、タイムアウトタイマーがリセットされます。
*/
public registerHit(): void {
// 現在時刻を記録
const now = game.totalTime / 1000.0;
this._lastHitTime = now;
// コンボ数の更新
if (this.startFromOne && this._currentCombo === 0) {
// 初回ヒット時に 1 からスタート
this._currentCombo = 1;
} else {
this._currentCombo += 1;
}
// 上限が設定されている場合はクランプ
if (this.maxCombo > 0 && this._currentCombo > this.maxCombo) {
this._currentCombo = this.maxCombo;
}
// 変化があればログ出力
if (this.autoLogOnChange && this._currentCombo !== this._previousCombo) {
console.log(`[ComboCounter] Combo changed: ${this._previousCombo} → ${this._currentCombo}`);
}
// 前回値を更新
this._previousCombo = this._currentCombo;
}
/**
* 外部から明示的にコンボをリセットしたい場合に呼び出します。
*/
public resetCombo(): void {
this._resetInternal('manual');
}
/**
* 現在のコンボ数を取得します。
*/
public getCurrentCombo(): number {
return this._currentCombo;
}
/**
* 最後のヒットから何秒経過したかを取得します。
* まだ一度もヒットしていない場合は 0 を返します。
*/
public getTimeSinceLastHit(): number {
if (this._lastHitTime === 0) {
return 0;
}
const now = game.totalTime / 1000.0;
return now - this._lastHitTime;
}
/**
* 内部用のリセット処理。
* reason はログ出力のための情報で、挙動には影響しません。
*/
private _resetInternal(reason: 'timeout' | 'manual' | 'disabled'): void {
if (this._currentCombo === 0) {
// すでに 0 の場合は何もしない
return;
}
const previous = this._currentCombo;
this._currentCombo = 0;
this._lastHitTime = 0;
if (this.autoLogOnReset) {
console.log(`[ComboCounter] Combo reset (${reason}). Previous combo: ${previous}`);
}
// 変化ログ(autoLogOnChange が有効な場合)
if (this.autoLogOnChange && previous !== this._currentCombo) {
console.log(`[ComboCounter] Combo changed: ${previous} → ${this._currentCombo}`);
}
this._previousCombo = this._currentCombo;
}
}
コードのポイント解説
onLoad- 内部状態(コンボ数・最後のヒット時間)を初期化しています。
- 他コンポーネントへの依存は一切なく、このクラス単体で完結しています。
update(deltaTime)comboTimeoutが0より大きく、かつコンボが1以上のときだけ経過時間を監視します。game.totalTimeを秒に変換して使用し、最後のヒットからの経過時間を計算しています。- 経過時間が
comboTimeout以上になったら、_resetInternal('timeout')で自動リセットします。
registerHit()- 攻撃がヒットしたタイミングで外部から呼び出すメソッドです。
- 内部で現在時刻を記録し、コンボ数をインクリメントします。
startFromOneがtrueかつ現在0なら、最初のヒットで1から開始します。maxComboが設定されている場合は、その値を超えないようにクランプします。autoLogOnChangeが有効なら、コンボ変化をログ出力します。
resetCombo()/_resetInternal()resetCombo()は外部からコンボを明示的に0にしたいときに使うメソッドです。- 実際の処理は
_resetInternal()に集約し、タイムアウト・無効化・手動リセットのどの場合でも同じロジックを使います。 autoLogOnResetが有効な場合、リセット時にログを出力します。
getCurrentCombo()/getTimeSinceLastHit()- 他コンポーネントから安全に状態を参照できるようにした 読み取り専用のAPI です。
- UI表示やスコア計算など、さまざまな用途から呼び出せます。
onDisable()resetOnDisableがtrueのとき、ノードが無効化されたタイミングでコンボをリセットします。- シーン切り替えやポーズメニュー遷移などで、不要なコンボが残らないようにするための防御的実装です。
使用手順と動作確認
1. スクリプトの作成
- Assets パネルで右クリックします。
- Create → TypeScript を選択します。
- 新しく作成されたスクリプトファイルの名前を
ComboCounter.tsに変更します。 ComboCounter.tsをダブルクリックしてエディタ(VS Code など)で開き、
中身をすべて削除して、前述の TypeScriptコード全体 をそのまま貼り付けて保存します。
2. テスト用ノードの作成とアタッチ
- Hierarchy パネルで右クリックし、Create → Empty Node を選択して空のノードを作成します。
- 分かりやすいように、ノード名を
ComboTesterなどに変更します。 ComboTesterノードを選択し、Inspector を確認します。- Inspector の下部にある Add Component ボタンをクリックします。
- Custom Component → ComboCounter を選択して、このノードにコンポーネントをアタッチします。
3. Inspector でプロパティを設定
ComboTester ノードを選択した状態で、Inspector に表示される ComboCounter コンポーネントのプロパティを設定します。
- Combo Timeout(
comboTimeout)- 例:
2.0(2秒間ヒットがなければコンボリセット)
- 例:
- Max Combo(
maxCombo)- 例:
50(50コンボまでカウント) - 上限を気にしない場合は
0のままでOKです。
- 例:
- Start From One(
startFromOne)- 通常のコンボ表示なら チェックON(初期値のままでOK)。
- Auto Log On Change(
autoLogOnChange)- デバッグ中は チェックON にすると、コンボ変化がコンソールに表示されて便利です。
- Auto Log On Reset(
autoLogOnReset)- リセットのタイミングを確認したい場合は チェックON にします。
- Reset On Disable(
resetOnDisable)- 基本的には チェックON のままで問題ありません。
4. 簡単な動作確認(コードから registerHit を呼ぶ)
実際のゲームでは「攻撃判定が敵に当たったとき」に registerHit() を呼びますが、
ここではテスト用に、キーボード入力でコンボを増やす簡単なスクリプトを作って確認してみます。
- Assets パネルで右クリック → Create → TypeScript を選択し、ファイル名を
ComboTestInput.tsにします。 - 以下のコードを
ComboTestInput.tsに貼り付けます。
import { _decorator, Component, input, Input, EventKeyboard, KeyCode } from 'cc';
import { ComboCounter } from './ComboCounter';
const { ccclass, property } = _decorator;
/**
* ComboTestInput
*
* キーボード入力で ComboCounter.registerHit() をテストするための簡易スクリプト。
* このスクリプトはデバッグ用であり、本記事の ComboCounter 本体とは独立しています。
*/
@ccclass('ComboTestInput')
export class ComboTestInput extends Component {
@property({
type: ComboCounter,
tooltip: '同じノード、または任意のノードにアタッチされた ComboCounter を指定します。'
})
public comboCounter: ComboCounter | null = null;
onLoad() {
input.on(Input.EventType.KEY_DOWN, this.onKeyDown, this);
if (!this.comboCounter) {
// 同じノードに付いている ComboCounter を自動取得してみる
const found = this.getComponent(ComboCounter);
if (!found) {
console.warn('[ComboTestInput] ComboCounter が指定されていません。同じノードに ComboCounter をアタッチするか、Inspector で参照を設定してください。');
} else {
this.comboCounter = found;
}
}
}
onDestroy() {
input.off(Input.EventType.KEY_DOWN, this.onKeyDown, this);
}
private onKeyDown(event: EventKeyboard) {
if (!this.comboCounter) {
return;
}
// スペースキーでヒットを疑似的に発生させる
if (event.keyCode === KeyCode.SPACE) {
this.comboCounter.registerHit();
}
// R キーでコンボを手動リセット
if (event.keyCode === KeyCode.KEY_R) {
this.comboCounter.resetCombo();
}
}
}
次に、このテストスクリプトを使います。
- Hierarchy で先ほど作成した
ComboTesterノードを選択します。 - Inspector で Add Component → Custom → ComboTestInput を追加します。
ComboTestInputの Combo Counter プロパティには、同じノードにアタッチされている ComboCounter をドラッグ&ドロップで設定するか、空のままにしておきます(空の場合はonLoadで自動取得を試みます)。- エディタ右上の Play ボタンでゲームを実行します。
- ゲームビューが表示されたら、以下の操作で動作を確認します。
- スペースキーを押すたびに
registerHit()が呼ばれ、コンボが増加します。 - コンボ変化ログ(
autoLogOnChangeON)とリセットログ(autoLogOnResetON)がコンソールに表示されます。 - 最後にスペースキーを押してから comboTimeout で設定した秒数 放置すると、自動でコンボが 0 に戻ります。
- Rキーを押すと、手動でコンボがリセットされます。
- スペースキーを押すたびに
このテスト用スクリプトはあくまで動作確認のための例であり、本番ゲームでは攻撃判定のスクリプトから直接 registerHit() を呼び出す設計に置き換えてください。
まとめ
- ComboCounter は、連続ヒット数(コンボ)の管理に特化した、完全に独立した汎用コンポーネントです。
- 攻撃システムやUIとは一切結合していないため、
- 攻撃判定がヒットしたときに
registerHit()を呼ぶ - UI表示側から
getCurrentCombo()を読む
といったシンプルな連携で、さまざまなゲームに簡単に組み込めます。
- 攻撃判定がヒットしたときに
comboTimeoutやmaxComboなどのパラメータを Inspector から調整できるため、- 短時間で切れるシビアなコンボ
- 長く続く爽快感重視のコンボ
など、ゲーム性に合わせたチューニングが容易です。
- 外部の GameManager やシングルトンに依存しない設計なので、プロジェクト間のコピペ移植も簡単で、再利用性が高いのも特徴です。
このコンポーネントをベースに、コンボ数に応じたダメージ倍率や、コンボが一定値を超えたときの演出トリガーなどを追加していけば、よりリッチなバトルシステムを効率よく構築できます。
まずはこの記事の ComboCounter をそのまま導入し、自分のプロジェクトに合わせてカスタマイズしてみてください。




