Files
ecs/ecs.odin

461 lines
14 KiB
Odin

package ecs
import "core:mem"
import "base:runtime"
import "core:container/queue"
import "core:fmt"
import "core:log"
import "core:time"
Entity :: struct {
id: Entity_ID,
generation: Entity_Generation,
}
Entity_ID :: distinct u32
Entity_Generation :: distinct u32
INVALID_ENTITY :: Entity { id = 0, generation = 0 }
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]
return entity.generation < current_gen
}
Component_Storage :: struct {
data: rawptr,
elem_size: u32,
alive: []bool,
}
Component_Registry :: map[typeid]Component_Storage
@(private="file") get_addr_of_component_for_entity_id_from_storage :: proc(storage: ^Component_Storage, entity_id: Entity_ID) -> rawptr {
return rawptr(uintptr(storage.data) + uintptr((u32(entity_id) * storage.elem_size)))
}
// 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,
max_entities: u32,
component_registry: Component_Registry,
entity_id_queue: queue.Queue(Entity_ID),
entity_generations: []Entity_Generation,
tick: Tick,
systems: []System,
}
create_world :: proc(name: string, max_entities: u32, systems: []System) -> (^World, bool) {
world := new(World)
world.name = name
world.max_entities = max_entities
registry, err := make(Component_Registry, max_entities)
if err != .None {
free(world)
return nil, false
}
world.component_registry = registry
{ // 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..<max_entities {
queue.push_back(&world.entity_id_queue, Entity_ID(i))
}
}
{ // Set up the world's array of entity-generations.
world.entity_generations = make([]Entity_Generation, max_entities)
}
{ // Systems.
// Add the systems to the world and set a reference to this world.
world.systems = systems
for &s in &world.systems {
s.world = world
}
// Then initialize them.
init_systems(world)
}
return world, true
}
register_component :: proc(world: ^World, $A: typeid) -> bool {
log.infof("Registering component '%v' (size: %v) in world '%v'.", typeid_of(A), size_of(A), world.name)
if A in world.component_registry {
log.warnf("Component '%v' already registered.", typeid_of(A))
return false
}
base := make([]A, world.max_entities)
world.component_registry[A] = Component_Storage {
data = &base[0],
elem_size = size_of(A),
alive = make([]bool, world.max_entities)
}
return true
}
destroy_world :: proc(world: ^World) {
assert(world != nil)
for c, e in &world.component_registry {
// delete(e.data)
free(e.data)
delete(e.alive)
delete_key(&world.component_registry, c)
}
clear(&world.component_registry)
delete(world.component_registry)
queue.destroy(&world.entity_id_queue)
delete(world.entity_generations)
{ // Systems.
// Dispose the world's systems.
dispose_systems(world)
world.systems = nil
}
free(world)
}
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 {
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.
gen := &world.entity_generations[id]
gen^ += 1;
return Entity {
id = id,
generation = gen^,
}, true
}
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
base_addr := get_addr_of_component_for_entity_id_from_storage(&v, entity.id)
mem.set(base_addr, 0, int(v.elem_size))
}
}
entity^ = INVALID_ENTITY
return true
}
add_component :: proc(entity: Entity, world: ^World, component: $T, loc := #caller_location) -> (^T, bool) { // MAYBE: SS - Remove 'world' here? Entity could cache the world they're a part of.. but that would make entities bigger.
if !entity_valid(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. 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. Location: %v", typeid_of(T), entity, loc)
return nil, false
}
component_storage := &world.component_registry[T]
if component_storage == nil {
log.warnf("Failed to add component %v - component does not exist in the world '%v's component-registry. Location: %v", typeid_of(T), world.name, loc)
return nil, false
}
comp := transmute(^T)(get_addr_of_component_for_entity_id_from_storage(component_storage, entity.id))
comp^ = component
component_storage.alive[entity.id] = true
return comp, true
}
has_component :: proc(t: typeid, entity: Entity, world: ^World, loc := #caller_location) -> bool {
if !entity_valid(entity) {
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 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 == nil {
log.warnf("Failed to get component %v - component does not exist in the world '%v's component-registry. Location: %v", t, world.name, 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
}
component_storage := &world.component_registry[T]
comp := transmute(^T)(get_addr_of_component_for_entity_id_from_storage(component_storage, entity.id))
// fmt.printfln("Comp is: %#v (size of elem in storage: %v, size of comp: %v)", comp, component_storage.elem_size, size_of(comp))
// fmt.printfln("Address is: %v.", )
return comp, true
}
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 {
comp^ = {}
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 = state,
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) {
sw: time.Stopwatch
for &s in &world.systems {
assert(&s != nil)
assert(s.tick != nil)
time.stopwatch_reset(&sw)
time.stopwatch_start(&sw)
{
s.tick(world, s.state)
}
time.stopwatch_stop(&sw)
// TODO: SS - Reintroduce this at some point. Really nice for debugging! :)
// system_tick_duration := time.stopwatch_duration(sw)
// fmt.printfln("System '%v' took %v ms.", s.name, time.duration_milliseconds(system_tick_duration))
}
world.tick += 1
}
Query :: struct {
entities: [dynamic]Entity,
include, exclude: [dynamic]typeid,
}
Query_Types :: []typeid
create_query :: proc(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 {
assert(world != nil)
component_storage := &world.component_registry[type]
if component_storage == nil {
fmt.eprintfln("No component storage for type '%v' in world '%v'.", type, world.name)
assert(false)
}
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
}
}
if component_with_fewest_entities == nil {
return {}
}
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[:]
}