Godot 4 で「スイッチ」を作ろうとすると、ついこういう構成になりがちですよね。

MovingPlatform (Node2D)
 ├── Sprite2D
 ├── CollisionShape2D
 ├── AnimationPlayer
 ├── Timer
 └── Area2D  ← プレイヤーが踏む判定
      └── CollisionShape2D

そして、さらに「一定時間だけ ON にして自動で OFF に戻したい」と思うと、

  • 親ノード側で Timer を持つのか?
  • ボタン側で Timer を持つのか?
  • アニメーションはどこで管理する?
  • 敵・ドア・足場など、いろんなオブジェクトで同じような “時限ボタン” ロジックをコピペ…

と、だんだんスクリプトが肥大化していきます。
しかも「ボタンの種類ごとにシーンを分けて継承」みたいな構造を取り始めると、継承ツリーがどんどん深くなって、後から仕様変更するときに地獄を見ます。

そこで今回は、「押すと ON になるが、数秒経過すると自動で OFF に戻る」動作だけを切り出した、コンポーネント指向の時限スイッチとして、「TimedButton」コンポーネントを作っていきましょう。

ボタンの見た目や押される条件(プレイヤーが踏む、弾が当たる、UI ボタンを押す)はそれぞれのシーンに任せて、
「ON/OFF 状態管理」と「一定時間で OFF に戻る」ロジックだけをこのコンポーネントに閉じ込めるのがポイントです。


【Godot 4】押したら自動で戻る時限スイッチ!「TimedButton」コンポーネント

この TimedButton コンポーネントは、

  • 外部から「押す(trigger)」だけ呼べば、ON → 一定時間 → OFF を自動でやってくれる
  • 状態変化を signal で通知してくれるので、ドアや足場など好きなオブジェクトを簡単に連動できる
  • シーン階層を汚さず、どのノードにもポン付けできる

という、「合成(コンポーネント)」にぴったりな作りになっています。


フルコード(GDScript / Godot 4)


extends Node
class_name TimedButton
## 時限スイッチ用コンポーネント
## - trigger() を呼ぶと ON になり、一定時間後に自動で OFF に戻る
## - 状態変化は signal で通知される
## - 見た目や押される条件は、このコンポーネントを付ける側のシーンで自由に実装する

## === エクスポート変数 ===

@export_range(0.1, 60.0, 0.1)
var on_duration: float = 3.0:
	## ON を維持する秒数
	## 例: 3.0 にすると、trigger() で ON → 3 秒後に自動で OFF
	set(value):
		on_duration = max(0.1, value) # あまりに短い値は防いでおく

@export var trigger_only_when_off: bool = true:
	## true: OFF のときだけ受け付ける(連打しても時間は延長されない)
	## false: ON 中に再度 trigger() すると、残り時間をリセット(延長)する
	set(value):
		trigger_only_when_off = value

@export var auto_start_disabled: bool = false:
	## true: _ready() 時に完全に無効化状態で始める(is_enabled = false)
	## false: 通常通り、最初から使える状態
	## 例: チュートリアル中は無効にしておき、イベント後に有効にする等
	set(value):
		auto_start_disabled = value

@export var debug_print: bool = false:
	## true にすると、状態変化を print する(デバッグ用)
	set(value):
		debug_print = value

## === ランタイム状態 ===

var is_on: bool = false:
	## 現在の ON/OFF 状態
	set(value):
		if is_on == value:
			return
		is_on = value
		if debug_print:
			print("[TimedButton] is_on = ", is_on)
		## 状態が変わったら signal を飛ばす
		state_changed.emit(is_on)
		if is_on:
			turned_on.emit()
		else:
			turned_off.emit()

var is_enabled: bool = true:
	## コンポーネント自体が有効かどうか
	## false にすると trigger() を受け付けなくなる
	set(value):
		is_enabled = value
		if debug_print:
			print("[TimedButton] is_enabled = ", is_enabled)
		enabled_changed.emit(is_enabled)

## 内部用タイマー
var _timer: SceneTreeTimer

## === Signals ===

## ON / OFF が切り替わったとき(どちらの遷移でも呼ばれる)
signal state_changed(is_on: bool)

## ON になったときだけ
signal turned_on

