Unity Input Systemで「猫と遊ぶ」ミニゲームを作ってみた

Phase遷移とInteractionを整理して、素直に動く入力設計にする

UnityのInput Systemを使って、
猫と遊ぶミニゲームを作ってみた。

操作はこんな感じ:

  • 顎なで(C)
  • 腹撫で(B)
  • 頭なで(H)
  • 肉球ぷにぷに(P)
  • 猫吸い(S)
  • またたび(M)
  • 猫じゃらし(T)

ただの入力処理…のはずが、実際には

「なんで二重発火するの?」
「Phaseって何?」
「Interactionっているの?」

で5時間くらい溶けた。

結論としては、

Action本体とBinding両方に、同一のInteractionを設定してただけ

というくだらない原因だった(笑)

→これだと

Action側とBinding側で「同じ入力解釈が二重に適用され」
結果としてイベントが2回発火することになる

今回の穴はこれだった。


Phase遷移の整理

Input Systemの内部状態はこう:

flowchart LR
    A[Waiting] --> B[Started]
    B --> C[Performed]
    C --> D[Canceled]

重要なのは、

全部が必ず発火するわけじゃない

ということ(条件で変わるからこそ、Phaseに分解されている)


実際の意味

Phase意味
Started入力が始まった瞬間
Performed「成立」と判定された瞬間(Interaction依存)
Canceled入力が終わった瞬間

今回の結論

今回のようなゲーム入力では:

  • 押した瞬間だけ欲しい → started
  • 押してる状態を管理したい → started / canceled
  • performed → 基本いらない

Interactionの正体

Interactionは、

入力の“解釈ルール”を追加するもの

たとえば:

  • Press
  • Hold
  • Tap
  • MultiTap

など。

Action側にInteractionを追加すれば、配下のBindingすべてにかかり
Binding側に設定すれば、そのBinding範囲でのみ適用される。

当然のことながら、Binding未設定でAction側にInteractionを設定しても何も起きない。
受ける入力自体がないから。


重要ポイント

  • ※Interactionは発火条件の追加に過ぎないので※ Action単体(Interactionなし)でも普通に動く
  • Interactionは“後付けの意味拡張”

今回なぜ外したか

Interaction(Pressなど)を付けると:

  • performed の発火タイミングが変わる
  • 意図しない2回目の発火が起きることがある

今回やりたいのは:

「押したら1回」

だけなので、

👉 Interactionを全部外した


入力設計のフロー

今回の設計はこう:

flowchart TD
    A[キー入力(複数種類)] --> B[InputAction]
    B --> C[startedイベント]
    C --> D[ゲーム内アクション]
    D --> E[状態更新]
    E --> F[結果判定]

InputAction設定


サンプルコード

using UnityEngine;
using UnityEngine.InputSystem;

public class CatPlayGame : MonoBehaviour
{
    [SerializeField] private InputActionAsset inputActions;

    private InputActionMap catPlayMap;

    private InputAction chinScratchAction;  // 顎なで
    private InputAction bellyRubAction;     // 腹撫で
    private InputAction headPatAction;      // 頭なで
    private InputAction pawPuniAction;      // 肉球ぷにぷに
    private InputAction catSniffAction;     // 猫の匂い嗅ぎ
    private InputAction matatabiAction;     // またたび
    private InputAction catTeaserAction;    // 猫じゃらし

    private int lastHeadPatFrame = -1;      // 頭なでの多重入力防止用
    private int lastPawPuniFrame = -1;      // 肉球ぷにぷにの多重入力防止用

    private int headPatCount = 0;           // 頭なでの回数
    private int pawPuniCount = 0;           // 肉球ぷにぷにの回数  

    private bool isCatSniffing = false;     // 猫吸い中かどうか
    private bool isMatatabiActive = false;  // またたびが発動中かどうか
    private bool isHeadPatPressed = false;  // 頭なでが押されているかどうか
    private bool isPawPuniPressed = false;  // 肉球ぷにぷにが押されているかどうか
    private bool inputReady = false;        // 入力が処理可能かどうか
    private bool isGameFinished = false;    // ゲームが終了したかどうか

    private int trust = 0;                  // 猫の信頼度
    private int comfort = 0;                // 猫の快適度
    private int excitement = 0;             // 猫の興奮度
    private int mood = 0;                   // 猫の気分

