Unity ゲーム開発 生成AI

Unity素人がAI活用で進める独学個人ゲーム開発 #14_キーバインド変更設定

記事の目的

前回、Inputsystemの実装を行いました。
前回記事 ⇒ #13_Inputsystem

次はオプションからキー設定を変更できるようにしたいと考えていました。
…が問題が発生し、上手く進めることができず、挫折しました。

キーリバインド機能

要件はこんな感じ

  • オプション画面から変更
  • 移動、攻撃、隊列変更のキーバインド変更が可能
  • キーボード操作時はキーボード、コントローラー操作時はコントローラーの表示にする
  • ボタンを押すと点滅してキーバインド変更受付状態になる


各ボタンに対して変更の処理はできるのですが、コントローラーで操作してもキーボード用の表示にしかなりません。また、ボタンのところは文字ではなく画像を表示するつもりですがそれも反映できていません。

進められなくなった主な原因としては
①スクリプトやオブジェクトを実装していく
②それを記憶するように頼む
③記憶したものを使ってと頼むと違うものが出てくる
という感じ

スクリプト

ある程度進んだ状態から進めますが現在のスクリプトを渡して改善をお願いしています。

各スクリプトの改善例を全文表示してください。必要であればそれぞれ別チャットに分けてください
   ①

ChatGPTの回答(クリックで展開)
保存されたメモリを更新しました

了解しました。以下に、貼り付けていただいた7つのスクリプトを役割ごとに分析・整理し、責務の重複を解消・改善したリファクタリング案を提示します。

チャット分割ルール:

各スクリプトごとに別チャットで解説・改善例を提示します(合計7回に分けて投稿します)。

🔧 改善スクリプト①:InputDeviceUpdater.cs(改善済)

目的:デバイスごとにUIテキストとアイコンを切り替える表示処理を担当。

using UnityEngine;
using UnityEngine.UI;
using TMPro;

public class InputDeviceUpdater : MonoBehaviour
{
    [Header("アクションと表示データ")]
    public string actionName;  // 例: "MoveUp"
    public ActionDisplayTable displayTable;

    [Header("UI参照")]
    public TextMeshProUGUI labelText;
    public Image iconImage;

    void OnEnable()
    {
        InputDeviceManager.OnDeviceChanged += UpdateUI;
        UpdateUI(InputDeviceManager.GetCurrentDeviceType());
    }

    void OnDisable()
    {
        InputDeviceManager.OnDeviceChanged -= UpdateUI;
    }

    private void UpdateUI(InputDeviceManager.DeviceType deviceType)
    {
        if (displayTable == null || string.IsNullOrEmpty(actionName)) return;

        var entry = displayTable.GetEntry(actionName, deviceType);
        if (entry == null) return;

        if (labelText != null)
            labelText.text = entry.displayText;

        if (iconImage != null)
        {
            iconImage.sprite = entry.buttonIcon;
            iconImage.enabled = (entry.buttonIcon != null);
        }
    }
}

改善点まとめ

項目 内容
不要ログの削除 Debugログを削除し、軽量化
nullチェック統合 displayTableとactionNameを事前にチェック
機能重複排除 RebindButtonと重複していた表示切替の責務は RebindButton.cs に集約予定

ChatGPTの回答(クリックで展開)
こちらが改善案【② InputRebindManager.cs】です。

🔧 改善スクリプト②:InputRebindManager.cs(改善済)

目的:

  • 入力アクションの保存・読み込み・全リセット
  • 現在の入力デバイス表示と切替通知
  • RebindButton群の初期表示更新
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.LowLevel;

public class InputRebindManager : MonoBehaviour
{
    [Header("Input Actions アセット")]
    [SerializeField] private TestInput inputActions;

    [Header("UI 表示要素")]
    public TextMeshProUGUI currentDeviceText;
    public Button resetButton;
    public RebindButton[] rebindButtons;

    private const string RebindsKey = "rebinds";

    private void Awake()
    {
        if (inputActions == null)
            inputActions = new TestInput();
    }

    private void OnEnable()
    {
        inputActions.Enable();
        InputSystem.onEvent += HandleInputEvent;
        LoadBindingOverrides();

        if (resetButton != null)
            resetButton.onClick.AddListener(ResetAllBindings);
    }

    private void OnDisable()
    {
        inputActions.Disable();
        InputSystem.onEvent -= HandleInputEvent;

        if (resetButton != null)
            resetButton.onClick.RemoveListener(ResetAllBindings);
    }

