Deconstructing Flecs Prefabs

One of the key design goals of Flecs is to enable reusability. When we talk about reusability, it is often the reusability of code. While that is a big part of it, reusability of data is just as important, especially in games. Data comes in many forms, like models, materials, textures or data for game mechanics.

Most, if not all games have elements of repetition. Whether it is shapes in a Tetris games, units in an RTS game, decorative assets like plants, mailboxes or refrigerators or just about any particle system, repetition is everywhere. Sometimes it is a simple element that is repeated, and sometimes it is a complex composition of many hierarchically organized nodes

When writing a game, we like to treat these compositions as a single entity, and not as the collection of entities that they really are. For example, when a player creates a new tank, we want to have a single command that creates the whole tank, instead of having to individually create the turret, machine gun, body and rubber tracks.

Additionally, a game may have many different kinds of tanks, each a slight variation of the other. The same tank body could be equipped with a gunpowder, railgun or laser turret. By being able to reuse components between tanks, we can reduce the amount of time spent on developing new assets. In some cases this ability can even be exposed as a game mechanic, where a user can compose his or her own custom units on the fly.

The ability to create, reuse and combine different elements is enabled by a prefab or template system.

While prefab systems can be built on top of an ECS, with Flecs I decided to integrate creating and instantiating prefabs with the ECS store. This enables several performance optimizations and abilities that would otherwise be hard or even impossible to implement. Another advantage is that having a consistent set of features makes it possible to reuse prefabs across unrelated projects.

The other side of that coin is that prefab requirements can be very project-specific, and trying to design a one-size-fits all can easily end up being too constraining or too over-engineered (or worse: both). To avoid this, the ability to create prefabs in Flecs is not designed as a single monolithic feature, but rather is the result of a number of individual features working together. The next sections walk through the different features that enable prefabs.

To explore prefabs, let’s start with a rudimentary tree, like the ones shown in the image. Each “tree” is composed out of two boxes, where one represents the trunk and another the leaves, which we will refer to as the canopy (I’m not sure this is the proper term, but it makes for a nice prefab name). We will design this prefab so that an application simply has to instantiate it without additional logic, starting with the trunk.

Modeling the trunk is easy. We just need to create an entity that describes a brownish box that is slightly taller than wide:

auto trunk = ecs.entity()
.set<Box>({0.4, 0.5, 0.4})
.set<Color>({0.25, 0.2, 0.1});

We can now easily create multiple instances of this trunk, by using Flecs instancing. Instancing allows us to share components from a “base” entity with any number of instances. Let’s use the trunk as our base, and create a few instances by running this statement a few times:

ecs.entity()
.add_instanceof(trunk)
.set<Position>({randf(10), 0, randf(10)});

While this is creating multiple trunks, something is wrong. The trunks are sunk into the ground. This happens because the box is centered around the position we gave our entities. To fix this, we could move our instances up a little bit. A cleaner solution however is to create a tree parent entity and position the trunk relative to the tree so that it starts at zero:

auto tree = ecs.entity()
auto trunk = ecs.entity()
.add_childof(tree)
.set<Position>({0, 0.25, 0})
.set<Box>({0.4, 0.5, 0.4})
.set<Color>({0.25, 0.2, 0.1});

Let’s update instance creation so that it uses the tree instead:

ecs.entity()
.add_instanceof(tree)
.set<Position>({randf(10), 0, randf(10)});

That worked! When instantiating an entity, children of that entity are created for the instance as well. While this is an very simple example, Flecs supports sprawling hierarchies, and even nested prefab trees.

There is one problem though, which is that our prefab is a regular entity, and regular entities get matched with systems. If we don’t do anything special, our prefabs will be rendered and that is definitely not what we want. To fix this, we can create the entity as a prefab, which adds a Prefab tag to the entity. This tag prevents it from getting matched with systems:

auto tree = ecs.prefab("PTree")
auto trunk = ecs.prefab()
.add_childof(tree)
.set<Position>({0, 0.25, 0})
.set<Box>({0.4, 0.5, 0.4})
.set<Color>({0.25, 0.2, 0.1});

Note that we also gave our top-level prefab a name. This is good practice as it makes our application easier to debug, but is not mandatory.

Next, let’s add the canopy. We can model this as just another box that is positioned above the trunk:

auto tree = ecs.prefab("PTree")
auto trunk = ecs.prefab()
.add_childof(tree)
.set<Position>({0, 0.25, 0})
.set<Box>({0.4, 0.5, 0.4})
.set<Color>({0.25, 0.2, 0.1});
auto canopy = ecs.prefab()
.add_childof(tree)
.set<Position>({0, 0.9, 0})
.set<Box>({0.8, 0.8, 0.8})
.set<Color>({0.2, 0.3, 0.15});

