【Cocos Creator 3.8】LeverSwitch の実装:アタッチするだけで「クリック/タップでON/OFFが切り替わるレバー」を実現する汎用スクリプト
このガイドでは、任意のノードにアタッチするだけで「レバーを左右に倒して ON/OFF を切り替える」挙動を実現できる汎用コンポーネント LeverSwitch を実装します。
マウスクリックやタップでインタラクトでき、ON/OFF 状態に応じて回転角度や色・ラベル表示を変えられるので、UI のスイッチやゲーム内のレバーギミックにそのまま使えます。
コンポーネントの設計方針
要件整理
- ノードにアタッチすると「レバー」として振る舞う。
- クリック / タップ(Pointer)でインタラクトすると、ON ⇔ OFF をトグル切り替え。
- ON/OFF に応じてノードの回転角度を変える(例:左に倒れて OFF、右に倒れて ON)。
- ON/OFF に応じて色やラベル文字を変えられる(任意)。
- ON/OFF 変更時に任意のノードに対して
EventHandlerを発火できる。 - 他のカスタムスクリプトへの依存は一切なし。全てインスペクタ設定で完結。
- 防御的実装:必要な標準コンポーネント(
UITransformなど)は存在チェックし、不足時はログで警告。
インスペクタで設定可能なプロパティ設計
LeverSwitch が持つ主なプロパティと役割は以下の通りです。
- initialOn (boolean)
- 初期状態が ON かどうか。
- true: シーン開始時に ON 状態、false: OFF 状態で開始。
- interactable (boolean)
- プレイヤー操作で切り替え可能かどうか。
- false にすると、スクリプトからの制御専用レバーとして使える。
- offAngle (number)
- OFF 状態のときの Z 回転角度(度数法)。
- 例: -30 で左に 30 度倒れた見た目。
- onAngle (number)
- ON 状態のときの Z 回転角度(度数法)。
- 例: 30 で右に 30 度倒れた見た目。
- useSmoothAnimation (boolean)
- ON/OFF 切り替え時に角度をスムーズに補間するか、瞬時に切り替えるか。
- animationDuration (number)
- スムーズアニメーションを行うときの所要時間(秒)。
- 例: 0.15 〜 0.3 くらいがレバーらしい。
- clickSound (AudioClip | null)
- ON/OFF を切り替えたときに再生する効果音。
- 未設定の場合は音は鳴らない。
- targetGraphicNode (Node | null)
- ON/OFF に応じて色を変えたい Node(Sprite や Label など)。
- 未設定なら色変更は行わない。
- offColor (Color)
- OFF 状態のときの色。
- onColor (Color)
- ON 状態のときの色。
- labelNode (Node | null)
- ON/OFF 状態を文字で表示したい Label ノード。
- 未設定ならテキスト変更は行わない。
- offText (string)
- OFF 状態のときに Label に表示する文字列。
- 例: “OFF”、”0” など。
- onText (string)
- ON 状態のときに Label に表示する文字列。
- 例: “ON”、”1” など。
- playClickSoundOnEveryToggle (boolean)
- ON → OFF / OFF → ON のどちらでも毎回クリック音を鳴らすかどうか。
- onTurnedOn (EventHandler[])
- onTurnedOff (EventHandler[])
- onToggled (EventHandler[])
これらは Cocos の EventHandler を使い、インスペクタから任意のノード・任意のコンポーネントのメソッドを呼び出せるようにします。
これにより、他のスクリプトに依存せず「レバーが ON になったらドアを開ける」「OFF になったらライトを消す」といった連携が可能です。
TypeScriptコードの実装
import { _decorator, Component, Node, EventTouch, EventMouse, input, Input, Vec3, Color, Sprite, Label, AudioSource, AudioClip, UITransform, EventHandler, tween } from 'cc';
const { ccclass, property } = _decorator;
/**
* LeverSwitch
* 任意のノードを「クリック/タップでON/OFFが切り替わるレバー」にする汎用コンポーネント
*/
@ccclass('LeverSwitch')
export class LeverSwitch extends Component {
@property({
tooltip: 'シーン開始時の初期状態がONかどうか。\nON: onAngle側に倒れた状態で開始 / OFF: offAngle側で開始'
})
public initialOn: boolean = false;
@property({
tooltip: 'プレイヤーのクリック/タップでレバーを操作できるかどうか。\nfalseにするとスクリプトからのみ制御されます。'
})
public interactable: boolean = true;
@property({
tooltip: 'OFF状態のときのZ回転角度(度数法)。\n例: -30 で左に30度倒れた見た目になります。'
})
public offAngle: number = -30;
@property({
tooltip: 'ON状態のときのZ回転角度(度数法)。\n例: 30 で右に30度倒れた見た目になります。'
})
public onAngle: number = 30;
@property({
tooltip: 'ON/OFF切り替え時に角度をスムーズに補間するかどうか。\nfalseの場合は瞬時に角度が切り替わります。'
})
public useSmoothAnimation: boolean = true;
@property({
tooltip: 'スムーズアニメーションの所要時間(秒)。\nuseSmoothAnimationがtrueのときのみ有効です。'
})
public animationDuration: number = 0.15;
@property({
type: AudioClip,
tooltip: 'ON/OFF切り替え時に再生するクリック音。\n未設定の場合は音は鳴りません。'
})
public clickSound: AudioClip | null = null;
@property({
tooltip: 'ON/OFF切り替えのたびに毎回クリック音を再生するかどうか。'
})
public playClickSoundOnEveryToggle: boolean = true;
@property({
type: Node,
tooltip: 'ON/OFFに応じて色を変えたいノード(SpriteやLabelなど)。\n未設定の場合は色の変更は行いません。'
})
public targetGraphicNode: Node | null = null;
@property({
tooltip: 'OFF状態のときに適用する色。\ntargetGraphicNodeにSpriteまたはLabelがある場合のみ有効です。'
})
public offColor: Color = new Color(150, 150, 150, 255);
@property({
tooltip: 'ON状態のときに適用する色。\ntargetGraphicNodeにSpriteまたはLabelがある場合のみ有効です。'
})
public onColor: Color = new Color(0, 255, 0, 255);
@property({
type: Node,
tooltip: 'ON/OFF状態を文字で表示したいLabelノード。\n未設定の場合はテキストの変更は行いません。'
})
public labelNode: Node | null = null;
@property({
tooltip: 'OFF状態のときに表示するテキスト。'
})
public offText: string = 'OFF';
@property({
tooltip: 'ON状態のときに表示するテキスト。'
})
public onText: string = 'ON';
@property({
type: [EventHandler],
tooltip: 'レバーがONになったときに呼び出されるイベントハンドラ一覧。'
})
public onTurnedOn: EventHandler[] = [];
@property({
type: [EventHandler],
tooltip: 'レバーがOFFになったときに呼び出されるイベントハンドラ一覧。'
})
public onTurnedOff: EventHandler[] = [];
@property({
type: [EventHandler],
tooltip: 'レバーの状態がトグル(ON⇔OFF)されたときに毎回呼び出されるイベントハンドラ一覧。'
})
public onToggled: EventHandler[] = [];
// 現在のON/OFF状態
private _isOn: boolean = false;
// アニメーション中かどうか
private _isAnimating: boolean = false;
// AudioSource(存在すれば使用)
private _audioSource: AudioSource | null = null;
// クリック判定に使うUITransform
private _uiTransform: UITransform | null = null;
onLoad() {
// AudioSourceを取得(あれば使用)
this._audioSource = this.getComponent(AudioSource);
if (!this._audioSource && this.clickSound) {
console.warn('[LeverSwitch] AudioSourceがアタッチされていませんが、clickSoundが設定されています。効果音を鳴らしたい場合は同じノードにAudioSourceコンポーネントを追加してください。');
}
// UITransformを取得して警告(クリック判定に推奨)
this._uiTransform = this.getComponent(UITransform);
if (!this._uiTransform) {
console.warn('[LeverSwitch] UITransformコンポーネントが見つかりません。UI要素としてクリック領域を持たせたい場合は、このノードにUITransformを追加してください。');
}
// 初期状態を設定
this._isOn = this.initialOn;
this.applyVisualState(false);
}
start() {
// ポインタイベントを登録(Node自体のイベントを使用)
this.node.on(Node.EventType.TOUCH_END, this.onPointerUp, this);
this.node.on(Node.EventType.MOUSE_UP, this.onPointerUp, this);
}
onDestroy() {
// イベント解除
this.node.off(Node.EventType.TOUCH_END, this.onPointerUp, this);
this.node.off(Node.EventType.MOUSE_UP, this.onPointerUp, this);
}
/**
* 現在の状態がONかどうかを返す
*/
public get isOn(): boolean {
return this._isOn;
}
/**
* 外部から状態を設定するためのメソッド(アニメーションの有無を指定可能)
*/
public setState(isOn: boolean, animate: boolean = true) {
if (this._isOn === isOn) {
return;
}
this._isOn = isOn;
this.applyVisualState(animate);
this.invokeEvents();
}
/**
* 外部からトグルさせるためのメソッド
*/
public toggle(animate: boolean = true) {
this.setState(!this._isOn, animate);
}
/**
* ポインタが離されたとき(クリック/タップ)に呼ばれる
*/
private onPointerUp(event: EventTouch | EventMouse) {
if (!this.interactable) {
return;
}
// すでにアニメーション中なら無視(連打防止)
if (this._isAnimating) {
return;
}
// クリック位置がノードの領域内かを簡易チェック(UITransformがある場合)
if (this._uiTransform && event instanceof EventTouch) {
const location = event.getUILocation();
const localPos = this._uiTransform.convertToNodeSpaceAR(new Vec3(location.x, location.y, 0));
const rect = this._uiTransform.contentSize;
const halfW = rect.width / 2;
const halfH = rect.height / 2;
if (localPos.x < -halfW || localPos.x > halfW || localPos.y < -halfH || localPos.y > halfH) {
// 領域外なら無視
return;
}
}
// 状態をトグル
this.toggle(true);
}
/**
* 現在の_isOn状態に応じて見た目を適用する
*/
private applyVisualState(animate: boolean) {
const targetAngle = this._isOn ? this.onAngle : this.offAngle;
// 回転の適用
if (this.useSmoothAnimation && animate && this.animationDuration > 0) {
this._isAnimating = true;
const currentEuler = this.node.eulerAngles;
const targetEuler = new Vec3(currentEuler.x, currentEuler.y, targetAngle);
tween(this.node)
.to(this.animationDuration, { eulerAngles: targetEuler })
.call(() => {
this._isAnimating = false;
})
.start();
} else {
const currentEuler = this.node.eulerAngles;
this.node.setRotationFromEuler(currentEuler.x, currentEuler.y, targetAngle);
}
// 色の適用
if (this.targetGraphicNode) {
const sprite = this.targetGraphicNode.getComponent(Sprite);
const label = this.targetGraphicNode.getComponent(Label);
if (!sprite && !label) {
console.warn('[LeverSwitch] targetGraphicNodeにSpriteまたはLabelコンポーネントが見つかりません。色変更は行われません。');
}
const colorToApply = this._isOn ? this.onColor : this.offColor;
if (sprite) {
sprite.color = colorToApply;
}
if (label) {
label.color = colorToApply;
}
}
// テキストの適用
if (this.labelNode) {
const label = this.labelNode.getComponent(Label);
if (!label) {
console.warn('[LeverSwitch] labelNodeにLabelコンポーネントが見つかりません。テキスト変更は行われません。');
} else {
label.string = this._isOn ? this.onText : this.offText;
}
}
// 効果音の再生
if (this.clickSound && this.playClickSoundOnEveryToggle) {
this.playClick();
}
}
/**
* 効果音を再生する
*/
private playClick() {
if (!this.clickSound) {
return;
}
if (this._audioSource) {
this._audioSource.playOneShot(this.clickSound, 1.0);
} else {
console.warn('[LeverSwitch] clickSoundは設定されていますが、このノードにAudioSourceがありません。効果音を鳴らすにはAudioSourceを追加してください。');
}
}
/**
* 状態変更時にイベントを呼び出す
*/
private invokeEvents() {
// ON/OFF共通
if (this.onToggled.length > 0) {
EventHandler.emitEvents(this.onToggled, this);
}
if (this._isOn) {
if (this.onTurnedOn.length > 0) {
EventHandler.emitEvents(this.onTurnedOn, this);
}
} else {
if (this.onTurnedOff.length > 0) {
EventHandler.emitEvents(this.onTurnedOff, this);
}
}
}
}
コードのポイント解説
- onLoad
- AudioSource と UITransform を
getComponentで取得し、なければ警告ログ。 initialOnに基づいて内部状態_isOnを決め、applyVisualState(false)で初期見た目を設定。
- AudioSource と UITransform を
- start
Node.EventType.TOUCH_ENDとNode.EventType.MOUSE_UPを登録し、クリック/タップでonPointerUpが呼ばれるようにする。
- onPointerUp
interactableが false なら無視。- アニメーション中(
_isAnimating)は連打による多重トグルを防止。 - UITransform がある場合は、タップ位置がノード矩形内かを簡易チェック。
- 条件を満たせば
toggle(true)で状態を反転。
- applyVisualState
- ON/OFF に応じて
offAngle/onAngleからターゲット角度を決定。 useSmoothAnimationとanimationDurationに応じて、tweenでスムーズ回転 or 即時回転。targetGraphicNodeに Sprite / Label があれば、offColor/onColorを適用。labelNodeに Label があれば、offText/onTextを表示。clickSoundが設定されていれば、AudioSource からplayOneShotで再生。
- ON/OFF に応じて
- invokeEvents
- 状態変更ごとに
onToggledを実行。 - ON のとき
onTurnedOn、OFF のときonTurnedOffをEventHandler.emitEventsで呼び出し。 - 引数には
this(LeverSwitch インスタンス)を渡すので、受け取り側でisOn参照も可能。
- 状態変更ごとに
- 公開API
get isOn(): booleanで現在の状態を参照。setState(isOn: boolean, animate = true)で外部から明示的に ON/OFF を切り替え。toggle(animate = true)で外部からトグル制御。
使用手順と動作確認
1. スクリプトファイルの作成
- エディタ下部の Assets パネルで、スクリプトを置きたいフォルダ(例:
assets/scripts)を選択します。 - そのフォルダを右クリック → Create → TypeScript を選択します。
- 新規スクリプトに
LeverSwitch.tsという名前を付けます。 - ダブルクリックしてエディタ(VS Code など)で開き、本文をすべて削除して上記の TypeScript コードを貼り付け、保存します。
2. テスト用レバーノードの作成
まずはシンプルに「レバー画像をクリックすると左右に倒れる」挙動だけ確認します。
- Hierarchy パネルで、Canvas 配下にテスト用ノードを作成します。
- 右クリック → Create → UI → Sprite
- 作成された Sprite ノードの名前を
TestLeverなどに変更します。
- Inspector で
TestLeverを選択し、Sprite の SpriteFrame にレバーっぽい画像を設定します(なければ四角でも可)。 - 同じく Inspector の Add Component ボタンをクリック → Custom → LeverSwitch を選択してアタッチします。
3. LeverSwitch の基本設定
TestLever ノードを選択した状態で、Inspector の LeverSwitch コンポーネントを次のように設定してみましょう。
- Initial On: false(初期は OFF)
- Interactable: true
- Off Angle: -30
- On Angle: 30
- Use Smooth Animation: true
- Animation Duration: 0.2
- Click Sound: (まだ未設定でOK)
- Play Click Sound On Every Toggle: true
- Target Graphic Node: (後で設定)
- Label Node: (後で設定)
この状態でプレビューすると、レバー画像をクリック/タップするたびに、左(OFF)と右(ON)にスムーズに倒れるはずです。
4. 色とラベル表示を連動させる
次に、ON/OFF に応じて色と文字を変えてみます。
- 色を変える対象ノードの設定
TestLeverノード自体の色を変えたい場合は、そのまま Target Graphic Node にTestLeverをドラッグ&ドロップします。- 別のノードの色を変えたい場合は、Canvas 配下などに任意の Sprite / Label ノードを作成し、そのノードを Target Graphic Node に設定してください。
- Off Color: (150,150,150,255) のままでOK(グレー)
- On Color: (0,255,0,255) にして緑色に。
- ラベル表示の設定
- Hierarchy で Canvas を右クリック → Create → UI → Label を選択し、
LeverLabelという名前にします。 LeverLabelをTestLeverの子にして、レバーの近くに配置します。LeverLabelを選択し、Inspector で Label コンポーネントに任意のフォントサイズや位置を設定します。TestLeverを選択し、LeverSwitch の Label Node にLeverLabelをドラッグ&ドロップします。- Off Text: “OFF”
- On Text: “ON”
- Hierarchy で Canvas を右クリック → Create → UI → Label を選択し、
再度プレビューすると、レバーをクリックするたびにレバーの色がグレー/緑に変わり、ラベルが “OFF”/”ON” と切り替わるのが確認できます。
5. 効果音を付ける
- Assets パネルで効果音の AudioClip(例:
click.wav)をインポートします。 TestLeverノードを選択し、Inspector の Add Component → Audio → AudioSource を追加します。- AudioSource の Clip は空のままで構いません(
playOneShotを使うため)。 - LeverSwitch の Click Sound に、インポートした AudioClip をドラッグ&ドロップします。
- Play Click Sound On Every Toggle が true になっていることを確認します。
プレビューすると、レバーを ON/OFF するたびにクリック音が鳴ります。
もし AudioSource を付け忘れると、コンソールに「AudioSourceがありません」と警告が出るので、追加してください。
6. イベントハンドラで他ノードと連携する
最後に、レバーの ON/OFF に応じて別ノードの挙動を変える例を紹介します。ここでは「レバーが ON のときにランプを表示、OFF のときに非表示にする」例です。
- Hierarchy で Canvas 配下に UI → Sprite を作成し、
Lampという名前にします。 Lampにランプ画像(または適当な画像)を設定し、初期状態では Enabled を ON にしておきます。Lampに簡単なスクリプトを付けてもよいですが、既にある Active プロパティを直接 EventHandler で制御できます。TestLeverを選択し、LeverSwitch コンポーネントの On Turned On の横にある+ボタンをクリックします。- 追加された EventHandler の Target に
Lampノードをドラッグ&ドロップします。 - Component ドロップダウンから cc.Node を選択します。
- Handler ドロップダウンから setActive を選択します。
- CustomEventData に
trueと入力します(文字列として渡されますが、setActive は “true” を truthy として扱います)。 - 同様に、On Turned Off にも EventHandler を追加し、
Target: Lamp、Component: cc.Node、Handler: setActive、CustomEventData: falseと設定します。
これで、レバーが ON になるとランプが表示され、OFF になるとランプが非表示になります。
より複雑な処理をしたい場合は、任意のコンポーネントの任意のメソッドを Handler に指定し、そのメソッド側で (event: Event, customData: string) などを受け取るように実装すれば対応できます。
まとめ
この LeverSwitch コンポーネントは、
- ノードにアタッチするだけで「クリック/タップで ON/OFF が切り替わるレバー」として機能する。
- 角度・アニメーション・色・ラベル・効果音をすべてインスペクタから調整できる。
EventHandlerを使って ON/OFF 時の任意の処理を外部スクリプトに通知できる。- 他のカスタムスクリプトに依存せず、このスクリプト単体で完結する。
UI スイッチ、ギミック用レバー、オプション画面のトグルなど、多くの場面で使い回せる汎用コンポーネントとしてプロジェクトに組み込んでおくと、
「レバーっぽい挙動を毎回実装し直す」コストを大きく削減できます。
必要に応じて、
- ドラッグで倒した方向に応じて ON/OFF を決める
- キーボードやゲームパッド入力でもトグルする
- マルチステート(3 段階以上)のレバーに拡張する
といった拡張も同じ設計パターンで実装できます。
まずは今回の LeverSwitch をベースに、あなたのゲームの UI/ギミックに合わせてカスタマイズしてみてください。




