MyraIntegration
当Texture2DManager,MyraRenderer和MyraPlatform都实现了之后,就可以将它们整合起来了。
首先,IMyraPlatform.Renderer
返回实现好的MyraRenderer,而IMyraRenderer.TextureManager
则返回实现好的Texture2DManager。
接下来建立一个新的static class MyraIntegration
。写一个Init()函数,这里需要设置一些状态:
csharp
MyraEnvironment.Platform = new MyraPlatform();
MyraEnvironment.EnableModalDarkening = true;
将实现好的MyraPlatform交给Myra环境。而EnableModalDarkening则表示在以ShowModal()方式显示FileDialog、ColorPickerDialog等Dialog时,会给背景加一层半透明黑色遮罩。告诉用户现在显示的是一个模态窗口。
接下来需要对IME提供支持。当你用输入法输入文字的时候,只用Input.IsKeyDown
是捕捉不到的。不过Unigine提供了其他的方法:
csharp
Desktop.HasExternalTextInput = true; //告诉Myra平台有额外的文字输入
Input.EventTextPress.Connect(static text => {
if (!Input.MouseGrab && text <= char.MaxValue) {
Desktop.OnChar((char)text);
}
});
Unigine发送的是uint,如果你输入了超过UCS-2字符集的文字,那么这个值是会超过char的值上限的,而Myra无法支持这些文字,因此要过滤一下再提交给Myra。
最后需要告诉Myra在什么时候可以进行渲染。一般来说,Unigine可以在三个事件内进行手动渲染工作:Render.EventEndScreen
,Render.EventEndVisualizer
和Engine.EventEndPluginsGui
。在Unigine提供的示例项目中三种都有出现。不过在前两个事件内要渲染的时候,需要获取当前RenderTarget(通过Renderer.RenderTarget
)或者获取一个临时RenderTarget(通过Render.GetTemporaryRenderTarget()
并在全部渲染结束之后调用Render.ReleaseTemporaryRenderTarget()
)。具体的做法ImGui.NET示例里有。而如果渲染工作放在EventEndPluginsGui则不需要RenderTarget的介入。前面在制作TextureQuadBatcher时没有添加RenderTarget支持,因此这里使用EventEndPluginsGui。代码很简单:
csharp
Engine.EventEndPluginsGui.Connect(static () => Desktop.Render());
基本的整合就已经完成了。实际上现在应用已经可以将Myra的界面正确的渲染出来,但在键盘鼠标交互上会有问题。Unigine在你点击了窗口内部之后,会自动捕获鼠标光标,并接管鼠标移动的消息处理。然而在用户操作UI控件的时候,不能让Unigine来捣乱,因此需要写一些代码来过滤掉部分事件。
和ImGui不同,Myra并没有一个简单的方法告诉你"我需要拦截发往游戏引擎的鼠标消息",因此这里使用一个简单暴力的方法:当鼠标位置在Myra的UI控件上面,并且引擎没有捕获鼠标的时候,则不向引擎发送鼠标消息。
首先,还是在Init()函数里,加这么一堆:
csharp
Engine.EventBeginRender.Connect(static () => {
if (Desktop.IsMouseOverGUI && !Input.MouseGrab) {
Gui.GetCurrent().MouseButtons = 0;
}
});
这几行其实是为了防止和Unigine自带的Gui系统冲突。如果你不用Unigine.Gui的话,有没有这些影响不大。
Init()函数就到此为止了。接下来,有些事件需要每帧都处理一下,因此再写一个Update()函数:
csharp
if (!Input.MouseGrab) {
ControlsApp.Enabled = !Desktop.IsMouseOverGUI;
if (Desktop.IsMouseOverGUI) {
ControlsApp.MouseDX = 0;
ControlsApp.MouseDY = 0;
}
if (Desktop.IsMouseOverGUI) {
Input.MouseHandle = Input.MOUSE_HANDLE.USER;
}
else {
Input.MouseHandle = Input.MOUSE_HANDLE.GRAB;
}
}
这坨代码是从ImGui.NET整合示例里抄来的,功能就是鼠标在非捕获状态下,移动到Myra控件上方时,不管怎么点都不让引擎捕获鼠标。而移出Myra控件的范围则恢复原来的功能。
接下来还要加一坨代码:
csharp
if (Input.MouseGrab && Desktop.FocusedKeyboardWidget != null) {
Desktop.FocusedKeyboardWidget = null;
}
功能也很简单:当鼠标进入了捕获状态,但某个控件仍然有键盘的Focus状态时(比如正在某个TextBox里输入),则取消这个控件的Focus状态。避免玩家在游戏里WASD移动的时候,输入框里一堆wwwwaaaaassssddddd出来了。
到此,MyraIntegration就算做完了。接下来到Unigine自动创建的AppSystemLogic.cs里面去。在AppSystemLogic.Init()
内调用MyraIntegration.Init()
,在AppSystemLogic.Update()
内调用MyraIntegration.Update()
。整合工作就算完成了。
接下来当然是测试一下。测试用例就用Myra官方示例提供的好了。
去前面clone出来的Myra的源代码里,去到Myra/samples/Myra.Samples.Silk.NET
文件夹,将AllWidgets.cs和AllWidgets.Generated.cs这两个文件复制过来。
接下来回到MyraIntegration.Init()
函数,加这么一行代码:
csharp
Desktop.Root = new Myra.Samples.AllWidgets.AllWidgets();
然后大胆的运行吧。只要程序没炸,应该能看到这样一个画面:
这就算成功了。点一点各个按钮,看看各种Dialog的效果,看看各种Debug显示的效果,其中将鼠标指向的控件高亮的功能可以多试试,顺便熟悉一下这个界面里Myra各个控件是怎么划分的。最后在TextBox里输入一下文字看看有没有正常显示,点到控件外面看看Unigine有没有正确拿回鼠标控制权。
由于Myra自带的字体文件是纯英文,因此输入中文会显示成空白,目前可以在前面MyraIntegration.Init()
里面Input.EventTextPress
的处理过程中下一个断点,看看输入的文字有没有被正确的传递进去。之后自行给TextBox换个字体就可以。
优化:新的QuadBatcher
前面在制作TextureQuadBatcher时,选择了比较简单直接的实现方式,效率不高。有一个优化方向就是将所有传递进来的Vertex先缓存起来,然后一次性全提交给MeshDynamic。之后在渲染的时候,根据不同的Scissor和Texture,每次只渲染其中的一部分。这样就省去了多次ClearVertex/SetVertexArray/FlushVertex的过程。
思路上是仿照ImGui.NET的整合示例:当调用Desktop.Render()
的时候,Myra会依次调用Begin -> Scissor/DrawQuad -> End,新的QuadBatcher则修改为在这整个过程中,并不进行实际的渲染,而是记录下Scissor/Texture状态,以及在这个状态下渲染的所有Vertex。
Github仓库里面已经是最终优化后代码。这里完整的代码就不贴了,大概说一下整个改进的过程是怎么样的。
首先看QuadBatcher.cs,这里相比于旧的TextureQuadBatcher,主要是多了这么一个变量:
csharp
readonly List<(Texture? texture, Rectangle? scissor, int vertexIndex)> renderData = new(16);
每当Texture或者Scissor发生变化的时候,就记录在这个renderData里面,同时记录下来当前的Vertex序号。之后渲染的时候就能利用这个数据一段一段的将MeshDynamic里面的东西渲染出去。
QuadBatcher.NewBatch()
负责将所有变量重置/清空。
QuadBatcher.DrawQuad()
和QuadBatcher.SetScissorTest()
可以看到将数据记录在renderData的过程。并且这里不再将顶点Flush出去,而只是在vertexCount超过了MaxVertices之后,统一渲染然后重新开始下一批次。
QuadBatcher.RenderBatch()
就是改动的重点了。既然所有的渲染都会在这里统一进行,那么首先把旧TextureQuadBatcher的Begin()/End()里面的代码拿过来。Flush()里面应用顶点数据的部分也可以照搬。
重点是后面RenderSurface的部分。由于现在只渲染一部分,因此这段代码改动量最大:
csharp
int currentVertex = 0;
foreach (var (texture, scissor, vertexIndex) in renderData) {
if (vertexIndex > currentVertex) {
quadMesh.RenderSurface(MeshDynamic.MODE_TRIANGLES, 0, currentVertex / 4 * 6, vertexIndex / 4 * 6);
currentVertex = vertexIndex;
}
if (texture != null) {
RenderState.SetTexture(RenderState.BIND_FRAGMENT, 0, texture);
}
if (scissor != null) {
int y = clientRenderSize.y - (scissor.Value.Y + scissor.Value.Height);
RenderState.SetScissorTest((float)scissor.Value.X / clientRenderSize.x, (float)y / clientRenderSize.y, (float)scissor.Value.Width / clientRenderSize.x, (float)scissor.Value.Height / clientRenderSize.y);
}
}
if (currentVertex < vertexCount) {
quadMesh.RenderSurface(MeshDynamic.MODE_TRIANGLES, 0, currentVertex / 4 * 6, vertexCount / 4 * 6);
}
由于我们把每个"分段"信息都记录在了renderData,因此遍历这个数组,按照里面记录的信息进行RenderSurface和SetTexture/SetScissorTest的调用工作。由于我们保存在renderData里的vertexIndex,记录的实际上是"从这里开始,后面这一段Vertex渲染时候用的Texture和Scissor",因此在整个数组遍历完之后,还要再调用一次RenderSurface把余下的一段都给渲染出来。
之后就要回到MyraRenderer,首先把旧的TextureQuadBatcher实例删掉。然后添加NewRender()和DrawToOutput(),并修改IMyraRenderer.Scissor和IMyraRenderer.DrawQuad()来调用QuadBatcher相应的函数,再把IMyraRenderer.Begin()和IMyraRenderer.End()的内容都删掉。
最后回到MyraIntegration,将Engine.EventEndPluginsGui
的事件处理被改成这个样子:
csharp
var renderer = (MyraRenderer)MyraEnvironment.Platform.Renderer;
renderer.NewRender(); //做准备工作
Desktop.Render(); //Myra进行"渲染"工作
renderer.DrawToOutput(); //Unigine进行实际的渲染工作
这样就完成了整个优化过程。和旧的TextureQuadBatcher相比,每次渲染MeshDynamic只被更新了一次,大大提高了CPU/GPU协同的效率。
完
可以看出整个整合的工作并不难,主要代码量是制作一个高效率的SpriteBatch类似物,以及映射鼠标/键盘消息。剩下的就是一些框架粘合代码。
Myra把大部分琐碎的事情都自己完成了,因此将其整合到任何一个引擎里(只要这个引擎里提供了SpriteBatch类似物,或者允许你直接渲染顶点数据)都是很容易的。比如在这里做好的这个QuadBatcher,拿去改一改,就能用到别的引擎的整合工作上。例如Flax Engine,把MeshDynamic相关的内容换成FlaxEngine.Render2D
就可以(Render2D能直接渲染顶点,无需创建额外的Mesh)。