## OFF になったときだけ
signal turned_off

## 有効 / 無効 が切り替わったとき
signal enabled_changed(is_enabled: bool)


func _ready() -> void:
	## 初期状態の設定
	if auto_start_disabled:
		is_enabled = false
	else:
		is_enabled = true

	is_on = false  # 念のため明示的に OFF からスタート

	if debug_print:
		print("[TimedButton] Ready. on_duration = ", on_duration)


## 外部から呼ぶ「ボタンが押された」トリガー
## 例: プレイヤーが踏んだ / UI ボタンを押した / レバーを引いた 等
func trigger() -> void:
	if not is_enabled:
		if debug_print:
			print("[TimedButton] trigger() ignored: disabled")
		return

	if trigger_only_when_off and is_on:
		# すでに ON のときは無視
		if debug_print:
			print("[TimedButton] trigger() ignored: already ON")
		return

	# ON にする
	_turn_on()


## 強制的に ON にする(タイマーもセットし直す)
## 通常は trigger() を使えば十分だが、スクリプトから直接 ON にしたいときに使える
func force_on() -> void:
	if not is_enabled:
		if debug_print:
			print("[TimedButton] force_on() ignored: disabled")
		return
	_turn_on()


## 強制的に OFF にする(タイマーも止める)
func force_off() -> void:
	_cancel_timer()
	if is_on:
		is_on = false


## コンポーネントを有効化する
func enable() -> void:
	is_enabled = true


## コンポーネントを無効化する(今 ON なら OFF にするかはオプション)
func disable(force_turn_off: bool = false) -> void:
	is_enabled = false
	if force_turn_off:
		force_off()


## === 内部処理 ===

func _turn_on() -> void:
	## すでに ON で、かつ「ON 中の再トリガーで時間延長しない」設定なら何もしない
	if is_on and trigger_only_when_off:
		return

	## 状態を ON に
	is_on = true

	## 既存タイマーがあればキャンセルして、時間をリセット
	_cancel_timer()
	_timer = get_tree().create_timer(on_duration)
	_timer.timeout.connect(_on_timer_timeout)


func _on_timer_timeout() -> void:
	## タイマーが切れたら OFF に戻す
	_timer = null
	if not is_enabled:
		# 無効化されていたら何もしない(仕様に応じて変えてOK)
		return
	is_on = false


func _cancel_timer() -> void:
	if _timer and is_instance_valid(_timer):
		_timer.timeout.disconnect(_on_timer_timeout)
		_timer = null

使い方の手順

手順①:コンポーネントをプロジェクトに追加

  1. 上記の TimedButton.gd をプロジェクトの適当な場所(例: res://components/timed_button.gd)に保存します。
  2. Godot エディタを再読み込みすると、スクリプトクラスとして TimedButton が使えるようになります。

手順②:ボタン役のノードにアタッチ

例えば「プレイヤーが踏むと一定時間だけ ON になる床スイッチ」を作る場合、シーン構成はこんな感じにします。

FloorSwitch (Area2D)  ← プレイヤーが踏む当たり判定
 ├── Sprite2D         ← スイッチの見た目
 ├── CollisionShape2D ← 踏まれる範囲
 └── TimedButton      ← ★コンポーネント(Node)

TimedButton ノードは単なる Node で OK です。
FloorSwitch の子として Node を追加し、スクリプトに TimedButton.gd をアタッチしましょう。

手順③:押されたときに trigger() を呼ぶ

次に、FloorSwitchArea2D)がプレイヤーに踏まれたタイミングで TimedButton.trigger() を呼び出します。


# FloorSwitch.gd (Area2D にアタッチするスクリプト)
extends Area2D

@onready var timed_button: TimedButton = $TimedButton

func _ready() -> void:
	# 状態変化を購読して、見た目を変えたり音を鳴らしたり
	timed_button.state_changed.connect(_on_state_changed)

func _on_body_entered(body: Node2D) -> void:
	if body.is_in_group("player"):
		timed_button.trigger()

func _on_state_changed(is_on: bool) -> void:
	# ここでは単純に色を変えてみる例
	var sprite := $Sprite2D
	if is_on:
		sprite.modulate = Color.GREEN
	else:
		sprite.modulate = Color.WHITE

