将组件从 C++ 公开到 C#
本章节将告诉您将组件从 C++ 公开到 C# 所需遵循的所有步骤。
以下是向 C# 公开组件所需完成的步骤概要:
- 添加
ETOD_REGISTER_MANAGED_COMPONENT(MyCustomComponent);
到ScriptGlue::RegisterComponentTypes
。 - 通过在
Components.cs
文件中定义一个类来实现 C# 组件,并使其从Component
中继承。 - 添加必要的内部调用,让 C# 组件与 C++ 组件和引擎系统进行交互。
请务必保持 ETOD_REGISTER_MANAGED_COMPONENT
与组件内部调用的顺序相同,因此如果您的内部调用定义在例如
ScriptGlue.h
文件中的 RigidBodyComponent
下,您应该在 ScriptGlue::RegisterComponentTypes
和
ScriptGlue::RegisterInternalCalls
中的 RigidBodyComponent
下注册您的组件和内部调用,这有助于保持文件代码结构的井然有序。
编写 C# 组件的方式在很大程度上取决于我们引擎的组件系统,但基本的实现原理如下:
- 您需要向
Components.cs
文件中添加一个继承自Component
的类, 例如:public class MyComponent : Component
。 - 您需要添加调用必要内部方法的方法或属性。(有关如何执行此操作的相关信息,请参阅上一章内容)
- 任何需要与您的组件交互的内部方法都必须接收实体的 ID,您可以通过访问
Entity.ID
来从组件内部访问该 ID。 - 实体 ID 是 64 位无符号整数,其 C# 类型称为
ulong
,C++ 类型称为uint64_t
。
关于 C# 中堆分配的概要说明
从一开始 EToD Engine 的核心脚本中就存在一个问题,是关于调用内部方法时的堆分配问题。我们不会深入探讨堆分配这个概念,
但在 C# 中,当您通过调 new MyClass()
创建类的新实例时,将导致该对象会被分配到堆上。
在 EToD Engine 的核心脚本中,我们经常在调用内部方法后返回一个类的新实例,例如下面是
MeshColliderComponent
的一个简单示例:
// 下面的示例代码并不是最优,因为每次我们调用 `MeshColliderComponent.ColliderMesh` 时,
// 我们每次都分配一个全新的 `Mesh` 实例,即使 Mesh 内部并没有发生任何的改变。
public Mesh ColliderMesh
{
get
{
InternalCalls.MeshColliderComponent_GetColliderMesh(Entity.ID, out AssetHandle outMeshHandle);
return new Mesh(outMeshHandle);
}
}
// 这是实现此属性的更优的解决方案:
// 首先我们添加一个名为 m_ColliderMesh 的私有成员变量。
private Mesh m_ColliderMesh = null;
// 其次我们添加一个名为 ColliderMeshHandle 的 AssetHandle 属性,
// 这实际上是 Mesh 的句柄
public AssetHandle ColliderMeshHandle
{
get
{
if (!InternalCalls.MeshColliderComponent_GetColliderMesh(Entity.ID, out AssetHandle colliderHandle))
return AssetHandle.Invalid;
return colliderHandle;
}
}
// 最后,我们添加一个返回 Mesh 的方法。
public Mesh GetColliderMesh()
{
// 这里我们确保内部存储的 Mesh 句柄仍然有效,
// 如果不是则返回 null
if (!ColliderMeshHandle.IsValid())
return null;
// 在这里,我们检查是否尚未设置 m_ColliderMesh 或者内部 Mesh 是否已更改,
// 如果是我们将创建一个新的 Mesh 实例
if (m_ColliderMesh == null || m_ColliderMesh.Handle != ColliderMeshHandle)
m_ColliderMesh = new Mesh(ColliderMeshHandle);
// 否则我们只返回 Mesh 实例的缓存版本
return m_ColliderMesh;
}
我们使用以上的解决方案可能不会去提高性能,它甚至可能还会 稍微 增加一些性能开销(但是性能开销并不明显), 但是使用以上解决方案这确实 减少了 堆分配的数量。
那么 为什么 我们要避免堆分配呢?因为如果你每一帧都在堆上分配对象,它则很有可能会导致引擎的性能出现问题,同时它也会导致 C# 的垃圾收集器运行得更频繁。我们在此不会深入的解释垃圾收集器是什么,或者它为什么有用,有什么用。而我们需要着重要了解的部分是,C# 的垃圾收集器会清理未使用的 C# 对象,这样做会导致严重的帧丢失问题的产生。 所以我们要尽可能的阻止垃圾收集器的运行,但是又因为 C# 的设计它最终还是会运行起来。因此我们不可能完全阻止它运行,但同时我们又不想让它完全运行起来,所以我们只能想办法来延迟它的运行。
在 C# 中缓存对象并不总是最好的解决方案,除非你真的需要在 C# 中缓存,否则你不需要将对象的原始类型缓存在 C# 中,并且结构 通常 是分配在堆栈上(尽管它们有时可以分配在堆上)。
该选择属性还是方法?
您应该使用属性还是方法来与需要实现的相关功能的 C++ 端进行交互? 我们的回答是:要看情况。
如果你正在做的缓存会存在与上面的示例中相似的代码,那么你应该使用一个方法,而不是一个属性。
我们不会对此进行过多的详细介绍,因为它更像是一个类似 C# 代码书写风格的相关问题,而不是在做 EToD Engine 拓展研发时会遇到的特定的代码问题。