    private void Awake()
    {
        // InputActionAsset と ActionMap の取得とエラーチェック
        if (inputActions == null)
        {
            Debug.LogError("InputActionAsset が Inspector に設定されていません。");
            return;
        }

        catPlayMap = inputActions.FindActionMap("MainUI");
        if (catPlayMap == null)
        {
            Debug.LogError("ActionMap 'MainUI' が見つかりませんでした。");
            return;
        }

        // 各アクションの取得とエラーチェック
        chinScratchAction = catPlayMap.FindAction("ChinScratch");
        bellyRubAction = catPlayMap.FindAction("BellyRub");
        headPatAction = catPlayMap.FindAction("HeadPat");
        pawPuniAction = catPlayMap.FindAction("PawPuni");
        catSniffAction = catPlayMap.FindAction("CatSniff");
        matatabiAction = catPlayMap.FindAction("MatatabiToggle");
        catTeaserAction = catPlayMap.FindAction("CatTeaser");

        Debug.Log($"ChinScratch found: {chinScratchAction != null}");
        Debug.Log($"BellyRub found: {bellyRubAction != null}");
        Debug.Log($"HeadPat found: {headPatAction != null}");
        Debug.Log($"PawPuni found: {pawPuniAction != null}");
        Debug.Log($"CatSniff found: {catSniffAction != null}");
        Debug.Log($"Matatabi found: {matatabiAction != null}");
        Debug.Log($"CatTeaser found: {catTeaserAction != null}");
    }

    private void OnEnable()
    {
        // 各アクションのイベント登録
        if (chinScratchAction != null)
        {
            chinScratchAction.started += OnChinScratchStarted;
            chinScratchAction.canceled += OnChinScratchCanceled;
        }

        if (bellyRubAction != null)
        {
            bellyRubAction.started += OnBellyRubStarted;
            bellyRubAction.canceled += OnBellyRubCanceled;
        }

        if (headPatAction != null)
        {
            headPatAction.started += OnHeadPatStarted;
            headPatAction.canceled += OnHeadPatCanceled;
        }

        if (pawPuniAction != null)
        {
            pawPuniAction.started += OnPawPuniStarted;
            pawPuniAction.canceled += OnPawPuniCanceled;
        }

        if (catSniffAction != null)
        {
            catSniffAction.started += OnCatSniffStarted;
            catSniffAction.canceled += OnCatSniffCanceled;
        }

        if (matatabiAction != null)
        {
            matatabiAction.started += OnMatatabiStarted;
        }

        if (catTeaserAction != null)
        {
            catTeaserAction.started += OnCatTeaserStarted;
        }
        // ActionMap を有効化
        if (catPlayMap != null)
        {
            catPlayMap.Enable();
        }

        inputReady = false;
    }

    private void Start()
    {
        Debug.Log("ゲーム開始! 猫と仲良く遊ぼう!");
        inputReady = true;
    }

    // ゲーム終了後やオブジェクトが無効化されたときにイベントを解除してクリーンアップ
    private void OnDisable()
    {
        if (chinScratchAction != null)
        {
            chinScratchAction.started -= OnChinScratchStarted;
            chinScratchAction.canceled -= OnChinScratchCanceled;
        }

        if (bellyRubAction != null)
        {
            bellyRubAction.started -= OnBellyRubStarted;
            bellyRubAction.canceled -= OnBellyRubCanceled;
        }

        if (headPatAction != null)
        {
            headPatAction.started -= OnHeadPatStarted;
            headPatAction.canceled -= OnHeadPatCanceled;
        }

        if (pawPuniAction != null)
        {
            pawPuniAction.started -= OnPawPuniStarted;
            pawPuniAction.canceled -= OnPawPuniCanceled;
        }

        if (catSniffAction != null)
        {
            catSniffAction.started -= OnCatSniffStarted;
            catSniffAction.canceled -= OnCatSniffCanceled;
        }

        if (matatabiAction != null)
        {
            matatabiAction.started -= OnMatatabiStarted;
        }

        if (catTeaserAction != null)
        {
            catTeaserAction.started -= OnCatTeaserStarted;
        }

        if (catPlayMap != null)
        {
            catPlayMap.Disable();
        }
    }

    private void OnChinScratchStarted(InputAction.CallbackContext context)
    {
        if (!inputReady || isGameFinished || !context.started) return;

        trust += 5;
        comfort += 4;
        mood += 3;

        Debug.Log("顎なで → ゴロゴロ");
    }

