ODE_ECS is a simple, fast, and type-safe ECS written in Odin.
- Simple and type-safe API.
- Everything is preallocated (no hidden memory reallocations during a game loop).
- All important operations are O(1), with no hidden linked lists.
- Supports custom allocators.
- No additional data is stored with components, ensuring maximum cache efficiency.
- Iteration over components or views is as fast as possible (no iteration over empty or deleted components; data is 100% dense for optimal caching).
- Entity IDs are not just indices; they also include a generation number. This ensures that if you save an entity ID and the entity is destroyed, any new entity created with the same index will have a different generation, letting you know it is not the same entity.
- Supports an unlimited number of component types (default is 128).
- MIT License.
- Basic sample is available here.
- Tests are here.
- An example with 100,000 entities is available here.
- An example demonstrating how to optimize your ECS can be found here.
Use git clone to clone this repository into your project folder, and then import ecs "ode_ecs":
git clone https://github.com/odin-engine/ode_ecs.git
An Entity is simply an ID. All data associated with an entity is stored in its components.
A Component represents your data and can be defined using a struct or other types in Odin. An entity can have many components.
An ECS Database is a database similar to a relational database instance, but for entities and components. Other ECS libraries refer to this concept as Worlds or Scenes. However, I believe Database is a better term because a single game world can use multiple ECS databases, and a single game scene can also use multiple ECS databases.
When initializing a Database, you can specify the maximum entities_cap as well as the allocator:
import ecs "ode_ecs"
my_ecs: ecs.Database
// in some procedure:
ecs.init(&my_ecs, entities_cap=100, allocator=my_allocator)Every other object (tables, views) linked to my_ecs will now use my_allocator to allocate memory.
NOTE: ODE_ECS never reallocates memory automatically. The reason for this is the same as avoiding garbage collectors — to prevent unexpected performance drops caused by unexpected memory allocations, deallocations, or memory copying. Usually, you know the maximum number of entities you want in your game, so you can preallocate that amount ahead of time.
You can have as many ECS databases in your game as you want:
ecs1: ecs.Database
ecs2: ecs.Database
ecs.init(&ecs1, entities_cap=100)
ecs.init(&ecs2, entities_cap=200)The other two main types of objects in ODE_ECS are tables and views.
A component Table is a dense array of components of the same type. I named it "table" because it is very similar to the concept of a table in relational databases. Each different type of component requires a separate table. For example, you might have a positions table for Position components and an ais table for AI components.
If you have a Position component, you can create a table like this:
Position :: struct { x, y: int } // component
positions : ecs.Table(Position) // component table
ecs.table_init(&positions, ecs=&my_ecs, cap=100)To create an entity, you can do this:
robot, _ = ecs.create_entity(&my_ecs)Now you can add a Position component to the robot entity:
// Assign one component from the table to the entity
position, _ = ecs.add_component(&positions, robot)
// Get the existing component from the table for the entity
position = ecs.get_component(&positions, robot)To iterate over components in a table, you can do this:
for &pos in positions.rows {
fmt.println(pos)
}Or this:
for i := 0; i < ecs.table_len(positions); i += 1 {
pos := &positions.rows[i]
fmt.println(pos^)
}NOTE: Iterating over components in a
Tableis as fast as possible because it is just iterating over a slice/array. There are no "empty" or "deleted" components inpositions.rows.
You can get the entity_id by index during iteration over components:
eid: ecs.entity_id
for &pos, index in positions.rows {
eid = ecs.get_entity(&positions, index)
fmt.println(eid, pos)
}Using an entity, you can access its other components:
eid: ecs.entity_id
ai: ^AI // AI component
for &pos, index in positions.rows {
eid = ecs.get_entity(&positions, index)
ai = ecs.get_component(&ais, eid) // assuming we have variable `ais: Table(AI)`
fmt.println(eid, pos, ai)
}A View is used when you want to iterate over entities that have specific components. To initialize a view for entities with both Position and AI components, you can do this:
ecs1: ecs.Database
positions: Table(Position)
ais: Table(AI)
view1: ecs.View
// ... skipping initialization of other objects
// This view will reference all entities that have Position and AI components
ecs.view_init(&view1, &ecs1, {&positions, &ais})At this point, the view might be empty because it tracks entities as they are created/destroyed or as components are added/removed. If you create the view before creating entities or adding/removing components, it will stay up to date. If you initialize your view at a later stage, you can use the rebuild() procedure to update it:
ecs.rebuild(&view1) // This operation might be relatively costly as it iterates over componentsTo iterate over views, you need to use an Iterator:
it: ecs.Iterator
ecs.iterator_init(&it, &view1)
for ecs.iterator_next(&it) {
// ...
}To get an entity or its components inside the iterator loop, you can do this:
eid: ecs.entity_id
pos: ^Position // Position component
ai: ^AI // AI component
for ecs.iterator_next(&it) {
// To get the entity
eid = ecs.get_entity(&it)
// To get the Position component
pos = ecs.get_component(&positions, &it)
// To get the AI component
ai = ecs.get_component(&ais, &it)
// ...
}Tag_Table is a variation of Table, but it doesn’t contain any components. A Tag_Table only “tags” entities. You can create a Tag_Table like this:
is_alive : ecs.Tag_Table
ecs.tag_table__init(&is_alive, &db, 10)Then you can tag or untag entities like this:
human, _ = ecs.create_entity(&db)
ecs.tag(&is_alive, human) // add tag
ecs.untag(&is_alive, human) // remove tagTag_Table is especially useful with View:
view : ecs.View
// create a view for all entities that have AI, Position components, and the alive tag
ecs.view_init(&view, &db, {&ais, &positions, &is_alive_table})You can iterate over tagged entities like this:
// iterate over entities tagged in is_alive_table
fmt.println("Tagged entities:")
for eid in is_alive_table.rows {
fmt.println("Entity tagged in `is_alive_table`:", eid)
}Sample06 demonstrates how to use Tag_Table.
A View filter is a proc that you can pass to ecs.view_init to filter view data:
view: ecs.View
My_User_Data :: struct {
human_eid: ecs.entity_id,
chair_eid: ecs.entity_id,
}
// if this proc returns true, the entity (and its components) will be added to the view
my_filter :: proc(row: ^ecs.View_Row, user_data: rawptr = nil) -> bool {
if user_data == nil do return false
eid := ecs.get_entity(row)
data := (^My_User_Data)(user_data)
// using entities saved in user_data
if eid == data.human_eid || eid == data.chair_eid do return true
return false
}
my_user_data := My_User_Data{
human_eid = human,
chair_eid = chair,
}
view.user_data = &my_user_data // set user_data!
err = ecs.view_init(&view, &db, {&is_alive_table}, my_filter)The my_filter proc determines whether an entity (and its components) will be added to the view.
Check Sample06 for an example of how to use a View filter.
To run samples, navigate to the appropriate folder (samples/basic or samples/sample01) and execute:
odin run .
To run tests, go to the tests folder and execute:
odin test .
In ODE_ECS, an entity is simply an ID. In the ecs.odin file, an entity is defined as follows:
entity_id :: oc.ix_genThe ix_gen is defined like this:
ix_gen :: bit_field i64 {
ix: int | 56, // index
gen: uint | 8, // generation
}This approach is very useful because it ensures that if you save an entity ID somewhere and the entity is destroyed, any new entity created with the same index will have a different generation, letting you know it is not the same entity.
By default, the maximum number of component types is 128. However, you can have an unlimited number of component types. To increase the maximum number of component types, modify TABLES_MULT either in ecs.odin or by using the command-line define ECS_TABLES_MULT:
TABLES_MULT :: #config(ECS_TABLES_MULT, 1)A value of 2 will set the maximum number of component types to 256, 3 will increase it to 384, 4 to 512, and so on. However, lower values make ODE_ECS slightly faster and more memory-efficient, so increase it only if necessary.
