rgame 0.1.0

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 (161) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +26 -0
  3. data/README.md +406 -0
  4. data/docs/api/README.md +167 -0
  5. data/docs/api/app.md +192 -0
  6. data/docs/api/assets.md +426 -0
  7. data/docs/api/audio.md +208 -0
  8. data/docs/api/components.md +321 -0
  9. data/docs/api/drawing.md +330 -0
  10. data/docs/api/game.md +99 -0
  11. data/docs/api/images.md +118 -0
  12. data/docs/api/input.md +179 -0
  13. data/docs/api/internals.md +110 -0
  14. data/docs/api/scene_graph.md +159 -0
  15. data/docs/api/signals.md +142 -0
  16. data/docs/api/systems.md +98 -0
  17. data/docs/api/text.md +116 -0
  18. data/docs/api/toolbox.md +240 -0
  19. data/docs/api/values.md +101 -0
  20. data/ext/README.md +225 -0
  21. data/ext/rgame_core/app/app.c +721 -0
  22. data/ext/rgame_core/app/app_gl.h +64 -0
  23. data/ext/rgame_core/app/frame_loop.c +42 -0
  24. data/ext/rgame_core/app/frame_loop.h +54 -0
  25. data/ext/rgame_core/audio/audio.c +466 -0
  26. data/ext/rgame_core/audio/audio_internal.h +45 -0
  27. data/ext/rgame_core/audio/vorbis_decoder.c +282 -0
  28. data/ext/rgame_core/audio/vorbis_decoder.h +45 -0
  29. data/ext/rgame_core/example.rb +188 -0
  30. data/ext/rgame_core/extconf.rb +167 -0
  31. data/ext/rgame_core/graphics/backend.c +52 -0
  32. data/ext/rgame_core/graphics/backend.h +64 -0
  33. data/ext/rgame_core/graphics/canvas.c +247 -0
  34. data/ext/rgame_core/graphics/canvas.h +143 -0
  35. data/ext/rgame_core/graphics/clip.c +87 -0
  36. data/ext/rgame_core/graphics/clip.h +89 -0
  37. data/ext/rgame_core/graphics/draw_queue.c +216 -0
  38. data/ext/rgame_core/graphics/draw_queue.h +174 -0
  39. data/ext/rgame_core/graphics/gl_backend.c +122 -0
  40. data/ext/rgame_core/graphics/gl_backend.h +43 -0
  41. data/ext/rgame_core/graphics/image.c +304 -0
  42. data/ext/rgame_core/graphics/image_internal.h +30 -0
  43. data/ext/rgame_core/graphics/primitives.c +189 -0
  44. data/ext/rgame_core/graphics/primitives.h +111 -0
  45. data/ext/rgame_core/graphics/recording.c +119 -0
  46. data/ext/rgame_core/graphics/recording.h +88 -0
  47. data/ext/rgame_core/graphics/texture.c +181 -0
  48. data/ext/rgame_core/graphics/texture.h +165 -0
  49. data/ext/rgame_core/graphics/transform.c +128 -0
  50. data/ext/rgame_core/graphics/transform.h +106 -0
  51. data/ext/rgame_core/include/rgame/core.h +577 -0
  52. data/ext/rgame_core/input/device_slots.c +103 -0
  53. data/ext/rgame_core/input/device_slots.h +93 -0
  54. data/ext/rgame_core/input/gamepad.c +145 -0
  55. data/ext/rgame_core/input/gamepad.h +63 -0
  56. data/ext/rgame_core/input/input.c +109 -0
  57. data/ext/rgame_core/input/input.h +99 -0
  58. data/ext/rgame_core/ruby/audio_ext.c +321 -0
  59. data/ext/rgame_core/ruby/core_ext.c +513 -0
  60. data/ext/rgame_core/ruby/core_ext.h +51 -0
  61. data/ext/rgame_core/ruby/font_ext.c +168 -0
  62. data/ext/rgame_core/ruby/image_ext.c +230 -0
  63. data/ext/rgame_core/ruby/recording_ext.c +186 -0
  64. data/ext/rgame_core/ruby/renderer_ext.c +376 -0
  65. data/ext/rgame_core/text/atlas.c +59 -0
  66. data/ext/rgame_core/text/atlas.h +85 -0
  67. data/ext/rgame_core/text/font.c +281 -0
  68. data/ext/rgame_core/text/font.h +139 -0
  69. data/ext/rgame_core/text/font_atlas.c +385 -0
  70. data/ext/rgame_core/text/font_internal.h +47 -0
  71. data/ext/rgame_core/text/glyph_cache.c +142 -0
  72. data/ext/rgame_core/text/glyph_cache.h +89 -0
  73. data/ext/rgame_core/vendor/README.md +159 -0
  74. data/ext/rgame_core/vendor/miniaudio.h +95864 -0
  75. data/ext/rgame_core/vendor/miniaudio_impl.c +62 -0
  76. data/ext/rgame_core/vendor/stb_image.h +7988 -0
  77. data/ext/rgame_core/vendor/stb_image_impl.c +31 -0
  78. data/ext/rgame_core/vendor/stb_truetype.h +5079 -0
  79. data/ext/rgame_core/vendor/stb_truetype_impl.c +23 -0
  80. data/ext/rgame_core/vendor/stb_vorbis.c +5584 -0
  81. data/ext/rgame_core/vendor/stb_vorbis_impl.c +29 -0
  82. data/ext/rgame_util/color.c +19 -0
  83. data/ext/rgame_util/color.h +60 -0
  84. data/ext/rgame_util/color_ext.c +156 -0
  85. data/ext/rgame_util/extconf.rb +27 -0
  86. data/ext/rgame_util/tensor.c +186 -0
  87. data/ext/rgame_util/util_ext.c +27 -0
  88. data/ext/rgame_util/util_ext.h +16 -0
  89. data/lib/rgame/boot.rb +13 -0
  90. data/lib/rgame/core/app.rb +82 -0
  91. data/lib/rgame/core/asset_manager.rb +224 -0
  92. data/lib/rgame/core/audio.rb +124 -0
  93. data/lib/rgame/core/font.rb +49 -0
  94. data/lib/rgame/core/gamepad.rb +55 -0
  95. data/lib/rgame/core/image.rb +55 -0
  96. data/lib/rgame/core/input.rb +77 -0
  97. data/lib/rgame/core/nine_slice.rb +163 -0
  98. data/lib/rgame/core/recording.rb +52 -0
  99. data/lib/rgame/core/renderer.rb +363 -0
  100. data/lib/rgame/core/sprite_sheet.rb +108 -0
  101. data/lib/rgame/core/tile_map_renderer.rb +160 -0
  102. data/lib/rgame/core/ui_atlas.rb +86 -0
  103. data/lib/rgame/core.rb +24 -0
  104. data/lib/rgame/engine/actor.rb +53 -0
  105. data/lib/rgame/engine/animation_set.rb +49 -0
  106. data/lib/rgame/engine/animator.rb +44 -0
  107. data/lib/rgame/engine/audio_bus.rb +24 -0
  108. data/lib/rgame/engine/audio_director.rb +29 -0
  109. data/lib/rgame/engine/body.rb +49 -0
  110. data/lib/rgame/engine/cached_label.rb +33 -0
  111. data/lib/rgame/engine/camera.rb +33 -0
  112. data/lib/rgame/engine/camera_view.rb +28 -0
  113. data/lib/rgame/engine/circle_collider.rb +32 -0
  114. data/lib/rgame/engine/collision_box.rb +34 -0
  115. data/lib/rgame/engine/collision_system.rb +44 -0
  116. data/lib/rgame/engine/component.rb +30 -0
  117. data/lib/rgame/engine/components/action_trigger.rb +41 -0
  118. data/lib/rgame/engine/components/animated_sprite.rb +63 -0
  119. data/lib/rgame/engine/components/character_body.rb +70 -0
  120. data/lib/rgame/engine/components/circle_collider.rb +44 -0
  121. data/lib/rgame/engine/components/collision_world.rb +103 -0
  122. data/lib/rgame/engine/components/despawn_offscreen.rb +26 -0
  123. data/lib/rgame/engine/components/path_follow.rb +84 -0
  124. data/lib/rgame/engine/components/player_controller.rb +24 -0
  125. data/lib/rgame/engine/components/pool.rb +53 -0
  126. data/lib/rgame/engine/components/screen_wrap.rb +27 -0
  127. data/lib/rgame/engine/components/sprite.rb +31 -0
  128. data/lib/rgame/engine/components/targeting.rb +54 -0
  129. data/lib/rgame/engine/components/thrust_controller.rb +65 -0
  130. data/lib/rgame/engine/components/tile_world.rb +68 -0
  131. data/lib/rgame/engine/components/timer.rb +75 -0
  132. data/lib/rgame/engine/components/velocity.rb +27 -0
  133. data/lib/rgame/engine/components/wander_controller.rb +60 -0
  134. data/lib/rgame/engine/debug_overlay.rb +106 -0
  135. data/lib/rgame/engine/i18n.rb +97 -0
  136. data/lib/rgame/engine/input/action_mapper.rb +46 -0
  137. data/lib/rgame/engine/input/actions.rb +41 -0
  138. data/lib/rgame/engine/input/player_controller.rb +14 -0
  139. data/lib/rgame/engine/matrix.rb +32 -0
  140. data/lib/rgame/engine/node2d.rb +271 -0
  141. data/lib/rgame/engine/path.rb +78 -0
  142. data/lib/rgame/engine/pool.rb +51 -0
  143. data/lib/rgame/engine/resettable.rb +67 -0
  144. data/lib/rgame/engine/scene/scene_stack.rb +65 -0
  145. data/lib/rgame/engine/signal.rb +75 -0
  146. data/lib/rgame/engine/spatial_hash.rb +71 -0
  147. data/lib/rgame/engine/tile_collision.rb +78 -0
  148. data/lib/rgame/engine/tile_map.rb +149 -0
  149. data/lib/rgame/engine/tileset.rb +101 -0
  150. data/lib/rgame/engine/timer.rb +51 -0
  151. data/lib/rgame/engine.rb +68 -0
  152. data/lib/rgame/fonts/LiberationSans-Regular.ttf +0 -0
  153. data/lib/rgame/fonts/OFL.txt +102 -0
  154. data/lib/rgame/game.rb +129 -0
  155. data/lib/rgame/util/color.rb +27 -0
  156. data/lib/rgame/util/controls.rb +107 -0
  157. data/lib/rgame/util/tensor.rb +12 -0
  158. data/lib/rgame/util.rb +8 -0
  159. data/lib/rgame/version.rb +12 -0
  160. data/lib/rgame.rb +20 -0
  161. metadata +215 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6d5bcc5fc00641f0ff886974c919bdebc2c9ae3260d4333740243a4ff118ed55
