1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
|
# raylib-jamstack — Flecs (ECS) Ruby API Specification (`Flecs::`)
The optional **Entity Component System** layer.
[flecs](https://github.com/SanderMertens/flecs) is a fast C/C++ ECS. These
bindings are hand-written and modeled on flecs' official
[Lua binding](https://github.com/flecs-hub/flecs-lua): both embed flecs' C API in
a dynamically-typed scripting VM, so the same approach applies.
This follows the same design rules as `API_SPEC.md` (snake_case, `?` predicates,
`=` setters, keyword args, blocks). Using the ECS is optional — game architecture
is up to the author.
## Mental model
- **World** (`Flecs::World`) owns everything; one per game (you may have more).
- **Components** are *real C structs* declared at runtime from a **meta
descriptor** string. Their values are (de)serialized to/from Ruby **Hashes** —
there is no per-component Ruby class.
- **Entities** are integer ids (wrapped in `Flecs::Entity`) with components/tags.
- **Systems** are Ruby blocks that run over matching entities every `progress`.
The key design choice (from flecs-lua): components use flecs' **meta/reflection
addon**, so `world.struct("Position", "{float x; float y;}")` registers a struct
with a known memory layout, and the binding moves data between that C memory and
Ruby Hashes. This needs no C codegen per component and works identically on
desktop and web.
---
## 1. World
```ruby
world = Flecs::World.new # wraps an ecs_world_t (freed automatically by GC)
world.progress(dt = 0.0) # -> Boolean; advance one step, run all systems.
# returns false when the world wants to quit.
```
`progress` is the ECS analogue of a frame tick. Typical integration with the
raylib loop:
```ruby
Rl.while_window_open do
world.progress(Rl.frame_time)
Rl.draw(clear_color: Rl::RAYWHITE) { ... } # systems may also do the drawing
end
```
> **Single-threaded only.** Multithreaded systems (`ecs_set_threads`) are not
> exposed; `progress` runs systems on the calling thread. This is also the only
> mode valid on the wasm/web build, so behaviour is identical across targets.
---
## 2. Components (meta structs)
Declare a component from a C-struct **descriptor string**. Returns a
`Flecs::Component` (usable anywhere an id is expected).
```ruby
pos = world.struct("Position", "{float x; float y;}")
vel = world.struct("Velocity", "{float x; float y;}")
world.component(...) # alias of struct
```
Supported member types in the descriptor:
| descriptor type | Ruby value |
|----------------------------|-----------------|
| `bool` | `true`/`false` |
| `char`, `i8/i16/i32/i64` | `Integer` |
| `u8/u16/u32/u64`, `byte` | `Integer` |
| `uptr`, `iptr` | `Integer` |
| `f32` (`float`), `f64` (`double`) | `Float` |
| `string` (`char*`) | `String` |
| `entity` | `Integer` (id) |
| nested `{...}` struct | nested `Hash` |
| inline array `T[N]` | `Array` |
```ruby
# nested + arrays
tf = world.struct("Transform", "{ {float x; float y;} pos; float scale; }")
```
A **tag** is a dataless id (no struct):
```ruby
npc = world.tag("Npc") # -> Flecs::Component (usable with add/remove/has?)
```
---
## 3. Entities
```ruby
e = world.entity("player") # named (name optional)
e = world.entity # anonymous
world.lookup("player") # -> Flecs::Entity | nil
world.entity_for(id) # wrap a raw id (e.g. from a system block)
```
`Flecs::Entity`:
```ruby
e.id ; e.to_i # the integer id
e.name ; e.name = "p2" # get/set name
e.alive? # still alive?
e.delete # destroy
# components & tags
e.set(pos, x: 1.0, y: 2.0) # kwargs ...
e.set(pos, {x: 1, y: 2}) # ... or an explicit Hash
e.get(pos) # -> {x: 1.0, y: 2.0} | nil
e.add(npc) ; e.remove(npc) # tags or components (add with default value)
e.has?(npc) # -> Boolean
e.set(pos, x: 0, y: 0).add(npc) # chainable; returns self
```
> `set`/`get`/`add`/`remove`/`has?` all accept a `Flecs::Component`, a
> `Flecs::Entity`, or a raw integer id (anything responding to `to_i`).
---
## 4. Systems
Register a system that runs every `progress` during a **phase**. The block is
invoked once per matched entity with `|entity_id, *component_hashes|`, where the
component hashes are in the order of `with:`. **Mutations to those hashes are
written back** into component memory after the block returns.
```ruby
world.system("Move", with: [pos, vel]) do |id, p, v|
p[:x] += v[:x]
p[:y] += v[:y]
end
world.system("Despawn", with: [pos], phase: Flecs::PRE_UPDATE) do |id, p|
world.entity_for(id).delete if p[:y] > 1000
end
```
Phases (run in this order each `progress`):
| constant | flecs phase |
|---------------------|---------------|
| `Flecs::ON_LOAD` | `EcsOnLoad` |
| `Flecs::PRE_UPDATE` | `EcsPreUpdate`|
| `Flecs::ON_UPDATE` | `EcsOnUpdate` (default) |
| `Flecs::ON_START` | `EcsOnStart` |
> The entity is passed as a raw **Integer id** (not a `Flecs::Entity`) to avoid
> allocating a wrapper per entity per frame. Wrap it with `world.entity_for(id)`
> when you need entity methods. Tag terms in `with:` yield `nil` for their slot.
---
## 5. Queries
Ad-hoc, cached queries over a set of components/tags. `Flecs::Query` is
`Enumerable`; `each` has the same `|id, *comps|` + writeback semantics as systems.
```ruby
q = world.query(pos, vel) # build once, reuse across frames
q.each { |id, p, v| ... }
world.query(pos).each { |id, p| puts "#{id}: #{p}" }
```
---
## 6. Worked example
```ruby
world = Flecs::World.new
pos = world.struct("Position", "{float x; float y;}")
vel = world.struct("Velocity", "{float x; float y;}")
100.times do |i|
world.entity.set(pos, x: i.to_f, y: 0.0).set(vel, x: 0.0, y: 1.0)
end
world.system("Gravity", with: [vel]) { |id, v| v[:y] += 0.5 }
world.system("Move", with: [pos, vel]) do |id, p, v|
p[:x] += v[:x]; p[:y] += v[:y]
end
Rl.init_window(800, 600, "ecs")
Rl.while_window_open do
world.progress(Rl.frame_time)
Rl.draw(clear_color: Rl::BLACK) do
world.query(pos).each { |id, p| Rl.draw_circle(p[:x].to_i, p[:y].to_i, 3, Rl::RAYWHITE) }
end
end
```
---
## 7. Web (wasm)
flecs — including the meta/reflection addon used for components — compiles and
runs under emscripten unchanged. The only build difference is the wasm link uses
`-sSTACK_SIZE=4MB`, because flecs' init/meta needs more than emscripten's 64 KB
default stack (otherwise you get a wasm `memory access out of bounds` trap). The
API and behaviour are identical to desktop.
---
## 8. Limitations / not yet exposed
These are deliberately omitted from the first cut (the underlying C API supports
them; bind them as needed):
- Multithreaded systems / staging (`ecs_set_threads`).
- Relationships / pairs, prefabs, hierarchies (`ChildOf`, `IsA`).
- Observers (event-driven callbacks), query filter operators (`Not`, `Or`,
optional, `inout` modifiers) — `with:` is plain "must have all" matching.
- The REST/Explorer remote UI.
- Enum/bitmask component members (structs, primitives, nested structs, and inline
arrays are supported).
|