This is looking more like a tree, except that they are all the same size. To make it look a bit more realistic (as realistic as boxes get) we should assign a random height to the canopy. To do this, we need to create a system that randomizes the height of the canopy box when the prefab is instantiated.

ecs.system<Position, Box>("RandomizeCanopy")
.kind(flecs::OnSet)
.each([](flecs::entity e, Position &p, Box &b) {
float h = randf(1.0) + 0.8;
b.height = h;
p.y = h / 2.0 + 0.5;
});

Let’s see what this does to our forest:

Hmm, not exactly what we were expecting. The reason why this is happening is because all the entities in the scene have both a Box and Position, and as a result, they got matched with our system. We need to make sure that we only apply this system to canopy entities. For this we will create a Canopy tag, and add it to our canopy entities and system.

First we create the tag as an empty struct:

struct Canopy { };

Then we add it to the canopy entity:

  auto canopy = ecs.prefab()
.add_childof(tree)
.add<Canopy>()
.set<Position>({0, 0.9, 0})
.set<Box>({0.8, 0.8, 0.8})
.set<Color>({0.2, 0.3, 0.15});

Finally we update our system query with the tag:

ecs.system<Position, Box, Canopy>("RandomizeCanopy")
.kind(flecs::OnSet)
.each([](flecs::entity e, Position &p, Box &b, Canopy) {
float h = randf(1.0) + 0.8;
b.height = h;
p.y = h / 2.0 + 0.5;
});

That looks a lot better:

Note that we used an OnSet system. OnSet systems are invoked whenever one of the components in its query is set, for an entity that has all the components in the query. When the canopy is instantiated, the instance receives new values for bothPosition and Box components, which is why the OnSet system is triggered.Canopy is only added to the query to ensure that we don’t match everything with a Position and a Box.

This is what the final code for the tree looks like:

struct Canopy { };auto tree = ecs.prefab("PTree")
auto trunk = ecs.prefab()
.add_childof(tree)
.set<Position>({0, 0.25, 0})
.set<Box>({0.4, 0.5, 0.4})
.set<Color>({0.25, 0.2, 0.1});
auto canopy = ecs.prefab()
.add_childof(tree)
.add<Canopy>()
.set<Position>({0, 0.9, 0})
.set<Box>({0.8, 0.8, 0.8})
.set<Color>({0.2, 0.3, 0.15});
ecs.system<Position, Box, Canopy>("RandomizeCanopy")
.kind(flecs::OnSet)
.each([](flecs::entity e, Position &p, Box &b, Canopy) {
float h = randf(1.0) + 0.8;
b.height = h;
p.y = h / 2.0 + 0.5;
});

Before we wrap up, let’s take this one step further into slightly more advanced territory. Let’s make another prefab that has more of a pyramid-shaped canopy, like a pine tree. This will demonstrate how we can reuse prefabs within prefabs, as we will reuse the trunk and canopy entities.

First, lets separate out the trunk and canopy prefabs, and use them inside our existing tree prefab with an instance-of relationship:

struct Canopy { };auto trunk = ecs.prefab("PTrunk")
.set<Position>({0, 0.25, 0})
.set<Box>({0.4, 0.5, 0.4})
.set<Color>({0.25, 0.2, 0.1});
auto canopy = ecs.prefab("PCanopy")
.add<Canopy>()
.set<Color>({0.2, 0.3, 0.15});
auto tree = ecs.prefab("PTree")
auto tree_trunk = ecs.prefab()
.add_childof(tree)
.add_instanceof(trunk);
auto tree_canopy = ecs.prefab()
.add_childof(tree)
.add_instanceof(canopy)
.set<Position>({0, 0.9, 0})
.set<Box>({0.8, 0.8, 0.8});

Note how we moved all the canopy components that are specific to this tree to the tree_canopy prefab. Let’s now create the pine tree prefab. This prefab will have three instances of canopy which decrease in size as they get higher. Without further ado, this is what the pine tree prefab code looks like:

auto pine = ecs.prefab("PPine");
auto pine_trunk = ecs.prefab()
.add_childof(pine)
.add_instanceof(trunk);
auto pine_canopy_1 = ecs.prefab()
.add_childof(pine)
.add_instanceof(canopy)
.set<Position>({0, 0.6, 0})
.set<Box>({0.8, 0.4, 0.8});
auto pine_canopy_2 = ecs.prefab()
.add_childof(pine)
.add_instanceof(canopy)
.set<Position>({0, 1.0, 0})
.set<Box>({0.6, 0.4, 0.6});
auto pine_canopy_3 = ecs.prefab()
.add_childof(pine)
.add_instanceof(canopy)
.set<Position>({0, 1.4, 0})
.set<Box>({0.4, 0.4, 0.4});

Notice how we instantiate the canopy three times with decreasing size, which gives us a nice pyramid shape:

Let’s now mix back in the other trees:

Not bad, but it looks like our canopies are non longer getting randomized! Why did that happen? To understand the reason for this we need a bit of background. When a prefab with children is instantiated, the instantiated instance-children are not instances of the prefab-children. Instead they have the exact same set of components as the prefab children. Sounds confusing right? Here is a diagram to clarify what happens:

Notice how the top-level instance has INSTANCEOF PTree, whereas the children just have the components, and no INSTANCEOF relationship. This means that our instantiated canopy children had the Canopy tag, which we used to match our system. Now look however what happened when we moved the canopy prefab outside of the tree prefab (omitting trunk for brevity):

Because the prefab child became an instance of PCanopy, our instantiated child also became an instance of PCanopy. As a result, it no longer directly has the Canopy tag. By default, systems only match “owned” components, that is, components that are added to the entity directly. To let the system know it should also match “shared” components, we add the ANY modifier:

ecs.system<Position, Box>("RandomizeCanopy", "ANY:Canopy")
.kind(flecs::OnSet)
.each([](flecs::entity e, Position &p, Box &b) {
float h = randf(1.0) + 0.8;
b.height = h;
p.y = h / 2.0 + 0.5;
});

That worked, except that we are now also resizing the canopy boxes of the pine trees! To prevent this, we have to add an additional filter that makes sure the system is only applied to children of a PTree, not a PPine. To accomplish this, we extend the query with PARENT: INSTANCEOF | PTree. This ensures the query only matches entities that have a parent which is an instance of PTree.

With this change, our scene renders as expected!

The final code for both the tree and the pine tree prefabs is:

// Canopy tag
struct Canopy { };
// Trunk and canopy prefabs
auto trunk = ecs.prefab("PTrunk")
.set<Position>({0, 0.25, 0})
.set<Box>({0.4, 0.5, 0.4})
.set<Color>({0.25, 0.2, 0.1});
auto canopy = ecs.prefab("PCanopy")
.add<Canopy>()
.set<Color>({0.2, 0.3, 0.15});
// Tree prefab
auto tree = ecs.prefab("PTree")
auto tree_trunk = ecs.prefab()
.add_childof(tree)
.add_instanceof(trunk);
auto tree_canopy = ecs.prefab()
.add_childof(tree)
.add_instanceof(canopy)
.set<Position>({0, 0.9, 0})
.set<Box>({0.8, 0.8, 0.8});
// Pine tree prefab
auto pine = ecs.prefab("PPine");
auto pine_trunk = ecs.prefab()
.add_childof(pine)
.add_instanceof(trunk);
auto pine_canopy_1 = ecs.prefab()
.add_childof(pine)
.add_instanceof(canopy)
.set<Position>({0, 0.6, 0})
.set<Box>({0.8, 0.4, 0.8});
auto pine_canopy_2 = ecs.prefab()
.add_childof(pine)
.add_instanceof(canopy)
.set<Position>({0, 1.0, 0})
.set<Box>({0.6, 0.4, 0.6});
auto pine_canopy_3 = ecs.prefab()
.add_childof(pine)
.add_instanceof(canopy)
.set<Position>({0, 1.4, 0})
.set<Box>({0.4, 0.4, 0.4});
// Randomize canopy system
ecs.system<Position, Box>("RandomizeCanopy", "ANY:Canopy,"
"PARENT:INSTANCEOF | PTree")
.kind(flecs::OnSet)
.each([](flecs::entity e, Position &p, Box &b) {
float h = randf(1.0) + 0.8;
b.height = h;
p.y = h / 2.0 + 0.5;
});

Because our prefabs are regular, fully capable entities, we can modify prefabs at runtime, and our entities will update accordingly. Imagine for example that our game has seasons, we could update the color of all our canopies with a single line of code:

canopy.set<Color>({0.35, 0.25, 0.0});

This turns all trees into a nice autumn-y orange:

The reason this works is because the Color component is shared across all canopy entities, which means that it is stored once in memory. We therefore can update the component for all entities by just updating that single value.

There is one big issue with this though: pine trees do not turn orange in the fall. To fix this, we’ll create a PPineCanopy prefab that specializes our original PCanopy prefab by overriding the Color component and setting it to green:

auto pine_canopy = ecs.prefab("PPineCanopy")
.add_instanceof(canopy)
.set<Color>({0.35, 0.25, 0.0});

Which looks much better:

I hope this provided a good overview of what can be achieved with Flecs prefabs! There is more to uncover, as there are other flecs features that work well with the features we’ve used today. In a next blog we will look at more advanced use cases for prefabs, like how to create a functioning turret.

The full code for the trees example can be found here: https://github.com/SanderMertens/trees

If you like what you see, leave a star on the Flecs repository: https://github.com/SanderMertens/flecs

Last but not least, if you have questions or suggestions, feel free to join the Discord: https://discord.gg/MRSAZqb

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store