Open33

InputSystemを利用した汎用ユーザーインプット作成の備忘録

ピン留めされたアイテム
ぎれるもぎれるも

顧客(自分)の要望: u1wだったりちょっとしたゲーム性の検証のために一からつくるのはめんどくさいのでオレオレインプットシステムが欲しい。

ピン留めされたアイテム
ぎれるもぎれるも

完成

ぎれるもぎれるも

container

using MessagePipe;
using UnityEngine.InputSystem;
using VContainer;
using VContainer.Unity;
public class GameLifetimeScope : LifetimeScope
{
    protected override void Configure(IContainerBuilder builder)
    {
        // MessagePipeの設定
        var options = builder.RegisterMessagePipe();
        
        builder.RegisterMessageBroker<InputAction.CallbackContext>(options);

        // InputPlayer
        builder.RegisterComponentInHierarchy<PlayerInput>();

        // Message発信源
        builder.RegisterEntryPoint<PlayerInputProvider>(Lifetime.Singleton);

        // prefab生成用
        builder.RegisterComponentInHierarchy<InstantiateTest>();
    }
}
ぎれるもぎれるも

PlayerInput発信

using MessagePipe;
using UnityEngine;
using UnityEngine.InputSystem;
using VContainer.Unity;
public sealed class PlayerInputProvider : IStartable
{
    /// <summary>
    /// MessagePipeにメッセージを流す用のインタフェース
    /// </summary>
    private readonly IPublisher<InputAction.CallbackContext> _inputPublisher;
    private readonly PlayerInput _playerInput;
    void IStartable.Start()
    {
        this._playerInput.onActionTriggered += OnAction;
    }

    public PlayerInputProvider(
        IPublisher<InputAction.CallbackContext> inputPublisher, PlayerInput playerInput
    ) {
        this._inputPublisher = inputPublisher;
        this._playerInput = playerInput;
    }
    private void OnAction(InputAction.CallbackContext context)
    {
        this._inputPublisher.Publish(context);
    }
}

ぎれるもぎれるも

Message受信側

using Cysharp.Threading.Tasks;
using MessagePipe;
using UnityEngine;
using UnityEngine.InputSystem;
using VContainer;
public sealed class PlayerInputReceiverTest : MonoBehaviour
{
    /// <summary>
    /// MessagePipeからメッセージを受け取る用インタフェース
    /// </summary>
    [Inject] private readonly ISubscriber<InputAction.CallbackContext> _inputEventSubscriber;
    private void Start()
    {
        // 入力イベントの受信を開始する
        _inputEventSubscriber.Subscribe(OnInputEventReceived)
            // MonoBehaviourに寿命を紐づける(これはUniTaskの機能)
            .AddTo(this.GetCancellationTokenOnDestroy());
    }

    /// <summary>
    /// 入力イベントを処理する
    /// </summary>
    private void OnInputEventReceived(InputAction.CallbackContext context)
    {
        Debug.Log("Sub: " + context);
    }
}
ぎれるもぎれるも

現状だとインプットを取得したいオブジェクトすべてにPlayerInputを入れないといけないのでちょっと密な気がするのでMessagePipeの学習がてらInputのメッセージを飛ばすやつを作成してみる

ぎれるもぎれるも

受け取る側

using Cysharp.Threading.Tasks;
using MessagePipe;
using UnityEngine;
using VContainer;
public sealed class PlayerInputRecieverTest : MonoBehaviour
{
    /// <summary>
    /// MessagePipeからメッセージを受け取る用インタフェース
    /// </summary>
    [Inject] private ISubscriber<InputParams> _inputEventSubscriber;

    // 各種フィールド

    private void Start()
    {
        // 入力イベントの受信を開始する
        _inputEventSubscriber.Subscribe(OnInputEventReceived)
            // MonoBehaviourに寿命を紐づける(これはUniTaskの機能)
            .AddTo(this.GetCancellationTokenOnDestroy());
    }

    /// <summary>
    /// 入力イベントを処理する
    /// </summary>
    private void OnInputEventReceived(InputParams input)
    {
        Debug.Log("Fire: " + input.Fire);
        Debug.Log("Move: " + input.Move);
    }
}

ぎれるもぎれるも

発信側

using MessagePipe;
using UnityEngine;
using UnityEngine.InputSystem;
using VContainer.Unity;

[RequireComponent(typeof(PlayerInput))]
public sealed class PlayerInputProvider : IStartable
{
    /// <summary>
    /// MessagePipeにメッセージを流す用のインタフェース
    /// </summary>
    private readonly IPublisher<InputParams> _inputPublisher;
    private readonly PlayerInput _playerInput;
    void IStartable.Start()
    {
        _playerInput.onActionTriggered += OnMove;
    }

