添加内部调用(与 C# 和 C++ 相互通信)

如果您希望 C# 代码调用 C++ 文件中的某个函数,则只应添加内部调用, 但是如果您希望增加 C++ 的性能,或者因为需要与 C# 接口进行通信,或向 C# 公开 C++ 的 API,则可能需要添加内部调用。

我们将首先提供添加内部调用的相关指南,请您 务必 遵循这些指南, 这些指南是为了让其他人更容易理解以及维护您所编写的代码,并保持代码的一致性。

在 C# 中编写内部调用的相关指南

当您向 C# API 添加内部调用时,您需要考虑以下几点:

  1. 内部方法应该 始终InternalCalls.cs 文件中定义,我们从不在使用这些方法的类中进行定义。
  2. 内部方法应该 始终#region 代码块内定义,以保持项目文件的组织结构体清晰,您也可以参考相关代码块,看看我们是如何编写的。
  3. 内部方法应该 始终 以以下格式命名:ClassName_FunctionName。例如: TransformComponent_GetTranslationMeshComponent_GetMaterial
  4. 内部方法应该 始终 定义为 internal static extern
  5. 内部方法在 C++ 中的名称应该 始终 与C#中的名称相同,因此在 C++ 中也应该调用名为 TransformComponent_GetTranslation 的方法。
  6. 内部方法 必须 标记此属性: [MethodImpl(MethodImplOptions.InternalCall)]
  7. 如果您需要将基元类型(numerical types, boolcharobject) 传递给内部调用,则应复制它们,或将其作为 out 传递, 而不是作为 ref 传递。
  8. 如果您需要一个内部方法来返回一个基元类型,那么它要么将该类型作为返回类型,要么将其作为 out 参数传递。 例如:如果 internal static extern boolinternal static extern void SomeClass_SomeFunc(out bool result) 需要返回多个参数时,应该使用 out 传递。
  9. 如果需要将 struct 传递给内部调用,则 必须 将其作为 refout 传递, 这取决于 C++ 函数是读取结构体还是写入结构体。
  10. 将结构体作为 ref 传递意味着 C++ 函数将只从结构体中读取,而不会对其进行写入。遵循这一规则是 的责任,Mono 不会强制执行它。
  11. 将结构体作为 out 传递意味着 C++ 函数将读取或写入该结构体。
  12. 将结构体作为 outref 传递意味着 C++ 函数将采用等效的 C++ 结构体作为指针。
  13. 枚举不应作为 refout 传递!它们只是基元类型,如果存在 C++ 等效枚举,它们可以按原样传递,或者它们可以作为 int 传递。

这些是您需要记住并注意的一些基本内容,如果您认为这些指南存在问题应该修改或是应该详细的展开来说,请随时联系我们!

在 C++ 中编写内部调用的相关指南

当您向 C++ API 添加内部调用时,您需要考虑以下几点:

  1. 要在 C++ 中实现内部调用应该 始终ScriptGlue.h 文件中声明,并在 ScripGlue.cpp 文件中进行定义。
  2. 为了向 C++ API 注册内部调用,必须在代码中添加以下行: ETOD_ADD_INTERNAL_CALL(ClassName_FunctionName);ScriptGlue::RegisterInternalCalls, 最好与 InternalCalls.cs 文件中的声明顺序相同。
  3. 如果使用内部方法记录日志消息,则最好将这些日志消息记录到 EToD-Editor 控制台, 例如使用 ETOD_CONSOLE_LOG_INFO 这行代码实现,但这不是必要的,主要取决于您正在记录的日志内容。
  4. 始终在 C++ 中 InternalCalls命名空间 内添加内部调用,这是注册函数所 必须 的。

可以了解到的是,需要记住的东西还有很多,但关键是要遵循这些准则,并确保您所添加的代码与 C++ 和 C# API 中已经存在的代码相一致。

示例

下面是一个如何向脚本 API 添加内部调用的非常基本的示例,我们将从使用 C# 声明内部调用开始:

假设我们在 C# 中有一个自定义结构体,我们希望用 C++ 中的一些数据填充该结构体,如果成功,我们的内部调用将返回 true,下面是该结构体的一种代码示例:

// 必须将此属性添加到要传递到的 C++ 结构体中
[StructLayout(LayoutKind.Sequential)]
public struct MyCustomData
{
	public float MyFloat;
	public int SomeInt;
}
// InternalCalls.cs

...

#region MyCustomComponent

[MethodImpl(MethodImplOptions.InternalCall)]
internal static extern bool MyCustomComponent_GetCustomStruct(ulong entityID, out MyCustomData outData);

#endregion

...

在这里,您可以看到我们定义了内部调用,它返回一个 bool,并接收两个参数, " entityID " 参数只是为了保持一致性。在这里,所有 C# 组件都必须将实体的 ID 传递给 C++,这样引擎就可以知道组件属于哪个实体。

以及它还将我们的自定义结构体作为 out 参数,这意味着我们希望 C++ 将一些数据写入该结构体,但它也可以从中读取数据。

这就是在 C# 中定义方法所需要做的全部工作,然后我们要在 C++ 中添加以下代码:

// MyCustomData struct (可以是C++中的类,但在可能的情况下我们可以使用结构体)
struct MyCustomData
{
	float MyFloat;
	int SomeInt;
};

// ScriptGlue.h

namespace InternalCalls {

...

#pragma region MyCustomComponent

bool MyCustomComponent_GetCustomStruct(uint64_t entityID, MyCustomData* outData);

#pragma endregion

...
}

然后在 ScriptGlue.cpp 文件中添加实现此函数的相关代码:

// ScriptGlue.cpp

namespace InternalCalls {

...

#pragma region MyCustomComponent

bool MyCustomComponent_GetCustomStruct(uint64_t entityID, MyCustomData* outData)
{
	// 注:伪代码,查看现有的内部调用以供参考
	if (!is_entity_valid)
	{
		// 在此处记录一些错误日志信息
		return false;
	}

	if (!entity_has_MyCustomComponent)
	{
		// 几乎不会发生,但如果发生则记录错误信息
		return false;
	}

	*outData = myCustomComponent.MyData;
	// 或是:
	*outData.MyFloat = someFloatValue;
	*outData.SomeInt = someIntValue;
	return true;
}

#pragma endregion

...
}

这只是介绍了添加内部调用的基础知识,但它应该能让您了解我们该如何向 C++ 或是 C# API 中添加内部调用。