Add event listener subscribe/unsubsribe system

This commit is contained in:
Riyyi
2026-09-20 18:26:09 +02:00
parent 22be96ee45
commit cbc320b7f5
4 changed files with 226 additions and 77 deletions
+2 -2
View File
@@ -24,7 +24,7 @@ odin build game/ -show-timings \
-collection:gram=vendor/gram/src \ -collection:gram=vendor/gram/src \
-collection:wgpu=vendor \ -collection:wgpu=vendor \
-build-mode:dynamic \ -build-mode:dynamic \
-out:build/game_tmp -microarch:native -define:VERSION="$VERSION-debug" -debug "$@" -out:build/game_tmp -microarch:native -o:minimal -use-separate-modules -define:VERSION="$VERSION-debug" -debug "$@"
# Need to use a temp file on Linux/macOS because it first writes an empty file, # Need to use a temp file on Linux/macOS because it first writes an empty file,
# which the engine will load before it is actually fully written. # which the engine will load before it is actually fully written.
@@ -49,7 +49,7 @@ if [ "$OPTION" = "debug" ]; then
-collection:sindri=src \ -collection:sindri=src \
-collection:gram=vendor/gram/src \ -collection:gram=vendor/gram/src \
-collection:wgpu=vendor \ -collection:wgpu=vendor \
-out:build/$PROJECT -microarch:native -use-separate-modules -define:VERSION="$VERSION-debug" -debug "$@" -out:build/$PROJECT -microarch:native -o:minimal -use-separate-modules -define:VERSION="$VERSION-debug" -debug "$@"
exit 0 exit 0
fi fi
+5
View File
@@ -4,6 +4,11 @@ This document contains specifics about the workings of the Odin programming
language, dynamic linking and state management design patterns. These are language, dynamic linking and state management design patterns. These are
tested and validated findings, for continued reference by me. tested and validated findings, for continued reference by me.
## Compiler
Odin's compiler is demand-driver: a procedure's body is only semantically
analyzed when the proc is actually referenced.
## Dynamic Library Memory ## Dynamic Library Memory
When a program loads in a dynamic library, what happens with package globals? When a program loads in a dynamic library, what happens with package globals?
+167 -35
View File
@@ -1,5 +1,7 @@
package base package base
import "base:intrinsics"
import "base:runtime"
import "core:fmt" import "core:fmt"
// ----------------------------------------- // -----------------------------------------
@@ -7,8 +9,7 @@ import "core:fmt"
// event category, bitfield (?) // event category, bitfield (?)
// Event_Data :: union { Event_Data :: union {
Event :: union {
Window_Close_Event, Window_Close_Event,
Window_Resize_Event, Window_Resize_Event,
Joystick_Connect_Event, Joystick_Connect_Event,
@@ -22,115 +23,246 @@ Event :: union {
Mouse_Scroll_Event, Mouse_Scroll_Event,
} }
// Event :: struct { Event :: struct {
// data: Event_Data, data: Event_Data,
// handled: bool,
// }
Window_Close_Event :: struct {
handled: bool, handled: bool,
} }
Window_Close_Event :: struct {}
Window_Resize_Event :: struct { Window_Resize_Event :: struct {
handled: bool,
width: i32, width: i32,
height: i32, height: i32,
} }
Joystick_Connect_Event :: struct {
handled: bool,
id: i32,
}
Joystick_Disconnect_Event :: struct {
handled: bool,
id: i32,
}
Key_Press_Event :: struct { Key_Press_Event :: struct {
handled: bool,
key: Key, key: Key,
mods: Mod_Set, mods: Mod_Set,
} }
Key_Release_Event :: struct { Key_Release_Event :: struct {
handled: bool,
key: Key, key: Key,
mods: Mod_Set, mods: Mod_Set,
} }
Key_Repeat_Event :: struct { Key_Repeat_Event :: struct {
handled: bool,
key: Key, key: Key,
mods: Mod_Set, mods: Mod_Set,
} }
Mouse_Button_Press_Event :: struct { Mouse_Button_Press_Event :: struct {
handled: bool,
button: Mouse_Button, button: Mouse_Button,
mods: Mod_Set, mods: Mod_Set,
} }
Mouse_Button_Release_Event :: struct { Mouse_Button_Release_Event :: struct {
handled: bool,
button: Mouse_Button, button: Mouse_Button,
mods: Mod_Set, mods: Mod_Set,
} }
Mouse_Position_Event :: struct { Mouse_Position_Event :: struct {
handled: bool,
x_pos: f32, x_pos: f32,
y_pos: f32, y_pos: f32,
} }
Mouse_Scroll_Event :: struct { Mouse_Scroll_Event :: struct {
handled: bool,
x_offset: f32, x_offset: f32,
y_offset: f32, y_offset: f32,
} }
Joystick_Connect_Event :: struct {
id: i32,
}
Joystick_Disconnect_Event :: struct {
id: i32,
}
// Message Receiver
@(private)
Listener :: struct {
callback: proc(event: ^Event_Data, user_ptr: rawptr) -> bool, // return true = don't propagate
user_ptr: rawptr,
// TODO: test if hot_reload keeps the same function pointers and the listeners remain working.
// if they dont add a flag "engine_listener: bool", which engine calls should set true,
// then clear all game listeners
}
// Transfers messages between Sender and Receiver
Event_Bus :: struct {
queue: [dynamic]Event,
listeners: map[typeid][dynamic]Listener,
allocator: runtime.Allocator,
}
// ----------------------------------------- // -----------------------------------------
// Variables // Variables
// I want to have a list of Listeners per Event type, but this is state event_bus: Event_Bus
// listeners: map[typeid][dynamic]Listener
// -----------------------------------------
// Constructor/destructor
event_bus_init :: proc(allocator := context.allocator) {
// make() doesnt allocate until there's capacity, so set it > 0
queue := make([dynamic]Event, 0, 4, allocator)
listeners := make(map[typeid][dynamic]Listener, 4, allocator)
event_bus = {
queue = queue,
listeners = listeners,
allocator = allocator,
}
}
event_bus_destroy :: proc() {
assert(event_bus.queue != nil)
assert(event_bus.listeners != nil)
for _, ls in event_bus.listeners do delete(ls)
delete(event_bus.queue)
delete(event_bus.listeners)
event_bus.queue = nil
event_bus.listeners = nil
}
// ----------------------------------------- // -----------------------------------------
// Public functions // Public functions
// Dispatcher(Event) -> forwards to the right function event_subscribe :: proc(
on_event :: proc(event: Event) { $T: typeid,
callback: proc(event: ^T, user_ptr: rawptr) -> bool,
user_ptr: rawptr = nil,
) where intrinsics.type_is_variant_of(Event_Data, T) {
cb := cast(proc(_: ^Event_Data, _: rawptr) -> bool)callback // upcast
event_subscribe_raw(T, cb, user_ptr)
}
event_unsubscribe :: proc(
$T: typeid,
callback: proc(event: ^T, user_ptr: rawptr) -> bool,
user_ptr: rawptr = nil,
) where intrinsics.type_is_variant_of(Event_Data, T) {
cb := cast(proc(_: ^Event_Data, _: rawptr) -> bool)callback // upcast
event_unsubscribe_raw(T, cb, user_ptr)
}
on_event :: proc(data: Event_Data) {
assert(event_bus.queue != nil)
append(&event_bus.queue, Event{data = data})
}
// Drain all the events that have been queued up, in enqueue order
drain_event_queue :: proc() {
assert(event_bus.queue != nil)
fmt.println("toby!") fmt.println("toby!")
#partial switch e in event { for &event in event_bus.queue {
#partial switch e in event.data {
case Window_Close_Event: case Window_Close_Event:
event_dispatch(Window_Close_Event, &event)
fmt.println("Close") fmt.println("Close")
case Window_Resize_Event: case Window_Resize_Event:
event_dispatch(Window_Resize_Event, &event)
fmt.println("Resize:", e.width, e.height) fmt.println("Resize:", e.width, e.height)
case Key_Press_Event: case Key_Press_Event:
event_dispatch(Key_Press_Event, &event)
fmt.println("Key press", e.key, e.mods) fmt.println("Key press", e.key, e.mods)
case Key_Release_Event: case Key_Release_Event:
event_dispatch(Key_Release_Event, &event)
fmt.println("Key release", e.key, e.mods) fmt.println("Key release", e.key, e.mods)
case Key_Repeat_Event: case Key_Repeat_Event:
event_dispatch(Key_Repeat_Event, &event)
fmt.println("Key repeat", e.key, e.mods) fmt.println("Key repeat", e.key, e.mods)
case Mouse_Button_Press_Event: case Mouse_Button_Press_Event:
event_dispatch(Mouse_Button_Press_Event, &event)
fmt.println("Mouse press", e.button, e.mods) fmt.println("Mouse press", e.button, e.mods)
case Mouse_Button_Release_Event: case Mouse_Button_Release_Event:
event_dispatch(Mouse_Button_Release_Event, &event)
fmt.println("Mouse release", e.button, e.mods) fmt.println("Mouse release", e.button, e.mods)
case Mouse_Position_Event: case Mouse_Position_Event:
event_dispatch(Mouse_Position_Event, &event)
fmt.printfln("Mouse pos: {}x{}", e.x_pos, e.y_pos) fmt.printfln("Mouse pos: {}x{}", e.x_pos, e.y_pos)
case Mouse_Scroll_Event:
event_dispatch(Mouse_Scroll_Event, &event)
fmt.printfln("Mouse scroll: {}x{}", e.x_offset, e.y_offset)
case Joystick_Connect_Event: case Joystick_Connect_Event:
event_dispatch(Joystick_Connect_Event, &event)
fmt.println("Joy con:", e.id) fmt.println("Joy con:", e.id)
case Joystick_Disconnect_Event: case Joystick_Disconnect_Event:
event_dispatch(Joystick_Disconnect_Event, &event)
fmt.println("Joy dis:", e.id) fmt.println("Joy dis:", e.id)
case:
panic("[event] unimplemented event")
} }
} }
/* clear(&event_bus.queue)
Notes: }
Have the events queued up into a frame buffer that gets drained at a specific time. // Dispatch to the listeners registered for T, in reverse subscription order.
*/ // First listener to return true claims the event and stops propagation
event_dispatch :: proc(
$T: typeid,
event: ^Event,
) -> bool where intrinsics.type_is_variant_of(Event_Data, T) {
assert(event_bus.listeners != nil)
ls, ok := event_bus.listeners[T]
if !ok do return false
#reverse for &l in ls {
callback := cast(proc(_: ^T, _: rawptr) -> bool)l.callback // downcast
if callback(&event.data.(T), l.user_ptr) {
event.handled = true
return true
}
}
return false
}
// -----------------------------------------
// Private functions
@(private)
event_subscribe_raw :: proc(
$T: typeid,
callback: proc(event: ^Event_Data, user_ptr: rawptr) -> bool,
user_ptr: rawptr = nil,
) where intrinsics.type_is_variant_of(Event_Data, T) {
assert(event_bus.listeners != nil)
if _, ok := event_bus.listeners[T]; !ok {
event_bus.listeners[T] = make([dynamic]Listener, event_bus.allocator)
}
append(&event_bus.listeners[T], Listener{callback, user_ptr})
}
@(private)
event_unsubscribe_raw :: proc(
$T: typeid,
callback: proc(event: ^Event_Data, user_ptr: rawptr) -> bool,
user_ptr: rawptr = nil,
) where intrinsics.type_is_variant_of(Event_Data, T) {
assert(event_bus.listeners != nil)
ls, ok := event_bus.listeners[T]
if !ok do return
for i, l in ls {
if l.callback == callback && l.user_ptr == user_ptr {
ordered_remove(&event_bus.listeners[T], i) // preserve subscription order
return
}
}
}
+12
View File
@@ -17,6 +17,12 @@ main :: proc() {
hr, err := base.hot_reload_init() hr, err := base.hot_reload_init()
defer base.hot_reload_destroy(&hr) defer base.hot_reload_destroy(&hr)
// Event system
base.event_bus_init()
defer base.event_bus_destroy()
base.event_subscribe(base.Key_Press_Event, test)
api := base.active_lib(&hr) api := base.active_lib(&hr)
settings := api.settings() settings := api.settings()
@@ -39,6 +45,7 @@ main :: proc() {
start := time.tick_now() start := time.tick_now()
platform.os_poll_events() platform.os_poll_events()
base.drain_event_queue()
api.update(dt) api.update(dt)
platform.frame(dt) platform.frame(dt)
@@ -53,3 +60,8 @@ main :: proc() {
api.destroy() api.destroy()
} }
test :: proc(e: ^base.Key_Press_Event, _: rawptr) -> bool {
fmt.println("main: @@@@@@@@@@@", e)
return true
}