update docs
This commit is contained in:
@@ -29,7 +29,9 @@ import ecs "shoal/src"
|
|||||||
```
|
```
|
||||||
|
|
||||||
## usage
|
## usage
|
||||||
first create a new app struct. this is where your systems and plugins live.
|
first create a new app struct. it holds your systems and plugins, and tucked
|
||||||
|
inside it a `World` holding every entity and component (your game data).
|
||||||
|
systems never see the app itself, only the world
|
||||||
|
|
||||||
```odin
|
```odin
|
||||||
import ecs "shoal/src"
|
import ecs "shoal/src"
|
||||||
@@ -37,7 +39,7 @@ import ecs "shoal/src"
|
|||||||
app: ecs.App
|
app: ecs.App
|
||||||
```
|
```
|
||||||
|
|
||||||
you can run your app with `run()`. run takes a reference to your app to run it's systems.
|
you can run your app with `run()`. run takes a pointer to your app and drives its systems.
|
||||||
|
|
||||||
```odin
|
```odin
|
||||||
main :: proc() {
|
main :: proc() {
|
||||||
@@ -48,21 +50,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 procedure runs\
|
a schedule determines when a procedure runs\
|
||||||
there are 8 of them, and `run()` walks them in this order:
|
there are 8 of them, and `run()` walks them in this order:
|
||||||
|
|
||||||
- `STARTUP` — once, before the first frame
|
- `STARTUP`: once, before the first frame
|
||||||
- `PRE_UPDATE` — every frame, first thing
|
- `PRE_UPDATE`: every frame, first thing
|
||||||
- `UPDATE` — every frame, your main logic
|
- `UPDATE`: every frame, your main logic
|
||||||
- `POST_UPDATE` — every frame, last stop before drawing (`signal_system` clears the signal buses here)
|
- `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)
|
- `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
|
- `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)
|
- `POST_DRAW`: every frame, closes the raylib frame (`DefaultPlugin` calls `EndDrawing` here)
|
||||||
- `DESTROY` — once, after the loop exits (`DefaultPlugin` calls `CloseWindow` 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.
|
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).
|
systems registered on the same schedule run in registration order: whoever was `add_system()`ed first runs first, including the systems your plugins add. plugins themselves always run once before *all* the `STARTUP` systems, no matter when you `add_plugin()` them, so adding `add_plugin(&app, ecs.DefaultPlugin)` first doesn't change *when* DefaultPlugin runs. it just puts shoal's defaults at the front of each schedule, which is still a good habit.
|
||||||
|
|
||||||
```odin
|
```odin
|
||||||
main :: proc() {
|
main :: proc() {
|
||||||
@@ -81,28 +83,38 @@ example_update :: proc() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
a system can also take the world. declare it as `proc(world: ^ecs.World)` and `run()` hands it the app's world, the same one holding every entity and component. that's all a system ever gets: systems structurally can't reach the app itself (add_system, plugins, run); only plugins and `main()` ever hold an `^App`
|
||||||
|
|
||||||
|
```odin
|
||||||
|
main :: proc() {
|
||||||
|
ecs.add_system(&app, ecs.STARTUP, example_spawn)
|
||||||
|
}
|
||||||
|
|
||||||
|
example_spawn :: proc(world: ^ecs.World) {
|
||||||
|
entity := ecs.spawn_entity(world) // the world, no global needed
|
||||||
|
ecs.add_component(world, entity, ExampleComponent{...})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
plain `proc()` systems work exactly as before. only ask for the world parameter when you actually need it
|
||||||
|
|
||||||
### components
|
### components
|
||||||
components are just structs with data, and kept with other components of that type in `Storage`.\
|
components are plain structs. declare one and you're done. the data lives in the world: every component type gets a storage inside it, created automatically the first time you add that component to an entity. there's no global to declare and no registration step
|
||||||
create a new component
|
|
||||||
|
|
||||||
```odin
|
```odin
|
||||||
ExampleComponent :: struct {}
|
ExampleComponent :: struct {}
|
||||||
```
|
```
|
||||||
|
|
||||||
and create it's storage
|
shoal ships a few built-ins already declared: `WindowComponent`, `CursorComponent`, `SpriteComponent` and `TransformComponent`. `DefaultPlugin` reads and writes them through the world: it opens the window from `WindowComponent`, keeps the `CursorComponent` entity on the mouse, and draws every `SpriteComponent` at its `TransformComponent`.
|
||||||
|
|
||||||
```odin
|
each world owns its own storages, keyed by the component's type: two worlds never share data, and a zero `ecs.World{}` is ready to use.
|
||||||
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()`, which hands out the next id from the world's entity list. ids are never reused. systems get the world as a parameter; in `main()` or a plugin it's `&app.world` (or `ecs.get_world(app)`)
|
||||||
|
|
||||||
```odin
|
```odin
|
||||||
main :: proc() {
|
example_spawn :: proc(world: ^ecs.World) {
|
||||||
entity := ecs.spawn_entity(&app)
|
entity := ecs.spawn_entity(world)
|
||||||
}
|
}
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -110,58 +122,66 @@ main :: proc() {
|
|||||||
and give it new components with `add_component()`
|
and give it new components with `add_component()`
|
||||||
|
|
||||||
```odin
|
```odin
|
||||||
main :: proc() {
|
example_spawn :: proc(world: ^ecs.World) {
|
||||||
ecs.add_component(entity, &Example) // make sure to pass a reference to the storage, not the component type
|
entity := ecs.spawn_entity(world)
|
||||||
|
ecs.add_component(world, entity, ExampleComponent) // just the type: a zero-initialized component
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
or fill it in on the spot with the data overload
|
or fill it in on the spot by passing data instead
|
||||||
|
|
||||||
```odin
|
```odin
|
||||||
main :: proc() {
|
example_spawn :: proc(world: ^ecs.World) {
|
||||||
ecs.add_component(entity, &Example, ExampleComponent{...})
|
entity := ecs.spawn_entity(world)
|
||||||
|
ecs.add_component(world, entity, ExampleComponent{...})
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
either way the world's storage for that component type is created on the spot if this is the first one
|
||||||
|
|
||||||
### query
|
### query
|
||||||
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]`
|
since a world keeps components of one type together, you can loop through them and pick out what you like. `slice()` and `slice_entity()` pair up by index: `entities[i]` owns `components[i]`
|
||||||
|
|
||||||
```odin
|
```odin
|
||||||
main :: proc() {
|
example_system :: proc(world: ^ecs.World) {
|
||||||
components := ecs.slice(&Example)
|
components := ecs.slice(world, ExampleComponent)
|
||||||
entities := ecs.slice_entity(&Example)
|
entities := ecs.slice_entity(world, ExampleComponent)
|
||||||
for v, i in components {
|
for v, i in components {
|
||||||
e := entities[i] // get entity that owns the component
|
e := entities[i] // get entity that owns the component
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
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.
|
one caveat: `slice()` hands back the live array inside the world, 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 only one entity has the component use `single()`
|
||||||
|
|
||||||
```odin
|
```odin
|
||||||
main :: proc() {
|
example_system :: proc(world: ^ecs.World) {
|
||||||
component, entity_id := ecs.single(&Example)
|
component, entity_id := ecs.single(world, ExampleComponent)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
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)
|
and once you have an entity id, `get_component()` reaches into any component type to get that entity's copy, handy for components that live alongside another one (a sprite's `TransformComponent`, say)
|
||||||
|
|
||||||
```odin
|
```odin
|
||||||
main :: proc() {
|
example_system :: proc(world: ^ecs.World) {
|
||||||
component, entity_id := ecs.single(&Example)
|
component, entity_id := ecs.single(world, ExampleComponent)
|
||||||
other := ecs.get_component(entity_id, &OtherExample)
|
other := ecs.get_component(world, entity_id, OtherExampleComponent)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
no existence check though: only ask for entities you know are in that storage.
|
no existence check though: only ask for entities you know have that component.
|
||||||
|
|
||||||
### 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 a procedure that takes `^App` as a parameter, the one bit of privilege over regular systems, which only ever see `^World`. a plugin can register systems and plugins on the app the same way main() does, and reach the game data through `get_world(app)`
|
||||||
|
|
||||||
```odin
|
```odin
|
||||||
ExamplePlugin :: proc(app: ^App) {
|
ExamplePlugin :: proc(app: ^App) {
|
||||||
ecs.add_system(app, ecs.STARTUP, some_plugin_system)
|
ecs.add_system(app, ecs.STARTUP, some_plugin_system)
|
||||||
|
|
||||||
|
entity := ecs.spawn_entity(ecs.get_world(app)) // same world the systems will get
|
||||||
|
ecs.add_component(ecs.get_world(app), entity, ExampleComponent{...})
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -172,12 +192,12 @@ 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 reusable. much of shoal's default behavior comes 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
|
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)
|
||||||
|
|
||||||
```odin
|
```odin
|
||||||
DamageEvent :: struct { amount: int }
|
DamageEvent :: struct { amount: int }
|
||||||
@@ -200,14 +220,14 @@ check_damage :: proc() {
|
|||||||
|
|
||||||
you can also write an empty payload with the bare type: `ecs.write(DamageEvent)`
|
you can also write an empty payload with the bare type: `ecs.write(DamageEvent)`
|
||||||
|
|
||||||
scheduling caveat: every system sees exactly the signals written since the last `signal_system()` run, and that runs at POST_UPDATE — *before* PRE_DRAW/DRAW. so:
|
scheduling caveat: every system sees exactly the signals written since the last `signal_system()` run, and that runs at POST_UPDATE, *before* PRE_DRAW/DRAW. so:
|
||||||
|
|
||||||
- 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. the next frame's DRAW never sees them either
|
||||||
|
|
||||||
### timers
|
### 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"
|
timers are named countdowns. they live in a package global for now, deliberately outside the world, since they're keyed by name rather than owned by an entity. `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
|
```odin
|
||||||
example_system :: proc() {
|
example_system :: proc() {
|
||||||
@@ -216,9 +236,9 @@ example_system :: proc() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
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.
|
timers tick down every frame in `run_timers`, which `DefaultPlugin` registers at UPDATE. the reset fires when the remaining lifetime drops to roughly one frame's delta (it compares against a fresh `GetFrameTime()` call, so it can reset a frame or two early) and resets the entry 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
|
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
|
### ui
|
||||||
`shoal/ui` wraps microui with its own plugin. add it before any system that wants to draw ui
|
`shoal/ui` wraps microui with its own plugin. add it before any system that wants to draw ui
|
||||||
@@ -247,4 +267,4 @@ my_ui :: proc() {
|
|||||||
```
|
```
|
||||||
|
|
||||||
### tween
|
### 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.
|
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.
|
||||||
|
|||||||
Reference in New Issue
Block a user