Godotでボタン入力を処理するとき、つい「プレイヤーのスクリプト」に直接こう書いてしまいがちですよね。
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("attack"):
# 攻撃開始
if event.is_action_released("attack"):
# 長押しかどうかを自前で判定して…
このやり方、最初はシンプルに見えますが、
- プレイヤーごとに「長押し判定ロジック」をコピペしがち
- UIボタンでも同じ処理を書きたくなって、またコピペ
- 「短押し・長押し・ダブルタップ…」と条件が増えると、
_unhandled_inputが地獄化
と、だんだんスクリプトが肥大化していきます。
Godotのシーン継承で「LongPressButton」みたいな派生シーンを作る手もありますが、それはそれで
- 「このボタンは継承版?オリジナル版?」と管理がややこしい
- プレイヤー・敵・UIなど、種類ごとに継承パターンが増殖する
と、継承のツリーが深くなりがちです。
そこで今回は、「押された時間を測って、短押しか長押しかを判定するだけ」のロジックを、
どのノードにもポン付けできるコンポーネントとして切り出してみましょう。
その名も TapHoldCheck コンポーネントです。
【Godot 4】短押し・長押しをキレイに分離!「TapHoldCheck」コンポーネント
TapHoldCheck は、
- 指定した
Inputアクション(例:"attack")を監視 - 押していた時間を計測
- しきい値(例: 0.35秒)より短ければ「短押し」、長ければ「長押し」と判定
- それぞれに対応するシグナルを発火
という、シンプルな「入力判定コンポーネント」です。
プレイヤー、敵AI、UIボタン、動く床のスイッチなど、
「とりあえず長押し判定が欲しい」ノードにアタッチして、シグナルをつなげるだけで使えます。
フルコード:TapHoldCheck.gd
extends Node
class_name TapHoldCheck
## 任意のノードにアタッチして使える「長押し判定」コンポーネント。
## 指定した Input アクションを監視し、短押し・長押しで別シグナルを発火します。
##
## 特徴:
## - プレイヤー、敵、UIボタンなど、どこでも再利用可能
## - 継承不要、コンポーネントとしてアタッチするだけ
## - 入力の監視方法を「グローバル入力」か「フォーカス中のUI」に切り替え可能
# ---------------------------------------------------------
# 設定パラメータ
# ---------------------------------------------------------
@export_group("Input Settings")
@export var action_name: StringName = "ui_accept":
## 監視する Input アクション名。
## Project Settings > Input Map で定義されている名前を指定します。
set(value):
action_name = value
@export_range(0.05, 5.0, 0.01, "or_greater")
var hold_threshold_seconds: float = 0.35:
## 何秒以上押されていたら「長押し」とみなすか。
## これより短ければ「短押し」とみなします。
set(value):
hold_threshold_seconds = max(0.0, value)
@export var consume_input: bool = false:
## true の場合、このコンポーネントが処理した入力を「消費」します。
## 他の _unhandled_input などに同じ入力を渡したくないときに有効にします。
var _dummy := false setget , _get_dummy
func _get_dummy(): return _dummy
@export_enum("Global(_unhandled_input)", "UI(_gui_input)")
var input_mode: int = 0:
## どの入力ルートで監視するか。
## - Global: _unhandled_input で監視(プレイヤー操作など)
## - UI: このノードが Control の子にある場合、その Control の _gui_input から呼び出す想定
## (UIモードでは、自前で forward_event() を呼ぶ必要があります)
pass
@export_group("Behavior")
@export var enable_while_paused: bool = false:
## 一時停止中(SceneTree.paused = true)でも判定を続けるかどうか。
## true の場合、process_mode を PAUSABLE から ALWAYS に切り替えます。
set(value):
enable_while_paused = value
_update_process_mode()
@export var auto_disable_on_tree_exit: bool = true:
## ノードがツリーから抜けたときに、自動的に状態をリセットするかどうか。
pass
# ---------------------------------------------------------
# シグナル
# ---------------------------------------------------------
## 短押しが確定したときに発火されます。
## 引数: pressed_time - 実際に押されていた時間(秒)
signal tap_short(action_name: StringName, pressed_time: float)
## 長押しが確定したときに発火されます。
## 引数: pressed_time - 実際に押されていた時間(秒)
signal tap_hold(action_name: StringName, pressed_time: float)
## 押し始めた瞬間に発火されます。
signal tap_started(action_name: StringName)
## 長押し状態に「移行した瞬間」に発火されます。
## 例: 0.35秒がしきい値なら、0.35秒に到達したフレームで一度だけ発火。
signal hold_entered(action_name: StringName)
# ---------------------------------------------------------
# 内部状態
# ---------------------------------------------------------
var _is_pressed: bool = false
var _pressed_time: float = 0.0
var _has_become_hold: bool = false
# ---------------------------------------------------------
# ライフサイクル
# ---------------------------------------------------------
func _ready() -> void:
_update_process_mode()
func _update_process_mode() -> void:
process_mode = Node.PROCESS_MODE_ALWAYS if enable_while_paused else Node.PROCESS_MODE_PAUSABLE
func _exit_tree() -> void:
if auto_disable_on_tree_exit:
_reset_state()
# ---------------------------------------------------------
# 入力の受け口
# ---------------------------------------------------------
## Global モード用: シーンツリー全体の未処理入力を監視します。
func _unhandled_input(event: InputEvent) -> void:
if input_mode != 0:
return
if _handle_input_event(event) and consume_input:
get_viewport().set_input_as_handled()
## UI モード用: Control ノードからイベントを渡すためのヘルパー。
## 例:
## func _gui_input(event):
## $TapHoldCheck.forward_event(event)
func forward_event(event: InputEvent) -> void:
if input_mode != 1:
return
_handle_input_event(event)
# ---------------------------------------------------------
# メインロジック
# ---------------------------------------------------------
func _process(delta: float) -> void:
if not _is_pressed:
return
_pressed_time += delta
# まだ「長押し状態」になっておらず、しきい値を超えたら hold_entered を一度だけ発火
if not _has_become_hold and _pressed_time >= hold_threshold_seconds:
_has_become_hold = true
emit_signal("hold_entered", action_name)
## 実際の入力イベント処理。
## 押された/離されたタイミングで内部状態を更新し、必要なシグナルを発火します。
func _handle_input_event(event: InputEvent) -> bool:
var handled := false
# 押された瞬間を検出
if event.is_action_pressed(action_name):
_on_action_pressed()
handled = true
# 離された瞬間を検出
if event.is_action_released(action_name):
_on_action_released()
handled = true
return handled
func _on_action_pressed() -> void:
_is_pressed = true
_pressed_time = 0.0
_has_become_hold = false
emit_signal("tap_started", action_name)
func _on_action_released() -> void:
if not _is_pressed:
return
_is_pressed = false
# 押されていた時間に応じて、短押しか長押しかを判定
var was_hold := _pressed_time >= hold_threshold_seconds
if was_hold:
emit_signal("tap_hold", action_name, _pressed_time)
else:
emit_signal("tap_short", action_name, _pressed_time)
# 状態リセット
_pressed_time = 0.0
_has_become_hold = false
func _reset_state() -> void:
_is_pressed = false
_pressed_time = 0.0
_has_become_hold = false
使い方の手順
ここでは代表的な 2 パターンを例にします。
- ① プレイヤーの攻撃ボタン(短押しで弱攻撃、長押しで溜め攻撃)
- ② UIボタン(短押しで決定、長押しで詳細表示)
手順①:Input Map の設定
まずは、監視したいアクションを Input Map に登録します。
- メニューから
Project > Project Settings...を開く Input Mapタブを選択attackという名前のアクションを追加- キーボードの
Jキーやゲームパッドのボタンなどを割り当てる
UI用なら ui_accept など既存のアクションを使ってもOKです。
手順②:プレイヤーにコンポーネントをアタッチ
プレイヤーシーンの例:
Player (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D └── TapHoldCheck (Node)
- Player シーンを開く
- Player ノードの子として
Nodeを追加し、スクリプトにTapHoldCheck.gdをアタッチ - インスペクタで以下のように設定
action_name:"attack"hold_threshold_seconds: 0.35 〜 0.5 あたり(好みで)input_mode:Global(_unhandled_input)consume_input: 他で同じアクションを使わないなら true でもOK
次に、Player 側のスクリプトでシグナルを受け取ります。
# Player.gd (例)
extends CharacterBody2D
@onready var tap_hold_check: TapHoldCheck = $TapHoldCheck
func _ready() -> void:
# 短押し・長押しのシグナルを接続
tap_hold_check.tap_short.connect(_on_attack_tap_short)
tap_hold_check.tap_hold.connect(_on_attack_tap_hold)
# 短押し: 通常攻撃
func _on_attack_tap_short(action_name: StringName, pressed_time: float) -> void:
# 短押し時間で威力を少し変える、みたいな拡張も可能
print("短押し攻撃!時間: %s 秒" % pressed_time)
_do_normal_attack()
# 長押し: 溜め攻撃
func _on_attack_tap_hold(action_name: StringName, pressed_time: float) -> void:
print("長押し攻撃!時間: %s 秒" % pressed_time)
_do_charged_attack()
func _do_normal_attack() -> void:
# ここに通常攻撃の処理を書く
pass
func _do_charged_attack() -> void:
# ここに溜め攻撃の処理を書く
pass
Player 側は「攻撃の中身」に集中できていて、
「長押し判定ロジック」は完全に TapHoldCheck に追い出されていますね。
手順③:UIボタンでの利用例
次は、UI の Button で短押しと長押しを分けたいケースです。
MenuRoot (Control) ├── VBoxContainer │ └── StartButton (Button) │ └── TapHoldCheck (Node) └── ...
StartButtonの子にNodeを追加し、TapHoldCheck.gdをアタッチ- TapHoldCheck の設定
action_name:"ui_accept"(または UI 用に定義したアクション)input_mode:UI(_gui_input)
UI モードでは、Button 側から _gui_input を経由してイベントを渡します。
# StartButton.gd
extends Button
@onready var tap_hold_check: TapHoldCheck = $TapHoldCheck
func _ready() -> void:
tap_hold_check.tap_short.connect(_on_tap_short)
tap_hold_check.tap_hold.connect(_on_tap_hold)
func _gui_input(event: InputEvent) -> void:
# Button が受け取った UI イベントを TapHoldCheck にそのまま渡す
tap_hold_check.forward_event(event)
func _on_tap_short(action_name: StringName, pressed_time: float) -> void:
print("UIボタン短押し: ゲーム開始")
_start_game()
func _on_tap_hold(action_name: StringName, pressed_time: float) -> void:
print("UIボタン長押し: 詳細メニューを開く")
_open_detail_menu()
func _start_game() -> void:
# シーン切り替えなど
pass
func _open_detail_menu() -> void:
# 詳細設定ウィンドウを開くなど
pass
Button 自体は普通の Button のままで、
「長押しをどう扱うか」だけを TapHoldCheck に任せる形になっています。
手順④:動く床スイッチなど、他の例
例えば、長押ししている間だけ動く床を動かしたい場合:
MovingPlatform (Node2D) ├── Sprite2D ├── CollisionShape2D └── TapHoldCheck (Node)
# MovingPlatform.gd
extends Node2D
@onready var tap_hold_check: TapHoldCheck = $TapHoldCheck
var _is_moving: bool = false
func _ready() -> void:
tap_hold_check.tap_started.connect(_on_tap_started)
tap_hold_check.tap_hold.connect(_on_tap_hold)
tap_hold_check.tap_short.connect(_on_tap_short)
func _process(delta: float) -> void:
if _is_moving:
position.x += 100.0 * delta # 適当な移動処理
func _on_tap_started(action_name: StringName) -> void:
# 押し始めたらとりあえず動かす
_is_moving = true
func _on_tap_hold(action_name: StringName, pressed_time: float) -> void:
# 長押し完了後も動かし続けたいなら何もしない、など
pass
func _on_tap_short(action_name: StringName, pressed_time: float) -> void:
# 短押しだったらすぐ止める
_is_moving = false
「動き方」は MovingPlatform が担当し、「どのボタンをどう解釈するか」は TapHoldCheck が担当、という
きれいな責務分離になっています。
メリットと応用
TapHoldCheck コンポーネントを使うメリットを整理すると:
- ノード構造がスッキリ
「LongPressPlayer」「LongPressButton」みたいな派生シーンを量産しなくて済みます。
どのノードにもTapHoldCheckを 1 個ぶら下げるだけ。 - ロジックの再利用性が高い
プレイヤー、敵、UI、ギミック…すべて同じコンポーネントを使い回し可能。
しきい値やアクション名は@exportで個別に調整できます。 - テストしやすい
入力の判定ロジックが 1 箇所にまとまっているので、
「長押し判定がバグってる?」と思ったら TapHoldCheck だけ見ればOK。 - 合成(Composition)志向の設計
「プレイヤーは CharacterBody2D で、+長押し判定、+HP管理、+ステートマシン…」
と、機能ごとにコンポーネントを足していく設計にしやすくなります。
応用としては、
hold_enteredを使って「長押し中だけチャージエフェクトを出す」pressed_timeを使って「押していた時間に応じて威力やゲージ量を変える」- 複数の TapHoldCheck を同じノードに付けて、「攻撃ボタンは短押し/長押し」「ジャンプボタンは長押しでホバリング」など複合入力
など、かなり遊べます。
改造案:押している間だけコールバックする「ホールド中コールバック」
例えば、「押している間ずっと何かしたい」ケース(チャージゲージの増加など)のために、
「ホールド中に毎フレーム呼ばれるコールバック」を足してみる案です。
# TapHoldCheck.gd のどこかに追加
## 長押し状態の間、毎フレーム呼び出されるコールバックを登録する例。
## 使用側:
## tap_hold_check.set_hold_update_callback(func(dt, t): _update_charge(dt, t))
var _hold_update_callback: Callable = Callable()
func set_hold_update_callback(callback: Callable) -> void:
_hold_update_callback = callback
func _process(delta: float) -> void:
if not _is_pressed:
return
_pressed_time += delta
if not _has_become_hold and _pressed_time >= hold_threshold_seconds:
_has_become_hold = true
emit_signal("hold_entered", action_name)
# 長押し状態なら、毎フレームコールバックを呼ぶ
if _has_become_hold and _hold_update_callback.is_valid():
_hold_update_callback.call(delta, _pressed_time)
これで、使用側はこんな感じで「チャージゲージを溜める」処理を分離できます。
# Player.gd の _ready などで
tap_hold_check.set_hold_update_callback(
func(delta: float, pressed_time: float) -> void:
_charge_gauge = clampf(_charge_gauge + delta * 0.5, 0.0, 1.0)
)
このように、TapHoldCheck 自体も「小さな責務」に保ちながら、
必要に応じてコールバックやシグナルを足していくと、
継承地獄に陥らずに、どんどん柔軟な入力システムを構築できます。
ぜひ、自分のプロジェクトでも「長押し判定」をコンポーネント化してみてください。
