rgame 0.2.0 → 0.3.1
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 +144 -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 +3 -5
- 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 +167 -12
- 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/assets.md
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Assets: sheets, atlases, maps and sounds
|
|
2
2
|
|
|
3
|
-
The
|
|
4
|
-
|
|
5
|
-
the
|
|
3
|
+
**The asset manager loads and caches every file a game uses**: images, sound
|
|
4
|
+
samples, songs, sprite sheets, UI atlases, tile maps and plain text. This page
|
|
5
|
+
covers the manager and the Ruby classes it builds from those files: a sprite sheet
|
|
6
|
+
sliced into frames, a nine-slice panel stretched to any size, a UI atlas and a
|
|
7
|
+
tile map. [Images](images.md) and [Audio](audio.md) cover the C-backed `Image`,
|
|
8
|
+
`Sample` and `Song`.
|
|
6
9
|
|
|
7
|
-
|
|
8
|
-
|
|
10
|
+
**These classes are pure Ruby, but live in `RGame::Core` because they hold GPU or
|
|
11
|
+
audio handles.** Game logic names assets by id and never holds one; see
|
|
9
12
|
[Testing what a scene draws](drawing.md#testing-what-a-scene-draws).
|
|
10
13
|
|
|
11
14
|
| Page section | Class |
|
|
@@ -16,25 +19,40 @@ an image is a GPU handle. Game logic names them by id and never holds one — se
|
|
|
16
19
|
| [UI atlases](#ui-atlases) | `RGame::Core::UiAtlas` |
|
|
17
20
|
| [Tile maps](#tile-maps) | `RGame::Core::TileMapRenderer` |
|
|
18
21
|
|
|
19
|
-
*This page grows as the rest lands.*
|
|
20
|
-
|
|
21
22
|
## The asset manager
|
|
22
23
|
|
|
23
|
-
The
|
|
24
|
-
|
|
24
|
+
**The asset manager loads and caches every file-backed asset.** Every game has
|
|
25
|
+
one, and none builds it. `app.assets` does, rooted at the app's `media_root:`:
|
|
25
26
|
|
|
26
27
|
```ruby
|
|
27
28
|
app.assets.image('space.png') # => RGame::Core::Image
|
|
28
|
-
app.assets.sound('
|
|
29
|
-
app.assets.song('
|
|
30
|
-
app.assets.sheet('
|
|
29
|
+
app.assets.sound('sounds/boom.ogg') # => RGame::Core::Sample
|
|
30
|
+
app.assets.song('music/theme.ogg') # => RGame::Core::Song
|
|
31
|
+
app.assets.sheet('sheets/player.json') # => RGame::Core::SpriteSheet
|
|
31
32
|
app.assets.ui_atlas('ui/ui_atlas.json') # => RGame::Core::UiAtlas
|
|
32
33
|
app.assets.read('data/levels.txt') # => String
|
|
33
34
|
```
|
|
34
35
|
|
|
35
|
-
Paths are relative to the media root; an absolute
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
Paths are relative to the media root; an absolute path is used as given. Every
|
|
37
|
+
accessor returns the same object each time. A file requested twice is read,
|
|
38
|
+
decoded and uploaded once. Several spellings of one file share one cache entry:
|
|
39
|
+
`'a/b.png'`, `'a/./b.png'` and the absolute form.
|
|
40
|
+
|
|
41
|
+
The manager gives a game one object that knows what is loaded. Setup code builds
|
|
42
|
+
no paths by hand and constructs no images inline.
|
|
43
|
+
|
|
44
|
+
### Listing what is there
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
app.assets.glob('locales/**/*.yml') # => ["locales/de.yml", "locales/en.yml"]
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`glob(pattern)` returns the paths under the media root that match `pattern`,
|
|
51
|
+
relative to the root and sorted. The order is therefore the same on every
|
|
52
|
+
platform, whatever order the file system lists them in. A directory that does not
|
|
53
|
+
exist matches nothing and returns `[]`. An absolute pattern is used as given, and
|
|
54
|
+
its matches come back absolute. `glob` loads nothing and caches nothing; hand
|
|
55
|
+
each path to an accessor to load it.
|
|
38
56
|
|
|
39
57
|
### Adding an asset type
|
|
40
58
|
|
|
@@ -43,21 +61,16 @@ app.assets.add_loader(:level) { |path| MyLevel.parse(File.read(path)) }
|
|
|
43
61
|
app.assets.level('levels/one.json') # cached and grouped like any other
|
|
44
62
|
```
|
|
45
63
|
|
|
46
|
-
The built-in types
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
Every path is **relative to the media root**, and every accessor returns the
|
|
51
|
-
same object each time it is asked — so a file wanted twice is read, decoded and
|
|
52
|
-
uploaded once. That is the point: loading stops being scattered across a game's
|
|
53
|
-
setup, building paths ad hoc and constructing images inline, and becomes one
|
|
54
|
-
object that knows what is loaded.
|
|
64
|
+
The built-in leaf types (`image`, `sound`, `song` and `read`) register through the
|
|
65
|
+
same method at construction, so an added type works exactly like them. `sheet` and
|
|
66
|
+
`ui_atlas` are composites built from those; see below. `add_loader` exists because `RGame::Core`
|
|
67
|
+
cannot build some types itself; see [Tile maps](#tile-maps).
|
|
55
68
|
|
|
56
69
|
### Groups, and what `release` frees
|
|
57
70
|
|
|
58
|
-
Each cached asset remembers the
|
|
59
|
-
ungrouped load belongs to a permanent
|
|
60
|
-
grouped
|
|
71
|
+
**Each cached asset remembers the set of groups that asked for it.** An
|
|
72
|
+
ungrouped load belongs to a permanent group and survives every `release`. A
|
|
73
|
+
grouped load is reference counted.
|
|
61
74
|
|
|
62
75
|
```ruby
|
|
63
76
|
app.assets.image('ui/buttons.png') # ungrouped: permanent
|
|
@@ -70,49 +83,47 @@ app.assets.release(:level1) # drops lvl1/* unless another group still holds it
|
|
|
70
83
|
app.assets.clear # drops everything, permanent included
|
|
71
84
|
```
|
|
72
85
|
|
|
73
|
-
An asset two levels
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
silently loses the second group's claim.
|
|
86
|
+
An asset that two levels loaded stays until **both** release it. Two scenes can
|
|
87
|
+
therefore share a texture safely. A cache *hit* under a new group also adds that
|
|
88
|
+
group. Otherwise the second group's claim would be lost without a trace.
|
|
77
89
|
|
|
78
|
-
Releasing drops this cache's reference.
|
|
79
|
-
|
|
80
|
-
watch it happen.
|
|
90
|
+
Releasing drops this cache's reference. The garbage collector decides when the
|
|
91
|
+
GPU texture goes. Watch it with `Image.debug_live_textures`.
|
|
81
92
|
|
|
82
|
-
`release` refuses the permanent
|
|
83
|
-
|
|
84
|
-
`clear`.
|
|
93
|
+
`release` refuses the permanent group. Releasing it would drop every ungrouped
|
|
94
|
+
asset, the opposite of "permanent". Use `clear` instead.
|
|
85
95
|
|
|
86
96
|
### Composites share their parts
|
|
87
97
|
|
|
88
|
-
A sprite sheet is a descriptor plus an image, and
|
|
89
|
-
|
|
98
|
+
**A sprite sheet is a descriptor plus an image, and the manager loads both
|
|
99
|
+
through its own cache.** These two calls therefore share one upload:
|
|
90
100
|
|
|
91
101
|
```ruby
|
|
92
102
|
sheet = app.assets.sheet('sheets/hero.json') # names hero.png inside
|
|
93
103
|
image = app.assets.image('sheets/hero.png') # the same texture, not a second one
|
|
94
104
|
```
|
|
95
105
|
|
|
96
|
-
The descriptor's image
|
|
97
|
-
it
|
|
98
|
-
|
|
106
|
+
The manager resolves the descriptor's image *next to the descriptor*. That gives
|
|
107
|
+
it the same cache key a standalone load would use. Releasing the sheet's group
|
|
108
|
+
releases its PNG too.
|
|
99
109
|
|
|
100
|
-
**One known gap.** A composite tags its parts with the group that first
|
|
101
|
-
it.
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
owns its assets
|
|
110
|
+
**One known gap remains.** A composite tags its parts with the group that first
|
|
111
|
+
built it. When a *second* group requests the cached composite, only the
|
|
112
|
+
composite's own key gains the new tag, not its parts. Releasing the first group
|
|
113
|
+
can then drop a PNG the second group still expects. The usual pattern, where each
|
|
114
|
+
level owns its assets, is unaffected. Closing the gap would need per-part
|
|
115
|
+
tracking.
|
|
105
116
|
|
|
106
117
|
### Failure
|
|
107
118
|
|
|
108
|
-
A loader's own error
|
|
109
|
-
`Sample::LoadError`, `Errno::ENOENT
|
|
110
|
-
|
|
111
|
-
|
|
119
|
+
A loader's own error passes through unchanged and names the file:
|
|
120
|
+
`Image::LoadError`, `Sample::LoadError`, `Errno::ENOENT`. **A failed load leaves
|
|
121
|
+
nothing behind**: no cache entry and no group tag. A retry starts clean, with no
|
|
122
|
+
half-registered asset that can never be released.
|
|
112
123
|
|
|
113
124
|
### Testing without files
|
|
114
125
|
|
|
115
|
-
Every asset type maps to a loader proc, and
|
|
126
|
+
Every asset type maps to a loader proc, and you can inject your own:
|
|
116
127
|
|
|
117
128
|
```ruby
|
|
118
129
|
assets = RGame::Core::AssetManager.new(
|
|
@@ -121,18 +132,17 @@ assets = RGame::Core::AssetManager.new(
|
|
|
121
132
|
)
|
|
122
133
|
```
|
|
123
134
|
|
|
124
|
-
The
|
|
125
|
-
time.
|
|
126
|
-
|
|
127
|
-
and no files at all.
|
|
135
|
+
The default loaders name `Image` and `Audio` only *inside* their bodies, never at
|
|
136
|
+
load time. Specs can therefore cover all the manager's logic with no window, no GL
|
|
137
|
+
context and no files: caching, path resolution and grouping.
|
|
128
138
|
|
|
129
139
|
## Sprite sheets
|
|
130
140
|
|
|
131
|
-
A sheet is one image plus a JSON descriptor
|
|
132
|
-
|
|
141
|
+
A sheet is one image plus a JSON descriptor. It slices the image into frames at
|
|
142
|
+
load time and draws one frame at a time.
|
|
133
143
|
|
|
134
144
|
```ruby
|
|
135
|
-
sheet =
|
|
145
|
+
sheet = app.assets.sheet('hero.json')
|
|
136
146
|
|
|
137
147
|
sheet.frame_width # => 16
|
|
138
148
|
sheet.grid # => [rows, columns]
|
|
@@ -159,14 +169,14 @@ sheet.draw(renderer, row, col, x, y, flip_x: false, z: 0)
|
|
|
159
169
|
}
|
|
160
170
|
```
|
|
161
171
|
|
|
162
|
-
`image`
|
|
163
|
-
|
|
164
|
-
|
|
172
|
+
The sheet resolves `image` **next to the descriptor**, so you can move both files
|
|
173
|
+
together without editing either. Only `frame_width` and `frame_height` are
|
|
174
|
+
required. A descriptor missing one raises `ArgumentError` naming the key.
|
|
165
175
|
|
|
166
176
|
### A frame can be smaller than its cell
|
|
167
177
|
|
|
168
|
-
Cells sit on a fixed `cell_width` x `cell_height` grid.
|
|
169
|
-
`frame_width` x `frame_height` rectangle offset by `origin_x` / `origin_y`
|
|
178
|
+
Cells sit on a fixed `cell_width` x `cell_height` grid. The sheet *draws* a
|
|
179
|
+
`frame_width` x `frame_height` rectangle, offset by `origin_x` / `origin_y`
|
|
170
180
|
inside its cell:
|
|
171
181
|
|
|
172
182
|
```
|
|
@@ -178,58 +188,58 @@ cell (32x32) frame (16x24) at origin (8, 4)
|
|
|
178
188
|
└──────────────┘ └────┴────┴────┘
|
|
179
189
|
```
|
|
180
190
|
|
|
181
|
-
|
|
182
|
-
a
|
|
183
|
-
|
|
184
|
-
frame == cell, which is what a simple sheet wants.
|
|
191
|
+
Cells can fit the widest pose, such as an attack swing, while walking frames keep
|
|
192
|
+
a tight, centred box. A character then keeps its apparent size when its animation
|
|
193
|
+
changes. Without the four keys, frame equals cell, which suits a simple sheet.
|
|
185
194
|
|
|
186
|
-
Only whole cells count
|
|
187
|
-
|
|
188
|
-
fifth.
|
|
195
|
+
Only whole cells count. A 70-pixel sheet with 16-pixel cells has four columns.
|
|
196
|
+
The sheet ignores the six leftover pixels instead of making a narrow fifth column.
|
|
189
197
|
|
|
190
198
|
### Facing
|
|
191
199
|
|
|
192
|
-
|
|
193
|
-
|
|
200
|
+
**`flip_x` mirrors the frame inside the same rectangle**, so a character covers
|
|
201
|
+
the same pixels whichever way it faces:
|
|
194
202
|
|
|
195
203
|
```ruby
|
|
196
204
|
sheet.draw(renderer, row, col, x, y, flip_x: moving_left)
|
|
197
205
|
```
|
|
198
206
|
|
|
199
|
-
|
|
200
|
-
[Mirroring](drawing.md#mirroring) for why, if you are coming from Gosu.
|
|
207
|
+
You add no width back; [Mirroring](drawing.md#mirroring) explains why.
|
|
201
208
|
|
|
202
|
-
### Animations
|
|
209
|
+
### Animations come back raw
|
|
203
210
|
|
|
204
|
-
|
|
205
|
-
about time
|
|
206
|
-
|
|
207
|
-
|
|
211
|
+
**`#animations` returns the descriptor's table untouched.** The sheet knows
|
|
212
|
+
nothing about time. The scene layer decides which frame to show, and builds its
|
|
213
|
+
own animation state from that hash. The raw form lets each side change on its
|
|
214
|
+
own.
|
|
208
215
|
|
|
209
|
-
A sheet
|
|
210
|
-
is a
|
|
216
|
+
A sheet without an `animations` key returns `{}`, not `nil`. A sheet of static
|
|
217
|
+
tiles is a valid sheet, and callers should not have to branch.
|
|
211
218
|
|
|
212
219
|
### Slicing costs nothing
|
|
213
220
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
`#draw` is an array index plus one draw call. Nothing is
|
|
221
|
+
The sheet cuts every frame once, at construction, as a view onto the single
|
|
222
|
+
upload. Two hundred frames are two hundred small objects and **one** texture.
|
|
223
|
+
`#draw` is an array index plus one draw call. Nothing is cut again per frame.
|
|
217
224
|
|
|
218
225
|
### Loading
|
|
219
226
|
|
|
220
227
|
```ruby
|
|
221
|
-
|
|
222
|
-
RGame::Core::SpriteSheet.new(image, atlas) # from an already-loaded image
|
|
228
|
+
app.assets.sheet(path) # what a game calls: cached and grouped
|
|
229
|
+
RGame::Core::SpriteSheet.new(image, atlas) # from an already-loaded image and parsed descriptor
|
|
230
|
+
RGame::Core::SpriteSheet.load(app, path) # reads both files directly, bypassing the cache
|
|
223
231
|
```
|
|
224
232
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
PNG is shared with
|
|
233
|
+
**Load sheets through `app.assets.sheet`.** Every app has an asset manager. It
|
|
234
|
+
builds the sheet with `.new`, from an image it pulls through its own cache, so the
|
|
235
|
+
sheet's PNG is shared with `app.assets.image` of the same file. `.load` reads the
|
|
236
|
+
descriptor and decodes the image itself, outside any cache. A second `.load` of
|
|
237
|
+
the same file decodes and uploads it again. `UiAtlas` has the same three forms.
|
|
228
238
|
|
|
229
239
|
## Nine-slices
|
|
230
240
|
|
|
231
|
-
A bordered texture
|
|
232
|
-
|
|
241
|
+
A nine-slice draws a bordered texture at any size. It cuts the texture into nine
|
|
242
|
+
pieces and treats each piece differently.
|
|
233
243
|
|
|
234
244
|
```ruby
|
|
235
245
|
panel = RGame::Core::NineSlice.new(image, x: 0, y: 0, w: 26, h: 28,
|
|
@@ -248,37 +258,36 @@ panel.draw(renderer, x, y, width, height, z: 0, color: nil)
|
|
|
248
258
|
└──┴────────┴──┘
|
|
249
259
|
```
|
|
250
260
|
|
|
251
|
-
One small piece of art fills a button, a dialog or a health bar of any size,
|
|
252
|
-
|
|
261
|
+
One small piece of art fills a button, a dialog or a health bar of any size, and
|
|
262
|
+
the corners never smear.
|
|
253
263
|
|
|
254
|
-
`(x, y, w, h)` is the source rectangle **inside** the image, so one sheet can
|
|
255
|
-
|
|
264
|
+
`(x, y, w, h)` is the source rectangle **inside** the image, so one sheet can hold
|
|
265
|
+
many nine-slices. A [UI atlas](#ui-atlases) relies on this.
|
|
256
266
|
|
|
257
267
|
### Tiled, not stretched
|
|
258
268
|
|
|
259
|
-
Edges and the centre
|
|
260
|
-
the
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
is more visible than an overhang that gets cropped.
|
|
269
|
+
**Edges and the centre repeat.** Stretching a 7-pixel motif would blur the detail
|
|
270
|
+
the art was drawn for. Repeating it keeps pixel art crisp at every size. Each
|
|
271
|
+
band clips to itself, so the last tile in a row is cropped cleanly instead of
|
|
272
|
+
spilling into the corner. The loops always start one extra tile, because a gap at
|
|
273
|
+
the seam shows more than a cropped overhang.
|
|
265
274
|
|
|
266
275
|
### `border` and `scale`
|
|
267
276
|
|
|
268
|
-
`border`
|
|
277
|
+
`border` takes a uniform integer or a hash:
|
|
269
278
|
|
|
270
279
|
```ruby
|
|
271
280
|
border: 7
|
|
272
281
|
border: { left: 2, right: 6, top: 4, bottom: 4 }
|
|
273
282
|
```
|
|
274
283
|
|
|
275
|
-
|
|
276
|
-
small
|
|
277
|
-
on a 640x480 screen
|
|
278
|
-
|
|
284
|
+
**`scale` is an integer pixel scale for the border art itself.** Source art is
|
|
285
|
+
small, with corners often 7 pixels wide. A scale of 2 or 3 makes borders legible
|
|
286
|
+
on a 640x480 screen without blur, because each source pixel becomes a whole
|
|
287
|
+
square of screen pixels. `scale` multiplies both the pieces and the step between
|
|
279
288
|
tiles, so the tiling stays seamless.
|
|
280
289
|
|
|
281
|
-
### Edge cases
|
|
290
|
+
### Edge cases
|
|
282
291
|
|
|
283
292
|
| | |
|
|
284
293
|
|---|---|
|
|
@@ -289,24 +298,29 @@ tiles, so the tiling stays seamless.
|
|
|
289
298
|
|
|
290
299
|
### What it costs
|
|
291
300
|
|
|
292
|
-
The nine
|
|
293
|
-
`#draw` allocates nothing. It issues one call per tile
|
|
294
|
-
`scale`
|
|
295
|
-
|
|
301
|
+
The nine-slice cuts its pieces once at construction, as views onto the one
|
|
302
|
+
upload, so `#draw` allocates nothing. It issues one call per tile. That is where
|
|
303
|
+
`scale` pays: a panel at 3x needs a ninth of the tiles of the same panel at 1x.
|
|
304
|
+
|
|
305
|
+
Inside a scene, draw a registered nine-slice by id with
|
|
306
|
+
`renderer.nine_slice(id, x, y, width, height, z: 0, tint: nil)`. Its `tint:` is
|
|
307
|
+
`NineSlice#draw`'s `color:`.
|
|
296
308
|
|
|
297
309
|
## UI atlases
|
|
298
310
|
|
|
299
|
-
|
|
311
|
+
A UI atlas cuts one sheet of UI art into named [nine-slices](#nine-slices) and
|
|
312
|
+
named images.
|
|
300
313
|
|
|
301
314
|
```ruby
|
|
302
315
|
atlas = app.assets.ui_atlas('ui/ui_atlas.json')
|
|
303
316
|
renderer.register_ui_atlas(atlas)
|
|
304
317
|
|
|
305
318
|
renderer.nine_slice(:button_idle, x, y, width, height)
|
|
319
|
+
renderer.image(:home, cx, cy)
|
|
306
320
|
```
|
|
307
321
|
|
|
308
|
-
A button has four states, a panel
|
|
309
|
-
|
|
322
|
+
A button has four states, a panel one, a scrollbar three pieces. All are small,
|
|
323
|
+
and sub-rectangles of one texture cost less than a dozen files.
|
|
310
324
|
|
|
311
325
|
### The descriptor
|
|
312
326
|
|
|
@@ -319,94 +333,117 @@ small, and all cheaper as sub-rectangles of one texture than as a dozen files.
|
|
|
319
333
|
"button_focus": { "x": 43, "y": 59, "w": 26, "h": 28, "border": 7 },
|
|
320
334
|
"panel": { "x": 0, "y": 0, "w": 32, "h": 32, "scale": 2,
|
|
321
335
|
"border": { "left": 4, "right": 4, "top": 8, "bottom": 4 } }
|
|
336
|
+
},
|
|
337
|
+
"images": {
|
|
338
|
+
"home": { "x": 0, "y": 96, "w": 50, "h": 50 },
|
|
339
|
+
"gear": { "x": 50, "y": 96, "w": 50, "h": 50 }
|
|
322
340
|
}
|
|
323
341
|
}
|
|
324
342
|
```
|
|
325
343
|
|
|
326
|
-
`image`
|
|
327
|
-
|
|
328
|
-
`scale`
|
|
344
|
+
The atlas resolves `image` next to the descriptor. Each `nine_slices` entry holds
|
|
345
|
+
a source rectangle and a `border`: a uniform integer or one value per side. An
|
|
346
|
+
optional `scale` overrides the sheet-wide one. A sheet without `scale` draws at 1.
|
|
347
|
+
|
|
348
|
+
Each `images` entry is a bare rectangle, cut from the sheet with
|
|
349
|
+
`Image#subimage`. An icon draws whole, so it needs no border. The draw call's
|
|
350
|
+
`scale:` sets its size, so it needs no scale either. `atlas.images` is a Hash of
|
|
351
|
+
name to `Image`. Either section may be missing or `null`, and one atlas may hold
|
|
352
|
+
both.
|
|
329
353
|
|
|
330
354
|
### Element names, not filenames
|
|
331
355
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
356
|
+
**Both sections are keyed by element name, and widgets ask for those names.** So
|
|
357
|
+
the renderer resolves nine-slices **by registration only**: `:button_focus` is
|
|
358
|
+
not a file and never can be. `register_ui_atlas` binds every element of both
|
|
359
|
+
kinds in one call. It registers nine-slices with `register_nine_slice` and images
|
|
360
|
+
with `register_image`:
|
|
336
361
|
|
|
337
362
|
```ruby
|
|
338
|
-
renderer.register_ui_atlas(atlas)
|
|
363
|
+
renderer.register_ui_atlas(atlas) # all of them
|
|
339
364
|
renderer.register_nine_slice(:panel, atlas.nine_slices[:panel]) # or one
|
|
365
|
+
renderer.register_image(:home, atlas.images[:home])
|
|
340
366
|
```
|
|
341
367
|
|
|
342
368
|
### When an entry is wrong
|
|
343
369
|
|
|
344
|
-
A
|
|
370
|
+
**A broken entry names itself.** A descriptor holds a dozen elements. A nine-slice
|
|
371
|
+
whose border does not fit, or an image rectangle past the sheet's edge, raises
|
|
372
|
+
with the element's name:
|
|
345
373
|
|
|
346
374
|
```
|
|
347
375
|
ArgumentError: ui atlas element :button_idle: nine-slice borders (40, 40, 40, 40)
|
|
348
376
|
do not fit in a 26x28 rect
|
|
349
377
|
```
|
|
350
378
|
|
|
351
|
-
Without the
|
|
352
|
-
|
|
379
|
+
Without the name, the error would be bare arithmetic from inside `NineSlice`.
|
|
380
|
+
Finding the culprit would mean bisecting the JSON by hand.
|
|
353
381
|
|
|
354
|
-
|
|
382
|
+
The atlas parses once, at load, and touches nothing again per frame.
|
|
355
383
|
|
|
356
384
|
## Tile maps
|
|
357
385
|
|
|
358
|
-
|
|
359
|
-
frame
|
|
386
|
+
`TileMapRenderer` draws a Tiled map. It bakes the static layers once, and draws
|
|
387
|
+
animated tiles each frame, culled to a rectangle of the world.
|
|
360
388
|
|
|
361
389
|
```ruby
|
|
362
|
-
tiles = app.assets.tilemap('map/island.tmx')
|
|
390
|
+
tiles = app.assets.tilemap('map/island.tmx') # => RGame::Core::TileMapRenderer
|
|
363
391
|
|
|
364
|
-
renderer.tilemap('map/island.tmx', 0,
|
|
392
|
+
renderer.tilemap('map/island.tmx', 0, cull_x, cull_y, cull_w, cull_h, elapsed: seconds)
|
|
365
393
|
# ... the scene draws its actors here ...
|
|
366
|
-
renderer.tilemap('map/island.tmx', 1,
|
|
394
|
+
renderer.tilemap('map/island.tmx', 1, cull_x, cull_y, cull_w, cull_h, elapsed: seconds)
|
|
367
395
|
```
|
|
368
396
|
|
|
369
|
-
|
|
397
|
+
**A game rarely makes these calls.** [`TileMapLayer`](components.md#tileworld)
|
|
398
|
+
mounts one node per layer and draws it.
|
|
370
399
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
400
|
+
**Tiles draw in world coordinates.** A tile at column 3 lands at
|
|
401
|
+
`3 * tile_width`. The caller's transform, usually a `WorldView`'s camera, puts it
|
|
402
|
+
on screen. The rectangle is only a **cull rect**: the part of the world worth
|
|
403
|
+
drawing. A camera supplies it but does not move the result, so one map can be
|
|
404
|
+
drawn through several cameras in one frame.
|
|
405
|
+
|
|
406
|
+
### One call per layer, so actors fit between layers
|
|
407
|
+
|
|
408
|
+
**Each call draws one layer, in the order the caller chooses.** A scene can put
|
|
409
|
+
its actors between two layers: trunks under, canopies over. Which layers those
|
|
410
|
+
are depends on the scene, not the map, so the call takes no `z`. In a game,
|
|
411
|
+
[`TileMapLayer`](components.md#tileworld) mounts a node per layer, and the scene
|
|
412
|
+
tree orders them.
|
|
376
413
|
|
|
377
414
|
### What it costs
|
|
378
415
|
|
|
379
|
-
|
|
380
|
-
[recording](drawing.md#recordings-bake-once-replay-cheaply) the first time
|
|
381
|
-
|
|
382
|
-
thousand tiles
|
|
383
|
-
|
|
384
|
-
costs only what is on screen.
|
|
416
|
+
**The renderer bakes each layer's non-animated tiles into a
|
|
417
|
+
[recording](drawing.md#recordings-bake-once-replay-cheaply)** the first time it
|
|
418
|
+
draws that layer. Scrolling the layer then costs one call per texture, however
|
|
419
|
+
many thousand tiles it holds. The few animated tiles draw individually, **culled
|
|
420
|
+
to the viewport**. A map far larger than the screen costs only what is on screen.
|
|
385
421
|
|
|
386
|
-
Two maps
|
|
387
|
-
|
|
422
|
+
Two maps that share a tileset share one GPU upload, because tiles load through the
|
|
423
|
+
asset manager, not through the map.
|
|
388
424
|
|
|
389
|
-
###
|
|
425
|
+
### You advance the animation
|
|
390
426
|
|
|
391
|
-
|
|
427
|
+
**`elapsed` is seconds, passed as an argument.** The renderer reads no clock:
|
|
392
428
|
|
|
393
429
|
```ruby
|
|
394
|
-
def
|
|
395
|
-
|
|
396
|
-
|
|
430
|
+
def on_update(dt) = @elapsed += dt
|
|
431
|
+
|
|
432
|
+
def on_draw(renderer, view)
|
|
433
|
+
camera = view.camera
|
|
434
|
+
renderer.tilemap(@id, @layer, camera.x, camera.y, view.width, view.height, elapsed: @elapsed)
|
|
397
435
|
end
|
|
398
436
|
```
|
|
399
437
|
|
|
400
|
-
Stop accumulating and the water freezes
|
|
401
|
-
spec passes `0.15` and gets the second frame.
|
|
402
|
-
|
|
403
|
-
clock.
|
|
438
|
+
Stop accumulating and the water freezes. Accumulate slower and it runs slow. A
|
|
439
|
+
spec passes `0.15` and gets the second frame. [The frame loop](app.md#the-frame-loop)
|
|
440
|
+
explains why nothing on a draw path reads a clock.
|
|
404
441
|
|
|
405
|
-
###
|
|
442
|
+
### Installed, not built in
|
|
406
443
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
444
|
+
**`RGame::Core` cannot parse a `.tmx`.** Parsing belongs to the engine layer, and
|
|
445
|
+
Core may not know that layer exists. `RGame::Game`, the one class that may name
|
|
446
|
+
both, installs the type:
|
|
410
447
|
|
|
411
448
|
```ruby
|
|
412
449
|
app.assets.add_loader(:tilemap) do |path|
|
|
@@ -417,9 +454,10 @@ app.assets.add_loader(:tilemap) do |path|
|
|
|
417
454
|
end
|
|
418
455
|
```
|
|
419
456
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
subsystem.
|
|
457
|
+
Every `RGame::Game` installs this loader when it is built. A plain
|
|
458
|
+
`RGame::Core::App` has none: its `app.assets` has no `tilemap` accessor, and a
|
|
459
|
+
tilemap draw id raises `KeyError`. A clear error beats a half-working subsystem.
|
|
423
460
|
|
|
424
|
-
`TileMapRenderer#map`
|
|
425
|
-
|
|
461
|
+
`TileMapRenderer#map` returns the parsed map, for the scene's own collision and
|
|
462
|
+
world-bounds queries. [Tile maps](tile_maps.md) documents `TileMap` and `Tileset`,
|
|
463
|
+
and which Tiled features rgame reads.
|