    // 入力イベントを受け取って現在のデバイスを更新
    private void HandleInputEvent(InputEventPtr eventPtr, InputDevice device)
    {
        if (!eventPtr.IsA<StateEvent>() && !eventPtr.IsA<DeltaStateEvent>())
            return;

        UpdateCurrentDevice(device);
    }

    // 入力デバイス表示と通知
    private void UpdateCurrentDevice(InputDevice device)
    {
        if (currentDeviceText != null)
            currentDeviceText.text = $"現在の入力デバイス:{device.displayName}";

        if (device is Gamepad)
            InputDeviceManager.ChangeDevice(InputDeviceManager.DeviceType.Gamepad);
        else if (device is Keyboard || device is Mouse)
            InputDeviceManager.ChangeDevice(InputDeviceManager.DeviceType.Keyboard);
    }

    // バインド全リセット処理
    private void ResetAllBindings()
    {
        foreach (var map in inputActions.asset.actionMaps)
        {
            map.RemoveAllBindingOverrides();
        }

        PlayerPrefs.DeleteKey(RebindsKey);
        PlayerPrefs.Save();

        foreach (var button in rebindButtons)
        {
            button.ClearOverride(); // overridePathも初期化
        }
    }

    // JSONとして保存
    public void SaveBindingOverrides()
    {
        string json = inputActions.asset.SaveBindingOverridesAsJson();
        PlayerPrefs.SetString(RebindsKey, json);
        PlayerPrefs.Save();
    }

    // JSONを読み込み反映
    private void LoadBindingOverrides()
    {
        if (PlayerPrefs.HasKey(RebindsKey))
        {
            string json = PlayerPrefs.GetString(RebindsKey);
            if (!string.IsNullOrEmpty(json))
                inputActions.asset.LoadBindingOverridesFromJson(json);
        }
    }
}

改善点まとめ

項目 内容
メソッドの責務を明確化 HandleInputEvent・UpdateCurrentDevice に分離
マジックストリング除去 "rebinds" を定数化
RebindButtonとの役割分離 個々のバインド更新責務は RebindButton 側に任せる
セーブ/ロード統一 PlayerPrefs 処理をメソッド化し再利用性向上

ChatGPTの回答(クリックで展開)
こちらが改善案【③ DisplayTableDebugger.cs】です。

🔧 改善スクリプト③:DisplayTableDebugger.cs(改善済)

目的:

  • ActionDisplayTable の内容を開発中に確認するデバッグ用スクリプト
  • Editor 専用に限定し、間違ってビルドに含めないようにする
#if UNITY_EDITOR
using UnityEngine;

[ExecuteInEditMode]
public class DisplayTableDebugger : MonoBehaviour
{
    public ActionDisplayTable displayTable;

    void Start()
    {
        if (displayTable == null)
        {
            Debug.LogError("[DisplayTableDebugger] displayTable が設定されていません");
            return;
        }

        Debug.Log($"[DisplayTableDebugger] {displayTable.entries.Count} 件のエントリを検出");

        foreach (var entry in displayTable.entries)
        {
            string device = entry.deviceType.ToString();
            string action = entry.actionName;
            string text = entry.displayText ?? "(null)";
            string iconName = (entry.buttonIcon != null) ? entry.buttonIcon.name : "(null)";

            Debug.Log($"[Entry] Action: {action}, Device: {device}, Text: '{text}', Icon: {iconName}");
        }
    }
}
#endif

改善点まとめ

項目 内容
#if UNITY_EDITOR を使用 ビルド時に不要なログ出力を含めないためにガード
ExecuteInEditMode 属性 Play しなくてもデバッグ確認できるように
null チェック改善 null の代わりに (null) を明示表示
エントリ数のログ出力 全体の件数を表示して視認性アップ

ChatGPTの回答(クリックで展開)
こちらが改善案【④ InputDeviceManager.cs】です。

🔧 改善スクリプト④:InputDeviceManager.cs(改善済)

目的:

  • 現在の入力デバイス(キーボード / ゲームパッド)を判別し、変更時に通知する
  • 他システム(UIなど)に OnDeviceChanged で通知可能にする
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.LowLevel;
using System;

public class InputDeviceManager : MonoBehaviour
{
    public enum DeviceType
    {
        Keyboard,
        Gamepad
    }

    public static event Action<DeviceType> OnDeviceChanged;

    private static DeviceType currentDevice = DeviceType.Keyboard;
    private static bool initialized = false;

    public static DeviceType GetCurrentDeviceType() => currentDevice;

    public static void ChangeDevice(DeviceType newDevice)
    {
        if (newDevice != currentDevice)
        {
            currentDevice = newDevice;
            OnDeviceChanged?.Invoke(currentDevice);
            Debug.Log($"[InputDeviceManager] Device changed to {newDevice}");
        }
    }

