update docs
This commit is contained in:
@@ -7,6 +7,15 @@ my silly little ecs engine for odin and raylib :3
|
|||||||
## contents
|
## contents
|
||||||
- [install](#install)
|
- [install](#install)
|
||||||
- [usage](#usage)
|
- [usage](#usage)
|
||||||
|
- [systems](#systems)
|
||||||
|
- [components](#components)
|
||||||
|
- [spawn entities](#spawn-entities)
|
||||||
|
- [query](#query)
|
||||||
|
- [plugins](#plugins)
|
||||||
|
- [signals](#signals)
|
||||||
|
- [timers](#timers)
|
||||||
|
- [ui](#ui)
|
||||||
|
- [tween](#tween)
|
||||||
|
|
||||||
## install
|
## install
|
||||||
clone this repo in your project directory
|
clone this repo in your project directory
|
||||||
@@ -39,15 +48,21 @@ main :: proc() {
|
|||||||
### systems
|
### systems
|
||||||
systems are procedures that run on set schedules
|
systems are procedures that run on set schedules
|
||||||
|
|
||||||
a schedule just determines when a producure runs\
|
a schedule just determines when a procedure runs\
|
||||||
schedules are:
|
there are 8 of them, and `run()` walks them in this order:
|
||||||
- STARTUP
|
|
||||||
- UPDATE
|
|
||||||
- DRAW
|
|
||||||
|
|
||||||
`STARTUP` runs once at the start of an app\
|
- `STARTUP` — once, before the first frame
|
||||||
`UPDATE` runs every frame\
|
- `PRE_UPDATE` — every frame, first thing
|
||||||
`DRAW` runs every frame *after* update, and makes draw calls to raylib\
|
- `UPDATE` — every frame, your main logic
|
||||||
|
- `POST_UPDATE` — every frame, last stop before drawing (`signal_system` clears the signal buses here)
|
||||||
|
- `PRE_DRAW` — every frame, opens the raylib frame (`DefaultPlugin` calls `BeginDrawing` + clears the background here)
|
||||||
|
- `DRAW` — every frame, inside the frame — this is where your raylib draw calls go
|
||||||
|
- `POST_DRAW` — every frame, closes the raylib frame (`DefaultPlugin` calls `EndDrawing` here)
|
||||||
|
- `DESTROY` — once, after the loop exits (`DefaultPlugin` calls `CloseWindow` here)
|
||||||
|
|
||||||
|
so: `STARTUP` runs once up front, the six update/draw ones run every frame in that order, `DESTROY` runs once when the window closes.
|
||||||
|
|
||||||
|
systems registered on the same schedule run in registration order — whoever was `add_system()`ed first runs first (that includes the systems your plugins add, so `add_plugin(&app, ecs.DefaultPlugin)` before your own systems means shoal's defaults run first).
|
||||||
|
|
||||||
```odin
|
```odin
|
||||||
main :: proc() {
|
main :: proc() {
|
||||||
@@ -80,6 +95,8 @@ and create it's storage
|
|||||||
Example: ecs.Storage(ExampleComponent)
|
Example: ecs.Storage(ExampleComponent)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
shoal ships a few built-ins already declared as globals, so there's nothing to create — just pass `&ecs.Window`, `&ecs.Cursor`, `&ecs.Sprite` or `&ecs.Transform`. `DefaultPlugin` reads and writes them: it opens the window from `Window`, keeps `Cursor` on the mouse, and draws every `Sprite` at its `Transform`.
|
||||||
|
|
||||||
### spawn entities
|
### spawn entities
|
||||||
create a new entity with `spawn_entity()`
|
create a new entity with `spawn_entity()`
|
||||||
|
|
||||||
@@ -98,8 +115,16 @@ main :: proc() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
or fill it in on the spot with the data overload
|
||||||
|
|
||||||
|
```odin
|
||||||
|
main :: proc() {
|
||||||
|
ecs.add_component(entity, &Example, ExampleComponent{...})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### query
|
### query
|
||||||
since components are saved in storage next to their entity, you can loop through a Storage and pick out what you like
|
since components are saved in storage next to their entity, you can loop through a Storage and pick out what you like. `slice()` and `slice_entity()` pair up by index: `entities[i]` owns `components[i]`
|
||||||
|
|
||||||
```odin
|
```odin
|
||||||
main :: proc() {
|
main :: proc() {
|
||||||
@@ -111,6 +136,8 @@ main :: proc() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
one caveat: `slice()` hands back the storage's live array, not a copy, so adding components afterwards can reallocate it out from under you — don't hold one across `add_component()` calls.
|
||||||
|
|
||||||
or if you know your storage only has one entry use `single()`
|
or if you know your storage only has one entry use `single()`
|
||||||
|
|
||||||
```odin
|
```odin
|
||||||
@@ -118,6 +145,17 @@ main :: proc() {
|
|||||||
component, entity_id := ecs.single(&Example)
|
component, entity_id := ecs.single(&Example)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
and once you have an entity id, `get_component()` reaches into any storage to get that entity's copy of the component — handy for components that live alongside another one (a sprite's `Transform`, say)
|
||||||
|
|
||||||
|
```odin
|
||||||
|
main :: proc() {
|
||||||
|
component, entity_id := ecs.single(&Example)
|
||||||
|
other := ecs.get_component(entity_id, &OtherExample)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
no existence check though: only ask for entities you know are in that storage.
|
||||||
### plugins
|
### plugins
|
||||||
plugins are just a procedure that takes `App` as a parameter, meaning it can make changes to `app`, just like main()
|
plugins are just a procedure that takes `App` as a parameter, meaning it can make changes to `app`, just like main()
|
||||||
|
|
||||||
@@ -136,6 +174,8 @@ main :: proc() {
|
|||||||
|
|
||||||
plugins make your scripts very reuseable. in fact, much of shoal's default behaviors come from plugins via `DefaultPlugin()`
|
plugins make your scripts very reuseable. in fact, much of shoal's default behaviors come from plugins via `DefaultPlugin()`
|
||||||
|
|
||||||
|
run() fires every plugin in the order you added them, once, before the STARTUP systems — so everything they register is in place before anything executes
|
||||||
|
|
||||||
### signals
|
### signals
|
||||||
signals are structs your systems can talk to each other with. one system writes a signal with data, another system reads it later in the same frame — as long as the reader runs before `signal_system` clears the buses at POST_UPDATE (see the caveat at the end of this section)
|
signals are structs your systems can talk to each other with. one system writes a signal with data, another system reads it later in the same frame — as long as the reader runs before `signal_system` clears the buses at POST_UPDATE (see the caveat at the end of this section)
|
||||||
|
|
||||||
@@ -165,3 +205,46 @@ scheduling caveat: every system sees exactly the signals written since the last
|
|||||||
- systems running before POST_UPDATE (STARTUP/PRE_UPDATE/UPDATE/early POST_UPDATE) see same-frame writes
|
- systems running before POST_UPDATE (STARTUP/PRE_UPDATE/UPDATE/early POST_UPDATE) see same-frame writes
|
||||||
- a DRAW system sees none of them — POST_UPDATE already cleared the bus. it only sees signals written after POST_UPDATE by an earlier system in the same frame (say, one DRAW system reacting to another's write)
|
- a DRAW system sees none of them — POST_UPDATE already cleared the bus. it only sees signals written after POST_UPDATE by an earlier system in the same frame (say, one DRAW system reacting to another's write)
|
||||||
- writes made after POST_UPDATE survive the rest of the frame and are picked up by the next frame's PRE_UPDATE/UPDATE systems, then cleared at the next POST_UPDATE — so the next frame's DRAW never sees them either
|
- writes made after POST_UPDATE survive the rest of the frame and are picked up by the next frame's PRE_UPDATE/UPDATE systems, then cleared at the next POST_UPDATE — so the next frame's DRAW never sees them either
|
||||||
|
|
||||||
|
### timers
|
||||||
|
timers are named countdowns. `start_timer()` starts one (or restarts one already stored under that name), and `get_timer()` reads it back — anything that never started or already finished reads back zeroed, so a nonzero `lifetime` means "still ticking"
|
||||||
|
|
||||||
|
```odin
|
||||||
|
example_system :: proc() {
|
||||||
|
if ecs.get_timer("cooldown").lifetime != 0 {return} // still cooling down
|
||||||
|
ecs.start_timer(0.5, "cooldown")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
timers tick down every frame in `run_timers`, which `DefaultPlugin` registers at UPDATE. when one runs out its entry is reset to `Timer{}` — that's the auto-clear, and why `get_timer()` goes back to zero when you've waited it out.
|
||||||
|
|
||||||
|
on expiry a `TimerFired{name = ...}` signal is *meant* to be queued for `read(TimerFired)` — wip: as written the reset zeroes the entry before the name is read, so `TimerFired` currently never fires (reading the timers themselves works fine). once that's fixed the usual signal caveat applies: a reader has to run before `signal_system` clears the buses at POST_UPDATE, see the end of the [signals](#signals) section
|
||||||
|
|
||||||
|
### ui
|
||||||
|
`shoal/ui` wraps microui with its own plugin. add it before any system that wants to draw ui
|
||||||
|
|
||||||
|
```odin
|
||||||
|
import ui "shoal/ui"
|
||||||
|
import mu "vendor:microui"
|
||||||
|
|
||||||
|
main :: proc() {
|
||||||
|
ecs.add_plugin(&app, ui.UIPlugin)
|
||||||
|
ecs.add_system(&app, ecs.DRAW, my_ui)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`ui.UIPlugin` registers `init()` at STARTUP (uploads microui's atlas texture), `begin()` at PRE_DRAW (forwards raylib input and starts the frame), frees the frame's temp memory and runs `end()` at POST_DRAW (renders the ui), and `destroy()` at DESTROY.
|
||||||
|
|
||||||
|
your system builds the ui against the context `get_ctx()` hands back during the frame
|
||||||
|
|
||||||
|
```odin
|
||||||
|
my_ui :: proc() {
|
||||||
|
if mu.begin_window(ui.get_ctx(), "hello", {100, 100, 100, 100}) {
|
||||||
|
defer mu.end_window(ui.get_ctx())
|
||||||
|
mu.label(ui.get_ctx(), "Hello, world")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### tween
|
||||||
|
wip: tweens don't do anything yet. `tween.odin` ships `Tween`, `start_tween()`, `create_tween()` (a stub that returns an empty `Tween` and ignores its arguments), `update_tween_system()` (empty, and no schedule registers it) and a few sine easing helpers nothing calls — don't build on it. the `EASE_IN`/`EASE_OUT`/`EASE_IN_OUT` constants are ids for the unused `Tween.ease` field.
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package main
|
|||||||
import "core:math/rand"
|
import "core:math/rand"
|
||||||
import "core:strconv"
|
import "core:strconv"
|
||||||
import "core:strings"
|
import "core:strings"
|
||||||
import ecs "shoal/src"
|
import ecs "../src"
|
||||||
import rl "vendor:raylib"
|
import rl "vendor:raylib"
|
||||||
|
|
||||||
app: ecs.App
|
app: ecs.App
|
||||||
@@ -13,7 +13,6 @@ BallComponent :: struct {
|
|||||||
radius: f32,
|
radius: f32,
|
||||||
speed: f32,
|
speed: f32,
|
||||||
}
|
}
|
||||||
Ball: ecs.Storage(BallComponent)
|
|
||||||
|
|
||||||
main :: proc() {
|
main :: proc() {
|
||||||
ecs.add_plugin(&app, ecs.DefaultPlugin)
|
ecs.add_plugin(&app, ecs.DefaultPlugin)
|
||||||
@@ -25,18 +24,22 @@ main :: proc() {
|
|||||||
ecs.run(&app)
|
ecs.run(&app)
|
||||||
}
|
}
|
||||||
|
|
||||||
setup_window :: proc() {
|
// STARTUP system taking the world: the window lives in it, so adjusting the
|
||||||
window, _ := ecs.single(&ecs.Window)
|
// default WindowComponent is a world lookup now
|
||||||
|
setup_window :: proc(world: ^ecs.World) {
|
||||||
|
window, _ := ecs.single(world, ecs.WindowComponent)
|
||||||
window.background = rl.BLACK
|
window.background = rl.BLACK
|
||||||
}
|
}
|
||||||
|
|
||||||
spawn_balls :: proc() {
|
// STARTUP system taking the world: spawn_entity wants it, and the balls'
|
||||||
|
// components live inside it, no global storage to declare
|
||||||
|
spawn_balls :: proc(world: ^ecs.World) {
|
||||||
for i in 0 ..< ball_count {
|
for i in 0 ..< ball_count {
|
||||||
ball := ecs.spawn_entity(&app)
|
ball := ecs.spawn_entity(world)
|
||||||
ecs.add_component(ball, &Ball, BallComponent{radius = 5, speed = rand.float32_range(4, 6)})
|
ecs.add_component(world, ball, BallComponent{radius = 5, speed = rand.float32_range(4, 6)})
|
||||||
ecs.add_component(
|
ecs.add_component(
|
||||||
|
world,
|
||||||
ball,
|
ball,
|
||||||
&ecs.Transform,
|
|
||||||
ecs.TransformComponent {
|
ecs.TransformComponent {
|
||||||
translation = {400, 300},
|
translation = {400, 300},
|
||||||
rotation = rand.float32_range(0, 360),
|
rotation = rand.float32_range(0, 360),
|
||||||
@@ -45,13 +48,13 @@ spawn_balls :: proc() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
update_balls :: proc() {
|
update_balls :: proc(world: ^ecs.World) {
|
||||||
window, _ := ecs.single(&ecs.Window)
|
window, _ := ecs.single(world, ecs.WindowComponent)
|
||||||
|
|
||||||
balls := ecs.slice(&Ball)
|
balls := ecs.slice(world, BallComponent)
|
||||||
entities := ecs.slice_entity(&Ball)
|
entities := ecs.slice_entity(world, BallComponent)
|
||||||
for ball, i in balls {
|
for ball, i in balls {
|
||||||
transform := ecs.get_component(entities[i], &ecs.Transform)
|
transform := ecs.get_component(world, entities[i], ecs.TransformComponent)
|
||||||
pos := transform.translation
|
pos := transform.translation
|
||||||
|
|
||||||
if pos.x <= 0 || pos.x >= f32(window.width) || pos.y <= 0 || pos.y >= f32(window.height) {
|
if pos.x <= 0 || pos.x >= f32(window.width) || pos.y <= 0 || pos.y >= f32(window.height) {
|
||||||
@@ -63,11 +66,11 @@ update_balls :: proc() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
draw_balls :: proc() {
|
draw_balls :: proc(world: ^ecs.World) {
|
||||||
balls := ecs.slice(&Ball)
|
balls := ecs.slice(world, BallComponent)
|
||||||
entities := ecs.slice_entity(&Ball)
|
entities := ecs.slice_entity(world, BallComponent)
|
||||||
for ball, i in balls {
|
for ball, i in balls {
|
||||||
transform := ecs.get_component(entities[i], &ecs.Transform)
|
transform := ecs.get_component(world, entities[i], ecs.TransformComponent)
|
||||||
rl.DrawCircle(
|
rl.DrawCircle(
|
||||||
i32(transform.translation.x),
|
i32(transform.translation.x),
|
||||||
i32(transform.translation.y),
|
i32(transform.translation.y),
|
||||||
@@ -81,6 +84,6 @@ show_framerate :: proc() {
|
|||||||
fps_buf: [19]byte
|
fps_buf: [19]byte
|
||||||
framerate := strconv.write_int(fps_buf[:], i64(1 / rl.GetFrameTime()), 10)
|
framerate := strconv.write_int(fps_buf[:], i64(1 / rl.GetFrameTime()), 10)
|
||||||
rl.DrawText(strings.clone_to_cstring(framerate), 0, 0, 24, rl.WHITE)
|
rl.DrawText(strings.clone_to_cstring(framerate), 0, 0, 24, rl.WHITE)
|
||||||
rl.DrawText(" fps", 0, 0, 24, rl.WHITE)
|
rl.DrawText(" fps", 0, 24, 24, rl.WHITE)
|
||||||
rl.DrawText("10000 balls", 0, 24, 24, rl.WHITE)
|
rl.DrawText("10000 balls", 0, 24, 24, rl.WHITE)
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-10
@@ -2,35 +2,36 @@ package shoal
|
|||||||
|
|
||||||
import rl "vendor:raylib"
|
import rl "vendor:raylib"
|
||||||
|
|
||||||
// what the app's window opens with: size, title, and the clear color
|
// what the app's window opens with: size, title, and the clear color.
|
||||||
// DefaultPlugin paints at PRE_DRAW
|
// DefaultPlugin opens the window from it at STARTUP and paints the clear
|
||||||
|
// color at PRE_DRAW
|
||||||
WindowComponent :: struct {
|
WindowComponent :: struct {
|
||||||
width: i32,
|
width: i32,
|
||||||
height: i32,
|
height: i32,
|
||||||
title: string,
|
title: string,
|
||||||
background: rl.Color,
|
background: rl.Color,
|
||||||
}
|
}
|
||||||
// pre-created global storage — just pass &Window, no need to declare it.
|
|
||||||
// the same goes for every storage in this file
|
|
||||||
Window: Storage(WindowComponent)
|
|
||||||
|
|
||||||
// the mouse position, refreshed every UPDATE by DefaultPlugin
|
// the mouse position, refreshed every UPDATE by DefaultPlugin
|
||||||
CursorComponent :: struct {
|
CursorComponent :: struct {
|
||||||
position: rl.Vector2,
|
position: rl.Vector2,
|
||||||
}
|
}
|
||||||
Cursor: Storage(CursorComponent)
|
|
||||||
|
|
||||||
// a texture DefaultPlugin draws at DRAW, placed by the same entity's
|
// a texture DefaultPlugin draws at DRAW, placed by the same entity's
|
||||||
// Transform (so every sprite entity needs a Transform component too)
|
// TransformComponent (so every sprite entity needs a Transform component
|
||||||
|
// too)
|
||||||
SpriteComponent :: struct {
|
SpriteComponent :: struct {
|
||||||
texture: rl.Texture2D,
|
texture: rl.Texture2D,
|
||||||
}
|
}
|
||||||
Sprite: Storage(SpriteComponent)
|
|
||||||
|
|
||||||
// 2D translation + rotation, read back by draw_sprite when it places the
|
// 2D translation + rotation, read back by draw_sprite when it places the
|
||||||
// entity's Sprite
|
// entity's SpriteComponent
|
||||||
TransformComponent :: struct {
|
TransformComponent :: struct {
|
||||||
translation: rl.Vector2,
|
translation: rl.Vector2,
|
||||||
rotation: f32,
|
rotation: f32,
|
||||||
}
|
}
|
||||||
Transform: Storage(TransformComponent)
|
|
||||||
|
// components are plain struct declarations. the world holds every storage
|
||||||
|
// internally, keyed by the component's typeid and created on first add (see
|
||||||
|
// get_storage in entities.odin), so there's nothing to declare next to a
|
||||||
|
// component
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
package shoal
|
package shoal
|
||||||
|
|
||||||
// schedule ids for add_system — run() walks them in this order:
|
// schedule ids for add_system. run() walks them in this order:
|
||||||
//
|
//
|
||||||
// STARTUP once, before the first frame (plugins run before this)
|
// STARTUP once, before the first frame (plugins run before this)
|
||||||
// PRE_UPDATE \
|
// PRE_UPDATE \
|
||||||
@@ -22,7 +22,7 @@ DRAW :: 5
|
|||||||
POST_DRAW :: 6
|
POST_DRAW :: 6
|
||||||
DESTROY :: 7
|
DESTROY :: 7
|
||||||
|
|
||||||
// easing ids for the (wip) Tween.ease field — see tween.odin, nothing reads
|
// easing ids for the (wip) Tween.ease field. see tween.odin, nothing reads
|
||||||
// them yet
|
// them yet
|
||||||
EASE_IN :: 0
|
EASE_IN :: 0
|
||||||
EASE_OUT :: 1
|
EASE_OUT :: 1
|
||||||
|
|||||||
+81
-28
@@ -2,59 +2,112 @@ package shoal
|
|||||||
|
|
||||||
import rl "vendor:raylib"
|
import rl "vendor:raylib"
|
||||||
|
|
||||||
// the app itself: systems grouped by schedule id (see the STARTUP..DESTROY
|
// the engine backend: systems grouped by schedule id (see the STARTUP..DESTROY
|
||||||
// constants), the plugins that run before everything else, and the list of
|
// constants), the plugins that run before everything else, and the world,
|
||||||
// every entity id handed out by spawn_entity
|
// holding every entity and component at a lower scope than the app itself. only
|
||||||
|
// main() and plugins ever see an ^App; systems are handed ^World and so
|
||||||
|
// structurally can't reach the registration machinery (add_system, plugins,
|
||||||
|
// run)
|
||||||
App :: struct {
|
App :: struct {
|
||||||
systems: map[int][dynamic]proc(),
|
systems: map[int][dynamic]System,
|
||||||
plugins: [dynamic]proc(app: ^App),
|
plugins: [dynamic]proc(app: ^App),
|
||||||
entities: [dynamic]Entity,
|
world: World,
|
||||||
|
}
|
||||||
|
|
||||||
|
// a system in either shape add_system accepts: a plain proc(), or one that
|
||||||
|
// takes the world as proc(world: ^World). stored uniformly so run() can give
|
||||||
|
// every system the same dispatch. more parameter shapes can go here later
|
||||||
|
System :: union {
|
||||||
|
proc(),
|
||||||
|
proc(world: ^World),
|
||||||
}
|
}
|
||||||
|
|
||||||
// register a system on a schedule (one of the STARTUP..DESTROY constants).
|
// register a system on a schedule (one of the STARTUP..DESTROY constants).
|
||||||
// systems on the same schedule run in registration order, every time that
|
// systems on the same schedule run in registration order, every time that
|
||||||
// schedule comes around
|
// schedule comes around. a system is either a plain proc(), or takes the
|
||||||
add_system :: proc(app: ^App, schedule: int, system: proc()) {
|
// world as proc(world: ^World) when it needs to spawn entities or touch
|
||||||
if _, ok := app.systems[schedule]; !ok {
|
// components
|
||||||
app.systems[schedule] = [dynamic]proc(){}
|
add_system :: proc {
|
||||||
}
|
add_plain_system,
|
||||||
append(&app.systems[schedule], system)
|
add_world_system,
|
||||||
}
|
}
|
||||||
|
|
||||||
// register a plugin: a proc that gets ^App and can add_system/add_plugin just
|
@(private)
|
||||||
// like main() would. run() executes them in registration order, once, before
|
add_plain_system :: proc(app: ^App, schedule: int, system: proc()) {
|
||||||
// the STARTUP systems
|
if _, ok := app.systems[schedule]; !ok {
|
||||||
|
app.systems[schedule] = [dynamic]System{}
|
||||||
|
}
|
||||||
|
append(&app.systems[schedule], System(system))
|
||||||
|
}
|
||||||
|
|
||||||
|
@(private)
|
||||||
|
add_world_system :: proc(app: ^App, schedule: int, system: proc(world: ^World)) {
|
||||||
|
if _, ok := app.systems[schedule]; !ok {
|
||||||
|
app.systems[schedule] = [dynamic]System{}
|
||||||
|
}
|
||||||
|
append(&app.systems[schedule], System(system))
|
||||||
|
}
|
||||||
|
|
||||||
|
// register a plugin: a proc that gets ^App, the one privilege over regular
|
||||||
|
// systems, which only ever see ^World. a plugin can add_system/add_plugin
|
||||||
|
// the same way main() would, and reach the game data through get_world. run()
|
||||||
|
// executes plugins in registration order, once, before the STARTUP systems.
|
||||||
|
// the systems a plugin adds can be plain proc() or take the world, same as
|
||||||
|
// any other add_system call
|
||||||
add_plugin :: proc(app: ^App, plugin: proc(app: ^App)) {
|
add_plugin :: proc(app: ^App, plugin: proc(app: ^App)) {
|
||||||
append(&app.plugins, plugin)
|
append(&app.plugins, plugin)
|
||||||
}
|
}
|
||||||
|
|
||||||
// everything registered on a schedule, in registration order — or an empty
|
// everything registered on a schedule, in registration order, or an empty
|
||||||
// list if nothing was added. this is what run() iterates each frame
|
// list if nothing was added. each entry is a System union: switch on it to
|
||||||
get_system :: proc(app: ^App, schedule: int) -> [dynamic]proc() {
|
// tell a plain proc() from a proc(world: ^World). this is what run() walks
|
||||||
|
// each frame
|
||||||
|
get_system :: proc(app: ^App, schedule: int) -> [dynamic]System {
|
||||||
if systems, ok := app.systems[schedule]; ok && len(systems) > 0 {
|
if systems, ok := app.systems[schedule]; ok && len(systems) > 0 {
|
||||||
return systems
|
return systems
|
||||||
}
|
}
|
||||||
return [dynamic]proc(){}
|
return [dynamic]System{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// the world tucked inside the app, the ^App equivalent of the ^World
|
||||||
|
// systems get handed. plugins use it to spawn entities or add components
|
||||||
|
// before the frame loop starts; regular systems never need it, run() gives
|
||||||
|
// them the same world directly
|
||||||
|
get_world :: proc(app: ^App) -> ^World {
|
||||||
|
return &app.world
|
||||||
}
|
}
|
||||||
|
|
||||||
// run every plugin (in registration order), then the STARTUP systems once,
|
// run every plugin (in registration order), then the STARTUP systems once,
|
||||||
// then the frame loop: PRE_UPDATE -> UPDATE -> POST_UPDATE -> PRE_DRAW ->
|
// then the frame loop: PRE_UPDATE -> UPDATE -> POST_UPDATE -> PRE_DRAW ->
|
||||||
// DRAW -> POST_DRAW, over and over until rl.WindowShouldClose() says stop,
|
// DRAW -> POST_DRAW, over and over until rl.WindowShouldClose() says stop,
|
||||||
// and finally the DESTROY systems
|
// and finally the DESTROY systems. world systems get handed the app's own
|
||||||
|
// world, the same one run() is holding. there's no second copy
|
||||||
run :: proc(app: ^App) {
|
run :: proc(app: ^App) {
|
||||||
for p in app.plugins {
|
for p in app.plugins {
|
||||||
p(app)
|
p(app)
|
||||||
}
|
}
|
||||||
for i in app.systems[STARTUP] {i()}
|
for s in get_system(app, STARTUP) {run_system(app, s)}
|
||||||
|
|
||||||
for !rl.WindowShouldClose() {
|
for !rl.WindowShouldClose() {
|
||||||
for i in get_system(app, PRE_UPDATE) {i()}
|
for s in get_system(app, PRE_UPDATE) {run_system(app, s)}
|
||||||
for i in get_system(app, UPDATE) {i()}
|
for s in get_system(app, UPDATE) {run_system(app, s)}
|
||||||
for i in get_system(app, POST_UPDATE) {i()}
|
for s in get_system(app, POST_UPDATE) {run_system(app, s)}
|
||||||
for i in get_system(app, PRE_DRAW) {i()}
|
for s in get_system(app, PRE_DRAW) {run_system(app, s)}
|
||||||
for i in get_system(app, DRAW) {i()}
|
for s in get_system(app, DRAW) {run_system(app, s)}
|
||||||
for i in get_system(app, POST_DRAW) {i()}
|
for s in get_system(app, POST_DRAW) {run_system(app, s)}
|
||||||
}
|
}
|
||||||
|
|
||||||
for i in get_system(app, DESTROY) {i()}
|
for s in get_system(app, DESTROY) {run_system(app, s)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// hand a stored system its call: a plain one just runs, a world one gets the
|
||||||
|
// app's world
|
||||||
|
@(private)
|
||||||
|
run_system :: proc(app: ^App, system: System) {
|
||||||
|
switch s in system {
|
||||||
|
case proc():
|
||||||
|
s()
|
||||||
|
case proc(world: ^World):
|
||||||
|
s(&app.world)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+107
-28
@@ -1,76 +1,155 @@
|
|||||||
package shoal
|
package shoal
|
||||||
|
|
||||||
import "core:fmt"
|
import "core:mem"
|
||||||
|
|
||||||
// an entity is just an id — a u32 handed out by spawn_entity and recorded in
|
// an entity is an id, a u32 handed out by spawn_entity and recorded in
|
||||||
// the app's entity list. no components hang off it, they live in storages
|
// the world's entity list. no components hang off it, they live in storages
|
||||||
Entity :: distinct u32
|
Entity :: distinct u32
|
||||||
|
|
||||||
|
// all the game data: every entity id handed out by spawn_entity, plus one
|
||||||
|
// storage per component type, created lazily the first time anything touches
|
||||||
|
// that component. a zero World{} is ready to use: the entity list grows on
|
||||||
|
// append and the storage map makes itself on first component add. App holds
|
||||||
|
// one of these; systems are handed a ^World directly, and plugins reach it
|
||||||
|
// through their ^App via get_world. never copy a World by value: the copy
|
||||||
|
// duplicates the header but shares the heap storages, so destroying both
|
||||||
|
// copies double-frees. pass ^World around, don't copy
|
||||||
|
World :: struct {
|
||||||
|
entities: [dynamic]Entity,
|
||||||
|
storages: map[typeid]Storage_Entry,
|
||||||
|
}
|
||||||
|
|
||||||
|
// one entry in the world's storage map: the storage itself, type-erased so
|
||||||
|
// every component fits the same map, plus a proc that knows how to destroy
|
||||||
|
// it. storages are heap-allocated once per world so the map can rehash
|
||||||
|
// without moving them around
|
||||||
|
Storage_Entry :: struct {
|
||||||
|
storage: rawptr,
|
||||||
|
drop: proc(storage: rawptr),
|
||||||
|
}
|
||||||
|
|
||||||
// all the components of one type, kept together: dense holds them in add
|
// all the components of one type, kept together: dense holds them in add
|
||||||
// order, entities[i] is who owns dense[i], and sparse maps entity -> index
|
// order, entities[i] is who owns dense[i], and sparse maps entity -> index
|
||||||
// into dense so get_component is a lookup instead of a scan.
|
// into dense so get_component is a lookup instead of a scan.
|
||||||
// storages are usually declared as a global: `MyComp: Storage(MyCompType)`
|
// you never declare one of these yourself; the world creates storages
|
||||||
|
// on first access, see get_storage below
|
||||||
Storage :: struct($T: typeid) {
|
Storage :: struct($T: typeid) {
|
||||||
dense: [dynamic]T,
|
dense: [dynamic]T,
|
||||||
entities: [dynamic]Entity,
|
entities: [dynamic]Entity,
|
||||||
sparse: map[Entity]int,
|
sparse: map[Entity]int,
|
||||||
}
|
}
|
||||||
|
|
||||||
// hand out a fresh entity id (the next index in app's entity list) and
|
// the storage for component type T inside this world, created on first
|
||||||
|
// access, the same lazy pattern as the signal buses in signals.odin. every
|
||||||
|
// world has its own copy, so two worlds never share components. package-
|
||||||
|
// private on purpose: outside code goes through add_component/slice/single/
|
||||||
|
// get_component, which find storages for you
|
||||||
|
@(private)
|
||||||
|
get_storage :: proc(world: ^World, $T: typeid) -> ^Storage(T) {
|
||||||
|
if world.storages == nil {
|
||||||
|
world.storages = {}
|
||||||
|
}
|
||||||
|
if entry, exists := world.storages[T]; exists {
|
||||||
|
return (^Storage(T))(entry.storage)
|
||||||
|
}
|
||||||
|
storage := new(Storage(T))
|
||||||
|
world.storages[T] = {
|
||||||
|
storage = storage,
|
||||||
|
drop = drop_storage(T),
|
||||||
|
}
|
||||||
|
return storage
|
||||||
|
}
|
||||||
|
|
||||||
|
// called once per component type, hands destroy_world a rawptr drop for
|
||||||
|
// that storage
|
||||||
|
@(private)
|
||||||
|
drop_storage :: proc($T: typeid) -> proc(storage: rawptr) {
|
||||||
|
return proc(storage: rawptr) {
|
||||||
|
s := (^Storage(T))(storage)
|
||||||
|
delete(s.dense)
|
||||||
|
delete(s.entities)
|
||||||
|
delete(s.sparse)
|
||||||
|
mem.free(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// free everything the world owns: every storage's data and the storages
|
||||||
|
// themselves, the storage map, and the entity list. the world is left empty
|
||||||
|
// and safe to drop. this is teardown, not per-frame cleanup; nothing needs
|
||||||
|
// calling while the app runs
|
||||||
|
destroy_world :: proc(world: ^World) {
|
||||||
|
for _, entry in world.storages {
|
||||||
|
entry.drop(entry.storage)
|
||||||
|
}
|
||||||
|
delete(world.storages)
|
||||||
|
delete(world.entities)
|
||||||
|
world.storages = nil
|
||||||
|
world.entities = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// hand out a fresh entity id (the next index in the world's entity list) and
|
||||||
// remember it. ids are never reused
|
// remember it. ids are never reused
|
||||||
spawn_entity :: proc(app: ^App) -> Entity {
|
spawn_entity :: proc(world: ^World) -> Entity {
|
||||||
entity_num := len(&app.entities)
|
entity_num := len(world.entities)
|
||||||
entity: Entity = Entity(entity_num)
|
entity: Entity = Entity(entity_num)
|
||||||
append(&app.entities, entity)
|
append(&world.entities, entity)
|
||||||
return entity
|
return entity
|
||||||
}
|
}
|
||||||
|
|
||||||
// add a component to an entity's storage — zero-initialized when you pass
|
// add a component to an entity, zero-initialized when you pass just the
|
||||||
// just the storage: add_component(entity, &Store), or filled in when you pass
|
// type: add_component(world, entity, MyComponent), or filled in when you
|
||||||
// data too: add_component(entity, &Store, StoreComponent{...})
|
// pass data too: add_component(world, entity, MyComponent{...}). the world's
|
||||||
|
// storage for that type is created on the spot if this is the first one
|
||||||
add_component :: proc {
|
add_component :: proc {
|
||||||
add_fresh_component,
|
add_fresh_component,
|
||||||
add_component_with_data,
|
add_component_with_data,
|
||||||
}
|
}
|
||||||
|
|
||||||
// append a zeroed T to the storage and note where it landed in the sparse map
|
// append a zeroed T to the entity's storage and note where it landed in the
|
||||||
add_fresh_component :: proc(entity: Entity, storage: ^Storage($T)) {
|
// sparse map
|
||||||
|
add_fresh_component :: proc(world: ^World, entity: Entity, $T: typeid) {
|
||||||
|
storage := get_storage(world, T)
|
||||||
append(&storage.dense, T{})
|
append(&storage.dense, T{})
|
||||||
append(&storage.entities, entity)
|
append(&storage.entities, entity)
|
||||||
i := len(storage.dense) - 1
|
i := len(storage.dense) - 1
|
||||||
defer storage.sparse[entity] = i
|
storage.sparse[entity] = i
|
||||||
}
|
}
|
||||||
|
|
||||||
// same, but starts from a copy of the component you pass in
|
// same, but starts from a copy of the component you pass in
|
||||||
add_component_with_data :: proc(entity: Entity, storage: ^Storage($T), component: T) {
|
add_component_with_data :: proc(world: ^World, entity: Entity, component: $T) {
|
||||||
|
storage := get_storage(world, T)
|
||||||
append(&storage.dense, component)
|
append(&storage.dense, component)
|
||||||
append(&storage.entities, entity)
|
append(&storage.entities, entity)
|
||||||
i := len(storage.dense) - 1
|
i := len(storage.dense) - 1
|
||||||
defer storage.sparse[entity] = i
|
storage.sparse[entity] = i
|
||||||
}
|
}
|
||||||
|
|
||||||
// the storage's dense array — the live backing array, not a copy of the data,
|
// every component of type T in the world: the live backing array, not a
|
||||||
// so index i lines up with slice_entity()[i]. a later add_component that grows
|
// copy of the data, so index i lines up with slice_entity()[i]. a later
|
||||||
// past capacity reallocates it, so don't hold the result across adds
|
// add_component that grows past capacity reallocates it, so don't hold the
|
||||||
slice :: proc(storage: ^Storage($T)) -> [dynamic]T {
|
// result across adds
|
||||||
return storage.dense
|
slice :: proc(world: ^World, $T: typeid) -> [dynamic]T {
|
||||||
|
return get_storage(world, T).dense
|
||||||
}
|
}
|
||||||
|
|
||||||
// the entity ids backing the dense array, index-aligned with slice().
|
// the entity ids backing the dense array, index-aligned with slice().
|
||||||
// same invalidation caveat: adds can reallocate it
|
// same invalidation caveat: adds can reallocate it
|
||||||
slice_entity :: proc(storage: ^Storage($T)) -> [dynamic]Entity {
|
slice_entity :: proc(world: ^World, $T: typeid) -> [dynamic]Entity {
|
||||||
return storage.entities
|
return get_storage(world, T).entities
|
||||||
}
|
}
|
||||||
|
|
||||||
// ^T of the storage's first entry plus the entity that owns it — handy for
|
// ^T of the world's first entry of that component plus the entity that owns
|
||||||
// singletons like Window. panics if the storage is empty
|
// it, handy for singletons like WindowComponent. panics if no entity has
|
||||||
single :: proc(storage: ^Storage($T)) -> (^T, Entity) {
|
// the component yet
|
||||||
|
single :: proc(world: ^World, $T: typeid) -> (^T, Entity) {
|
||||||
|
storage := get_storage(world, T)
|
||||||
return &storage.dense[0], storage.entities[0]
|
return &storage.dense[0], storage.entities[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
// a pointer to that entity's component, looked up through the sparse map.
|
// a pointer to that entity's component, looked up through the sparse map.
|
||||||
// no existence check: an entity that was never added to this storage reads
|
// no existence check: an entity that was never added to this storage reads
|
||||||
// back as dense[0], and an empty storage panics
|
// back as dense[0], and a world where nobody has the component panics
|
||||||
get_component :: proc(entity: Entity, storage: ^Storage($T)) -> ^T {
|
get_component :: proc(world: ^World, entity: Entity, $T: typeid) -> ^T {
|
||||||
|
storage := get_storage(world, T)
|
||||||
return &storage.dense[storage.sparse[entity]]
|
return &storage.dense[storage.sparse[entity]]
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-6
@@ -3,7 +3,7 @@ package shoal
|
|||||||
// per-type signal bus, created on first use.
|
// per-type signal bus, created on first use.
|
||||||
// one system writes a signal with data, any number of other systems read it
|
// one system writes a signal with data, any number of other systems read it
|
||||||
// after. same-frame reads only work until signal_system clears the buses at
|
// after. same-frame reads only work until signal_system clears the buses at
|
||||||
// POST_UPDATE — see read/signal_system below for the details.
|
// POST_UPDATE, see read and signal_system below
|
||||||
Bus :: struct($T: typeid) {
|
Bus :: struct($T: typeid) {
|
||||||
pending: [dynamic]T,
|
pending: [dynamic]T,
|
||||||
}
|
}
|
||||||
@@ -40,7 +40,7 @@ clear_bus :: proc($T: typeid) -> proc(b: rawptr) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// queue a signal, readable until signal_system clears it at POST_UPDATE.
|
// queue a signal, readable until signal_system clears it at POST_UPDATE.
|
||||||
// note POST_UPDATE runs before PRE_DRAW/DRAW, so a reader sees exactly what's
|
// POST_UPDATE runs before PRE_DRAW/DRAW, so a reader only sees what's
|
||||||
// been written since the last POST_UPDATE: UPDATE systems see same-frame
|
// been written since the last POST_UPDATE: UPDATE systems see same-frame
|
||||||
// writes, but a DRAW system only sees writes made after this frame's
|
// writes, but a DRAW system only sees writes made after this frame's
|
||||||
// POST_UPDATE (by PRE_DRAW or earlier DRAW systems)
|
// POST_UPDATE (by PRE_DRAW or earlier DRAW systems)
|
||||||
@@ -63,8 +63,8 @@ write_with_data :: proc(data: $T) {
|
|||||||
// every signal of type T written since the last POST_UPDATE, non-consuming,
|
// every signal of type T written since the last POST_UPDATE, non-consuming,
|
||||||
// empty if none. iterate it: for s in read(MySignal)
|
// empty if none. iterate it: for s in read(MySignal)
|
||||||
// copied into context.temp_allocator so write() during iteration can't
|
// copied into context.temp_allocator so write() during iteration can't
|
||||||
// reallocate the pending buffer out from under you — but that means the result
|
// reallocate the pending buffer out from under you. that also means the
|
||||||
// dies with the frame's temp arena, don't hold onto it past that
|
// result dies with the frame's temp arena, don't hold onto it past that
|
||||||
read :: proc($T: typeid) -> []T {
|
read :: proc($T: typeid) -> []T {
|
||||||
bus := get_bus(T)
|
bus := get_bus(T)
|
||||||
if len(bus.pending) == 0 {return nil}
|
if len(bus.pending) == 0 {return nil}
|
||||||
@@ -76,8 +76,8 @@ read :: proc($T: typeid) -> []T {
|
|||||||
// runs at POST_UPDATE, clears every bus so the next frame starts fresh.
|
// runs at POST_UPDATE, clears every bus so the next frame starts fresh.
|
||||||
// writes made after it (e.g. in DRAW) survive until the *next* frame's
|
// writes made after it (e.g. in DRAW) survive until the *next* frame's
|
||||||
// POST_UPDATE, so the rest of this frame and the next frame's
|
// POST_UPDATE, so the rest of this frame and the next frame's
|
||||||
// PRE_UPDATE/UPDATE systems still read them — but the next frame's DRAW
|
// PRE_UPDATE/UPDATE systems still read them. the next frame's DRAW
|
||||||
// never does, POST_UPDATE gets there first
|
// never does though, POST_UPDATE gets there first
|
||||||
signal_system :: proc() {
|
signal_system :: proc() {
|
||||||
for _, e in buses {
|
for _, e in buses {
|
||||||
e.clear(e.bus)
|
e.clear(e.bus)
|
||||||
|
|||||||
+22
-19
@@ -5,53 +5,56 @@ import rl "vendor:raylib"
|
|||||||
|
|
||||||
// shoal's out-of-the-box behavior. registers, in frame order:
|
// shoal's out-of-the-box behavior. registers, in frame order:
|
||||||
//
|
//
|
||||||
// STARTUP startup_window (InitWindow from the Window component, 60 fps cap)
|
// STARTUP startup_window (InitWindow from the WindowComponent, 60 fps cap)
|
||||||
// UPDATE run_timers, update_cursor_position
|
// UPDATE run_timers, update_cursor_position
|
||||||
// POST_UPDATE signal_system (clears every signal bus)
|
// POST_UPDATE signal_system (clears every signal bus)
|
||||||
// PRE_DRAW BeginDrawing + ClearBackground(window.background)
|
// PRE_DRAW BeginDrawing + ClearBackground(window.background)
|
||||||
// DRAW draw_sprite (each Sprite drawn at its Transform)
|
// DRAW draw_sprite (each SpriteComponent drawn at its TransformComponent)
|
||||||
// POST_DRAW EndDrawing
|
// POST_DRAW EndDrawing
|
||||||
// DESTROY CloseWindow
|
// DESTROY CloseWindow
|
||||||
//
|
//
|
||||||
// and spawns two entities: a display holding the default WindowComponent
|
// and spawns two entities: a display holding the default WindowComponent
|
||||||
// (800x600, "shoal window", black background) and a cursor holding a Cursor
|
// (800x600, "shoal window", black background) and a cursor holding a
|
||||||
// component. add it before your own systems if you want theirs to run first —
|
// CursorComponent. add it before your own systems if you want theirs to run
|
||||||
// order within a schedule is registration order
|
// first; order within a schedule is registration order
|
||||||
DefaultPlugin :: proc(app: ^App) {
|
DefaultPlugin :: proc(app: ^App) {
|
||||||
add_system(app, STARTUP, startup_window)
|
add_system(app, STARTUP, startup_window)
|
||||||
add_system(app, DRAW, draw_sprite)
|
add_system(app, DRAW, draw_sprite)
|
||||||
add_system(app, POST_UPDATE, signal_system)
|
add_system(app, POST_UPDATE, signal_system)
|
||||||
add_system(app, UPDATE, run_timers)
|
add_system(app, UPDATE, run_timers)
|
||||||
add_system(app, UPDATE, update_cursor_position)
|
add_system(app, UPDATE, update_cursor_position)
|
||||||
add_system(app, PRE_DRAW, proc() {rl.BeginDrawing()
|
add_system(app, PRE_DRAW, proc(world: ^World) {rl.BeginDrawing()
|
||||||
window, _ := single(&Window)
|
window, _ := single(world, WindowComponent)
|
||||||
rl.ClearBackground(window.background)})
|
rl.ClearBackground(window.background)})
|
||||||
add_system(app, POST_DRAW, proc() {rl.EndDrawing()})
|
add_system(app, POST_DRAW, proc() {rl.EndDrawing()})
|
||||||
add_system(app, DESTROY, proc() {rl.CloseWindow()})
|
add_system(app, DESTROY, proc() {rl.CloseWindow()})
|
||||||
|
|
||||||
display := spawn_entity(app)
|
// a plugin is one of the few callers that holds an ^App, so it can reach
|
||||||
|
// the world through it and set up entities before the frame loop starts
|
||||||
|
world := get_world(app)
|
||||||
|
display := spawn_entity(world)
|
||||||
add_component(
|
add_component(
|
||||||
|
world,
|
||||||
display,
|
display,
|
||||||
&Window,
|
|
||||||
WindowComponent{width = 800, height = 600, title = "shoal window", background = rl.BLACK},
|
WindowComponent{width = 800, height = 600, title = "shoal window", background = rl.BLACK},
|
||||||
)
|
)
|
||||||
|
|
||||||
cursor := spawn_entity(app)
|
cursor := spawn_entity(world)
|
||||||
add_component(cursor, &Cursor)
|
add_component(world, cursor, CursorComponent)
|
||||||
}
|
}
|
||||||
|
|
||||||
@(private)
|
@(private)
|
||||||
update_cursor_position :: proc() {
|
update_cursor_position :: proc(world: ^World) {
|
||||||
cursor, _ := single(&Cursor)
|
cursor, _ := single(world, CursorComponent)
|
||||||
cursor.position = rl.GetMousePosition()
|
cursor.position = rl.GetMousePosition()
|
||||||
}
|
}
|
||||||
|
|
||||||
@(private)
|
@(private)
|
||||||
draw_sprite :: proc() {
|
draw_sprite :: proc(world: ^World) {
|
||||||
components := slice(&Sprite)
|
components := slice(world, SpriteComponent)
|
||||||
entities := slice_entity(&Sprite)
|
entities := slice_entity(world, SpriteComponent)
|
||||||
for &v, i in components {
|
for &v, i in components {
|
||||||
sprite_transform := get_component(entities[i], &Transform)
|
sprite_transform := get_component(world, entities[i], TransformComponent)
|
||||||
rl.DrawTexture(
|
rl.DrawTexture(
|
||||||
v.texture,
|
v.texture,
|
||||||
i32(sprite_transform.translation.x),
|
i32(sprite_transform.translation.x),
|
||||||
@@ -62,8 +65,8 @@ draw_sprite :: proc() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@(private)
|
@(private)
|
||||||
startup_window :: proc() {
|
startup_window :: proc(world: ^World) {
|
||||||
window, _ := single(&Window)
|
window, _ := single(world, WindowComponent)
|
||||||
rl.InitWindow(window.width, window.height, strings.clone_to_cstring(window.title))
|
rl.InitWindow(window.width, window.height, strings.clone_to_cstring(window.title))
|
||||||
rl.SetTargetFPS(60)
|
rl.SetTargetFPS(60)
|
||||||
}
|
}
|
||||||
|
|||||||
+30
-1
@@ -3,14 +3,29 @@ package shoal
|
|||||||
import "core:fmt"
|
import "core:fmt"
|
||||||
import rl "vendor:raylib"
|
import rl "vendor:raylib"
|
||||||
|
|
||||||
|
// a named countdown: how many seconds are left (lifetime) and the name it
|
||||||
|
// fires under (also the key it's stored under)
|
||||||
Timer :: struct {
|
Timer :: struct {
|
||||||
lifetime: f32,
|
lifetime: f32,
|
||||||
signal: string,
|
signal: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// queued when a timer runs out, for systems to pick up with
|
||||||
|
// read(TimerFired). run_timers below has the current caveat
|
||||||
|
TimerFired :: struct {
|
||||||
|
name: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
// timers stay in a package global for now, deliberately outside the
|
||||||
|
// World: they're keyed by name, not owned by an entity, so there's nothing
|
||||||
|
// per-world to hang them on yet. same goes for the signal buses and the
|
||||||
|
// (wip) tween queue in their own files
|
||||||
@(private)
|
@(private)
|
||||||
timers: map[string]Timer
|
timers: map[string]Timer
|
||||||
|
|
||||||
|
// start (or restart) a timer under this name. a name that's already running
|
||||||
|
// gets overwritten. it starts ticking on the next UPDATE, where run_timers
|
||||||
|
// counts it down
|
||||||
start_timer :: proc(time: f32, signal: string) {
|
start_timer :: proc(time: f32, signal: string) {
|
||||||
new_timer := Timer {
|
new_timer := Timer {
|
||||||
lifetime = time,
|
lifetime = time,
|
||||||
@@ -19,6 +34,9 @@ start_timer :: proc(time: f32, signal: string) {
|
|||||||
timers[signal] = new_timer
|
timers[signal] = new_timer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// the timer stored under this name, or a zero Timer if there is none. an
|
||||||
|
// expired timer is reset to Timer{} too, so lifetime != 0 means "still
|
||||||
|
// ticking" and lifetime <= 0 means "done (or never started)"
|
||||||
get_timer :: proc(signal: string) -> Timer {
|
get_timer :: proc(signal: string) -> Timer {
|
||||||
if timer, exists := timers[signal]; exists {
|
if timer, exists := timers[signal]; exists {
|
||||||
return timers[signal]
|
return timers[signal]
|
||||||
@@ -26,13 +44,24 @@ get_timer :: proc(signal: string) -> Timer {
|
|||||||
return Timer{}
|
return Timer{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// registered at UPDATE by DefaultPlugin: ticks every timer down by the frame
|
||||||
|
// time, and when the remaining lifetime drops to roughly one frame's delta it
|
||||||
|
// resets its entry to Timer{} so get_timer reads it back zeroed. it compares
|
||||||
|
// against a fresh GetFrameTime() call, so it can reset a frame or two
|
||||||
|
// early. it's meant to then queue TimerFired{name = <timer name>} for
|
||||||
|
// read(TimerFired). like any signal, it's readable only until signal_system
|
||||||
|
// clears the buses at POST_UPDATE, see signals.odin for how that lands per
|
||||||
|
// schedule.
|
||||||
|
// known bug: the reset writes the same map slot t points at, so t.signal is
|
||||||
|
// already "" when it's checked just after and nothing gets queued, so
|
||||||
|
// TimerFired doesn't actually fire yet (the timers themselves work fine)
|
||||||
run_timers :: proc() {
|
run_timers :: proc() {
|
||||||
for i, &t in timers {
|
for i, &t in timers {
|
||||||
t.lifetime -= rl.GetFrameTime()
|
t.lifetime -= rl.GetFrameTime()
|
||||||
if t.lifetime <= rl.GetFrameTime() {
|
if t.lifetime <= rl.GetFrameTime() {
|
||||||
timers[i] = Timer{}
|
timers[i] = Timer{}
|
||||||
if t.signal != "" {
|
if t.signal != "" {
|
||||||
write(t.signal)
|
write(TimerFired{name = t.signal})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user