自定义UDP设备 InputSystem

解耦

经常会有各种奇奇怪怪的硬件需要使用TCP/UDP去交互,这次尝试下使用InputSystem去解耦。

IInputStateTypeInfo

定义一个结构体实现这个接口。

FourCC

四个字符来定义一个唯一标识符。

InputControl

使用这个描述符来定义一些字段,其中标记了各个按钮或者遥感的定义。

[InputControl(name = "firstButton", layout = "Button", bit = 0, displayName = "First Button")]
[InputControl(name = "secondButton", layout = "Button", bit = 1, displayName = "Second Button")]
[InputControl(name = "thirdButton", layout = "Button", bit = 2, displayName = "Third Button")]
public ushort buttons;

遥感的定义需要额外的format属性,这里的VC2B指的是a Vector2 of bytes
定义X轴y轴
剩下的上下左右似乎是硬编码的。

[InputControl(name = "stick", format = "VC2B", layout = "Stick", displayName = "Main Stick")]
[InputControl(name = "stick/x", defaultState = 127, format = "BYTE",offset = 0,
    parameters = "normalize,normalizeMin=0,normalizeMax=1,normalizeZero=0.5")]
public byte x;


[InputControl(name = "stick/up", parameters = "normalize,normalizeMin=0,normalizeMax=1,normalizeZero=0.5,clamp=2,clampMin=0,clampMax=1")]
[InputControl(name = "stick/down", parameters = "normalize,normalizeMin=0,normalizeMax=1,normalizeZero=0.5,clamp=2,clampMin=-1,clampMax=0,invert")]
[InputControl(name = "stick/left", parameters = "normalize,normalizeMin=0,normalizeMax=1,normalizeZero=0.5,clamp=2,clampMin=-1,clampMax=0,invert")]
[InputControl(name = "stick/right", parameters = "normalize,normalizeMin=0,normalizeMax=1,normalizeZero=0.5,clamp=2,clampMin=0,clampMax=1")]
public byte y;

总结下就是依靠一个结构体以及各个描述符来定义数据交换的大小和约定的格式。

InputDevice

定义好了结构体,接下来就是要实现真正的InputDevice了。

初始化

首先是确保在编辑器以及运行时都会初始化。

这里的初始化中包含一段模板代码。用来通过”Custom”来发现设备。

#if UNITY_EDITOR
[InitializeOnLoad] // Call static class constructor in editor.
#endif
[InputControlLayout(stateType = typeof(CustomDeviceState))]
public class CustomDevice : InputDevice, IInputUpdateCallbackReceiver
{
    #if UNITY_EDITOR
    static CustomDevice()
    {
        Initialize();
    }

