【Godot】キーコンフィグを作る

はじめに

PC向けにゲームを作ると様々な入力デバイスがあることに気づきます。

Godotではプロジェクト設定からインプットマップを設定できますね。

f:id:ueshita:20200909140848p:plain

複数のデバイスを1つのactionに割り当てることができます。

しかし、キーアサインを好みに設定したいプレイヤーもいて、キーコンフィグ機能があるゲームも多いです。

本記事ではインプットマップを実行時に設定する方法を解説します。

InputMapを使う

プロジェクト設定から設定したインプットマップは、InputMapというAPIでアクセスできます。今回はこのAPIを主に使います。

docs.godotengine.org

キーコンフィグを作る

今回は下記のようにキーボードとゲームパッド両方から操作できるような設定です。

f:id:ueshita:20200909140848p:plain

InputMapの設定

ここでは入力イベントを受け取り、InputMapに上書きする方法を解説します。

InputMap設定メソッド

InputMapに設定します。設定を上書きするAPIは存在しないので、一旦InputMap.action_erase_eventsで削除してからInputMap.action_add_eventでInputEventを設定します。

func set_inputmap(action: String, event_index: int, event: InputEvent):
    # InputMapからactionを取得
    var list = InputMap.get_action_list(action)
    # actionを一旦削除
    InputMap.action_erase_events(action)
    # リストを上書き
    list[event_index] = event
    # actionを改めて追加
    InputMap.action_add_event(action, list[0])  # キー設定
    InputMap.action_add_event(action, list[1])  # パッド設定

入力を受けるメソッド

_gui_input で入力を受け付けます (Controlノードにスクリプトを付けます)

受け取ったInputEventをset_inputmapに設定します。

func _gui_input(event: InputEvent):
    var action = "act_jump"  # 暫定的にact_jump固定
    if event is InputEventKey:
        # キー入力された
        var key := event as InputEventKey
        if key.is_pressed():
            set_inputmap(action, 0, event)
    elif event is InputEventJoypadButton:
        # パッド(ボタン)入力された
        var joypad_button := event as InputEventJoypadButton
        if joypad_button.is_pressed():
            set_inputmap(action, 1, event)
    elif event is InputEventJoypadMotion:
        # パッド(スティック)入力された
        var joypad_motion := event as InputEventJoypadMotion
        if joypad_motion.axis_value != 0:
            set_inputmap(action, 1, event)

InputMapのセーブとロード

InputMapは再起動するとプロジェクト設定のデータにリセットされます。

キーコンフィグ機能として成り立たせるためには、設定時にファイルにコンフィグ情報を保存して、ゲーム起動時にロードする必要があります。

保存対象のaction

const input_actions := [
    "act_attack",
    "act_jump",
    "act_up",
    "act_down",
    "act_left",
    "act_right",
    "ui_accept",
    "ui_cancel",
    "ui_up",
    "ui_down",
    "ui_left",
    "ui_right",
]

InputMapのシリアライズ

設定したInputMapを保存するときに使います。

func _serialize_input() -> Dictionary:
    var data = {}
    for action in input_actions:
        var list = InputMap.get_action_list(action)
        var item = {}
        item["key"] = list[0].scancode
        
        if list[1] is InputEventJoypadButton:
            item["button"] = list[1].button_index
        elif list[1] is InputEventJoypadMotion:
            item["axis"] = list[1].axis
            item["axis_dir"] = 1 if list[1].axis_value > 0 else -1

        data[action] = item

    return data

InputMapのデシリアライズ

保存されたコンフィグ情報をInputMapに再設定するときに使います。

func _deserialize_input(data):
    if not data is Dictionary:
        return

    for action_name in data:
        var item = data[action_name] as Dictionary
        if not item:
            continue
        
        var list = InputMap.get_action_list(action_name) as Array
        if not list:
            continue
        
        if item.has("key"):
            var keycode = item["key"]
            if typeof(keycode) == TYPE_REAL:
                list[0].scancode = keycode
        
        if item.has("button"):
            var button_index = item["button"]
            if typeof(button_index) == TYPE_REAL:
                var event = InputEventJoypadButton.new()
                event.button_index = button_index
                list[1] = event
        elif item.has("axis") and item.has("axis_dir"):
            var axis_id = item["axis"]
            var axis_dir = item["axis_dir"]
            if typeof(axis_id) == TYPE_REAL and typeof(axis_dir) == TYPE_REAL:
                var event = InputEventJoypadMotion.new()
                event.axis = axis_id
                event.axis_value = 1 if axis_dir > 0 else -1
                list[1] = event
        
        InputMap.action_erase_events(action_name)
        InputMap.action_add_event(action_name, list[0])
        InputMap.action_add_event(action_name, list[1])

JSONにセーブ

シリアライズしたInputMapの情報をJSON化してファイルに保存します。

func savefile():   
    var file := File.new()
    if file.open("user://settings.json", File.WRITE) != OK:
        return

    var jsonstr := JSON.print({
        "input": _serialize_input(),
    }, "  ")

    file.store_string(jsonstr)
    file.close()

JSONからロード

JSON形式で読み込んだ情報をInputMapに再設定します。

func loadfile():
    var file := File.new()
    if file.open("user://settings.json", File.READ) != OK:
        return

    var jsonstr := file.get_as_text()
    file.close()
    
    if not jsonstr:
        return
    
    var json_res := JSON.parse(jsonstr)
    if json_res.error:
        printerr("failed to load settings: " + json_res.error_string)
    
    var json := json_res.result as Dictionary

    _deserialize_input(json.get("input"))

注意

JSONはユーザーでもテキストエディタを使っていじれるため、 エラー処理をしっかりしないとセキュリティホールになるかもしれないので注意です。