    private void OnChinScratchCanceled(InputAction.CallbackContext context)
    {
        if (isGameFinished) return;
        Debug.Log("顎なで終了");
    }

    private void OnBellyRubStarted(InputAction.CallbackContext context)
    {
        if (!inputReady || isGameFinished || !context.started) return;

        comfort += 6;

        if (trust < 15)
        {
            excitement += 3;
            Debug.Log("警戒された");
        }
        else
        {
            trust += 4;
            Debug.Log("受け入れられた");
        }
    }

    private void OnBellyRubCanceled(InputAction.CallbackContext context)
    {
        if (isGameFinished) return;
        Debug.Log("腹撫で終了");
    }

    private void OnHeadPatStarted(InputAction.CallbackContext context)
    {
        if (!inputReady || isGameFinished || !context.started) return;
        if (pawPuniAction != null && pawPuniAction.IsPressed()) return;
        if (lastHeadPatFrame == Time.frameCount) return;
        if (isHeadPatPressed) return;

        lastHeadPatFrame = Time.frameCount;
        isHeadPatPressed = true;

        headPatCount++;
        trust += 2;
        mood += 2;

        Debug.Log($"頭なで {headPatCount}回");

        Judge();
    }

    private void OnHeadPatCanceled(InputAction.CallbackContext context)
    {
        if (isGameFinished) return;
        isHeadPatPressed = false;
        Debug.Log("頭なで終了");
    }

    private void OnPawPuniStarted(InputAction.CallbackContext context)
    {
        if (!inputReady || isGameFinished || !context.started) return;
        if (headPatAction != null && headPatAction.IsPressed()) return;
        if (lastPawPuniFrame == Time.frameCount) return;
        if (isPawPuniPressed) return;

        lastPawPuniFrame = Time.frameCount;
        isPawPuniPressed = true;

        pawPuniCount++;
        mood += 4;
        excitement += 2;

        Debug.Log($"肉球ぷにぷに {pawPuniCount}回");

		if (isCatSniffing)
		{
			Debug.Log("猫吸い中で猫がさらに興奮する!");
		    excitement += 4;
		}

        Judge();
    }

    private void OnPawPuniCanceled(InputAction.CallbackContext context)
    {
        if (isGameFinished) return;
        isPawPuniPressed = false;
        Debug.Log("肉球ぷにぷに終了");
    }

    private void OnCatSniffStarted(InputAction.CallbackContext context)
    {
        if (!inputReady || isGameFinished || !context.started) return;

        isCatSniffing = true;
        Debug.Log("猫吸い開始");

        if (trust < 15)
        {
            excitement += 10;
            Debug.Log("警戒された");
        }
        else
        {
            trust += 4;
            Debug.Log("受け入れられた");
        }
    }

    private void OnCatSniffCanceled(InputAction.CallbackContext context)
    {
        if (isGameFinished) return;

        isCatSniffing = false;
        Debug.Log("猫吸い終了");
    }

    private void OnMatatabiStarted(InputAction.CallbackContext context)
    {
        if (isGameFinished || !context.started) return;

        isMatatabiActive = !isMatatabiActive;
        Debug.Log(isMatatabiActive ? "またたび発動!" : "またたび終了");
    }

    private void OnCatTeaserStarted(InputAction.CallbackContext context)
    {
        if (!inputReady || isGameFinished || !context.started) return;

        excitement += 6;
        mood += 5;

        Debug.Log("猫じゃらしで遊んだ!");
    }

    private void Judge()
    {
        int total = trust + comfort + mood;

        if (total < 50) return;

        Debug.Log("なついた![GameEnd]");
        isGameFinished = true;
    }
}

CSharpScriptをHierarchyにDrag&Dropしてオブジェクト作成

そしてInputActionAssetをアタッチ。


まとめ

今回のポイントはシンプルにこれ:


✔ Input Systemの扱い方

  • started を基本にする
  • canceled で状態を戻す
  • performed は必要なときだけ

✔ Interactionの扱い

  • 最初は使わない
  • 必要になったら追加する


最後に

Input Systemは入力を一元的に扱うために作られ、かつ「開発途中の自由な追加変更を容認」しているので、「設定もれ」「矛盾」も検知されない。
今回はキーボード入力を単純に受けるだけだったので問題は単純だったが、複雑な設計だと事故りやすいだろうなぁ、とは感じた。

コメント

タイトルとURLをコピーしました