    #endif

    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
    private static void Initialize()
    {
        InputSystem.RegisterLayout<CustomDevice>(
            matches: new InputDeviceMatcher()
                .WithInterface("Custom"));
    }

ButtonControl

公开一些属性,方便其他人调用。
FinishSetup

public ButtonControl firstButton { get; private set; }
public ButtonControl secondButton { get; private set; }
public ButtonControl thirdButton { get; private set; }
public StickControl stick { get; private set; }
protected override void FinishSetup()
{
    base.FinishSetup();

    firstButton = GetChildControl<ButtonControl>("firstButton");
    secondButton = GetChildControl<ButtonControl>("secondButton");
    thirdButton = GetChildControl<ButtonControl>("thirdButton");
    stick = GetChildControl<StickControl>("stick");
}

current

保留一个静态对象,类似单例,但是每次都更新,而不会销毁之前的。

public static CustomDevice current { get; private set; }
public override void MakeCurrent()
{
    base.MakeCurrent();
    current = this;
}

protected override void OnRemoved()
{
    base.OnRemoved();
    if (current == this)
        current = null;
}

创建设备

一切就绪,接下来就是创建一个设备了。
设备是不能通过new或者其他方式创建,只能在事先注册布局时传入匹配器,然后通过匹配器去创建
这里是在编辑器中虚拟了一个设备

#if UNITY_EDITOR
[MenuItem("Tools/Custom Device Sample/Create Device")]
private static void CreateDevice()
{
    InputSystem.AddDevice(new InputDeviceDescription
    {
        interfaceName = "Custom",
        product = "Sample Product"
    });
}

[MenuItem("Tools/Custom Device Sample/Remove Device")]
private static void RemoveDevice()
{
    var customDevice = InputSystem.devices.FirstOrDefault(x => x is CustomDevice);
    if (customDevice != null)
        InputSystem.RemoveDevice(customDevice);
}

#endif

轮询键盘去更新设备状态

public void OnUpdate()
{
    var keyboard = Keyboard.current;
    if (keyboard == null)
        return;

    var state = new CustomDeviceState();

    state.x = 127;
    state.y = 127;
    // Map WASD to stick.
    var wPressed = keyboard.wKey.isPressed;
    var aPressed = keyboard.aKey.isPressed;
    var sPressed = keyboard.sKey.isPressed;
    var dPressed = keyboard.dKey.isPressed;

    if (aPressed)
        state.x -= 127;
    if (dPressed)
        state.x += 127;
    if (wPressed)
        state.y += 127;
    if (sPressed)
        state.y -= 127;

    // Map buttons to 1, 2, and 3.
    if (keyboard.spaceKey.isPressed)
    {
        // Debug.Log("VAR");
        state.buttons |= 1 << 0;
    }
    if (keyboard.digit2Key.isPressed)
        state.buttons |= 1 << 1;
    if (keyboard.digit3Key.isPressed)
        state.buttons |= 1 << 2;

    InputSystem.QueueStateEvent(this, state);
}

UDP Driver

栗子吃完了,用UDP写一个吧
先是一个基础的UDPServer

public class UDPServer : MonoBehaviour
{
    public int listenPort  = 9000;
    
    private Thread thread;
    private readonly ConcurrentQueue<string> _messages = new();
    private UdpClient _listener;

    private void Start()
    {
        
        thread = new Thread(StartListen);
        thread.Start();
    }
    
    private void OnDestroy()
    {
        thread.Abort();
        _listener.Close();
    }

    private void Update()
    {
        if (_messages.TryDequeue(out var message))
        {
            Debug.Log($"Message: {message}");
        }
    }

    private void StartListen()
    {
        _listener = new UdpClient(listenPort);
        var groupEP = new IPEndPoint(IPAddress.Any, listenPort);
        try
        {
            while (true)
            {
                Debug.Log("Waiting for broadcast");

                byte[] bytes = _listener.Receive(ref groupEP);

                Debug.Log($"Received broadcast from {groupEP} :");
                var data = Encoding.UTF8.GetString(bytes, 0, bytes.Length);
                _messages.Enqueue(data);
                Debug.Log($" {data}");
            }
        }
        catch (SocketException e)
        {
            Debug.Log(e);
        }
        finally
        {
            _listener.Close();
        }
    }
}

然后增加设备相关代码即可:

private void Start()
{
    InputSystem.AddDevice<UDPDevice>();
    _udpDevice = UDPDevice.current;
    ......
}.

private void OnDestroy()
{
    ......
    InputSystem.RemoveDevice(_udpDevice);
}

public UDPDeviceState state;
private void Update()
{
    var newState = new UDPDeviceState
    {
        buttons = 0
    };
    
    if (_messages.TryDequeue(out var message))
    {
        newState.buttons = 1;
        Debug.Log($"Message: {message}");
    }

    if (state != newState)
    {
        InputSystem.QueueStateEvent(_udpDevice, newState);
    }
    state = newState;
}

基础的UDP消息充当按钮就OK了。后续可以根据需要调整为遥感或其他各种类型的控件。


自定义UDP设备 InputSystem
https://www.kuanmi.top/2023/02/01/CustomInputSystem/
作者
KuanMi
发布于
2023年2月2日
许可协议