最近看了某個基于 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 = https://www.cnblogs.com/tiancaiwrk/p/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:\Users\CASC\Desktop\PairFile/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.uj5u.com/qita/270704.html
標籤:其他
