レバー的なスイッチって、つい専用の Lever シーンを作って、その中にアニメーションや当たり判定、インタラクト処理をごっそり書いてしまいがちですよね。さらに「プレイヤーが近づいたらEキーで操作」「敵AIもレバーを操作」「UIボタンからもON/OFFしたい」…と要件が増えてくると、レバーシーンのスクリプトがどんどん肥大化していきます。
Godot標準のやり方だと、
- レバーごとに専用シーン&スクリプトを継承で増やしていく
- 「インタラクト処理」と「ON/OFF状態管理」と「見た目のアニメーション」が1つのスクリプトにべったり結合する
- 別のオブジェクトでも同じようなスイッチ機能を使いたいときに、コピペや継承で泥沼化
という状態になりがちです。
そこで今回は、レバーの「ON/OFFトグル機能」だけを切り出したコンポーネント LeverSwitch を作ってみましょう。どんなノードにもペタッと貼れるようにしておけば、
- レバー型スイッチ
- 壁のスイッチ
- 床にあるペダルスイッチ
など、見た目が違うオブジェクトにも同じロジックを再利用できます。継承より合成、ですね。
【Godot 4】レバーで世界をON/OFF!「LeverSwitch」コンポーネント
このコンポーネントは、
- インタラクト(プレイヤー操作・信号・スクリプト呼び出し)でON/OFFをトグル
- 左右に倒れた見た目(回転 or スプライトフレーム)を自動制御
- ON/OFF時にシグナル発火(ドアや仕掛けに接続しやすい)
という、レバー系ギミックの「コア機能」を提供します。
フルコード:LeverSwitch.gd
extends Node
class_name LeverSwitch
## レバー型スイッチのコンポーネント
## - インタラクトでON/OFFを切り替え
## - 見た目(回転 or スプライト)を自動更新
## - ON/OFF時にシグナルを発火
## スイッチがONになったとき
signal switched_on
## スイッチがOFFになったとき
signal switched_off
## ON/OFFが変化したとき(どちらでも)
signal switched(state: bool)
## 現在のON/OFF状態
@export var is_on: bool = false:
set(value):
if is_on == value:
return
is_on = value
_update_visual()
_emit_state_signals()
## 見た目の更新方法
enum VisualMode {
ROTATE_NODE, ## 親ノード(または指定ノード)を回転させる
SPRITE_FRAME, ## Sprite2D/AnimatedSprite2D のフレームを切り替える
NONE ## 見た目はこのコンポーネントでは制御しない
}
## 見た目の制御方法
@export var visual_mode: VisualMode = VisualMode.ROTATE_NODE
## 見た目を操作する対象ノードへのパス
## - 空なら親ノード(self.get_parent())を対象にする
## - 例: Sprite2D を制御したい場合は NodePath("Sprite2D")
@export var visual_target_path: NodePath
## ROTATE_NODE モード用: OFF時の回転角度(度)
@export var rotation_off_deg: float = -30.0
## ROTATE_NODE モード用: ON時の回転角度(度)
@export var rotation_on_deg: float = 30.0
## ROTATE_NODE モード用: 補間時間(秒)。0だと即座に切り替え
@export var rotate_transition_time: float = 0.1
## SPRITE_FRAME モード用: OFF時に表示するフレーム番号
@export var frame_off: int = 0
## SPRITE_FRAME モード用: ON時に表示するフレーム番号
@export var frame_on: int = 1
## インタラクト受付のクールダウン(秒)
## - 連打対策やアニメーション完了を待つため
@export var interact_cooldown: float = 0.1
## 最初に自動で見た目を状態に合わせるか
@export var sync_visual_on_ready: bool = true
## 内部用: クールダウンタイマー
var _cooldown_timer: float = 0.0
## 内部用: 補間用Tween(回転アニメ用)
var _tween: Tween
func _ready() -> void:
# シーン起動時に見た目を状態に合わせる
if sync_visual_on_ready:
_update_visual(force_instant := true)
func _process(delta: float) -> void:
# クールダウンを減らす
if _cooldown_timer > 0.0:
_cooldown_timer -= delta
## 外部から呼ぶインタラクト用API
## - プレイヤーの入力、エリアの衝突、UIボタンなどから呼び出してください
func interact() -> void:
if _cooldown_timer > 0.0:
return # まだクールダウン中
toggle()
_cooldown_timer = interact_cooldown
## 状態をトグル(ON <-> OFF)
func toggle() -> void:
is_on = !is_on
## 明示的にONにする
func turn_on() -> void:
is_on = true
## 明示的にOFFにする
func turn_off() -> void:
is_on = false
## 現在の状態を返す(読みやすさのためのヘルパー)
func is_on_state() -> bool:
return is_on
## --- 内部処理 -----------------------------------------------------------
## 見た目を現在の state に合わせて更新
func _update_visual(force_instant: bool = false) -> void:
match visual_mode:
VisualMode.ROTATE_NODE:
_update_visual_rotate(force_instant)
VisualMode.SPRITE_FRAME:
_update_visual_sprite()
VisualMode.NONE:
pass
## ROTATE_NODE モードの見た目更新
func _update_visual_rotate(force_instant: bool) -> void:
var target_node := _get_visual_target()
if target_node == null:
return
# 既存のTweenがあれば止める
if _tween and _tween.is_valid():
_tween.kill()
var target_deg := rotation_on_deg if is_on else rotation_off_deg
var target_rad := deg_to_rad(target_deg)
if force_instant or rotate_transition_time <= 0.0:
target_node.rotation = target_rad
else:
_tween = create_tween()
_tween.tween_property(
target_node,
"rotation",
target_rad,
rotate_transition_time
).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
## SPRITE_FRAME モードの見た目更新
func _update_visual_sprite() -> void:
var target_node := _get_visual_target()
if target_node == null:
return
# Sprite2D の場合
if target_node is Sprite2D:
var sprite := target_node as Sprite2D
# Godot 4 の Sprite2D は "frame" プロパティでアニメーションフレームを扱える
sprite.frame = frame_on if is_on else frame_off
return
# AnimatedSprite2D の場合
if target_node is AnimatedSprite2D:
var anim_sprite := target_node as AnimatedSprite2D
# AnimatedSprite2D では "frame" プロパティで現在のフレームを指定できる
anim_sprite.frame = frame_on if is_on else frame_off
return
# 対応していないノードだった場合は何もしない
push_warning("LeverSwitch: visual_mode = SPRITE_FRAME ですが、対象ノードが Sprite2D / AnimatedSprite2D ではありません。")
## 見た目ターゲットノードを取得
func _get_visual_target() -> Node2D:
var target: Node = null
if visual_target_path.is_empty():
target = get_parent()
else:
target = get_node_or_null(visual_target_path)
if target == null:
push_warning("LeverSwitch: visual target not found. visual_target_path=%s" % visual_target_path)
return null
if not (target is Node2D):
push_warning("LeverSwitch: visual target is not Node2D. path=%s" % visual_target_path)
return null
return target as Node2D
## 状態変化シグナルを発火
func _emit_state_signals() -> void:
switched.emit(is_on)
if is_on:
switched_on.emit()
else:
switched_off.emit()
使い方の手順
ここでは典型的な「プレイヤーが近づいてEキーでレバーを倒し、ドアが開く」例で説明します。
① シーン構成を作る
まずはレバー本体のシーンを作りましょう。
Lever (Node2D) ├── Sprite2D ├── Area2D │ └── CollisionShape2D └── LeverSwitch (Node)
- Lever (Node2D): レバーのルート
- Sprite2D: レバーの見た目。回転させたりフレームを切り替えたりします
- Area2D + CollisionShape2D: プレイヤーが近づいたかどうかを検知するためのエリア
- LeverSwitch: 上記のコンポーネントスクリプトをアタッチした
Node
LeverSwitch ノードには、先ほどの LeverSwitch.gd をアタッチしてください。
② LeverSwitch のパラメータを設定する
インスペクタで LeverSwitch ノードを選択し、以下のように設定します(例):
- is_on: 初期状態(OFFにしたいなら false)
- visual_mode:
ROTATE_NODE(レバーを左右に倒す) - visual_target_path:
"../Sprite2D"(親から見たパス) - rotation_off_deg:
-30 - rotation_on_deg:
30 - rotate_transition_time:
0.1(ちょっとだけアニメーション) - interact_cooldown:
0.2くらい(お好み)
もしスプライトシートで「左に倒れたフレーム」「右に倒れたフレーム」を持っているなら、
- visual_mode:
SPRITE_FRAME - visual_target_path:
"../Sprite2D" - frame_off: OFFのフレーム番号
- frame_on: ONのフレーム番号
とすることで、回転ではなくフレーム切り替えで表現できます。
③ プレイヤーからインタラクトする
次に、プレイヤーキャラからレバーを操作してみましょう。プレイヤーシーンは例えばこんな感じ:
Player (CharacterBody2D)
├── Sprite2D
├── CollisionShape2D
└── InteractArea (Area2D)
└── CollisionShape2D
InteractArea でレバーに近づいているかを検知し、Eキー入力で LeverSwitch.interact() を呼び出します。
# Player.gd(サンプル)
extends CharacterBody2D
@export var interact_action_name := "interact" # InputMap で "E" などに割り当て
var _current_lever: LeverSwitch = null
func _ready() -> void:
# InteractArea のシグナル接続(エディタからでもOK)
var interact_area := $InteractArea
interact_area.body_entered.connect(_on_interact_body_entered)
interact_area.body_exited.connect(_on_interact_body_exited)
func _physics_process(delta: float) -> void:
# 省略: 移動処理など
if Input.is_action_just_pressed(interact_action_name) and _current_lever:
_current_lever.interact()
func _on_interact_body_entered(body: Node) -> void:
# 近づいたオブジェクトに LeverSwitch コンポーネントがあるか探す
var lever := body.get_node_or_null("LeverSwitch")
if lever and lever is LeverSwitch:
_current_lever = lever
func _on_interact_body_exited(body: Node) -> void:
if _current_lever and _current_lever.get_parent() == body:
_current_lever = null
ここでは「プレイヤーの InteractArea が Lever 本体のコリジョンに重なっているときだけ _current_lever をセットし、Eキーで interact() を呼ぶ」というシンプルな構成にしています。
④ レバーでドアなどのギミックを動かす
最後に、レバーON/OFFでドアを開閉してみましょう。ドア側もコンポーネント指向で作るとスッキリしますが、ここでは簡単な例としてドアのスクリプトに直接書いてみます。
Door (Node2D) ├── Sprite2D └── CollisionShape2D
# Door.gd(サンプル)
extends Node2D
@export var is_open: bool = false:
set(value):
is_open = value
_update_visual()
func _ready() -> void:
_update_visual()
func _update_visual() -> void:
# 開いているときはコリジョンを無効化、スプライトを半透明にするなど
$CollisionShape2D.disabled = is_open
$Sprite2D.modulate.a = 0.5 if is_open else 1.0
## LeverSwitch の switched シグナルに接続して使う
func on_lever_switched(state: bool) -> void:
is_open = state
エディタ上で、レバーシーンを開き、LeverSwitch ノードを選択して、
switchedシグナル → ドアのon_lever_switchedに接続
とするだけで、レバーONでドアが開き、OFFで閉じるようになります。
メリットと応用
LeverSwitch をコンポーネントとして切り出すことで、次のようなメリットがあります。
- シーン構造がシンプルに保てる
レバーの「見た目」「当たり判定」「インタラクト」「ON/OFF状態管理」が、それぞれ別ノード・別スクリプトに分かれます。
レバー専用の巨大スクリプトを作らずに済むので、後から仕様変更が来ても怖くありません。 - どんなノードにも貼り付けて再利用できる
見た目がレバーじゃなくてもOKです。壁スイッチにも、床スイッチにも、UI用の疑似スイッチにもそのまま使えます。
visual_mode = NONEにして、見た目は別のコンポーネントに任せる、なんて構成もアリですね。 - テストがしやすい
turn_on(),turn_off(),toggle()といった明示的なAPIがあるので、ユニットテストやデバッグ用のスクリプトからも扱いやすいです。
応用としては、
- 複数のレバーが同時にONになったら扉が開く「パズルドア」
- 一定時間だけONになる「タイマー付きレバー」(クールダウンを応用)
- 敵AIが巡回中にレバーを切り替える「罠の制御」
などに広げていけます。
改造案:一定時間で自動的にOFFに戻るレバー
例えば「押してから3秒後に自動でOFFに戻るレバー」にしたい場合、こんな関数を追加できます:
## 一定時間だけONにして、自動でOFFに戻す
@export var auto_off_time: float = 0.0 # 0以下なら自動OFFなし
func _emit_state_signals() -> void:
switched.emit(is_on)
if is_on:
switched_on.emit()
# ONになったタイミングで自動OFFを仕込む
if auto_off_time > 0.0:
# 既存のタイマーTweenを殺してから新規作成するなどの工夫をしてもOK
var timer_tween := create_tween()
timer_tween.tween_interval(auto_off_time)
timer_tween.tween_callback(turn_off)
else:
switched_off.emit()
こうしておけば、インスペクタで auto_off_time に 3.0 を入れるだけで「3秒だけONになるレバー」が完成します。レバーの挙動を変えたいときも、レバー本体のシーンをいじらずにコンポーネントだけ差し替え/改造できるのが、合成志向の気持ちいいところですね。