この時点で、

  • プレイヤーが踏むと trigger() → ON
  • on_duration 秒後に自動で OFF
  • ON / OFF に応じてスプライトの色が変わる

という「時限スイッチ」の基本動作が完成します。

手順④:他オブジェクトと連動させる

例えば「一定時間だけ開くドア」を作る場合は、以下のようなシーン構造にします。

TimedDoor (Node2D)
 ├── Sprite2D
 ├── CollisionShape2D
 ├── AnimationPlayer
 └── TimedButton   ← ドア自身が時限ボタンを持っても良い

TimedDoor.gd では、TimedButton の signal に反応してドアの開閉を行います。


# TimedDoor.gd
extends Node2D

@onready var timed_button: TimedButton = $TimedButton
@onready var anim: AnimationPlayer = $AnimationPlayer
@onready var collision: CollisionShape2D = $CollisionShape2D

func _ready() -> void:
	timed_button.turned_on.connect(_on_open)
	timed_button.turned_off.connect(_on_close)

func _on_open() -> void:
	anim.play("open")
	collision.disabled = true

func _on_close() -> void:
	anim.play("close")
	collision.disabled = false

こうしておけば、TimedDoor の外側から timed_button.trigger() を呼ぶだけで、「一定時間だけ開くドア」がどこからでも簡単に作れます。

例えばレバー側から制御する構成もありです。

Lever (Area2D)
 ├── Sprite2D
 └── CollisionShape2D

TimedDoor (Node2D)
 ├── Sprite2D
 ├── CollisionShape2D
 ├── AnimationPlayer
 └── TimedButton

# Lever.gd
extends Area2D

@export var target_door: NodePath
var _door: Node2D
var _timed_button: TimedButton

func _ready() -> void:
	_door = get_node(target_door)
	_timed_button = _door.get_node("TimedButton") as TimedButton

func _on_body_entered(body: Node2D) -> void:
	if body.is_in_group("player"):
		_timed_button.trigger()

レバーとドアはまったく別シーンでも構いません。
「時限ロジック」は TimedButton に任せることで、オブジェクト同士の関係がかなりスッキリしますね。


メリットと応用

TimedButton コンポーネントを使うことで、次のようなメリットがあります。

  • 継承ツリーを増やさずに済む
    「TimedFloorSwitch」「TimedDoor」「TimedPlatform」…といった継承クラスを作らなくても、
    それぞれのシーンに TimedButton合成(コンポーネントとしてアタッチ) するだけで済みます。
  • シーン構造がシンプル
    「時限ロジック用の Timer やフラグ管理」が 1 か所にまとまるので、
    各シーンは「いつ ON するか」「ON/OFF のときに何をするか」だけを考えればよくなります。
  • テストがしやすい
    TimedButton 単体で ON/OFF のテストができるので、
    ドアや足場のロジックと混ざってバグを追いかける必要が減ります。
  • 使い回しがしやすい
    敵 AI の「一定時間だけ無敵」「一定時間だけ怒り状態」など、
    時限的な状態管理をしたいところにも、そのまま流用できます。

応用例として、

  • 敵が踏むと「一定時間だけトゲ床が出る」
  • UI のボタンを押すと「一定時間だけスコア倍率 2 倍」
  • チェックポイントを通過すると「一定時間だけ安全地帯」

など、「一時的な状態変化」全般に使えます。

改造案:残り時間を取得できるようにする

例えば「スイッチの上に残り時間ゲージを出したい」場合、
内部タイマーの残り時間を返す関数を追加すると便利です。


# TimedButton.gd に追加
func get_remaining_time() -> float:
	## 残り時間(秒)を返す。OFF やタイマー未設定時は 0
	if _timer and is_instance_valid(_timer):
		return _timer.time_left
	return 0.0

これを使えば、ProgressBarLabel に残り秒数を表示して、
「そろそろスイッチが切れる!」という演出も簡単にできますね。

継承ベースでロジックを増やすよりも、こうした小さなコンポーネントを組み合わせていく方が、
後からの仕様変更にも強く、シーン構造もきれいに保てます。ぜひ自分のプロジェクト用にカスタマイズしてみてください。