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.
- checksums.yaml +7 -0
- data/LICENSE +26 -0
- data/README.md +406 -0
- data/docs/api/README.md +167 -0
- data/docs/api/app.md +192 -0
- data/docs/api/assets.md +426 -0
- data/docs/api/audio.md +208 -0
- data/docs/api/components.md +321 -0
- data/docs/api/drawing.md +330 -0
- data/docs/api/game.md +99 -0
- data/docs/api/images.md +118 -0
- data/docs/api/input.md +179 -0
- data/docs/api/internals.md +110 -0
- data/docs/api/scene_graph.md +159 -0
- data/docs/api/signals.md +142 -0
- data/docs/api/systems.md +98 -0
- data/docs/api/text.md +116 -0
- data/docs/api/toolbox.md +240 -0
- data/docs/api/values.md +101 -0
- data/ext/README.md +225 -0
- data/ext/rgame_core/app/app.c +721 -0
- data/ext/rgame_core/app/app_gl.h +64 -0
- data/ext/rgame_core/app/frame_loop.c +42 -0
- data/ext/rgame_core/app/frame_loop.h +54 -0
- data/ext/rgame_core/audio/audio.c +466 -0
- data/ext/rgame_core/audio/audio_internal.h +45 -0
- data/ext/rgame_core/audio/vorbis_decoder.c +282 -0
- data/ext/rgame_core/audio/vorbis_decoder.h +45 -0
- data/ext/rgame_core/example.rb +188 -0
- data/ext/rgame_core/extconf.rb +167 -0
- data/ext/rgame_core/graphics/backend.c +52 -0
- data/ext/rgame_core/graphics/backend.h +64 -0
- data/ext/rgame_core/graphics/canvas.c +247 -0
- data/ext/rgame_core/graphics/canvas.h +143 -0
- data/ext/rgame_core/graphics/clip.c +87 -0
- data/ext/rgame_core/graphics/clip.h +89 -0
- data/ext/rgame_core/graphics/draw_queue.c +216 -0
- data/ext/rgame_core/graphics/draw_queue.h +174 -0
- data/ext/rgame_core/graphics/gl_backend.c +122 -0
- data/ext/rgame_core/graphics/gl_backend.h +43 -0
- data/ext/rgame_core/graphics/image.c +304 -0
- data/ext/rgame_core/graphics/image_internal.h +30 -0
- data/ext/rgame_core/graphics/primitives.c +189 -0
- data/ext/rgame_core/graphics/primitives.h +111 -0
- data/ext/rgame_core/graphics/recording.c +119 -0
- data/ext/rgame_core/graphics/recording.h +88 -0
- data/ext/rgame_core/graphics/texture.c +181 -0
- data/ext/rgame_core/graphics/texture.h +165 -0
- data/ext/rgame_core/graphics/transform.c +128 -0
- data/ext/rgame_core/graphics/transform.h +106 -0
- data/ext/rgame_core/include/rgame/core.h +577 -0
- data/ext/rgame_core/input/device_slots.c +103 -0
- data/ext/rgame_core/input/device_slots.h +93 -0
- data/ext/rgame_core/input/gamepad.c +145 -0
- data/ext/rgame_core/input/gamepad.h +63 -0
- data/ext/rgame_core/input/input.c +109 -0
- data/ext/rgame_core/input/input.h +99 -0
- data/ext/rgame_core/ruby/audio_ext.c +321 -0
- data/ext/rgame_core/ruby/core_ext.c +513 -0
- data/ext/rgame_core/ruby/core_ext.h +51 -0
- data/ext/rgame_core/ruby/font_ext.c +168 -0
- data/ext/rgame_core/ruby/image_ext.c +230 -0
- data/ext/rgame_core/ruby/recording_ext.c +186 -0
- data/ext/rgame_core/ruby/renderer_ext.c +376 -0
- data/ext/rgame_core/text/atlas.c +59 -0
- data/ext/rgame_core/text/atlas.h +85 -0
- data/ext/rgame_core/text/font.c +281 -0
- data/ext/rgame_core/text/font.h +139 -0
- data/ext/rgame_core/text/font_atlas.c +385 -0
- data/ext/rgame_core/text/font_internal.h +47 -0
- data/ext/rgame_core/text/glyph_cache.c +142 -0
- data/ext/rgame_core/text/glyph_cache.h +89 -0
- data/ext/rgame_core/vendor/README.md +159 -0
- data/ext/rgame_core/vendor/miniaudio.h +95864 -0
- data/ext/rgame_core/vendor/miniaudio_impl.c +62 -0
- data/ext/rgame_core/vendor/stb_image.h +7988 -0
- data/ext/rgame_core/vendor/stb_image_impl.c +31 -0
- data/ext/rgame_core/vendor/stb_truetype.h +5079 -0
- data/ext/rgame_core/vendor/stb_truetype_impl.c +23 -0
- data/ext/rgame_core/vendor/stb_vorbis.c +5584 -0
- data/ext/rgame_core/vendor/stb_vorbis_impl.c +29 -0
- data/ext/rgame_util/color.c +19 -0
- data/ext/rgame_util/color.h +60 -0
- data/ext/rgame_util/color_ext.c +156 -0
- data/ext/rgame_util/extconf.rb +27 -0
- data/ext/rgame_util/tensor.c +186 -0
- data/ext/rgame_util/util_ext.c +27 -0
- data/ext/rgame_util/util_ext.h +16 -0
- data/lib/rgame/boot.rb +13 -0
- data/lib/rgame/core/app.rb +82 -0
- data/lib/rgame/core/asset_manager.rb +224 -0
- data/lib/rgame/core/audio.rb +124 -0
- data/lib/rgame/core/font.rb +49 -0
- data/lib/rgame/core/gamepad.rb +55 -0
- data/lib/rgame/core/image.rb +55 -0
- data/lib/rgame/core/input.rb +77 -0
- data/lib/rgame/core/nine_slice.rb +163 -0
- data/lib/rgame/core/recording.rb +52 -0
- data/lib/rgame/core/renderer.rb +363 -0
- data/lib/rgame/core/sprite_sheet.rb +108 -0
- data/lib/rgame/core/tile_map_renderer.rb +160 -0
- data/lib/rgame/core/ui_atlas.rb +86 -0
- data/lib/rgame/core.rb +24 -0
- data/lib/rgame/engine/actor.rb +53 -0
- data/lib/rgame/engine/animation_set.rb +49 -0
- data/lib/rgame/engine/animator.rb +44 -0
- data/lib/rgame/engine/audio_bus.rb +24 -0
- data/lib/rgame/engine/audio_director.rb +29 -0
- data/lib/rgame/engine/body.rb +49 -0
- data/lib/rgame/engine/cached_label.rb +33 -0
- data/lib/rgame/engine/camera.rb +33 -0
- data/lib/rgame/engine/camera_view.rb +28 -0
- data/lib/rgame/engine/circle_collider.rb +32 -0
- data/lib/rgame/engine/collision_box.rb +34 -0
- data/lib/rgame/engine/collision_system.rb +44 -0
- data/lib/rgame/engine/component.rb +30 -0
- data/lib/rgame/engine/components/action_trigger.rb +41 -0
- data/lib/rgame/engine/components/animated_sprite.rb +63 -0
- data/lib/rgame/engine/components/character_body.rb +70 -0
- data/lib/rgame/engine/components/circle_collider.rb +44 -0
- data/lib/rgame/engine/components/collision_world.rb +103 -0
- data/lib/rgame/engine/components/despawn_offscreen.rb +26 -0
- data/lib/rgame/engine/components/path_follow.rb +84 -0
- data/lib/rgame/engine/components/player_controller.rb +24 -0
- data/lib/rgame/engine/components/pool.rb +53 -0
- data/lib/rgame/engine/components/screen_wrap.rb +27 -0
- data/lib/rgame/engine/components/sprite.rb +31 -0
- data/lib/rgame/engine/components/targeting.rb +54 -0
- data/lib/rgame/engine/components/thrust_controller.rb +65 -0
- data/lib/rgame/engine/components/tile_world.rb +68 -0
- data/lib/rgame/engine/components/timer.rb +75 -0
- data/lib/rgame/engine/components/velocity.rb +27 -0
- data/lib/rgame/engine/components/wander_controller.rb +60 -0
- data/lib/rgame/engine/debug_overlay.rb +106 -0
- data/lib/rgame/engine/i18n.rb +97 -0
- data/lib/rgame/engine/input/action_mapper.rb +46 -0
- data/lib/rgame/engine/input/actions.rb +41 -0
- data/lib/rgame/engine/input/player_controller.rb +14 -0
- data/lib/rgame/engine/matrix.rb +32 -0
- data/lib/rgame/engine/node2d.rb +271 -0
- data/lib/rgame/engine/path.rb +78 -0
- data/lib/rgame/engine/pool.rb +51 -0
- data/lib/rgame/engine/resettable.rb +67 -0
- data/lib/rgame/engine/scene/scene_stack.rb +65 -0
- data/lib/rgame/engine/signal.rb +75 -0
- data/lib/rgame/engine/spatial_hash.rb +71 -0
- data/lib/rgame/engine/tile_collision.rb +78 -0
- data/lib/rgame/engine/tile_map.rb +149 -0
- data/lib/rgame/engine/tileset.rb +101 -0
- data/lib/rgame/engine/timer.rb +51 -0
- data/lib/rgame/engine.rb +68 -0
- data/lib/rgame/fonts/LiberationSans-Regular.ttf +0 -0
- data/lib/rgame/fonts/OFL.txt +102 -0
- data/lib/rgame/game.rb +129 -0
- data/lib/rgame/util/color.rb +27 -0
- data/lib/rgame/util/controls.rb +107 -0
- data/lib/rgame/util/tensor.rb +12 -0
- data/lib/rgame/util.rb +8 -0
- data/lib/rgame/version.rb +12 -0
- data/lib/rgame.rb +20 -0
- 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
|
+
```
|
data/docs/api/README.md
ADDED
|
@@ -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.
|