    private void OnEnable()
    {
        if (!initialized)
        {
            InputSystem.onEvent += OnInputEvent;
            initialized = true;
        }
    }

    private void OnDisable()
    {
        if (initialized)
        {
            InputSystem.onEvent -= OnInputEvent;
            initialized = false;
        }
    }

    private void OnInputEvent(InputEventPtr eventPtr, InputDevice device)
    {
        if (!eventPtr.IsA<StateEvent>() && !eventPtr.IsA<DeltaStateEvent>())
            return;

        // マウスだけの入力には反応しない(クリック誤検知防止)
        if (device is Mouse) return;

        if (device is Gamepad)
        {
            ChangeDevice(DeviceType.Gamepad);
        }
        else if (device is Keyboard)
        {
            ChangeDevice(DeviceType.Keyboard);
        }
    }
}

改善点まとめ

改善箇所 内容
initialized フラグ 二重登録の防止(再生成されても多重に onEvent += されない)
Mouse 無視 マウスクリックによる誤検知(ゲームパッド→マウス切り替わり)を防止
簡潔な => 式 GetCurrentDeviceType() を1行に簡略化
コメント追加 機能ごとの意図が明確に

ChatGPTの回答(クリックで展開)
以下が改善案【⑤ ActionDisplayTable.cs】です。

🔧 改善スクリプト⑤:ActionDisplayTable.cs(改善済)

目的:

  • 各 InputAction のデバイス別表示データ(テキスト + アイコン)を ScriptableObject で管理する
  • 他スクリプト(InputDeviceUpdater, RebindButton など)から GetEntry() 経由で柔軟に参照できるようにする
using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(fileName = "ActionDisplayTable", menuName = "Input/Action Display Table")]
public class ActionDisplayTable : ScriptableObject
{
    [System.Serializable]
    public class Entry
    {
        public string actionName;
        public InputDeviceManager.DeviceType deviceType;
        public string displayText;
        public Sprite buttonIcon;
    }

    [Tooltip("表示切り替え用データ一覧")]
    public List<Entry> entries = new List<Entry>();

    /// <summary>
    /// 指定されたアクション名とデバイス種別に対応する表示データを取得
    /// </summary>
    /// <param name="actionName">例: "MoveUp"</param>
    /// <param name="deviceType">Keyboard or Gamepad</param>
    /// <returns>対応するEntry。なければ null</returns>
    public Entry GetEntry(string actionName, InputDeviceManager.DeviceType deviceType)
    {
        if (string.IsNullOrEmpty(actionName))
        {
            Debug.LogWarning("[ActionDisplayTable] actionName is null or empty");
            return null;
        }

        var entry = entries.Find(e => e.actionName == actionName && e.deviceType == deviceType);

        if (entry == null)
        {
            Debug.LogWarning($"[ActionDisplayTable] Entry not found for action '{actionName}' and device '{deviceType}'");
        }

        return entry;
    }
}

改善点まとめ

改善箇所 内容
nullチェック強化 actionName が null / 空文字 の場合も警告表示
コメント充実 GetEntry() に対して機能説明を明記
命名統一 e => e.actionName == ... でラムダ変数を entry → e に簡潔化(内部的に)
Tooltip追加 entries フィールドの説明強化(インスペクター用)

ChatGPTの回答(クリックで展開)
以下が改善案【⑥ RebindButton.cs】です。

🔧 改善スクリプト⑥:RebindButton.cs(改善済)

目的:

  • InputSystem のバインド変更(リバインド)処理と UI 表示更新を担う
  • ActionDisplayTable によるデバイス依存の表示切り替えにも対応
using System.Collections;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.UI;
using TMPro;

public class RebindButton : MonoBehaviour
{
    [Header("Input Action Info")]
    public InputActionReference actionReference;
    public int bindingIndex = 0;

    [Header("UI References")]
    public Button bindButton;
    public TextMeshProUGUI bindText;
    public Image iconImage;

    [Header("Display Table")]
    public ActionDisplayTable displayTable;

    private string originalBindingDisplay;
    private Coroutine blinkCoroutine;
    private bool isRebinding = false;
    private InputBinding overrideBinding;

    void Awake()
    {
        if (bindButton == null)
            bindButton = GetComponentInChildren<Button>();
    }

    void OnEnable()
    {
        bindButton.onClick.AddListener(StartRebinding);
        InputDeviceManager.OnDeviceChanged += UpdateBindingDisplay;
        UpdateBindingDisplay(InputDeviceManager.GetCurrentDeviceType());
    }

