Skip to main content
Cardinal uses the Entity Component System (ECS) architecture to structure game code. ECS separates data from logic and encourages a data-driven design that scales well with complexity and performance demands. In ECS:
  • Entities are plain identifiers that represent “objects” in your game, e.g. players, projectiles, mobs, etc.
  • Components contain the data of the properties of your entities, for example: a projectile entity contains the position and velocity components.
  • Systems are the game logic that operates on your entities, for example: a physics system acts on all entities that have mass and position components, or a regeneration system acts on all entities that have a health component.

The World

Before getting into how to use ECS, we’ll briefly cover the World type. This is your game world. It holds all your entities, components, and systems together. Here’s what a typical main.go looks like:
main.go
Above, we also pass several options to configure the behavior of Cardinal:
  1. TickRate sets how many times per second the game loop runs. A tick represents a single state change in Cardinal.
  2. EpochFrequency sets how many ticks to include in an epoch. An epoch is a group of ticks that will be persisted to a blockchain.
There are other options, but these are all you need to run your world.

Components

Components are plain Go structs that implement the Component interface. This interface requires a single Name() method that returns a unique string identifier.
Component names must start with a letter or underscore, and contain only letters, digits, and underscores (e.g. Health, player_health).

Tag Components

Components don’t need to contain data. You can use empty structs as “tags” to mark entities:
This is useful for filtering entities without storing additional data, e.g. finding all player entities vs. NPC entities.

Systems

Systems are plain functions that take a single parameter and return an error. The parameter is a pointer to a user-defined struct type that embeds BaseSystemState. This struct defines a system’s dependencies and what it can access, e.g. components, commands, events, etc. (We’ll cover these in more detail soon.) This is the simplest possible system:
You must register systems with the world to run them:
Registered systems run once every tick, in registration order. Cardinal’s scheduler automatically runs systems without shared dependencies in parallel. A system’s dependencies include the components and system events it accesses.

System Hooks

You can control when a system executes during a tick by specifying a hook when you register the system, for example:
By default, systems run on the update phase. Here are the different hooks you can use: Each of these corresponds to a tick phase, except Init, which runs only once in the first tick.

Searches

To work with entities and their components in your systems, you need to define a search. A search lets you find and manipulate entities with specific components. Add a search field to your system state struct using one of these types:
  • Exact[T]: finds entities that have exactly the specified components, nothing more.
  • Contains[T]: finds entities that have at least the specified components, but may have others.
For example, say you have player and enemy entities that both have a Health component, but only enemies have an AIBehavior component. Using Contains with just Health would match both players and enemies. Using Exact with Health and AIBehavior would match only enemies. The type parameter T is a struct that lists the components you want to search for. Each field must use Ref[C], where C is a component type:
This defines a search called Mobs that matches all entities with at least Health and Position components. We’ll use it to demonstrate entity operations below.

Creating an Entity

Use Create to spawn a new entity with the components defined in your search. It returns the entity ID and a handle to access its components. All components are initialized to their zero values.
Create always creates an entity with exactly the components in your search, even if you’re using Contains. The difference between Contains and Exact only affects which entities are matched when iterating or querying.

Destroying an Entity

Use Destroy to remove an entity and all its components from the world. Returns true if the entity existed and was destroyed:

Iterating Over Entities

Use Iter to loop through all entities matching the search. It yields both the entity ID and a handle to access components:

Getting a Specific Entity

Use GetByID to retrieve a specific entity’s handle. Returns false if the entity doesn’t exist or doesn’t have the matching components in the search:

Reading and Writing Components

Use Get and Set on component references to read and write component data:

Adding a Component

Because systems must declare all components they access upfront, adding a component requires including it in your search definition. Define a Contains[T] search with both the entity’s existing components and the component you want to add, then use Set to add the new component:
Because you’re using a Contains search, it may match entities you don’t intend to modify. To avoid this, add a tag component so you can distinguish your target entities from the rest.

Removing a Component

Use Remove to detach a component from an entity: