rgame 0.4.0-x86_64-linux-gnu

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (239) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +251 -0
  3. data/LICENSE +26 -0
  4. data/README.md +325 -0
  5. data/docs/api/README.md +224 -0
  6. data/docs/api/app.md +249 -0
  7. data/docs/api/assets.md +463 -0
  8. data/docs/api/audio.md +249 -0
  9. data/docs/api/cli.md +259 -0
  10. data/docs/api/components.md +1112 -0
  11. data/docs/api/drawing.md +398 -0
  12. data/docs/api/examples.md +263 -0
  13. data/docs/api/game.md +219 -0
  14. data/docs/api/images.md +118 -0
  15. data/docs/api/input.md +476 -0
  16. data/docs/api/internals.md +289 -0
  17. data/docs/api/localization.md +285 -0
  18. data/docs/api/scene_graph.md +539 -0
  19. data/docs/api/signals.md +147 -0
  20. data/docs/api/systems.md +231 -0
  21. data/docs/api/text.md +133 -0
  22. data/docs/api/tile_maps.md +237 -0
  23. data/docs/api/toolbox.md +493 -0
  24. data/docs/api/ui.md +927 -0
  25. data/docs/api/values.md +316 -0
  26. data/examples/assets/README.md +322 -0
  27. data/examples/assets/blip.ogg +0 -0
  28. data/examples/assets/glyphs.json +5 -0
  29. data/examples/assets/glyphs.png +0 -0
  30. data/examples/assets/hero.json +12 -0
  31. data/examples/assets/hero.png +0 -0
  32. data/examples/assets/icons.json +13 -0
  33. data/examples/assets/icons.png +0 -0
  34. data/examples/assets/music.ogg +0 -0
  35. data/examples/assets/skills.json +10 -0
  36. data/examples/assets/skills.png +0 -0
  37. data/examples/assets/tileset.png +0 -0
  38. data/examples/assets/tileset.tsx +65 -0
  39. data/examples/assets/town.tmx +26 -0
  40. data/examples/assets/ui.json +11 -0
  41. data/examples/assets/ui.png +0 -0
  42. data/examples/collision/locales/en.yml +8 -0
  43. data/examples/collision/main.rb +316 -0
  44. data/examples/collision_tiles/locales/en.yml +9 -0
  45. data/examples/collision_tiles/main.rb +274 -0
  46. data/examples/fullscreen/locales/en.yml +10 -0
  47. data/examples/fullscreen/main.rb +216 -0
  48. data/examples/game_menu/locales/en.yml +8 -0
  49. data/examples/game_menu/main.rb +170 -0
  50. data/examples/input_glyphs/locales/en.yml +14 -0
  51. data/examples/input_glyphs/main.rb +213 -0
  52. data/examples/jump_topdown/locales/en.yml +9 -0
  53. data/examples/jump_topdown/main.rb +178 -0
  54. data/examples/localization/locales/de.yml +12 -0
  55. data/examples/localization/locales/en.yml +13 -0
  56. data/examples/localization/main.rb +158 -0
  57. data/examples/menu_navigation/locales/en.yml +23 -0
  58. data/examples/menu_navigation/main.rb +365 -0
  59. data/examples/music/locales/en.yml +7 -0
  60. data/examples/music/main.rb +134 -0
  61. data/examples/pathfinding/locales/en.yml +17 -0
  62. data/examples/pathfinding/main.rb +298 -0
  63. data/examples/pooling/locales/en.yml +7 -0
  64. data/examples/pooling/main.rb +259 -0
  65. data/examples/quick_wheel/locales/en.yml +16 -0
  66. data/examples/quick_wheel/main.rb +184 -0
  67. data/examples/radial_menu/locales/en.yml +16 -0
  68. data/examples/radial_menu/main.rb +184 -0
  69. data/examples/save_load/locales/en.yml +11 -0
  70. data/examples/save_load/main.rb +207 -0
  71. data/examples/save_load_ids/locales/en.yml +11 -0
  72. data/examples/save_load_ids/main.rb +322 -0
  73. data/examples/scroll_map/locales/en.yml +4 -0
  74. data/examples/scroll_map/main.rb +140 -0
  75. data/examples/signals/locales/en.yml +6 -0
  76. data/examples/signals/main.rb +278 -0
  77. data/examples/skill_bar/locales/en.yml +14 -0
  78. data/examples/skill_bar/main.rb +159 -0
  79. data/examples/sound/locales/en.yml +6 -0
  80. data/examples/sound/main.rb +122 -0
  81. data/examples/split_screen/locales/en.yml +9 -0
  82. data/examples/split_screen/main.rb +304 -0
  83. data/examples/sprite/locales/en.yml +8 -0
  84. data/examples/sprite/main.rb +180 -0
  85. data/examples/timer/locales/en.yml +12 -0
  86. data/examples/timer/main.rb +273 -0
  87. data/examples/velocity/locales/en.yml +6 -0
  88. data/examples/velocity/main.rb +196 -0
  89. data/examples/walk/locales/en.yml +4 -0
  90. data/examples/walk/main.rb +99 -0
  91. data/exe/rgame +9 -0
  92. data/lib/rgame/boot.rb +3 -0
  93. data/lib/rgame/cli/new_project.rb +139 -0
  94. data/lib/rgame/cli/templates/Gemfile.tt +23 -0
  95. data/lib/rgame/cli/templates/README.md.tt +93 -0
  96. data/lib/rgame/cli/templates/Rakefile.tt +9 -0
  97. data/lib/rgame/cli/templates/assets/locales/en.yml.tt +10 -0
  98. data/lib/rgame/cli/templates/game.rb.tt +23 -0
  99. data/lib/rgame/cli/templates/gitignore.tt +12 -0
  100. data/lib/rgame/cli/templates/main.rb.tt +11 -0
  101. data/lib/rgame/cli/templates/nodes/root.rb.tt +24 -0
  102. data/lib/rgame/cli/templates/rspec.tt +2 -0
  103. data/lib/rgame/cli/templates/rubocop.yml.tt +75 -0
  104. data/lib/rgame/cli/templates/ruby-version.tt +1 -0
  105. data/lib/rgame/cli/templates/spec/locales_spec.rb.tt +18 -0
  106. data/lib/rgame/cli/templates/spec/nodes/root_spec.rb.tt +18 -0
  107. data/lib/rgame/cli/templates/spec/spec_helper.rb.tt +37 -0
  108. data/lib/rgame/cli.rb +66 -0
  109. data/lib/rgame/core/app.rb +46 -0
  110. data/lib/rgame/core/asset_manager.rb +206 -0
  111. data/lib/rgame/core/audio.rb +145 -0
  112. data/lib/rgame/core/font.rb +46 -0
  113. data/lib/rgame/core/gamepad.rb +55 -0
  114. data/lib/rgame/core/image.rb +55 -0
  115. data/lib/rgame/core/input.rb +71 -0
  116. data/lib/rgame/core/locale.rb +22 -0
  117. data/lib/rgame/core/nine_slice.rb +142 -0
  118. data/lib/rgame/core/recording.rb +54 -0
  119. data/lib/rgame/core/renderer.rb +354 -0
  120. data/lib/rgame/core/sprite_sheet.rb +105 -0
  121. data/lib/rgame/core/tile_map_renderer.rb +172 -0
  122. data/lib/rgame/core/ui_atlas.rb +101 -0
  123. data/lib/rgame/core/virtual_gamepad.rb +26 -0
  124. data/lib/rgame/core.rb +18 -0
  125. data/lib/rgame/core_ext.so +0 -0
  126. data/lib/rgame/engine/actor_blockers.rb +131 -0
  127. data/lib/rgame/engine/animation_set.rb +50 -0
  128. data/lib/rgame/engine/animator.rb +44 -0
  129. data/lib/rgame/engine/audio_bus.rb +24 -0
  130. data/lib/rgame/engine/audio_director.rb +59 -0
  131. data/lib/rgame/engine/bounds_blockers.rb +74 -0
  132. data/lib/rgame/engine/camera.rb +78 -0
  133. data/lib/rgame/engine/circle_collider.rb +34 -0
  134. data/lib/rgame/engine/collision_box.rb +59 -0
  135. data/lib/rgame/engine/collision_system.rb +132 -0
  136. data/lib/rgame/engine/component.rb +64 -0
  137. data/lib/rgame/engine/components/action_trigger.rb +40 -0
  138. data/lib/rgame/engine/components/animated_sprite.rb +71 -0
  139. data/lib/rgame/engine/components/box_collider.rb +99 -0
  140. data/lib/rgame/engine/components/camera_follow.rb +45 -0
  141. data/lib/rgame/engine/components/character_body.rb +50 -0
  142. data/lib/rgame/engine/components/circle_collider.rb +80 -0
  143. data/lib/rgame/engine/components/collision_world.rb +231 -0
  144. data/lib/rgame/engine/components/despawn_offscreen.rb +42 -0
  145. data/lib/rgame/engine/components/feet_collider.rb +61 -0
  146. data/lib/rgame/engine/components/hop.rb +76 -0
  147. data/lib/rgame/engine/components/identity.rb +73 -0
  148. data/lib/rgame/engine/components/mover.rb +285 -0
  149. data/lib/rgame/engine/components/navigator.rb +145 -0
  150. data/lib/rgame/engine/components/path_follow.rb +176 -0
  151. data/lib/rgame/engine/components/player_controller.rb +27 -0
  152. data/lib/rgame/engine/components/pool.rb +53 -0
  153. data/lib/rgame/engine/components/screen_wrap.rb +49 -0
  154. data/lib/rgame/engine/components/sprite.rb +47 -0
  155. data/lib/rgame/engine/components/targeting.rb +52 -0
  156. data/lib/rgame/engine/components/thrust_controller.rb +65 -0
  157. data/lib/rgame/engine/components/tile_world.rb +109 -0
  158. data/lib/rgame/engine/components/timer.rb +75 -0
  159. data/lib/rgame/engine/components/velocity.rb +43 -0
  160. data/lib/rgame/engine/components/wander_controller.rb +64 -0
  161. data/lib/rgame/engine/components/world.rb +133 -0
  162. data/lib/rgame/engine/contact_set.rb +74 -0
  163. data/lib/rgame/engine/culling.rb +45 -0
  164. data/lib/rgame/engine/debug_overlay.rb +111 -0
  165. data/lib/rgame/engine/i18n/plural.rb +45 -0
  166. data/lib/rgame/engine/i18n/plural_rules.rb +82 -0
  167. data/lib/rgame/engine/i18n/template.rb +59 -0
  168. data/lib/rgame/engine/i18n.rb +322 -0
  169. data/lib/rgame/engine/input/action_mapper.rb +100 -0
  170. data/lib/rgame/engine/input/actions.rb +92 -0
  171. data/lib/rgame/engine/input/input_map.rb +196 -0
  172. data/lib/rgame/engine/layout.rb +82 -0
  173. data/lib/rgame/engine/nav_grid.rb +87 -0
  174. data/lib/rgame/engine/node2d.rb +555 -0
  175. data/lib/rgame/engine/path.rb +76 -0
  176. data/lib/rgame/engine/player.rb +69 -0
  177. data/lib/rgame/engine/player_layer.rb +70 -0
  178. data/lib/rgame/engine/players.rb +205 -0
  179. data/lib/rgame/engine/pool.rb +51 -0
  180. data/lib/rgame/engine/presentation.rb +171 -0
  181. data/lib/rgame/engine/scene/scene_stack.rb +87 -0
  182. data/lib/rgame/engine/sealed_privates.rb +54 -0
  183. data/lib/rgame/engine/signal.rb +75 -0
  184. data/lib/rgame/engine/spatial_hash.rb +116 -0
  185. data/lib/rgame/engine/text.rb +194 -0
  186. data/lib/rgame/engine/tile_blockers.rb +63 -0
  187. data/lib/rgame/engine/tile_map.rb +147 -0
  188. data/lib/rgame/engine/tile_map_layer.rb +82 -0
  189. data/lib/rgame/engine/tileset.rb +99 -0
  190. data/lib/rgame/engine/timer.rb +51 -0
  191. data/lib/rgame/engine/ui/button.rb +248 -0
  192. data/lib/rgame/engine/ui/column.rb +20 -0
  193. data/lib/rgame/engine/ui/icon_button.rb +93 -0
  194. data/lib/rgame/engine/ui/menu.rb +290 -0
  195. data/lib/rgame/engine/ui/navigation.rb +57 -0
  196. data/lib/rgame/engine/ui/nine_slice_style.rb +50 -0
  197. data/lib/rgame/engine/ui/option_button.rb +163 -0
  198. data/lib/rgame/engine/ui/panel_button.rb +32 -0
  199. data/lib/rgame/engine/ui/panel_menu.rb +36 -0
  200. data/lib/rgame/engine/ui/pointing.rb +146 -0
  201. data/lib/rgame/engine/ui/radial_menu.rb +85 -0
  202. data/lib/rgame/engine/ui/ring.rb +55 -0
  203. data/lib/rgame/engine/ui/row.rb +21 -0
  204. data/lib/rgame/engine/ui/shape_style.rb +102 -0
  205. data/lib/rgame/engine/ui/stack.rb +58 -0
  206. data/lib/rgame/engine/ui/stepping.rb +93 -0
  207. data/lib/rgame/engine/ui/text_button.rb +59 -0
  208. data/lib/rgame/engine/view.rb +76 -0
  209. data/lib/rgame/engine/viewports.rb +171 -0
  210. data/lib/rgame/engine/world_view.rb +71 -0
  211. data/lib/rgame/engine.rb +86 -0
  212. data/lib/rgame/fonts/LiberationSans-Regular.ttf +0 -0
  213. data/lib/rgame/fonts/OFL.txt +102 -0
  214. data/lib/rgame/game.rb +271 -0
  215. data/lib/rgame/rubocop/cop/game/draw_in_local_space.rb +103 -0
  216. data/lib/rgame/rubocop/cop/game/hot_path.rb +36 -0
  217. data/lib/rgame/rubocop/cop/game/layer_boundary.rb +43 -0
  218. data/lib/rgame/rubocop/cop/game/no_core_in_engine_layer.rb +100 -0
  219. data/lib/rgame/rubocop/cop/game/no_engine_in_core_layer.rb +84 -0
  220. data/lib/rgame/rubocop/cop/game/no_interpolation_in_hot_path.rb +50 -0
  221. data/lib/rgame/rubocop/cop/game/no_literal_text.rb +41 -0
  222. data/lib/rgame/rubocop/cop/game/no_needless_allocation.rb +112 -0
  223. data/lib/rgame/rubocop/default.yml +39 -0
  224. data/lib/rgame/rubocop/plugin.rb +45 -0
  225. data/lib/rgame/rubocop.rb +11 -0
  226. data/lib/rgame/util/color.rb +23 -0
  227. data/lib/rgame/util/controls.rb +169 -0
  228. data/lib/rgame/util/route_search.rb +27 -0
  229. data/lib/rgame/util/save_file.rb +107 -0
  230. data/lib/rgame/util/solid_grid.rb +37 -0
  231. data/lib/rgame/util/tensor.rb +3 -0
  232. data/lib/rgame/util/tile_sweep.rb +36 -0
  233. data/lib/rgame/util/z.rb +123 -0
  234. data/lib/rgame/util.rb +10 -0
  235. data/lib/rgame/util_ext.so +0 -0
  236. data/lib/rgame/version.rb +12 -0
  237. data/lib/rgame.rb +5 -0
  238. data/licenses/SDL2/LICENSE.txt +18 -0
  239. metadata +309 -0
