Unity 基于 WebRTC 的云功能

  最近看了某个基于 WebRTC 的推流交互方案, 在 Unity 下基本实现了一个云平台的功能, 拿来测试了一下, 发现经过修改之后非常符合我们的需求, 也许之后就有机会运用起来了. 它应该也能成为云游戏的通信架构的方案, 问题只在于怎样实现云客户端罢了...

  Github : https://github.com/Unity-Technologies/com.unity.webrtc

  简单来说就是 客户端 -> 服务器 -> 客户网页 这样的流程, 也就是说只要客户端连接到服务器, 那么客户端就成了云端了, 用户只要用支持 WebRTC 的浏览器就能云客户端了.

  不过估计这个方案获取视频流的方式是从显卡获取, 并且基于 CUDA 来加速的, 所以需要 Nvdia 的显卡, 并且安装相应驱动. 看下图 : 

  软件编码的效率估计不看也罢, 似乎Metal天生就支持这个编码, 不知道是编成 H.264 / VP8 还是啥.

  不过对于我们来说, 只需要 Unity 的编辑器能够运行起来连接上服务器, 甲方打开浏览器就能看到最新版本并且能够操作, 就完美了. 比起打包发过去或者打一个WebGL相比, 省了时间不说, 版本能够随时看到最新的才是超神啊, 只需要用一台空闲电脑运行编辑器, 然后加个检测命令, 如果 SVN 有更新, 编辑器自动更新工程, 然后再自动运行起来连接上服务器, 完美......

  它的工程还是比较简单或者粗糙的, 当然问题多多, 不过好在我们的需求也不多, 只需要解决几个"小问题"就能拿来用了.

  首先它从输入上就跟原来的 Standalone Input Module 不兼容, 我们工程以及很多插件都是基于 UnityEngine.Input 的, 它的 Demo 系统使用了 UnityEngine.InputSystem 这套新的 IO 系统, 如果要跟原系统产生影响, 就需要从底层去触发 Unity 的响应才行, 第一步要把双 Input 系统支持起来 : 

  因为我们工程最终的使用还是打包给客户运行客户端, 并不是使用 WebRTC 的方式作为运行, 它只是一个编辑器行为, 那么在不修改工程代码的基础上, 怎样才能扩展出接收远程输入的呢? 先看看 Demo 中的远程输入代码 : 

    enum KeyboardEventType
    {
        KeyUp = 0,
        KeyDown = 1,
    }
    void ProcessKeyEvent(KeyboardEventType state, bool repeat, byte keyCode, char character)
    {
        switch(state)
        {
            case KeyboardEventType.KeyDown:
                if (!repeat)
                {
                    InputSystem.QueueStateEvent(RemoteKeyboard, new KeyboardState((UnityEngine.InputSystem.Key)keyCode));
                }
                if(character != 0)
                {
                    InputSystem.QueueTextEvent(RemoteKeyboard, character);
                }
                break;
            case KeyboardEventType.KeyUp:
                InputSystem.QueueStateEvent(RemoteKeyboard, new KeyboardState());
                break;
        }
    }

  新的系统能很容易给不同的输入对象定义, 区分不同的输入, 这样在多人游戏的时候就很方便(一个客户端多人一起玩), 不过我只需要给单一用户使用, 只要考虑怎样通过它触发 UnityEngine.Input 就行了...

  然后说到怎样触发, 通过尝试各种 Uniry 的 API 发现都是无法触发的, 必须通过 Win32 API 才能触发, 并且不能通过 SendMessage 的方式发送给 Unity 窗口来触发, 必须通过全局方式才能触发 : 

    [DllImport("user32.dll", SetLastError = true)]
    public static extern UInt32 SendInput(UInt32 numberOfInputs, INPUT[] inputs, Int32 sizeOfInputStructure);
    
    [System.Runtime.InteropServices.DllImport("user32.dll", CharSet = System.Runtime.InteropServices.CharSet.Auto, CallingConvention = System.Runtime.InteropServices.CallingConvention.StdCall)]
    public static extern void mouse_event(uint dwFlags, uint dx, uint dy, uint cButtons, uint dwExtraInfo);

  主要就是使用 Native API 来完成.

  鼠标的操作相对简单, 因为 Demo 提供的服务器, 已经把用户输入的坐标转换成 Unity 的屏幕坐标了, 只需要直接设定鼠标位置即可 : 

  UnityEngine.InputSystem.Mouse.current.WarpCursorPosition(mousePos);

  这里使用了 InputSystem 提供的鼠标坐标位置设定.

  然后将鼠标状态转换成 Windows 的状态码就行了 : 

        [Flags]
        public enum MouseEventFlags
        {
            LeftDown = 0x00000002,
            LeftUp = 0x00000004,
            MiddleDown = 0x00000020,
            MiddleUp = 0x00000040,
            Move = 0x00000001,
            Absolute = 0x00008000,
            RightDown = 0x00000008,
            RightUp = 0x00000010
        }

  而键盘的输入, 这里用到了一个微软的库 : WindowsInput

  它提供了很方便的输入调用封装, 只不现在系统中有了三种键盘枚举 : 

    1. WindowsInput : WindowsInput.Native.VirtualKeyCode
    2. Unity Input :  UnityEngine.KeyCode
    3. Unity InputSystem : UnityEngine.InputSystem.Key

  在这里需要通过 RemoteInput 获取远程输入得到 UnityEngine.InputSystem.Key 然后需要转成 WindowsInput.Native.VirtualKeyCode 才能发送消息给 Win32. 而最终触发了工程中的 UnityEngine.KeyCode (UnityEngine.Input)......

  因为有上百个按键呢, 不可能一个个去写, 所幸可以通过监听输入来获取对应关系 : 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using WindowsInput.Native;

