在我们的案例中,我们实际上有一个专有的。它远不是一种功能齐全的语言,而只是一种旨在使使用 ECS 数据结构和组件更容易的 DSEL。
我们的主要关注点实际上是编译时反射,例如语言中定义的任何数据类型的自动序列化(UDT 使用 C 中相同的 struct 接口定义,具有相同的成员对齐)以及允许任何数据语言中定义的类型可以立即用作场景中的组件类型,无需任何明确的数据类型注册(每个数据类型都有自己的唯一类型索引,可以在编译时轻松获取,无需 RTTI)。大多数情况下,我们发现像序列化和类型注册,最重要的是,在 C 和 C++ 等语言中进行反射往往需要大量的样板文件或极其花哨的模板元编程。我们开始厌倦这一切,所以我们制作了一种相当简单的类 C 语言(而编译器实际上只是生成 C 代码)为我们自动完成所有工作。实际上,一旦 C++ 从Reflection TS 实现reflexpr,我们将不再从中受益。
该语言实际上主要侧重于以与 C 兼容的方式定义数据类型,但会自动生成函数来序列化、反序列化、比较、交换和向 ECS 注册它们。在编译时迭代 struct 的所有成员变量并将它们序列化、交换它们等操作很简单。我们最终仍然在 C++ 中实现大部分逻辑,并通过我们的语言传递数据到 C++。例如,我们可以这样写:
struct Foo
{
float x, y, z;
};
... 并立即将Foo 组件插入到我们的 ECS 场景中,而无需编写类似 ecs.register<Foo>(); 或类似性质的任何内容。我从来没有发现类型注册如此重要,因为大多数引擎没有超过几十个组件类型,但是序列化和反序列化尤其是在我们的编辑器中生成 GUI 是一个很大的 PITA。该语言的重点主要是让它变得轻而易举,尽管它确实围绕着 ECS 作为存储和表示所有程序数据的中心方式。它允许我们执行以下操作:
def(generic_type) void serialize(out_stream& out, generic_type udt)
{
// Compile-time reflection: the key feature of our language and the
// main rationale for its existence. Calls serialize(out, member) for
// each member of the user-defined type/struct.
$for_each_member(udt, serialize, out);
}
def void serialize(out_stream& out, float val)
{
out.write_float(val);
}
我们还对泛型提供了一些有限的支持,如上所示,以及函数重载(我们实际上没有成员函数,但编写 x.some_func(y) 只是编写 some_func(x, y) 的语法替代方案——我们没有由于主要关注的是 ECS,因此不必担心封装和面向对象的编程)。
至于 ECS 数据结构,它们与我们用 C 或 C++ 等语言实现它们的方式没有什么不同,实际上,它们是用 C++ 实现的。我们对编译器所做的第一件事是确保它可以轻松地与 C 互操作,并调用 C API 函数,以便我们可以在 C 端实现我们需要的任何东西(包括基本的 ECS 数据结构以及图形函数之类的东西) .这是相当标准的东西,尽管在实现一种新语言时,编译器/解释器是通过一些统一的 API 或外部函数接口让它与像 C 这样的语言互操作。在我们的例子中,由于我们的编译器生成的是 C 代码而不是机器代码,因此允许它立即调用我们喜欢通过 dylib 导出的任何 C 函数(包括用 C++ 实现的函数)非常简单。