    void OnDisable()
    {
        bindButton.onClick.RemoveListener(StartRebinding);
        InputDeviceManager.OnDeviceChanged -= UpdateBindingDisplay;
    }

    void Start()
    {
        UpdateBindingDisplay(InputDeviceManager.GetCurrentDeviceType());
    }

    /// <summary>
    /// テキストおよびアイコン表示を更新(ActionDisplayTable優先、なければInputSystemの表示)
    /// </summary>
    public void UpdateBindingDisplay(InputDeviceManager.DeviceType deviceType)
    {
        string actionName = actionReference?.action?.name;
        if (string.IsNullOrEmpty(actionName))
        {
            Debug.LogWarning("[RebindButton] Action name is missing.");
            return;
        }

        var entry = displayTable?.GetEntry(actionName, deviceType);
        if (entry != null)
        {
            if (bindText != null)
                bindText.text = entry.displayText;

            if (iconImage != null)
            {
                iconImage.sprite = entry.buttonIcon;
                iconImage.enabled = (entry.buttonIcon != null);
            }
        }
        else
        {
            // Fallback 表示(InputSystemのバインディング名)
            string display = actionReference.action.GetBindingDisplayString(bindingIndex);
            if (bindText != null)
                bindText.text = display;
            if (iconImage != null)
                iconImage.enabled = false;
        }

        originalBindingDisplay = bindText != null ? bindText.text : "";
    }

    /// <summary>
    /// インタラクティブリバインド開始
    /// </summary>
    public void StartRebinding()
    {
        if (isRebinding) return;

        isRebinding = true;
        overrideBinding = actionReference.action.bindings[bindingIndex];

        blinkCoroutine = StartCoroutine(BlinkText());
        actionReference.action.Disable();

        actionReference.action.PerformInteractiveRebinding(bindingIndex)
            .WithControlsExcluding("Mouse")
            .WithCancelingThrough("<Keyboard>/escape")
            .OnMatchWaitForAnother(0.1f)
            .OnCancel(operation =>
            {
                operation.Dispose();
                CancelRebinding();
            })
            .OnComplete(operation =>
            {
                operation.Dispose();
                overrideBinding.overridePath = actionReference.action.bindings[bindingIndex].effectivePath;

                UpdateBindingDisplay(InputDeviceManager.GetCurrentDeviceType());

                isRebinding = false;
                if (blinkCoroutine != null) StopCoroutine(blinkCoroutine);
                if (bindText != null) bindText.alpha = 1f;

                actionReference.action.Enable();
                // ※必要であれば Apply ボタンに通知を送る処理を追加
            })
            .Start();
    }

    /// <summary>
    /// Escでキャンセルされたときの処理
    /// </summary>
    void CancelRebinding()
    {
        isRebinding = false;

        if (bindText != null)
        {
            bindText.text = originalBindingDisplay;
            bindText.alpha = 1f;
        }

        if (blinkCoroutine != null)
            StopCoroutine(blinkCoroutine);

        actionReference.action.Enable();
    }

    /// <summary>
    /// バインド中にテキストを点滅表示
    /// </summary>
    IEnumerator BlinkText()
    {
        float interval = 0.5f;
        while (true)
        {
            if (bindText != null)
                bindText.alpha = (bindText.alpha < 0.5f) ? 1f : 0.3f;
            yield return new WaitForSecondsRealtime(interval);
        }
    }

    /// <summary>
    /// 一時的なオーバーライドを適用
    /// </summary>
    public void ApplyOverride()
    {
        if (!string.IsNullOrEmpty(overrideBinding.overridePath))
        {
            actionReference.action.ApplyBindingOverride(bindingIndex, overrideBinding.overridePath);
        }
    }

    /// <summary>
    /// オーバーライドを削除
    /// </summary>
    public void ClearOverride()
    {
        overrideBinding.overridePath = null;
        actionReference.action.RemoveBindingOverride(bindingIndex);
        UpdateBindingDisplay(InputDeviceManager.GetCurrentDeviceType());
    }

    /// <summary>
    /// 未適用の変更があるかチェック
    /// </summary>
    public bool HasPendingChange()
    {
        return !string.IsNullOrEmpty(overrideBinding.overridePath);
    }
}

改善点まとめ

改善箇所 内容
コメント整備 すべての関数に用途を明記(ドキュメント化)
安定性向上 actionName の null チェックと警告
可読性向上 冗長な処理を簡潔に、構造を整理
拡張性 ApplyOverride() などを将来の保存機能にも活用しやすく

この6つが主なスクリプトです。
これを起点に改善を進めていくつもりでした。

