commit dc2a692c7df6b2ebd6f2bb316c42d6c7841411d0 Author: samstalhandske Date: Fri Nov 21 03:41:29 2025 +0100 Initial commit. diff --git a/ecs.odin b/ecs.odin new file mode 100644 index 0000000..67e5f70 --- /dev/null +++ b/ecs.odin @@ -0,0 +1,176 @@ +package ecs + +import "core:container/queue" +import "core:log" + +Entity :: struct { + id: Entity_ID, + generation: Entity_Generation, +} + +Entity_ID :: distinct u32 +Entity_Generation :: distinct u32 + +Invalid_Entity :: Entity { id = 0, generation = 0 } +@(private="file") Start_Entity_ID :: 1 + +entity_valid :: proc(entity: Entity) -> bool { + return entity != Invalid_Entity +} + +entity_old :: proc(world: ^World, entity: Entity) -> bool { + if !entity_valid(entity) { + return false; + } + + current_gen := world.entity_generations[entity.id - Start_Entity_ID] + return entity.generation < current_gen +} + +Component_Storage :: struct { + data: []typeid, + alive: []bool, +} +Component_Registry :: map[typeid]Component_Storage + +World :: struct { + name: string, + component_registry: Component_Registry, + + entity_id_queue: queue.Queue(Entity_ID), + entity_generations: []Entity_Generation +} + +create_world :: proc(name: string, max_entities: u32, components_to_register: []typeid) -> (^World, bool) { + world := new(World) + + registry, err := make(Component_Registry, max_entities) + if err != .None { + free(world) + return nil, false + } + + world.component_registry = registry + + for c in components_to_register { + // log.infof("Registered component '%v'.", c) + + if c in world.component_registry { + // log.warnf("Component '%v' already registered.", c) + continue + } + + world.component_registry[c] = Component_Storage { + data = make([]typeid, max_entities), + alive = make([]bool, max_entities) + } + } + + { // Set up the world's queue of available entity-ids. + assert(u64(max_entities) <= u64(max(int))) + queue.init(&world.entity_id_queue, capacity = int(max_entities)) + for i in 0.. (Entity, bool) { + // Try to get a free entity. + id, ok := queue.pop_front_safe(&world.entity_id_queue) + if !ok { + return Invalid_Entity, false + } + + // Grow a generation. + gen := &world.entity_generations[id] + gen^ += 1; + + return Entity { + id = id, + generation = gen^, + }, true +} + +add_component :: proc(entity: Entity, world: ^World, component: $T) -> (^T, bool) { + if !entity_valid(entity) { + log.warnf("Failed to add component %v - entity is invalid.", typeid_of(T), entity) + return nil, false + } + + _, has_component := get_component(T, entity, world) + if has_component { + log.warnf("Failed to add component %v - entity already has this component on it.", typeid_of(T), entity) + return nil, false + } + + if entity_old(world, entity) { + log.warnf("Failed to add component %v - entity %v is old.", typeid_of(T), entity) + return nil, false + } + + component_storage := &world.component_registry[T] + comp := transmute(^T)(&component_storage.data[entity.id - Start_Entity_ID]) + comp^ = component + + component_storage.alive[entity.id - Start_Entity_ID] = true + + return comp, true +} + +get_component :: proc($T: typeid, entity: Entity, world: ^World) -> (^T, bool) { + if !entity_valid(entity) { + log.warnf("Failed to get component %v - entity is invalid.", typeid_of(T), entity) + return nil, false + } + + if entity_old(world, entity) { + log.warnf("Failed to get component %v - entity %v is old.", typeid_of(T), entity) + return nil, false + } + + component_storage := &world.component_registry[T] + if !component_storage.alive[entity.id - Start_Entity_ID] { + return nil, false + } + + comp := transmute(^T)(&component_storage.data[entity.id - Start_Entity_ID]) + return comp, true +} + +remove_component :: proc($T: typeid, entity: Entity, world: ^World) -> (bool) { + if comp, ok := get_component(T, entity, world); ok { + component_storage := &world.component_registry[T] + comp^ = {} + component_storage.alive[entity.id - Start_Entity_ID] = false + return true + } + + return false +} \ No newline at end of file diff --git a/ecs_test.odin b/ecs_test.odin new file mode 100644 index 0000000..3dc8435 --- /dev/null +++ b/ecs_test.odin @@ -0,0 +1,82 @@ +package ecs + +import "core:container/queue" +import "core:testing" + +@(test) create_world_test :: proc(t: ^testing.T) { + A :: struct { v: [3]u32 } + B :: struct { v: [3]u32 } + C :: struct { v: [3]u32 } + + w, ok := create_world( + name = "World", + max_entities = 16, + components_to_register = { A, B, C, } + ) + assert(ok) + defer destroy_world(w) +} + +@(test) check_entity_id_queue_test :: proc(t: ^testing.T) { + A :: struct { v: [3]u32 } + B :: struct { v: [3]u32 } + C :: struct { v: [3]u32 } + + MAX_ENTITIES :: 16 + w, ok := create_world( + name = "World", + max_entities = MAX_ENTITIES, + components_to_register = { A, B, C, } + ) + assert(ok) + defer destroy_world(w) + + assert(queue.len(w.entity_id_queue) == MAX_ENTITIES) +} + +@(test) create_entities_test :: proc(t: ^testing.T) { + A :: struct { v: [3]u32 } + B :: struct { v: [3]u32 } + C :: struct { v: [3]u32 } + + w, ok := create_world( + name = "World", + max_entities = 1, + components_to_register = { A, B, C, } + ) + assert(ok) + defer destroy_world(w) + + // This entity should be OK. + e1, created_1 := create_entity(w) + assert(created_1) + assert(e1 != Invalid_Entity) + + // This entity should NOT be OK (because 'max_entities' is 1). + e2, created_2 := create_entity(w) + assert(!created_2) + assert(e2 == Invalid_Entity) +} + +@(test) add_component_test :: proc(t: ^testing.T) { + A :: struct { v: [3]u32 } + B :: struct { v: [3]u32 } + C :: struct { v: [3]u32 } + + w, ok := create_world( + name = "World", + max_entities = 1, + components_to_register = { A, B, C, } + ) + assert(ok) + defer destroy_world(w) + + e, created := create_entity(w) + assert(created) + assert(e != Invalid_Entity) + + added_component, component_added := add_component(e, w, A { v = { 1, 2, 3 } }) + assert(component_added) + assert(added_component != nil) + assert(added_component.v == { 1, 2, 3}) +} \ No newline at end of file