4
+ data.tar.gz: 42d88501d55f42b2ecc2a037d080e74d2f1420bf46ad1aa965cc1cc069288d7e
5
+ SHA512:
6
+ metadata.gz: d119e1dfe2b47bbc9444633a95cc8b3d4d4c4a1c2ae94329993e107df68f5f63421581c4bee023bf7ebaeca736410fbcca7c2366d8ba5fbdbc338421a11b545e
7
+ data.tar.gz: de260cc2b6a2c8ebf01db793da132d11a1bf5fa973426e4b846b22cdab369f7674f5045b48231b0a823961c5a36a220d3a09db67a601fc60998a44878f856872
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,406 @@
1
+ # RGame
2
+
3
+ A small 2D game engine written in Ruby and C, on top of SDL2, OpenGL and miniaudio.
4
+
5
+ ## Why does this exist and should you use it?
6
+
7
+ 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.
8
+
9
+ 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.
10
+
11
+ 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.
12
+
13
+ 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).
14
+
15
+ 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!
16
+
17
+ 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, you can try this. 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.
18
+
19
+ ## Hello world
20
+
21
+ ```
22
+ require 'rgame/game'
23
+
24
+ class Scene < RGame::Engine::Node2D
25
+ def on_draw(renderer)
26
+ renderer.text('Hello world!', 250, 200)
27
+ end
28
+ end
29
+
30
+ game = RGame::Game.new(
31
+ root: Scene.new,
32
+ caption: 'Hello world!'
33
+ )
34
+
35
+ game.start
36
+ ```
37
+
38
+ You can learn more about how it works in the [documentation](docs/api/README.md).
39
+
40
+ # Where does stuff live
41
+
42
+ Everything Ruby-visible lives under the `RGame` module, split in two by what it
43
+ depends on:
44
+
45
+ | | `RGame::Core` | `RGame::Util` |
46
+ |---|---|---|
47
+ | For | anything depending on SDL/OpenGL (or on something that does) | everything else |
48
+ | C source | `ext/rgame_core/` | `ext/rgame_util/` |
49
+ | Extension | `rgame/core_ext` | `rgame/util_ext` |
50
+ | Links | SDL2 + OpenGL + pthread | nothing but Ruby |
51
+ | Holds today | `App` — window, GL context, fixed-timestep main loop; `Input`, `Gamepad`, `Image`, `Renderer`, `Recording`, `Font`, `Audio`, `Sample`, `Song` | `Tensor`, `Controls`, `Color` |
52
+
53
+ That split is load-bearing, not cosmetic: `require "rgame"` gives you the value
54
+ types *and* the scene graph with **no graphics libraries loaded into the process
55
+ at all**, so game logic and its specs run with no display and no SDL present.
56
+ `RGame::Core` is an explicit opt-in:
57
+
58
+ ```ruby
59
+ require "rgame" # RGame::Util + RGame::Engine, no graphics
60
+ require "rgame/core" # adds RGame::Core, pulls in SDL2 + OpenGL
61
+ require "rgame/game" # all of it, wired — what a game writes
62
+ ```
63
+
64
+ The engine opens a window, runs a fixed-timestep loop, reads keyboard and
65
+ controllers, loads PNGs onto the GPU, draws shapes, sprites and text through
66
+ a z-sorted batching renderer, and plays Ogg Vorbis and WAV. `RGame::Game` puts
67
+ those together with a scene graph, and the two games under `examples/` run on
68
+ it. Its C sources build two ways from one copy: a standalone binary (`build/rgame`, via the root
69
+ `Makefile`) and the `core_ext` extension (via `extconf.rb`).
70
+
71
+ Both halves ship as one gem — `rgame.gemspec` builds both extensions — though
72
+ nothing is published yet, so it is installed from a checkout or a built `.gem`.
73
+
74
+ **[docs/api/](docs/api/README.md) is the reference documentation** for using the
75
+ engine from Ruby: the entry point, the frame loop, input, drawing, text, audio,
76
+ assets and the value types. Start there if you want to write a game rather than
77
+ work on the engine.
78
+
79
+ ## Requirements
80
+
81
+ ### C engine
82
+
83
+ - A C compiler — `gcc` or `clang`
84
+ - `make`
85
+ - `pkg-config`
86
+ - SDL2 development headers (`sdl2` pkg-config package)
87
+ - OpenGL development headers/libs (provided by Mesa on Linux)
88
+ - [Check](https://libcheck.github.io/check/) (`check` pkg-config package) — C unit test framework, only needed for `make test`
89
+
90
+ PNG decoding, text and audio need no system libraries: `stb_image.h`,
91
+ `stb_truetype.h`, `stb_vorbis.c` and `miniaudio.h` are vendored in
92
+ `ext/rgame_core/vendor/` (public domain / MIT), and the default font ships in
93
+ `lib/rgame/fonts/` (SIL OFL 1.1). miniaudio finds ALSA or PulseAudio at runtime,
94
+ so there is nothing to install for sound either. See the README in
95
+ `ext/rgame_core/vendor/` for all of it.
96
+
97
+ `tools/` holds development tools that are not part of the engine and are not
98
+ built by `make` — currently one, which generates the audio suite's `.ogg`
99
+ fixture and needs `libvorbisenc` to run.
100
+
101
+ ### Ruby side
102
+
103
+ - **Ruby 4.0.5**, pinned in `.ruby-version`. Installed here with
104
+ [mise](https://mise.jdx.dev) (`mise install` in the project root picks up
105
+ `.ruby-version`); any version manager that reads `.ruby-version` works just
106
+ as well.
107
+ - **Ruby development headers.** Version-manager builds (mise, rbenv, rvm,
108
+ asdf) include them. On a distro-packaged Ruby, install `ruby-dev`
109
+ (Debian/Ubuntu). These are what `extconf.rb` compiles against.
110
+ - **Bundler**, then `bundle install` for the dev/test gems (RSpec, RuboCop).
111
+
112
+ Nothing else — the engine has no runtime Ruby dependencies, and the `Gemfile`
113
+ holds only development gems.
114
+
115
+ ### Debian / Ubuntu
116
+
117
+ ```
118
+ sudo apt install build-essential pkg-config libsdl2-dev libgl1-mesa-dev check
119
+ ```
120
+
121
+ ### macOS (Homebrew)
122
+
123
+ ```
124
+ brew install sdl2 pkg-config check
125
+ ```
126
+
127
+ OpenGL headers/libs ship with Xcode Command Line Tools (`xcode-select --install`).
128
+
129
+ ## Build & run
130
+
131
+ ```
132
+ make # builds build/rgame (standalone C binary)
133
+ make run # build and run it
134
+ make test # build and run the Check unit tests (C, pure logic)
135
+ make ext # build both Ruby extensions
136
+ make ext-core # build only ext/rgame_core -> lib/rgame/core_ext.so
137
+ make ext-util # build only ext/rgame_util -> lib/rgame/util_ext.so
138
+ make clean # remove build artifacts, including both extensions'
139
+ ```
140
+
141
+ `make run` opens a window with one of each drawing primitive in it — a rotating
142
+ square, a clipped rectangle, a circle, a thick line, a baked strip replayed
143
+ every frame, and a line of accented text. `Esc` or closing the window quits.
144
+ `ruby ext/rgame_core/example.rb` is the same scene driven from Ruby, and takes
145
+ an optional sound file — `ruby ext/rgame_core/example.rb theme.ogg` binds Space
146
+ to play it as a sample and Return to start and stop it as looping music. That is
147
+ the only place a real sound device is driven; everything automated runs against
148
+ a null or offline one.
149
+
150
+ The Ruby specs:
151
+
152
+ ```
153
+ bundle install
154
+ make ext # both extensions; the suites need the compiled .so files
155
+ rake spec # headless specs: RGame::Util, the engine layer, packaging
156
+ rake spec:core # RGame::Core specs; opens real windows, boots its own Xvfb
157
+ rake # everything: make test, rake spec, rake spec:core
158
+ bundle exec rubocop # lint; configured in .rubocop.yml, which also loads the
159
+ # project's own cops from rubocop/cop/game/.
160
+ ```
161
+
162
+ The two Ruby suites are two directories and two processes on purpose. `spec/`
163
+ must never load SDL — the engine layer's whole value is that it can be
164
+ specified with no window — and RSpec loads one root into one process, so a
165
+ single `require "rgame/core"` anywhere would define `RGame::Core` for every
166
+ other example in the run. Separate runners cannot be forgotten the way an
167
+ exclude rule can. `rake spec` needs `make ext-util`; `rake spec:core` needs
168
+ `make ext-core`.
169
+
170
+ Each `make ext-*` target compiles its extension and copies the resulting `.so`
171
+ into `lib/rgame/`, which is where `require "rgame/util_ext"` and `require
172
+ "rgame/core_ext"` look for it. That mirrors how rake-compiler installs a
173
+ compiled extension into `lib/<gem>/`. Without that step the specs can't even
174
+ load, since `RGame::Util::Tensor` now lives in C.
175
+
176
+ `rake spec` needs only `ext-util` — it never touches `RGame::Core`, which is
177
+ what keeps it runnable with no display and no SDL. To drive the *engine* from
178
+ Ruby by hand (opens a real window):
179
+
180
+ ```
181
+ make ext-core
182
+ ruby ext/rgame_core/example.rb
183
+ ```
184
+
185
+ ## Packaging
186
+
187
+ Both extensions and the Ruby layer ship as one gem, built from `rgame.gemspec`:
188
+
189
+ ```
190
+ rake build # package into pkg/rgame-<version>.gem
191
+ gem install pkg/rgame-0.1.0.gem # compiles both extensions on this machine
192
+ ```
193
+
194
+ `gem install` runs each `extconf.rb` and installs the resulting `.so` into the
195
+ gem's own `lib/rgame/`, which is the same layout `make ext` produces in a
196
+ checkout — so `require "rgame"` and `require "rgame/core"` behave identically
197
+ either way, including the guarantee that the first of those loads no graphics
198
+ libraries. The system dependencies are the same ones the C engine needs
199
+ (SDL2, OpenGL, pkg-config, a compiler); `extconf.rb` aborts with the package to
200
+ install if one is missing, rather than failing later at the link step.
201
+
202
+ What ships is a glob over `lib/`, `ext/` and `docs/api/`, not a hand-written
203
+ list: a new C source or a runtime asset dropped into either tree is packaged
204
+ without being registered anywhere. `spec/packaging_spec.rb` holds that up from
205
+ the other side — it asserts every source, header and data file is in the gem and
206
+ that no build artifact, spec directory or plan is, so the parts of this that
207
+ would otherwise be a checklist fail the suite instead.
208
+
209
+ The version is `RGame::VERSION` in [lib/rgame/version.rb](lib/rgame/version.rb).
210
+ Nothing is published to RubyGems.
211
+
212
+ ## Project structure
213
+
214
+ The engine C lives under `ext/rgame_core/` — a Ruby C extension directory —
215
+ rather than a top-level `src/`. That's deliberate: `gem install` unpacks the gem
216
+ and runs each `extconf.rb`, which can only build sources inside its own
217
+ directory, so keeping the C there means one copy of the code serves both the
218
+ standalone binary and the gem.
219
+
220
+ ```
221
+ ext/rgame_core/ RGame::Core — the SDL/GL half. The sources are
222
+ grouped by subsystem, one folder each, and a file
223
+ names the folder it includes from: graphics/canvas.c
224
+ says #include "graphics/clip.h".
225
+ include/rgame/core.h Public C API (opaque handle, no SDL/GL types
226
+ leaked) — what both src/main.c and the extension
227
+ bind against.
228
+ app/ The window, the context and the loop.
229
+ app.c Engine implementation: SDL window + OpenGL context
230
+ setup; owns the main loop and calls back to the
231
+ caller's update/draw callbacks.
232
+ app_gl.h Private: the GL context behind the opaque handle.
233
+ frame_loop.h/.c Pure fixed-timestep + FPS logic, no SDL/GL — unit-
234
+ tested without a window (see CLAUDE.md's layering).
235
+ graphics/ Everything on the drawing path.
236
+ transform.h/.c Pure 2D affine transform stack — rotate, scale,
237
+ translate, composed. No SDL.
238
+ clip.h/.c Pure rects and the intersecting clip stack, in
239
+ screen space. No SDL.
240
+ draw_queue.h/.c Pure z-sort and batching: collects draw commands,
241
+ orders them by z, merges what can share a GL call.
242
+ canvas.h/.c Pure composition of transform + clip + queue; the
243
+ seam the drawing API is written against.
244
+ backend.h/.c The layer-2 seam: a function-pointer table a real
245
+ GL backend or a recording fake plugs into, plus
246
+ the loop that drives it from a prepared frame.
247
+ texture.h/.c Pure: refcounted texture sheets, the sub-rects
248
+ sprites cut out of them, and pixels -> UVs.
249
+ primitives.h/.c Pure: rects, thick lines, circles and sprites, in
250
+ terms of the canvas's triangles and quads.
251
+ recording.h/.c Pure: a baked block of drawing, kept between
252
+ frames and replayed as one call per texture.
253
+ gl_backend.h/.c The real GL calls — the only file that issues
254
+ them on the drawing path.
255
+ image.c Decode a PNG and upload it — the thin GL shim
256
+ over texture.h. Views share one upload.
257
+ image_internal.h What the draw path needs from inside an image.
258
+ text/ Glyphs, from a .ttf to a texture page.
259
+ atlas.h/.c Pure: shelf packing for the glyph atlas — where
260
+ the next glyph goes on a texture page.
261
+ glyph_cache.h/.c Pure: codepoint -> rasterised glyph, open
262
+ addressed, never evicted.
263
+ font.h/.c Pure: a typeface at one size — glyph metrics,
264
+ kerning, rasterisation and UTF-8, over
265
+ stb_truetype. No atlas, no GL.
266
+ font_atlas.c Composes font + atlas + glyph cache and owns the
267
+ GL pages — the only text file that calls gl*.
268
+ font_internal.h What the draw path needs from inside a font.
269
+ input/ Keyboard and controllers.
270
+ input.h/.c Pure input snapshot + the flat button-id space
271
+ (keyboard and gamepad ranges). No SDL.
272
+ device_slots.h/.c Pure player-slot table for controllers: keeps a
273
+ player on the same slot across a disconnect. No SDL.
274
+ gamepad.h/.c Thin SDL_GameController shim: opens/closes pads on
275
+ hot-plug and copies their state into the snapshot.
276
+ audio/ Sound, which touches neither SDL nor GL.
277
+ audio.c The sound device, samples and songs — miniaudio
278
+ talks to the platform directly.
279
+ audio_internal.h The live-sound counter, for tests.
280
+ vorbis_decoder.h/.c Ogg Vorbis for miniaudio, over stb_vorbis —
281
+ miniaudio cannot read ogg on its own.
282
+ ruby/ The Ruby-facing glue, and the only C here that
283
+ includes ruby.h.
284
+ core_ext.c VALUE wrappers + callback trampolines, and the
285
+ extension's entry point.
286
+ core_ext.h One init function per Ruby-visible class here.
287
+ image_ext.c RGame::Core::Image — the Ruby binding.
288
+ audio_ext.c RGame::Core::Audio, Sample and Song — the
289
+ bindings; three classes in one file because they
290
+ share a wrapping shape.
291
+ renderer_ext.c RGame::Core::Renderer — the drawing primitives.
292
+ font_ext.c RGame::Core::Font — the Ruby binding.
293
+ recording_ext.c RGame::Core::Recording — baked, replayable draws.
294
+ vendor/ Third-party sources + their licences.
295
+ <name>_impl.c One per vendored library (stb_image, stb_truetype,
296
+ stb_vorbis, miniaudio): instantiates it and picks
297
+ its features. The only files built without
298
+ -Wall -Wextra; the suffix is what selects that.
299
+ extconf.rb mkmf script; pkg_config("sdl2"), -lGL. It lists
300
+ the subsystem folders, because mkmf's own default
301
+ only finds sources one level up from here.
302
+ example.rb Manual smoke test driven from Ruby.
303
+
304
+ ext/rgame_util/ RGame::Util — the graphics-free half, so pure-data
305
+ helpers can be required without pulling in SDL/GL.
306
+ util_ext.c Entry point; hands RGame::Util to each class init.
307
+ tensor.c RGame::Util::Tensor — flat-array 3D grid.
308
+ color.c/.h Pure RGBA packing, no Ruby — Check-tested.
309
+ color_ext.c RGame::Util::Color — the Ruby binding over it.
310
+ extconf.rb mkmf script; no pkg_config, no -lGL.
311
+
312
+ lib/rgame.rb `require "rgame"` — loads RGame::Util only.
313
+ lib/rgame/version.rb RGame::VERSION, and nothing else — the gemspec
314
+ loads this file before anything is compiled.
315
+ lib/rgame/util.rb Namespace loader.
316
+ lib/rgame/util/tensor.rb Requires the compiled rgame/util_ext.
317
+ lib/rgame/util/controls.rb Input id vocabulary (keys, pad buttons, axes,
318
+ device slots) + default bindings. Pure Ruby
319
+ values, so a game may name them without Core.
320
+ lib/rgame/util/color.rb Requires the compiled rgame/util_ext for Color.
321
+ lib/rgame/core.rb `require "rgame/core"` — opt-in, loads SDL/GL.
322
+ lib/rgame/core/app.rb Requires the compiled rgame/core_ext.
323
+ lib/rgame/core/input.rb Symbolic action -> button, over the C queries.
324
+ lib/rgame/core/gamepad.rb Which controllers are plugged in, and their names.
325
+ lib/rgame/core/image.rb Sprite-sheet slicing over the C-backed Image.
326
+ lib/rgame/core/renderer.rb Keyword args, colours and transform blocks over
327
+ the C-backed Renderer.
328
+ lib/rgame/core/recording.rb #draw over the C-backed Recording.
329
+ lib/rgame/core/font.rb The default font path, over the C-backed Font.
330
+ lib/rgame/fonts/ The default font shipped with the engine:
331
+ Liberation Sans 2.1.5 (SIL OFL 1.1). Data read at
332
+ runtime, so it lives here rather than in ext/.
333
+ lib/rgame/*.so Build artifacts, copied here by `make ext`.
334
+
335
+ src/main.c Standalone executable entry point — the C
336
+ equivalent of example.rb. Only talks to
337
+ rgame/core.h, never touches SDL/GL directly. Kept
338
+ outside ext/ so mkmf doesn't compile its main()
339
+ into the extension.
340
+
341
+ test/ Check unit tests for the pure C logic (`make test`).
342
+ test_main.c Runs every suite; one binary, build/test_rgame.
343
+ suites.h Each test_<x>.c exposes a Suite, declared here.
344
+ support/ Test-only helpers, e.g. the recording draw backend
345
+ that stands in for OpenGL.
346
+ spec/ Headless RSpec specs: RGame::Util and
347
+ RGame::Engine (`rake spec`). Never loads SDL.
348
+ packaging_spec.rb What the gem ships, asserted against the tree so a
349
+ new source or data file cannot be left out of it.
350
+ spec_core/ RSpec specs for RGame::Core (`rake spec:core`).
351
+ Opens real windows; boots its own Xvfb.
352
+ docs/ The feature spec the engine is being built out to.
353
+ api/ Reference documentation for using it from Ruby.
354
+
355
+ rgame.gemspec Packages both halves as one gem: both extconf.rb
356
+ files, and a globbed file list so a new source or
357
+ asset ships without being listed anywhere.
358
+ ```
359
+
360
+ Three test suites: `make test` covers the C (Check), `rake spec` the headless
361
+ Ruby half, `rake spec:core` the parts that open a window. None needs a display
362
+ of its own — `spec:core` boots Xvfb itself. `rake` runs all three.
363
+
364
+ ## Roadmap
365
+
366
+ 1. **C core** (in progress) — SDL2 window, OpenGL rendering, basic app loop.
367
+ Drawing primitives are the current gap.
368
+ 2. **Ruby C extensions** (done in first form) — `RGame::Core::App` wraps
369
+ `include/rgame/core.h`, so the engine can be driven from Ruby
370
+ (`ext/rgame_core/example.rb`); `RGame::Util::Tensor` covers the
371
+ graphics-free half.
372
+ 3. **Pure-Ruby half** (started) — `lib/` holds the namespace loaders; so far
373
+ the classes underneath them are all C-backed.
374
+ 4. **Gem** (done) — `rgame.gemspec` packages both halves, building each
375
+ extension into `lib/rgame/` on install the way `make ext` does in a
376
+ checkout. Not published to RubyGems.
377
+
378
+ ## Ruby API
379
+
380
+ See [ext/README.md](ext/README.md) for detail.
381
+
382
+ ```ruby
383
+ require "rgame/core"
384
+
385
+ class MyGame < RGame::Core::App
386
+ def initialize = super(width: 800, height: 600, caption: "title")
387
+
388
+ def update(dt); end # one fixed simulation tick
389
+ def draw; end # render one frame
390
+ def needs_redraw?; end # false skips the draw
391
+ def button_down(id); end # discrete key press
392
+ end
393
+
394
+ MyGame.new.run
395
+ ```
396
+
397
+ The util half, with no graphics libraries loaded:
398
+
399
+ ```ruby
400
+ require "rgame"
401
+
402
+ grid = RGame::Util::Tensor.new(width, height, depth, initial: nil)
403
+ grid[x, y, z] = value
404
+ grid[x, y, z]
405
+ grid.width # => Integer, also #height / #depth
406
+ ```
@@ -0,0 +1,167 @@
1
+ # rgame API guide
2
+
3
+ Reference documentation for using rgame from Ruby. The engine is written in C
4
+ and exposed as two Ruby extensions; nothing here assumes you will read or write
5
+ any C.
6
+
7
+ | Page | Covers |
8
+ |---|---|
9
+ | This page | Loading the library, the two namespaces, a working program, testing |
10
+ | [App](app.md) | `RGame::Core::App` — the window and the frame loop |
11
+ | [Game](game.md) | `RGame::Game` — the entry point that wires both halves together |
12
+ | [Input](input.md) | `RGame::Core::Input`, `RGame::Util::Controls`, `RGame::Core::Gamepad` |
13
+ | [Drawing](drawing.md) | `RGame::Core::Renderer` — shapes, images, transforms, clipping, recordings |
14
+ | [Images](images.md) | `RGame::Core::Image` — loading PNGs, subimages, sprite sheets |
15
+ | [Text](text.md) | `RGame::Core::Font` and `Renderer#text` |
16
+ | [Audio](audio.md) | `RGame::Core::Audio`, `Sample`, `Song` — samples and streamed music |
17
+ | [Sheets, atlases and maps](assets.md) | `RGame::Core::SpriteSheet` and the rest of the asset layer |
18
+ | [Values](values.md) | `RGame::Util::Color`, `RGame::Util::Tensor` |
19
+
20
+ The scene graph — `RGame::Engine`, the layer a game is actually written in:
21
+
22
+ | Page | Covers |
23
+ |---|---|
24
+ | [Scene graph](scene_graph.md) | `Node2D`, the tree, the lifecycle, transforms and the camera |
25
+ | [Components](components.md) | Reusable behaviour attached to a node |
26
+ | [Systems](systems.md) | Services a subtree shares — collision worlds, tile worlds |
27
+ | [Signals](signals.md) | The typed observer pattern nodes talk through |
28
+ | [Toolbox](toolbox.md) | What a game author reaches for directly: pooling, timers, camera, i18n, the audio bus |
29
+ | [Internal building blocks](internals.md) | What components are built from: collision maths, the spatial index, animation playback |
30
+
31
+ **The engine is a work in progress.** A window opens, the loop runs, input
32
+ works, shapes, images and text can be drawn, sound plays, and a scene graph runs
33
+ on top of it — the two games under `examples/` are built on exactly what is
34
+ documented here. What is missing is a UI toolkit and split-screen; pages here
35
+ describe what exists today and grow as more lands.
36
+
37
+ ## Loading it
38
+
39
+ Three requires, each a strict superset of the last:
40
+
41
+ ```ruby
42
+ require 'rgame' # RGame::Util + RGame::Engine — no graphics libraries at all
43
+ require 'rgame/core' # adds the window, the GPU and the sound device (SDL2 + OpenGL)
44
+ require 'rgame/game' # all of it, wired together — what a game writes
45
+ ```
46
+
47
+ A game wants the last one. `RGame::Game` is the entry point; see
48
+ [Game](game.md).
49
+
50
+ The first is **everything that runs without a window**: the value types and the
51
+ whole scene graph, in a process with no SDL and no OpenGL loaded. That is what
52
+ lets game logic and its specs run with no display present, and it is asserted
53
+ rather than assumed — `spec/rgame/no_graphics_spec.rb` reads the process's own
54
+ memory map.
55
+
56
+ Nothing is forced through those files: `rgame/util`, `rgame/engine` and
57
+ `rgame/core` are separately requirable, which is how the Core spec suite loads
58
+ exactly one layer.
59
+
60
+ For convenience, `rgame/core` also defines `RGame::Util::Controls` (the input
61
+ id vocabulary), because the input classes need it.
62
+
63
+ Both extensions must be compiled before they can be required:
64
+
65
+ ```
66
+ make ext # builds both, copies them into lib/rgame/
67
+ ```
68
+
69
+ ## The two namespaces
70
+
71
+ Everything lives under `RGame`, split in two by what it depends on:
72
+
73
+ | | `RGame::Util` | `RGame::Core` |
74
+ |---|---|---|
75
+ | Contains | shareable *values* — no window, no GPU, nothing to release | things owning a window, GPU or OS handle |
76
+ | Today | `Color`, `Tensor`, `Controls` | `App`, `Input`, `Gamepad`, `Image`, `Renderer`, `Recording`, `Font` |
77
+ | Loading it costs | nothing | SDL2 + OpenGL in your process |
78
+
79
+ The rule for deciding where something belongs: **a value goes in `Util`; only a
80
+ handle-owner goes in `Core`.** A colour is a value. A window is not.
81
+
82
+ This is not tidiness. Game logic is expected to hold `Util` types freely as
83
+ attributes, and to reach `Core` only through objects handed to it — a node's
84
+ `draw` receives a renderer and calls methods on it, rather than naming a class.
85
+ That is what keeps game code runnable with no window, which the testing section
86
+ below relies on.
87
+
88
+ ## A complete program
89
+
90
+ ```ruby
91
+ require 'rgame'
92
+ require 'rgame/core'
93
+
94
+ class MyGame < RGame::Core::App
95
+ Controls = RGame::Util::Controls
96
+
97
+ def initialize
98
+ super(width: 800, height: 600, caption: 'My Game')
99
+ @input = RGame::Core::Input.new(self)
100
+ @renderer = RGame::Core::Renderer.new(self)
101
+ @x = 400.0
102
+ @y = 300.0
103
+ end
104
+
105
+ # One fixed simulation tick. `dt` is always the same fixed step, never
106
+ # wall-clock frame time, so movement is deterministic.
107
+ def update(dt)
108
+ speed = 200.0 * dt
109
+ @x -= speed if @input.down?(:left)
110
+ @x += speed if @input.down?(:right)
111
+ @y -= speed if @input.down?(:up)
112
+ @y += speed if @input.down?(:down)
113
+ end
114
+
115
+ # Everything is drawn here, through a renderer built in initialize. See
116
+ # docs/api/drawing.md.
117
+ def draw
118
+ @renderer.rect(@x - 8, @y - 8, 16, 16)
119
+ end
120
+
121
+ def button_down(id)
122
+ close if id == Controls::KEY_ESCAPE
123
+ end
124
+ end
125
+
126
+ MyGame.new.run
127
+ ```
128
+
129
+ You subclass `App` and override the hooks you care about. Everything you do not
130
+ override is an inherited no-op — there is no `super` to remember, and no way to
131
+ break the loop by forgetting one. See [App](app.md) for the full list.
132
+
133
+ ## Testing a game built on this
134
+
135
+ The namespace split exists so that game logic can be tested without opening a
136
+ window. Drive `update` directly and it runs as fast as the CPU allows:
137
+
138
+ ```ruby
139
+ # A plain object holding your game's rules — no RGame::Core anywhere.
140
+ class Player
141
+ attr_reader :x
142
+
143
+ def initialize = @x = 0.0
144
+
145
+ def update(dt, moving_right:)
146
+ @x += 200.0 * dt if moving_right
147
+ end
148
+ end
149
+
150
+ RSpec.describe Player do
151
+ it 'walks right at 200 units a second' do
152
+ player = Player.new
153
+ # One simulated second, sixty ticks, no window and no clock.
154
+ 60.times { player.update(1.0 / 60.0, moving_right: true) }
155
+
156
+ expect(player.x).to be_within(0.01).of(200.0)
157
+ end
158
+ end
159
+ ```
160
+
161
+ The engine deliberately makes this easy: `update` takes `dt` as an argument
162
+ rather than reading a clock, so a test can pass whatever timestep it likes and
163
+ simulate an hour in milliseconds.
164
+
165
+ Keep the parts of your game that decide *what happens* free of `RGame::Core`,
166
+ and hand them a renderer at draw time rather than storing one. Then the only
167
+ code that needs a window is the thin layer that puts pixels on screen.