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/text.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# Text
|
|
2
2
|
|
|
3
|
+
**The renderer comes with a font, and `text` draws with it.** Most games need
|
|
4
|
+
nothing more:
|
|
5
|
+
|
|
3
6
|
```ruby
|
|
4
7
|
require 'rgame'
|
|
5
8
|
require 'rgame/core'
|
|
@@ -14,25 +17,29 @@ class MyGame < RGame::Core::App
|
|
|
14
17
|
@renderer.text('Score: 1200', 10, 10)
|
|
15
18
|
end
|
|
16
19
|
end
|
|
17
|
-
```
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
MyGame.new.run
|
|
22
|
+
```
|
|
21
23
|
|
|
22
24
|
## Where text goes
|
|
23
25
|
|
|
24
|
-
`text(string, x, y, …)` puts the **top-left corner** of the line at `(x, y)
|
|
25
|
-
|
|
26
|
-
baseline
|
|
26
|
+
`text(string, x, y, …)` puts the **top-left corner** of the line at `(x, y)`.
|
|
27
|
+
Every other drawing method uses the same corner. Typography measures from the
|
|
28
|
+
baseline, but a caller placing a label does not have to.
|
|
27
29
|
|
|
28
30
|
```ruby
|
|
29
31
|
renderer.text(string, x, y, z: 10, color: nil, font: nil)
|
|
30
|
-
renderer.text_width(string, font: nil) # => Float
|
|
31
|
-
renderer.text_height(font: nil) # => Integer
|
|
32
|
+
renderer.text_width(string, font: nil) # => Float — pixels
|
|
33
|
+
renderer.text_height(font: nil) # => Integer — the line height
|
|
32
34
|
```
|
|
33
35
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
**`string` is a String or anything with `to_str`**, such as an
|
|
37
|
+
[`Engine::Text`](toolbox.md#text--the-string-a-node-draws), which a node passes
|
|
38
|
+
as it is. `text` and `text_width` raise `TypeError` for `nil`, a number, or a
|
|
39
|
+
`to_str` that returns something other than a String.
|
|
40
|
+
|
|
41
|
+
**A string is one line.** A newline has no special meaning. Draw two lines with
|
|
42
|
+
two calls, stepped by `text_height`:
|
|
36
43
|
|
|
37
44
|
```ruby
|
|
38
45
|
lines.each_with_index do |line, i|
|
|
@@ -40,15 +47,25 @@ lines.each_with_index do |line, i|
|
|
|
40
47
|
end
|
|
41
48
|
```
|
|
42
49
|
|
|
43
|
-
**`text_width` and `text` agree.** They
|
|
44
|
-
|
|
50
|
+
**`text_width` and `text` agree.** They run the same code, so a label centred by
|
|
51
|
+
its measured width lands exactly there:
|
|
45
52
|
|
|
46
53
|
```ruby
|
|
47
54
|
@renderer.text(label, (width - @renderer.text_width(label)) / 2, 20)
|
|
48
55
|
```
|
|
49
56
|
|
|
50
|
-
|
|
51
|
-
|
|
57
|
+
**`text_width` and `text_height` also work outside `draw`**, unlike the drawing
|
|
58
|
+
methods. Measuring touches no GPU, and a menu lays itself out while updating.
|
|
59
|
+
|
|
60
|
+
A label built from a changing value, like a score, should come from
|
|
61
|
+
[`RGame::Engine::Text`](toolbox.md#text--the-string-a-node-draws). It renders
|
|
62
|
+
the string only when a variable or the language changes:
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
@score = RGame::Engine::Text.new('hud.score', :score) # once
|
|
66
|
+
|
|
67
|
+
renderer.text(@score.with(score: @points), 10, 10) # every frame
|
|
68
|
+
```
|
|
52
69
|
|
|
53
70
|
## Fonts
|
|
54
71
|
|
|
@@ -62,55 +79,55 @@ font.text_width('Hello') # => 38.7
|
|
|
62
79
|
renderer.text('Hello', 10, 10, font: font)
|
|
63
80
|
```
|
|
64
81
|
|
|
65
|
-
A `Font` is **one typeface at one pixel size**. Two sizes
|
|
66
|
-
|
|
67
|
-
through another app's renderer raises
|
|
82
|
+
A `Font` is **one typeface at one pixel size**. Two sizes need two fonts. A font
|
|
83
|
+
belongs to the app whose GPU context holds its glyphs, like an image. Drawing it
|
|
84
|
+
through another app's renderer raises instead of painting blank boxes.
|
|
68
85
|
|
|
69
|
-
The renderer builds its own font
|
|
70
|
-
|
|
86
|
+
The renderer builds its own 18px font on first use. Replace it, and every `text`
|
|
87
|
+
call without a `font:` follows:
|
|
71
88
|
|
|
72
89
|
```ruby
|
|
73
90
|
@renderer.font = RGame::Core::Font.new(self, 24)
|
|
74
91
|
```
|
|
75
92
|
|
|
76
|
-
A file that
|
|
93
|
+
A file that is unreadable or not a TrueType font raises
|
|
77
94
|
`RGame::Core::Font::LoadError`, naming the path.
|
|
78
95
|
|
|
79
96
|
### The default font, and what it covers
|
|
80
97
|
|
|
81
|
-
The engine ships
|
|
82
|
-
|
|
98
|
+
**The engine ships Liberation Sans and uses it when you pass no path.** It never
|
|
99
|
+
looks a font up by name and never asks a system font database. A font is a file.
|
|
83
100
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
developer's
|
|
87
|
-
identically everywhere,
|
|
101
|
+
This is a deliberate trade. Asking the operating system for "Arial" gets whatever
|
|
102
|
+
that machine keeps under the name, or a substitute. A UI laid out on the
|
|
103
|
+
developer's machine can then overflow on a player's. A shipped font renders
|
|
104
|
+
identically everywhere, and costs about 400 KB in the gem.
|
|
88
105
|
|
|
89
106
|
| | |
|
|
90
107
|
|---|---|
|
|
91
108
|
| Covers | English, German, French, Italian, Spanish, Portuguese, Nordic, Polish — in full, including `ß`, `ẞ`, `« »`, curly quotes and `€`. Greek and Cyrillic too. |
|
|
92
109
|
| Does not cover | CJK, Arabic, Hebrew, Devanagari. Pass your own font file for those; no font of this size includes them. |
|
|
93
110
|
|
|
94
|
-
Text is UTF-8. A malformed byte draws one replacement character and the rest of
|
|
95
|
-
the string
|
|
96
|
-
label.
|
|
111
|
+
Text is UTF-8. A malformed byte draws one replacement character, and the rest of
|
|
112
|
+
the string still draws. A bad byte in a data file costs one visible box, not the
|
|
113
|
+
whole label.
|
|
97
114
|
|
|
98
115
|
## What it costs
|
|
99
116
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
strings it draws. A score that changes every frame
|
|
103
|
-
digits
|
|
104
|
-
is one draw call.
|
|
117
|
+
**The engine rasterises each glyph the first time it is drawn**, then keeps it in
|
|
118
|
+
a texture atlas. Cost therefore grows with the **characters** a game uses, not
|
|
119
|
+
with the strings it draws. A score that changes every frame costs no more glyph
|
|
120
|
+
work after the ten digits. A whole Latin character set fits on one 512×512 page,
|
|
121
|
+
so a line of text is one draw call.
|
|
105
122
|
|
|
106
|
-
A font that is only measured
|
|
123
|
+
A font that is only measured, never drawn, uses no video memory.
|
|
107
124
|
|
|
108
|
-
Nothing needs freeing
|
|
109
|
-
|
|
110
|
-
atlas pages exist
|
|
125
|
+
Nothing needs freeing. The engine releases a font's atlas when the font is
|
|
126
|
+
collected, whether before or after its app. `Font.debug_live_pages` returns how
|
|
127
|
+
many atlas pages exist. It serves tests, not gameplay.
|
|
111
128
|
|
|
112
129
|
## What is not here
|
|
113
130
|
|
|
114
|
-
|
|
115
|
-
wrapping
|
|
116
|
-
of left-to-right glyphs.
|
|
131
|
+
rgame text has no markup (`<b>`, colour tags), no bold or italic variants, no
|
|
132
|
+
multi-line layout and no word wrapping. It has no text input, no right-to-left
|
|
133
|
+
text and no complex shaping. A string is one line of left-to-right glyphs.
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# Tile maps
|
|
2
|
+
|
|
3
|
+
**`RGame::Engine::TileMap` and `RGame::Engine::Tileset` hold a map made in
|
|
4
|
+
[Tiled](https://www.mapeditor.org/) as plain data.** `TileMap` holds the grid: its
|
|
5
|
+
size, its layers and the tile id in every cell. `Tileset` holds what each tile id
|
|
6
|
+
means: which tiles are solid and which are animated. Neither class loads an image
|
|
7
|
+
or names a renderer, so both run headless and in specs.
|
|
8
|
+
|
|
9
|
+
A game rarely touches either class directly. The pieces that use them are:
|
|
10
|
+
|
|
11
|
+
| | Uses the map to |
|
|
12
|
+
|---|---|
|
|
13
|
+
| [`TileWorld`](components.md#tileworld) | answer solidity and world-size questions for actors |
|
|
14
|
+
| [`TileMapLayer`](components.md#tileworld) | draw one layer per node |
|
|
15
|
+
| [`TileMapRenderer`](assets.md#tile-maps) | bake and draw the tiles |
|
|
16
|
+
| `RGame::Game`'s `:tilemap` asset loader | load a `.tmx` and pair it with its tileset image |
|
|
17
|
+
|
|
18
|
+
Read on when a scene queries the map itself, or when you author maps for rgame.
|
|
19
|
+
|
|
20
|
+
## Loading a map
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
require 'rgame'
|
|
24
|
+
|
|
25
|
+
map, image_path = RGame::Engine::TileMap.load('examples/assets/town.tmx')
|
|
26
|
+
|
|
27
|
+
image_path # => "examples/assets/tileset.png"
|
|
28
|
+
map.width # => 60 — in tiles
|
|
29
|
+
map.pixel_width # => 960
|
|
30
|
+
map.tileset # => RGame::Engine::Tileset
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**`TileMap.load(tmx_path)` returns two values: the map and its tileset image
|
|
34
|
+
path.** It reads the `.tmx`, follows it to the `.tsx` it names, parses both, and
|
|
35
|
+
attaches the tileset to the map. It does not load the image. An image is a GPU
|
|
36
|
+
handle, and the engine layer may not hold one. `RGame::Game`'s asset loader
|
|
37
|
+
passes the path to the asset manager instead:
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
app.assets.add_loader(:tilemap) do |path|
|
|
41
|
+
map, image_path = RGame::Engine::TileMap.load(path)
|
|
42
|
+
tiles = app.assets.image(image_path).tiles(map.tileset.tile_width,
|
|
43
|
+
map.tileset.tile_height)
|
|
44
|
+
RGame::Core::TileMapRenderer.new(map, tiles)
|
|
45
|
+
end
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
In a game, reach the parsed map through that loader:
|
|
49
|
+
`app.assets.tilemap('map/island.tmx').map`.
|
|
50
|
+
|
|
51
|
+
**Every path resolves relative to the file that names it.** The `.tsx` resolves
|
|
52
|
+
next to the `.tmx`, and the image next to the `.tsx`. Tiled writes paths this
|
|
53
|
+
way, so you can move a map and its tileset together.
|
|
54
|
+
|
|
55
|
+
`TileMap.parse(tmx_string)` parses a `.tmx` from a String and touches no files. It
|
|
56
|
+
leaves `tileset` as `nil`. Attach one with `map.tileset = Tileset.parse(...)`
|
|
57
|
+
before asking about solidity.
|
|
58
|
+
|
|
59
|
+
### What rgame reads from Tiled
|
|
60
|
+
|
|
61
|
+
rgame supports a subset of the `.tmx` format. A map outside it fails to parse or
|
|
62
|
+
parses wrongly, so author within these limits:
|
|
63
|
+
|
|
64
|
+
| | Supported |
|
|
65
|
+
|---|---|
|
|
66
|
+
| Orientation | orthogonal |
|
|
67
|
+
| Tilesets | **one**, in an external `.tsx` file; an embedded tileset has no `source` and `load` raises |
|
|
68
|
+
| Layer data | base64 with zlib compression (Tiled's "Base64 (zlib compressed)"); other encodings raise |
|
|
69
|
+
| Layers | tile layers only; object and image layers are ignored |
|
|
70
|
+
| Flipped or rotated tiles | the flip flags are stripped, so the tile draws unflipped |
|
|
71
|
+
| Map size | fixed; infinite maps are not read |
|
|
72
|
+
|
|
73
|
+
## `RGame::Engine::TileMap`
|
|
74
|
+
|
|
75
|
+
### Geometry
|
|
76
|
+
|
|
77
|
+
| Reader | |
|
|
78
|
+
|---|---|
|
|
79
|
+
| `width`, `height` | the map's size in tiles |
|
|
80
|
+
| `tile_width`, `tile_height` | one tile's size in pixels |
|
|
81
|
+
| `pixel_width`, `pixel_height` | the map's size in pixels |
|
|
82
|
+
| `layer_count` | how many tile layers the map has |
|
|
83
|
+
| `tileset`, `tileset=` | the attached `Tileset`, or `nil` |
|
|
84
|
+
| `tileset_source`, `firstgid` | the `.tsx` path the map names, and the tileset's first gid |
|
|
85
|
+
|
|
86
|
+
### Reading cells
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
map.gid(0, 12, 7) # the gid in layer 0 at column 12, row 7
|
|
90
|
+
map.in_bounds?(60, 0) # => false — columns run 0..59
|
|
91
|
+
map.gid(0, -1, 0) # => 0 — outside the map, every layer is empty
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**A gid is Tiled's global tile id.** `0` means an empty cell. Any other gid names a
|
|
95
|
+
tile in the tileset; `Tileset#local_id(gid)` turns it into the tile's index in the
|
|
96
|
+
tileset image. Layer `0` is the bottom layer, as Tiled lists them.
|
|
97
|
+
|
|
98
|
+
`gid` answers `0` for any cell outside the map, so a caller never checks bounds
|
|
99
|
+
first. The map stores its gids in one
|
|
100
|
+
[`Util::Tensor`](values.md#rgameutiltensor), indexed column, row, layer.
|
|
101
|
+
|
|
102
|
+
### Solidity
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
map.solid_tile?(12, 7) # any layer solid at that cell?
|
|
106
|
+
map.solid_at?(200.5, 116.0) # the same, from a world position in pixels
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**A cell is solid when any layer holds a solid tile there.** Collision considers
|
|
110
|
+
every layer, whatever it draws like. `solid_at?(world_x, world_y)` divides by the
|
|
111
|
+
tile size and asks `solid_tile?` for that cell. **Outside the map is not solid.**
|
|
112
|
+
Keep actors inside with `blocked_by: [:bounds]` on their mover.
|
|
113
|
+
|
|
114
|
+
Both methods need an attached tileset, which `load` provides.
|
|
115
|
+
|
|
116
|
+
Actors do not call these per step. [`TileWorld`](components.md#tileworld) reads
|
|
117
|
+
`solid_tile?` once per cell into a
|
|
118
|
+
[`Util::SolidGrid`](values.md#rgameutilsolidgrid), and collision and pathfinding
|
|
119
|
+
read that grid.
|
|
120
|
+
|
|
121
|
+
### Layers drawn above the actors
|
|
122
|
+
|
|
123
|
+
```ruby
|
|
124
|
+
map.above_layer?(2) # => true when Tiled marks layer 2 `above`
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**Mark a layer `above` in Tiled** to draw it over the actors, for tree canopies
|
|
128
|
+
or roofs. Add a custom **bool** property named `above` to the layer and tick it. A
|
|
129
|
+
layer without the property draws below.
|
|
130
|
+
|
|
131
|
+
The flag affects drawing order only. `TileWorld#first_above_layer` returns the
|
|
132
|
+
first flagged layer, and
|
|
133
|
+
[`TileMapLayer.mount`](components.md#tileworld) leaves the actors' gap below it.
|
|
134
|
+
A map with no flagged layer puts the actors over everything.
|
|
135
|
+
|
|
136
|
+
### Building a map by hand
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
require 'rgame'
|
|
140
|
+
|
|
141
|
+
map = RGame::Engine::TileMap.new(
|
|
142
|
+
width: 2, height: 1, tile_width: 16, tile_height: 16,
|
|
143
|
+
tileset_source: 'tiles.tsx', firstgid: 1,
|
|
144
|
+
layers: [[1, 2]], # one layer, gids row by row
|
|
145
|
+
above: [false]
|
|
146
|
+
)
|
|
147
|
+
map.tileset = RGame::Engine::Tileset.new(
|
|
148
|
+
firstgid: 1, columns: 2, tile_width: 16, tile_height: 16,
|
|
149
|
+
image_source: 'tiles.png', animations: {}, solid_ids: Set[1]
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
map.solid_at?(20.0, 3.0) # => true — gid 2 is local tile 1, which is solid
|
|
153
|
+
map.solid_at?(3.0, 3.0) # => false
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**Specs build small maps this way**, with no files. `layers:` is an Array of
|
|
157
|
+
layers. Each layer is a flat Array of `width * height` gids, row by row. `above:`
|
|
158
|
+
is optional and defaults to no layer above.
|
|
159
|
+
|
|
160
|
+
## `RGame::Engine::Tileset`
|
|
161
|
+
|
|
162
|
+
**A `Tileset` says what each tile in a sheet is.** It knows the sheet's geometry,
|
|
163
|
+
which tiles are solid, and which are animated.
|
|
164
|
+
|
|
165
|
+
| Reader | |
|
|
166
|
+
|---|---|
|
|
167
|
+
| `firstgid` | the gid of the tileset's first tile in the map |
|
|
168
|
+
| `columns` | how many tiles one row of the sheet holds |
|
|
169
|
+
| `tile_width`, `tile_height` | one tile's size in pixels |
|
|
170
|
+
| `image_source` | the sheet image, as the `.tsx` names it |
|
|
171
|
+
| `animations` | `{ local_id => [Frame, ...] }` for every animated tile |
|
|
172
|
+
| `animated_ids` | the local ids that have an animation |
|
|
173
|
+
| `solid_ids`, `solid_ids=` | a Set of the local ids that are solid |
|
|
174
|
+
|
|
175
|
+
`Tileset.parse(tsx_string, firstgid:)` parses a `.tsx` from a String. The
|
|
176
|
+
`firstgid` comes from the map, because Tiled stores it in the `.tmx`, not in the
|
|
177
|
+
`.tsx`.
|
|
178
|
+
|
|
179
|
+
### Local ids
|
|
180
|
+
|
|
181
|
+
```ruby
|
|
182
|
+
tileset.local_id(37) # => 36 when firstgid is 1
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**A local id is a tile's index in the sheet**, counted from 0, left to right and
|
|
186
|
+
top to bottom. It equals `gid - firstgid`. `Image#tiles` slices a sheet in the same
|
|
187
|
+
order, so a local id indexes that Array directly.
|
|
188
|
+
|
|
189
|
+
### Solid tiles
|
|
190
|
+
|
|
191
|
+
```ruby
|
|
192
|
+
tileset.solid?(gid) # takes a gid, not a local id; 0 is never solid
|
|
193
|
+
tileset.solid_ids << 12 # make local tile 12 solid
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
**A tile is solid when it has a collision shape in Tiled.** Open the tileset in
|
|
197
|
+
Tiled's collision editor and draw any shape on the tile. `parse` marks every tile
|
|
198
|
+
whose `<objectgroup>` holds at least one object. The shape itself is ignored:
|
|
199
|
+
rgame treats a solid tile as a solid square. The map carries its collision, so
|
|
200
|
+
changing which tiles block needs no code.
|
|
201
|
+
|
|
202
|
+
`solid_ids` is writable, for a game that adds or removes solid tiles in code. Change
|
|
203
|
+
it before a `TileWorld` first asks, because the world copies solidity into its own
|
|
204
|
+
grid once.
|
|
205
|
+
|
|
206
|
+
### Animated tiles
|
|
207
|
+
|
|
208
|
+
```ruby
|
|
209
|
+
tileset.animations[37] # => [#<struct Frame tile_id=37, duration=250>, ...]
|
|
210
|
+
tileset.frame_local_id(37, 0) # => 37
|
|
211
|
+
tileset.frame_local_id(37, 250) # => 46
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
**Animate a tile in Tiled's tile animation editor.** `parse` reads each frame as a
|
|
215
|
+
`Tileset::Frame` with a `tile_id` (a local id) and a `duration` in milliseconds.
|
|
216
|
+
|
|
217
|
+
`frame_local_id(local, ms)` returns the local id to draw for tile `local` after
|
|
218
|
+
`ms` milliseconds. The animation loops. A tile without an animation returns
|
|
219
|
+
itself. The time is an argument, not a clock read, so pausing is "stop
|
|
220
|
+
accumulating".
|
|
221
|
+
|
|
222
|
+
**Tiled speaks milliseconds; the engine speaks seconds.** `TileWorld` accumulates
|
|
223
|
+
`elapsed` in seconds, and `TileMapRenderer` converts it once per draw, as
|
|
224
|
+
`(elapsed * 1000).to_i`. Convert the same way when you call `frame_local_id`
|
|
225
|
+
yourself.
|
|
226
|
+
|
|
227
|
+
## Testing against a map
|
|
228
|
+
|
|
229
|
+
`TileMapRenderer` draws any object that answers the tile map contract, and never
|
|
230
|
+
names `TileMap`. rgame's own suite states that contract in
|
|
231
|
+
`spec/support/shared_examples/a_tile_map.rb`. It checks both `TileMap` and the
|
|
232
|
+
spec stand-in `StubTileMap` against it. The contract covers `layer_count`,
|
|
233
|
+
`width`, `height`, `tile_width`, `tile_height`, `gid`, and a `tileset` answering
|
|
234
|
+
`local_id`, `animations` and `frame_local_id`.
|
|
235
|
+
|
|
236
|
+
A spec that needs a map but no files builds one [by hand](#building-a-map-by-hand),
|
|
237
|
+
or parses a `.tmx` string with `TileMap.parse`.
|