【Cocos Creator 3.8】StunEffectの実装:アタッチするだけで「スタン状態で物理挙動を一時停止し、頭上にピヨりマークを表示する」汎用スクリプト
このガイドでは、任意のキャラクターノードにアタッチするだけで、一定時間だけ物理挙動を止めて(スタン状態)、頭上にピヨりマークを表示する汎用コンポーネント StunEffect を実装します。
外部の GameManager やシングルトンには一切依存せず、このスクリプト単体で完結するように設計します。スタン時間やピヨりマークの見た目・位置はすべてインスペクタから調整できます。
コンポーネントの設計方針
機能要件の整理
- このコンポーネントをアタッチしたノード(親)に対して、スタン状態を付与できる。
- スタン中は、以下のような「物理挙動」を一時停止する:
RigidBody2D/RigidBodyがあれば、速度を 0 にしてシミュレーションを停止。- 物理ボディが無い場合でも、最低限として
Nodeの移動を止めるために「スタン中は update 内で位置を固定」する。
- スタン中はノードの頭上に「ピヨりマーク」を表示し、スタン終了時に自動で消す。
- スタンの開始・終了は以下のメソッドで制御:
stun(duration?: number): void… 指定秒数スタン。引数省略時はデフォルト時間。clearStun(): void… 即座にスタン解除。isStunned(): boolean… 現在スタン中かどうか。
- 外部スクリプトに依存せず、インスペクタのプロパティだけで設定できる。
- ピヨりマークは
PrefabまたはNodeを指定できるようにする。
外部依存をなくすためのアプローチ
- 物理制御は、存在する場合のみ
RigidBody2D/RigidBodyを取得し、無ければ「最低限のスタン」だけ行う。 - ピヨりマークも、インスペクタから
Prefabまたは既存のNodeをアサインする方式にし、自動生成や他スクリプトへの問い合わせは行わない。 - 必須ではないコンポーネントは、
getComponentで取得を試みて、存在しなければconsole.warnでログを出すだけにする。
インスペクタで設定可能なプロパティ
以下のようなプロパティを用意します。
- defaultStunDuration: number
- デフォルトのスタン時間(秒)。
stun()の引数を省略したときに使われる。- 例:1.0~3.0 秒程度。
- allowOverrideDuration: boolean
stun(duration)で引数を渡したときに、その値を許可するかどうか。falseの場合、常にdefaultStunDurationが使われる。
- freezePhysics2D: boolean
RigidBody2Dを持つ場合に、スタン中は物理シミュレーションを停止するかどうか。- ON の場合:スタン開始時に速度を 0 にし、
enabled = falseで一時停止、解除時に元の状態へ戻す。
- freezePhysics3D: boolean
RigidBody(3D)に対して同様に制御するかどうか。
- freezeTransform: boolean
- 物理ボディがない場合でも、スタン中はノードの位置を固定するかどうか。
- ON の場合:スタン開始時の位置を記録し、
updateでその位置に戻す。
- stunIconPrefab: Prefab | null
- 頭上に表示するピヨりマークの
Prefab。 - 未設定でも動作はするが、スタン中の視覚効果は出ない(ログで警告)。
- 頭上に表示するピヨりマークの
- stunIconNode: Node | null
- 既にシーン上に存在するピヨりマーク用ノードを直接指定したい場合に使用。
stunIconPrefabよりも優先される。- スタン中のみ
active = trueにし、解除時にactive = falseに戻す。
- iconOffsetY: number
- ピヨりマークをノードの頭上にどれだけオフセットして表示するか(ローカルY座標)。
- 例:1.5 ~ 2.0 など。
- destroyIconOnEnd: boolean
- スタン終了時に、生成したアイコンを破棄するかどうか。
- ON:毎回インスタンスを生成・破棄する。
- OFF:生成したアイコンを再利用し、
activeの切り替えだけ行う。
- autoStunOnStart: boolean
- ゲーム開始時(
start())に自動的にスタンさせるかどうか(テスト用にも便利)。
- ゲーム開始時(
- autoStunDuration: number
autoStunOnStartが ON のときに使うスタン時間。
TypeScriptコードの実装
以下が完成した StunEffect.ts の全コードです。
import {
_decorator,
Component,
Node,
Prefab,
instantiate,
Vec3,
RigidBody2D,
RigidBody,
v3,
} from 'cc';
const { ccclass, property } = _decorator;
@ccclass('StunEffect')
export class StunEffect extends Component {
@property({
tooltip: 'stun() の引数を省略したときに使われるデフォルトのスタン時間(秒)。'
})
public defaultStunDuration: number = 2.0;
@property({
tooltip: 'stun(duration) で渡された時間を使用するかどうか。false の場合は常に defaultStunDuration が使用されます。'
})
public allowOverrideDuration: boolean = true;
@property({
tooltip: '2D物理 (RigidBody2D) を持つ場合、スタン中に物理シミュレーションを停止するかどうか。'
})
public freezePhysics2D: boolean = true;
@property({
tooltip: '3D物理 (RigidBody) を持つ場合、スタン中に物理シミュレーションを停止するかどうか。'
})
public freezePhysics3D: boolean = true;
@property({
tooltip: '物理ボディが無い場合でも、スタン中はノードの位置を固定するかどうか。'
})
public freezeTransform: boolean = true;
@property({
type: Prefab,
tooltip: '頭上に表示するピヨりマークの Prefab。未設定の場合は stunIconNode が使用されます。'
})
public stunIconPrefab: Prefab | null = null;
@property({
type: Node,
tooltip: '既にシーン上に存在するピヨりマーク用ノード。設定されている場合はこちらが優先されます。'
})
public stunIconNode: Node | null = null;
@property({
tooltip: 'ピヨりマークを親ノードの頭上に表示するためのローカルYオフセット。'
})
public iconOffsetY: number = 1.5;
@property({
tooltip: 'スタン終了時に生成したピヨりマークを破棄するかどうか。false の場合は非表示にして再利用します。'
})
public destroyIconOnEnd: boolean = false;
@property({
tooltip: 'ゲーム開始時 (start) に自動的にスタンさせるかどうか(テスト用)。'
})
public autoStunOnStart: boolean = false;
@property({
tooltip: 'autoStunOnStart が true のときに使用されるスタン時間(秒)。'
})
public autoStunDuration: number = 1.0;
// 内部状態
private _isStunned: boolean = false;
private _stunTimer: number = 0;
// 物理関連のキャッシュ
private _rb2d: RigidBody2D | null = null;
private _rb3d: RigidBody | null = null;
private _rb2dWasEnabled: boolean = false;
private _rb3dWasEnabled: boolean = false;
// 位置固定用
private _frozenPosition: Vec3 | null = null;
// アイコン管理用
private _iconInstance: Node | null = null;
private _iconInitialParent: Node | null = null;
onLoad() {
// 可能なら物理ボディを取得(存在しない場合は null のまま)
this._rb2d = this.getComponent(RigidBody2D);
this._rb3d = this.getComponent(RigidBody);
if (!this._rb2d && !this._rb3d) {
console.warn(
`[StunEffect] Node "${this.node.name}" に RigidBody2D / RigidBody が見つかりません。` +
'freezeTransform を有効にしている場合は、スタン中に位置のみ固定されます。'
);
}
// 既存の stunIconNode が指定されていれば、最初は非表示にしておく
if (this.stunIconNode) {
this._iconInstance = this.stunIconNode;
this._iconInitialParent = this.stunIconNode.parent;
this.stunIconNode.active = false;
}
}
start() {
// 自動スタンが有効なら開始時にスタンをかける
if (this.autoStunOnStart) {
const duration = this.autoStunDuration > 0 ? this.autoStunDuration : this.defaultStunDuration;
this.stun(duration);
}
}
update(deltaTime: number) {
if (!this._isStunned) {
return;
}
// タイマー更新
this._stunTimer -= deltaTime;
if (this._stunTimer <= 0) {
this.clearStun();
return;
}
// 位置固定が有効なら、毎フレーム位置を固定
if (this.freezeTransform && this._frozenPosition) {
this.node.setPosition(this._frozenPosition);
}
// アイコンが親ノードの頭上に追従するように位置を更新
if (this._iconInstance && this._iconInstance.parent === this.node) {
const pos = this.node.position;
this._iconInstance.setPosition(pos.x, pos.y + this.iconOffsetY, pos.z);
}
}
/**
* 指定秒数だけスタン状態にする。
* duration を省略した場合は defaultStunDuration が使用されます。
*/
public stun(duration?: number): void {
let stunDuration = this.defaultStunDuration;
if (this.allowOverrideDuration && duration !== undefined && duration > 0) {
stunDuration = duration;
}
if (stunDuration <= 0) {
console.warn('[StunEffect] スタン時間が 0 以下のため、stun() は無視されました。');
return;
}
// すでにスタン中の場合は、タイマーだけ延長する
if (this._isStunned) {
this._stunTimer = stunDuration;
return;
}
this._isStunned = true;
this._stunTimer = stunDuration;
// 位置固定用の座標を記録
if (this.freezeTransform) {
this._frozenPosition = v3(this.node.position);
} else {
this._frozenPosition = null;
}
// 2D物理の停止
if (this.freezePhysics2D && this._rb2d) {
this._rb2dWasEnabled = this._rb2d.enabled;
// 速度をリセットしてから無効化
this._rb2d.linearVelocity = v3(0, 0, 0);
this._rb2d.angularVelocity = 0;
this._rb2d.enabled = false;
}
// 3D物理の停止
if (this.freezePhysics3D && this._rb3d) {
this._rb3dWasEnabled = this._rb3d.enabled;
this._rb3d.setLinearVelocity(v3(0, 0, 0));
this._rb3d.setAngularVelocity(v3(0, 0, 0));
this._rb3d.enabled = false;
}
// ピヨりマークの表示
this.showStunIcon();
}
/**
* 即座にスタン状態を解除します。
*/
public clearStun(): void {
if (!this._isStunned) {
return;
}
this._isStunned = false;
this._stunTimer = 0;
// 位置固定を解除
this._frozenPosition = null;
// 2D物理を元に戻す
if (this.freezePhysics2D && this._rb2d) {
this._rb2d.enabled = this._rb2dWasEnabled;
}
// 3D物理を元に戻す
if (this.freezePhysics3D && this._rb3d) {
this._rb3d.enabled = this._rb3dWasEnabled;
}
// ピヨりマークを非表示または破棄
this.hideStunIcon();
}
/**
* 現在スタン中かどうかを返します。
*/
public isStunned(): boolean {
return this._isStunned;
}
// ピヨりマーク表示処理
private showStunIcon(): void {
// 既存ノードが指定されている場合はそれを使用
if (this._iconInstance) {
this.attachIconToOwner();
this._iconInstance.active = true;
return;
}
// Prefab が設定されていない場合は警告を出して終了
if (!this.stunIconPrefab) {
console.warn(
`[StunEffect] Node "${this.node.name}" に stunIconPrefab / stunIconNode が設定されていないため、` +
'スタン中のピヨりマークは表示されません。'
);
return;
}
// Prefab からインスタンスを生成
const icon = instantiate(this.stunIconPrefab);
if (!icon) {
console.error('[StunEffect] stunIconPrefab のインスタンス生成に失敗しました。');
return;
}
this._iconInstance = icon;
this._iconInitialParent = icon.parent;
this.attachIconToOwner();
this._iconInstance.active = true;
}
// ピヨりマーク非表示処理
private hideStunIcon(): void {
if (!this._iconInstance) {
return;
}
if (this.destroyIconOnEnd) {
// 完全に破棄
this._iconInstance.destroy();
this._iconInstance = null;
this._iconInitialParent = null;
} else {
// 非表示にして、元の親に戻す(存在する場合)
this._iconInstance.active = false;
if (this._iconInitialParent && this._iconInstance.parent !== this._iconInitialParent) {
this._iconInstance.parent = this._iconInitialParent;
}
}
}
// ピヨりマークを親ノードの頭上にアタッチ
private attachIconToOwner(): void {
if (!this._iconInstance) {
return;
}
// 親をこのコンポーネントのノードに変更
this._iconInstance.parent = this.node;
const pos = this.node.position;
this._iconInstance.setPosition(pos.x, pos.y + this.iconOffsetY, pos.z);
}
}
コードの主要ポイント解説
onLoad()RigidBody2D/RigidBodyを取得し、存在しない場合はconsole.warnで通知。- インスペクタで
stunIconNodeが指定されていれば、それを内部インスタンスとしてキャッシュし、最初は非表示にする。
start()autoStunOnStartが ON の場合、自動でstun()を呼び出してスタンテストを行う。
update(deltaTime)- スタンタイマーを減算し、0 以下になったら
clearStun()で解除。 freezeTransformが ON の場合は、毎フレーム位置を固定。- ピヨりマークが親ノードに追従するよう、頭上位置を毎フレーム更新。
- スタンタイマーを減算し、0 以下になったら
stun(duration?)- スタン時間を決定し、すでにスタン中ならタイマーだけ延長。
- 初回スタン時は、物理ボディの
enabled状態を保存し、速度を 0 にしてから無効化。 - 位置固定用に現在座標を保存。
showStunIcon()でピヨりマークを表示。
clearStun()- スタンフラグとタイマーをリセットし、位置固定を解除。
- 保存しておいた
enabled状態を使って物理ボディを元に戻す。 hideStunIcon()でアイコンを非表示または破棄。
- アイコン関連メソッド
showStunIcon():既存ノード優先 → Prefab インスタンス生成 → 警告ログの順で処理。hideStunIcon():destroyIconOnEndに応じて破棄 or 非表示+元親へ戻す。attachIconToOwner():親ノードをthis.nodeに変更し、頭上座標に配置。
使用手順と動作確認
1. スクリプトファイルの作成
- Assets パネルで右クリック → Create → TypeScript を選択します。
- ファイル名を
StunEffect.tsにします。 - 自動生成されたファイルを開き、内容をすべて削除して、前節の TypeScript コードを丸ごと貼り付けて保存します。
2. ピヨりマーク用 Prefab の用意(推奨)
すでにピヨりマーク用のノードがある場合はこの手順はスキップして構いません。
- Hierarchy パネルで右クリック → Create → 2D Object → Sprite などで、新しいノードを作成します。
- Inspector で Sprite コンポーネントの SpriteFrame に、ピヨりマーク用の画像を設定します。
- サイズや位置を調整し、ピヨりマークとしてちょうど良い見た目にします。
- 作成したノードを Assets パネルにドラッグ&ドロップして Prefab にします(例:
StunIcon.prefab)。 - Hierarchy 上の元ノードは不要であれば削除して構いません。
3. テスト用キャラクターノードの作成
- Hierarchy パネルで右クリック → Create → 2D Object → Sprite を選択し、テスト用のキャラクターノードを作成します。
- 名前の例:
TestCharacter
- 名前の例:
- 物理挙動を試したい場合は、
TestCharacterを選択した状態で Inspector の Add Component から- Physics 2D → RigidBody2D を追加(2D の場合)
- または Physics → RigidBody を追加(3D の場合)
- 2D 物理の場合は、さらに Collider2D(BoxCollider2D など)も追加しておくと、物理挙動が確認しやすくなります。
4. StunEffect コンポーネントをアタッチ
- Hierarchy で
TestCharacterノードを選択します。 - Inspector の一番下の Add Component ボタンをクリックします。
- Custom → StunEffect を選択して追加します。
5. プロパティの設定
TestCharacter にアタッチされた StunEffect コンポーネントの各プロパティを設定します。
- Default Stun Duration:
2.0など(2 秒スタン)。 - Allow Override Duration:チェック ON(任意)。
- Freeze Physics 2D:
RigidBody2Dを使う場合は ON にします。 - Freeze Physics 3D:3D 物理を使わない場合は OFF のままで構いません。
- Freeze Transform:物理ボディが無い場合にスタン中も止めたい場合は ON。
- Stun Icon Prefab:先ほど作成した
StunIcon.prefabをドラッグ&ドロップで設定します。 - Stun Icon Node:既存ノードを使う場合のみ設定(今回は空のままで OK)。
- Icon Offset Y:
1.5~2.0など、キャラクターの頭上にちょうど良い値を設定します。 - Destroy Icon On End:ON にするとスタン終了ごとにアイコンを破棄します。OFF の場合は再利用(パフォーマンス的には OFF 推奨)。
- Auto Stun On Start:テストしやすいように ON にします。
- Auto Stun Duration:
2.0など。
6. 再生して動作確認
- エディタ右上の Play ボタンを押してゲームを再生します。
- ゲーム開始直後に、
TestCharacterの頭上にピヨりマークが表示され、物理挙動(落下や移動)が一時停止していることを確認します。 - 設定した
Auto Stun Duration秒が経過すると、ピヨりマークが消え、物理挙動が再開されることを確認します。
7. スクリプトから手動でスタンさせる(任意)
他のスクリプトからスタンを発動したい場合は、次のように呼び出せます。
// 例: 攻撃ヒット時に相手を 3 秒スタンさせる
const targetNode = someTargetNode;
const stun = targetNode.getComponent(StunEffect);
if (stun) {
stun.stun(3.0); // 3 秒スタン
}
引数を省略すると、defaultStunDuration の値が使われます。
まとめ
StunEffectを任意のノードにアタッチするだけで、- スタン中の物理挙動の一時停止(2D / 3D 対応)
- 位置固定による簡易スタン
- 頭上のピヨりマーク表示
を一括管理できるようになりました。
- 外部の GameManager やシングルトンに依存せず、すべてインスペクタのプロパティで完結しているため、どのプロジェクトにも簡単に持ち込んで再利用できます。
- 応用例:
- 敵 AI の被弾時に
stun()を呼び出して行動停止。 - プレイヤーがトラップにかかったときの硬直演出。
- オンライン対戦ゲームで、スキルによるスタンデバフの表現。
- 敵 AI の被弾時に
このコンポーネントをベースに、スタン中のアニメーション切り替えや、スタン終了時のエフェクト再生などを追加すれば、よりリッチな状態異常システムを、依然として単一コンポーネントとして拡張していくことができます。




