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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +251 -0
- data/LICENSE +26 -0
- data/README.md +325 -0
- data/docs/api/README.md +224 -0
- data/docs/api/app.md +249 -0
- data/docs/api/assets.md +463 -0
- data/docs/api/audio.md +249 -0
- data/docs/api/cli.md +259 -0
- data/docs/api/components.md +1112 -0
- data/docs/api/drawing.md +398 -0
- data/docs/api/examples.md +263 -0
- data/docs/api/game.md +219 -0
- data/docs/api/images.md +118 -0
- data/docs/api/input.md +476 -0
- data/docs/api/internals.md +289 -0
- data/docs/api/localization.md +285 -0
- data/docs/api/scene_graph.md +539 -0
- data/docs/api/signals.md +147 -0
- data/docs/api/systems.md +231 -0
- data/docs/api/text.md +133 -0
- data/docs/api/tile_maps.md +237 -0
- data/docs/api/toolbox.md +493 -0
- data/docs/api/ui.md +927 -0
- data/docs/api/values.md +316 -0
- data/examples/assets/README.md +322 -0
- data/examples/assets/blip.ogg +0 -0
- data/examples/assets/glyphs.json +5 -0
- data/examples/assets/glyphs.png +0 -0
- data/examples/assets/hero.json +12 -0
- data/examples/assets/hero.png +0 -0
- data/examples/assets/icons.json +13 -0
- data/examples/assets/icons.png +0 -0
- data/examples/assets/music.ogg +0 -0
- data/examples/assets/skills.json +10 -0
- data/examples/assets/skills.png +0 -0
- data/examples/assets/tileset.png +0 -0
- data/examples/assets/tileset.tsx +65 -0
- data/examples/assets/town.tmx +26 -0
- data/examples/assets/ui.json +11 -0
- data/examples/assets/ui.png +0 -0
- data/examples/collision/locales/en.yml +8 -0
- data/examples/collision/main.rb +316 -0
- data/examples/collision_tiles/locales/en.yml +9 -0
- data/examples/collision_tiles/main.rb +274 -0
- data/examples/fullscreen/locales/en.yml +10 -0
- data/examples/fullscreen/main.rb +216 -0
- data/examples/game_menu/locales/en.yml +8 -0
- data/examples/game_menu/main.rb +170 -0
- data/examples/input_glyphs/locales/en.yml +14 -0
- data/examples/input_glyphs/main.rb +213 -0
- data/examples/jump_topdown/locales/en.yml +9 -0
- data/examples/jump_topdown/main.rb +178 -0
- data/examples/localization/locales/de.yml +12 -0
- data/examples/localization/locales/en.yml +13 -0
- data/examples/localization/main.rb +158 -0
- data/examples/menu_navigation/locales/en.yml +23 -0
- data/examples/menu_navigation/main.rb +365 -0
- data/examples/music/locales/en.yml +7 -0
- data/examples/music/main.rb +134 -0
- data/examples/pathfinding/locales/en.yml +17 -0
- data/examples/pathfinding/main.rb +298 -0
- data/examples/pooling/locales/en.yml +7 -0
- data/examples/pooling/main.rb +259 -0
- data/examples/quick_wheel/locales/en.yml +16 -0
- data/examples/quick_wheel/main.rb +184 -0
- data/examples/radial_menu/locales/en.yml +16 -0
- data/examples/radial_menu/main.rb +184 -0
- data/examples/save_load/locales/en.yml +11 -0
- data/examples/save_load/main.rb +207 -0
- data/examples/save_load_ids/locales/en.yml +11 -0
- data/examples/save_load_ids/main.rb +322 -0
- data/examples/scroll_map/locales/en.yml +4 -0
- data/examples/scroll_map/main.rb +140 -0
- data/examples/signals/locales/en.yml +6 -0
- data/examples/signals/main.rb +278 -0
- data/examples/skill_bar/locales/en.yml +14 -0
- data/examples/skill_bar/main.rb +159 -0
- data/examples/sound/locales/en.yml +6 -0
- data/examples/sound/main.rb +122 -0
- data/examples/split_screen/locales/en.yml +9 -0
- data/examples/split_screen/main.rb +304 -0
- data/examples/sprite/locales/en.yml +8 -0
- data/examples/sprite/main.rb +180 -0
- data/examples/timer/locales/en.yml +12 -0
- data/examples/timer/main.rb +273 -0
- data/examples/velocity/locales/en.yml +6 -0
- data/examples/velocity/main.rb +196 -0
- data/examples/walk/locales/en.yml +4 -0
- data/examples/walk/main.rb +99 -0
- data/exe/rgame +9 -0
- data/lib/rgame/boot.rb +3 -0
- data/lib/rgame/cli/new_project.rb +139 -0
- data/lib/rgame/cli/templates/Gemfile.tt +23 -0
- data/lib/rgame/cli/templates/README.md.tt +93 -0
- data/lib/rgame/cli/templates/Rakefile.tt +9 -0
- data/lib/rgame/cli/templates/assets/locales/en.yml.tt +10 -0
- data/lib/rgame/cli/templates/game.rb.tt +23 -0
- data/lib/rgame/cli/templates/gitignore.tt +12 -0
- data/lib/rgame/cli/templates/main.rb.tt +11 -0
- data/lib/rgame/cli/templates/nodes/root.rb.tt +24 -0
- data/lib/rgame/cli/templates/rspec.tt +2 -0
- data/lib/rgame/cli/templates/rubocop.yml.tt +75 -0
- data/lib/rgame/cli/templates/ruby-version.tt +1 -0
- data/lib/rgame/cli/templates/spec/locales_spec.rb.tt +18 -0
- data/lib/rgame/cli/templates/spec/nodes/root_spec.rb.tt +18 -0
- data/lib/rgame/cli/templates/spec/spec_helper.rb.tt +37 -0
- data/lib/rgame/cli.rb +66 -0
- data/lib/rgame/core/app.rb +46 -0
- data/lib/rgame/core/asset_manager.rb +206 -0
- data/lib/rgame/core/audio.rb +145 -0
- data/lib/rgame/core/font.rb +46 -0
- data/lib/rgame/core/gamepad.rb +55 -0
- data/lib/rgame/core/image.rb +55 -0
- data/lib/rgame/core/input.rb +71 -0
- data/lib/rgame/core/locale.rb +22 -0
- data/lib/rgame/core/nine_slice.rb +142 -0
- data/lib/rgame/core/recording.rb +54 -0
- data/lib/rgame/core/renderer.rb +354 -0
- data/lib/rgame/core/sprite_sheet.rb +105 -0
- data/lib/rgame/core/tile_map_renderer.rb +172 -0
- data/lib/rgame/core/ui_atlas.rb +101 -0
- data/lib/rgame/core/virtual_gamepad.rb +26 -0
- data/lib/rgame/core.rb +18 -0
- data/lib/rgame/core_ext.so +0 -0
- data/lib/rgame/engine/actor_blockers.rb +131 -0
- data/lib/rgame/engine/animation_set.rb +50 -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 +59 -0
- data/lib/rgame/engine/bounds_blockers.rb +74 -0
- data/lib/rgame/engine/camera.rb +78 -0
- data/lib/rgame/engine/circle_collider.rb +34 -0
- data/lib/rgame/engine/collision_box.rb +59 -0
- data/lib/rgame/engine/collision_system.rb +132 -0
- data/lib/rgame/engine/component.rb +64 -0
- data/lib/rgame/engine/components/action_trigger.rb +40 -0
- data/lib/rgame/engine/components/animated_sprite.rb +71 -0
- data/lib/rgame/engine/components/box_collider.rb +99 -0
- data/lib/rgame/engine/components/camera_follow.rb +45 -0
- data/lib/rgame/engine/components/character_body.rb +50 -0
- data/lib/rgame/engine/components/circle_collider.rb +80 -0
- data/lib/rgame/engine/components/collision_world.rb +231 -0
- data/lib/rgame/engine/components/despawn_offscreen.rb +42 -0
- data/lib/rgame/engine/components/feet_collider.rb +61 -0
- data/lib/rgame/engine/components/hop.rb +76 -0
- data/lib/rgame/engine/components/identity.rb +73 -0
- data/lib/rgame/engine/components/mover.rb +285 -0
- data/lib/rgame/engine/components/navigator.rb +145 -0
- data/lib/rgame/engine/components/path_follow.rb +176 -0
- data/lib/rgame/engine/components/player_controller.rb +27 -0
- data/lib/rgame/engine/components/pool.rb +53 -0
- data/lib/rgame/engine/components/screen_wrap.rb +49 -0
- data/lib/rgame/engine/components/sprite.rb +47 -0
- data/lib/rgame/engine/components/targeting.rb +52 -0
- data/lib/rgame/engine/components/thrust_controller.rb +65 -0
- data/lib/rgame/engine/components/tile_world.rb +109 -0
- data/lib/rgame/engine/components/timer.rb +75 -0
- data/lib/rgame/engine/components/velocity.rb +43 -0
- data/lib/rgame/engine/components/wander_controller.rb +64 -0
- data/lib/rgame/engine/components/world.rb +133 -0
- data/lib/rgame/engine/contact_set.rb +74 -0
- data/lib/rgame/engine/culling.rb +45 -0
- data/lib/rgame/engine/debug_overlay.rb +111 -0
- data/lib/rgame/engine/i18n/plural.rb +45 -0
- data/lib/rgame/engine/i18n/plural_rules.rb +82 -0
- data/lib/rgame/engine/i18n/template.rb +59 -0
- data/lib/rgame/engine/i18n.rb +322 -0
- data/lib/rgame/engine/input/action_mapper.rb +100 -0
- data/lib/rgame/engine/input/actions.rb +92 -0
- data/lib/rgame/engine/input/input_map.rb +196 -0
- data/lib/rgame/engine/layout.rb +82 -0
- data/lib/rgame/engine/nav_grid.rb +87 -0
- data/lib/rgame/engine/node2d.rb +555 -0
- data/lib/rgame/engine/path.rb +76 -0
- data/lib/rgame/engine/player.rb +69 -0
- data/lib/rgame/engine/player_layer.rb +70 -0
- data/lib/rgame/engine/players.rb +205 -0
- data/lib/rgame/engine/pool.rb +51 -0
- data/lib/rgame/engine/presentation.rb +171 -0
- data/lib/rgame/engine/scene/scene_stack.rb +87 -0
- data/lib/rgame/engine/sealed_privates.rb +54 -0
- data/lib/rgame/engine/signal.rb +75 -0
- data/lib/rgame/engine/spatial_hash.rb +116 -0
- data/lib/rgame/engine/text.rb +194 -0
- data/lib/rgame/engine/tile_blockers.rb +63 -0
- data/lib/rgame/engine/tile_map.rb +147 -0
- data/lib/rgame/engine/tile_map_layer.rb +82 -0
- data/lib/rgame/engine/tileset.rb +99 -0
- data/lib/rgame/engine/timer.rb +51 -0
- data/lib/rgame/engine/ui/button.rb +248 -0
- data/lib/rgame/engine/ui/column.rb +20 -0
- data/lib/rgame/engine/ui/icon_button.rb +93 -0
- data/lib/rgame/engine/ui/menu.rb +290 -0
- data/lib/rgame/engine/ui/navigation.rb +57 -0
- data/lib/rgame/engine/ui/nine_slice_style.rb +50 -0
- data/lib/rgame/engine/ui/option_button.rb +163 -0
- data/lib/rgame/engine/ui/panel_button.rb +32 -0
- data/lib/rgame/engine/ui/panel_menu.rb +36 -0
- data/lib/rgame/engine/ui/pointing.rb +146 -0
- data/lib/rgame/engine/ui/radial_menu.rb +85 -0
- data/lib/rgame/engine/ui/ring.rb +55 -0
- data/lib/rgame/engine/ui/row.rb +21 -0
- data/lib/rgame/engine/ui/shape_style.rb +102 -0
- data/lib/rgame/engine/ui/stack.rb +58 -0
- data/lib/rgame/engine/ui/stepping.rb +93 -0
- data/lib/rgame/engine/ui/text_button.rb +59 -0
- data/lib/rgame/engine/view.rb +76 -0
- data/lib/rgame/engine/viewports.rb +171 -0
- data/lib/rgame/engine/world_view.rb +71 -0
- data/lib/rgame/engine.rb +86 -0
- data/lib/rgame/fonts/LiberationSans-Regular.ttf +0 -0
- data/lib/rgame/fonts/OFL.txt +102 -0
- data/lib/rgame/game.rb +271 -0
- data/lib/rgame/rubocop/cop/game/draw_in_local_space.rb +103 -0
- data/lib/rgame/rubocop/cop/game/hot_path.rb +36 -0
- data/lib/rgame/rubocop/cop/game/layer_boundary.rb +43 -0
- data/lib/rgame/rubocop/cop/game/no_core_in_engine_layer.rb +100 -0
- data/lib/rgame/rubocop/cop/game/no_engine_in_core_layer.rb +84 -0
- data/lib/rgame/rubocop/cop/game/no_interpolation_in_hot_path.rb +50 -0
- data/lib/rgame/rubocop/cop/game/no_literal_text.rb +41 -0
- data/lib/rgame/rubocop/cop/game/no_needless_allocation.rb +112 -0
- data/lib/rgame/rubocop/default.yml +39 -0
- data/lib/rgame/rubocop/plugin.rb +45 -0
- data/lib/rgame/rubocop.rb +11 -0
- data/lib/rgame/util/color.rb +23 -0
- data/lib/rgame/util/controls.rb +169 -0
- data/lib/rgame/util/route_search.rb +27 -0
- data/lib/rgame/util/save_file.rb +107 -0
- data/lib/rgame/util/solid_grid.rb +37 -0
- data/lib/rgame/util/tensor.rb +3 -0
- data/lib/rgame/util/tile_sweep.rb +36 -0
- data/lib/rgame/util/z.rb +123 -0
- data/lib/rgame/util.rb +10 -0
- data/lib/rgame/util_ext.so +0 -0
- data/lib/rgame/version.rb +12 -0
- data/lib/rgame.rb +5 -0
- data/licenses/SDL2/LICENSE.txt +18 -0
- metadata +309 -0
data/docs/api/README.md
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# rgame API guide
|
|
2
|
+
|
|
3
|
+
These pages document how to use rgame from Ruby. The engine is written in C and
|
|
4
|
+
ships as two Ruby extensions. The scene graph sits on top of them in pure Ruby,
|
|
5
|
+
and a game is written against that scene graph. Nothing here asks you to read
|
|
6
|
+
or write C.
|
|
7
|
+
|
|
8
|
+
| Page | Covers |
|
|
9
|
+
|---|---|
|
|
10
|
+
| This page | Loading the library, the three namespaces, a working program, testing |
|
|
11
|
+
| [The `rgame` command](cli.md) | `rgame new NAME` — starting a project, and the layout it gives you |
|
|
12
|
+
| [App](app.md) | `RGame::Core::App` — the window and the frame loop |
|
|
13
|
+
| [Game](game.md) | `RGame::Game` — the entry point that wires both halves together |
|
|
14
|
+
| [Input](input.md) | `RGame::Core::Input`, `RGame::Util::Controls`, `RGame::Core::Gamepad`, `RGame::Core::VirtualGamepad` |
|
|
15
|
+
| [Drawing](drawing.md) | `RGame::Core::Renderer` — shapes, images, transforms, clipping, recordings |
|
|
16
|
+
| [Images](images.md) | `RGame::Core::Image` — loading PNGs, subimages, tiles |
|
|
17
|
+
| [Text](text.md) | `RGame::Core::Font` and `Renderer#text` |
|
|
18
|
+
| [Audio](audio.md) | `RGame::Core::Audio`, `Sample`, `Song` — samples and streamed music |
|
|
19
|
+
| [Assets](assets.md) | `RGame::Core::AssetManager`, `SpriteSheet`, `NineSlice`, `UiAtlas`, `TileMapRenderer` |
|
|
20
|
+
| [Values](values.md) | `RGame::Util::Color`, `Tensor`, `SolidGrid`, `RouteSearch`, `TileSweep`, `Z`, `SaveFile` |
|
|
21
|
+
| [Examples](examples.md) | What each program under `examples/` demonstrates |
|
|
22
|
+
|
|
23
|
+
The scene graph is `RGame::Engine`, the layer a game is written in:
|
|
24
|
+
|
|
25
|
+
| Page | Covers |
|
|
26
|
+
|---|---|
|
|
27
|
+
| [Scene graph](scene_graph.md) | `Node2D`, the tree, the lifecycle, transforms and the camera |
|
|
28
|
+
| [Components](components.md) | Reusable behaviour attached to a node |
|
|
29
|
+
| [Systems](systems.md) | Services a subtree shares — collision worlds, tile worlds |
|
|
30
|
+
| [Tile maps](tile_maps.md) | `TileMap` and `Tileset` — a Tiled map as data: loading, cells, solidity, animated tiles |
|
|
31
|
+
| [UI](ui.md) | `PlayerLayer` and `UI::Menu` — a player's own screen, and a list or wheel navigated by focus |
|
|
32
|
+
| [Signals](signals.md) | The typed observer pattern nodes talk through |
|
|
33
|
+
| [Toolbox](toolbox.md) | What a game author reaches for directly: the text a node draws, pooling, paths and routes, timers, the camera, collision boxes, the audio bus |
|
|
34
|
+
| [Localization](localization.md) | `I18n` and translation tables — where they go, plurals, the fallback chain, the player's language, missing keys in specs |
|
|
35
|
+
| [Internal building blocks](internals.md) | What components are built from: collision maths, the spatial index, animation playback |
|
|
36
|
+
|
|
37
|
+
**The engine is a work in progress.** It opens a window, runs the loop, reads
|
|
38
|
+
input, draws shapes, images and text, and plays sound. A scene graph with
|
|
39
|
+
split-screen players runs on top. The games under `test_projects/` use exactly
|
|
40
|
+
what these pages document. The missing piece is a UI *toolkit*. [UI](ui.md)
|
|
41
|
+
gives each player a region of the screen, menus with focus and activation, and a
|
|
42
|
+
column, row or ring of equal-sized buttons. It has no general layout, no scrolling
|
|
43
|
+
lists and no text entry. These pages describe
|
|
44
|
+
what exists and grow with the engine.
|
|
45
|
+
|
|
46
|
+
## Loading it
|
|
47
|
+
|
|
48
|
+
rgame has three entry points. Each one loads everything the one before it does:
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
require 'rgame' # RGame::Util + RGame::Engine — no graphics libraries at all
|
|
52
|
+
require 'rgame/core' # adds the window, the GPU and the sound device (SDL2 + OpenGL)
|
|
53
|
+
require 'rgame/game' # all of it, wired together — what a game writes
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
A game requires the last one. Its entry point is `RGame::Game`; see
|
|
57
|
+
[Game](game.md).
|
|
58
|
+
|
|
59
|
+
`require 'rgame'` loads **everything that runs without a window**: the value
|
|
60
|
+
types and the whole scene graph. The process loads no SDL and no OpenGL, so game
|
|
61
|
+
logic and its specs run with no display. `spec/rgame/no_graphics_spec.rb` checks
|
|
62
|
+
this by reading the process's own memory map.
|
|
63
|
+
|
|
64
|
+
You can also require `rgame/util`, `rgame/engine` and `rgame/core` on their own.
|
|
65
|
+
The Core spec suite does this to load exactly one layer.
|
|
66
|
+
|
|
67
|
+
`rgame/core` also loads `RGame::Util::Controls`, the input id vocabulary,
|
|
68
|
+
because the input classes need it.
|
|
69
|
+
|
|
70
|
+
**Your install either compiled both extensions or arrived with them built.** On
|
|
71
|
+
Apple Silicon macOS, x86-64 Linux and 64-bit Windows, `gem install rgame` fetches
|
|
72
|
+
a gem that already holds `core_ext` and `util_ext`, with SDL2 inside `core_ext`,
|
|
73
|
+
and compiles nothing. Every other machine gets the gem that ships the C and
|
|
74
|
+
builds both on install, against a system SDL2. Either way the two land in the
|
|
75
|
+
same place and the three requires above behave the same.
|
|
76
|
+
|
|
77
|
+
In a checkout of the repository, compile them before you require anything:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
make ext # builds both, copies them into lib/rgame/
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## The three namespaces
|
|
84
|
+
|
|
85
|
+
Everything lives under `RGame`. What a class depends on decides between `Util`
|
|
86
|
+
and `Core`. `Engine` holds what a game is written in.
|
|
87
|
+
|
|
88
|
+
| | `RGame::Util` | `RGame::Core` | `RGame::Engine` |
|
|
89
|
+
|---|---|---|---|
|
|
90
|
+
| Contains | shareable *values* — no window, no GPU, nothing to release | things owning a window, GPU or OS handle | game concepts: the scene graph a game is written in |
|
|
91
|
+
| Classes | `Color`, `Tensor`, `Controls`, `Z`, `SolidGrid`, `RouteSearch`, `TileSweep`, `SaveFile` | `App`, `Input`, `Gamepad`, `VirtualGamepad`, `Image`, `Renderer`, `Recording`, `Font`, `Audio`, `SpriteSheet`, `NineSlice`, `UiAtlas`, `TileMapRenderer`, `AssetManager` | `Node2D`, components, systems, signals, `TileMap`, `Player`, `InputMap`, `UI::Menu` |
|
|
92
|
+
| Loading it costs | nothing | SDL2 + OpenGL in your process | nothing |
|
|
93
|
+
|
|
94
|
+
**A value goes in `Util`; only a handle-owner goes in `Core`.** A colour is a
|
|
95
|
+
value. A window is not.
|
|
96
|
+
|
|
97
|
+
`RGame::Engine` sits above both and follows three rules:
|
|
98
|
+
|
|
99
|
+
- It may hold `Util` values as attributes, such as a `Color` or a `Tensor`.
|
|
100
|
+
- It may **not name `Core` at all**: no require, no constant, no attribute.
|
|
101
|
+
- It reaches `Core` only through objects it receives. A node's `on_draw`
|
|
102
|
+
receives a renderer and calls its methods by name. The node never stores the
|
|
103
|
+
renderer and never checks its class.
|
|
104
|
+
|
|
105
|
+
These rules keep a whole game runnable and testable with no window: its rules,
|
|
106
|
+
its scenes, its collisions. The testing section below relies on that. RuboCop
|
|
107
|
+
cops enforce the rules in both directions inside rgame's own repository, and a
|
|
108
|
+
project from `rgame new` runs the first of them over its `nodes/` and `spec/`.
|
|
109
|
+
See [the generated RuboCop configuration](cli.md#the-generated-rubocop-configuration).
|
|
110
|
+
|
|
111
|
+
`RGame::Game` is the one exception: the only class that names both `Engine` and
|
|
112
|
+
`Core`. It exists to connect the two halves. Keeping that in one file lets the
|
|
113
|
+
rule hold everywhere else.
|
|
114
|
+
|
|
115
|
+
## A complete program
|
|
116
|
+
|
|
117
|
+
A game is a tree of nodes, run by `RGame::Game`.
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
require 'rgame/game'
|
|
121
|
+
|
|
122
|
+
# One game object: a square the player walks around. Pure Engine — it names no
|
|
123
|
+
# graphics class, so it runs unchanged in a spec with no window.
|
|
124
|
+
class Hero < RGame::Engine::Node2D
|
|
125
|
+
SPEED = 200.0
|
|
126
|
+
|
|
127
|
+
def initialize
|
|
128
|
+
super(x: 400, y: 300, width: 16, height: 16)
|
|
129
|
+
@vx = 0.0
|
|
130
|
+
@vy = 0.0
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Intent, read once per simulation tick. Never a key: `move_x` is whatever
|
|
134
|
+
# this player's input map binds it to — arrows, WASD or a stick.
|
|
135
|
+
def on_control(actions)
|
|
136
|
+
@vx = actions.axis(:move_x) * SPEED
|
|
137
|
+
@vy = actions.axis(:move_y) * SPEED
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# `dt` is always the same fixed step, never wall-clock frame time, so
|
|
141
|
+
# movement is deterministic. `x`/`y` are relative to the parent.
|
|
142
|
+
def on_update(dt)
|
|
143
|
+
self.x += @vx * dt
|
|
144
|
+
self.y += @vy * dt
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# The renderer is handed in and never stored; `view` is the viewport being
|
|
148
|
+
# drawn into, which most nodes ignore. Draw in the node's own space: the
|
|
149
|
+
# traversal has already put the renderer on this node, so (0, 0) is here.
|
|
150
|
+
# See docs/api/scene_graph.md, "Drawing happens in local space".
|
|
151
|
+
def on_draw(renderer, _view)
|
|
152
|
+
renderer.rect(0, 0, width, height)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# The root of the tree. Children are added in `on_add`, once the node is in a
|
|
157
|
+
# tree and can reach the game around it.
|
|
158
|
+
class Scene < RGame::Engine::Node2D
|
|
159
|
+
def on_add
|
|
160
|
+
add_node(Hero.new)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
RGame::Game.new(root: Scene.new, width: 800, height: 600, caption: 'My Game').start
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Subclass `Node2D` and override the hooks you need: `on_control`, `on_update`,
|
|
168
|
+
`on_draw`, and the lifecycle hooks around them. A hook you do not override does
|
|
169
|
+
nothing. Separate phase methods do the bookkeeping: they push the node's
|
|
170
|
+
transform, drive components and descend into children. You never override
|
|
171
|
+
those, so there is no `super` to forget. [Scene graph](scene_graph.md) lists
|
|
172
|
+
every hook. [Game](game.md) describes what `Game` builds around the tree: the
|
|
173
|
+
window, the renderer, the asset manager, the sound device, the input mapper and
|
|
174
|
+
the players.
|
|
175
|
+
|
|
176
|
+
Without an `input_map:`, `Game` uses the default map shown above. It binds
|
|
177
|
+
eight-way `move_x` / `move_y` to the arrows, WASD, the d-pad and the left stick,
|
|
178
|
+
and adds `fire`. See [Input](input.md).
|
|
179
|
+
|
|
180
|
+
## Testing a game built on this
|
|
181
|
+
|
|
182
|
+
`require 'rgame'` loads `Util` and the whole scene graph with no SDL and no
|
|
183
|
+
OpenGL. The nodes from the program above run there unchanged. A spec drives
|
|
184
|
+
their phases directly, so a simulated hour takes milliseconds:
|
|
185
|
+
|
|
186
|
+
<!-- doc-example: skip — an RSpec file for the Hero above, run by rspec -->
|
|
187
|
+
```ruby
|
|
188
|
+
require 'rgame'
|
|
189
|
+
|
|
190
|
+
RSpec.describe Hero do
|
|
191
|
+
it 'walks right at 200 units a second' do
|
|
192
|
+
hero = Hero.new
|
|
193
|
+
# The same snapshot object the input mapper hands a node at runtime, built
|
|
194
|
+
# by hand with the stick pushed fully right.
|
|
195
|
+
actions = RGame::Engine::Actions.new(axes: { move_x: 1.0, move_y: 0.0 })
|
|
196
|
+
|
|
197
|
+
# One simulated second, sixty ticks, no window and no clock.
|
|
198
|
+
60.times do
|
|
199
|
+
hero.control(actions)
|
|
200
|
+
hero.update(1.0 / 60.0)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
expect(hero.x).to be_within(0.01).of(600.0)
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Two properties make this work:
|
|
209
|
+
|
|
210
|
+
- **`update` takes `dt` as an argument and reads no clock.** A test passes any
|
|
211
|
+
timestep it likes, so it can simulate minutes of play in milliseconds.
|
|
212
|
+
- **A node never holds a renderer.** `on_draw` receives one. A spec passes a
|
|
213
|
+
recording double and asserts on what the node asked it to draw. rgame's own
|
|
214
|
+
suite checks its fake renderer against the real one with a shared contract in
|
|
215
|
+
`spec/support/shared_examples/`. The whole suite runs headless.
|
|
216
|
+
|
|
217
|
+
The spec asserts `hero.x`, not `world_x`, because this hero has no parent. The
|
|
218
|
+
world transform accumulates from the parent, and a node without one resolves to
|
|
219
|
+
the origin. Under a root, game logic reads `world_x`. Drawing reads neither; see
|
|
220
|
+
[Scene graph](scene_graph.md#drawing-happens-in-local-space).
|
|
221
|
+
|
|
222
|
+
Keep the code that decides *what happens* in `RGame::Engine`. That layer cannot
|
|
223
|
+
name `RGame::Core`, so it cannot come to depend on a window. Only the thin layer
|
|
224
|
+
that puts pixels on screen then needs one.
|
data/docs/api/app.md
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# `RGame::Core::App`
|
|
2
|
+
|
|
3
|
+
`App` owns the window and runs the frame loop. Subclass it, override the hooks
|
|
4
|
+
you need, and call `run`.
|
|
5
|
+
|
|
6
|
+
```ruby
|
|
7
|
+
require 'rgame/core'
|
|
8
|
+
|
|
9
|
+
class MyGame < RGame::Core::App
|
|
10
|
+
def initialize = super(width: 800, height: 600, caption: 'My Game')
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
MyGame.new.run # returns when the loop stops
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`App.new` takes keyword arguments only. It requires `width:`, `height:` and
|
|
17
|
+
`caption:`. `media_root:` is optional and defaults to `'media'`. The constructor
|
|
18
|
+
opens a real window at once.
|
|
19
|
+
|
|
20
|
+
## What the app owns
|
|
21
|
+
|
|
22
|
+
The app builds two objects on first use. A game needs exactly one of each:
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
require 'rgame/core'
|
|
26
|
+
|
|
27
|
+
class MyGame < RGame::Core::App
|
|
28
|
+
def initialize = super(width: 640, height: 480, caption: 'demo', media_root: 'assets')
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
app = MyGame.new
|
|
32
|
+
app.assets # => RGame::Core::AssetManager — rooted at media_root
|
|
33
|
+
app.audio # => RGame::Core::Audio — the sound device
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**A game never constructs either of them.** An image belongs to one OpenGL
|
|
37
|
+
context, so whatever loads it must know the app. The app builds its asset manager
|
|
38
|
+
once, and scenes load through it by path. No class has to pass the app along to
|
|
39
|
+
reach an image.
|
|
40
|
+
|
|
41
|
+
Both objects are built on first use. `RGame::Core::Renderer.new(app)` asks for the
|
|
42
|
+
asset manager, so any app that draws has one. An app that never plays a sound
|
|
43
|
+
never opens a sound device; the first sound request opens it.
|
|
44
|
+
|
|
45
|
+
`media_root` is read-only and fixed at construction. It has no writer: changing
|
|
46
|
+
the root after a load would leave one cache keyed against two roots.
|
|
47
|
+
|
|
48
|
+
[Assets](assets.md) describes the asset manager.
|
|
49
|
+
|
|
50
|
+
## The frame loop
|
|
51
|
+
|
|
52
|
+
`run` drives the loop until something stops it, and calls back into your object.
|
|
53
|
+
One rendered frame runs these steps:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
poll input and window events → button_down / button_up / resize
|
|
57
|
+
gamepad_connected / gamepad_disconnected
|
|
58
|
+
frame_begin → once, before any ticks
|
|
59
|
+
update(dt) → zero or more times (see below)
|
|
60
|
+
needs_redraw? → once; false skips the draw
|
|
61
|
+
draw → once, unless skipped
|
|
62
|
+
frame_end → once, after draw, unless skipped
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### `update(dt)` runs a *fixed* number of times, not once per frame
|
|
66
|
+
|
|
67
|
+
**The simulation advances in fixed steps.** Each frame adds the real elapsed
|
|
68
|
+
time to an accumulator. The frame then runs every whole step that has come due.
|
|
69
|
+
That can be **zero** steps, when the machine renders faster than the simulation
|
|
70
|
+
needs. It can be **several**, when a slow frame forces the simulation to catch
|
|
71
|
+
up. The loop caps catch-up, so a slow frame slows time down instead of spiralling.
|
|
72
|
+
|
|
73
|
+
`dt` is always the same fixed step: **1/60 second**. It is never wall-clock
|
|
74
|
+
frame time. A fixed step makes movement reproducible. It also lets a test call
|
|
75
|
+
`update` directly and simulate any amount of time.
|
|
76
|
+
|
|
77
|
+
**Do not sample input inside `update`.** A frame may run one tick or five, so a
|
|
78
|
+
key held for one frame would be read a varying number of times. Sample input in
|
|
79
|
+
`frame_begin`, or use `Input`. `Input` reads a snapshot taken once per frame, so
|
|
80
|
+
it answers the same for every tick of that frame.
|
|
81
|
+
|
|
82
|
+
### `needs_redraw?`
|
|
83
|
+
|
|
84
|
+
Return `false` to skip the draw for that frame. The simulation still advances.
|
|
85
|
+
Use it when nothing changed and drawing costs a lot. The default is `true`.
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
def update(_dt)
|
|
89
|
+
@dirty = true if something_moved
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def needs_redraw? = @dirty
|
|
93
|
+
|
|
94
|
+
def draw
|
|
95
|
+
# ...
|
|
96
|
+
@dirty = false
|
|
97
|
+
end
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`update` runs only when a step happened. Setting `@dirty = true` there is usually
|
|
101
|
+
the whole rule.
|
|
102
|
+
|
|
103
|
+
## Hooks you can override
|
|
104
|
+
|
|
105
|
+
Every hook inherits a default that does nothing. Override only what you use.
|
|
106
|
+
|
|
107
|
+
| Hook | When |
|
|
108
|
+
|---|---|
|
|
109
|
+
| `frame_begin` | Once per frame, before that frame's ticks. Sample input here. |
|
|
110
|
+
| `update(dt)` | One fixed simulation tick. |
|
|
111
|
+
| `needs_redraw?` | Before drawing; `false` skips `draw`. Default `true`. |
|
|
112
|
+
| `draw` | Render one frame. |
|
|
113
|
+
| `frame_end` | After `draw` reaches the GPU, before the buffer swap. Not called when the draw was skipped. |
|
|
114
|
+
| `button_down(id)` | A key was pressed. The loop filters auto-repeats, so a held key fires once. |
|
|
115
|
+
| `button_up(id)` | A key was released. |
|
|
116
|
+
| `resize(width, height)` | The window changed size. |
|
|
117
|
+
| `gamepad_connected(slot)` | A controller arrived in a player slot. |
|
|
118
|
+
| `gamepad_disconnected(slot)` | A controller left a slot. |
|
|
119
|
+
|
|
120
|
+
`id` is a value from [`RGame::Util::Controls`](input.md), such as
|
|
121
|
+
`Controls::KEY_ESCAPE`.
|
|
122
|
+
|
|
123
|
+
`frame_end` is the one point where a test can read back the frame it drew.
|
|
124
|
+
Game code rarely needs it.
|
|
125
|
+
|
|
126
|
+
### There is no built-in quit key
|
|
127
|
+
|
|
128
|
+
Closing the window stops the loop, because the platform decides that. Quitting
|
|
129
|
+
on Escape is *your* decision, so the engine leaves it to you:
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
def button_down(id)
|
|
133
|
+
close if id == RGame::Util::Controls::KEY_ESCAPE
|
|
134
|
+
end
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Window methods
|
|
138
|
+
|
|
139
|
+
| Method | |
|
|
140
|
+
|---|---|
|
|
141
|
+
| `run` | Runs the loop until it stops. Returns `self`. |
|
|
142
|
+
| `close` | Asks the loop to stop. Safe to call from inside any hook. |
|
|
143
|
+
| `width`, `height` | Current window size. |
|
|
144
|
+
| `caption`, `caption=` | The window title. |
|
|
145
|
+
| `fullscreen?`, `fullscreen=` | Whether the window covers the screen. |
|
|
146
|
+
| `ticks_ms` | Monotonic milliseconds since startup. For measuring frames, not for drawing. |
|
|
147
|
+
| `fps` | Most recent frames-per-second reading, updated about once a second. |
|
|
148
|
+
|
|
149
|
+
`close` takes effect promptly. The loop checks between steps and starts no more
|
|
150
|
+
work in the current frame.
|
|
151
|
+
|
|
152
|
+
`ticks_ms` is the raw clock. A draw never reads it: animation accumulates its own
|
|
153
|
+
time in `update`.
|
|
154
|
+
|
|
155
|
+
### Fullscreen
|
|
156
|
+
|
|
157
|
+
```ruby
|
|
158
|
+
require 'rgame/core'
|
|
159
|
+
|
|
160
|
+
app = RGame::Core::App.new(width: 640, height: 480, caption: 'demo', fullscreen: true) # opens fullscreen
|
|
161
|
+
app.fullscreen = !app.fullscreen? # switches either way
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
**To open fullscreen, pass `fullscreen: true` to the constructor.** Setting it
|
|
165
|
+
after the window is up also works, but shows one windowed frame first. Players
|
|
166
|
+
read that flash as a broken startup. A game whose settings say fullscreen passes
|
|
167
|
+
the setting to `new`.
|
|
168
|
+
|
|
169
|
+
`width` and `height` still matter when the window opens fullscreen. The window
|
|
170
|
+
takes that size when it leaves fullscreen.
|
|
171
|
+
|
|
172
|
+
rgame uses **desktop** fullscreen: the window covers the screen at the screen's
|
|
173
|
+
own resolution. The display never changes mode. The switch is instant, needs no
|
|
174
|
+
mode list, and leaves other windows alone. The game gets a bigger view, not a
|
|
175
|
+
different one.
|
|
176
|
+
|
|
177
|
+
**Switching resizes the window**, so the loop calls
|
|
178
|
+
[`resize`](#hooks-you-can-override) with the new size. A user dragging a window
|
|
179
|
+
edge triggers the same call. Everything that lays out against the window learns
|
|
180
|
+
of the change through that one path. So a scene reads the `view` it is drawn
|
|
181
|
+
with, not the width it passed to `new`.
|
|
182
|
+
|
|
183
|
+
A layout written against fixed numbers will not follow a bigger view.
|
|
184
|
+
[`scale_mode:`](game.md#scale_mode--what-width-and-height-mean) on `RGame::Game`
|
|
185
|
+
offers the alternative: keep a logical size and scale it onto the window.
|
|
186
|
+
|
|
187
|
+
`examples/fullscreen` shows both ways to open, the switch, and every scale mode.
|
|
188
|
+
|
|
189
|
+
## Raw input queries
|
|
190
|
+
|
|
191
|
+
`App` exposes the input snapshot directly. Code written against Core alone
|
|
192
|
+
usually calls [`RGame::Core::Input`](input.md) instead, whose `down?` and `axis`
|
|
193
|
+
default the device to the keyboard. A game on `RGame::Game` reads actions and
|
|
194
|
+
never calls either. These queries are the primitives underneath:
|
|
195
|
+
|
|
196
|
+
| Method | |
|
|
197
|
+
|---|---|
|
|
198
|
+
| `input_down?(device, button_id)` | Is that button held on that device? |
|
|
199
|
+
| `input_axis(device, axis_id)` | Analog axis value; sticks −1.0…1.0, triggers 0.0…1.0. |
|
|
200
|
+
| `gamepad_present?(slot)` | Is a controller plugged into that player slot? |
|
|
201
|
+
| `gamepad_name(slot)` | Its human-readable name, or `nil`. |
|
|
202
|
+
| `gamepad_count` | How many controllers are connected. |
|
|
203
|
+
|
|
204
|
+
The query is named `gamepad_present?`, not `gamepad_connected?`. The hot-plug
|
|
205
|
+
*hook* above owns that name, and two methods that differ only by a `?` invite
|
|
206
|
+
mistakes.
|
|
207
|
+
|
|
208
|
+
## The player's languages
|
|
209
|
+
|
|
210
|
+
```ruby
|
|
211
|
+
require 'rgame/core'
|
|
212
|
+
|
|
213
|
+
RGame::Core.preferred_locales # => ["de-AT", "en"] — most preferred first
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
`RGame::Core.preferred_locales` returns the locales the operating system says the
|
|
217
|
+
user prefers, most preferred first. It is a module function: it needs no app and
|
|
218
|
+
no window. Each entry is a language, followed by a hyphen and a country when the
|
|
219
|
+
OS names one. The list is `[]` when the OS names none, as on Linux under
|
|
220
|
+
`LANG=C`. It never holds `nil` or an empty String.
|
|
221
|
+
|
|
222
|
+
It returns what SDL reports, unfiltered. On Linux SDL reads `LANG`, then
|
|
223
|
+
`LANGUAGE`, and ignores `LC_ALL`, so a locale can appear twice. macOS and Windows
|
|
224
|
+
ask the OS. Picking the language a game shows is
|
|
225
|
+
[`I18n.choose`](localization.md#locales-and-the-fallback-chain), and `RGame::Game`
|
|
226
|
+
makes that choice at startup.
|
|
227
|
+
|
|
228
|
+
## When a hook raises
|
|
229
|
+
|
|
230
|
+
`run` re-raises any exception from a hook, with its class, message and backtrace
|
|
231
|
+
intact. The loop shuts down cleanly first and closes the window:
|
|
232
|
+
|
|
233
|
+
```ruby
|
|
234
|
+
begin
|
|
235
|
+
MyGame.new.run
|
|
236
|
+
rescue MyGameError => e
|
|
237
|
+
# the loop has already stopped by the time this runs
|
|
238
|
+
end
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
A **non-local exit** cannot cross the loop that way. That covers `throw`,
|
|
242
|
+
`break` or `return` leaving a hook. `run` reports it as a `RuntimeError` that
|
|
243
|
+
tells you to use `close`. Only `close` and closing the window stop the loop.
|
|
244
|
+
|
|
245
|
+
## Several windows in one process
|
|
246
|
+
|
|
247
|
+
A process can create more than one `App`, and their lifetimes may overlap. The
|
|
248
|
+
engine keeps SDL alive until the last one is gone. Test suites rely on this: they
|
|
249
|
+
create and discard a window per example.
|