太長不看:https://github.com/horeaper/UnigineMyraIntegration
什麼是Myra?
這個是Myra:https://github.com/rds1983/Myra
熟悉WPF/Avalonia/Silverlight/UWP開發的朋友肯定一眼就能看出來這個UI庫用的是什麼佐料了。它當然不是完整的WPF實現,對於遊戲開發而言也沒必要用完整的WPF,太重了。
Myra有着和WPF非常類似的Layout系統,運行效率也不錯,同時還支持XML聲明(被稱為MML,Myra Markup Language),既可以實時加載XML也可以用MyraPad將這些XML轉換成C#代碼。整合進引擎裏也不麻煩。這就夠了。
這玩意兒甚至FileDialog和ColorPickerDialog都給你做了一份,省了大事了🤣
第一步:先學再用
本文寫於Myra 1.5.9版本。
首先建議把Github上Myra的Wiki都看一遍,大概瞭解一下Myra是怎麼玩的。之後我建議找個空文件夾git clone https://github.com/rds1983/Myra.git,然後打開Myra\build\Myra.PlatformAgnostic.sln,你會看到幾個項目,其中在Samples文件夾下面有這三個示例項目:
Samples/Myra.Samples.PlatformAgnostic:用MonoGame手動整合,不使用自帶的整合方式,而是手動從接口繼承並實現所有接口功能。如果你要整合的目標引擎裏面有Xna的SpriteBatch類似物,那麼直接照着這個例子去做就夠了。Samples/Myra.Samples.Silk.NET:用Silk.NET + OpenGL整合,是最底層的整合方式。雖然窗口框架還是Silk.NET提供的,不過其負責渲染的QuadBatch.cs完全是手寫。如果你要整合的目標引擎裏面只提供了最基礎的Mesh渲染方式(比如Unigine),那麼就要參考這個例子來做。Samples/Myra.Samples.Silk.NET.TrippyGL:仍然是Silk.NET + OpenGL整合,但使用TrippyGL簡化了代碼。TrippyGL提供了一個SpriteBatch類似物,叫TextureBatcher,因此這個整合過程和手動MonoGame整合很類似。其實可以跳過不看。
最後是Myra.PlatformAgnostic,Myra主項目,代碼雖然很多但整理的很有序,想看可以鑽進去看,但目前先不走那麼深。
接下來建議去這裏:https://github.com/unigine-engine/unigine-imgui-csharp-integration-sample
這個例子是Unigine整合ImGui.NET的例子,其中ImGuiImpl.cs是整個實現過程。別被這看上去亂七八糟的文件嚇到,其實它內容還是挺簡單的,只是把幾個不同的模塊全寫在了一個類裏面。好孩子不要這麼做哦!
向Unigine整合新的GUI系統,絕大部分內容都可以參考這個ImGui.NET的實現方式。後文我也會多次提到這個東西。
第二步:準備工作
Myra自帶有MonoGame、FNA和Stride的整合,同時還有PlatformAgnostic包用來應付其他的情況。我們當然要用這個包,給項目加上Myra支持很簡單,畢竟這是Unigine😁:
dotnet add package Myra.PlatformAgnostic
然後在source文件夾下建個新的文件夾,就叫MyraIntegration好了。
整合Myra到Unigine,包括整合到其他所有引擎,大概有這麼幾步工作:
- 實現接口
ITexture2DManager,實現對紋理的創建和屬性獲取。 - 實現接口
IMyraRenderer,實現窗口Scissor的設置和紋理的繪製 - 實現接口
IMyraPlatform,向Myra提供窗口、鍵盤、鼠標和觸屏的信息。目前我們暫時不管觸屏。 - 最後,將上述實現提供給
MyraEnvironment.Platform,再創建一個Myra.Graphics2D.UI.Desktop對象,將Desktop.Root設置成UI控件的實現,最後通過Desktop.Render()渲染出結果。
那麼,接下來一個一個的處理:
ITexture2DManager
Unigine創建紋理要分兩步走,注意創建為RGBA8格式,Usage要加上Dynamic,並且設置為Point Filter:
object ITexture2DManager.CreateTexture(int width, int height)
{
var texture = new Texture();
texture.Create2D(width, height, Texture.FORMAT_RGBA8, Texture.FORMAT_USAGE_DYNAMIC | Texture.SAMPLER_FILTER_POINT);
return texture;
}
Myra有個功能是Smooth Font,需要將紋理Filter設置為Bilinear。這個功能並不是指定渲染的字體是否有抗鋸齒(抗鋸齒是一直啓用的),而是在UI發生縮放的時候是否對渲染出來的文字做平滑化。目前我們先不管這個。
之後要告訴Myra紋理的尺寸,畢竟傳給Myra的是一個object而沒有其他的信息:
Point ITexture2DManager.GetTextureSize(object obj)
{
var texture = (Texture)obj;
return new Point(texture.GetWidth(), texture.GetHeight());
}
接下來要將圖像數據傳遞給紋理:
void ITexture2DManager.SetTextureData(object obj, Rectangle bounds, byte[] data)
{
using var image = new Image();
image.Create2D(bounds.Width, bounds.Height, Image.FORMAT_RGBA8, 1, false);
image.SetPixels(data);
var texture = (Texture)obj;
texture.SetImage2D(image, bounds.X, bounds.Y);
image.SetPixels((byte[])null!);
}
Unigine沒有類似OpenGL的glTexSubImage2D,不能直接往紋理上寫數據,需要創建一個Image對象然後拷貝過去。
創建的Image對象也得是RGBA8格式,和紋理保持一致。不需要Mipmap,並將clear參數設置成false,畢竟馬上就要用數據寫滿整個Image。
後面就很好理解了,將Image傳遞給Texture進行數據上傳。接下來這一行image.SetPixels((byte[])null!)不是C#裏常見的操作:將Image的緩衝區設置為null。這一點和Unigine的C++底層實現有關,它的C++底層會直接拿data的指針去用,而不進行數據拷貝。因此在最後Image.Dispose的時候會報錯。因此這裏要設置為空。
這個古怪的設計卡了我好一段時間,直到我仔細翻閲了ImGui.NET的實現才搞明白。你可以在ImGuiImpl.cs的create_font_texture()函數裏找到類似的東西。示例裏使用了一個Blob進行中轉,因為示例從ImGui獲取的數據是RGBA32格式的,需要多一個步驟轉換成RGBA8。Myra這邊數據格式是相同的因此可以省略這一步。
IMyraPlatform
Renderer牽扯的東西多一些,先來搞Platform。
Myra需要知道渲染窗口的大小,也就是Unigine的ClientRenderSize:
Point IMyraPlatform.ViewSize
{
get {
var clientRenderSize = WindowManager.MainWindow.ClientRenderSize;
return new Point(clientRenderSize.x, clientRenderSize.y);
}
}
之後實現向Myra提供鼠標信息的接口:
int mouseWheelValue;
MouseInfo IMyraPlatform.GetMouseInfo()
{
var position = Input.MousePosition - WindowManager.MainWindow.ClientPosition;
mouseWheelValue += Input.MouseWheel;
return new MouseInfo {
IsLeftButtonDown = Input.IsMouseButtonPressed(Input.MOUSE_BUTTON.LEFT),
IsRightButtonDown = Input.IsMouseButtonPressed(Input.MOUSE_BUTTON.RIGHT),
IsMiddleButtonDown = Input.IsMouseButtonPressed(Input.MOUSE_BUTTON.MIDDLE),
Position = new Point(position.x, position.y),
Wheel = mouseWheelValue,
};
}
有兩點要注意。第一點是這裏要使用Input.IsMouseButtonPressed而不是Input.IsMouseButtonDown,後者返回的是當前幀內鼠標按鍵是否有被按下過。另一點是鼠標滾輪數據,Myra需要的是累計後的絕對值(Xna的處理方式)而不是常見的相對值,因此這裏定義了一個mouseWheelValue變量將歷史數據累加起來再傳遞給Myra。
接下來需要向Myra提供鍵盤信息。由於Myra的Keys值和Unigine的不一樣(Myra用的是Xna的值,也就是Windows平台的值,Unigine使用了一套自己的東西),因此需要創建一個映射表:
readonly Keys[] UnigineToMyraKeyMap = new Keys[(int)Input.KEY.NUM_KEYS];
void GenerateMyraKeyMap()
{
UnigineToMyraKeyMap[(int)Input.KEY.ESC] = Keys.Escape;
UnigineToMyraKeyMap[(int)Input.KEY.F1] = Keys.F1;
UnigineToMyraKeyMap[(int)Input.KEY.F2] = Keys.F2;
UnigineToMyraKeyMap[(int)Input.KEY.F3] = Keys.F3;
UnigineToMyraKeyMap[(int)Input.KEY.F4] = Keys.F4;
UnigineToMyraKeyMap[(int)Input.KEY.F5] = Keys.F5;
UnigineToMyraKeyMap[(int)Input.KEY.F6] = Keys.F6;
UnigineToMyraKeyMap[(int)Input.KEY.F7] = Keys.F7;
UnigineToMyraKeyMap[(int)Input.KEY.F8] = Keys.F8;
UnigineToMyraKeyMap[(int)Input.KEY.F9] = Keys.F9;
UnigineToMyraKeyMap[(int)Input.KEY.F10] = Keys.F10;
UnigineToMyraKeyMap[(int)Input.KEY.F11] = Keys.F11;
UnigineToMyraKeyMap[(int)Input.KEY.F12] = Keys.F12;
UnigineToMyraKeyMap[(int)Input.KEY.PRINTSCREEN] = Keys.PrintScreen;
UnigineToMyraKeyMap[(int)Input.KEY.SCROLL_LOCK] = Keys.Scroll;
UnigineToMyraKeyMap[(int)Input.KEY.PAUSE] = Keys.Pause;
UnigineToMyraKeyMap[(int)Input.KEY.BACK_QUOTE] = Keys.OemTilde;
UnigineToMyraKeyMap[(int)Input.KEY.DIGIT_1] = Keys.D1;
UnigineToMyraKeyMap[(int)Input.KEY.DIGIT_2] = Keys.D2;
UnigineToMyraKeyMap[(int)Input.KEY.DIGIT_3] = Keys.D3;
UnigineToMyraKeyMap[(int)Input.KEY.DIGIT_4] = Keys.D4;
UnigineToMyraKeyMap[(int)Input.KEY.DIGIT_5] = Keys.D5;
UnigineToMyraKeyMap[(int)Input.KEY.DIGIT_6] = Keys.D6;
UnigineToMyraKeyMap[(int)Input.KEY.DIGIT_7] = Keys.D7;
UnigineToMyraKeyMap[(int)Input.KEY.DIGIT_8] = Keys.D8;
UnigineToMyraKeyMap[(int)Input.KEY.DIGIT_9] = Keys.D9;
UnigineToMyraKeyMap[(int)Input.KEY.DIGIT_0] = Keys.D0;
UnigineToMyraKeyMap[(int)Input.KEY.MINUS] = Keys.OemMinus;
UnigineToMyraKeyMap[(int)Input.KEY.EQUALS] = Keys.OemPlus;
UnigineToMyraKeyMap[(int)Input.KEY.BACKSPACE] = Keys.Back;
UnigineToMyraKeyMap[(int)Input.KEY.TAB] = Keys.Tab;
UnigineToMyraKeyMap[(int)Input.KEY.Q] = Keys.Q;
UnigineToMyraKeyMap[(int)Input.KEY.W] = Keys.W;
UnigineToMyraKeyMap[(int)Input.KEY.E] = Keys.E;
UnigineToMyraKeyMap[(int)Input.KEY.R] = Keys.R;
UnigineToMyraKeyMap[(int)Input.KEY.T] = Keys.T;
UnigineToMyraKeyMap[(int)Input.KEY.Y] = Keys.Y;
UnigineToMyraKeyMap[(int)Input.KEY.U] = Keys.U;
UnigineToMyraKeyMap[(int)Input.KEY.I] = Keys.I;
UnigineToMyraKeyMap[(int)Input.KEY.O] = Keys.O;
UnigineToMyraKeyMap[(int)Input.KEY.P] = Keys.P;
UnigineToMyraKeyMap[(int)Input.KEY.LEFT_BRACKET] = Keys.OemOpenBrackets;
UnigineToMyraKeyMap[(int)Input.KEY.RIGHT_BRACKET] = Keys.OemCloseBrackets;
UnigineToMyraKeyMap[(int)Input.KEY.ENTER] = Keys.Enter;
UnigineToMyraKeyMap[(int)Input.KEY.CAPS_LOCK] = Keys.CapsLock;
UnigineToMyraKeyMap[(int)Input.KEY.A] = Keys.A;
UnigineToMyraKeyMap[(int)Input.KEY.S] = Keys.S;
UnigineToMyraKeyMap[(int)Input.KEY.D] = Keys.D;
UnigineToMyraKeyMap[(int)Input.KEY.F] = Keys.F;
UnigineToMyraKeyMap[(int)Input.KEY.G] = Keys.G;
UnigineToMyraKeyMap[(int)Input.KEY.H] = Keys.H;
UnigineToMyraKeyMap[(int)Input.KEY.J] = Keys.J;
UnigineToMyraKeyMap[(int)Input.KEY.K] = Keys.K;
UnigineToMyraKeyMap[(int)Input.KEY.L] = Keys.L;
UnigineToMyraKeyMap[(int)Input.KEY.SEMICOLON] = Keys.OemSemicolon;
UnigineToMyraKeyMap[(int)Input.KEY.QUOTE] = Keys.OemQuotes;
UnigineToMyraKeyMap[(int)Input.KEY.BACK_SLASH] = Keys.OemBackslash;
UnigineToMyraKeyMap[(int)Input.KEY.LEFT_SHIFT] = Keys.LeftShift;
UnigineToMyraKeyMap[(int)Input.KEY.LESS] = Keys.Apps;
UnigineToMyraKeyMap[(int)Input.KEY.Z] = Keys.Z;
UnigineToMyraKeyMap[(int)Input.KEY.X] = Keys.X;
UnigineToMyraKeyMap[(int)Input.KEY.C] = Keys.C;
UnigineToMyraKeyMap[(int)Input.KEY.V] = Keys.V;
UnigineToMyraKeyMap[(int)Input.KEY.B] = Keys.B;
UnigineToMyraKeyMap[(int)Input.KEY.N] = Keys.N;
UnigineToMyraKeyMap[(int)Input.KEY.M] = Keys.M;
UnigineToMyraKeyMap[(int)Input.KEY.COMMA] = Keys.OemComma;
UnigineToMyraKeyMap[(int)Input.KEY.DOT] = Keys.OemPeriod;
UnigineToMyraKeyMap[(int)Input.KEY.SLASH] = Keys.OemQuestion;
UnigineToMyraKeyMap[(int)Input.KEY.RIGHT_SHIFT] = Keys.RightShift;
UnigineToMyraKeyMap[(int)Input.KEY.LEFT_CTRL] = Keys.LeftControl;
UnigineToMyraKeyMap[(int)Input.KEY.LEFT_CMD] = Keys.LeftWindows;
UnigineToMyraKeyMap[(int)Input.KEY.LEFT_ALT] = Keys.LeftAlt;
UnigineToMyraKeyMap[(int)Input.KEY.SPACE] = Keys.Space;
UnigineToMyraKeyMap[(int)Input.KEY.RIGHT_ALT] = Keys.RightAlt;
UnigineToMyraKeyMap[(int)Input.KEY.RIGHT_CMD] = Keys.RightWindows;
UnigineToMyraKeyMap[(int)Input.KEY.MENU] = Keys.None;
UnigineToMyraKeyMap[(int)Input.KEY.RIGHT_CTRL] = Keys.RightControl;
UnigineToMyraKeyMap[(int)Input.KEY.INSERT] = Keys.Insert;
UnigineToMyraKeyMap[(int)Input.KEY.DELETE] = Keys.Delete;
UnigineToMyraKeyMap[(int)Input.KEY.HOME] = Keys.Home;
UnigineToMyraKeyMap[(int)Input.KEY.END] = Keys.End;
UnigineToMyraKeyMap[(int)Input.KEY.PGUP] = Keys.PageUp;
UnigineToMyraKeyMap[(int)Input.KEY.PGDOWN] = Keys.PageDown;
UnigineToMyraKeyMap[(int)Input.KEY.UP] = Keys.Up;
UnigineToMyraKeyMap[(int)Input.KEY.LEFT] = Keys.Left;
UnigineToMyraKeyMap[(int)Input.KEY.DOWN] = Keys.Down;
UnigineToMyraKeyMap[(int)Input.KEY.RIGHT] = Keys.Right;
UnigineToMyraKeyMap[(int)Input.KEY.NUM_LOCK] = Keys.NumLock;
UnigineToMyraKeyMap[(int)Input.KEY.NUMPAD_DIVIDE] = Keys.Divide;
UnigineToMyraKeyMap[(int)Input.KEY.NUMPAD_MULTIPLY] = Keys.Multiply;
UnigineToMyraKeyMap[(int)Input.KEY.NUMPAD_MINUS] = Keys.Subtract;
UnigineToMyraKeyMap[(int)Input.KEY.NUMPAD_DIGIT_7] = Keys.NumPad7;
UnigineToMyraKeyMap[(int)Input.KEY.NUMPAD_DIGIT_8] = Keys.NumPad8;
UnigineToMyraKeyMap[(int)Input.KEY.NUMPAD_DIGIT_9] = Keys.NumPad9;
UnigineToMyraKeyMap[(int)Input.KEY.NUMPAD_PLUS] = Keys.Add;
UnigineToMyraKeyMap[(int)Input.KEY.NUMPAD_DIGIT_4] = Keys.NumPad4;
UnigineToMyraKeyMap[(int)Input.KEY.NUMPAD_DIGIT_5] = Keys.NumPad5;
UnigineToMyraKeyMap[(int)Input.KEY.NUMPAD_DIGIT_6] = Keys.NumPad6;
UnigineToMyraKeyMap[(int)Input.KEY.NUMPAD_DIGIT_1] = Keys.NumPad1;
UnigineToMyraKeyMap[(int)Input.KEY.NUMPAD_DIGIT_2] = Keys.NumPad2;
UnigineToMyraKeyMap[(int)Input.KEY.NUMPAD_DIGIT_3] = Keys.NumPad3;
UnigineToMyraKeyMap[(int)Input.KEY.NUMPAD_ENTER] = Keys.Enter;
UnigineToMyraKeyMap[(int)Input.KEY.NUMPAD_DIGIT_0] = Keys.NumPad0;
UnigineToMyraKeyMap[(int)Input.KEY.NUMPAD_DOT] = Keys.Decimal;
}
有了這個映射表之後,向Myra提供鍵盤信息就很簡單了:
void IMyraPlatform.SetKeysDown(bool[] keys)
{
for (int key = 0; key < (int)Input.KEY.NUM_KEYS; ++key) {
var myraKey = UnigineToMyraKeyMap[key];
keys[(int)myraKey] = Input.IsKeyDown((Input.KEY)key);
}
}
和鼠標那邊不同,長按一個鍵盤按鍵的時候,Input.IsKeyDown會多次觸發,因此可以實現長按按鍵連續輸入的效果。
剩下的兩個:
void IMyraPlatform.SetMouseCursorType(MouseCursorType mouseCursorType)
{
//TODO: Use game's custom cursor with Input.SetMouseCursorCustom()
}
TouchCollection IMyraPlatform.GetTouchState()
{
return TouchCollection.Empty;
}
SetMouseCursorType這裏,根據傳進來的MouseCursorType,用Input.SetMouseCursorCustom()設置成遊戲自定義的光標即可。目前先略過。
GetTouchState這裏,可以先忽略。Unigine是有觸控處理的API的,就在Input裏面,想實現也可以實現,不過目前Unigine不支持移動平台,忽略掉也不會有太大的問題。