Skip to content

Entities and components

Most Orblit games will never need this page. You can write a game whose state is ordinary Dart objects and whose scene is built out of them, and it’ll be perfectly happy at thousands of entities.

The core is there for the games where it won’t be.

The store groups entities by the exact set of components they have. Everything with {Transform, Renderable} sits in one block, and everything with {Transform, Renderable, Velocity} sits in another.

So a system asking for every Transform gets contiguous runs of them, rather than chasing a pointer per entity. That’s the difference between a cache miss per entity and a cache miss per cache line, which at a hundred thousand entities adds up to most of the frame.

The cost is that adding or removing a component moves the entity into the block for its new archetype. Doing that per entity per frame is the surest way to make an archetype store slower than a plain array of objects. Anything that comes and goes every frame wants to be a flag on a component, rather than a component in its own right.

Dart reaches the store over a C ABI. A query hands back typed data backed by the store’s own memory, so reading it is a read and writing to it writes to the store. There’s no marshalling step in either direction.

One rule comes with that: a view must not outlive the system that asked for it. Structural changes, meaning creating entities, destroying them or adding components, can move an archetype’s storage, and a view held across one of those is a view of memory that has since moved. If you need a value afterwards, copy it out.

Components are annotated Dart classes. orblit_codegen turns them into registration for the core, and into a manifest.

import 'package:orblit_codegen/orblit_codegen.dart';
@OrblitComponent()
class Velocity {
double x = 0;
double y = 0;
double z = 0;
}

The kind and the width are inferred from the fields: three doubles make a float32 component of arity 3, int gives int32, and bool gives uint8. A component whose fields don’t agree has to state its kind explicitly, rather than have one guessed for it.

The manifest is the part worth knowing about. It describes the components in a form other front ends can read without compiling the package that declared them, which is what lets a TypeScript script address the same components a Dart system does, instead of every front end keeping its own parallel set.

The core owns parenting. A child’s world transform is its parent’s composed with its own, and the composition is done in C++ over the whole hierarchy rather than per entity from Dart.