From 2ef1ffff43bde8e690750fbbaa8be9686c0c4800 Mon Sep 17 00:00:00 2001 From: samstalhandske Date: Fri, 21 Nov 2025 23:50:42 +0100 Subject: [PATCH] Querying, systems, wow! --- ecs.odin | 295 ++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 269 insertions(+), 26 deletions(-) diff --git a/ecs.odin b/ecs.odin index 67e5f70..227c24f 100644 --- a/ecs.odin +++ b/ecs.odin @@ -1,6 +1,7 @@ package ecs import "core:container/queue" +import "core:fmt" import "core:log" Entity :: struct { @@ -11,11 +12,10 @@ Entity :: struct { Entity_ID :: distinct u32 Entity_Generation :: distinct u32 -Invalid_Entity :: Entity { id = 0, generation = 0 } -@(private="file") Start_Entity_ID :: 1 +INVALID_ENTITY :: Entity { id = 0, generation = 0 } entity_valid :: proc(entity: Entity) -> bool { - return entity != Invalid_Entity + return entity != INVALID_ENTITY } entity_old :: proc(world: ^World, entity: Entity) -> bool { @@ -23,7 +23,7 @@ entity_old :: proc(world: ^World, entity: Entity) -> bool { return false; } - current_gen := world.entity_generations[entity.id - Start_Entity_ID] + current_gen := world.entity_generations[entity.id] return entity.generation < current_gen } @@ -33,15 +33,19 @@ Component_Storage :: struct { } Component_Registry :: map[typeid]Component_Storage +// IDEA: SS - Add a 'World_State' variable that contains the 'tick' variable and entities? Idk. All the things that are "dynamic". World :: struct { name: string, component_registry: Component_Registry, entity_id_queue: queue.Queue(Entity_ID), - entity_generations: []Entity_Generation + entity_generations: []Entity_Generation, + + tick: Tick, + systems: []System, } -create_world :: proc(name: string, max_entities: u32, components_to_register: []typeid) -> (^World, bool) { +create_world :: proc(name: string, max_entities: u32, components_to_register: []typeid, systems: []System) -> (^World, bool) { world := new(World) registry, err := make(Component_Registry, max_entities) @@ -70,7 +74,7 @@ create_world :: proc(name: string, max_entities: u32, components_to_register: [] assert(u64(max_entities) <= u64(max(int))) queue.init(&world.entity_id_queue, capacity = int(max_entities)) for i in 0.. (Entity, bool) { +create_entity :: proc(world: ^World, loc := #caller_location) -> (Entity, bool) { // Try to get a free entity. id, ok := queue.pop_front_safe(&world.entity_id_queue) if !ok { - return Invalid_Entity, false + log.warnf("Failed to create entity - no id to pop from the world's (%v) queue of IDs. Location: %v", world.name, loc) + return INVALID_ENTITY, false } // Grow a generation. @@ -118,59 +140,280 @@ create_entity :: proc(world: ^World) -> (Entity, bool) { }, true } -add_component :: proc(entity: Entity, world: ^World, component: $T) -> (^T, bool) { +destroy_entity :: proc(entity: ^Entity, world: ^World, loc := #caller_location) -> bool { + assert(entity != nil) + + if !entity_valid(entity^) { + log.warnf("Failed to destroy entity %v - entity is invalid. Location: %v", entity, loc) + return false + } + + // Return the entity's ID to the world's queue. + queue.push_back(&world.entity_id_queue, entity.id) + + // Reset the entity's components. + for c, v in &world.component_registry { + if v.alive[entity.id] { + v.alive[entity.id] = false + v.data[entity.id] = {} + } + } + + entity^ = INVALID_ENTITY + + return true +} + +add_component :: proc(entity: Entity, world: ^World, component: $T, loc := #caller_location) -> (^T, bool) { if !entity_valid(entity) { - log.warnf("Failed to add component %v - entity is invalid.", typeid_of(T), entity) + log.warnf("Failed to add component %v - entity is invalid. Location: %v", typeid_of(T), entity, loc) 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) + log.warnf("Failed to add component %v - entity already has this component on it. Location: %v", typeid_of(T), entity, loc) return nil, false } if entity_old(world, entity) { - log.warnf("Failed to add component %v - entity %v is old.", typeid_of(T), entity) + log.warnf("Failed to add component %v - entity %v is old. Location: %v", typeid_of(T), entity, loc) return nil, false } component_storage := &world.component_registry[T] - comp := transmute(^T)(&component_storage.data[entity.id - Start_Entity_ID]) + if component_storage == nil { + log.warnf("Failed to add component %v - component does not exist in the registry. Location: %v", typeid_of(T), loc) + return nil, false + } + comp := transmute(^T)(&component_storage.data[entity.id]) comp^ = component - component_storage.alive[entity.id - Start_Entity_ID] = true + component_storage.alive[entity.id] = true return comp, true } -get_component :: proc($T: typeid, entity: Entity, world: ^World) -> (^T, bool) { +has_component :: proc(t: typeid, entity: Entity, world: ^World, loc := #caller_location) -> bool { if !entity_valid(entity) { - log.warnf("Failed to get component %v - entity is invalid.", typeid_of(T), entity) - return nil, false + log.warnf("Failed to check if entity has component %v - entity %v is invalid. Location: %v", t, entity, loc) + return false } if entity_old(world, entity) { - log.warnf("Failed to get component %v - entity %v is old.", typeid_of(T), entity) - return nil, false + log.warnf("Failed to check if entity has component %v - entity %v is old. Location: %v", t, entity, loc) + return false } - component_storage := &world.component_registry[T] - if !component_storage.alive[entity.id - Start_Entity_ID] { + component_storage := &world.component_registry[t] + if component_storage == nil { + log.warnf("Failed to get component %v - component does not exist in the registry. Location: %v", t, loc) + return false + } + + if !component_storage.alive[entity.id] { + return false + } + + return true +} + +get_component :: proc($T: typeid, entity: Entity, world: ^World, loc := #caller_location) -> (^T, bool) { + if !has_component(T, entity, world) { return nil, false } - comp := transmute(^T)(&component_storage.data[entity.id - Start_Entity_ID]) + component_storage := &world.component_registry[T] + comp := transmute(^T)(&component_storage.data[entity.id]) return comp, true } -remove_component :: proc($T: typeid, entity: Entity, world: ^World) -> (bool) { +remove_component :: proc($T: typeid, entity: Entity, world: ^World, loc := #caller_location) -> (bool) { + component_storage := &world.component_registry[T] + if component_storage == nil { + log.warnf("Failed to remove component %v from entity %v - component does not exist in the registry. Location: %v", typeid_of(T), entity, loc) + return false + } + 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 + component_storage.alive[offset_entity_id(entity.id)] = false return true } return false +} + +Tick :: distinct u64 + +System :: struct { + name: string, + state: rawptr, + world: ^World, + + init, dispose: proc(world: ^World, state: rawptr) -> bool, + tick: proc(world: ^World, state: rawptr), +} + +create_system :: proc( + name: string, + state: $T, + init, dispose: proc(world: ^World, state: rawptr) -> bool, + tick: proc(world: ^World, state: rawptr), + allocator := context.allocator, +) -> System +{ + return System { + name = name, + state = new_clone(state, allocator), + init = init, dispose = dispose, + tick = tick, + } +} + +@(private="file") init_systems :: proc(world: ^World) { + for &s in &world.systems { + assert(&s != nil) + + if s.init != nil { + s.init(world, s.state) + } + } +} + +@(private="file") dispose_systems :: proc(world: ^World) { + #reverse for &s in &world.systems { + assert(&s != nil) + + if s.dispose != nil { + s.dispose(world, s.state) + } + + free(s.state) + s.state = nil + } + + world.systems = nil +} + +tick :: proc(world: ^World) { + for &s in &world.systems { + assert(&s != nil) + assert(s.tick != nil) + s.tick(world, s.state) + } + + world.tick += 1 +} + +Query :: struct { + entities: [dynamic]Entity, + include, exclude: [dynamic]typeid, +} + +Query_Types :: []typeid + +create_query :: proc(world: ^World, include: []typeid, exclude: []typeid = {}) -> Query { + query: Query + + // TODO: SS - Verify that the same component/typeid doesn't exist in both the include AND the exclude list. + + query.entities = make([dynamic]Entity, 0, 1024) // NOTE: SS - Start capacity is hardcoded. + query.include = make([dynamic]typeid, 0, len(include)) + query.exclude = make([dynamic]typeid, 0, len(exclude)) + + append_elems(&query.include, ..include) + append_elems(&query.exclude, ..exclude) + + return query +} + +destroy_query :: proc(query: ^Query) { + delete(query.entities) + + delete(query.include) + delete(query.exclude) +} + +entities_alive_with_component :: proc(type: typeid, world: ^World) -> u32 { + component_storage := &world.component_registry[type] + assert(component_storage != nil) + + // Check how many entities have this component. + amount := u32(0) + for alive, i in component_storage.alive { + if alive { + amount += 1 + } + } + + return amount +} + +query_entities :: proc(world: ^World, query: ^Query) -> []Entity { + assert(query != nil) + clear(&query.entities) + + // Begin with the types-to-include. + component_with_fewest_entities: typeid = nil + amount_of_entities_with_component := max(u32) + + for t in query.include { + amount := entities_alive_with_component(t, world) + + if amount == 0 { + continue + } + + // If this component has less entities than 'amount_of_entities_with_component', we assign it as the 'component_with_fewest_entities'. + if amount < amount_of_entities_with_component { + amount_of_entities_with_component = amount + component_with_fewest_entities = t + } + } + + assert(component_with_fewest_entities != nil) + assert(amount_of_entities_with_component > 0) + + // fmt.printfln("component_with_fewest_entities: %v (%v)", component_with_fewest_entities, amount_of_entities_with_component) + + // Add all entities with the 'component_with_fewest_entities' component. + component_storage := &world.component_registry[component_with_fewest_entities] + for alive, i in component_storage.alive { + if alive { + id := Entity_ID(i) + generation := world.entity_generations[id] + + append(&query.entities, Entity { + id = id, + generation = generation, + }) + } + } + + // Remove entities that don't have the other 'include'-components. + #reverse for entity, i in query.entities { + for t in query.include { + if t == component_with_fewest_entities { + continue + } + + if !has_component(t, entity, world) { + unordered_remove(&query.entities, i) + } + } + } + + // Remove entities that have any of the 'exclude'-components. + #reverse for entity, i in query.entities { + for t in query.exclude { + assert(t != component_with_fewest_entities) // We should not be here. A component that is in the 'include' part of the query can't be in the 'exclude' part of the query too. + + if has_component(t, entity, world) { + unordered_remove(&query.entities, i) + } + } + } + + return query.entities[:] } \ No newline at end of file