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/values.md
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
# Value types
|
|
2
2
|
|
|
3
|
-
Everything in `RGame::Util` is a
|
|
4
|
-
GPU handle
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
**Everything in `RGame::Util` is a value**: cheap, comparable, and owning no
|
|
4
|
+
window or GPU handle. Game logic can therefore hold these types as attributes.
|
|
5
|
+
They load with `require 'rgame'` and pull in no graphics library.
|
|
6
|
+
|
|
7
|
+
`SaveFile` touches the disk, and still belongs here. It holds a *path*, not an
|
|
8
|
+
open handle. It opens a file, reads or writes it, and closes it again. What
|
|
9
|
+
decides the namespace is ownership, not I/O.
|
|
7
10
|
|
|
8
11
|
```ruby
|
|
9
12
|
require 'rgame'
|
|
@@ -11,8 +14,8 @@ require 'rgame'
|
|
|
11
14
|
|
|
12
15
|
## `RGame::Util::Color`
|
|
13
16
|
|
|
14
|
-
|
|
15
|
-
|
|
17
|
+
`Color` is an RGBA colour. Instances are **frozen** and compare **by value**, so
|
|
18
|
+
you can share one freely and use it as a Hash key.
|
|
16
19
|
|
|
17
20
|
```ruby
|
|
18
21
|
Color = RGame::Util::Color
|
|
@@ -30,7 +33,18 @@ Color.from_packed(0xFF8000C8) # 0xRRGGBBAA
|
|
|
30
33
|
| `==`, `eql?`, `hash` | Value semantics. |
|
|
31
34
|
| `inspect` | `#<RGame::Util::Color r=1 g=2 b=3 a=4>` |
|
|
32
35
|
|
|
33
|
-
|
|
36
|
+
### The named palette
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
Color::WHITE Color::BLACK Color::TRANSPARENT
|
|
40
|
+
Color::RED Color::GREEN Color::BLUE
|
|
41
|
+
Color::YELLOW Color::CYAN Color::MAGENTA
|
|
42
|
+
Color::ORANGE Color::PURPLE Color::BROWN Color::PINK
|
|
43
|
+
Color::GRAY Color::LIGHT_GRAY Color::DARK_GRAY
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Every named colour except `TRANSPARENT` is opaque. They use the CSS/X11 values,
|
|
47
|
+
so `Color::ORANGE` matches the orange a colour picker gives you.
|
|
34
48
|
|
|
35
49
|
### Out-of-range components raise
|
|
36
50
|
|
|
@@ -38,21 +52,22 @@ Named colours: `Color::WHITE`, `Color::BLACK`, `Color::TRANSPARENT`.
|
|
|
38
52
|
Color.new(300, 0, 0) # ArgumentError: red must be in 0..255, got 300
|
|
39
53
|
```
|
|
40
54
|
|
|
41
|
-
|
|
55
|
+
Clamping would hide the bug that produced the 300.
|
|
42
56
|
|
|
43
57
|
### `Color.coerce`
|
|
44
58
|
|
|
45
|
-
Drawing calls accept a colour in several forms
|
|
46
|
-
|
|
59
|
+
Drawing calls accept a colour in several forms. **`coerce` performs every such
|
|
60
|
+
conversion:**
|
|
47
61
|
|
|
48
62
|
```ruby
|
|
49
63
|
Color.coerce(nil) # => Color::WHITE — an untinted draw
|
|
50
64
|
Color.coerce([255, 128, 0]) # => opaque
|
|
51
65
|
Color.coerce([255, 128, 0, 64]) # => with alpha
|
|
52
|
-
Color.coerce(Color::WHITE) # =>
|
|
66
|
+
Color.coerce(Color::WHITE) # => Color::WHITE — the same object, not copied
|
|
53
67
|
```
|
|
54
68
|
|
|
55
|
-
|
|
69
|
+
Any other type raises `TypeError`. An array of the wrong size raises
|
|
70
|
+
`ArgumentError`.
|
|
56
71
|
|
|
57
72
|
### Value semantics in practice
|
|
58
73
|
|
|
@@ -65,14 +80,14 @@ a == b # => true — two objects, one value
|
|
|
65
80
|
a.frozen? # => true
|
|
66
81
|
```
|
|
67
82
|
|
|
68
|
-
|
|
69
|
-
|
|
83
|
+
A frozen colour is safe to hand to two sprites. Neither can change it under the
|
|
84
|
+
other.
|
|
70
85
|
|
|
71
86
|
## `RGame::Util::Tensor`
|
|
72
87
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
88
|
+
`Tensor` is a fixed-size three-dimensional grid, addressed as `[x, y, z]`. One
|
|
89
|
+
flat C array backs it, so it stays compact at the sizes a tile map or a lighting
|
|
90
|
+
volume needs.
|
|
76
91
|
|
|
77
92
|
```ruby
|
|
78
93
|
grid = RGame::Util::Tensor.new(width, height, depth)
|
|
@@ -86,8 +101,8 @@ grid.width # also #height and #depth
|
|
|
86
101
|
|
|
87
102
|
Cells hold any Ruby object. `initial:` is optional and defaults to `nil`.
|
|
88
103
|
|
|
89
|
-
|
|
90
|
-
|
|
104
|
+
**x varies fastest, then y, then z**, so each z-slice is one contiguous run. For
|
|
105
|
+
good locality, iterate a layer at a time:
|
|
91
106
|
|
|
92
107
|
```ruby
|
|
93
108
|
grid.depth.times do |z|
|
|
@@ -100,26 +115,123 @@ grid.depth.times do |z|
|
|
|
100
115
|
end
|
|
101
116
|
```
|
|
102
117
|
|
|
118
|
+
## `RGame::Util::SolidGrid`
|
|
119
|
+
|
|
120
|
+
`SolidGrid` records which cells of a tile grid are solid, one byte per cell, in C.
|
|
121
|
+
**It is the single store of a tile world's solidity.**
|
|
122
|
+
[`TileWorld`](components.md#tileworld) builds one from its map. The blockers that
|
|
123
|
+
stop a walker and the search that plans a route both read it, so they never
|
|
124
|
+
disagree about a wall.
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
fence = RGame::Util::SolidGrid.build(8, 3) { |col, row| col == 3 && row < 2 } # asks once per cell, row by row
|
|
128
|
+
fence.solid?(3, 1) # => true
|
|
129
|
+
fence.solid?(-1, 0) # => false — outside the grid is open
|
|
130
|
+
fence.width # => 8 — and #height
|
|
131
|
+
|
|
132
|
+
field = RGame::Util::SolidGrid.new(8, 3) # every cell open
|
|
133
|
+
field.set_solid(3, 1, true)
|
|
134
|
+
field.set_solid(3, 1, true)
|
|
135
|
+
field.revision # => 1 — the second write changed nothing
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
- **`revision` counts changes.** It advances only when `set_solid` changes a
|
|
139
|
+
cell. Anything derived from the cells can then tell whether it is stale, such
|
|
140
|
+
as a [`RouteSearch`](#rgameutilroutesearch)'s region labels.
|
|
141
|
+
- **Coordinates are Integers**; anything else raises `TypeError`. An Integer
|
|
142
|
+
outside the grid, however large, is outside. `solid?` answers `false` there.
|
|
143
|
+
`set_solid` raises `IndexError`, because a wall written nowhere does not exist.
|
|
144
|
+
- **The size is fixed.** A negative size, or more than `2**31 - 1` cells, raises
|
|
145
|
+
`ArgumentError`. A zero size gives an empty grid. A grid cannot be `dup`ed,
|
|
146
|
+
because a search holds on to the grid it was built over.
|
|
147
|
+
- **`SolidGrid.debug_live_grids`** returns how many grids hold cells. rgame's own
|
|
148
|
+
suite uses it to check that a grid frees its cells.
|
|
149
|
+
|
|
150
|
+
## `RGame::Util::RouteSearch`
|
|
151
|
+
|
|
152
|
+
`RouteSearch` finds connected regions and A* routes over a `SolidGrid`, in C.
|
|
153
|
+
Games use [`Engine::NavGrid`](toolbox.md#navgrid--routes-over-a-tile-grid), which
|
|
154
|
+
wraps a `RouteSearch` and states the route rules.
|
|
155
|
+
|
|
156
|
+
```ruby
|
|
157
|
+
grid = RGame::Util::SolidGrid.build(8, 3) { |col, row| col == 3 && row < 2 }
|
|
158
|
+
search = RGame::Util::RouteSearch.new(grid)
|
|
159
|
+
|
|
160
|
+
search.find(0, 0, 7, 0) # => [[0, 0], [1, 1], [2, 2], [3, 2], [4, 2], [5, 1], [6, 0], [7, 0]]
|
|
161
|
+
search.region(0, 0) # => 0 — nil for a solid cell or one outside the grid
|
|
162
|
+
search.grid # => the grid — which the search keeps alive
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
- **It reads its grid and never writes it.** Any number of searches may share a
|
|
166
|
+
grid. Each sees a change on its next query. It recomputes region labels then,
|
|
167
|
+
but only if the grid's `revision` moved.
|
|
168
|
+
- **A repeated query allocates nothing but its result.** The search allocates
|
|
169
|
+
its per-cell buffers once. Its heap grows to the largest query so far and
|
|
170
|
+
stays there. One search is therefore not safe to use from two threads at once.
|
|
171
|
+
- Coordinates follow `SolidGrid`'s rules. A non-Integer raises `TypeError`, and
|
|
172
|
+
for a cell outside the grid `find` and `region` return `nil`.
|
|
173
|
+
- **`RouteSearch.debug_live_searches`** returns how many searches hold buffers,
|
|
174
|
+
for leak checks in rgame's own suite.
|
|
175
|
+
|
|
176
|
+
## `RGame::Util::TileSweep`
|
|
177
|
+
|
|
178
|
+
`TileSweep` tests an axis-aligned box against the solid tiles of a `SolidGrid`,
|
|
179
|
+
at a given tile size, in C. Games use
|
|
180
|
+
[`Engine::TileBlockers`](internals.md#tileblockers--the-tile-grid-as-a-blocker-source).
|
|
181
|
+
Every mover that declares `blocked_by: [:tiles]` resolves against it.
|
|
182
|
+
`TileSweep` does the arithmetic underneath, and both of its queries share one
|
|
183
|
+
implementation.
|
|
184
|
+
|
|
185
|
+
```ruby
|
|
186
|
+
grid = RGame::Util::SolidGrid.build(20, 15) { |col, _row| col == 5 } # a wall at x 80..96
|
|
187
|
+
sweep = RGame::Util::TileSweep.new(grid, 16, 16)
|
|
188
|
+
|
|
189
|
+
sweep.resolve_x(58.0, 32.0, 12, 6, 14.0) # => 68.0 — flush against the wall
|
|
190
|
+
sweep.resolve_y(58.0, 32.0, 12, 6, 4.0) # => 36.0
|
|
191
|
+
sweep.travel?(10.0, 32.0, 12, 6, 50.0, 0) # => true
|
|
192
|
+
sweep.travel?(10.0, 32.0, 12, 6, 90.0, 0) # => false
|
|
193
|
+
sweep.grid # => the grid — which the sweep keeps alive
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
- **A box is a top-left corner and a size, in pixels.** Results are Floats.
|
|
197
|
+
Outside the grid is open.
|
|
198
|
+
- **`resolve_x` and `resolve_y`** move the box along one axis. If the box would
|
|
199
|
+
enter a solid tile, they snap it flush against that tile. They assume a step
|
|
200
|
+
smaller than a tile.
|
|
201
|
+
- **`travel?`** answers whether the box can move `(dx, dy)` without any resolve
|
|
202
|
+
stopping it short. It sweeps overlapping half-tile windows a quarter tile
|
|
203
|
+
apart, resolving each in both axis orders. The answer holds for a walker that
|
|
204
|
+
steps less than a quarter tile at a time.
|
|
205
|
+
- **It reads its grid and never writes it**, so the next call sees a
|
|
206
|
+
`set_solid`. No query allocates.
|
|
207
|
+
- **Refusals.** A tile size that is not positive and finite raises
|
|
208
|
+
`ArgumentError`. So does a travel too long to sweep, beyond `2**31 - 1`
|
|
209
|
+
windows. A coordinate that is not a number raises `TypeError`. A non-finite
|
|
210
|
+
coordinate raises `FloatDomainError`, except in a resolve that does not move:
|
|
211
|
+
that returns the box where it is.
|
|
212
|
+
- **`TileSweep.debug_live_sweeps`** returns how many sweeps are allocated, for
|
|
213
|
+
leak checks in rgame's own suite.
|
|
214
|
+
|
|
103
215
|
## `RGame::Util::Z`
|
|
104
216
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
sorts a frame by.
|
|
217
|
+
`Z` is the vocabulary of draw order. It names the bands, and turns a band plus a
|
|
218
|
+
position in the tree into the single number the renderer sorts by.
|
|
108
219
|
|
|
109
220
|
```ruby
|
|
110
221
|
RGame::Util::Z::BANDS # => [:world, :hud, :overlay, :debug]
|
|
111
222
|
RGame::Util::Z::DEFAULT # => :world
|
|
112
|
-
RGame::Util::Z::Z_MIN # => -512
|
|
223
|
+
RGame::Util::Z::Z_MIN # => -512 — the smallest `z:` a drawing call may pass
|
|
113
224
|
RGame::Util::Z::Z_MAX # => 511
|
|
225
|
+
RGame::Util::Z.band?(:hud) # => true
|
|
114
226
|
```
|
|
115
227
|
|
|
116
|
-
It lives
|
|
117
|
-
graph
|
|
118
|
-
|
|
228
|
+
It lives in `Util` for the same reason as [`Controls`](input.md). The scene
|
|
229
|
+
graph decides a node's band and the renderer turns it into a z. Both must name
|
|
230
|
+
`Z`, and neither may name the other's layer.
|
|
119
231
|
|
|
120
|
-
Games rarely touch
|
|
121
|
-
`band
|
|
122
|
-
|
|
232
|
+
**Games rarely touch `Z`.** A game sets a node's `z` and, occasionally, a
|
|
233
|
+
`band:`; see [the scene graph](scene_graph.md#draw-order). `Z` guarantees that `z`
|
|
234
|
+
numbers cannot leak between nodes, and bands cannot leak into each other:
|
|
123
235
|
|
|
124
236
|
| | |
|
|
125
237
|
|---|---|
|
|
@@ -127,7 +239,78 @@ Games rarely touch it. What a game writes is a node's `z` and, occasionally, a
|
|
|
127
239
|
| `Z_MIN`…`Z_MAX` | what a `z:` on a drawing call may be; anything else raises |
|
|
128
240
|
| `STRIDE` | `2**40` — the gap between bands, which no `z:` can cross |
|
|
129
241
|
|
|
130
|
-
Every value is an integer below `2**42
|
|
131
|
-
is exact below `2**53
|
|
132
|
-
|
|
133
|
-
|
|
242
|
+
Every value is an integer below `2**42`. The draw queue sorts on a `double`, which
|
|
243
|
+
is exact below `2**53`. Two different slots therefore never round to the same
|
|
244
|
+
key. If they did, two sprites would swap places between frames, and nobody would
|
|
245
|
+
suspect a precision problem.
|
|
246
|
+
|
|
247
|
+
## `RGame::Util::SaveFile`
|
|
248
|
+
|
|
249
|
+
`SaveFile` stores a game's saved state as one JSON file.
|
|
250
|
+
|
|
251
|
+
```ruby
|
|
252
|
+
save = RGame::Util::SaveFile.new('slot1.json', game: 'sheepdog')
|
|
253
|
+
|
|
254
|
+
save.write(dog: [120, 80], sheep: [[40, 40], [90, 30]])
|
|
255
|
+
save.read # => { dog: [120, 80], sheep: [[40, 40], [90, 30]] }
|
|
256
|
+
save.exist? # => true
|
|
257
|
+
save.delete
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
`SaveFile.new(name, game: 'rgame', dir: nil)` places `name` in
|
|
261
|
+
`SaveFile.directory(game)`, or in `dir:` when given. `path` returns the full path.
|
|
262
|
+
`delete` does nothing when the file is already gone.
|
|
263
|
+
|
|
264
|
+
Keys come back as Symbols, so a game writes and reads one shape. JSON comes from
|
|
265
|
+
the standard library, so `SaveFile` adds no runtime dependency.
|
|
266
|
+
|
|
267
|
+
### Reading never raises
|
|
268
|
+
|
|
269
|
+
**`read` returns its default for any file it cannot use.** The default is `{}`,
|
|
270
|
+
or whatever you pass. That covers a file that is missing, empty, truncated, not
|
|
271
|
+
JSON, or JSON that is not an object. A directory in the way and an unreadable
|
|
272
|
+
file get the same answer.
|
|
273
|
+
|
|
274
|
+
This is why to prefer `SaveFile` over `JSON.parse(File.read(path))`. A save file
|
|
275
|
+
is the one input a game did not produce this run. It survives crashes, full
|
|
276
|
+
disks, killed processes, text editors, and copies from other machines. A game
|
|
277
|
+
that raises on any of those cannot start again. The player's only remedy is to
|
|
278
|
+
find and delete a file nobody told them about. Losing a save is bad; refusing to
|
|
279
|
+
launch is worse.
|
|
280
|
+
|
|
281
|
+
**Writing still raises.** A failed read has a sensible answer: "there is no
|
|
282
|
+
save". A failed write has none. Discarding progress without a word gets noticed
|
|
283
|
+
hours later, when the progress is gone.
|
|
284
|
+
|
|
285
|
+
### Writing is atomic
|
|
286
|
+
|
|
287
|
+
**`write` never leaves half a save.** It writes a temporary file beside the
|
|
288
|
+
target and renames it over the top. `File.rename` within one directory is atomic
|
|
289
|
+
on every platform the engine supports. A save is either the old one or the new
|
|
290
|
+
one.
|
|
291
|
+
|
|
292
|
+
A game most wants to save on the way out, which is also when it is most likely
|
|
293
|
+
to be killed. A plain `File.write` truncates first and fills in after. A crash in
|
|
294
|
+
between leaves a zero-byte file where the afternoon's progress was.
|
|
295
|
+
|
|
296
|
+
### Where saves go
|
|
297
|
+
|
|
298
|
+
`SaveFile.directory(game)` follows each platform's convention instead of
|
|
299
|
+
dropping a dotfile in the home directory:
|
|
300
|
+
|
|
301
|
+
| | |
|
|
302
|
+
|---|---|
|
|
303
|
+
| Linux | `$XDG_DATA_HOME/<game>`, or `~/.local/share/<game>` |
|
|
304
|
+
| macOS | `~/Library/Application Support/<game>` |
|
|
305
|
+
| Windows | `%APPDATA%\<game>` |
|
|
306
|
+
|
|
307
|
+
Pass `dir:` to store the file somewhere else. Specs do this.
|
|
308
|
+
|
|
309
|
+
### What it does not do
|
|
310
|
+
|
|
311
|
+
**`SaveFile` does not serialize a scene tree, and a game should not either.** A
|
|
312
|
+
scene is a recipe; a save file is state. The scene rebuilds itself identically
|
|
313
|
+
every run, and the save supplies the few facts that differ. `examples/save_load`
|
|
314
|
+
shows the pattern, matching objects back up without naming any node.
|
|
315
|
+
`examples/save_load_ids` shows the case that needs names, using
|
|
316
|
+
[`Components::Identity`](components.md#identity).
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
# Example assets
|
|
2
|
+
|
|
3
|
+
Everything the examples draw and play. Fifteen files besides this one, about
|
|
4
|
+
124 KB in total — of which the music is 93 KB, and the reason for `tools/shrink_ogg.c`.
|
|
5
|
+
|
|
6
|
+
## Why these files and not the ones in `media/`
|
|
7
|
+
|
|
8
|
+
`media/` is gitignored because its contents cannot be redistributed. These can:
|
|
9
|
+
**this directory ships inside the gem**, so every `gem install rgame`
|
|
10
|
+
redistributes each file below to somebody else. That is the test any asset here
|
|
11
|
+
has to pass — not "may I use it in my game" but *"may I hand copies of it to
|
|
12
|
+
everyone who installs this library"* — and it is why the rule is CC0 or
|
|
13
|
+
authored here, with nothing in between. A licence that merely permits use, or
|
|
14
|
+
asks for a credit line, would attach an obligation to rgame and to everyone
|
|
15
|
+
downstream of it.
|
|
16
|
+
|
|
17
|
+
CC0 waives copyright entirely, so none of the sources below require a licence
|
|
18
|
+
file to be shipped alongside them or a credit to be given. This README is
|
|
19
|
+
recorded anyway: provenance is the thing that becomes impossible to reconstruct
|
|
20
|
+
later, and "where did this PNG come from" is not a question anyone should have
|
|
21
|
+
to answer from memory.
|
|
22
|
+
|
|
23
|
+
## The files
|
|
24
|
+
|
|
25
|
+
### `tileset.png` — Kenney, *Tiny Town*
|
|
26
|
+
|
|
27
|
+
- Source: <https://kenney.nl/assets/tiny-town>
|
|
28
|
+
- Licence: CC0 1.0 (stated on the page and in the `License.txt` inside the
|
|
29
|
+
download)
|
|
30
|
+
- Modification: none. This is `Tilemap/tilemap_packed.png` from the pack,
|
|
31
|
+
copied unchanged.
|
|
32
|
+
|
|
33
|
+
16x16 tiles, 12 columns x 11 rows = 132 tiles, 192x176 pixels. Kenney asks for
|
|
34
|
+
a credit but does not require one; the licence is CC0 either way.
|
|
35
|
+
|
|
36
|
+
### `tileset.tsx` — ours
|
|
37
|
+
|
|
38
|
+
A Tiled tileset over `tileset.png`. Written here rather than taken from the
|
|
39
|
+
pack, because the pack has no Tiled metadata and because **collision lives in
|
|
40
|
+
this file**: `RGame::Engine::Tileset` treats a tile as solid when it carries an
|
|
41
|
+
`<objectgroup>` with at least one object, which is what Tiled's per-tile
|
|
42
|
+
collision editor writes. There is no solid-tile list in code to fall back on, so
|
|
43
|
+
a tile with no shape here is walkable no matter what it looks like.
|
|
44
|
+
|
|
45
|
+
Solid tiles are the trees and the fence pieces: **3, 4, 5, 15, 16, 27, 28** and
|
|
46
|
+
**44, 45, 46, 47**. Everything else — grass, sand, mushrooms, plants, buildings,
|
|
47
|
+
items — is walkable. That is a deliberate floor rather than an inventory: the
|
|
48
|
+
examples only need vegetation and fences to be obstacles, and marking a tile
|
|
49
|
+
solid that no example places is a claim nothing checks. Add shapes in Tiled as
|
|
50
|
+
new examples need them.
|
|
51
|
+
|
|
52
|
+
### `town.tmx` — ours
|
|
53
|
+
|
|
54
|
+
60x40 tiles = 960x640 pixels, deliberately larger than the 640x480 window on
|
|
55
|
+
both axes so a camera has somewhere to scroll. Two layers, `ground` (entirely
|
|
56
|
+
walkable) and `obstacles` (a tree border, a fence across the middle, scattered
|
|
57
|
+
trees).
|
|
58
|
+
|
|
59
|
+
Layer data is **base64 + zlib**, which is what `RGame::Engine::TileMap.parse`
|
|
60
|
+
reads — it inflates the layer and unpacks little-endian `uint32` gids. CSV will
|
|
61
|
+
not load. Tiled writes this format when the layer format is set to "Base64
|
|
62
|
+
(zlib compressed)"; keep it that way when editing.
|
|
63
|
+
|
|
64
|
+
The fence has exactly one gap, at x=12..14, far west of both clearings. The
|
|
65
|
+
placement is the point, and it took two tries to get right:
|
|
66
|
+
|
|
67
|
+
- **A gap between the start and the goal is not an obstacle.** With the gap at
|
|
68
|
+
x=29..31 the shortest route cost exactly the straight-line distance — 55 steps
|
|
69
|
+
against a 55-step Manhattan distance — so a pathfinder would have drawn what
|
|
70
|
+
looks like a straight line. Out west, the same trip is 84 steps against 22,
|
|
71
|
+
and the walker has to head *away* from its goal to get through.
|
|
72
|
+
- **A fence has to span the whole interior.** Stopping it a tile short of the
|
|
73
|
+
border left a second gap nobody planned, at x=58, and the route quietly used
|
|
74
|
+
that one instead. It now runs x=1..58, and the fence row is solid everywhere
|
|
75
|
+
except the three gap tiles.
|
|
76
|
+
|
|
77
|
+
### `ui.png` + `ui.json` — Kenney, *UI Pack - Pixel Adventure*
|
|
78
|
+
|
|
79
|
+
- Source: <https://kenney.nl/assets/ui-pack-pixel-adventure>
|
|
80
|
+
- Licence: CC0 1.0 (stated on the page and in the `License.txt` inside the
|
|
81
|
+
download)
|
|
82
|
+
- Modification: five 32x32 tiles were cut out of
|
|
83
|
+
`Tilesheets/Large tiles/Thick outline/tilemap_packed.png` and laid side by
|
|
84
|
+
side. The pixels are untouched; only the sheet is ours.
|
|
85
|
+
|
|
86
|
+
The pack is 91 tiles and we use five, so shipping the strip rather than the
|
|
87
|
+
sheet keeps this to 1 KB and makes the descriptor readable — each element is at
|
|
88
|
+
a round multiple of 32.
|
|
89
|
+
|
|
90
|
+
**This is not optional chrome.** `RGame::Engine::UI::PanelButton` draws its
|
|
91
|
+
background with `renderer.nine_slice`, and a nine-slice id is resolved by
|
|
92
|
+
*registration* only — it names an element of an atlas, never a file — so
|
|
93
|
+
`UI::Menu` cannot draw at all without one of these registered. Which elements
|
|
94
|
+
are needed is not our choice either: `PanelButton::STYLE` names `button_idle`,
|
|
95
|
+
`button_focus`, `button_pressed` and `button_disabled`, and a menu draws each of
|
|
96
|
+
them when an item reaches that state.
|
|
97
|
+
|
|
98
|
+
Two things constrained which tiles could be used, both discovered by looking:
|
|
99
|
+
|
|
100
|
+
- **Several of the pack's panels are frames with transparent middles.** They
|
|
101
|
+
read as solid panels on the sheet's dark background and then show the world
|
|
102
|
+
through them. The five here are all filled.
|
|
103
|
+
- **`PanelButton`'s label colour defaults to** a dark brown, with a muted grey
|
|
104
|
+
for a disabled item, so with the default the buttons have to be *light* or the
|
|
105
|
+
label disappears into them. `label_color:` changes it.
|
|
106
|
+
|
|
107
|
+
`border` is 8 for every element: the largest decorated frame in the set is 8
|
|
108
|
+
pixels, the interiors are flat, and a uniform value keeps a button's inner
|
|
109
|
+
geometry from shifting as it changes state.
|
|
110
|
+
|
|
111
|
+
### `hero.png` + `hero.json` — sodri's *Character 4 directional walking*, repacked
|
|
112
|
+
|
|
113
|
+
- Source: <https://opengameart.org/content/character-4-directional-walking>
|
|
114
|
+
- Author: sodri
|
|
115
|
+
- Licence: CC0
|
|
116
|
+
- Modifications, both ours: the white background was keyed out, and the four
|
|
117
|
+
separate strips were repacked into one uniform sheet.
|
|
118
|
+
|
|
119
|
+
The original is four PNGs — `walk_down`/`walk_up` at 14x22 per frame,
|
|
120
|
+
`walk_left`/`walk_right` at 13x22 — fully opaque, with a pure white background.
|
|
121
|
+
Two things had to change before `RGame::Core::SpriteSheet` could read it:
|
|
122
|
+
|
|
123
|
+
- **Transparency.** Pure white was replaced with alpha 0. Checked first rather
|
|
124
|
+
than assumed: flood-filling from the borders showed white is only ever
|
|
125
|
+
background — the handful of enclosed white pixels are the gaps between arm and
|
|
126
|
+
torso — and the art uses 7 to 9 colours with no near-white among them, so a
|
|
127
|
+
flat colour key cannot eat anything intended.
|
|
128
|
+
- **A uniform grid.** A sheet is one fixed cell size, so each frame was centred
|
|
129
|
+
horizontally in a 16x22 cell (`(16 - w) // 2`, so the 13px-wide side frames
|
|
130
|
+
sit one pixel left of true centre — half a pixel of asymmetry between facings,
|
|
131
|
+
which nothing can see).
|
|
132
|
+
|
|
133
|
+
`walk_left` is not in the sheet. Every left frame is a pixel-exact mirror of its
|
|
134
|
+
right counterpart — verified frame by frame, not assumed — so the descriptor
|
|
135
|
+
reuses row 2 with `"flip_x": true` and the sheet is three rows instead of four.
|
|
136
|
+
|
|
137
|
+
Layout, 6 columns x 3 rows of 16x22:
|
|
138
|
+
|
|
139
|
+
| Row | Animation |
|
|
140
|
+
|---|---|
|
|
141
|
+
| 0 | `walk_down`, and `stand` is its first column |
|
|
142
|
+
| 1 | `walk_up` |
|
|
143
|
+
| 2 | `walk_right`, and `walk_left` mirrored |
|
|
144
|
+
|
|
145
|
+
There is no idle art in the original, so `stand` is a single frame off the walk
|
|
146
|
+
cycle rather than an animation of its own.
|
|
147
|
+
|
|
148
|
+
### `glyphs.png` + `glyphs.json` — Kenney, *Input Prompts*, repacked
|
|
149
|
+
|
|
150
|
+
- Source: <https://kenney.nl/assets/input-prompts>
|
|
151
|
+
- Licence: CC0 1.0 (stated on the page and in the `License.txt` inside the
|
|
152
|
+
download)
|
|
153
|
+
- Modification: five 64x64 PNGs were laid side by side into one strip. The
|
|
154
|
+
pixels are untouched; only the sheet and the descriptor are ours.
|
|
155
|
+
|
|
156
|
+
The pack is thousands of files across seventeen controller families, and
|
|
157
|
+
`examples/input_glyphs` names five buttons, so the strip is 3 KB where the
|
|
158
|
+
download is 5 MB.
|
|
159
|
+
|
|
160
|
+
Layout, 5 columns x 1 row of 64x64, and **the order is the example's table**:
|
|
161
|
+
`GLYPH_COLUMN` in `examples/input_glyphs/main.rb` maps a `Controls` button id to
|
|
162
|
+
a column here.
|
|
163
|
+
|
|
164
|
+
| Column | Button id | From the pack |
|
|
165
|
+
|---|---|---|
|
|
166
|
+
| 0 | `KEY_SPACE` | `Keyboard & Mouse/Default/keyboard_space.png` |
|
|
167
|
+
| 1 | `KEY_RETURN` | `Keyboard & Mouse/Default/keyboard_enter.png` |
|
|
168
|
+
| 2 | `KEY_ESCAPE` | `Keyboard & Mouse/Default/keyboard_escape.png` |
|
|
169
|
+
| 3 | `PAD_A` | `Xbox Series/Default/xbox_button_color_a.png` |
|
|
170
|
+
| 4 | `PAD_B` | `Xbox Series/Default/xbox_button_color_b.png` |
|
|
171
|
+
|
|
172
|
+
**Xbox rather than PlayStation or Switch**, because SDL's button names are
|
|
173
|
+
Xbox's — `PAD_A` is SDL's A — and a sheet whose faces disagree with the ids
|
|
174
|
+
would make every prompt a translation. A game that wants the pad in the player's
|
|
175
|
+
actual hands ships a second sheet per family and picks a table; the ids do not
|
|
176
|
+
change, only the pictures.
|
|
177
|
+
|
|
178
|
+
The keyboard glyphs are light and the pad glyphs are the pack's colour versions,
|
|
179
|
+
so both need a dark panel under them — which is what `examples/input_glyphs`
|
|
180
|
+
draws.
|
|
181
|
+
|
|
182
|
+
### `icons.png` + `icons.json` — Kenney, *Game Icons*, repacked
|
|
183
|
+
|
|
184
|
+
- Source: <https://kenney.nl/assets/game-icons>, mirrored with the same licence
|
|
185
|
+
at <https://opengameart.org/content/game-icons>
|
|
186
|
+
- Licence: CC0 1.0 (stated on the page and in the `license.txt` inside the
|
|
187
|
+
download)
|
|
188
|
+
- Modification: eight 50x50 PNGs were laid side by side into one strip. The
|
|
189
|
+
pixels are untouched; only the sheet and the descriptor are ours.
|
|
190
|
+
|
|
191
|
+
Layout, 8 columns x 1 row of 50x50, named in the descriptor's `images` section
|
|
192
|
+
in `snake_case`:
|
|
193
|
+
|
|
194
|
+
| Column | Name | From the pack |
|
|
195
|
+
|---|---|---|
|
|
196
|
+
| 0 | `home` | `PNG/White/1x/home.png` |
|
|
197
|
+
| 1 | `gear` | `PNG/White/1x/gear.png` |
|
|
198
|
+
| 2 | `save` | `PNG/White/1x/save.png` |
|
|
199
|
+
| 3 | `star` | `PNG/White/1x/star.png` |
|
|
200
|
+
| 4 | `trophy` | `PNG/White/1x/trophy.png` |
|
|
201
|
+
| 5 | `audio_on` | `PNG/White/1x/audioOn.png` |
|
|
202
|
+
| 6 | `music_on` | `PNG/White/1x/musicOn.png` |
|
|
203
|
+
| 7 | `locked` | `PNG/White/1x/locked.png` |
|
|
204
|
+
|
|
205
|
+
**White, because a tint is a multiply.** The white variant is RGB 255 with the
|
|
206
|
+
shape in alpha only, so `renderer.image(..., color:)` — and `UI::IconButton`'s
|
|
207
|
+
per-state tints — colour it exactly; the black variant would stay black under
|
|
208
|
+
any tint.
|
|
209
|
+
|
|
210
|
+
**1x, because images sample nearest-neighbour.** A 50-pixel icon in
|
|
211
|
+
`examples/radial_menu`'s 64-pixel slots draws at scale 1 and the chosen one in
|
|
212
|
+
the middle at scale 2, both whole numbers; a 2x icon would need scale 0.5 or
|
|
213
|
+
thereabouts, which drops pixel rows unevenly.
|
|
214
|
+
|
|
215
|
+
**A strip, because the separate files carry metadata.** Each PNG in the pack
|
|
216
|
+
holds Adobe XMP, so the eight files are 121 KB; the strip is 2 KB. It is a UI
|
|
217
|
+
atlas rather than a sprite sheet so the icons are cut and registered by name —
|
|
218
|
+
`renderer.register_ui_atlas(game.assets.ui_atlas('icons.json'))` makes
|
|
219
|
+
`UI::IconButton.new(image: :home)` draw.
|
|
220
|
+
|
|
221
|
+
### `skills.png` + `skills.json` — Kenney, *Cursor Pack*, repacked
|
|
222
|
+
|
|
223
|
+
- Source: <https://kenney.nl/assets/cursor-pack>, version 1.1
|
|
224
|
+
- Licence: CC0 1.0 (the `License.txt` inside the download reads "License:
|
|
225
|
+
(Creative Commons Zero, CC0)")
|
|
226
|
+
- Modification: five 64x64 PNGs were laid side by side into one strip and saved
|
|
227
|
+
as RGBA. The pixels are untouched; only the sheet and the descriptor are ours.
|
|
228
|
+
|
|
229
|
+
Layout, 5 columns x 1 row of 64x64, named in the descriptor's `images` section
|
|
230
|
+
in `snake_case`:
|
|
231
|
+
|
|
232
|
+
| Column | Name | From the pack |
|
|
233
|
+
|---|---|---|
|
|
234
|
+
| 0 | `wand` | `PNG/Basic/Double/tool_wand.png` |
|
|
235
|
+
| 1 | `wrench` | `PNG/Basic/Double/tool_wrench.png` |
|
|
236
|
+
| 2 | `torch` | `PNG/Basic/Double/tool_torch.png` |
|
|
237
|
+
| 3 | `hammer` | `PNG/Basic/Double/tool_hammer.png` |
|
|
238
|
+
| 4 | `watering_can` | `PNG/Basic/Double/tool_watering_can.png` |
|
|
239
|
+
|
|
240
|
+
`torch` is the pack's flashlight, named for the British word. The pack has
|
|
241
|
+
`tool_hoe`, `tool_shovel`, `tool_axe` and `tool_pickaxe` in the same styles, if
|
|
242
|
+
the bar ever wants more.
|
|
243
|
+
|
|
244
|
+
**`Basic`, not `Outline`.** Both were drawn on `examples/skill_bar`'s discs in
|
|
245
|
+
all four states. `Basic` is a light-grey silhouette with no border, so a tint
|
|
246
|
+
colours all of it: grey at rest, white focused, dim disabled, dark on the gold
|
|
247
|
+
pressed disc. `Outline` adds a black border, which a multiply leaves black —
|
|
248
|
+
a disabled tool barely dims, and a pressed one is a dark shape on gold with its
|
|
249
|
+
edges lost.
|
|
250
|
+
|
|
251
|
+
**`Double` (64 pixels), not `Default` (32).** The glyph sits in about half its
|
|
252
|
+
square, so a 32-pixel cursor on an 80-pixel disc is a thumbnail. Either draws at
|
|
253
|
+
scale 1, which nearest-neighbour sampling needs.
|
|
254
|
+
|
|
255
|
+
**One pack per atlas.** These are not in `icons.png` so that each file's
|
|
256
|
+
provenance stays one paragraph; one more `register_ui_atlas` call is the whole
|
|
257
|
+
cost.
|
|
258
|
+
|
|
259
|
+
### `blip.ogg` — Kenney, *Interface Sounds*
|
|
260
|
+
|
|
261
|
+
- Source: <https://kenney.nl/assets/interface-sounds>
|
|
262
|
+
- Licence: CC0 1.0 (stated on the page and in the `License.txt` inside the
|
|
263
|
+
download)
|
|
264
|
+
- Modification: none. This is `Audio/click_001.ogg` from the pack, renamed.
|
|
265
|
+
|
|
266
|
+
4.8 KB. The pack has 100 of these, 4 to 29 KB each, so if an example ever wants
|
|
267
|
+
a second sound it costs nothing to take another.
|
|
268
|
+
|
|
269
|
+
### `music.ogg` — hernandack's *Short Loops Background Music Pack*, re-encoded
|
|
270
|
+
|
|
271
|
+
- Source: <https://opengameart.org/content/short-loops-background-music-pack>
|
|
272
|
+
- Author: hernandack
|
|
273
|
+
- Licence: CC0
|
|
274
|
+
- Modification: the track *Just Saying Tho* downmixed to mono and re-encoded at
|
|
275
|
+
Vorbis quality -0.1, with `tools/shrink_ogg.c`. **The length is unchanged.**
|
|
276
|
+
|
|
277
|
+
24.05 seconds, 93 KB, down from 388 KB. The original is stereo, 44.1 kHz,
|
|
278
|
+
~128 kbps — encoded for listening rather than for a library gem — and mono at a
|
|
279
|
+
low quality setting is background music in a teaching example.
|
|
280
|
+
|
|
281
|
+
**Length was deliberately not touched.** A seamless loop is seamless at exactly
|
|
282
|
+
its own length, because the author arranged for the end to lead back into the
|
|
283
|
+
start. Trimming it to save more bytes would put a seam in the middle of the one
|
|
284
|
+
property the file is shipped to demonstrate.
|
|
285
|
+
|
|
286
|
+
#### Why this is the second track here
|
|
287
|
+
|
|
288
|
+
The first one shipped was *A Brand New Wisdom* from the same pack, chosen on a
|
|
289
|
+
seam measurement of 2.6% — and it loops audibly badly, because **it ends with
|
|
290
|
+
0.79 seconds of silence.** The wrap has no click. It has a *gap*.
|
|
291
|
+
|
|
292
|
+
That is a hole in the measurement, not bad luck. A seam figure asks "does the
|
|
293
|
+
last sample join smoothly onto the first", and silence joins onto silence
|
|
294
|
+
perfectly. Fading out to nothing is the ordinary way to end a piece of music and
|
|
295
|
+
the ordinary way to ruin a loop, and the number said 0.7% the whole time.
|
|
296
|
+
|
|
297
|
+
`tools/shrink_ogg.c` now reports both, and the whole pack looks different under
|
|
298
|
+
the second one:
|
|
299
|
+
|
|
300
|
+
| track | seam | tail silence | |
|
|
301
|
+
|---|---|---|---|
|
|
302
|
+
| A Brand New Wisdom | 2.6% | **0.79s** | shipped first, wrong |
|
|
303
|
+
| Swinging Sweet | 4.5% | **1.48s** | worse |
|
|
304
|
+
| Winter Dust | 34.9% | 0.00s | no gap, bad seam |
|
|
305
|
+
| **Just Saying Tho** | 2.8% | **0.00s** | shipped now |
|
|
306
|
+
| 8BitBattleLoop (other pack) | 0.0% | 0.01s | cleanest, but 108 KB and chiptune |
|
|
307
|
+
|
|
308
|
+
Measured back off the shipped file: seam 2.3%, no silence at either end.
|
|
309
|
+
|
|
310
|
+
**A loop wants both numbers.** A small seam alone is not evidence.
|
|
311
|
+
|
|
312
|
+
## Adding an asset here
|
|
313
|
+
|
|
314
|
+
1. CC0, or drawn in this repo. If the licence says anything about redistribution
|
|
315
|
+
at all, it does not go here — see the top of this file.
|
|
316
|
+
2. Record it above: source URL, author, licence, and every modification made.
|
|
317
|
+
3. **Audio is Ogg Vorbis or WAV, and nothing else.** MP3 and FLAC are compiled
|
|
318
|
+
out of miniaudio (`ext/rgame_core/vendor/miniaudio_impl.c`), so a file that
|
|
319
|
+
plays in every desktop player can still fail to load here.
|
|
320
|
+
4. No leading dots in filenames. `Dir.glob` does not match them, so the gemspec
|
|
321
|
+
would silently leave the file out of the gem while the checkout kept working
|
|
322
|
+
— `spec/packaging_spec.rb` has an example that catches it.
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"image": "hero.png",
|
|
3
|
+
"frame_width": 16,
|
|
4
|
+
"frame_height": 22,
|
|
5
|
+
"animations": {
|
|
6
|
+
"stand": { "row": 0, "col": 0, "frames": 1, "fps": 1 },
|
|
7
|
+
"walk_down": { "row": 0, "frames": 6, "fps": 8 },
|
|
8
|
+
"walk_up": { "row": 1, "frames": 6, "fps": 8 },
|
|
9
|
+
"walk_right": { "row": 2, "frames": 6, "fps": 8 },
|
|
10
|
+
"walk_left": { "row": 2, "frames": 6, "fps": 8, "flip_x": true }
|
|
11
|
+
}
|
|
12
|
+
}
|
|
Binary file
|