Godotでキーコンフィグをちゃんと実装しようとすると、思ったより面倒なんですよね。
- InputMap はプロジェクト設定(Project Settings)で固定しがち
- UI から「今押したキーをこのアクションに割り当て直す」仕組みを自前で書く必要がある
- シーンごとにバラバラに処理を書くと、あとから管理不能になる
さらに、ありがちな実装としては「Player.gd の中にキーコンフィグ処理も全部ねじ込む」というパターンがあります。すると、
- Player のスクリプトが肥大化してテストしづらい
- メニューシーンやオプションシーンからも Player のスクリプトに依存してしまう
- 別プロジェクトに持っていきにくい
そこで今回は、「どのシーンにもポン付けできる汎用コンポーネント」として、InputMap のアクションに対してプレイヤーが押したキーを割り当て直す KeyConfig コンポーネント を作ってみましょう。継承ではなく「合成(Composition)」で、キーコンフィグ機能をあと付けするイメージですね。
【Godot 4】押したキーをそのまま割り当て!「KeyConfig」コンポーネント
このコンポーネントは、
- 指定したアクション名(例:
"move_left","jump")に対して - ユーザーが次に押したキー(キーボード or ゲームパッドボタン)を検出し
- その入力を InputMap に登録し直す
という処理を、どのシーンからでも簡単に使えるようにしたものです。
フルコード:KeyConfig.gd
extends Node
class_name KeyConfig
## 汎用キーコンフィグ用コンポーネント
## - InputMap のアクションに対して、ユーザーが押したキーを割り当て直す
## - UI(ボタンなど)から「このアクションを設定したい」と呼び出して使う
signal waiting_started(action_name: String)
signal waiting_canceled(action_name: String)
signal key_assigned(action_name: String, event: InputEvent)
signal key_assignment_failed(action_name: String, reason: String)
## 設定を JSON で保存する簡易オプション
@export var enable_save: bool = true
## 設定ファイルのパス(user:// はユーザーデータフォルダ)
@export var save_path: String = "user://key_config.json"
## 設定変更時に、既存の割り当てをすべて消してから新しいキーだけを登録するかどうか
@export var clear_existing_on_assign: bool = true
## どのデバイス入力を受け付けるかのフラグ
@export var accept_keyboard: bool = true
@export var accept_mouse: bool = false
@export var accept_joypad_button: bool = true
@export var accept_joypad_axis: bool = false
## 連打による誤登録を防ぐための、入力受付までの待ち時間(秒)
@export var wait_delay_sec: float = 0.1
## 現在「どのアクションの割り当て待ち」をしているか
var _waiting_action: StringName = ""
## 待ち状態に入った時間
var _wait_start_time: float = 0.0
## 内部的に使用する一時フラグ
var _is_waiting: bool = false
func _ready() -> void:
# 起動時に保存ファイルがあれば読み込む
if enable_save:
_load_from_file()
func _unhandled_input(event: InputEvent) -> void:
# 何も待っていないときはスルー
if not _is_waiting:
return
# wait_delay_sec 経過前は誤入力とみなしてスルー
if Time.get_ticks_msec() / 1000.0 - _wait_start_time < wait_delay_sec:
return
# 押された瞬間のイベントだけを拾う
if not _is_press_event(event):
return
# 許可されている入力種別かチェック
if not _is_accepted_event(event):
key_assignment_failed.emit(str(_waiting_action), "unsupported_input_type")
return
# 実際に InputMap に登録する
_assign_event_to_action(_waiting_action, event)
# 保存設定が有効ならファイルに書き出し
if enable_save:
_save_to_file()
key_assigned.emit(str(_waiting_action), event)
_clear_waiting_state()
# --- 公開 API --------------------------------------------------------------
## 指定したアクション名に対する「次に押したキー」を登録するモードに入る
func start_waiting(action_name: StringName) -> void:
_waiting_action = action_name
_wait_start_time = Time.get_ticks_msec() / 1000.0
_is_waiting = true
waiting_started.emit(str(_waiting_action))
## 待ち状態をキャンセルする(例:キャンセルボタンを押したときなど)
func cancel_waiting() -> void:
if not _is_waiting:
return
var tmp := _waiting_action
_clear_waiting_state()
waiting_canceled.emit(str(tmp))
## 現在待ち状態かどうか
func is_waiting() -> bool:
return _is_waiting
## 指定アクションに現在登録されている InputEvent の配列を取得
func get_events_for_action(action_name: StringName) -> Array[InputEvent]:
return InputMap.action_get_events(action_name)
## 指定アクションの割り当てをすべて削除
func clear_action(action_name: StringName) -> void:
InputMap.action_erase_events(action_name)
if enable_save:
_save_to_file()
## すべてのアクションの割り当てを削除(リセット用)
func clear_all_actions() -> void:
for action_name in InputMap.get_actions():
InputMap.action_erase_events(action_name)
if enable_save:
_save_to_file()
# --- 内部処理 --------------------------------------------------------------
func _clear_waiting_state() -> void:
_waiting_action = ""
_is_waiting = false
_wait_start_time = 0.0
func _is_press_event(event: InputEvent) -> bool:
# 「押した瞬間」のイベントだけを拾いたいので is_pressed() を利用
if event is InputEventKey:
return event.pressed and not event.echo
if event is InputEventMouseButton:
return event.pressed
if event is InputEventJoypadButton:
return event.pressed
if event is InputEventJoypadMotion:
# 軸は「ある程度以上倒した瞬間」を押下とみなす
return abs(event.axis_value) >= 0.5
return false
func _is_accepted_event(event: InputEvent) -> bool:
if event is InputEventKey:
return accept_keyboard
if event is InputEventMouseButton:
return accept_mouse
if event is InputEventJoypadButton:
return accept_joypad_button
if event is InputEventJoypadMotion:
return accept_joypad_axis
return false
func _assign_event_to_action(action_name: StringName, event: InputEvent) -> void:
if clear_existing_on_assign:
InputMap.action_erase_events(action_name)
# 既に同じイベントが登録されている場合は何もしない
for existing in InputMap.action_get_events(action_name):
if existing.is_match(event, true):
return
InputMap.action_add_event(action_name, event)
# --- 保存/読み込み処理 -----------------------------------------------------
## 現在の InputMap を JSON としてファイルに保存する
func _save_to_file() -> void:
var data: Dictionary = {}
for action_name in InputMap.get_actions():
var events: Array = []
for e in InputMap.action_get_events(action_name):
# InputEvent は Dictionary に変換してシリアライズする
events.append(e.as_text()) # ここでは簡易的に文字列表現を保存
data[action_name] = events
var file := FileAccess.open(save_path, FileAccess.WRITE)
if file == null:
push_warning("KeyConfig: Failed to open save file: %s" % save_path)
return
file.store_string(JSON.stringify(data, " "))
## 保存ファイルから InputMap を復元する
func _load_from_file() -> void:
if not FileAccess.file_exists(save_path):
return
var file := FileAccess.open(save_path, FileAccess.READ)
if file == null:
push_warning("KeyConfig: Failed to open save file: %s" % save_path)
return
var text := file.get_as_text()
var result := JSON.parse_string(text)
if typeof(result) != TYPE_DICTIONARY:
push_warning("KeyConfig: Invalid JSON format in %s" % save_path)
return
var data: Dictionary = result
# 既存を全消ししてから復元
for action_name in InputMap.get_actions():
InputMap.action_erase_events(action_name)
for action_name in data.keys():
var events: Array = data[action_name]
for event_text in events:
if typeof(event_text) != TYPE_STRING:
continue
var event := _event_from_text(event_text)
if event:
InputMap.action_add_event(action_name, event)
## as_text() で保存した文字列から InputEvent を復元する簡易版
## ※ Godot 公式の完全な逆変換はないので、よく使うキー/ボタンを中心に対応
func _event_from_text(text: String) -> InputEvent:
# 例: "InputEventKey : keycode=KEY_A, physical_keycode=KEY_A"
if text.begins_with("InputEventKey"):
var ev := InputEventKey.new()
# 超簡易パース(本気でやるなら正規表現などを使う)
var keycode_str := _extract_after(text, "keycode=")
if keycode_str != "":
var code := @GDScript.NameConstant(keycode_str) if keycode_str.begins_with("KEY_") else 0
ev.keycode = code
return ev
if text.begins_with("InputEventJoypadButton"):
var evb := InputEventJoypadButton.new()
var button_index_str := _extract_after(text, "button_index=")
if button_index_str != "":
evb.button_index = int(button_index_str)
return evb
# 必要に応じてマウスや軸も拡張可能
return null
func _extract_after(text: String, key: String) -> String:
var idx := text.find(key)
if idx == -1:
return ""
var sub := text.substr(idx + key.length())
var comma := sub.find(",")
if comma != -1:
sub = sub.substr(0, comma)
return sub.strip_edges()
上のコードはそのまま KeyConfig.gd として保存して OK です。最低限の JSON 保存・読み込みも含めてあるので、「一度設定したキーを次回起動時にも復元する」ところまでカバーしています。
使い方の手順
① コンポーネントをシーンに置く
まずはどこかのシーンに KeyConfig ノードを 1 個置きましょう。ゲーム全体で 1 つのオプションメニューを使う場合は、オプションシーンに置くのが分かりやすいです。
OptionsMenu (Control)
├── VBoxContainer
│ ├── Button ("Jump"再設定ボタン)
│ └── Button ("Shoot"再設定ボタン)
└── KeyConfig (Node)
- KeyConfig ノードに、上記の
KeyConfig.gdをアタッチします。 - インスペクタから
enable_saveやsave_pathを好みに合わせて設定しましょう。
② ボタンから KeyConfig を呼び出す
例えば「Jump のキーを変更する」ボタンに次のようなスクリプトを付けます。
extends Button
@onready var key_config: KeyConfig = get_node("../KeyConfig")
func _ready() -> void:
pressed.connect(_on_pressed)
# Signal をつないで UI 表示も更新できる
key_config.waiting_started.connect(_on_waiting_started)
key_config.key_assigned.connect(_on_key_assigned)
func _on_pressed() -> void:
# "jump" アクションのキー設定を開始
key_config.start_waiting("jump")
func _on_waiting_started(action_name: String) -> void:
if action_name == "jump":
text = "Press a key..."
func _on_key_assigned(action_name: String, event: InputEvent) -> void:
if action_name == "jump":
text = "Jump: " + event.as_text()
これで、ボタンをクリックしたあとに押したキー(またはゲームパッドボタン)が "jump" アクションに割り当てられます。
③ プレイヤー側は InputMap をそのまま使うだけ
プレイヤーのスクリプトは、特にキーコンフィグを意識する必要はありません。普通に Input.is_action_pressed() を使うだけです。
extends CharacterBody2D
@export var speed: float = 200.0
func _physics_process(delta: float) -> void:
var dir := Vector2.ZERO
if Input.is_action_pressed("move_left"):
dir.x -= 1.0
if Input.is_action_pressed("move_right"):
dir.x += 1.0
if Input.is_action_just_pressed("jump"):
_jump()
velocity.x = dir.x * speed
move_and_slide()
func _jump() -> void:
# ジャンプ処理
pass
キーコンフィグで "jump" に何を割り当てようが、プレイヤー側は同じコードで動き続けます。まさに「合成による後付け機能」ですね。
④ 別シーンでもそのまま再利用する
コンポーネント化しているので、例えばタイトル画面に簡易オプションを置きたい場合も、同じ KeyConfig をペタっと貼るだけで済みます。
TitleScreen (Control) ├── StartButton ├── OptionsButton └── KeyConfig (Node)
Options シーンを開いたときに、そこにある KeyConfig が JSON を読んでくれるので、どのシーンからでも同じ設定を共有できます。
メリットと応用
- シーン構造がスッキリ
プレイヤーや UI のスクリプトにキーコンフィグロジックを埋めこまず、KeyConfigノードに集約できます。 - 使い回しが簡単
別プロジェクトにキーコンフィグ機能を持っていきたいときも、KeyConfig.gdとシーンに置くノード 1 個で完結します。 - テストしやすい
KeyConfigだけを単体で動かし、「このアクションにこのキーが入るか?」を確認できるので、バグ切り分けが楽になります。 - 合成で拡張しやすい
「ゲームパッドだけ別設定にしたい」「プレイヤー 1 / 2 で別の設定を持ちたい」なども、KeyConfig を複数置く or 派生バージョンを作るだけで対応できます。
例えば、「特定のアクションだけは上書き禁止」にしたいケースを考えてみましょう。ゲームの根幹に関わる "ui_cancel" などを守りたい場合ですね。そんなときは、次のような簡単な改造ができます。
## KeyConfig.gd に追加する例
@export var locked_actions: Array[StringName] = []
func start_waiting(action_name: StringName) -> void:
if action_name in locked_actions:
key_assignment_failed.emit(str(action_name), "locked_action")
return
_waiting_action = action_name
_wait_start_time = Time.get_ticks_msec() / 1000.0
_is_waiting = true
waiting_started.emit(str(_waiting_action))
locked_actions に "ui_cancel" や "ui_accept" を登録しておけば、それらは UI から変更できないようにできます。こういう「細かいルール」を後から足していけるのも、コンポーネント化の強みですね。
ぜひ、自分のプロジェクト用に少しずつカスタマイズしながら、「継承より合成」スタイルのキーコンフィグを育てていきましょう。
