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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e0c1ec022c88712a5c9252b0f4c67a56d152008adc614c88a6f50c896828942c
4
+ data.tar.gz: 790d121dd4042e904424d3194bc3b7806951b487060cedd80f18f1febd9c01c3
5
+ SHA512:
6
+ metadata.gz: e89da5c98b8af06a2473bdd6a7a78993e466af07e63019035828b337d348ba1fc1dfa4b5ee3e7a19a92a98c9d79653314afe3b123b9085b34c20ff1148026d4b
7
+ data.tar.gz: 98549e0663ee4a3cbf71cfc829931ff3a4887113de41fd9ff7d5fef6f720d3d5855a1a9334bc32f28f1c33f809e50dfa51da2bf407d39b409736f9cc94482414
data/CHANGELOG.md ADDED
@@ -0,0 +1,251 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here.
4
+
5
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
6
+ this project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html) —
7
+ which before 1.0 means the public API can still change in a minor release.
8
+
9
+ Entries describe what changed for someone *using* the engine. The reasoning
10
+ behind a change belongs in the documentation it lands with; this file is the
11
+ index, not the argument.
12
+
13
+ ## [Unreleased]
14
+
15
+ ## [0.4.0] - 2026-09-16
16
+
17
+ ### Added
18
+
19
+ - **`gem install rgame` needs no compiler and no SDL2 on a common desktop.**
20
+ Apple Silicon Macs, x86-64 Linux and 64-bit Windows get a gem whose two
21
+ extensions are already built, with SDL2 linked into them. Every other machine,
22
+ and every Ruby but 4.0, installs the source gem and compiles it as before.
23
+ - **A virtual gamepad for tests.** `RGame::Core::VirtualGamepad.new` plugs a
24
+ synthetic controller into a running `App`, which seats it and reads it like a
25
+ real pad; `set_button`, `set_axis` and `detach` drive it. See
26
+ [docs/api/input.md](docs/api/input.md#rgamecorevirtualgamepad).
27
+
28
+ ## [0.3.1] - 2026-09-15
29
+
30
+ ### Fixed
31
+
32
+ - `require "rgame"` failed under Bundler with `cannot load such file --
33
+ rexml/document`. The gem now declares `rexml` as a dependency, which Ruby 4.0
34
+ no longer loads without one.
35
+
36
+ ## [0.3.0] - 2026-09-15
37
+
38
+ ### Added
39
+
40
+ - **`rgame new NAME`.** The gem installs an `rgame` command, and `rgame new`
41
+ generates a project: a game class, a root node, an English translation table,
42
+ and a spec suite and RuboCop config that pass. Only `game.rb` loads SDL, so the
43
+ generated specs run without a display. See [docs/api/cli.md](docs/api/cli.md).
44
+ - **RuboCop cops for games.** The gem ships rgame's `Game/` cops as a RuboCop
45
+ plugin, and `rgame new` enables them. They catch per-frame allocations, a node
46
+ drawn at its own position twice, literal text and Core named from headless
47
+ code. See [docs/api/cli.md](docs/api/cli.md#the-generated-rubocop-configuration).
48
+ - **Translation by default.** `RGame::Game` loads every `locales/**/*.yml` and
49
+ picks the player's language from `RGame::Core.preferred_locales`. A node draws
50
+ an `Engine::Text` built from a key, and a UI button's `label:` is a key. See
51
+ [docs/api/localization.md](docs/api/localization.md).
52
+ - **Fullscreen and scaling.** `Game.new` takes `fullscreen:` and `scale_mode:`,
53
+ and both can change while the game runs. `scale_mode:` maps the logical size
54
+ onto the window, and defaults to `:letterbox`. See
55
+ [docs/api/game.md](docs/api/game.md).
56
+ - **Saving.** `Util::SaveFile` reads and writes a game's state in the player's
57
+ save directory. `Components::Identity` gives a node a stable id to save it
58
+ under. See [docs/api/values.md](docs/api/values.md).
59
+ - **Box colliders.** `Components::BoxCollider` is a rectangle in the same
60
+ `CollisionWorld` as `CircleCollider`. `FeetCollider` derives its box from the
61
+ node's size, so a top-down character collides at its feet. See
62
+ [docs/api/components.md](docs/api/components.md).
63
+ - **Movers that something can stop.** `CharacterBody`, `Velocity` and
64
+ `PathFollow` share the base `Components::Mover`. Its `blocked_by:` lists what
65
+ stops a step: solid tiles, the world's edge or collider layers. `on_blocked` and
66
+ `on_unblocked` report the blocker and the axis. See
67
+ [docs/api/components.md](docs/api/components.md).
68
+ - **Pathfinding.** `Engine::NavGrid` finds routes over a tile grid, and
69
+ `TileWorld#nav_grid` hands one out. `Components::Navigator#go_to` walks a node
70
+ there. The search runs in C, in `Util::SolidGrid`, `Util::RouteSearch` and
71
+ `Util::TileSweep`. See [docs/api/toolbox.md](docs/api/toolbox.md).
72
+ - **`Components::World`** gives a scene its bounds without a tile map.
73
+ `ScreenWrap` and `DespawnOffscreen` read the bounds from it or from
74
+ `TileWorld`. See [docs/api/components.md](docs/api/components.md).
75
+ - **`Components::Hop`** lifts a character's picture off the ground through the
76
+ new `Node2D#elevation`. Colliders and cameras ignore the lift. See
77
+ [docs/api/components.md](docs/api/components.md).
78
+ - **New buttons and styles.** `UI::Button` is the base for a button with its
79
+ own look. `TextButton`, `PanelButton`, `OptionButton` and `IconButton` draw
80
+ their background from a `NineSliceStyle` or a `ShapeStyle`. A `hotkey:` presses
81
+ a button without focusing it. See [docs/api/ui.md](docs/api/ui.md).
82
+ - **Menu layouts and navigations.** `UI::Menu` takes a `layout:` (`Column`,
83
+ `Row` or `Ring`) and a `navigation:` (`Stepping`, `Pointing` or `nil`).
84
+ `UI::RadialMenu` focuses whatever the stick points at. `UI::PanelMenu` draws a
85
+ panel that grows with its buttons. See [docs/api/ui.md](docs/api/ui.md).
86
+ - **Menus open and close.** A menu answers `open?`, `open` and `close`, and
87
+ emits `on_opened` and `on_closed`. A closed menu draws nothing and takes no
88
+ input. `trigger:` names an action that holds a menu open while it is down. See
89
+ [docs/api/ui.md](docs/api/ui.md).
90
+ - **A UI atlas can name images.** A descriptor's `images` section cuts
91
+ rectangles from the sheet, and `Renderer#register_ui_atlas` registers each by
92
+ name. `UI::IconButton.new(image: :home)` then draws one.
93
+ - **Input prompts.** `InputMap#button_for(action, device)` returns the bound
94
+ button that device can press. `Controls.gamepad?` and `Controls.pad_button?`
95
+ tell the two kinds of id apart. See [docs/api/input.md](docs/api/input.md).
96
+ - **Sounds by path.** `audio.play_sound` and `play_music` accept a path and
97
+ load it through the asset manager on first use. `RGame::Game` subscribes an
98
+ `AudioDirector`, so `AudioBus` sounds play without setup. See
99
+ [docs/api/audio.md](docs/api/audio.md).
100
+ - **24 examples, one concept each.** Each is a single `main.rb` that runs on its
101
+ own, from `walk` and `sprite` to `pathfinding` and `localization`. See
102
+ [docs/api/examples.md](docs/api/examples.md).
103
+ - `AssetManager#glob` lists the files under the media root that match a pattern.
104
+ - `Util::Color` gains named colours, from `RED` to `DARK_GRAY`.
105
+
106
+ ### Changed
107
+
108
+ - **A subclass may not redefine `Node2D`'s or `Component`'s internal methods.**
109
+ Their private and protected machinery now starts with `_`. A subclass that
110
+ defines one of those names raises `NameError` where the class is defined.
111
+ Before, it silently switched that machinery off. See
112
+ [docs/api/scene_graph.md](docs/api/scene_graph.md).
113
+ - **Nodes draw in local space.** `Node2D#draw` pushes the node's transform
114
+ before it calls `on_draw`, so a node draws at its own origin:
115
+ `renderer.rect(0, 0, width, height)`. Drop `abs_x`/`abs_y` from draw calls, or
116
+ the position applies twice. See
117
+ [docs/api/scene_graph.md](docs/api/scene_graph.md).
118
+ - **`abs_x`, `abs_y` and `abs_angle` are now `world_x`, `world_y` and
119
+ `world_angle`.** A node computes them when read, so they are never a tick
120
+ stale. `world_x=` and `world_y=` place a node in world space. See
121
+ [docs/api/scene_graph.md](docs/api/scene_graph.md).
122
+ - **`UI::Menu` is handed its buttons.** `Menu#add(button)` replaces `add_item`,
123
+ `items` is now `buttons`, and `MenuItem` is now `UI::PanelButton`. The item
124
+ size moves to `layout: UI::Column.new(...)`. Confirm activates a button when
125
+ released. See [docs/api/ui.md](docs/api/ui.md).
126
+ - **`CharacterBody` takes its shape from a sibling collider.**
127
+ `CharacterBody.new(speed:, blocked_by:)` replaces `feet_width:` and
128
+ `feet_height:`. A body with no `blocked_by:` moves freely, and `TileWorld#move`
129
+ is gone. See [docs/api/components.md](docs/api/components.md).
130
+ - **Contacts are edges.** `on_hit` fires once when two colliders start to
131
+ overlap, and the new `on_separated` fires once when they part. Before,
132
+ `on_hit` fired on every step of an overlap.
133
+ - **`I18n` reads YAML tables keyed by locale.** A key falls back from `de-AT`
134
+ to `de` to the default. Plurals follow CLDR rules, and `I18n.missing` decides
135
+ what a missing key shows. `load_file` is gone. See
136
+ [docs/api/localization.md](docs/api/localization.md).
137
+ - **`ScreenWrap` and `DespawnOffscreen` work in world space**, so a node under an
138
+ offset parent wraps at the world's edge. Their `width:` and `height:` are now
139
+ optional. A node with two responses to the edge raises when the second
140
+ attaches.
141
+ - **A component raises when its sibling is missing.** `PlayerController`,
142
+ `WanderController` and `AnimatedSprite` name what they need when they attach.
143
+ A node with two movers raises too.
144
+ - **`AnimatedSprite` follows any mover**, and faces along the larger axis of
145
+ its heading.
146
+ - **Engine internals are private.** `Renderer`'s raw `draw_*`, `push_*`, `pop`
147
+ and `*_record` methods, `Recording#draw_at` and `Song#play_looping` are
148
+ private. Call `rect`, `translated`, `record`, `Recording#draw` and `Song#play`
149
+ instead. The Tiled parse helpers are private too.
150
+
151
+ ### Removed
152
+
153
+ - `Engine::CachedLabel`. Use `Engine::Text`.
154
+ - `Engine::Body`, `Engine::Actor`, `Engine::Matrix`, `Engine::Resettable` and
155
+ `Engine::PlayerController`. None had a caller in the engine.
156
+ `Components::PlayerController` stays.
157
+ - `Engine::TileCollision`, which is now `Engine::TileBlockers`.
158
+ - The examples `14_asteroids`, `15_tiled_world` and `16_hello_world`. They are
159
+ whole games rather than examples of one concept, and no longer ship.
160
+
161
+ ### Fixed
162
+
163
+ - The README's hello-world gave `on_draw` one parameter; it takes two
164
+ (`renderer, view`).
165
+ - A new `UI::Menu` drew its first item without a highlight until the first
166
+ press.
167
+ - `Path#distance_to` returned wrong distances for Integer coordinates.
168
+
169
+ ## [0.2.0] - 2026-08-26
170
+
171
+ ### Added
172
+
173
+ - **Split-screen.** A game has seats (`RGame::Game.new(players: 2)`), and a
174
+ `RGame::Engine::Player` owns a device, a binding table, a camera and a region
175
+ of the screen. The shared world is updated once and drawn once per viewport by
176
+ a `WorldView`. Which player a node answers to is inherited down the tree like
177
+ its transform, so `ship.input_owner = players[1]` moves a whole subtree.
178
+ New: `Player`, `Players`, `Viewports`, `View`, `WorldView`, `Layout`.
179
+ - **`RGame::Engine::InputMap`** — one binding table per player, written in terms
180
+ of physical ids from `RGame::Util::Controls`, so a game names keys and pad
181
+ buttons in one place and reads named actions everywhere else.
182
+ - **Draw bands** (`RGame::Util::Z`): `:world`, `:hud`, `:overlay` and `:debug`.
183
+ A band beats every `z` in the tree, so nothing in the world can draw over the
184
+ HUD. Inherited down the tree, and set by the nodes that exist to mark one.
185
+ - **Culling** (`RGame::Engine::Culling`, `view.visible?`), which stops being an
186
+ optimisation once the world is drawn once per player.
187
+ - **A bare-bones UI package**: `PlayerLayer` gives a player their own screen, and
188
+ `UI::Menu` / `UI::MenuItem` navigate it by focus and activation — enough for
189
+ keyboard and controller menus. Layout, nesting, scrolling and text entry are
190
+ not in it; see [docs/api/ui.md](docs/api/ui.md).
191
+ - **Pausing** (`Node2D#paused`), which stops `control` and `update` for a node
192
+ and its whole subtree while it keeps drawing — a frozen world under a cutscene
193
+ overlay that goes on animating.
194
+ - `Components::CameraFollow`, and `TileMapLayer` as a node of its own.
195
+ - **`tools/drive_example.rb`** — boots an example unmodified, feeds it a
196
+ scripted input backend and reports what the game actually asked for: draws,
197
+ clips, sounds, scenes, ticks against frames.
198
+ - **macOS and Windows support**, and CI that runs every verification tier on
199
+ all three platforms.
200
+ - This changelog, linked from the gem's RubyGems page through `changelog_uri`.
201
+
202
+ ### Changed
203
+
204
+ - `Node2D#draw` and `on_draw` take the viewport being drawn into:
205
+ `on_draw(renderer, view)`. Most nodes ignore it; laying out against the edges
206
+ of a player's region, and culling, need it.
207
+ - `z` orders a node among its **siblings** only. It is never added to anything
208
+ and never reaches the renderer, so a node's whole subtree draws before or
209
+ after a sibling's, never interleaved with it. This replaces the additive
210
+ `abs_z = parent.abs_z + z`, under which a node at z 2 with a child at z 5
211
+ resolved to 7 and overtook a sibling at 4.
212
+ - `F2` quits and `F1` toggles the debug overlay. `Esc` is deliberately left to
213
+ the game, because it is the button a player expects to back out of a menu.
214
+ - Reading an action no `InputMap` declares raises `KeyError` instead of reading
215
+ as "never pressed" forever.
216
+
217
+ ### Removed
218
+
219
+ - `RGame::Engine::CameraView`. A camera belongs to a `Player` and is applied by
220
+ the `View` being drawn.
221
+
222
+ ## [0.1.0] - 2026-08-20
223
+
224
+ First release, and the first version that runs a game end to end.
225
+
226
+ ### Added
227
+
228
+ - **The C engine**, as two Ruby extensions built from one source tree: an SDL2
229
+ window and a fixed-timestep main loop, keyboard and gamepad input with
230
+ hot-plug, a z-sorted batching renderer with transforms, clipping and baked
231
+ recordings, text from a shipped TrueType font, and audio (samples and
232
+ streamed Ogg Vorbis or WAV).
233
+ - **`RGame::Core`** — the half that owns the window, the GPU and the sound
234
+ device: `App`, `Input`, `Gamepad`, `Image`, `Renderer`, `Recording`, `Font`,
235
+ `Audio`, plus the asset layer (`AssetManager`, `SpriteSheet`, `NineSlice`,
236
+ `UIAtlas`, `TileMapRenderer`).
237
+ - **`RGame::Util`** — the graphics-free half, so values can be required with no
238
+ SDL and no OpenGL in the process: `Tensor`, `Color`, `Controls`.
239
+ - **`RGame::Engine`** — the scene graph a game is written in: nodes,
240
+ components, signals, sprites, tile maps, collision and pathfinding. Pure
241
+ Ruby, and unable to name `RGame::Core` at all, which is what lets game logic
242
+ and its specs run with no display.
243
+ - **`RGame::Game`** — the entry point that wires the two halves together.
244
+ - Examples: `14_asteroids`, `15_tiled_world`, `16_hello_world`.
245
+
246
+ [Unreleased]: https://github.com/psuessenb/rgame/compare/v0.4.0...HEAD
247
+ [0.4.0]: https://github.com/psuessenb/rgame/compare/v0.3.1...v0.4.0
248
+ [0.3.1]: https://github.com/psuessenb/rgame/compare/v0.3.0...v0.3.1
249
+ [0.3.0]: https://github.com/psuessenb/rgame/compare/v0.2.0...v0.3.0
250
+ [0.2.0]: https://github.com/psuessenb/rgame/compare/v0.1.0...v0.2.0
251
+ [0.1.0]: https://github.com/psuessenb/rgame/releases/tag/v0.1.0
data/LICENSE ADDED
@@ -0,0 +1,26 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Paul Süßenbach
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ ---
24
+
25
+ This project vendors third-party source under ext/rgame_core/vendor/, which
26
+ carries its own licence terms. See ext/rgame_core/vendor/README.md.
data/README.md ADDED
@@ -0,0 +1,325 @@
1
+ # RGame
2
+
3
+ RGame is a small 2D game engine for Ruby, written in Ruby and C. It's build on top of SDL2, OpenGL and miniaudio. It's built with testability and performance in mind, and aims to be an engine where you can write your whole game code in Ruby, test it as usual with RSpec (or Minitest, or another test framework) and still have acceptable performance.
4
+
5
+ While still a work in progress, RGame aims to be more than a SDL/OpenGL
6
+ binding - it ships with high level features like a scene graph, sprites,
7
+ collision systems, debugging tools and an UI toolkit. You can check out the examples to get a feel for its capabilities.
8
+
9
+ ## Why does this exist and should you use it?
10
+
11
+ RGame is the product of both my lazyness and me looking for something that did not exist. Coming from Ruby on Rails, I wanted to write games while not learning a new language. There are Ruby game engines, but none offered me the option to write modern, standard Ruby and proper specs for the game logic. The closest I found was Gosu, and this project initially started as a "high level engine on top of Gosu", but eventually the limitations of Gosu drove me into rewriting this layer myself.
12
+
13
+ RGame puts a lot of emphazis on testing and being testable: It separates the layers that talk to SDL2/OpenGL from the high level engine concepts, so the whole game logic is testable headless.
14
+
15
+ It also tries to marry the beauty of Ruby with the hard performance requirements of games: Hot paths have no per-frame allocation, because garbadge collection is what really slows down Ruby interpreation, and math-heavy use-cases are backed by C code instead of Ruby classes.
16
+
17
+ Should you use it, though? If you're looking for something mature, free, and more battle-tested take a look at Godot instead. If you're looking for something mature and battle-tested _in Ruby land_, take a look at dragonruby instead (it's not free, but it's probably worth the price).
18
+
19
+ If you're just starting with game development and planning on making the next big indie hit, might as well pick this one as the engine for the game you never finish!
20
+
21
+ In all seriousness, though: This is a hobby project of mine, and while it might develop into something actually useful, at the time of writing it's a playground. If you search for something I searched and found nothing - try RGame! If you want to learn how to write games or just need a small prototype, and you really like Ruby - try RGame! If all you know is Rails, but you want to make a game that doesn't run in a browser and don't care about shipping it - try RGame!
22
+
23
+ I would be really happy if someone else actually uses it, but at this point I can't really recommend it for anything else than small projects and/or learning the ropes of game development.
24
+
25
+ ## Getting started
26
+
27
+ Installing the gem puts an `rgame` command on your PATH, which you can use to setup a project:
28
+
29
+ ```
30
+ gem install rgame
31
+ rgame new tictactoe
32
+
33
+ cd tictactoe
34
+ bundle install
35
+ bundle exec rspec # the game logic, headless — no window needed
36
+ ruby main.rb # the game itself
37
+ ```
38
+
39
+ It's just a small skeleton — a game class, a root node, a spec and the usual configuration — but it is laid out the way the engine wants to be used: one file loads SDL, everything else stays graphics-free and therefore testable with no display. [The `rgame` command](docs/api/cli.md) explains the layout and the reasoning. This is also sets you up with a Rubocop configuration already geared towards RGame and game development in general.
40
+
41
+ **On a common desktop that first line needs nothing else** — no compiler, no SDL2, no header files. `gem install rgame` fetches a gem whose two C extensions are already built, with SDL2 linked into them:
42
+
43
+ - macOS 11 or later on Apple Silicon
44
+ - x86-64 Linux with glibc 2.29 or later
45
+ - 64-bit Windows, on a RubyInstaller Ruby
46
+
47
+ All three need Ruby 4.0. Anywhere else — an Intel Mac, a Raspberry Pi, Ruby 4.1 — `gem install` falls back to the gem that ships the C and compiles it on your machine. That one needs [a compiler and SDL2](#building-from-source).
48
+
49
+ ## Hello world
50
+
51
+ The simplest "game" you can write, all in one file:
52
+
53
+ ```ruby
54
+ require 'rgame/game'
55
+
56
+ class Scene < RGame::Engine::Node2D
57
+ def on_draw(renderer, _view)
58
+ renderer.text('Hello world!', 250, 200)
59
+ end
60
+ end
61
+
62
+ game = RGame::Game.new(
63
+ root: Scene.new,
64
+ caption: 'Hello world!'
65
+ )
66
+
67
+ game.start
68
+ ```
69
+
70
+ You can learn more about how it works in the [documentation](docs/api/README.md).
71
+
72
+ ## Examples
73
+
74
+ `examples/` holds one small program per concept, each a single file you can run with `ruby examples/<name>/main.rb`. [The examples page](docs/api/examples.md) describes them in more detail. They're also all full of code comments so you have an easier time learning the concepts.
75
+
76
+ | Example | Shows |
77
+ |---|---|
78
+ | [walk](docs/api/examples.md#walk) | A player-controlled sprite — a node, three components, input as actions |
79
+ | [sprite](docs/api/examples.md#sprite) | One frame drawn at a node, with no animation behind it |
80
+ | [velocity](docs/api/examples.md#velocity) | Movement with nobody driving: a velocity, integrated |
81
+ | [scroll_map](docs/api/examples.md#scroll_map) | A Tiled map larger than the window, scrolled by a camera |
82
+ | [collision](docs/api/examples.md#collision) | Two shapes touching, and who gets told about it |
83
+ | [collision_tiles](docs/api/examples.md#collision_tiles) | Walking into a wall of solid tiles, and sliding along it |
84
+ | [jump_topdown](docs/api/examples.md#jump_topdown) | A hop in a top-down view, where the sprite rises and the feet stay on the ground |
85
+ | [signals](docs/api/examples.md#signals) | A node announcing something happened, to nobody in particular |
86
+ | [timer](docs/api/examples.md#timer) | Things that happen on a clock, with nothing pressed |
87
+ | [pooling](docs/api/examples.md#pooling) | Spawning a lot of things without allocating them |
88
+ | [game_menu](docs/api/examples.md#game_menu) | A menu over a world that keeps running |
89
+ | [menu_navigation](docs/api/examples.md#menu_navigation) | Several screens, and settings that persist |
90
+ | [radial_menu](docs/api/examples.md#radial_menu) | A wheel of icons, chosen by the direction of the stick |
91
+ | [quick_wheel](docs/api/examples.md#quick_wheel) | A wheel held open by a button and chosen by letting go |
92
+ | [skill_bar](docs/api/examples.md#skill_bar) | A row of tools, stepped through or fired by hotkeys |
93
+ | [sound](docs/api/examples.md#sound) | A sound effect fired by a button, and the seam it travels |
94
+ | [music](docs/api/examples.md#music) | A looping track, started and stopped |
95
+ | [split_screen](docs/api/examples.md#split_screen) | Two players in one world, drawn once per viewport |
96
+ | [input_glyphs](docs/api/examples.md#input_glyphs) | Prompts that match the device in your hands |
97
+ | [fullscreen](docs/api/examples.md#fullscreen) | Fullscreen, switched at any time, and the scale modes |
98
+ | [save_load](docs/api/examples.md#save_load) | Writing game state to disk and putting it back |
99
+ | [save_load_ids](docs/api/examples.md#save_load_ids) | A save that has to name things, and why a reference forces ids |
100
+ | [localization](docs/api/examples.md#localization) | The same screen in two languages, switched and remembered |
101
+ | [pathfinding](docs/api/examples.md#pathfinding) | Setting a target for an actor and let if find its way there |
102
+
103
+ ## Building from source
104
+
105
+ **This section is for two readers: anyone on a platform the binary gems miss, and anyone working on RGame itself.** Installing on one of the three platforms above needs none of it.
106
+
107
+ Compiling the C engine needs a compiler and the system libraries it links against. A source install of the gem needs the compiler, `pkg-config`, SDL2 and OpenGL from the list below. The rest is for running rgame's own suites.
108
+
109
+ ### C engine
110
+
111
+ - A C compiler — `gcc` or `clang`
112
+ - `make`
113
+ - `pkg-config`
114
+ - SDL2 development headers (`sdl2` pkg-config package)
115
+ - OpenGL development headers/libs (provided by Mesa on Linux)
116
+ - [Check](https://libcheck.github.io/check/) (`check` pkg-config package) — C unit test framework, only needed for `make test`
117
+
118
+ PNG decoding, text and audio need no system libraries: `stb_image.h`,
119
+ `stb_truetype.h`, `stb_vorbis.c` and `miniaudio.h` are vendored in
120
+ `ext/rgame_core/vendor/` (public domain / MIT), and the default font ships in
121
+ `lib/rgame/fonts/` (SIL OFL 1.1). miniaudio finds ALSA or PulseAudio at runtime,
122
+ so there is nothing to install for sound either. See the README in
123
+ `ext/rgame_core/vendor/` for all of it.
124
+
125
+ `tools/` holds development tools that are not part of the engine and are not
126
+ built by `make` — currently one, which generates the audio suite's `.ogg`
127
+ fixture and needs `libvorbisenc` to run.
128
+
129
+ ### Ruby side
130
+
131
+ - **Ruby 4.0.5**, pinned in `.ruby-version`.
132
+ - **Ruby development headers.** Version-manager builds (mise, rbenv, rvm, asdf) include them. On a distro-packaged Ruby, install `ruby-dev`
133
+ (Debian/Ubuntu). These are what `extconf.rb` compiles against.
134
+ - **Bundler**, then `bundle install` for the dev/test gems (RSpec, RuboCop).
135
+
136
+ Nothing else. The engine's one runtime gem is `rexml`, which Bundler installs with it, and the `Gemfile` holds only development gems.
137
+
138
+ All three platforms below are built and tested on every push by
139
+ [CI](.github/workflows/ci.yml) - with that people have usually at home, so Apple Silicon and not Apple Intel, etc.
140
+
141
+ ### Debian / Ubuntu
142
+
143
+ ```
144
+ sudo apt install build-essential pkg-config libsdl2-dev libgl1-mesa-dev check
145
+ ```
146
+
147
+ `rake spec:core` opens real windows and starts its own Xvfb, which needs a few
148
+ more packages — the display itself, the `xwininfo` it polls to know the display
149
+ is up, a software rasteriser (Xvfb has no GPU, and `SDL_GL_CreateContext` fails
150
+ without one) and the XTEST runtime for synthetic keystrokes:
151
+
152
+ ```
153
+ sudo apt install xvfb x11-utils libgl1-mesa-dri libxtst6
154
+ ```
155
+
156
+ ### macOS (Homebrew)
157
+
158
+ ```
159
+ brew install sdl2 pkg-config check
160
+ ```
161
+
162
+ OpenGL ships with the Xcode Command Line Tools (`xcode-select --install`) — the
163
+ full Xcode is not needed, and neither is anything else: `rake spec:core` uses
164
+ the native window server, so there is no Xvfb equivalent to set up. Note
165
+ Homebrew's `sdl2` formula now installs **sdl2-compat**, which is SDL2's API
166
+ implemented on top of SDL3; the engine works through it unchanged.
167
+
168
+ ### Windows
169
+
170
+ Use a **RubyInstaller-built** Ruby (mise, vfox and rbenv-style managers all
171
+ fetch those), which is what supplies `ridk`. The combined DevKit installer is
172
+ not required — a standalone MSYS2 that `ridk` can find works just as well, and
173
+ `C:\msys64` is one of the places it looks:
174
+
175
+ ```
176
+ winget install --id MSYS2.MSYS2 -e
177
+ ridk exec pacman -Syu --noconfirm # core update; may need a second pass
178
+ ```
179
+
180
+ Then the libraries. Note `make` is an **msys** package with no prefix while
181
+ everything else is **ucrt64**-prefixed — that split is the whole Windows story:
182
+
183
+ ```
184
+ ridk exec pacman -S --needed \
185
+ mingw-w64-ucrt-x86_64-SDL2 \
186
+ mingw-w64-ucrt-x86_64-check \
187
+ mingw-w64-ucrt-x86_64-pkgconf \
188
+ mingw-w64-ucrt-x86_64-gcc \
189
+ make
190
+ ```
191
+
192
+ **MSYS2 is several environments in one install, and picking the wrong one fails
193
+ in a way that looks like missing packages.** UCRT64 builds native Windows
194
+ binaries, which is what RubyInstaller's Ruby can load; the plain `msys`
195
+ environment builds against a Cygwin-like runtime, which it cannot. Work from
196
+ the "MSYS2 UCRT64" shell or run `ridk enable ucrt64` first, and verify before
197
+ trusting anything:
198
+
199
+ ```
200
+ which gcc # must be /ucrt64/bin/gcc, NOT /usr/bin/gcc
201
+ pkg-config --cflags sdl2 # must print a ucrt64 include path
202
+ ```
203
+
204
+ One more Windows fact worth knowing up front: **Check has no usable fork
205
+ there**, so the first segfault kills the whole test binary and the output stops
206
+ mid-suite. Use a debugger rather than reading the log — `CK_FORK=no gdb --args
207
+ ./build/test_rgame`, and `CK_RUN_SUITE=<name>` to run one suite.
208
+
209
+ ## Build & run
210
+
211
+ ```
212
+ make # builds build/rgame (standalone C binary)
213
+ make run # build and run it
214
+ make test # build and run the Check unit tests (C, pure logic)
215
+ make ext # build both Ruby extensions
216
+ make ext-core # build only ext/rgame_core -> lib/rgame/core_ext.so
217
+ make ext-util # build only ext/rgame_util -> lib/rgame/util_ext.so
218
+ make clean # remove build artifacts, including both extensions'
219
+ ```
220
+
221
+ `make run` opens a window with one of each drawing primitive in it — a rotating
222
+ square, a clipped rectangle, a circle, a thick line, a baked strip replayed
223
+ every frame, and a line of accented text. `Esc` or closing the window quits.
224
+ `ruby ext/rgame_core/example.rb` is the same scene driven from Ruby, and takes
225
+ an optional sound file — `ruby ext/rgame_core/example.rb theme.ogg` binds Space
226
+ to play it as a sample and Return to start and stop it as looping music. That is
227
+ the only place a real sound device is driven; everything automated runs against
228
+ a null or offline one.
229
+
230
+ The Ruby specs:
231
+
232
+ ```
233
+ bundle install
234
+ make ext # both extensions; the suites need the compiled .so files
235
+ rake spec # headless specs: RGame::Util, the engine layer, packaging
236
+ rake spec:core # RGame::Core specs; opens real windows, boots its own Xvfb
237
+ rake # everything: make test, rake spec, rake spec:core
238
+ bundle exec rubocop # lint; configured in .rubocop.yml, which also loads the
239
+ # project's own cops from lib/rgame/rubocop/.
240
+ ```
241
+
242
+ The two Ruby suites are two directories and two processes on purpose. `spec/`
243
+ must never load SDL — the engine layer's whole value is that it can be
244
+ specified with no window — and RSpec loads one root into one process, so a
245
+ single `require "rgame/core"` anywhere would define `RGame::Core` for every
246
+ other example in the run. Separate runners cannot be forgotten the way an
247
+ exclude rule can. `rake spec` needs `make ext-util`; `rake spec:core` needs
248
+ `make ext-core`.
249
+
250
+ Each `make ext-*` target compiles its extension and copies the resulting `.so`
251
+ into `lib/rgame/`, which is where `require "rgame/util_ext"` and `require
252
+ "rgame/core_ext"` look for it. That mirrors how rake-compiler installs a
253
+ compiled extension into `lib/<gem>/`. Without that step the specs can't even
254
+ load, since `RGame::Util::Tensor` now lives in C.
255
+
256
+ `rake spec` needs only `ext-util` — it never touches `RGame::Core`, which is
257
+ what keeps it runnable with no display and no SDL. To drive the *engine* from
258
+ Ruby by hand (opens a real window):
259
+
260
+ ```
261
+ make ext-core
262
+ ruby ext/rgame_core/example.rb
263
+ ```
264
+
265
+ ## Packaging
266
+
267
+ Both extensions and the Ruby layer ship as one gem, built from `rgame.gemspec` and published at [rubygems.org/gems/rgame](https://rubygems.org/gems/rgame):
268
+
269
+ ```
270
+ gem install rgame # from RubyGems; compiles nothing on a covered platform
271
+ ```
272
+
273
+ Or from a checkout, which builds the source gem locally:
274
+
275
+ ```
276
+ rake build # package into pkg/rgame-<version>.gem
277
+ gem install pkg/rgame-*.gem # compiles both extensions on this machine
278
+ ```
279
+
280
+ **RubyGems holds two kinds of gem for each version.** The source gem carries the
281
+ C and compiles on install. Three platform gems carry `core_ext` and `util_ext`
282
+ already built, with SDL2 linked statically into `core_ext`; they ship no `.c`,
283
+ no `extconf.rb` and declare no extensions, so they compile nothing. `gem
284
+ install` picks by platform and Ruby version on its own.
285
+
286
+ CI builds and publishes all four. `tools/platform_gem.rake` builds a platform
287
+ gem, `tools/check_platform_gem.rb` checks it against nine rules before it may be
288
+ published, and the `smoke` job installs it on a runner with no SDL2 and plays
289
+ the examples out of it.
290
+
291
+ Installing the source gem runs each `extconf.rb` and installs the resulting
292
+ `.so` into the gem's own `lib/rgame/`, which is the same layout `make ext`
293
+ produces in a checkout — so `require "rgame"` and `require "rgame/core"` behave
294
+ identically whichever gem you got, including the guarantee that the first of
295
+ those loads no graphics libraries. Its system dependencies are the ones the C
296
+ engine needs (SDL2, OpenGL, pkg-config, a compiler); `extconf.rb` aborts with
297
+ the package to install if one is missing, rather than failing later at the link
298
+ step.
299
+
300
+ What ships is a glob over `lib/`, `ext/` and `docs/api/`, not a hand-written
301
+ list: a new C source or a runtime asset dropped into either tree is packaged
302
+ without being registered anywhere. `spec/packaging_spec.rb` holds that up from
303
+ the other side — it asserts every source, header and data file is in the gem and
304
+ that no build artifact, spec directory or plan is, so the parts of this that
305
+ would otherwise be a checklist fail the suite instead.
306
+
307
+ The version is `RGame::VERSION` in [lib/rgame/version.rb](lib/rgame/version.rb).
308
+
309
+ ## Project structure
310
+
311
+ The C lives under `ext/rgame_core/` rather than a top-level `src/`, because`gem install` runs each `extconf.rb` and an extension can only build sources inside its own directory — so one copy of the code serves both the standalone binary and the gem. The Ruby half is split the same way it is namespaced:
312
+ `lib/rgame/util/`, `lib/rgame/core/` and `lib/rgame/engine/`.
313
+
314
+ A file-by-file map of the whole repository is in
315
+ [docs/project_structure.md](docs/project_structure.md).
316
+
317
+ ## Roadmap
318
+
319
+ There is currently no roadmap for the next version.
320
+
321
+ ## AI clause
322
+
323
+ This project is not vibe-coded, but AI tools were used heavily while
324
+ writing code. If you dislike AI generated code, this project is not for
325
+ you.