public class KeyCodeListener : MonoBehaviour
{
    public class InputPair
    {
        public VirtualKeyCode vk;
        public KeyCode keyCode = KeyCode.None;
    }

    List<InputPair> inputPair = new List<InputPair>();
    List<KeyCode> keyCodes = new List<KeyCode>();

    void Start()
    {
        StartCoroutine(VirtualKeyCodeToKeyCode());
    }

    // Update is called once per frame
    void Update()
    {
        foreach(var keyCode in keyCodes)
        {
            if(Input.GetKeyDown(keyCode))
            {
                if(inputPair.Count > 0)
                {
                    var data = inputPair[inputPair.Count - 1];
                    data.keyCode = keyCode;
                }
            }
        }
    }

    IEnumerator VirtualKeyCodeToKeyCode()
    {
        keyCodes.Clear();
        foreach(int keyVal in System.Enum.GetValues(typeof(KeyCode)))
        {
            var keyCode = (KeyCode)keyVal;
            keyCodes.Add(keyCode);
        }
        var inputSimulator = new WindowsInput.InputSimulator();
        inputPair.Clear();
        foreach(int vk in System.Enum.GetValues(typeof(VirtualKeyCode)))
        {
            var keyCode = (VirtualKeyCode)vk;
            if(keyCode != VirtualKeyCode.LWIN && keyCode != VirtualKeyCode.RWIN)
            {
                inputPair.Add(new InputPair() { vk = keyCode });
                inputSimulator.Keyboard.KeyDown(keyCode);
                yield return null;
                inputSimulator.Keyboard.KeyUp(keyCode);
                yield return null;

                inputPair.Add(new InputPair() { vk = keyCode });
                inputSimulator.Keyboard.KeyDown(keyCode);
                yield return null;
                inputSimulator.Keyboard.KeyUp(keyCode);
                yield return null;
            }
        }
        Debug.Log("Listen end");
        yield return new WaitForSeconds(1.5F);
        WriteToFile();
    }

    void WriteToFile()
    {
        string path = @"C:UsersCASCDesktopPairFile/file.txt";
        System.Text.StringBuilder sb = new System.Text.StringBuilder();
        sb.AppendLine("public static VirtualKeyCode KeyCodeToVirtualKeyCode(KeyCode keyCode){");

        string format = @" case #CASE#:
                    {
                        return #RETURN#;
                    }";

        string variable = @"
    switch(keyCode){
        #FORMAT#
    }";
        System.Text.StringBuilder formatBuilder = new System.Text.StringBuilder();

        var uniqueInput = new Dictionary<KeyCode, InputPair>();

        foreach(var data in inputPair)
        {
            if(data.keyCode != KeyCode.None)
            {
                uniqueInput[data.keyCode] = data;
            }
            else
            {
                Debug.Log("NONE Code : " + data.vk);
            }
        }
        foreach(var data in uniqueInput.Values)
        {
            var line = format.Replace("#CASE#", "KeyCode." + data.keyCode).Replace("#RETURN#", "VirtualKeyCode." + data.vk);
            formatBuilder.AppendLine(line);
        }
        formatBuilder.AppendLine("default: { throw null; } break;");
        sb.AppendLine(variable.Replace("#FORMAT#", formatBuilder.ToString()));

        sb.AppendLine("}");
        System.IO.File.WriteAllText(path, sb.ToString());
    }
}

  然后可以生成出来一个对应关系 : 

  从 KeyCode -> VirtualKeyCode 完成了, 然后因为 KeyCode 跟 InputSystem.Key 的枚举名称是一样的, 所以可以根据名称来进行 Key -> KeyCode -> VirtualKeyCode 的转换, 这样就把远程输入转到本地的 Windows 输入了, 就能触发原工程中的逻辑了哈哈.

  PS : 就是因为上面的触发方式需要使用全局的方法, 所以运行的编辑器必须处在顶层窗口, 如果被其它窗口遮挡就会操作到其它窗口去了......

     强制最前窗口的方式, 非常暴力

    [DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
    public static extern IntPtr GetForegroundWindow();
    
    [DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
    public static extern bool SetForegroundWindow(IntPtr hWnd);
        
    static IntPtr ms_unityWindow = IntPtr.Zero;
    
    [UnityEngine.RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterAssembliesLoaded)]
    private static void Init()
    {
        ms_unityWindow = GetForegroundWindow();
    }
    
    void Update(){
        SetForegroundWindow(ms_unityWindow);
    }
原文地址:https://www.cnblogs.com/tiancaiwrk/p/14572822.html