Hello Cubos
Using Cubos to create a simple program.
This example shows the basics of how cubos::
#include <cubos/engine/prelude.hpp> using namespace cubos::engine;
First we'll need to get a Cubos
object.
int main(int argc, char** argv) { Cubos cubos{argc, argv};
The Cubos class represents the engine. We'll need it to add functionality to our program.
Let's start by defining what functionality we want to add, by adding our first system.
cubos.startupSystem("say Hello Cubos").call([]() { CUBOS_INFO("Hello Cubos"); });
This startup system simply prints Hello Cubos
to the console, using one of Cubos's logging macros. You can find more about them here. Startup systems run only once when the engine is loaded.
Now let's make things more interesting. Let's print Hello World
, but split it over two different systems.
cubos.system("say Hello").tagged(helloTag).call([]() { CUBOS_INFO("Hello"); }); cubos.system("say World").tagged(worldTag).call([]() { CUBOS_INFO("World"); });
Instead of using startupSystem
, we'll use Cubos::
Notice that we can't just do as we did for Hello Cubos
and call it a day. We want Hello
to come before World
, so we'll have to explicitly tell that to the engine, or else we risk having them in the wrong order. To do that we use tags.
cubos.tag(helloTag); cubos.tag(worldTag).after(helloTag);
Cubos::
Now let's see a bit about entities, components and resources. First we are going to need to use a few more things. We'll go over what each does as it comes up.
Lets define a new component type, which stores a single integer, which we can use to identify the entity.
#include <cubos/core/ecs/reflection.hpp> #include <cubos/core/reflection/external/primitives.hpp> struct Num { CUBOS_REFLECT; int value; }; CUBOS_REFLECT_IMPL(Num) { return cubos::core::ecs::TypeBuilder<Num>("Num").withField("value", &Num::value).build(); }
Notice that not only we define the type, but we also define reflection for it. This is a way to let Cubos know what our data type is made of, making it compatible with serialization, UI debug tools and others automatically.
We also need to register the component type with the Cubos
object.
cubos.component<Num>();
Now, lets create our own resource.
struct Pop { CUBOS_REFLECT; int count = 0; }; CUBOS_REFLECT_IMPL(Pop) { return cubos::core::ecs::TypeBuilder<Pop>("Pop").withField("count", &Pop::count).build(); }
This resource will store the total number of spawned entities, a population counter of sorts. It too needs to be registered.
cubos.resource<Pop>();
Now let's create a startup system that spawns some entities.
cubos.startupSystem("spawn entities").call([](Commands cmds, Pop& pop) { for (int i = 0; i < 10; i++) { cmds.create().add(Num{i}); pop.count += 1; } });
Commands is a system argument that allows us to interact with the world, in this case, by creating entities that have a Num
component.
To access the resource Pop
, we just add a reference to it as a system argument (Pop&
).
Finally, we'll want a system that prints our entities.
cubos.system("check entities").call([](Query<const Num&> query, const Pop& pop) { for (auto [num] : query) { CUBOS_INFO("Entity {} of {}", num.value, pop.count); } });
In this case, we don't change Pop
, and only read its value. Thus, we use const Pop&
. This allows Cubos to make some optimizations behind the scenes.
Query allows us to access all entities with a given configuration of components. In this case, it will give us all entities with the Num
component.
With everything properly set up, all that remains is to run the engine.
cubos.run(); }
Try running the sample yourself, and you should see both the hello world messages and the list of entities we spawned!