rgame 0.2.0 → 0.3.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 +4 -4
- data/CHANGELOG.md +135 -1
- data/README.md +67 -65
- data/docs/api/README.md +86 -82
- data/docs/api/app.md +125 -68
- data/docs/api/assets.md +203 -165
- data/docs/api/audio.md +130 -89
- data/docs/api/cli.md +259 -0
- data/docs/api/components.md +1032 -265
- data/docs/api/drawing.md +160 -160
- data/docs/api/examples.md +263 -0
- data/docs/api/game.md +149 -50
- data/docs/api/images.md +39 -39
- data/docs/api/input.md +226 -148
- data/docs/api/internals.md +241 -62
- data/docs/api/localization.md +285 -0
- data/docs/api/scene_graph.md +397 -244
- data/docs/api/signals.md +81 -76
- data/docs/api/systems.md +186 -73
- data/docs/api/text.md +58 -41
- data/docs/api/tile_maps.md +237 -0
- data/docs/api/toolbox.md +395 -146
- data/docs/api/ui.md +883 -54
- data/docs/api/values.md +216 -33
- 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/ext/rgame_core/app/app.c +33 -3
- data/ext/rgame_core/app/locale.c +67 -0
- data/ext/rgame_core/app/locale.h +28 -0
- data/ext/rgame_core/audio/audio.c +39 -2
- data/ext/rgame_core/example.rb +0 -49
- data/ext/rgame_core/extconf.rb +0 -125
- data/ext/rgame_core/include/rgame/core.h +38 -2
- data/ext/rgame_core/ruby/audio_ext.c +10 -5
- data/ext/rgame_core/ruby/core_ext.c +30 -7
- data/ext/rgame_core/ruby/core_ext.h +3 -0
- data/ext/rgame_core/ruby/locale_ext.c +44 -0
- data/ext/rgame_core/ruby/recording_ext.c +1 -1
- data/ext/rgame_core/ruby/renderer_ext.c +20 -20
- data/ext/rgame_util/extconf.rb +2 -20
- data/ext/rgame_util/route_search.c +305 -0
- data/ext/rgame_util/route_search.h +86 -0
- data/ext/rgame_util/route_search_ext.c +150 -0
- data/ext/rgame_util/solid_grid.c +58 -0
- data/ext/rgame_util/solid_grid.h +49 -0
- data/ext/rgame_util/solid_grid_ext.c +161 -0
- data/ext/rgame_util/tile_sweep.c +164 -0
- data/ext/rgame_util/tile_sweep.h +62 -0
- data/ext/rgame_util/tile_sweep_ext.c +155 -0
- data/ext/rgame_util/util_ext.c +3 -0
- data/ext/rgame_util/util_ext.h +15 -0
- data/lib/rgame/boot.rb +0 -10
- 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 +6 -44
- data/lib/rgame/core/asset_manager.rb +13 -31
- data/lib/rgame/core/audio.rb +37 -16
- data/lib/rgame/core/font.rb +0 -3
- data/lib/rgame/core/locale.rb +22 -0
- data/lib/rgame/core/nine_slice.rb +0 -21
- data/lib/rgame/core/renderer.rb +6 -63
- data/lib/rgame/core/sprite_sheet.rb +0 -3
- data/lib/rgame/core/tile_map_renderer.rb +2 -19
- data/lib/rgame/core/ui_atlas.rb +28 -13
- data/lib/rgame/core.rb +1 -8
- data/lib/rgame/engine/actor_blockers.rb +131 -0
- data/lib/rgame/engine/animation_set.rb +1 -0
- data/lib/rgame/engine/audio_director.rb +36 -6
- data/lib/rgame/engine/bounds_blockers.rb +74 -0
- data/lib/rgame/engine/camera.rb +3 -3
- data/lib/rgame/engine/circle_collider.rb +4 -2
- data/lib/rgame/engine/collision_box.rb +26 -1
- data/lib/rgame/engine/collision_system.rb +110 -22
- data/lib/rgame/engine/component.rb +34 -10
- data/lib/rgame/engine/components/action_trigger.rb +0 -1
- data/lib/rgame/engine/components/animated_sprite.rb +27 -25
- data/lib/rgame/engine/components/box_collider.rb +99 -0
- data/lib/rgame/engine/components/camera_follow.rb +6 -5
- data/lib/rgame/engine/components/character_body.rb +20 -61
- data/lib/rgame/engine/components/circle_collider.rb +47 -11
- data/lib/rgame/engine/components/collision_world.rb +159 -31
- data/lib/rgame/engine/components/despawn_offscreen.rb +24 -8
- 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 +123 -31
- data/lib/rgame/engine/components/player_controller.rb +5 -2
- data/lib/rgame/engine/components/pool.rb +1 -1
- data/lib/rgame/engine/components/screen_wrap.rb +33 -11
- data/lib/rgame/engine/components/sprite.rb +12 -6
- data/lib/rgame/engine/components/targeting.rb +9 -11
- data/lib/rgame/engine/components/thrust_controller.rb +1 -1
- data/lib/rgame/engine/components/tile_world.rb +41 -13
- data/lib/rgame/engine/components/timer.rb +1 -1
- data/lib/rgame/engine/components/velocity.rb +23 -7
- data/lib/rgame/engine/components/wander_controller.rb +6 -2
- data/lib/rgame/engine/components/world.rb +133 -0
- data/lib/rgame/engine/contact_set.rb +74 -0
- data/lib/rgame/engine/culling.rb +4 -6
- data/lib/rgame/engine/debug_overlay.rb +5 -11
- 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 +276 -51
- data/lib/rgame/engine/input/action_mapper.rb +0 -26
- data/lib/rgame/engine/input/actions.rb +2 -8
- data/lib/rgame/engine/input/input_map.rb +39 -21
- data/lib/rgame/engine/nav_grid.rb +87 -0
- data/lib/rgame/engine/node2d.rb +248 -133
- data/lib/rgame/engine/path.rb +4 -6
- data/lib/rgame/engine/players.rb +6 -13
- data/lib/rgame/engine/presentation.rb +171 -0
- data/lib/rgame/engine/scene/scene_stack.rb +4 -4
- data/lib/rgame/engine/sealed_privates.rb +54 -0
- data/lib/rgame/engine/spatial_hash.rb +51 -19
- data/lib/rgame/engine/text.rb +194 -0
- data/lib/rgame/engine/tile_blockers.rb +63 -0
- data/lib/rgame/engine/tile_map.rb +2 -3
- data/lib/rgame/engine/tile_map_layer.rb +0 -2
- data/lib/rgame/engine/tileset.rb +2 -4
- data/lib/rgame/engine/timer.rb +2 -2
- 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 +246 -71
- 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/viewports.rb +2 -5
- data/lib/rgame/engine/world_view.rb +5 -4
- data/lib/rgame/engine.rb +31 -25
- data/lib/rgame/game.rb +99 -27
- 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 +20 -24
- data/lib/rgame/util/controls.rb +14 -28
- 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 +0 -9
- data/lib/rgame/util/tile_sweep.rb +36 -0
- data/lib/rgame/util/z.rb +4 -14
- data/lib/rgame/util.rb +4 -3
- data/lib/rgame/version.rb +1 -1
- data/lib/rgame.rb +0 -15
- metadata +152 -11
- data/lib/rgame/engine/actor.rb +0 -53
- data/lib/rgame/engine/body.rb +0 -49
- data/lib/rgame/engine/cached_label.rb +0 -33
- data/lib/rgame/engine/input/player_controller.rb +0 -14
- data/lib/rgame/engine/matrix.rb +0 -32
- data/lib/rgame/engine/resettable.rb +0 -67
- data/lib/rgame/engine/tile_collision.rb +0 -78
- data/lib/rgame/engine/ui/menu_item.rb +0 -84
data/docs/api/README.md
CHANGED
|
@@ -1,46 +1,51 @@
|
|
|
1
1
|
# rgame API guide
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
7
|
|
|
8
8
|
| Page | Covers |
|
|
9
9
|
|---|---|
|
|
10
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 |
|
|
11
12
|
| [App](app.md) | `RGame::Core::App` — the window and the frame loop |
|
|
12
13
|
| [Game](game.md) | `RGame::Game` — the entry point that wires both halves together |
|
|
13
14
|
| [Input](input.md) | `RGame::Core::Input`, `RGame::Util::Controls`, `RGame::Core::Gamepad` |
|
|
14
15
|
| [Drawing](drawing.md) | `RGame::Core::Renderer` — shapes, images, transforms, clipping, recordings |
|
|
15
|
-
| [Images](images.md) | `RGame::Core::Image` — loading PNGs, subimages,
|
|
16
|
+
| [Images](images.md) | `RGame::Core::Image` — loading PNGs, subimages, tiles |
|
|
16
17
|
| [Text](text.md) | `RGame::Core::Font` and `Renderer#text` |
|
|
17
18
|
| [Audio](audio.md) | `RGame::Core::Audio`, `Sample`, `Song` — samples and streamed music |
|
|
18
|
-
| [
|
|
19
|
-
| [Values](values.md) | `RGame::Util::Color`, `
|
|
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 |
|
|
20
22
|
|
|
21
|
-
The scene graph
|
|
23
|
+
The scene graph is `RGame::Engine`, the layer a game is written in:
|
|
22
24
|
|
|
23
25
|
| Page | Covers |
|
|
24
26
|
|---|---|
|
|
25
27
|
| [Scene graph](scene_graph.md) | `Node2D`, the tree, the lifecycle, transforms and the camera |
|
|
26
28
|
| [Components](components.md) | Reusable behaviour attached to a node |
|
|
27
29
|
| [Systems](systems.md) | Services a subtree shares — collision worlds, tile worlds |
|
|
28
|
-
| [
|
|
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 |
|
|
29
32
|
| [Signals](signals.md) | The typed observer pattern nodes talk through |
|
|
30
|
-
| [Toolbox](toolbox.md) | What a game author reaches for directly: pooling, timers, camera,
|
|
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 |
|
|
31
35
|
| [Internal building blocks](internals.md) | What components are built from: collision maths, the spatial index, animation playback |
|
|
32
36
|
|
|
33
|
-
**The engine is a work in progress.**
|
|
34
|
-
|
|
35
|
-
split-screen players runs on top
|
|
36
|
-
|
|
37
|
-
gives each player a region of the screen, focus and activation, and
|
|
38
|
-
|
|
39
|
-
|
|
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.
|
|
40
45
|
|
|
41
46
|
## Loading it
|
|
42
47
|
|
|
43
|
-
|
|
48
|
+
rgame has three entry points. Each one loads everything the one before it does:
|
|
44
49
|
|
|
45
50
|
```ruby
|
|
46
51
|
require 'rgame' # RGame::Util + RGame::Engine — no graphics libraries at all
|
|
@@ -48,23 +53,22 @@ require 'rgame/core' # adds the window, the GPU and the sound device (SDL2 + Op
|
|
|
48
53
|
require 'rgame/game' # all of it, wired together — what a game writes
|
|
49
54
|
```
|
|
50
55
|
|
|
51
|
-
A game
|
|
56
|
+
A game requires the last one. Its entry point is `RGame::Game`; see
|
|
52
57
|
[Game](game.md).
|
|
53
58
|
|
|
54
|
-
|
|
55
|
-
whole scene graph
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
memory map.
|
|
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.
|
|
59
63
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
exactly one layer.
|
|
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.
|
|
63
66
|
|
|
64
|
-
|
|
65
|
-
|
|
67
|
+
`rgame/core` also loads `RGame::Util::Controls`, the input id vocabulary,
|
|
68
|
+
because the input classes need it.
|
|
66
69
|
|
|
67
|
-
|
|
70
|
+
`gem install rgame` compiles both extensions. In a checkout of the repository,
|
|
71
|
+
compile them before you require anything:
|
|
68
72
|
|
|
69
73
|
```
|
|
70
74
|
make ext # builds both, copies them into lib/rgame/
|
|
@@ -72,45 +76,45 @@ make ext # builds both, copies them into lib/rgame/
|
|
|
72
76
|
|
|
73
77
|
## The three namespaces
|
|
74
78
|
|
|
75
|
-
Everything lives under `RGame
|
|
76
|
-
|
|
79
|
+
Everything lives under `RGame`. What a class depends on decides between `Util`
|
|
80
|
+
and `Core`. `Engine` holds what a game is written in.
|
|
77
81
|
|
|
78
82
|
| | `RGame::Util` | `RGame::Core` | `RGame::Engine` |
|
|
79
83
|
|---|---|---|---|
|
|
80
84
|
| 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 |
|
|
81
|
-
|
|
|
85
|
+
| Classes | `Color`, `Tensor`, `Controls`, `Z`, `SolidGrid`, `RouteSearch`, `TileSweep`, `SaveFile` | `App`, `Input`, `Gamepad`, `Image`, `Renderer`, `Recording`, `Font`, `Audio`, `SpriteSheet`, `NineSlice`, `UiAtlas`, `TileMapRenderer`, `AssetManager` | `Node2D`, components, systems, signals, `TileMap`, `Player`, `InputMap`, `UI::Menu` |
|
|
82
86
|
| Loading it costs | nothing | SDL2 + OpenGL in your process | nothing |
|
|
83
87
|
|
|
84
|
-
|
|
85
|
-
|
|
88
|
+
**A value goes in `Util`; only a handle-owner goes in `Core`.** A colour is a
|
|
89
|
+
value. A window is not.
|
|
86
90
|
|
|
87
|
-
`RGame::Engine` sits above both
|
|
88
|
-
having:
|
|
91
|
+
`RGame::Engine` sits above both and follows three rules:
|
|
89
92
|
|
|
90
|
-
-
|
|
91
|
-
-
|
|
92
|
-
-
|
|
93
|
-
receives a renderer and calls methods
|
|
94
|
-
never
|
|
93
|
+
- It may hold `Util` values as attributes, such as a `Color` or a `Tensor`.
|
|
94
|
+
- It may **not name `Core` at all**: no require, no constant, no attribute.
|
|
95
|
+
- It reaches `Core` only through objects it receives. A node's `on_draw`
|
|
96
|
+
receives a renderer and calls its methods by name. The node never stores the
|
|
97
|
+
renderer and never checks its class.
|
|
95
98
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
99
|
+
These rules keep a whole game runnable and testable with no window: its rules,
|
|
100
|
+
its scenes, its collisions. The testing section below relies on that. RuboCop
|
|
101
|
+
cops enforce the rules in both directions inside rgame's own repository, and a
|
|
102
|
+
project from `rgame new` runs the first of them over its `nodes/` and `spec/`.
|
|
103
|
+
See [the generated RuboCop configuration](cli.md#the-generated-rubocop-configuration).
|
|
100
104
|
|
|
101
|
-
`RGame::Game` is the
|
|
102
|
-
`
|
|
103
|
-
|
|
105
|
+
`RGame::Game` is the one exception: the only class that names both `Engine` and
|
|
106
|
+
`Core`. It exists to connect the two halves. Keeping that in one file lets the
|
|
107
|
+
rule hold everywhere else.
|
|
104
108
|
|
|
105
109
|
## A complete program
|
|
106
110
|
|
|
107
|
-
A game is a tree of nodes
|
|
111
|
+
A game is a tree of nodes, run by `RGame::Game`.
|
|
108
112
|
|
|
109
113
|
```ruby
|
|
110
114
|
require 'rgame/game'
|
|
111
115
|
|
|
112
116
|
# One game object: a square the player walks around. Pure Engine — it names no
|
|
113
|
-
# graphics class, so it runs
|
|
117
|
+
# graphics class, so it runs unchanged in a spec with no window.
|
|
114
118
|
class Hero < RGame::Engine::Node2D
|
|
115
119
|
SPEED = 200.0
|
|
116
120
|
|
|
@@ -135,10 +139,11 @@ class Hero < RGame::Engine::Node2D
|
|
|
135
139
|
end
|
|
136
140
|
|
|
137
141
|
# The renderer is handed in and never stored; `view` is the viewport being
|
|
138
|
-
# drawn into, which most nodes ignore. Draw
|
|
139
|
-
#
|
|
142
|
+
# drawn into, which most nodes ignore. Draw in the node's own space: the
|
|
143
|
+
# traversal has already put the renderer on this node, so (0, 0) is here.
|
|
144
|
+
# See docs/api/scene_graph.md, "Drawing happens in local space".
|
|
140
145
|
def on_draw(renderer, _view)
|
|
141
|
-
renderer.rect(
|
|
146
|
+
renderer.rect(0, 0, width, height)
|
|
142
147
|
end
|
|
143
148
|
end
|
|
144
149
|
|
|
@@ -153,27 +158,26 @@ end
|
|
|
153
158
|
RGame::Game.new(root: Scene.new, width: 800, height: 600, caption: 'My Game').start
|
|
154
159
|
```
|
|
155
160
|
|
|
156
|
-
|
|
157
|
-
`
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
for the full list, and [Game](game.md) for what `Game` assembles around it: the
|
|
161
|
+
Subclass `Node2D` and override the hooks you need: `on_control`, `on_update`,
|
|
162
|
+
`on_draw`, and the lifecycle hooks around them. A hook you do not override does
|
|
163
|
+
nothing. Separate phase methods do the bookkeeping: they push the node's
|
|
164
|
+
transform, drive components and descend into children. You never override
|
|
165
|
+
those, so there is no `super` to forget. [Scene graph](scene_graph.md) lists
|
|
166
|
+
every hook. [Game](game.md) describes what `Game` builds around the tree: the
|
|
163
167
|
window, the renderer, the asset manager, the sound device, the input mapper and
|
|
164
168
|
the players.
|
|
165
169
|
|
|
166
|
-
|
|
167
|
-
/ `move_y`
|
|
168
|
-
[Input](input.md).
|
|
170
|
+
Without an `input_map:`, `Game` uses the default map shown above. It binds
|
|
171
|
+
eight-way `move_x` / `move_y` to the arrows, WASD, the d-pad and the left stick,
|
|
172
|
+
and adds `fire`. See [Input](input.md).
|
|
169
173
|
|
|
170
174
|
## Testing a game built on this
|
|
171
175
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
unchanged — drive their phases directly and a simulated hour takes milliseconds:
|
|
176
|
+
`require 'rgame'` loads `Util` and the whole scene graph with no SDL and no
|
|
177
|
+
OpenGL. The nodes from the program above run there unchanged. A spec drives
|
|
178
|
+
their phases directly, so a simulated hour takes milliseconds:
|
|
176
179
|
|
|
180
|
+
<!-- doc-example: skip — an RSpec file for the Hero above, run by rspec -->
|
|
177
181
|
```ruby
|
|
178
182
|
require 'rgame'
|
|
179
183
|
|
|
@@ -195,20 +199,20 @@ RSpec.describe Hero do
|
|
|
195
199
|
end
|
|
196
200
|
```
|
|
197
201
|
|
|
198
|
-
Two
|
|
202
|
+
Two properties make this work:
|
|
199
203
|
|
|
200
|
-
- **`update` takes `dt` as an argument
|
|
201
|
-
|
|
202
|
-
- **A node never holds a renderer.** `on_draw`
|
|
203
|
-
|
|
204
|
-
|
|
204
|
+
- **`update` takes `dt` as an argument and reads no clock.** A test passes any
|
|
205
|
+
timestep it likes, so it can simulate minutes of play in milliseconds.
|
|
206
|
+
- **A node never holds a renderer.** `on_draw` receives one. A spec passes a
|
|
207
|
+
recording double and asserts on what the node asked it to draw. rgame's own
|
|
208
|
+
suite checks its fake renderer against the real one with a shared contract in
|
|
209
|
+
`spec/support/shared_examples/`. The whole suite runs headless.
|
|
205
210
|
|
|
206
|
-
`hero.x
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
211
|
+
The spec asserts `hero.x`, not `world_x`, because this hero has no parent. The
|
|
212
|
+
world transform accumulates from the parent, and a node without one resolves to
|
|
213
|
+
the origin. Under a root, game logic reads `world_x`. Drawing reads neither; see
|
|
214
|
+
[Scene graph](scene_graph.md#drawing-happens-in-local-space).
|
|
210
215
|
|
|
211
|
-
Keep the
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
pixels on screen.
|
|
216
|
+
Keep the code that decides *what happens* in `RGame::Engine`. That layer cannot
|
|
217
|
+
name `RGame::Core`, so it cannot come to depend on a window. Only the thin layer
|
|
218
|
+
that puts pixels on screen then needs one.
|
data/docs/api/app.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# `RGame::Core::App`
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
`run`.
|
|
3
|
+
`App` owns the window and runs the frame loop. Subclass it, override the hooks
|
|
4
|
+
you need, and call `run`.
|
|
5
5
|
|
|
6
6
|
```ruby
|
|
7
7
|
require 'rgame/core'
|
|
@@ -13,44 +13,44 @@ end
|
|
|
13
13
|
MyGame.new.run # returns when the loop stops
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
`App.new` takes keyword arguments only. `width:`, `height:` and
|
|
17
|
-
|
|
18
|
-
opens a real window
|
|
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
19
|
|
|
20
20
|
## What the app owns
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
The app builds two objects on first use. A game needs exactly one of each:
|
|
23
23
|
|
|
24
24
|
```ruby
|
|
25
|
+
require 'rgame/core'
|
|
26
|
+
|
|
25
27
|
class MyGame < RGame::Core::App
|
|
26
|
-
def initialize = super(width: 640, height: 480, caption: 'demo', media_root:
|
|
28
|
+
def initialize = super(width: 640, height: 480, caption: 'demo', media_root: 'assets')
|
|
27
29
|
end
|
|
28
30
|
|
|
29
|
-
app
|
|
30
|
-
app.
|
|
31
|
+
app = MyGame.new
|
|
32
|
+
app.assets # => RGame::Core::AssetManager — rooted at media_root
|
|
33
|
+
app.audio # => RGame::Core::Audio — the sound device
|
|
31
34
|
```
|
|
32
35
|
|
|
33
36
|
**A game never constructs either of them.** An image belongs to one OpenGL
|
|
34
|
-
context
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
every class that ends up owning an image.
|
|
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.
|
|
38
40
|
|
|
39
|
-
Both are
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
also the right moment to open it.
|
|
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.
|
|
43
44
|
|
|
44
|
-
`media_root` is read-only and
|
|
45
|
-
|
|
46
|
-
against two roots.
|
|
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
47
|
|
|
48
|
-
|
|
48
|
+
[Assets](assets.md) describes the asset manager.
|
|
49
49
|
|
|
50
50
|
## The frame loop
|
|
51
51
|
|
|
52
|
-
`run` drives the loop until something stops it,
|
|
53
|
-
One rendered frame
|
|
52
|
+
`run` drives the loop until something stops it, and calls back into your object.
|
|
53
|
+
One rendered frame runs these steps:
|
|
54
54
|
|
|
55
55
|
```
|
|
56
56
|
poll input and window events → button_down / button_up / resize
|
|
@@ -59,33 +59,30 @@ One rendered frame looks like this:
|
|
|
59
59
|
update(dt) → zero or more times (see below)
|
|
60
60
|
needs_redraw? → once; false skips the draw
|
|
61
61
|
draw → once, unless skipped
|
|
62
|
+
frame_end → once, after draw, unless skipped
|
|
62
63
|
```
|
|
63
64
|
|
|
64
65
|
### `update(dt)` runs a *fixed* number of times, not once per frame
|
|
65
66
|
|
|
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.
|
|
67
72
|
|
|
68
|
-
|
|
69
|
-
frame
|
|
70
|
-
|
|
71
|
-
frame took a long time and the simulation is catching up). Catch-up is capped,
|
|
72
|
-
so a very slow frame makes time slow down rather than spiral.
|
|
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.
|
|
73
76
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
The practical consequence: **do not sample input inside `update`.** A key held
|
|
79
|
-
for one frame would be read once or five times depending on how slow the last
|
|
80
|
-
frame was. Sample it in `frame_begin` instead, or rely on `Input`, which reads
|
|
81
|
-
a snapshot taken once per frame and therefore answers identically for every
|
|
82
|
-
tick of that frame.
|
|
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.
|
|
83
81
|
|
|
84
82
|
### `needs_redraw?`
|
|
85
83
|
|
|
86
|
-
Return `false`
|
|
87
|
-
|
|
88
|
-
is `true`.
|
|
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`.
|
|
89
86
|
|
|
90
87
|
```ruby
|
|
91
88
|
def update(_dt)
|
|
@@ -100,12 +97,12 @@ def draw
|
|
|
100
97
|
end
|
|
101
98
|
```
|
|
102
99
|
|
|
103
|
-
|
|
104
|
-
|
|
100
|
+
`update` runs only when a step happened. Setting `@dirty = true` there is usually
|
|
101
|
+
the whole rule.
|
|
105
102
|
|
|
106
103
|
## Hooks you can override
|
|
107
104
|
|
|
108
|
-
Every
|
|
105
|
+
Every hook inherits a default that does nothing. Override only what you use.
|
|
109
106
|
|
|
110
107
|
| Hook | When |
|
|
111
108
|
|---|---|
|
|
@@ -113,20 +110,23 @@ Every one has an inherited no-op default, so override only what you use.
|
|
|
113
110
|
| `update(dt)` | One fixed simulation tick. |
|
|
114
111
|
| `needs_redraw?` | Before drawing; `false` skips `draw`. Default `true`. |
|
|
115
112
|
| `draw` | Render one frame. |
|
|
116
|
-
| `
|
|
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. |
|
|
117
115
|
| `button_up(id)` | A key was released. |
|
|
118
116
|
| `resize(width, height)` | The window changed size. |
|
|
119
117
|
| `gamepad_connected(slot)` | A controller arrived in a player slot. |
|
|
120
118
|
| `gamepad_disconnected(slot)` | A controller left a slot. |
|
|
121
119
|
|
|
122
|
-
`id` is a value from [`RGame::Util::Controls`](input.md)
|
|
120
|
+
`id` is a value from [`RGame::Util::Controls`](input.md), such as
|
|
123
121
|
`Controls::KEY_ESCAPE`.
|
|
124
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
|
+
|
|
125
126
|
### There is no built-in quit key
|
|
126
127
|
|
|
127
|
-
Closing the window stops the loop, because
|
|
128
|
-
|
|
129
|
-
for you:
|
|
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
130
|
|
|
131
131
|
```ruby
|
|
132
132
|
def button_down(id)
|
|
@@ -142,17 +142,56 @@ end
|
|
|
142
142
|
| `close` | Asks the loop to stop. Safe to call from inside any hook. |
|
|
143
143
|
| `width`, `height` | Current window size. |
|
|
144
144
|
| `caption`, `caption=` | The window title. |
|
|
145
|
-
| `
|
|
145
|
+
| `fullscreen?`, `fullscreen=` | Whether the window covers the screen. |
|
|
146
|
+
| `ticks_ms` | Monotonic milliseconds since startup. For measuring frames, not for drawing. |
|
|
146
147
|
| `fps` | Most recent frames-per-second reading, updated about once a second. |
|
|
147
148
|
|
|
148
|
-
`close` takes effect promptly
|
|
149
|
-
|
|
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.
|
|
150
188
|
|
|
151
189
|
## Raw input queries
|
|
152
190
|
|
|
153
|
-
`App` exposes the input snapshot directly.
|
|
154
|
-
[`RGame::Core::Input`](input.md),
|
|
155
|
-
|
|
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:
|
|
156
195
|
|
|
157
196
|
| Method | |
|
|
158
197
|
|---|---|
|
|
@@ -162,15 +201,34 @@ numeric ids, but these are the primitives underneath:
|
|
|
162
201
|
| `gamepad_name(slot)` | Its human-readable name, or `nil`. |
|
|
163
202
|
| `gamepad_count` | How many controllers are connected. |
|
|
164
203
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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.
|
|
168
227
|
|
|
169
228
|
## When a hook raises
|
|
170
229
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
is not left stranded:
|
|
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:
|
|
174
232
|
|
|
175
233
|
```ruby
|
|
176
234
|
begin
|
|
@@ -180,13 +238,12 @@ rescue MyGameError => e
|
|
|
180
238
|
end
|
|
181
239
|
```
|
|
182
240
|
|
|
183
|
-
A **non-local exit**
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
loop; it is the only supported way out other than closing the window.
|
|
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.
|
|
187
244
|
|
|
188
245
|
## Several windows in one process
|
|
189
246
|
|
|
190
|
-
|
|
191
|
-
engine keeps SDL alive until the last one is gone.
|
|
192
|
-
|
|
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.
|