この6スクリプトはメモリに保存して今後確実に読み込み・更新ができるようにしておいてください

ChatGPTの回答(クリックで展開)
保存されたメモリを更新しました

了解しました。以下の7スクリプトは、今後の開発で確実に読み込み・更新できるよう、メモリに保存済みです。

✅ InputDeviceUpdater.cs
✅ DisplayTableDebugger.cs
✅ InputDeviceManager.cs
✅ ActionDisplayTable.cs
✅ RebindButton.cs
✅ InputRebindManager.cs
>

いつでも「修正したい」「表示して」と言っていただければ、それに基づいて個別に改善案やリファクタリング案をご提案できます。続きを進める準備が整っています。

どこにどのスクリプトをアタッチするか聞いてみました。

現在の階層にどのスクリプトがついているか記憶しておいてください

ChatGPTの回答(クリックで展開)
了解しました。以下に、現在の階層(Hierarchy)とそれぞれにアタッチされている(またはすべき)主要スクリプトの割り当て状況を整理して、今後の記憶として保存します。

現在のUI階層(PauseCanvas 配下)

PauseCanvas
├── PausePanel
├── ConfirmPanel
├── OptionPanel
│   ├── Page_OptionHome
│   └── Panel_Volume
│   └── Panel_KeyOption
│       ├── Text (TMP)
│       ├── KeyOptionManager           ← ✅ KeyOptionManager.cs
│       ├── KeyBindScrollView
│       │   └── Viewport
│       │       └── Content
│       │           ├── KeyBindRow_MoveUp        ← ✅ RebindButton.cs + DeviceSensitiveDisplay.cs
│       │           ├── KeyBindRow_MoveDown      ← ✅ RebindButton.cs + DeviceSensitiveDisplay.cs
│       │           ├── KeyBindRow_Fire          ← ✅ RebindButton.cs + DeviceSensitiveDisplay.cs
│       │           ├── KeyBindRow_Formation1    ← ✅ RebindButton.cs + DeviceSensitiveDisplay.cs
│       │           ├── KeyBindRow_Formation2    ← ✅ RebindButton.cs + DeviceSensitiveDisplay.cs
│       │           └── KeyBindRow_Formation3    ← ✅ RebindButton.cs + DeviceSensitiveDisplay.cs
│       ├── InputRebindManager         ← ✅ InputRebindManager.cs(共通管理者)
│       ├── ResetButton
│       ├── CurrentDevice              ← ✅ InputDeviceUpdater.cs
│       └── Scrollbar Vertical
├── BackButton
└── EventSystem

各スクリプトの割り当て場所(記憶保存)

スクリプト名 割り当て場所 / オブジェクト名
KeyOptionManager.cs KeyOptionManager オブジェクト
InputRebindManager.cs InputRebindManager オブジェクト
InputDeviceUpdater.cs CurrentDevice オブジェクト
RebindButton.cs 各 KeyBindRow_~ 内の BindButton にアタッチ
DeviceSensitiveDisplay.cs 各 KeyBindRow_~(親またはBindButton)にアタッチ
BlinkText.cs (PressAnyKeyText系の TMP テキスト用)
ActionDisplayTable.asset RebindButton のインスペクター内で設定
DisplayTableDebugger.cs 一時的なデバッグ用。必要に応じて空オブジェクトへアタッチ可

この構成と割り当てを今後のやりとりの前提として記憶しておきます。
UIの追加やスクリプトの構成変更があった際は、更新の指示をいただければ反映します。

当然、記憶してくださいと頼んだものが出てくるものと思っていましたが、明らかに違うものが紛れています。そもそも数も違っていますし。

これらはすべて過去のチャットでもう使わないので削除したり、重複していたので統合したりというのをした名残が出てきた模様です。

ここから軌道修正しようとしましたが叶わなかったというような状況になっています。色々と思いつくことは試してみたものの今の実力では解決に至りませんでした。

解決法

スクリプトの記憶が確実なものとして"Cursor"というAI開発ツールだとファイルを直接読み込んで各種実装ができるようなので今後そちらを使ってみたいと思います。

VScodeを基本にしたAI駆動開発ツール
画面左のようにファイルを直接読み取ってくれるので今回のChatGPTのような心配はなさそう

今回はここまで。次回もキーバインド設定についてやっていきます。

  • この記事を書いた人

たげお

ソロゲームもオンラインゲームも対戦ゲームもソシャゲも楽しむ雑食ゲーマー。特筆して好きなジャンルはRPG、FPS。最近は格ゲーにお熱。 思い出のゲームは『FF9』

-Unity, ゲーム開発, 生成AI
-, , ,