    public PlayerInputProvider(IPublisher<InputParams> inputPublisher, PlayerInput playerInput)
    {
        this._inputPublisher = inputPublisher;
        this._playerInput = playerInput;
    }
    private void OnMove(InputAction.CallbackContext context)
    {
        if (context.action.name != "Move") {
            return;
        }
        var axis = context.ReadValue<Vector2>();
        Debug.Log(axis);
        // メッセージを作成
        var inputParams = new InputParams(false, axis);
        // メッセージ送信
        _inputPublisher.Publish(inputParams);
        // Debug.Log(axis);
    }
}

ぎれるもぎれるも

発信側と受け取る側をつなぐやつ

using MessagePipe;
using UnityEngine;
using UnityEngine.InputSystem;
using VContainer;
using VContainer.Unity;
public class GameLifetimeScope : LifetimeScope
{
    // MoveCubeのPrefabへの参照
    [SerializeField] private PlayerInputRecieverTest _playerInputReciever;

    protected override void Configure(IContainerBuilder builder)
    {
        // MessagePipeの設定
        var options = builder.RegisterMessagePipe();
        
        // InputParamsを伝達できるように設定する
        builder.RegisterMessageBroker<InputParams>(options);
        
        // InputEventProviderを起動
        builder.RegisterComponentInHierarchy<PlayerInput>();
        builder.RegisterEntryPoint<PlayerInputProvider>();

        // MoveCubeをDIしながらInstantiate
        builder.RegisterBuildCallback(resolver =>
        {
            resolver.Instantiate(_playerInputReciever);
        });
    }
}
ぎれるもぎれるも

送るもの

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public readonly struct InputParams : System.IEquatable<InputParams>
{
    /// <summary>
    /// 撃つフラグ
    /// </summary>
    public bool Fire { get; }

    /// <summary>
    /// 移動操作
    /// </summary>
    public Vector2 Move { get; }
    
    public InputParams(bool isFire, Vector2 move)
    {
        Fire = isFire;
        Move = move;
    }

    public bool Equals(InputParams other)
    {
        return Fire == other.Fire && Move.Equals(other.Move);
    }

    public override bool Equals(object obj)
    {
        return obj is InputParams other && Equals(other);
    }

    public override int GetHashCode()
    {
        return HashCode.Combine(Fire, Move);
    }
}

ぎれるもぎれるも

builder.RegisterComponentInHierarchy<PlayerInput>();
をつかっているためシーンにPlayerInputをもったオブジェクトを置かないといけないのがちょっと気になる。
PlayerInputをもったオブジェクトも

builder.RegisterBuildCallback(resolver =>
        {
            resolver.Instantiate(_playerInputReciever);
        });

で生成できるのだろうか。

ぎれるもぎれるも

というかvcontainerをつかえばmessagepipeつかわんでもええのか?
でも使わなかった場合コンテナの煩雑度が上昇する気がする

ぎれるもぎれるも

vcontainerなんもわからん
動的に生成したGameObjectにはどうやってInjectすればいいんだ
インスタンスを注入されたFactoryから注入されたインスタンスをわたしてGameObjectを生成するのか?

ぎれるもぎれるも

container

public class GameLifetimeScope : LifetimeScope
{
    // MoveCubeのPrefabへの参照

    protected override void Configure(IContainerBuilder builder)
    {
        // MessagePipeの設定
        var options = builder.RegisterMessagePipe();
        
        // InputParamsを伝達できるように設定する
        builder.RegisterMessageBroker<Vector2>(options);
        
        // InputEventProviderを起動
        builder.RegisterComponentInHierarchy<PlayerInput>();
        builder.RegisterEntryPoint<PlayerInputProvider>(Lifetime.Scoped);
        builder.RegisterComponentInHierarchy<InstantiateTest>();
    }
}
ぎれるもぎれるも

prefab生成

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
using VContainer;
using VContainer.Unity;

public class InstantiateTest : MonoBehaviour
{
    // MoveCubeのPrefabへの参照
    [SerializeField] private GameObject _playerInputReciever;
    private IObjectResolver _container;
    [Inject]
    public void InjectContainer(IObjectResolver container) {
        this._container = container;
    }
    void Update()
    {
        var key = Input.GetKeyDown(KeyCode.Space);
        if (key) {
            _container.Instantiate(_playerInputReciever);
        }
    }
}

ぎれるもぎれるも

どういう関係になるかを図でまとめる

ぎれるもぎれるも

個人的にはPlayerInputProviderとPlayerInputをvcontainerでつなげる意義が見つけられない。そのままGameObjectとして置いておけばよさそう。vcontainerを使う理由として疎結合にして各クラス間の動作のテストをしやすくするというものがあるが、そもそもPlayerInputProviderがPlayerInputに依存してなければならないのでここはvcontainerでつなぐ必要がなさそうに思える。むしろゲームオブジェクトとしてプレファブ化できれば別シーンでの使いまわしができそう。

ぎれるもぎれるも

一旦完成として、次のunity1weekで改善点と実際に使ったときに必要な機能を見つける

ぎれるもぎれるも

unity1weekのお題とこの機能との相性が悪いため、unity1weekとは別に作る