LoginSignup
14
11

More than 1 year has passed since last update.

【Unity】入力を記録してリプレイできるInputEventTraceの使い方【InputSystem】

Last updated at Posted at 2021-11-09

はじめに

ローグライクゲームを開発した時、バグを再現するのが大変だったので「自動でリプレイをしてバグを再現できないか?」と考えた。

バグを再現する材料の一つとして「入力情報の記録」が必要だと思い、InputSystemのドキュメントを調べたところ、InputEventTrace なるAPIを見つけました。

この記事では、InputEventTrace の使い方について解説します。

InputEventTraceは何ができる?

まず最初に、InputEventTrace を使用すると何ができるかを簡単に見ていきます。

例1.入力をRecord(記録) → Replay(再生)

手動での入力を記録して再生する例。

InputEventTrace_Recording.gif

例2.記録をSave(セーブ) → Load(ロード) → Replay(再生)

例1での入力をセーブ&ロードして再生する例。

InputEventTrace_Load.gif

コード全体

今回書いたコードの全体です。(詳しい解説は後ほど)

InputRecording.cs
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.LowLevel;
using System.IO;

public class InputRecording : MonoBehaviour {

    [SerializeField]
    Transform m_Player;

    [SerializeField]
    Button m_RecordButton;

    [SerializeField]
    Button m_ReplayButton;

    [SerializeField]
    Button m_SaveButton;

    [SerializeField]
    Button m_LoadButton;

    [SerializeField]
    string m_FileName = "recording";

    InputEventTrace m_Trace;
    PlayerInputActions m_PlayerInputActions;
    InputAction m_MoveInput;

    void Awake () {
        // InputActionの初期化(PlayerInputActionsは別のファイルで定義されています)
        m_PlayerInputActions = new PlayerInputActions();
        m_PlayerInputActions.Enable();
        m_MoveInput = m_PlayerInputActions.Player.Move;

        // InputEventTraceの初期化
        m_Trace = new InputEventTrace(Keyboard.current);
        m_Trace.onEvent += ev => Debug.Log(ev.ToString());

        // UIへのイベント登録
        m_RecordButton.onClick.AddListener(ToggleRecording);
        m_ReplayButton.onClick.AddListener(Replay);
        m_SaveButton.onClick.AddListener(Save);
        m_LoadButton.onClick.AddListener(Load);
    }

    void OnDestroy () {
        m_PlayerInputActions.Dispose();

        // InputEventTraceを使った後は必ず解放する必要がある。
        m_Trace.Dispose();
    }

    void Update () {
        // 入力に従って、プレイヤーを移動させる
        var input = m_MoveInput.ReadValue<Vector2>();
        var movement = new Vector3(input.x,0f,input.y) * 4f * Time.deltaTime;
        m_Player.Translate(movement,Space.Self);
    }

    void ToggleRecording () {
        if (m_Trace.enabled) {
            // 入力の記録を停止
            m_Trace.Disable();
        } else {
            // 既存の入力記録を削除してから、入力の記録を開始
            m_Trace.Clear();
            m_Trace.Enable();
        }

        m_Player.position = Vector3.zero;
        m_ReplayButton.interactable = !m_Trace.enabled;

        Debug.Log(m_Trace.enabled ? "Start Recording" : "Stop Recording");
    }

    void Replay () {
        m_Player.position = Vector3.zero;
        m_RecordButton.interactable = false;

        // 入力記録を再生する
        m_Trace.Replay() // ReplayControllerを取得する(※これだけ呼んでも再生されない)
            .OnFinished(() => m_RecordButton.interactable = true) // 記録の再生が終わった時のコールバック
            .PlayAllEventsAccordingToTimestamps(); // 入力されたタイミングも含めて再現

        Debug.Log("Replay");
    }

    void Save () {
        string filePath = GetFilePath();
        string directoryPath = Path.GetDirectoryName(filePath);
        if (!Directory.Exists(directoryPath)) {
            Directory.CreateDirectory(directoryPath);
        }

        // 入力記録を指定したファイルにセーブする
        m_Trace.WriteTo(filePath);
        Debug.Log("Save to " + filePath);
    }

    void Load () {
        string filePath = GetFilePath();
        if (!File.Exists(filePath)) {
            throw new FileNotFoundException();
        }

        // 入力記録を指定したファイルからロードする
        m_Trace.ReadFrom(filePath);
        Debug.Log("Load from " + filePath);
    }

    string GetFilePath () => Path.Combine(Path.GetDirectoryName(Application.dataPath),"InputRecordings",m_FileName + ".txt");

}

使い方(コード解説)

1. InputEventTrace を生成

m_Trace = new InputEventTrace(Keyboard.current);

引数には Keyboard.current を指定したので、キーボードの入力のみを記録します。
何も指定しない場合、すべてのデバイスからの入力が記録されます。

2. 入力の記録を開始・停止する

Enable / Disable 関数で、入力記録の状態を切り替えます。
今回は記録開始前に既存の記録を削除したかったので、 Enable 前に Clear を呼んでいます。

if (m_Trace.enabled) {
    // 入力の記録を停止
    m_Trace.Disable();
} else {
    // 既存の入力記録を削除してから、入力の記録を開始
    m_Trace.Clear();
    m_Trace.Enable();
}

3. 入力記録を再生する

InputEventTrace.Replay 関数で ReplayController を取得できます。
Replayを呼ぶだけでは再生されないことに注意

// 入力記録を再生する
m_Trace.Replay() // ReplayControllerを取得する(※これだけ呼んでも再生されない)
    .OnFinished(() => m_RecordButton.interactable = true) // 記録の再生が終わった時のコールバック
    .PlayAllEventsAccordingToTimestamps(); // 入力されたタイミングも含めて再現

ReplayController には PlayAllEventsAccordingToTimestamps 以外にも再生に関する関数があるので、自分がやりたいことに適したものを使用します。

InputEventTrace.ReplayController | InputSystem

4. 入力記録をセーブ・ロードする

WriteTo / ReadFrom 関数を使用することで、入力記録を指定したファイルに読み書きできます。

void Save () {
    // ...

    // 入力記録を指定したファイルにセーブする
    m_Trace.WriteTo(filePath);
    Debug.Log("Save to " + filePath);
}

void Load () {
    // ...

    // 入力記録を指定したファイルからロードする
    m_Trace.ReadFrom(filePath);
    Debug.Log("Load from " + filePath);
}

WriteTo 関数では、以下のような文字列が書き出されます。

スクリーンショット 2021-11-07 203915.jpg

おわりに

以下が使用したリポジトリです。

ついでに、自動QAについて調べているときに見つけた面白い記事も紹介しておきます。

InputEventTraceのソースコード

14
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
11