Moecs - simple entity component system

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
1 Like

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.

1 Like

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.