@@ -0,0 +1,231 @@
1
+ # Systems & shared resources
2
+
3
+ Some things a node needs do not live on the node: a tile map, the world bounds, a
4
+ shared collision world. **Shared resources are systems. A system lives on an
5
+ anchor node, and other nodes reach it by walking the tree**, not through
6
+ constructor arguments. A system is an ordinary `RGame::Engine::Component` on a
7
+ boundary node, found with the same `get_component` every node has.
8
+
9
+ ## Two scopes, two anchor nodes
10
+
11
+ **Scope belongs to the node you attach a system to**, not to the system.
12
+
13
+ - **Global scope → the root node.** Every node can reach the root, and it never
14
+ changes. Systems that live as long as the program are components on the root,
15
+ such as `Players` and `Viewports`: `node.root.get_component(RGame::Engine::Players)`.
16
+ - **Scene scope → the scene node**, the node `SceneStack` pushes. Systems that
17
+ live as long as a scene are components on *that* node, such as the collision
18
+ world or the tile world. They appear when the scene is pushed and go when it is
19
+ popped: `node.scene.get_component(CollisionWorld)`.
20
+
21
+ ## The anchors
22
+
23
+ **Both anchors are methods that walk the parent chain**, not cached fields. A
24
+ back-link cached at add time would go stale for a node built before it is
25
+ mounted: its children would cache the wrong root. Resolving on every access
26
+ cannot go stale.
27
+
28
+ - `root` is `@parent ? @parent.root : self`. The top-most node is its own root.
29
+ - `scene` is the nearest ancestor marked as a scene boundary. `SceneStack#push`
30
+ marks the pushed scene with `scene.scene = scene`, and descendants resolve up to
31
+ it. Outside any scene, `scene` is `nil`.
32
+
33
+ ### Looking a system up
34
+
35
+ ```ruby
36
+ node.system(CollisionWorld)
37
+ ```
38
+
39
+ **`Node2D#system(klass)` checks the scene first, then the root.** A scene can
40
+ therefore override a global default, and nodes outside any scene still find
41
+ globals. To mean one scope specifically, use its anchor:
42
+ `node.root.get_component` or `node.scene.get_component`.
43
+
44
+ ### Ask for a contract, not a class
45
+
46
+ **The lookup matches by ancestry**, so `klass` can be a module the system
47
+ includes instead of its own class. One question can then have several answers.
48
+
49
+ "How big is the world" shows this. A flat game mounts
50
+ [`Components::World`](components.md#world). A tile game mounts
51
+ [`Components::TileWorld`](components.md#tileworld), which derives the same two
52
+ numbers from its map. Both include `Components::WorldBounds`, so a component
53
+ that needs bounds asks for the *contract*:
54
+
55
+ ```ruby
56
+ def on_attach
57
+ world = node.system(RGame::Engine::Components::WorldBounds)
58
+ @width = world.world_width
59
+ @height = world.world_height
60
+ end
61
+ ```
62
+
63
+ `ScreenWrap` and `DespawnOffscreen` work this way. They run unchanged in either
64
+ kind of scene and never learn which one they are in. Naming the contract keeps
65
+ the two implementations from drifting apart.
66
+
67
+ ## Registering with a system: use the lifecycle, not `initialize`
68
+
69
+ **A system and its clients connect in the tree-lifecycle hooks**, never in
70
+ `initialize`. They can only connect once everything is in the live tree, and a
71
+ node under construction has no anchors. See
72
+ [Lifecycle](scene_graph.md#lifecycle-constructing-vs-entering-the-tree).
73
+
74
+ The entered-tree cascade fixes an order that makes this safe. The scene's own
75
+ components run `on_attach` first, so a `CollisionWorld` on the scene node exists.
76
+ Then the scene's `on_add` runs, then its children enter. By the time a child
77
+ collider attaches, the scene-scoped system it looks up is already there.
78
+
79
+ ```ruby
80
+ # CircleCollider (engine/components/circle_collider.rb) registers itself when it
81
+ # enters the tree and releases the registration when it leaves — the engine fires
82
+ # both hooks, so a spawned/despawned entity can't leak a registration.
83
+ class CircleCollider < RGame::Engine::Component
84
+ def on_attach = node.system(CollisionWorld)&.register(self)
85
+ def on_detach = node.system(CollisionWorld)&.unregister(self)
86
+ end
87
+ ```
88
+
89
+ **A collider tolerates a missing world; most clients should not.** Both hooks
90
+ above use `&.`. A collider in a scene without a `CollisionWorld` is a shape that
91
+ reports nothing. A tile-only game wants exactly that: its character carries a
92
+ feet box to be *stopped* by (see [`Mover`](components.md#mover)), and there are no
93
+ pairs to find. The cost is that an `on_hit` handler in such a scene never fires,
94
+ and nothing reports it. Weigh that deliberately. A client that is useless without
95
+ its system raises instead, as a mover's `blocked_by:` does.
96
+
97
+ ## The two systems `Game` mounts
98
+
99
+ **`RGame::Game` puts two systems on the root before the tree goes live.** Any node
100
+ can reach them without the game wiring anything:
101
+
102
+ | | |
103
+ |---|---|
104
+ | `node.system(RGame::Engine::Players)` | who is playing — devices, bindings, cameras, and who a newly used controller belongs to |
105
+ | `node.system(RGame::Engine::Viewports)` | how the screen is divided — one `View` per active player, and collapsing the split |
106
+
107
+ They are ordinary root-scoped systems, mounted the way a game mounts its own. A
108
+ scene that needs a camera to follow asks `Players` (`players.primary.camera`). A
109
+ cutscene that collapses the split asks `Viewports` (`viewports.solo!(camera)`).
110
+ Both work from anywhere in the tree, with nothing passed in. That reach is why
111
+ they are systems and not objects `Game` hands down.
112
+
113
+ See [Input](input.md#players-seats-and-joining) and
114
+ [Scene graph](scene_graph.md#viewports-and-views).
115
+
116
+ ## Collision: two indexes, one resolver
117
+
118
+ Collision is the largest structure built from systems. Three pages cover its
119
+ parts: the [components](components.md#boxcollider) a node carries, the
120
+ [systems](#systems-that-index-their-clients-the-tag-registry-pattern) a scene
121
+ mounts, and the
122
+ [building blocks](internals.md#collisionsystem--move-an-actor-against-its-blockers)
123
+ underneath. This section shows how they fit together.
124
+
125
+ **A node has exactly one collision shape, and exactly one component owns it.** The
126
+ [collider](components.md#boxcollider) *is* the shape. A
127
+ [mover](components.md#mover) that wants to be stopped reads its sibling's box
128
+ instead of building a second one. The rectangle that stops a step is the
129
+ rectangle that reports a contact, so retuning one retunes both.
130
+
131
+ **Collision uses two indexes, on purpose.** A tile map is already an index: the
132
+ wall a step would hit follows from arithmetic on the step. A `TileWorld` divides
133
+ by the tile size and asks the grid. Actors have no such structure, so a
134
+ `CollisionWorld` buckets them into a
135
+ [`SpatialHash`](internals.md#spatialhash--uniform-grid-broadphase) each step.
136
+ Baking tile shapes into the broadphase would rebuild an index the grid already
137
+ is. It would also cost time every frame on a map of tens of thousands of tiles.
138
+
139
+ **The unification happens one level up.** A **blocker source** answers one
140
+ question over plain numbers: where does this box land when it moves `dx`? Three
141
+ sources exist:
142
+
143
+ - `TileBlockers`, over the grid;
144
+ - `ActorBlockers`, over the broadphase;
145
+ - `BoundsBlockers`, over the world's edges.
146
+
147
+ A blocked mover builds a
148
+ [`CollisionSystem`](internals.md#collisionsystem--move-an-actor-against-its-blockers)
149
+ at attach, from the sources its `blocked_by:` names. That system asks each source
150
+ and takes the most restrictive answer on each axis. The axis-separated order that
151
+ produces wall-sliding therefore exists **once**. An actor slides off a villager
152
+ exactly as it slides off a fence.
153
+
154
+ A source may also answer `travel?`: can this box move along a segment without
155
+ being stopped? `TileBlockers` does. A [`Navigator`](components.md#navigator) uses
156
+ it to check a route against the same resolver that will stop its walk.
157
+
158
+ | | Mounted on the scene | Owned by the node |
159
+ |---|---|---|
160
+ | Tiles | [`TileWorld`](components.md#tileworld), which hands out one shared `TileBlockers` | — |
161
+ | Actors | [`CollisionWorld`](components.md#collisionworld), the broadphase | an `ActorBlockers` per mover, holding its own collider and layer list |
162
+ | The world's edge | any [`WorldBounds`](components.md#world) | a `BoundsBlockers` |
163
+ | The step | — | one `CollisionSystem`, built at attach from the names above |
164
+
165
+ Neither system needs the other, and most scenes mount one. `examples/scroll_map`
166
+ has a map and no broadphase. `examples/collision` has a broadphase and no map.
167
+ `examples/collision_tiles` mounts both, and the difference shows only in the names
168
+ listed in `blocked_by`.
169
+
170
+ "What stops a mover" and "what a collider touches" remain two questions, and a
171
+ game sees the difference. See
172
+ [Blocking and overlapping](#blocking-and-overlapping-are-two-reports-and-a-pair-gets-one-of-them)
173
+ below, after the broadphase.
174
+
175
+ ## Systems that index their clients (the tag-registry pattern)
176
+
177
+ **A many-to-many system keeps its own index of registered clients**, so it checks
178
+ only nearby candidates instead of walking the tree for every pair. Broadphase
179
+ collision works this way, on the scene node. `CollisionWorld`
180
+ (engine/components/collision_world.rb) holds a `SpatialHash`: a spatial index of
181
+ registered colliders, rebuilt each `update`. The index ignores shape, so
182
+ `CircleCollider` and `BoxCollider` share one and collide with each other. The
183
+ index holds node references only. It carries no component data and gives no
184
+ data-locality benefit; it is a lightweight registry.
185
+
186
+ **`CollisionWorld` ignores layers.** It reports every overlapping pair to both
187
+ colliders. The owning node decides what a contact *means* by reading the other
188
+ collider's `layer` tag:
189
+
190
+ ```ruby
191
+ collider.on_hit { |other| queue_free if other.layer == :bullet } # in a Rock node
192
+ ```
193
+
194
+ **A contact arrives as two edges, not as a state.** `on_hit` fires on the step a
195
+ pair starts overlapping. `on_separated` fires on the step it stops. Each fires
196
+ once per pair, with nothing in between. A handler can therefore count, play a
197
+ sound or spend a life without guarding itself. That is why the system keeps an
198
+ `Engine::ContactSet` per collider, instead of forwarding each step's broadphase
199
+ results.
200
+
201
+ `CollisionWorld` is a normal component on the scene node. It runs in the
202
+ `update` traversal, where its broadphase runs, and goes away with the scene.
203
+ `examples/collision` shows the whole loop: circles and crates registering,
204
+ overlapping and separating through this system.
205
+
206
+ ### Blocking and overlapping are two reports, and a pair gets one of them
207
+
208
+ **The same broadphase answers a second question.** A
209
+ [mover](components.md#mover) that names a collider layer in `blocked_by:` is
210
+ *stopped* by every box with that layer. It stops flush against the box's edge, as
211
+ against a solid tile. One `BoxCollider` can thus be a wall to one actor and a
212
+ trigger for another. Who declared the layer decides which; the collider itself
213
+ says nothing.
214
+
215
+ **The two reports never both fire for one pair.** Blocking leaves the boxes
216
+ exactly touching. `CollisionBox.overlap?` uses the half-open span `[x, x + w)` on
217
+ purpose. On a grid, pieces on neighbouring squares border each other constantly,
218
+ and an inclusive test would report each as a contact. Measured on two 12×6 boxes:
219
+ touching exactly, `on_hit` does not fire; overlapping by half a pixel, it does.
220
+ That convention makes the two reports mutually exclusive by construction.
221
+
222
+ A blocked pair therefore reports **no** contact. A mover that must both stop and
223
+ react listens to `on_blocked`, not `on_hit`; see [`Mover`](components.md#mover).
224
+ The rule of thumb:
225
+
226
+ | The question | The report | Where it lives |
227
+ |---|---|---|
228
+ | what may I not pass through | `on_blocked` / `on_unblocked` | the mover that was stopped |
229
+ | what am I touching | `on_hit` / `on_separated` | both colliders of the pair |
230
+
231
+ `examples/collision_tiles` shows the first, and `examples/collision` the second.
data/docs/api/text.md ADDED
@@ -0,0 +1,133 @@
1
+ # Text
2
+
3
+ **The renderer comes with a font, and `text` draws with it.** Most games need
4
+ nothing more:
5
+
6
+ ```ruby
7
+ require 'rgame'
8
+ require 'rgame/core'
9
+
10
+ class MyGame < RGame::Core::App
11
+ def initialize
12
+ super(width: 800, height: 600, caption: 'demo')
13
+ @renderer = RGame::Core::Renderer.new(self)
14
+ end
15
+
16
+ def draw
17
+ @renderer.text('Score: 1200', 10, 10)
18
+ end
19
+ end
20
+
21
+ MyGame.new.run
22
+ ```
23
+
24
+ ## Where text goes
25
+
26
+ `text(string, x, y, …)` puts the **top-left corner** of the line at `(x, y)`.
27
+ Every other drawing method uses the same corner. Typography measures from the
28
+ baseline, but a caller placing a label does not have to.
29
+
30
+ ```ruby
31
+ renderer.text(string, x, y, z: 10, color: nil, font: nil)
32
+ renderer.text_width(string, font: nil) # => Float — pixels
33
+ renderer.text_height(font: nil) # => Integer — the line height
34
+ ```
35
+
36
+ **`string` is a String or anything with `to_str`**, such as an
37
+ [`Engine::Text`](toolbox.md#text--the-string-a-node-draws), which a node passes
38
+ as it is. `text` and `text_width` raise `TypeError` for `nil`, a number, or a
39
+ `to_str` that returns something other than a String.
40
+
41
+ **A string is one line.** A newline has no special meaning. Draw two lines with
42
+ two calls, stepped by `text_height`:
43
+
44
+ ```ruby
45
+ lines.each_with_index do |line, i|
46
+ @renderer.text(line, 10, 10 + (i * @renderer.text_height))
47
+ end
48
+ ```
49
+
50
+ **`text_width` and `text` agree.** They run the same code, so a label centred by
51
+ its measured width lands exactly there:
52
+
53
+ ```ruby
54
+ @renderer.text(label, (width - @renderer.text_width(label)) / 2, 20)
55
+ ```
56
+
57
+ **`text_width` and `text_height` also work outside `draw`**, unlike the drawing
58
+ methods. Measuring touches no GPU, and a menu lays itself out while updating.
59
+
60
+ A label built from a changing value, like a score, should come from
61
+ [`RGame::Engine::Text`](toolbox.md#text--the-string-a-node-draws). It renders
62
+ the string only when a variable or the language changes:
63
+
64
+ ```ruby
65
+ @score = RGame::Engine::Text.new('hud.score', :score) # once
66
+
67
+ renderer.text(@score.with(score: @points), 10, 10) # every frame
68
+ ```
69
+
70
+ ## Fonts
71
+
72
+ ```ruby
73
+ font = RGame::Core::Font.new(app, 18) # the shipped font
74
+ font = RGame::Core::Font.new(app, 18, path: 'assets/pixel.ttf')
75
+
76
+ font.height # => 18
77
+ font.text_width('Hello') # => 38.7
78
+
79
+ renderer.text('Hello', 10, 10, font: font)
80
+ ```
81
+
82
+ A `Font` is **one typeface at one pixel size**. Two sizes need two fonts. A font
83
+ belongs to the app whose GPU context holds its glyphs, like an image. Drawing it
84
+ through another app's renderer raises instead of painting blank boxes.
85
+
86
+ The renderer builds its own 18px font on first use. Replace it, and every `text`
87
+ call without a `font:` follows:
88
+
89
+ ```ruby
90
+ @renderer.font = RGame::Core::Font.new(self, 24)
91
+ ```
92
+
93
+ A file that is unreadable or not a TrueType font raises
94
+ `RGame::Core::Font::LoadError`, naming the path.
95
+
96
+ ### The default font, and what it covers
97
+
98
+ **The engine ships Liberation Sans and uses it when you pass no path.** It never
99
+ looks a font up by name and never asks a system font database. A font is a file.
100
+
101
+ This is a deliberate trade. Asking the operating system for "Arial" gets whatever
102
+ that machine keeps under the name, or a substitute. A UI laid out on the
103
+ developer's machine can then overflow on a player's. A shipped font renders
104
+ identically everywhere, and costs about 400 KB in the gem.
105
+
106
+ | | |
107
+ |---|---|
108
+ | Covers | English, German, French, Italian, Spanish, Portuguese, Nordic, Polish — in full, including `ß`, `ẞ`, `« »`, curly quotes and `€`. Greek and Cyrillic too. |
109
+ | Does not cover | CJK, Arabic, Hebrew, Devanagari. Pass your own font file for those; no font of this size includes them. |
110
+
111
+ Text is UTF-8. A malformed byte draws one replacement character, and the rest of
112
+ the string still draws. A bad byte in a data file costs one visible box, not the
113
+ whole label.
114
+
115
+ ## What it costs
116
+
117
+ **The engine rasterises each glyph the first time it is drawn**, then keeps it in
118
+ a texture atlas. Cost therefore grows with the **characters** a game uses, not
119
+ with the strings it draws. A score that changes every frame costs no more glyph
120
+ work after the ten digits. A whole Latin character set fits on one 512×512 page,
121
+ so a line of text is one draw call.
122
+
123
+ A font that is only measured, never drawn, uses no video memory.
124
+
125
+ Nothing needs freeing. The engine releases a font's atlas when the font is
126
+ collected, whether before or after its app. `Font.debug_live_pages` returns how
127
+ many atlas pages exist. It serves tests, not gameplay.
128
+
129
+ ## What is not here
130
+
131
+ rgame text has no markup (`<b>`, colour tags), no bold or italic variants, no
132
+ multi-line layout and no word wrapping. It has no text input, no right-to-left
133
+ text and no complex shaping. A string is one line of left-to-right glyphs.
@@ -0,0 +1,237 @@
1
+ # Tile maps
2
+
3
+ **`RGame::Engine::TileMap` and `RGame::Engine::Tileset` hold a map made in
4
+ [Tiled](https://www.mapeditor.org/) as plain data.** `TileMap` holds the grid: its
5
+ size, its layers and the tile id in every cell. `Tileset` holds what each tile id
6
+ means: which tiles are solid and which are animated. Neither class loads an image
7
+ or names a renderer, so both run headless and in specs.
8
+
9
+ A game rarely touches either class directly. The pieces that use them are:
10
+
11
+ | | Uses the map to |
12
+ |---|---|
13
+ | [`TileWorld`](components.md#tileworld) | answer solidity and world-size questions for actors |
14
+ | [`TileMapLayer`](components.md#tileworld) | draw one layer per node |
15
+ | [`TileMapRenderer`](assets.md#tile-maps) | bake and draw the tiles |
16
+ | `RGame::Game`'s `:tilemap` asset loader | load a `.tmx` and pair it with its tileset image |
17
+
18
+ Read on when a scene queries the map itself, or when you author maps for rgame.
19
+
20
+ ## Loading a map
21
+
22
+ ```ruby
23
+ require 'rgame'
24
+
25
+ map, image_path = RGame::Engine::TileMap.load('examples/assets/town.tmx')
26
+
27
+ image_path # => "examples/assets/tileset.png"
28
+ map.width # => 60 — in tiles
29
+ map.pixel_width # => 960
30
+ map.tileset # => RGame::Engine::Tileset
31
+ ```
32
+
33
+ **`TileMap.load(tmx_path)` returns two values: the map and its tileset image
34
+ path.** It reads the `.tmx`, follows it to the `.tsx` it names, parses both, and
35
+ attaches the tileset to the map. It does not load the image. An image is a GPU
36
+ handle, and the engine layer may not hold one. `RGame::Game`'s asset loader
37
+ passes the path to the asset manager instead:
38
+
39
+ ```ruby
40
+ app.assets.add_loader(:tilemap) do |path|
41
+ map, image_path = RGame::Engine::TileMap.load(path)
42
+ tiles = app.assets.image(image_path).tiles(map.tileset.tile_width,
43
+ map.tileset.tile_height)
44
+ RGame::Core::TileMapRenderer.new(map, tiles)
45
+ end
46
+ ```
47
+
48
+ In a game, reach the parsed map through that loader:
49
+ `app.assets.tilemap('map/island.tmx').map`.
50
+
51
+ **Every path resolves relative to the file that names it.** The `.tsx` resolves
52
+ next to the `.tmx`, and the image next to the `.tsx`. Tiled writes paths this
53
+ way, so you can move a map and its tileset together.
54
+
55
+ `TileMap.parse(tmx_string)` parses a `.tmx` from a String and touches no files. It
56
+ leaves `tileset` as `nil`. Attach one with `map.tileset = Tileset.parse(...)`
57
+ before asking about solidity.
58
+
59
+ ### What rgame reads from Tiled
60
+
61
+ rgame supports a subset of the `.tmx` format. A map outside it fails to parse or
62
+ parses wrongly, so author within these limits:
63
+
64
+ | | Supported |
65
+ |---|---|
66
+ | Orientation | orthogonal |
67
+ | Tilesets | **one**, in an external `.tsx` file; an embedded tileset has no `source` and `load` raises |
68
+ | Layer data | base64 with zlib compression (Tiled's "Base64 (zlib compressed)"); other encodings raise |
69
+ | Layers | tile layers only; object and image layers are ignored |
70
+ | Flipped or rotated tiles | the flip flags are stripped, so the tile draws unflipped |
71
+ | Map size | fixed; infinite maps are not read |
72
+
73
+ ## `RGame::Engine::TileMap`
74
+
75
+ ### Geometry
76
+
77
+ | Reader | |
78
+ |---|---|
79
+ | `width`, `height` | the map's size in tiles |
80
+ | `tile_width`, `tile_height` | one tile's size in pixels |
81
+ | `pixel_width`, `pixel_height` | the map's size in pixels |
82
+ | `layer_count` | how many tile layers the map has |
83
+ | `tileset`, `tileset=` | the attached `Tileset`, or `nil` |
84
+ | `tileset_source`, `firstgid` | the `.tsx` path the map names, and the tileset's first gid |
85
+
86
+ ### Reading cells
87
+
88
+ ```ruby
89
+ map.gid(0, 12, 7) # the gid in layer 0 at column 12, row 7
90
+ map.in_bounds?(60, 0) # => false — columns run 0..59
91
+ map.gid(0, -1, 0) # => 0 — outside the map, every layer is empty
92
+ ```
93
+
94
+ **A gid is Tiled's global tile id.** `0` means an empty cell. Any other gid names a
95
+ tile in the tileset; `Tileset#local_id(gid)` turns it into the tile's index in the
96
+ tileset image. Layer `0` is the bottom layer, as Tiled lists them.
97
+
98
+ `gid` answers `0` for any cell outside the map, so a caller never checks bounds
99
+ first. The map stores its gids in one
100
+ [`Util::Tensor`](values.md#rgameutiltensor), indexed column, row, layer.
101
+
102
+ ### Solidity
103
+
104
+ ```ruby
105
+ map.solid_tile?(12, 7) # any layer solid at that cell?
106
+ map.solid_at?(200.5, 116.0) # the same, from a world position in pixels
107
+ ```
108
+
109
+ **A cell is solid when any layer holds a solid tile there.** Collision considers
110
+ every layer, whatever it draws like. `solid_at?(world_x, world_y)` divides by the
111
+ tile size and asks `solid_tile?` for that cell. **Outside the map is not solid.**
112
+ Keep actors inside with `blocked_by: [:bounds]` on their mover.
113
+
114
+ Both methods need an attached tileset, which `load` provides.
115
+
116
+ Actors do not call these per step. [`TileWorld`](components.md#tileworld) reads
117
+ `solid_tile?` once per cell into a
118
+ [`Util::SolidGrid`](values.md#rgameutilsolidgrid), and collision and pathfinding
119
+ read that grid.
120
+
121
+ ### Layers drawn above the actors
122
+
123
+ ```ruby
124
+ map.above_layer?(2) # => true when Tiled marks layer 2 `above`
125
+ ```
126
+
127
+ **Mark a layer `above` in Tiled** to draw it over the actors, for tree canopies
128
+ or roofs. Add a custom **bool** property named `above` to the layer and tick it. A
129
+ layer without the property draws below.
130
+
131
+ The flag affects drawing order only. `TileWorld#first_above_layer` returns the
132
+ first flagged layer, and
133
+ [`TileMapLayer.mount`](components.md#tileworld) leaves the actors' gap below it.
134
+ A map with no flagged layer puts the actors over everything.
135
+
136
+ ### Building a map by hand
137
+
138
+ ```ruby
139
+ require 'rgame'
140
+
141
+ map = RGame::Engine::TileMap.new(
142
+ width: 2, height: 1, tile_width: 16, tile_height: 16,
143
+ tileset_source: 'tiles.tsx', firstgid: 1,
144
+ layers: [[1, 2]], # one layer, gids row by row
145
+ above: [false]
146
+ )
147
+ map.tileset = RGame::Engine::Tileset.new(
148
+ firstgid: 1, columns: 2, tile_width: 16, tile_height: 16,
149
+ image_source: 'tiles.png', animations: {}, solid_ids: Set[1]
150
+ )
151
+
152
+ map.solid_at?(20.0, 3.0) # => true — gid 2 is local tile 1, which is solid
153
+ map.solid_at?(3.0, 3.0) # => false
154
+ ```
155
+
156
+ **Specs build small maps this way**, with no files. `layers:` is an Array of
157
+ layers. Each layer is a flat Array of `width * height` gids, row by row. `above:`
158
+ is optional and defaults to no layer above.
159
+
160
+ ## `RGame::Engine::Tileset`
161
+
162
+ **A `Tileset` says what each tile in a sheet is.** It knows the sheet's geometry,
163
+ which tiles are solid, and which are animated.
164
+
165
+ | Reader | |
166
+ |---|---|
167
+ | `firstgid` | the gid of the tileset's first tile in the map |
168
+ | `columns` | how many tiles one row of the sheet holds |
169
+ | `tile_width`, `tile_height` | one tile's size in pixels |
170
+ | `image_source` | the sheet image, as the `.tsx` names it |
171
+ | `animations` | `{ local_id => [Frame, ...] }` for every animated tile |
172
+ | `animated_ids` | the local ids that have an animation |
173
+ | `solid_ids`, `solid_ids=` | a Set of the local ids that are solid |
174
+
175
+ `Tileset.parse(tsx_string, firstgid:)` parses a `.tsx` from a String. The
176
+ `firstgid` comes from the map, because Tiled stores it in the `.tmx`, not in the
177
+ `.tsx`.
178
+
179
+ ### Local ids
180
+
181
+ ```ruby
182
+ tileset.local_id(37) # => 36 when firstgid is 1
183
+ ```
184
+
185
+ **A local id is a tile's index in the sheet**, counted from 0, left to right and
186
+ top to bottom. It equals `gid - firstgid`. `Image#tiles` slices a sheet in the same
187
+ order, so a local id indexes that Array directly.
188
+
189
+ ### Solid tiles
190
+
191
+ ```ruby
192
+ tileset.solid?(gid) # takes a gid, not a local id; 0 is never solid
193
+ tileset.solid_ids << 12 # make local tile 12 solid
194
+ ```
195
+
196
+ **A tile is solid when it has a collision shape in Tiled.** Open the tileset in
197
+ Tiled's collision editor and draw any shape on the tile. `parse` marks every tile
198
+ whose `<objectgroup>` holds at least one object. The shape itself is ignored:
199
+ rgame treats a solid tile as a solid square. The map carries its collision, so
200
+ changing which tiles block needs no code.
201
+
202
+ `solid_ids` is writable, for a game that adds or removes solid tiles in code. Change
203
+ it before a `TileWorld` first asks, because the world copies solidity into its own
204
+ grid once.
205
+
206
+ ### Animated tiles
207
+
208
+ ```ruby
209
+ tileset.animations[37] # => [#<struct Frame tile_id=37, duration=250>, ...]
210
+ tileset.frame_local_id(37, 0) # => 37
211
+ tileset.frame_local_id(37, 250) # => 46
212
+ ```
213
+
214
+ **Animate a tile in Tiled's tile animation editor.** `parse` reads each frame as a
215
+ `Tileset::Frame` with a `tile_id` (a local id) and a `duration` in milliseconds.
216
+
217
+ `frame_local_id(local, ms)` returns the local id to draw for tile `local` after
218
+ `ms` milliseconds. The animation loops. A tile without an animation returns
219
+ itself. The time is an argument, not a clock read, so pausing is "stop
220
+ accumulating".
221
+
222
+ **Tiled speaks milliseconds; the engine speaks seconds.** `TileWorld` accumulates
223
+ `elapsed` in seconds, and `TileMapRenderer` converts it once per draw, as
224
+ `(elapsed * 1000).to_i`. Convert the same way when you call `frame_local_id`
225
+ yourself.
226
+
227
+ ## Testing against a map
228
+
229
+ `TileMapRenderer` draws any object that answers the tile map contract, and never
230
+ names `TileMap`. rgame's own suite states that contract in
231
+ `spec/support/shared_examples/a_tile_map.rb`. It checks both `TileMap` and the
232
+ spec stand-in `StubTileMap` against it. The contract covers `layer_count`,
233
+ `width`, `height`, `tile_width`, `tile_height`, `gid`, and a `tileset` answering
234
+ `local_id`, `animations` and `frame_local_id`.
235
+
236
+ A spec that needs a map but no files builds one [by hand](#building-a-map-by-hand),
237
+ or parses a `.tmx` string with `TileMap.parse`.