Skip to content

odin-engine/ode_ecs

Repository files navigation

alt text

ODE_ECS (Entity-Component-System)

ODE_ECS is a simple, fast, and type-safe ECS written in Odin.

Features

  • 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.

How to install

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

Basics

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.


Table

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 Table is as fast as possible because it is just iterating over a slice/array. There are no "empty" or "deleted" components in positions.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)
    }

View

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 components

To 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

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 tag

Tag_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.


View Filter

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.


How to Run Samples and Tests

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 .  

Advanced

Entity

In ODE_ECS, an entity is simply an ID. In the ecs.odin file, an entity is defined as follows:

    entity_id ::        oc.ix_gen

The 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.


Maximum Number of Component Types

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.

Documentation

If you have any questions about ODE_ECS or encounter any issues, please open an issue ticket, and I’ll try to answer, fix, or add new functionality.

About

Simple, fast, and type-safe ECS written in Odin.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •  

Languages