Unigine整合Myra UI Library全紀(jì)錄(1):引言,紋理與平臺
太長不看:https://github.com/horeaper/UnigineMyraIntegration
什么是Myra?
這個是Myra:https://github.com/rds1983/Myra
熟悉WPF/Avalonia/Silverlight/UWP開發(fā)的朋友肯定一眼就能看出來這個UI庫用的是什么佐料了。它當(dāng)然不是完整的WPF實(shí)現(xiàn),對于游戲開發(fā)而言也沒必要用完整的WPF,太重了。
Myra有著和WPF非常類似的Layout系統(tǒng),運(yùn)行效率也不錯,同時還支持XML聲明(被稱為MML,Myra Markup Language),既可以實(shí)時加載XML也可以用MyraPad將這些XML轉(zhuǎn)換成C#代碼。整合進(jìn)引擎里也不麻煩。這就夠了。
這玩意兒甚至FileDialog和ColorPickerDialog都給你做了一份,省了大事了??
第一步:先學(xué)再用
本文寫于Myra 1.5.9版本。
首先建議把Github上Myra的Wiki都看一遍,大概了解一下Myra是怎么玩的。之后我建議找個空文件夾git clone https://github.com/rds1983/Myra.git,然后打開Myra\build\Myra.PlatformAgnostic.sln,你會看到幾個項(xiàng)目,其中在Samples文件夾下面有這三個示例項(xiàng)目:
Samples/Myra.Samples.PlatformAgnostic:用MonoGame手動整合,不使用自帶的整合方式,而是手動從接口繼承并實(shí)現(xiàn)所有接口功能。如果你要整合的目標(biāo)引擎里面有Xna的SpriteBatch類似物,那么直接照著這個例子去做就夠了。Samples/Myra.Samples.Silk.NET:用Silk.NET + OpenGL整合,是最底層的整合方式。雖然窗口框架還是Silk.NET提供的,不過其負(fù)責(zé)渲染的QuadBatch.cs完全是手寫。如果你要整合的目標(biāo)引擎里面只提供了最基礎(chǔ)的Mesh渲染方式(比如Unigine),那么就要參考這個例子來做。Samples/Myra.Samples.Silk.NET.TrippyGL:仍然是Silk.NET + OpenGL整合,但使用TrippyGL簡化了代碼。TrippyGL提供了一個SpriteBatch類似物,叫TextureBatcher,因此這個整合過程和手動MonoGame整合很類似。其實(shí)可以跳過不看。
最后是Myra.PlatformAgnostic,Myra主項(xiàng)目,代碼雖然很多但整理的很有序,想看可以鉆進(jìn)去看,但目前先不走那么深。
接下來建議去這里:https://github.com/unigine-engine/unigine-imgui-csharp-integration-sample
這個例子是Unigine整合ImGui.NET的例子,其中ImGuiImpl.cs是整個實(shí)現(xiàn)過程。別被這看上去亂七八糟的文件嚇到,其實(shí)它內(nèi)容還是挺簡單的,只是把幾個不同的模塊全寫在了一個類里面。好孩子不要這么做哦!
向Unigine整合新的GUI系統(tǒng),絕大部分內(nèi)容都可以參考這個ImGui.NET的實(shí)現(xiàn)方式。后文我也會多次提到這個東西。
第二步:準(zhǔn)備工作
Myra自帶有MonoGame、FNA和Stride的整合,同時還有PlatformAgnostic包用來應(yīng)付其他的情況。我們當(dāng)然要用這個包,給項(xiàng)目加上Myra支持很簡單,畢竟這是Unigine??:
dotnet add package Myra.PlatformAgnostic
然后在source文件夾下建個新的文件夾,就叫MyraIntegration好了。
整合Myra到Unigine,包括整合到其他所有引擎,大概有這么幾步工作:
- 實(shí)現(xiàn)接口
ITexture2DManager,實(shí)現(xiàn)對紋理的創(chuàng)建和屬性獲取。 - 實(shí)現(xiàn)接口
IMyraRenderer,實(shí)現(xiàn)窗口Scissor的設(shè)置和紋理的繪制 - 實(shí)現(xiàn)接口
IMyraPlatform,向Myra提供窗口、鍵盤、鼠標(biāo)和觸屏的信息。目前我們暫時不管觸屏。 - 最后,將上述實(shí)現(xiàn)提供給
MyraEnvironment.Platform,再創(chuàng)建一個Myra.Graphics2D.UI.Desktop對象,將Desktop.Root設(shè)置成UI控件的實(shí)現(xiàn),最后通過Desktop.Render()渲染出結(jié)果。
那么,接下來一個一個的處理:
ITexture2DManager
Unigine創(chuàng)建紋理要分兩步走,注意創(chuàng)建為RGBA8格式,Usage要加上Dynamic,并且設(shè)置為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設(shè)置為Bilinear。這個功能并不是指定渲染的字體是否有抗鋸齒(抗鋸齒是一直啟用的),而是在UI發(fā)生縮放的時候是否對渲染出來的文字做平滑化。目前我們先不管這個。
之后要告訴Myra紋理的尺寸,畢竟傳給Myra的是一個object而沒有其他的信息:
Point ITexture2DManager.GetTextureSize(object obj)
{
var texture = (Texture)obj;
return new Point(texture.GetWidth(), texture.GetHeight());
}
接下來要將圖像數(shù)據(jù)傳遞給紋理:
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,不能直接往紋理上寫數(shù)據(jù),需要創(chuàng)建一個Image對象然后拷貝過去。
創(chuàng)建的Image對象也得是RGBA8格式,和紋理保持一致。不需要Mipmap,并將clear參數(shù)設(shè)置成false,畢竟馬上就要用數(shù)據(jù)寫滿整個Image。
后面就很好理解了,將Image傳遞給Texture進(jìn)行數(shù)據(jù)上傳。接下來這一行image.SetPixels((byte[])null!)不是C#里常見的操作:將Image的緩沖區(qū)設(shè)置為null。這一點(diǎn)和Unigine的C++底層實(shí)現(xiàn)有關(guān),它的C++底層會直接拿data的指針去用,而不進(jìn)行數(shù)據(jù)拷貝。因此在最后Image.Dispose的時候會報錯。因此這里要設(shè)置為空。
這個古怪的設(shè)計卡了我好一段時間,直到我仔細(xì)翻閱了ImGui.NET的實(shí)現(xiàn)才搞明白。你可以在ImGuiImpl.cs的create_font_texture()函數(shù)里找到類似的東西。示例里使用了一個Blob進(jìn)行中轉(zhuǎn),因?yàn)槭纠龔腎mGui獲取的數(shù)據(jù)是RGBA32格式的,需要多一個步驟轉(zhuǎn)換成RGBA8。Myra這邊數(shù)據(jù)格式是相同的因此可以省略這一步。
IMyraPlatform
Renderer牽扯的東西多一些,先來搞Platform。
Myra需要知道渲染窗口的大小,也就是Unigine的ClientRenderSize:
Point IMyraPlatform.ViewSize
{
get {
var clientRenderSize = WindowManager.MainWindow.ClientRenderSize;
return new Point(clientRenderSize.x, clientRenderSize.y);
}
}
之后實(shí)現(xiàn)向Myra提供鼠標(biāo)信息的接口:
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,
};
}
有兩點(diǎn)要注意。第一點(diǎn)是這里要使用Input.IsMouseButtonPressed而不是Input.IsMouseButtonDown,后者返回的是當(dāng)前幀內(nèi)鼠標(biāo)按鍵是否有被按下過。另一點(diǎn)是鼠標(biāo)滾輪數(shù)據(jù),Myra需要的是累計后的絕對值(Xna的處理方式)而不是常見的相對值,因此這里定義了一個mouseWheelValue變量將歷史數(shù)據(jù)累加起來再傳遞給Myra。
接下來需要向Myra提供鍵盤信息。由于Myra的Keys值和Unigine的不一樣(Myra用的是Xna的值,也就是Windows平臺的值,Unigine使用了一套自己的東西),因此需要創(chuàng)建一個映射表:
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);
}
}
和鼠標(biāo)那邊不同,長按一個鍵盤按鍵的時候,Input.IsKeyDown會多次觸發(fā),因此可以實(shí)現(xiàn)長按按鍵連續(xù)輸入的效果。
剩下的兩個:
void IMyraPlatform.SetMouseCursorType(MouseCursorType mouseCursorType)
{
//TODO: Use game's custom cursor with Input.SetMouseCursorCustom()
}
TouchCollection IMyraPlatform.GetTouchState()
{
return TouchCollection.Empty;
}
SetMouseCursorType這里,根據(jù)傳進(jìn)來的MouseCursorType,用Input.SetMouseCursorCustom()設(shè)置成游戲自定義的光標(biāo)即可。目前先略過。
GetTouchState這里,可以先忽略。Unigine是有觸控處理的API的,就在Input里面,想實(shí)現(xiàn)也可以實(shí)現(xiàn),不過目前Unigine不支持移動平臺,忽略掉也不會有太大的問題。

浙公網(wǎng)安備 33010602011771號