Thank you. Perfect conditions do not exists, so my expectations confirmed. Seems it becomes faster…
Implemented without condition to systems query.
I thought that there should exists ability to select entities that does not have some components/tags yet to, for example, add them; or to select entities excluding some types of them.
You just list components/tags in without filed of system definition struct when mounting them.
Currently systems queries works this way:
You pass a list of component types and/or tag types when mounting a system and these set is a match query for selection of entities which will be passed to system callback. Entity must have all components and tags defined for the system to match its query condition (but it also may have more, it hasn’t to be exact match). If you need to exclude entities without some components/tags from the query result (entities mustn’t have them added), you can use without condition when mount the system. If system has no specified components/tags and without conditions it is considered as a task, no queries are executed for them at each progress step, and nil is passed as first argument of callback procedure (instead of matched entities array).
import ecs "moecs/src"
import k2 "karl2d"
main :: proc() {
ecs.init()
/* You can pass approach here, default is .ARCHETYPE, recommended. */
world := ecs.new_world(.ARCHETYPE)
/* Mount system that will run only once after world starts. */
ecs.mount(world, { callback = load_world, phase = .START })
/* Mount systems which will run in .UPDATE phase (default). */
ecs.mount(world, { callback = actions, components = { Handle, Actions, Weapon, Ship }, tags = { Player } })
ecs.mount(world, { callback = physics, name = "physics" })
/* You can use query or/and components and tags fields to define system query (list of components and tags). */
ecs.mount(world, { callback = draw, query = { Position, Rotation, Sprite, Center, Size } })
ecs.mount(world, { callback = collisions, components = { Collision, Handle, Position, Center } })
/* Use without condition to exclude listed components/tags from system query result. */
ecs.mount(world, { callback = materialize, query = { Position, Rotation }, without = { Handle, Player } })
/* Mount systems to run them manually (phase = .MANUAL). */
ecs.mount(world, { callback = load_resources, name = "load-resources", phase = .MANUAL })
ecs.mount(world, { callback = destroy, name = "destroy", phase = .MANUAL })
/* We must mount systems before the world run. */
ecs.run(world)
/* Execule system by its name. */
ecs.execute(world, "load-resources")
for k2.update() {
k2.clear(k2.BLACK)
/* World progress step. Systems run in mounting order for each phase
in phases order: PRE_UPDATE, UPDATE, POST_UPDATE. */
ecs.progress(world)
k2.present()
/* Turn off/on physics processing. */
if condition() do ecs.disable(world, "physics")
else do ecs.enable(world, "physics")
}
/* You can unmount the system this way even if the world is already running.
Maybe you know that do not need it any more, but disabling is recommended. */
if ecs.has(world, "physics") do ecs.unmount(world, "physics")
/* Manually free all game resources in this system. */
ecs.execute(world, "destroy")
ecs.destroy()
k2.shutdown()
p.s.: I would like to edit first post and change links to only one GitHub - helioscout/moecs: Easy to use Entity Component System (ECS) crafted with Odin. · GitHub
-- spawning dynamics ( 100003 )
-- ellapsed: 14.9194ms
-- spawning dynamics ( 500003 )
-- ellapsed: 345.7638ms
-- spawning statics ( 300003 )
-- ellapsed: 197.4697ms
--- iterating ---
-- ellapsed: 3.9613ms, count: 1000017
--- progress 100 times ---
-- startup system...
-- ellapsed: 30.7651 ms
-- ellapsed: 30.7651ms
I know here living other ecs crafters, maybe you are interested in sharing ideas and experience to each other.
Created discussion at github.
Have you tried Odin Discord or Odin reddit? You might get more interaction there. I cannot say for sure since I do not use social media at all. Personally, I stick to only forums.
This made me remember some old code I had when I was experimenting with ECS. Look at example() proc for some usage code.
package ecs
import ix "base:intrinsics"
import rt "base:runtime"
import "core:fmt"
import "core:log"
import "core:mem"
pn :: fmt.println
pf :: fmt.printf
pfn :: fmt.printfln
n16 :: max(u16)
log2 :: ix.count_trailing_zeros
check :: proc(cond: bool, err_str: string, args: ..any, loc := #caller_location) -> bool {
if cond do return true
log.errorf(err_str, ..args, location = loc)
return false
}
error :: #force_inline proc(str: string, args: ..any, loc := #caller_location) -> bool {
log.errorf(str, ..args, location = loc)
return false
}
_type :: #force_inline proc(x: $T) -> typeid {
return typeid_of(type_of(x))
}
is_set :: #force_inline proc(bitarray: [$N]u64, index: u16) -> bool {
return ((bitarray[index >> 6] >> (index & 63)) & 1) == 1
}
set_bit :: #force_inline proc(bitarray: ^[$N]u64, index: u16) {
bitarray[index >> 6] |= 1 << (index & 63)
}
unset_bit :: #force_inline proc(bitarray: ^[$N]u64, index: u16) {
bitarray[index >> 6] &~= 1 << (index & 63)
}
Item :: struct($T: typeid) #raw_union {
item: T,
next_free: u16,
}
Raw_Array :: struct($M, $N: u16) where M > 0 && N >= 64 && N < n16 {
items: [N]Item([M]u8),
used: [N >> 6]u64,
count: u16,
tail_free: u16,
}
raw_array_add :: proc(a: ^Raw_Array($M, $N), item: [M]u8) -> (id: u16, ok: bool) {
switch {
case a.count == 0:
a.tail_free = n16
id = 0
case a.tail_free != n16:
id = a.tail_free
a.tail_free = a.items[id].next_free
case:
check(a.count < N, "Array is full") or_return
id = a.count
}
a.items[id].item = item
a.count += 1
set_bit(&a.used, id)
return id, true
}
raw_array_remove :: proc(a: ^Raw_Array($M, $N), id: u16) -> bool {
check(id < a.count, "Id %d out of bounds (count = %d).", id, a.count) or_return
a.items[id].next_free = a.tail_free
a.tail_free = id
a.count -= 1
unset_bit(&a.used, id)
return true
}
raw_array_get_ptr :: #force_inline proc(
a: ^Raw_Array($M, $N),
id: u16,
) -> (
ptr: ^[M]u8,
ok: bool,
) #optional_ok {
if id >= a.count {
log.errorf("Id %d out of bounds (count = %d).", id, a.count)
return nil, false
}
if !is_set(a.used, id) {
log.errorf("Item not found at id %v.", id)
return nil, false
}
return &a.items[id].item, true
}
Array :: struct($T: typeid, $N: u16) where size_of(T) > 0 && N > 0 {
ra: Raw_Array(size_of(T), N),
}
array_count :: #force_inline proc(a: Array($T, $N)) -> u16 {
return a.ra.count
}
array_add :: #force_inline proc(a: ^Array($T, $N), item: T) -> (id: u16, ok: bool) {
item := item
return raw_array_add(&a.ra, (^[size_of(T)]u8)(&item)^)
}
array_remove :: #force_inline proc(a: ^Array($T, $N), id: u16) -> bool {
return raw_array_remove(&a.ra, id)
}
array_item_exists :: #force_inline proc(a: Array($T, $N), id: u16) -> bool {
return is_set(a.ra.used, id)
}
array_get_ptr :: #force_inline proc(
a: ^Array($T, $N),
id: u16,
) -> (
ptr: ^T,
ok: bool,
) #optional_ok {
_ptr := raw_array_get_ptr(&a.ra, id) or_return
return (^T)(_ptr), true
}
LOG_MIN_ALLOC :: 4
MIN_ALLOC :: 1 << LOG_MIN_ALLOC
B :: ^Block
Block :: struct {
data: [64 * MIN_ALLOC]u8,
used: u64,
}
log2_ceil :: #force_inline proc(x: u64) -> (exp: u64) {
lead := ix.count_leading_zeros(x)
return 64 - lead - u64((x << lead) == (1 << 63))
}
block_alloc :: proc(b: B, N: u64) -> (id: u64, ok: bool) {
(N >= MIN_ALLOC && N <= 64 * MIN_ALLOC) or_return
exp := log2_ceil(N)
size: u64 = 1 << (exp - LOG_MIN_ALLOC)
mask: u64 = (1 << size) - 1
if b == nil do return
for i := u64(0); i < 64; i += size {
if (b.used >> i) & mask > 0 {
continue
}
b.used |= mask << i
return i, true
}
return
}
block_free :: proc(b: B, id: u64, N: u64) -> bool {
(N >= MIN_ALLOC && N <= 64 * MIN_ALLOC) or_return
exp := log2_ceil(N)
size: u64 = 1 << (exp - 6)
mask: u64 = (1 << size) - 1
((b.used >> id) & mask == mask) or_return
b.used &~= mask << id
return true
}
Chunk :: struct {
blocks: [1024]Block,
}
chunk_alloc :: proc(c: ^Chunk, at: u16, N: u64) -> (id: u16, ok: bool) {
(N >= MIN_ALLOC && N <= 64 * MIN_ALLOC) or_return
M :: len(c.blocks)
for i in 0 ..< u64(M) {
offset := (i >> 1) if i & 1 == 0 else (M - 1) - (i >> 1)
try_id := (u64(at) + offset) & (M - 1)
if _id, _ok := block_alloc(&c.blocks[try_id], N); _ok {
return u16((try_id << 6) | _id), true
}
}
return
}
chunk_free :: proc(c: ^Chunk, at: u16, N: u64) -> bool {
return block_free(&c.blocks[at >> 6], u64(at & 63), N)
}
chunk_get_byte_ptr :: #force_inline proc(c: ^Chunk, id: u16) -> [^]u8 {
return &c.blocks[id >> 6].data[(id & 63) * MIN_ALLOC]
}
chunk_get_ptr :: #force_inline proc(c: ^Chunk, id: u16, $T: typeid) -> ^T {
return (^T)(chunk_get_byte_ptr(c, id))
}
chunk_get :: #force_inline proc(c: ^Chunk, id: u16, $T: typeid) -> T {
return chunk_get_ptr(c, id, T)^
}
chunk_set :: #force_inline proc(c: ^Chunk, id: u16, item: $T) {
chunk_get_ptr(c, id, T)^ = item
}
chunk_add :: proc(c: ^Chunk, at: u16, item: $T) -> (id: u16, ok: bool) {
id = chunk_alloc(c, at, size_of(T)) or_return
chunk_get_ptr(c, id, T)^ = item
return id, true
}
chunk_remove :: proc(c: ^Chunk, at: u16, t: typeid) -> bool {
return chunk_free(c, at, size_of(t))
}
chunk_used_mem :: proc(c: Chunk) -> (used_mb: f64) {
for b in c.blocks {
used_mb += f64(ix.count_ones(b.used) * 16) / (1 << 20)
}
return used_mb
}
_Entity :: struct {
occupied: u64,
components: [16]u16,
}
ENTITY_LOG_CAPACITY :: 14
#assert(ENTITY_LOG_CAPACITY >= 10)
Entity_Chunk :: struct($C: typeid) where ix.type_is_struct(C) {
entities: Array(_Entity, 1 << ENTITY_LOG_CAPACITY),
chunk: Chunk,
histogram: [64]i16,
// Iterator
index: int,
next_id: u16,
has: u64,
without: u64,
}
component_mask :: proc(
ec: ^Entity_Chunk($C),
components: []typeid,
) -> (
mask: u64,
ok: bool,
) #optional_ok {
for t in components {
offset := get_component_offset(C, t) or_return
mask |= 1 << offset
}
return mask, true
}
iter_reset :: proc(ec: ^Entity_Chunk($C)) {
ec.next_id = 0
ec.index = -1
ec.has = max(u64)
ec.without = 0
}
iter_has :: proc(ec: ^Entity_Chunk($C), components: ..typeid) -> bool {
ec.has = component_mask(ec, components) or_return
return true
}
iter_without :: proc(ec: ^Entity_Chunk($C), components: ..typeid) -> bool {
ec.without = component_mask(ec, components) or_return
return true
}
Components :: struct {
transform: ^Transform,
collision: ^Collision,
physics: ^Physics,
raycaster: ^Raycaster,
instance: ^Instance,
skinned: ^Skinned,
blendshapes: ^Blendshapes,
camera: ^Camera,
light: ^Light,
ai: ^AI,
inventory: ^Inventory,
equipment: ^Equipment,
}
get_struct_info :: proc(t: typeid) -> rt.Type_Info_Struct {
return type_info_of(t).variant.(rt.Type_Info_Named).base.variant.(rt.Type_Info_Struct)
}
get_elem_info :: proc(info: ^rt.Type_Info) -> ^rt.Type_Info {
return info.variant.(rt.Type_Info_Pointer).elem
}
check_components_struct :: proc($T: typeid) -> bool where ix.type_is_struct(T) {
info := get_struct_info(T)
for i in 0 ..< info.field_count {
elem := get_elem_info(info.types[i])
switch elem.size {
case MIN_ALLOC ..= 64 * MIN_ALLOC:
case:
log.errorf(
"Component '%v' has size=%v which out of range [%v, %v]",
elem.id,
elem.size,
MIN_ALLOC,
64 * MIN_ALLOC,
)
return false
}
for j in 0 ..< i {
elem_j := get_elem_info(info.types[j])
if elem.id == elem_j.id {
log.errorf("Duplicate component found: %v", elem.id)
return false
}
}
}
return true
}
get_component_offset :: proc(c, t: typeid) -> (uint, bool) {
info := get_struct_info(c)
for i in 0 ..< info.field_count {
elem := get_elem_info(info.types[i])
if elem.id == t {
return uint(i), true
}
}
log.errorf("'%v' is not a valid component.", t)
return uint(info.field_count), false
}
get_component_typeid :: proc(c: typeid, offset: uint) -> (t: typeid, ok: bool) {
info := get_struct_info(c)
if offset >= uint(info.field_count) {
log.errorf("Offset %v is out of bounds (Component count = %d).", offset, info.field_count)
return
}
elem := get_elem_info(info.types[offset])
return elem.id, true
}
component_count :: proc(ec: ^Entity_Chunk($C), t: typeid) -> (count: i16, ok: bool) #optional_ok {
offset := get_component_offset(C, t) or_return
return ec.histogram[offset], true
}
add_component :: proc(ec: ^Entity_Chunk($C), entity_id: u16, item: $T) -> bool {
(ec != nil) or_return
offset := get_component_offset(C, T) or_return
_e := array_get_ptr(&ec.entities, entity_id) or_return
if ix.count_ones(_e.occupied) >= len(_e.components) {
log.errorf(
"Entity %v is already at component capacity of %v, cannot add '%v'.",
entity_id,
len(_e.components),
typeid_of(T),
)
return false
}
if (_e.occupied >> offset) & 1 == 1 {
log.errorf(
"Entity %v already has component '%v', duplicates are not allowed.",
entity_id,
typeid_of(T),
)
return false
}
id := chunk_add(&ec.chunk, entity_id >> (ENTITY_LOG_CAPACITY - 10), item) or_return
index := ix.count_ones(_e.occupied & ((1 << offset) - 1))
_e.occupied |= 1 << offset
_e.components[index] = id
ec.histogram[offset] += 1
return true
}
remove_component :: proc(ec: ^Entity_Chunk($C), entity_id: u16, t: typeid) -> bool {
(ec != nil) or_return
offset := get_component_offset(C, t) or_return
_e := array_get_ptr(&ec.entities, entity_id) or_return
if (_e.occupied >> offset) & 1 == 0 {
log.errorf("Entity %v doesn't have component '%v', nothing to remove.", entity_id, t)
return false
}
for i in offset + 1 ..< len(_e.components) {
_e.components[i - 1] = _e.components[i]
}
_e.occupied &~= 1 << offset
ec.histogram[offset] -= 1
return true
}
get_components :: proc(ec: ^Entity_Chunk($C), id: u16) -> (c: C, ok: bool) #optional_ok {
(ec != nil) or_return
_e := array_get_ptr(&ec.entities, id) or_return
if _e.occupied == 0 do return {}, true
chunk := &ec.chunk
id_count := u64(ix.count_ones(_e.occupied))
// pfn("%b", _e.occupied)
ptr_count := u64(size_of(C) / size_of(rawptr))
(id_count <= ptr_count) or_return
p := ([^][^]u8)(&c)
index := 0
for i := u64(0);; i += 1 {
i += u64(ix.count_trailing_zeros(_e.occupied >> i))
(i < 64) or_break
id := _e.components[index]
index += 1
p[i] = chunk_get_byte_ptr(chunk, id)
}
return c, true
}
create_entity :: proc(ec: ^Entity_Chunk($C)) -> (id: u16, ok: bool) {
return array_add(&ec.entities, _Entity{})
}
remove_entity :: proc(ec: ^Entity_Chunk($C), id: u16) -> bool {
_e := array_get_ptr(&ec.entities, id) or_return
index := 0
for i := u64(0); i < 64; i += 1 {
if ((_e.occupied >> i) & 1) == 0 do continue
type_id := get_component_typeid(C, uint(i)) or_return
chunk_free(&ec.chunk, _e.components[index], size_of(type_id)) or_return
index += 1
}
return array_remove(&ec.entities, id)
}
has :: proc(ptrs: ..rawptr) -> bool {
for p in ptrs do if p == nil do return false
return true
}
without :: proc(ptrs: ..rawptr) -> bool {
for p in ptrs do if p != nil do return false
return true
}
iter_next :: proc(ec: ^Entity_Chunk($C)) -> (c: C, id: u16, ind: int, cond: bool) {
(ec != nil) or_return
for i in ec.next_id ..< array_count(ec.entities) {
array_item_exists(ec.entities, i) or_continue
_e := array_get_ptr(&ec.entities, i) or_return
has := (_e.occupied & ec.has) > 0
without := (_e.occupied & ec.without) == 0
(has && without) or_continue
ec.next_id = i + 1
ec.index += 1
c = get_components(ec, i) or_return
return c, i, ec.index, true
}
return
}
example :: proc() -> bool {
ec := new(Entity_Chunk(Components))
defer free(ec)
for i in 0 ..< 8 {
id := create_entity(ec) or_return
add_component(ec, id, Transform{})
if i & 1 == 0 {
add_component(ec, id, Collision{})
}
if i & 3 == 0 {
add_component(ec, id, Physics{})
}
}
iter_reset(ec)
iter_has(ec, Physics)
for c, id, i in iter_next(ec) {
if i & 7 == 0 {
remove_component(ec, id, Physics)
}
}
iter_reset(ec)
iter_has(ec, Transform)
iter_without(ec, Physics)
for c, id, i in iter_next(ec) {
if i & 1 == 0 {
add_component(ec, id, Instance{})
} else {
add_component(ec, id, Skinned{})
}
}
iter_reset(ec)
for c, id, i in iter_next(ec) {
pfn("%#v", c)
}
iter_reset(ec)
iter_has(ec, Transform, Skinned)
for c, id, i in iter_next(ec) {
if i & 1 == 0 {
add_component(ec, id, AI{})
}
}
iter_reset(ec)
for c, id, i in iter_next(ec) {
if has(c.ai, c.collision) && without(c.raycaster, c.instance) {
add_component(ec, id, Raycaster{})
}
if has(c.raycaster, c.ai, c.equipment) {
}
if has(c.camera, c.collision) {
remove_component(ec, id, Collision)
}
if has(c.light, c.camera, c.transform) {
}
if has(c.ai, c.skinned, c.instance, c.blendshapes) {
}
}
pn(chunk_used_mem(ec.chunk))
return true
}
// example :: proc() -> bool {
// ec := new(Entity_Chunk(Components))
// defer free(ec)
// e_id := create_entity(ec) or_return
// add_component(ec, e_id, Transform{}) or_return
// c := get_components(ec, e_id) or_return
// pn(c)
// remove_component(ec, e_id, Transform) or_return
// return true
// }
main :: proc() {
context.logger = log.create_console_logger()
pn(f64(size_of(Entity_Chunk)) / (1 << 20))
pn(check_components_struct(Components))
pn(example())
}
Collision_Shape :: enum {
Box,
Sphere,
Cylinder,
Capsule,
Cone,
Uneven_Capsule,
}
Bone_Constraint :: struct {}
Blendshape :: struct {
targets: [][3]f32,
interpolation: enum {
Linear,
Ease_In,
Ease_Out,
Smoothstep,
},
factor: f32,
}
// Components
Transform :: struct {
position: [3]f32,
rotation: quaternion128,
scale: [3]f32,
}
Collision :: struct {
shape: Collision_Shape,
data: [4]f32, // [3]f32 for Box, f32 for Sphere, etc.
bounds: [2]f32, // AABB used for broadphase and BVH
}
Physics :: struct {
impulse: [3]f32,
mass: f32,
physics_material: u32,
}
Raycaster :: struct {
queries: [][3]f32,
hit_entities: []u32,
hit_distances: []f32,
}
Instance :: struct {
base_mesh: u32,
base_material: u32,
mesh_instance: u32,
material_instance: u32,
}
Skinned :: struct {
base_rig: u32,
pose: []Transform,
constraints: []Bone_Constraint,
}
Blendshapes :: struct {
shapes: []Blendshape,
pre_transform: Transform,
post_transform: Transform,
}
Camera :: struct {
projection: matrix[4, 4]f32,
frustum: [6][4]f32,
look_at: [3]f32,
}
Light :: struct {
intensity: f32,
color: [3]f32,
shadow_map_resolution: [2]u32,
}
AI :: struct {
follow_target: u32,
look_at: [3]f32,
vision_cone_radius: f32,
vision_distance: f32,
cooldown_time: f32,
}
Inventory :: struct {
items: []u32,
capacity: u32,
weight: u32,
selected: u32,
}
Equipment :: struct {
swords: []u32,
shields: []u32,
buffs: []u32,
multiplier: f32,
}
Eventually I gave up and opted for a simpler architecture:
package split_entity
// Data shared by most entities
Entity :: struct {
flags: bit_set[enum {
Dynamic,
Shadow_caster,
Occluder,
Skeletal,
Physics,
Enemy,
Inventory,
}],
position: [3]f32,
rotation: quaternion128,
scale: [3]f32,
bounds: [2][3]f32,
collision: enum {
Box,
Sphere,
Cone,
Cylinder,
Capsule,
},
mesh: rawptr,
material: rawptr,
extended: ^Entity_Ex,
}
// Extended data for fewer, complex entities.
Entity_Ex :: struct {
// I got lazy, so use your imagination
physics, ai, rig, animation, combat, inventory: struct{},
// For game engine users to make custom entity types
custom_data: rawptr,
before_physics, before_ai, before_animation: proc(e: ^Entity),
}
entity_update :: proc(e: ^Entity) {
if .Dynamic in e.flags {
// do something
if .Occluder in e.flags {
// rasterize depth I guess
}
} else {
if .Shadow_caster in e.flags {
// if light moved then re-rasterize
}
}
if x := e.extended; x != nil {
// do extended update for complex types
if x.before_physics != nil {
x.before_physics(e)
}
if .Physics in e.flags {
// update physics
}
if x.before_ai != nil {
x.before_ai(e)
}
if .Enemy in e.flags {
if .Inventory in e.flags {
// update enemy inventory
}
}
if x.before_animation != nil {
x.before_animation(e)
}
if .Skeletal in e.flags {
// update animation
}
}
}
entity_custom_update_example :: proc(e: ^Entity) {
// update base data with custom logic if needed
e.collision = .Capsule
Custom :: struct {
type: enum {
Player,
Cyclops,
Wizard,
Researcher,
},
inventory, armor, weapon, eye_laser, wand, jetpack: struct{},
}
// update custom data or return if there is none
m := (^Custom)(e.extended.custom_data)
if m == nil do return
switch m.type {
case .Player:
case .Cyclops:
case .Wizard:
case .Researcher:
}
}
I have implemented Observers for moecs.
Observers are a mechanism that allows to subscribe on events of structural and data changes in the world. By default observers are disable for performance reasons, so you need to pass true for observable argument of new_world procedure when you create the world. You also can change observable property of the world to turn off/on observers globally.
There are different event types that can be handled for entities, components, and tags.
| Event | Description |
|---|---|
| SPAWNED | Entity has been spawned. |
| DESPAWNED | Entity has been despawned. |
| ADDED | Component has been added to an entity. |
| REMOVED | Component has been removed from an entity. |
| SET | Component value has been set (changed). |
| TAGGED | Tag has been added to an entity. |
| UNTAGGED | Tag has been removed from an entity. |
Keep in mind that when you add/remove a component repeatedly or set/unset a tag repeatedly, the events will also be fired repeatedly for each operation even if you already made it before. It is safe for the data to call add_component (for example) procedure several times and pass the same component type to it, but your observers logic can be broken, so you need care about it yourself.
You can turn on/off observers for a specific component/tag type or globally for an event type. You can also check whether an observer is set or turned on. Events are not supported for resources.
When you set an observer using observe you must provide callback procedure that should follow ObserverCallback procedure type. SPAWNED/DESPAWNED events are being thrown for all entities and there are nothing to pass for type and component parameters, so the will be nil for these events in callback. Pointer to event target entity will be passed as entity parameter to callback. For TAGGED/UNTAGGED events tag type will be passed as type parameter, but component will be equals to nil. And finally for ADDED/REMOVED/SET events all callback parameters will be set, and component parameter is a pointer to event target component value, you can safety change it’s value in place or read it, previously cast rawptr to expecting component type pointer.
When you provide several types in observe/unobserve procedures the same callback will be assigned as specific event handler for each of these types. This is done for convenience, there are no group observers, they are set separately for a specific event and element (component/tag) type.
You must set observers only when the world is already running, because of necessary indexes sorting made in run procedure of the world. Subsequent setting observers for some configuration will replace previous ones.
import ecs "moecs/src"
/* Observer callback procedure declaration. */
added :: proc(world: ^ecs.World, entity: ^ecs.Entity, event: ecs.Event, type: typeid, component: rawptr) {
switch type {
case Position:
pos := cast(^Position)component
/* Do not use observers for such purposes, it's just example. */
pos.x += 50
pos.y += 50
case Center:
center := cast(^Center)component
/* Component values will be safety changed in place. */
center.cx += 50
center.cy += 50
}
}
main :: proc() {
ecs.init()
/* Enable observers when create the world. */
world := ecs.new_world(observable = true)
/* ...register tags and components types here. */
ecs.run(world)
/* Set observers for entity spawning/despawning, you need to provide only callbacks. */
ecs.observe(world, event = .SPAWNED, callback = spawned)
ecs.observe(world, event = .DESPAWNED, callback = despawned)
/* You can set observers for one or several types, subsequent assignments replace previous ones. */
ecs.observe(world, event = .ADDED, types = { Rotation }, callback = added_rot)
ecs.observe(world, event = .ADDED, types = { Position, Center, Health, Velocity }, callback = added)
ecs.observe(world, event = .REMOVED, types = { Center, Position }, callback = removed)
ecs.observe(world, event = .SET, types = { Position }, callback = set_pos)
ecs.observe(world, event = .SET, types = { Center, Rotation, Health, Velocity }, callback = set)
ecs.observe(world, event = .TAGGED, types = { Ship, Asteroid }, callback = tagged)
ecs.observe(world, event = .UNTAGGED, types = { Ship, Asteroid }, callback = untagged)
/* Turn off all added events for all component types. */
ecs.turn_off(world, .ADDED)
/* Turn off added events for Velocity component type. */
ecs.turn_off(world, .ADDED, Velocity)
if ecs.observable(world, .SET, Position) {
/* Remove observer for set event of Position component type. */
ecs.unobserve(world, .SET, { Position })
}
/* Turn on all added events for all component types.
It is still turned off for Velocity component type. */
if !ecs.turned_on(world, .ADDED) do ecs.turn_on(world, .ADDED)
ecs.destroy()
}
| Procedure | Description |
|---|---|
| observe | Sets observer for specified event and type(s). |
| unobserve | Unsets observer for specified event and type(s). |
| observable | Checks if observer is set for specific event and type. |
| turn_on | Turn on observer for specific event and type. |
| turn_off | Turn off observer for specific event and type. |
| turned_on | Checks if the observer for specific event and type is turned on. |
Do not enable and use observers unless absolutely necessary. Only do so if something can’t be done using systems, as observers are very inefficient and reduce the speed of the ECS. For example, if you’re developing a library that utilizes the ECS and initializes and runs the game’s physics under the hood using specific components. You need to track the addition and modification of these components to make the appropriate changes to the physics engine. In this case observers are really necessary, for game/app logic use systems, it’s much more efficient.