rgame 0.4.0-x86_64-linux-gnu
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +251 -0
- data/LICENSE +26 -0
- data/README.md +325 -0
- data/docs/api/README.md +224 -0
- data/docs/api/app.md +249 -0
- data/docs/api/assets.md +463 -0
- data/docs/api/audio.md +249 -0
- data/docs/api/cli.md +259 -0
- data/docs/api/components.md +1112 -0
- data/docs/api/drawing.md +398 -0
- data/docs/api/examples.md +263 -0
- data/docs/api/game.md +219 -0
- data/docs/api/images.md +118 -0
- data/docs/api/input.md +476 -0
- data/docs/api/internals.md +289 -0
- data/docs/api/localization.md +285 -0
- data/docs/api/scene_graph.md +539 -0
- data/docs/api/signals.md +147 -0
- data/docs/api/systems.md +231 -0
- data/docs/api/text.md +133 -0
- data/docs/api/tile_maps.md +237 -0
- data/docs/api/toolbox.md +493 -0
- data/docs/api/ui.md +927 -0
- data/docs/api/values.md +316 -0
- data/examples/assets/README.md +322 -0
- data/examples/assets/blip.ogg +0 -0
- data/examples/assets/glyphs.json +5 -0
- data/examples/assets/glyphs.png +0 -0
- data/examples/assets/hero.json +12 -0
- data/examples/assets/hero.png +0 -0
- data/examples/assets/icons.json +13 -0
- data/examples/assets/icons.png +0 -0
- data/examples/assets/music.ogg +0 -0
- data/examples/assets/skills.json +10 -0
- data/examples/assets/skills.png +0 -0
- data/examples/assets/tileset.png +0 -0
- data/examples/assets/tileset.tsx +65 -0
- data/examples/assets/town.tmx +26 -0
- data/examples/assets/ui.json +11 -0
- data/examples/assets/ui.png +0 -0
- data/examples/collision/locales/en.yml +8 -0
- data/examples/collision/main.rb +316 -0
- data/examples/collision_tiles/locales/en.yml +9 -0
- data/examples/collision_tiles/main.rb +274 -0
- data/examples/fullscreen/locales/en.yml +10 -0
- data/examples/fullscreen/main.rb +216 -0
- data/examples/game_menu/locales/en.yml +8 -0
- data/examples/game_menu/main.rb +170 -0
- data/examples/input_glyphs/locales/en.yml +14 -0
- data/examples/input_glyphs/main.rb +213 -0
- data/examples/jump_topdown/locales/en.yml +9 -0
- data/examples/jump_topdown/main.rb +178 -0
- data/examples/localization/locales/de.yml +12 -0
- data/examples/localization/locales/en.yml +13 -0
- data/examples/localization/main.rb +158 -0
- data/examples/menu_navigation/locales/en.yml +23 -0
- data/examples/menu_navigation/main.rb +365 -0
- data/examples/music/locales/en.yml +7 -0
- data/examples/music/main.rb +134 -0
- data/examples/pathfinding/locales/en.yml +17 -0
- data/examples/pathfinding/main.rb +298 -0
- data/examples/pooling/locales/en.yml +7 -0
- data/examples/pooling/main.rb +259 -0
- data/examples/quick_wheel/locales/en.yml +16 -0
- data/examples/quick_wheel/main.rb +184 -0
- data/examples/radial_menu/locales/en.yml +16 -0
- data/examples/radial_menu/main.rb +184 -0
- data/examples/save_load/locales/en.yml +11 -0
- data/examples/save_load/main.rb +207 -0
- data/examples/save_load_ids/locales/en.yml +11 -0
- data/examples/save_load_ids/main.rb +322 -0
- data/examples/scroll_map/locales/en.yml +4 -0
- data/examples/scroll_map/main.rb +140 -0
- data/examples/signals/locales/en.yml +6 -0
- data/examples/signals/main.rb +278 -0
- data/examples/skill_bar/locales/en.yml +14 -0
- data/examples/skill_bar/main.rb +159 -0
- data/examples/sound/locales/en.yml +6 -0
- data/examples/sound/main.rb +122 -0
- data/examples/split_screen/locales/en.yml +9 -0
- data/examples/split_screen/main.rb +304 -0
- data/examples/sprite/locales/en.yml +8 -0
- data/examples/sprite/main.rb +180 -0
- data/examples/timer/locales/en.yml +12 -0
- data/examples/timer/main.rb +273 -0
- data/examples/velocity/locales/en.yml +6 -0
- data/examples/velocity/main.rb +196 -0
- data/examples/walk/locales/en.yml +4 -0
- data/examples/walk/main.rb +99 -0
- data/exe/rgame +9 -0
- data/lib/rgame/boot.rb +3 -0
- data/lib/rgame/cli/new_project.rb +139 -0
- data/lib/rgame/cli/templates/Gemfile.tt +23 -0
- data/lib/rgame/cli/templates/README.md.tt +93 -0
- data/lib/rgame/cli/templates/Rakefile.tt +9 -0
- data/lib/rgame/cli/templates/assets/locales/en.yml.tt +10 -0
- data/lib/rgame/cli/templates/game.rb.tt +23 -0
- data/lib/rgame/cli/templates/gitignore.tt +12 -0
- data/lib/rgame/cli/templates/main.rb.tt +11 -0
- data/lib/rgame/cli/templates/nodes/root.rb.tt +24 -0
- data/lib/rgame/cli/templates/rspec.tt +2 -0
- data/lib/rgame/cli/templates/rubocop.yml.tt +75 -0
- data/lib/rgame/cli/templates/ruby-version.tt +1 -0
- data/lib/rgame/cli/templates/spec/locales_spec.rb.tt +18 -0
- data/lib/rgame/cli/templates/spec/nodes/root_spec.rb.tt +18 -0
- data/lib/rgame/cli/templates/spec/spec_helper.rb.tt +37 -0
- data/lib/rgame/cli.rb +66 -0
- data/lib/rgame/core/app.rb +46 -0
- data/lib/rgame/core/asset_manager.rb +206 -0
- data/lib/rgame/core/audio.rb +145 -0
- data/lib/rgame/core/font.rb +46 -0
- data/lib/rgame/core/gamepad.rb +55 -0
- data/lib/rgame/core/image.rb +55 -0
- data/lib/rgame/core/input.rb +71 -0
- data/lib/rgame/core/locale.rb +22 -0
- data/lib/rgame/core/nine_slice.rb +142 -0
- data/lib/rgame/core/recording.rb +54 -0
- data/lib/rgame/core/renderer.rb +354 -0
- data/lib/rgame/core/sprite_sheet.rb +105 -0
- data/lib/rgame/core/tile_map_renderer.rb +172 -0
- data/lib/rgame/core/ui_atlas.rb +101 -0
- data/lib/rgame/core/virtual_gamepad.rb +26 -0
- data/lib/rgame/core.rb +18 -0
- data/lib/rgame/core_ext.so +0 -0
- data/lib/rgame/engine/actor_blockers.rb +131 -0
- data/lib/rgame/engine/animation_set.rb +50 -0
- data/lib/rgame/engine/animator.rb +44 -0
- data/lib/rgame/engine/audio_bus.rb +24 -0
- data/lib/rgame/engine/audio_director.rb +59 -0
- data/lib/rgame/engine/bounds_blockers.rb +74 -0
- data/lib/rgame/engine/camera.rb +78 -0
- data/lib/rgame/engine/circle_collider.rb +34 -0
- data/lib/rgame/engine/collision_box.rb +59 -0
- data/lib/rgame/engine/collision_system.rb +132 -0
- data/lib/rgame/engine/component.rb +64 -0
- data/lib/rgame/engine/components/action_trigger.rb +40 -0
- data/lib/rgame/engine/components/animated_sprite.rb +71 -0
- data/lib/rgame/engine/components/box_collider.rb +99 -0
- data/lib/rgame/engine/components/camera_follow.rb +45 -0
- data/lib/rgame/engine/components/character_body.rb +50 -0
- data/lib/rgame/engine/components/circle_collider.rb +80 -0
- data/lib/rgame/engine/components/collision_world.rb +231 -0
- data/lib/rgame/engine/components/despawn_offscreen.rb +42 -0
- data/lib/rgame/engine/components/feet_collider.rb +61 -0
- data/lib/rgame/engine/components/hop.rb +76 -0
- data/lib/rgame/engine/components/identity.rb +73 -0
- data/lib/rgame/engine/components/mover.rb +285 -0
- data/lib/rgame/engine/components/navigator.rb +145 -0
- data/lib/rgame/engine/components/path_follow.rb +176 -0
- data/lib/rgame/engine/components/player_controller.rb +27 -0
- data/lib/rgame/engine/components/pool.rb +53 -0
- data/lib/rgame/engine/components/screen_wrap.rb +49 -0
- data/lib/rgame/engine/components/sprite.rb +47 -0
- data/lib/rgame/engine/components/targeting.rb +52 -0
- data/lib/rgame/engine/components/thrust_controller.rb +65 -0
- data/lib/rgame/engine/components/tile_world.rb +109 -0
- data/lib/rgame/engine/components/timer.rb +75 -0
- data/lib/rgame/engine/components/velocity.rb +43 -0
- data/lib/rgame/engine/components/wander_controller.rb +64 -0
- data/lib/rgame/engine/components/world.rb +133 -0
- data/lib/rgame/engine/contact_set.rb +74 -0
- data/lib/rgame/engine/culling.rb +45 -0
- data/lib/rgame/engine/debug_overlay.rb +111 -0
- data/lib/rgame/engine/i18n/plural.rb +45 -0
- data/lib/rgame/engine/i18n/plural_rules.rb +82 -0
- data/lib/rgame/engine/i18n/template.rb +59 -0
- data/lib/rgame/engine/i18n.rb +322 -0
- data/lib/rgame/engine/input/action_mapper.rb +100 -0
- data/lib/rgame/engine/input/actions.rb +92 -0
- data/lib/rgame/engine/input/input_map.rb +196 -0
- data/lib/rgame/engine/layout.rb +82 -0
- data/lib/rgame/engine/nav_grid.rb +87 -0
- data/lib/rgame/engine/node2d.rb +555 -0
- data/lib/rgame/engine/path.rb +76 -0
- data/lib/rgame/engine/player.rb +69 -0
- data/lib/rgame/engine/player_layer.rb +70 -0
- data/lib/rgame/engine/players.rb +205 -0
- data/lib/rgame/engine/pool.rb +51 -0
- data/lib/rgame/engine/presentation.rb +171 -0
- data/lib/rgame/engine/scene/scene_stack.rb +87 -0
- data/lib/rgame/engine/sealed_privates.rb +54 -0
- data/lib/rgame/engine/signal.rb +75 -0
- data/lib/rgame/engine/spatial_hash.rb +116 -0
- data/lib/rgame/engine/text.rb +194 -0
- data/lib/rgame/engine/tile_blockers.rb +63 -0
- data/lib/rgame/engine/tile_map.rb +147 -0
- data/lib/rgame/engine/tile_map_layer.rb +82 -0
- data/lib/rgame/engine/tileset.rb +99 -0
- data/lib/rgame/engine/timer.rb +51 -0
- data/lib/rgame/engine/ui/button.rb +248 -0
- data/lib/rgame/engine/ui/column.rb +20 -0
- data/lib/rgame/engine/ui/icon_button.rb +93 -0
- data/lib/rgame/engine/ui/menu.rb +290 -0
- data/lib/rgame/engine/ui/navigation.rb +57 -0
- data/lib/rgame/engine/ui/nine_slice_style.rb +50 -0
- data/lib/rgame/engine/ui/option_button.rb +163 -0
- data/lib/rgame/engine/ui/panel_button.rb +32 -0
- data/lib/rgame/engine/ui/panel_menu.rb +36 -0
- data/lib/rgame/engine/ui/pointing.rb +146 -0
- data/lib/rgame/engine/ui/radial_menu.rb +85 -0
- data/lib/rgame/engine/ui/ring.rb +55 -0
- data/lib/rgame/engine/ui/row.rb +21 -0
- data/lib/rgame/engine/ui/shape_style.rb +102 -0
- data/lib/rgame/engine/ui/stack.rb +58 -0
- data/lib/rgame/engine/ui/stepping.rb +93 -0
- data/lib/rgame/engine/ui/text_button.rb +59 -0
- data/lib/rgame/engine/view.rb +76 -0
- data/lib/rgame/engine/viewports.rb +171 -0
- data/lib/rgame/engine/world_view.rb +71 -0
- data/lib/rgame/engine.rb +86 -0
- data/lib/rgame/fonts/LiberationSans-Regular.ttf +0 -0
- data/lib/rgame/fonts/OFL.txt +102 -0
- data/lib/rgame/game.rb +271 -0
- data/lib/rgame/rubocop/cop/game/draw_in_local_space.rb +103 -0
- data/lib/rgame/rubocop/cop/game/hot_path.rb +36 -0
- data/lib/rgame/rubocop/cop/game/layer_boundary.rb +43 -0
- data/lib/rgame/rubocop/cop/game/no_core_in_engine_layer.rb +100 -0
- data/lib/rgame/rubocop/cop/game/no_engine_in_core_layer.rb +84 -0
- data/lib/rgame/rubocop/cop/game/no_interpolation_in_hot_path.rb +50 -0
- data/lib/rgame/rubocop/cop/game/no_literal_text.rb +41 -0
- data/lib/rgame/rubocop/cop/game/no_needless_allocation.rb +112 -0
- data/lib/rgame/rubocop/default.yml +39 -0
- data/lib/rgame/rubocop/plugin.rb +45 -0
- data/lib/rgame/rubocop.rb +11 -0
- data/lib/rgame/util/color.rb +23 -0
- data/lib/rgame/util/controls.rb +169 -0
- data/lib/rgame/util/route_search.rb +27 -0
- data/lib/rgame/util/save_file.rb +107 -0
- data/lib/rgame/util/solid_grid.rb +37 -0
- data/lib/rgame/util/tensor.rb +3 -0
- data/lib/rgame/util/tile_sweep.rb +36 -0
- data/lib/rgame/util/z.rb +123 -0
- data/lib/rgame/util.rb +10 -0
- data/lib/rgame/util_ext.so +0 -0
- data/lib/rgame/version.rb +12 -0
- data/lib/rgame.rb +5 -0
- data/licenses/SDL2/LICENSE.txt +18 -0
- metadata +309 -0
data/docs/api/assets.md
ADDED
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
# Assets: sheets, atlases, maps and sounds
|
|
2
|
+
|
|
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`.
|
|
9
|
+
|
|
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
|
|
12
|
+
[Testing what a scene draws](drawing.md#testing-what-a-scene-draws).
|
|
13
|
+
|
|
14
|
+
| Page section | Class |
|
|
15
|
+
|---|---|
|
|
16
|
+
| [The asset manager](#the-asset-manager) | `RGame::Core::AssetManager` |
|
|
17
|
+
| [Sprite sheets](#sprite-sheets) | `RGame::Core::SpriteSheet` |
|
|
18
|
+
| [Nine-slices](#nine-slices) | `RGame::Core::NineSlice` |
|
|
19
|
+
| [UI atlases](#ui-atlases) | `RGame::Core::UiAtlas` |
|
|
20
|
+
| [Tile maps](#tile-maps) | `RGame::Core::TileMapRenderer` |
|
|
21
|
+
|
|
22
|
+
## The asset manager
|
|
23
|
+
|
|
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:`:
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
app.assets.image('space.png') # => RGame::Core::Image
|
|
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
|
|
32
|
+
app.assets.ui_atlas('ui/ui_atlas.json') # => RGame::Core::UiAtlas
|
|
33
|
+
app.assets.read('data/levels.txt') # => String
|
|
34
|
+
```
|
|
35
|
+
|
|
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.
|
|
56
|
+
|
|
57
|
+
### Adding an asset type
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
app.assets.add_loader(:level) { |path| MyLevel.parse(File.read(path)) }
|
|
61
|
+
app.assets.level('levels/one.json') # cached and grouped like any other
|
|
62
|
+
```
|
|
63
|
+
|
|
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).
|
|
68
|
+
|
|
69
|
+
### Groups, and what `release` frees
|
|
70
|
+
|
|
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.
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
app.assets.image('ui/buttons.png') # ungrouped: permanent
|
|
77
|
+
app.assets.preload(:level1, image: ['lvl1/bg.png'],
|
|
78
|
+
sound: ['lvl1/hit.ogg'],
|
|
79
|
+
sheet: ['lvl1/foes.json'])
|
|
80
|
+
app.assets.image('shared.png', :level2) # one group, by hand
|
|
81
|
+
|
|
82
|
+
app.assets.release(:level1) # drops lvl1/* unless another group still holds it
|
|
83
|
+
app.assets.clear # drops everything, permanent included
|
|
84
|
+
```
|
|
85
|
+
|
|
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.
|
|
89
|
+
|
|
90
|
+
Releasing drops this cache's reference. The garbage collector decides when the
|
|
91
|
+
GPU texture goes. Watch it with `Image.debug_live_textures`.
|
|
92
|
+
|
|
93
|
+
`release` refuses the permanent group. Releasing it would drop every ungrouped
|
|
94
|
+
asset, the opposite of "permanent". Use `clear` instead.
|
|
95
|
+
|
|
96
|
+
### Composites share their parts
|
|
97
|
+
|
|
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:
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
sheet = app.assets.sheet('sheets/hero.json') # names hero.png inside
|
|
103
|
+
image = app.assets.image('sheets/hero.png') # the same texture, not a second one
|
|
104
|
+
```
|
|
105
|
+
|
|
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.
|
|
109
|
+
|
|
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.
|
|
116
|
+
|
|
117
|
+
### Failure
|
|
118
|
+
|
|
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.
|
|
123
|
+
|
|
124
|
+
### Testing without files
|
|
125
|
+
|
|
126
|
+
Every asset type maps to a loader proc, and you can inject your own:
|
|
127
|
+
|
|
128
|
+
```ruby
|
|
129
|
+
assets = RGame::Core::AssetManager.new(
|
|
130
|
+
root: '/media', app: nil,
|
|
131
|
+
loaders: { image: ->(path) { FakeImage.new(path) } }
|
|
132
|
+
)
|
|
133
|
+
```
|
|
134
|
+
|
|
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.
|
|
138
|
+
|
|
139
|
+
## Sprite sheets
|
|
140
|
+
|
|
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.
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
sheet = app.assets.sheet('hero.json')
|
|
146
|
+
|
|
147
|
+
sheet.frame_width # => 16
|
|
148
|
+
sheet.grid # => [rows, columns]
|
|
149
|
+
sheet.animations # => the raw table from the descriptor
|
|
150
|
+
|
|
151
|
+
sheet.draw(renderer, row, col, x, y, flip_x: false, z: 0)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### The descriptor
|
|
155
|
+
|
|
156
|
+
```json
|
|
157
|
+
{
|
|
158
|
+
"image": "hero.png",
|
|
159
|
+
"frame_width": 16,
|
|
160
|
+
"frame_height": 24,
|
|
161
|
+
"cell_width": 32,
|
|
162
|
+
"cell_height": 32,
|
|
163
|
+
"origin_x": 8,
|
|
164
|
+
"origin_y": 4,
|
|
165
|
+
"animations": {
|
|
166
|
+
"walk_left": { "row": 1, "frames": 4, "fps": 8 },
|
|
167
|
+
"stand": { "row": 0, "col": 1, "frames": 1, "fps": 1 }
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
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.
|
|
175
|
+
|
|
176
|
+
### A frame can be smaller than its cell
|
|
177
|
+
|
|
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`
|
|
180
|
+
inside its cell:
|
|
181
|
+
|
|
182
|
+
```
|
|
183
|
+
cell (32x32) frame (16x24) at origin (8, 4)
|
|
184
|
+
┌──────────────┐ ┌──────────────┐
|
|
185
|
+
│ │ │ ┌────┐ │
|
|
186
|
+
│ │ │ │ │ │
|
|
187
|
+
│ │ │ │ │ │
|
|
188
|
+
└──────────────┘ └────┴────┴────┘
|
|
189
|
+
```
|
|
190
|
+
|
|
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.
|
|
194
|
+
|
|
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.
|
|
197
|
+
|
|
198
|
+
### Facing
|
|
199
|
+
|
|
200
|
+
**`flip_x` mirrors the frame inside the same rectangle**, so a character covers
|
|
201
|
+
the same pixels whichever way it faces:
|
|
202
|
+
|
|
203
|
+
```ruby
|
|
204
|
+
sheet.draw(renderer, row, col, x, y, flip_x: moving_left)
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
You add no width back; [Mirroring](drawing.md#mirroring) explains why.
|
|
208
|
+
|
|
209
|
+
### Animations come back raw
|
|
210
|
+
|
|
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.
|
|
215
|
+
|
|
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.
|
|
218
|
+
|
|
219
|
+
### Slicing costs nothing
|
|
220
|
+
|
|
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.
|
|
224
|
+
|
|
225
|
+
### Loading
|
|
226
|
+
|
|
227
|
+
```ruby
|
|
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
|
|
231
|
+
```
|
|
232
|
+
|
|
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.
|
|
238
|
+
|
|
239
|
+
## Nine-slices
|
|
240
|
+
|
|
241
|
+
A nine-slice draws a bordered texture at any size. It cuts the texture into nine
|
|
242
|
+
pieces and treats each piece differently.
|
|
243
|
+
|
|
244
|
+
```ruby
|
|
245
|
+
panel = RGame::Core::NineSlice.new(image, x: 0, y: 0, w: 26, h: 28,
|
|
246
|
+
border: 7, scale: 3)
|
|
247
|
+
|
|
248
|
+
panel.draw(renderer, x, y, width, height, z: 0, color: nil)
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
```
|
|
252
|
+
┌──┬────────┬──┐ corners: fixed size
|
|
253
|
+
│tl│ top │tr│ top / bottom: tiled across
|
|
254
|
+
├──┼────────┼──┤ left / right: tiled down
|
|
255
|
+
│l │ centre │ r│ centre: tiled both ways
|
|
256
|
+
├──┼────────┼──┤
|
|
257
|
+
│bl│ bottom │br│
|
|
258
|
+
└──┴────────┴──┘
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
One small piece of art fills a button, a dialog or a health bar of any size, and
|
|
262
|
+
the corners never smear.
|
|
263
|
+
|
|
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.
|
|
266
|
+
|
|
267
|
+
### Tiled, not stretched
|
|
268
|
+
|
|
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.
|
|
274
|
+
|
|
275
|
+
### `border` and `scale`
|
|
276
|
+
|
|
277
|
+
`border` takes a uniform integer or a hash:
|
|
278
|
+
|
|
279
|
+
```ruby
|
|
280
|
+
border: 7
|
|
281
|
+
border: { left: 2, right: 6, top: 4, bottom: 4 }
|
|
282
|
+
```
|
|
283
|
+
|
|
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
|
|
288
|
+
tiles, so the tiling stays seamless.
|
|
289
|
+
|
|
290
|
+
### Edge cases
|
|
291
|
+
|
|
292
|
+
| | |
|
|
293
|
+
|---|---|
|
|
294
|
+
| A rectangle smaller than its own borders | draws its corners and no bands |
|
|
295
|
+
| A border with no room for a centre (`left + right == w`) | fine — a bar that stretches only vertically |
|
|
296
|
+
| Borders wider than the source rect | `ArgumentError`, naming the borders and the rect |
|
|
297
|
+
| `scale` of zero or less | `ArgumentError` — the tiling loop would never advance |
|
|
298
|
+
|
|
299
|
+
### What it costs
|
|
300
|
+
|
|
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:`.
|
|
308
|
+
|
|
309
|
+
## UI atlases
|
|
310
|
+
|
|
311
|
+
A UI atlas cuts one sheet of UI art into named [nine-slices](#nine-slices) and
|
|
312
|
+
named images.
|
|
313
|
+
|
|
314
|
+
```ruby
|
|
315
|
+
atlas = app.assets.ui_atlas('ui/ui_atlas.json')
|
|
316
|
+
renderer.register_ui_atlas(atlas)
|
|
317
|
+
|
|
318
|
+
renderer.nine_slice(:button_idle, x, y, width, height)
|
|
319
|
+
renderer.image(:home, cx, cy)
|
|
320
|
+
```
|
|
321
|
+
|
|
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.
|
|
324
|
+
|
|
325
|
+
### The descriptor
|
|
326
|
+
|
|
327
|
+
```json
|
|
328
|
+
{
|
|
329
|
+
"image": "buttons.png",
|
|
330
|
+
"scale": 3,
|
|
331
|
+
"nine_slices": {
|
|
332
|
+
"button_idle": { "x": 11, "y": 59, "w": 26, "h": 28, "border": 7 },
|
|
333
|
+
"button_focus": { "x": 43, "y": 59, "w": 26, "h": 28, "border": 7 },
|
|
334
|
+
"panel": { "x": 0, "y": 0, "w": 32, "h": 32, "scale": 2,
|
|
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 }
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
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.
|
|
353
|
+
|
|
354
|
+
### Element names, not filenames
|
|
355
|
+
|
|
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`:
|
|
361
|
+
|
|
362
|
+
```ruby
|
|
363
|
+
renderer.register_ui_atlas(atlas) # all of them
|
|
364
|
+
renderer.register_nine_slice(:panel, atlas.nine_slices[:panel]) # or one
|
|
365
|
+
renderer.register_image(:home, atlas.images[:home])
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
### When an entry is wrong
|
|
369
|
+
|
|
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:
|
|
373
|
+
|
|
374
|
+
```
|
|
375
|
+
ArgumentError: ui atlas element :button_idle: nine-slice borders (40, 40, 40, 40)
|
|
376
|
+
do not fit in a 26x28 rect
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
Without the name, the error would be bare arithmetic from inside `NineSlice`.
|
|
380
|
+
Finding the culprit would mean bisecting the JSON by hand.
|
|
381
|
+
|
|
382
|
+
The atlas parses once, at load, and touches nothing again per frame.
|
|
383
|
+
|
|
384
|
+
## Tile maps
|
|
385
|
+
|
|
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.
|
|
388
|
+
|
|
389
|
+
```ruby
|
|
390
|
+
tiles = app.assets.tilemap('map/island.tmx') # => RGame::Core::TileMapRenderer
|
|
391
|
+
|
|
392
|
+
renderer.tilemap('map/island.tmx', 0, cull_x, cull_y, cull_w, cull_h, elapsed: seconds)
|
|
393
|
+
# ... the scene draws its actors here ...
|
|
394
|
+
renderer.tilemap('map/island.tmx', 1, cull_x, cull_y, cull_w, cull_h, elapsed: seconds)
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
**A game rarely makes these calls.** [`TileMapLayer`](components.md#tileworld)
|
|
398
|
+
mounts one node per layer and draws it.
|
|
399
|
+
|
|
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.
|
|
413
|
+
|
|
414
|
+
### What it costs
|
|
415
|
+
|
|
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.
|
|
421
|
+
|
|
422
|
+
Two maps that share a tileset share one GPU upload, because tiles load through the
|
|
423
|
+
asset manager, not through the map.
|
|
424
|
+
|
|
425
|
+
### You advance the animation
|
|
426
|
+
|
|
427
|
+
**`elapsed` is seconds, passed as an argument.** The renderer reads no clock:
|
|
428
|
+
|
|
429
|
+
```ruby
|
|
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)
|
|
435
|
+
end
|
|
436
|
+
```
|
|
437
|
+
|
|
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.
|
|
441
|
+
|
|
442
|
+
### Installed, not built in
|
|
443
|
+
|
|
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:
|
|
447
|
+
|
|
448
|
+
```ruby
|
|
449
|
+
app.assets.add_loader(:tilemap) do |path|
|
|
450
|
+
map, image_path = RGame::Engine::TileMap.load(path)
|
|
451
|
+
tiles = app.assets.image(image_path).tiles(map.tileset.tile_width,
|
|
452
|
+
map.tileset.tile_height)
|
|
453
|
+
RGame::Core::TileMapRenderer.new(map, tiles)
|
|
454
|
+
end
|
|
455
|
+
```
|
|
456
|
+
|
|
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.
|
|
460
|
+
|
|
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.
|