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/systems.md
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# Systems & shared resources
|
|
2
|
+
|
|
3
|
+
Some things a node needs do not live on the node: a tile map, the world bounds, a
|
|
4
|
+
shared collision world. **Shared resources are systems. A system lives on an
|
|
5
|
+
anchor node, and other nodes reach it by walking the tree**, not through
|
|
6
|
+
constructor arguments. A system is an ordinary `RGame::Engine::Component` on a
|
|
7
|
+
boundary node, found with the same `get_component` every node has.
|
|
8
|
+
|
|
9
|
+
## Two scopes, two anchor nodes
|
|
10
|
+
|
|
11
|
+
**Scope belongs to the node you attach a system to**, not to the system.
|
|
12
|
+
|
|
13
|
+
- **Global scope → the root node.** Every node can reach the root, and it never
|
|
14
|
+
changes. Systems that live as long as the program are components on the root,
|
|
15
|
+
such as `Players` and `Viewports`: `node.root.get_component(RGame::Engine::Players)`.
|
|
16
|
+
- **Scene scope → the scene node**, the node `SceneStack` pushes. Systems that
|
|
17
|
+
live as long as a scene are components on *that* node, such as the collision
|
|
18
|
+
world or the tile world. They appear when the scene is pushed and go when it is
|
|
19
|
+
popped: `node.scene.get_component(CollisionWorld)`.
|
|
20
|
+
|
|
21
|
+
## The anchors
|
|
22
|
+
|
|
23
|
+
**Both anchors are methods that walk the parent chain**, not cached fields. A
|
|
24
|
+
back-link cached at add time would go stale for a node built before it is
|
|
25
|
+
mounted: its children would cache the wrong root. Resolving on every access
|
|
26
|
+
cannot go stale.
|
|
27
|
+
|
|
28
|
+
- `root` is `@parent ? @parent.root : self`. The top-most node is its own root.
|
|
29
|
+
- `scene` is the nearest ancestor marked as a scene boundary. `SceneStack#push`
|
|
30
|
+
marks the pushed scene with `scene.scene = scene`, and descendants resolve up to
|
|
31
|
+
it. Outside any scene, `scene` is `nil`.
|
|
32
|
+
|
|
33
|
+
### Looking a system up
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
node.system(CollisionWorld)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**`Node2D#system(klass)` checks the scene first, then the root.** A scene can
|
|
40
|
+
therefore override a global default, and nodes outside any scene still find
|
|
41
|
+
globals. To mean one scope specifically, use its anchor:
|
|
42
|
+
`node.root.get_component` or `node.scene.get_component`.
|
|
43
|
+
|
|
44
|
+
### Ask for a contract, not a class
|
|
45
|
+
|
|
46
|
+
**The lookup matches by ancestry**, so `klass` can be a module the system
|
|
47
|
+
includes instead of its own class. One question can then have several answers.
|
|
48
|
+
|
|
49
|
+
"How big is the world" shows this. A flat game mounts
|
|
50
|
+
[`Components::World`](components.md#world). A tile game mounts
|
|
51
|
+
[`Components::TileWorld`](components.md#tileworld), which derives the same two
|
|
52
|
+
numbers from its map. Both include `Components::WorldBounds`, so a component
|
|
53
|
+
that needs bounds asks for the *contract*:
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
def on_attach
|
|
57
|
+
world = node.system(RGame::Engine::Components::WorldBounds)
|
|
58
|
+
@width = world.world_width
|
|
59
|
+
@height = world.world_height
|
|
60
|
+
end
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`ScreenWrap` and `DespawnOffscreen` work this way. They run unchanged in either
|
|
64
|
+
kind of scene and never learn which one they are in. Naming the contract keeps
|
|
65
|
+
the two implementations from drifting apart.
|
|
66
|
+
|
|
67
|
+
## Registering with a system: use the lifecycle, not `initialize`
|
|
68
|
+
|
|
69
|
+
**A system and its clients connect in the tree-lifecycle hooks**, never in
|
|
70
|
+
`initialize`. They can only connect once everything is in the live tree, and a
|
|
71
|
+
node under construction has no anchors. See
|
|
72
|
+
[Lifecycle](scene_graph.md#lifecycle-constructing-vs-entering-the-tree).
|
|
73
|
+
|
|
74
|
+
The entered-tree cascade fixes an order that makes this safe. The scene's own
|
|
75
|
+
components run `on_attach` first, so a `CollisionWorld` on the scene node exists.
|
|
76
|
+
Then the scene's `on_add` runs, then its children enter. By the time a child
|
|
77
|
+
collider attaches, the scene-scoped system it looks up is already there.
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
# CircleCollider (engine/components/circle_collider.rb) registers itself when it
|
|
81
|
+
# enters the tree and releases the registration when it leaves — the engine fires
|
|
82
|
+
# both hooks, so a spawned/despawned entity can't leak a registration.
|
|
83
|
+
class CircleCollider < RGame::Engine::Component
|
|
84
|
+
def on_attach = node.system(CollisionWorld)&.register(self)
|
|
85
|
+
def on_detach = node.system(CollisionWorld)&.unregister(self)
|
|
86
|
+
end
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**A collider tolerates a missing world; most clients should not.** Both hooks
|
|
90
|
+
above use `&.`. A collider in a scene without a `CollisionWorld` is a shape that
|
|
91
|
+
reports nothing. A tile-only game wants exactly that: its character carries a
|
|
92
|
+
feet box to be *stopped* by (see [`Mover`](components.md#mover)), and there are no
|
|
93
|
+
pairs to find. The cost is that an `on_hit` handler in such a scene never fires,
|
|
94
|
+
and nothing reports it. Weigh that deliberately. A client that is useless without
|
|
95
|
+
its system raises instead, as a mover's `blocked_by:` does.
|
|
96
|
+
|
|
97
|
+
## The two systems `Game` mounts
|
|
98
|
+
|
|
99
|
+
**`RGame::Game` puts two systems on the root before the tree goes live.** Any node
|
|
100
|
+
can reach them without the game wiring anything:
|
|
101
|
+
|
|
102
|
+
| | |
|
|
103
|
+
|---|---|
|
|
104
|
+
| `node.system(RGame::Engine::Players)` | who is playing — devices, bindings, cameras, and who a newly used controller belongs to |
|
|
105
|
+
| `node.system(RGame::Engine::Viewports)` | how the screen is divided — one `View` per active player, and collapsing the split |
|
|
106
|
+
|
|
107
|
+
They are ordinary root-scoped systems, mounted the way a game mounts its own. A
|
|
108
|
+
scene that needs a camera to follow asks `Players` (`players.primary.camera`). A
|
|
109
|
+
cutscene that collapses the split asks `Viewports` (`viewports.solo!(camera)`).
|
|
110
|
+
Both work from anywhere in the tree, with nothing passed in. That reach is why
|
|
111
|
+
they are systems and not objects `Game` hands down.
|
|
112
|
+
|
|
113
|
+
See [Input](input.md#players-seats-and-joining) and
|
|
114
|
+
[Scene graph](scene_graph.md#viewports-and-views).
|
|
115
|
+
|
|
116
|
+
## Collision: two indexes, one resolver
|
|
117
|
+
|
|
118
|
+
Collision is the largest structure built from systems. Three pages cover its
|
|
119
|
+
parts: the [components](components.md#boxcollider) a node carries, the
|
|
120
|
+
[systems](#systems-that-index-their-clients-the-tag-registry-pattern) a scene
|
|
121
|
+
mounts, and the
|
|
122
|
+
[building blocks](internals.md#collisionsystem--move-an-actor-against-its-blockers)
|
|
123
|
+
underneath. This section shows how they fit together.
|
|
124
|
+
|
|
125
|
+
**A node has exactly one collision shape, and exactly one component owns it.** The
|
|
126
|
+
[collider](components.md#boxcollider) *is* the shape. A
|
|
127
|
+
[mover](components.md#mover) that wants to be stopped reads its sibling's box
|
|
128
|
+
instead of building a second one. The rectangle that stops a step is the
|
|
129
|
+
rectangle that reports a contact, so retuning one retunes both.
|
|
130
|
+
|
|
131
|
+
**Collision uses two indexes, on purpose.** A tile map is already an index: the
|
|
132
|
+
wall a step would hit follows from arithmetic on the step. A `TileWorld` divides
|
|
133
|
+
by the tile size and asks the grid. Actors have no such structure, so a
|
|
134
|
+
`CollisionWorld` buckets them into a
|
|
135
|
+
[`SpatialHash`](internals.md#spatialhash--uniform-grid-broadphase) each step.
|
|
136
|
+
Baking tile shapes into the broadphase would rebuild an index the grid already
|
|
137
|
+
is. It would also cost time every frame on a map of tens of thousands of tiles.
|
|
138
|
+
|
|
139
|
+
**The unification happens one level up.** A **blocker source** answers one
|
|
140
|
+
question over plain numbers: where does this box land when it moves `dx`? Three
|
|
141
|
+
sources exist:
|
|
142
|
+
|
|
143
|
+
- `TileBlockers`, over the grid;
|
|
144
|
+
- `ActorBlockers`, over the broadphase;
|
|
145
|
+
- `BoundsBlockers`, over the world's edges.
|
|
146
|
+
|
|
147
|
+
A blocked mover builds a
|
|
148
|
+
[`CollisionSystem`](internals.md#collisionsystem--move-an-actor-against-its-blockers)
|
|
149
|
+
at attach, from the sources its `blocked_by:` names. That system asks each source
|
|
150
|
+
and takes the most restrictive answer on each axis. The axis-separated order that
|
|
151
|
+
produces wall-sliding therefore exists **once**. An actor slides off a villager
|
|
152
|
+
exactly as it slides off a fence.
|
|
153
|
+
|
|
154
|
+
A source may also answer `travel?`: can this box move along a segment without
|
|
155
|
+
being stopped? `TileBlockers` does. A [`Navigator`](components.md#navigator) uses
|
|
156
|
+
it to check a route against the same resolver that will stop its walk.
|
|
157
|
+
|
|
158
|
+
| | Mounted on the scene | Owned by the node |
|
|
159
|
+
|---|---|---|
|
|
160
|
+
| Tiles | [`TileWorld`](components.md#tileworld), which hands out one shared `TileBlockers` | — |
|
|
161
|
+
| Actors | [`CollisionWorld`](components.md#collisionworld), the broadphase | an `ActorBlockers` per mover, holding its own collider and layer list |
|
|
162
|
+
| The world's edge | any [`WorldBounds`](components.md#world) | a `BoundsBlockers` |
|
|
163
|
+
| The step | — | one `CollisionSystem`, built at attach from the names above |
|
|
164
|
+
|
|
165
|
+
Neither system needs the other, and most scenes mount one. `examples/scroll_map`
|
|
166
|
+
has a map and no broadphase. `examples/collision` has a broadphase and no map.
|
|
167
|
+
`examples/collision_tiles` mounts both, and the difference shows only in the names
|
|
168
|
+
listed in `blocked_by`.
|
|
169
|
+
|
|
170
|
+
"What stops a mover" and "what a collider touches" remain two questions, and a
|
|
171
|
+
game sees the difference. See
|
|
172
|
+
[Blocking and overlapping](#blocking-and-overlapping-are-two-reports-and-a-pair-gets-one-of-them)
|
|
173
|
+
below, after the broadphase.
|
|
174
|
+
|
|
175
|
+
## Systems that index their clients (the tag-registry pattern)
|
|
176
|
+
|
|
177
|
+
**A many-to-many system keeps its own index of registered clients**, so it checks
|
|
178
|
+
only nearby candidates instead of walking the tree for every pair. Broadphase
|
|
179
|
+
collision works this way, on the scene node. `CollisionWorld`
|
|
180
|
+
(engine/components/collision_world.rb) holds a `SpatialHash`: a spatial index of
|
|
181
|
+
registered colliders, rebuilt each `update`. The index ignores shape, so
|
|
182
|
+
`CircleCollider` and `BoxCollider` share one and collide with each other. The
|
|
183
|
+
index holds node references only. It carries no component data and gives no
|
|
184
|
+
data-locality benefit; it is a lightweight registry.
|
|
185
|
+
|
|
186
|
+
**`CollisionWorld` ignores layers.** It reports every overlapping pair to both
|
|
187
|
+
colliders. The owning node decides what a contact *means* by reading the other
|
|
188
|
+
collider's `layer` tag:
|
|
189
|
+
|
|
190
|
+
```ruby
|
|
191
|
+
collider.on_hit { |other| queue_free if other.layer == :bullet } # in a Rock node
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
**A contact arrives as two edges, not as a state.** `on_hit` fires on the step a
|
|
195
|
+
pair starts overlapping. `on_separated` fires on the step it stops. Each fires
|
|
196
|
+
once per pair, with nothing in between. A handler can therefore count, play a
|
|
197
|
+
sound or spend a life without guarding itself. That is why the system keeps an
|
|
198
|
+
`Engine::ContactSet` per collider, instead of forwarding each step's broadphase
|
|
199
|
+
results.
|
|
200
|
+
|
|
201
|
+
`CollisionWorld` is a normal component on the scene node. It runs in the
|
|
202
|
+
`update` traversal, where its broadphase runs, and goes away with the scene.
|
|
203
|
+
`examples/collision` shows the whole loop: circles and crates registering,
|
|
204
|
+
overlapping and separating through this system.
|
|
205
|
+
|
|
206
|
+
### Blocking and overlapping are two reports, and a pair gets one of them
|
|
207
|
+
|
|
208
|
+
**The same broadphase answers a second question.** A
|
|
209
|
+
[mover](components.md#mover) that names a collider layer in `blocked_by:` is
|
|
210
|
+
*stopped* by every box with that layer. It stops flush against the box's edge, as
|
|
211
|
+
against a solid tile. One `BoxCollider` can thus be a wall to one actor and a
|
|
212
|
+
trigger for another. Who declared the layer decides which; the collider itself
|
|
213
|
+
says nothing.
|
|
214
|
+
|
|
215
|
+
**The two reports never both fire for one pair.** Blocking leaves the boxes
|
|
216
|
+
exactly touching. `CollisionBox.overlap?` uses the half-open span `[x, x + w)` on
|
|
217
|
+
purpose. On a grid, pieces on neighbouring squares border each other constantly,
|
|
218
|
+
and an inclusive test would report each as a contact. Measured on two 12×6 boxes:
|
|
219
|
+
touching exactly, `on_hit` does not fire; overlapping by half a pixel, it does.
|
|
220
|
+
That convention makes the two reports mutually exclusive by construction.
|
|
221
|
+
|
|
222
|
+
A blocked pair therefore reports **no** contact. A mover that must both stop and
|
|
223
|
+
react listens to `on_blocked`, not `on_hit`; see [`Mover`](components.md#mover).
|
|
224
|
+
The rule of thumb:
|
|
225
|
+
|
|
226
|
+
| The question | The report | Where it lives |
|
|
227
|
+
|---|---|---|
|
|
228
|
+
| what may I not pass through | `on_blocked` / `on_unblocked` | the mover that was stopped |
|
|
229
|
+
| what am I touching | `on_hit` / `on_separated` | both colliders of the pair |
|
|
230
|
+
|
|
231
|
+
`examples/collision_tiles` shows the first, and `examples/collision` the second.
|
data/docs/api/text.md
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Text
|
|
2
|
+
|
|
3
|
+
**The renderer comes with a font, and `text` draws with it.** Most games need
|
|
4
|
+
nothing more:
|
|
5
|
+
|
|
6
|
+
```ruby
|
|
7
|
+
require 'rgame'
|
|
8
|
+
require 'rgame/core'
|
|
9
|
+
|
|
10
|
+
class MyGame < RGame::Core::App
|
|
11
|
+
def initialize
|
|
12
|
+
super(width: 800, height: 600, caption: 'demo')
|
|
13
|
+
@renderer = RGame::Core::Renderer.new(self)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def draw
|
|
17
|
+
@renderer.text('Score: 1200', 10, 10)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
MyGame.new.run
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Where text goes
|
|
25
|
+
|
|
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.
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
renderer.text(string, x, y, z: 10, color: nil, font: nil)
|
|
32
|
+
renderer.text_width(string, font: nil) # => Float — pixels
|
|
33
|
+
renderer.text_height(font: nil) # => Integer — the line height
|
|
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`:
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
lines.each_with_index do |line, i|
|
|
46
|
+
@renderer.text(line, 10, 10 + (i * @renderer.text_height))
|
|
47
|
+
end
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**`text_width` and `text` agree.** They run the same code, so a label centred by
|
|
51
|
+
its measured width lands exactly there:
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
@renderer.text(label, (width - @renderer.text_width(label)) / 2, 20)
|
|
55
|
+
```
|
|
56
|
+
|
|
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
|
+
```
|
|
69
|
+
|
|
70
|
+
## Fonts
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
font = RGame::Core::Font.new(app, 18) # the shipped font
|
|
74
|
+
font = RGame::Core::Font.new(app, 18, path: 'assets/pixel.ttf')
|
|
75
|
+
|
|
76
|
+
font.height # => 18
|
|
77
|
+
font.text_width('Hello') # => 38.7
|
|
78
|
+
|
|
79
|
+
renderer.text('Hello', 10, 10, font: font)
|
|
80
|
+
```
|
|
81
|
+
|
|
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.
|
|
85
|
+
|
|
86
|
+
The renderer builds its own 18px font on first use. Replace it, and every `text`
|
|
87
|
+
call without a `font:` follows:
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
@renderer.font = RGame::Core::Font.new(self, 24)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
A file that is unreadable or not a TrueType font raises
|
|
94
|
+
`RGame::Core::Font::LoadError`, naming the path.
|
|
95
|
+
|
|
96
|
+
### The default font, and what it covers
|
|
97
|
+
|
|
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.
|
|
100
|
+
|
|
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.
|
|
105
|
+
|
|
106
|
+
| | |
|
|
107
|
+
|---|---|
|
|
108
|
+
| Covers | English, German, French, Italian, Spanish, Portuguese, Nordic, Polish — in full, including `ß`, `ẞ`, `« »`, curly quotes and `€`. Greek and Cyrillic too. |
|
|
109
|
+
| Does not cover | CJK, Arabic, Hebrew, Devanagari. Pass your own font file for those; no font of this size includes them. |
|
|
110
|
+
|
|
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.
|
|
114
|
+
|
|
115
|
+
## What it costs
|
|
116
|
+
|
|
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.
|
|
122
|
+
|
|
123
|
+
A font that is only measured, never drawn, uses no video memory.
|
|
124
|
+
|
|
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.
|
|
128
|
+
|
|
129
|
+
## What is not here
|
|
130
|
+
|
|
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`.
|