We actually have a proprietary one we use in our case. It's far from a full-featured language and just a DSEL designed to make working with ECS data structures and components easier.
The main focus for us was actually compile-time reflection for things like automatic serialization of any data type defined in the language (UDTs are defined using an identical struct
interface in C with identical member alignment) along with allowing any data type defined in the language to be used immediately as a component type in a scene without any explicit registration of the data type (each data type gets its own unique type index which can be grabbed at compile-time easily without RTTI). Mostly we found that doing things like serialization and type registration and, most of all, reflection in languages like C and C++ tend to require enormous amounts of boilerplate or extremely fancy template metaprogramming. We started to get tired of it all, so we made a fairly simple C-like language (and the compiler actually just generates C code) that automates it all for us. Actually, we would cease to benefit from it so much once C++ implements reflexpr
from Reflection TS
.
The language is actually focused predominantly on defining data types in a way compatible with C, but automatically generating functions to serialize, deserialize, compare, swap, and register them with the ECS. It's straightforward there to do things like iterate over all the member variables of a struct
at compile-time and serialize them, swap them, etc. We still end up implementing most of our logic in C++ and just pass the data through our language to C++. For example, we can just write:
struct Foo
{
float x, y, z;
};
... and immediately insert Foo
components into our ECS scene without having to write like, ecs.register<Foo>();
or anything of this nature. I never found that type registration to be such a huge deal since most engines don't have more than a few dozen component types, but serialization and deserialization in particular along with generating GUIs in our editor was a big PITA. The language's focus was primarily to make that a breeze although it does revolve around the ECS as the central way of storing and representing all the program data. It allows us to do things like:
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);
}
We also have some limited support for generics as can be seen above along with function overloading (we don't actually have member functions but writing x.some_func(y)
is just a syntactical alternative for writing some_func(x, y)
-- we didn't bother with encapsulation and object-oriented programming since the primary focus is ECS).
As for the ECS data structures, they're not different than how we'd implement them in languages like C or C++, and actually, they are implemented in C++. The first thing we did with our compiler was to make sure it can easily interop with C, and call C API functions so that we could implement whatever we need on the C end (including the fundamental ECS data structures and also things like graphical functions). That's fairly standard stuff though when implementing a new language and compiler/interpreter is to have it interop with a language like C through some uniform API or foreign function interface. In our case, since our compiler generates C code rather than machine code, it was very straightforward to allow it to call any C functions (including ones implemented in C++) we like exported through a dylib immediately.