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
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Core
|
|
5
|
+
# Draws a tile map: the static layers baked once, the animated tiles drawn
|
|
6
|
+
# per frame, both culled to a rectangle of the world.
|
|
7
|
+
#
|
|
8
|
+
# tiles = RGame::Core::TileMapRenderer.new(map, tileset_images)
|
|
9
|
+
#
|
|
10
|
+
# map.layer_count.times do |layer|
|
|
11
|
+
# tiles.draw_layer(renderer, layer, cull_x, cull_y, cull_w, cull_h,
|
|
12
|
+
# elapsed: seconds)
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# ## It draws in world coordinates
|
|
16
|
+
#
|
|
17
|
+
# A tile at column 3 is drawn at `3 * tile_width`, and getting it onto the
|
|
18
|
+
# screen is the caller's transform — the same deal every other drawable
|
|
19
|
+
# gets. The rectangle passed in is therefore a **cull rect** and nothing
|
|
20
|
+
# else: which part of the world is worth drawing.
|
|
21
|
+
#
|
|
22
|
+
# It used to be both, offsetting the output by `-camera` as well as culling
|
|
23
|
+
# to it, which worked exactly as long as there was one camera. Under
|
|
24
|
+
# split-screen the same map is drawn through several, so a call that bakes
|
|
25
|
+
# placement into its output can only be right for one of them. Culling is
|
|
26
|
+
# genuinely per-camera; placement is the transform stack's job.
|
|
27
|
+
#
|
|
28
|
+
# ## One call per layer, because the actors go between them
|
|
29
|
+
#
|
|
30
|
+
# A layer is drawn on its own, and the order they are drawn in is the
|
|
31
|
+
# caller's. That is what lets a scene put its actors between two of them —
|
|
32
|
+
# tree trunks below, canopies above — which is the whole reason this does
|
|
33
|
+
# not simply draw the map in one go.
|
|
34
|
+
#
|
|
35
|
+
# It is also why nothing here consults the map's `above_layer?` flag any
|
|
36
|
+
# more: which layers cover the actors is a question about where the actors
|
|
37
|
+
# are in the scene, and Tiled already answers "in what order do the layers
|
|
38
|
+
# go" by listing them. `RGame::Engine::TileMapLayer` mounts one node per
|
|
39
|
+
# layer and the tree does the rest.
|
|
40
|
+
#
|
|
41
|
+
# ## What is baked and what is not
|
|
42
|
+
#
|
|
43
|
+
# Within each layer, every tile that is *not* animated is baked into one
|
|
44
|
+
# recording, the first time that layer is drawn. Scrolling a baked layer is
|
|
45
|
+
# then one call per texture however many thousand tiles went into it. The
|
|
46
|
+
# handful that *are* animated are drawn individually each frame, culled to
|
|
47
|
+
# the viewport — a map far larger than the screen costs only what is on it.
|
|
48
|
+
#
|
|
49
|
+
# Splitting per layer rather than into two bands bakes the same tiles into
|
|
50
|
+
# more recordings, not more vertices: the partition changed, the contents
|
|
51
|
+
# did not.
|
|
52
|
+
#
|
|
53
|
+
# ## It loads nothing and holds no clock
|
|
54
|
+
#
|
|
55
|
+
# The tiles arrive already sliced, so two maps sharing a tileset share one
|
|
56
|
+
# GPU upload — which is only true if something above pulled the image
|
|
57
|
+
# through the asset manager, and is why this class does not load its own.
|
|
58
|
+
#
|
|
59
|
+
# And `elapsed` is an argument rather than a clock read, so animation is
|
|
60
|
+
# something the caller advances. Pausing is "stop accumulating"; a spec
|
|
61
|
+
# picks the frame it wants. See CLAUDE.md, "`draw` renders state; time
|
|
62
|
+
# enters through `update`".
|
|
63
|
+
#
|
|
64
|
+
# ## What it needs of a map
|
|
65
|
+
#
|
|
66
|
+
# It never names the map's class — the tile map lives a layer *above* this
|
|
67
|
+
# one and Core may not reach up (CLAUDE.md, "The rule points both ways").
|
|
68
|
+
# What it calls is the 'a tile map' contract in
|
|
69
|
+
# `spec/support/shared_examples/`: `layer_count`, `width`, `height`,
|
|
70
|
+
# `tile_width`, `tile_height`, `gid`, and a `tileset` answering `local_id`,
|
|
71
|
+
# `animations` and `frame_local_id`.
|
|
72
|
+
class TileMapRenderer
|
|
73
|
+
# The map this was built from. A scene reads it for collision and world
|
|
74
|
+
# bounds, which are its business rather than this class's.
|
|
75
|
+
attr_reader :map
|
|
76
|
+
|
|
77
|
+
# `tiles` is the tileset image sliced into an Array indexed by local tile
|
|
78
|
+
# id — what `Image#tiles` returns.
|
|
79
|
+
def initialize(map, tiles)
|
|
80
|
+
@map = map
|
|
81
|
+
@tileset = map.tileset
|
|
82
|
+
@tiles = tiles
|
|
83
|
+
@animated = collect_animated_tiles
|
|
84
|
+
@static = Array.new(map.layer_count)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def layer_count = @map.layer_count
|
|
88
|
+
|
|
89
|
+
# One layer, culled to `(cull_x, cull_y, cull_width, cull_height)` in world
|
|
90
|
+
# coordinates and drawn in them.
|
|
91
|
+
#
|
|
92
|
+
# The recording is replayed at its own origin, so it lands wherever the
|
|
93
|
+
# caller's transform puts it. That also makes it **view-independent**: one
|
|
94
|
+
# bake serves every viewport, which is what keeps split-screen affordable
|
|
95
|
+
# and is why the bake is not keyed on a camera. Baking happens on the
|
|
96
|
+
# first draw, and it is safe to do that inside a transform or a clip —
|
|
97
|
+
# recording runs on its own canvas, begun at identity, and captures
|
|
98
|
+
# neither.
|
|
99
|
+
#
|
|
100
|
+
# No `z:`. A layer is drawn by a node of its own, so where it sits is the
|
|
101
|
+
# scene tree's answer; everything this issues belongs to that one node and
|
|
102
|
+
# goes in its slot.
|
|
103
|
+
#
|
|
104
|
+
# @api private
|
|
105
|
+
def draw_layer(renderer, index, cull_x, cull_y, cull_width, cull_height, elapsed: 0.0)
|
|
106
|
+
unless index.is_a?(Integer) && index >= 0 && index < @static.size
|
|
107
|
+
raise ArgumentError, "no layer #{index.inspect} in this map (it has #{@static.size})"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
@static[index] ||= bake(renderer, index)
|
|
111
|
+
@static[index].draw
|
|
112
|
+
draw_animated(renderer, @animated[index], cull_x, cull_y,
|
|
113
|
+
cull_width, cull_height, elapsed)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
private
|
|
117
|
+
|
|
118
|
+
def collect_animated_tiles
|
|
119
|
+
found = Array.new(@map.layer_count) { [] }
|
|
120
|
+
each_tile do |layer, col, row, local|
|
|
121
|
+
next unless @tileset.animations.key?(local)
|
|
122
|
+
|
|
123
|
+
found[layer] << [col, row, local]
|
|
124
|
+
end
|
|
125
|
+
found
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def each_tile
|
|
129
|
+
@map.layer_count.times do |layer|
|
|
130
|
+
@map.height.times do |row|
|
|
131
|
+
@map.width.times do |col|
|
|
132
|
+
gid = @map.gid(layer, col, row)
|
|
133
|
+
next if gid.zero?
|
|
134
|
+
|
|
135
|
+
yield(layer, col, row, @tileset.local_id(gid))
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def bake(renderer, index)
|
|
142
|
+
renderer.record do
|
|
143
|
+
each_tile do |layer, col, row, local|
|
|
144
|
+
next unless layer == index
|
|
145
|
+
next if @tileset.animations.key?(local)
|
|
146
|
+
|
|
147
|
+
renderer.image_at(@tiles[local], col * @map.tile_width, row * @map.tile_height)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def draw_animated(renderer, tiles, cull_x, cull_y, cull_width, cull_height, elapsed)
|
|
153
|
+
tile_width = @map.tile_width
|
|
154
|
+
tile_height = @map.tile_height
|
|
155
|
+
|
|
156
|
+
ms = (elapsed * 1000.0).to_i
|
|
157
|
+
|
|
158
|
+
col_start = cull_x.fdiv(tile_width).floor
|
|
159
|
+
row_start = cull_y.fdiv(tile_height).floor
|
|
160
|
+
col_end = (cull_x + cull_width).fdiv(tile_width).ceil
|
|
161
|
+
row_end = (cull_y + cull_height).fdiv(tile_height).ceil
|
|
162
|
+
|
|
163
|
+
tiles.each do |col, row, local|
|
|
164
|
+
next if col < col_start || col >= col_end || row < row_start || row >= row_end
|
|
165
|
+
|
|
166
|
+
renderer.image_at(@tiles[@tileset.frame_local_id(local, ms)],
|
|
167
|
+
col * tile_width, row * tile_height)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
require_relative 'image'
|
|
6
|
+
require_relative 'nine_slice'
|
|
7
|
+
|
|
8
|
+
module RGame
|
|
9
|
+
module Core
|
|
10
|
+
# One sheet of UI chrome, cut into named nine-slices and named images.
|
|
11
|
+
#
|
|
12
|
+
# atlas = RGame::Core::UiAtlas.load(app, 'media/ui.json')
|
|
13
|
+
# renderer.register_ui_atlas(atlas)
|
|
14
|
+
# renderer.nine_slice(:button_idle, x, y, width, height)
|
|
15
|
+
# renderer.image(:home, cx, cy)
|
|
16
|
+
#
|
|
17
|
+
# A button has four states, a panel has one, a scrollbar has three pieces —
|
|
18
|
+
# all of them small, and all of them cheaper as sub-rectangles of a single
|
|
19
|
+
# texture than as a dozen separate files. This is that texture plus a
|
|
20
|
+
# descriptor naming what is where.
|
|
21
|
+
#
|
|
22
|
+
# ## The descriptor
|
|
23
|
+
#
|
|
24
|
+
# {
|
|
25
|
+
# "image": "buttons.png",
|
|
26
|
+
# "scale": 3,
|
|
27
|
+
# "nine_slices": {
|
|
28
|
+
# "button_idle": { "x": 11, "y": 59, "w": 26, "h": 28, "border": 7 },
|
|
29
|
+
# "button_focus": { "x": 43, "y": 59, "w": 26, "h": 28, "border": 7 },
|
|
30
|
+
# "panel": { "x": 0, "y": 0, "w": 32, "h": 32,
|
|
31
|
+
# "border": { "left": 4, "right": 4, "top": 8, "bottom": 4 },
|
|
32
|
+
# "scale": 2 }
|
|
33
|
+
# },
|
|
34
|
+
# "images": {
|
|
35
|
+
# "home": { "x": 0, "y": 96, "w": 50, "h": 50 },
|
|
36
|
+
# "gear": { "x": 50, "y": 96, "w": 50, "h": 50 }
|
|
37
|
+
# }
|
|
38
|
+
# }
|
|
39
|
+
#
|
|
40
|
+
# `image` is resolved next to the descriptor. Each nine-slice entry is a
|
|
41
|
+
# source rectangle plus a border — a uniform integer or one value per side —
|
|
42
|
+
# and an optional `scale` overriding the sheet default. See
|
|
43
|
+
# RGame::Core::NineSlice for what those mean when it is drawn.
|
|
44
|
+
#
|
|
45
|
+
# Each `images` entry is a rectangle and nothing else: an icon is drawn
|
|
46
|
+
# whole, so it has no border, and no scale either, because how large to draw
|
|
47
|
+
# it is the draw call's `scale:`. Either section may be absent or `null`.
|
|
48
|
+
#
|
|
49
|
+
# ## Element names, not filenames
|
|
50
|
+
#
|
|
51
|
+
# Both sections are keyed by whatever the descriptor calls each element, and
|
|
52
|
+
# those names are what a widget asks for. That is why nine-slices are the
|
|
53
|
+
# one asset the renderer resolves by registration only: `:button_focus` is
|
|
54
|
+
# not a file and never can be. `Renderer#register_ui_atlas` registers every
|
|
55
|
+
# element of both kinds in one call.
|
|
56
|
+
#
|
|
57
|
+
# Parsing happens once, at load. Nothing here is touched again per frame.
|
|
58
|
+
class UiAtlas
|
|
59
|
+
# The nine-slice elements, by name. Values are `NineSlice`s.
|
|
60
|
+
attr_reader :nine_slices
|
|
61
|
+
|
|
62
|
+
# The image elements, by name. Values are `Image`s cut from the sheet.
|
|
63
|
+
attr_reader :images
|
|
64
|
+
|
|
65
|
+
# Loads a descriptor and the sheet beside it.
|
|
66
|
+
#
|
|
67
|
+
# The `AssetManager` uses `.new` instead, with an image it has already
|
|
68
|
+
# cached, so the sheet is shared with any other use of the same file.
|
|
69
|
+
def self.load(app, atlas_path)
|
|
70
|
+
data = JSON.parse(File.read(atlas_path), symbolize_names: true)
|
|
71
|
+
directory = File.dirname(File.expand_path(atlas_path))
|
|
72
|
+
new(Image.new(app, File.join(directory, data[:image])), data)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# `data` is the parsed descriptor; `image` an already-loaded sheet.
|
|
76
|
+
def initialize(image, data)
|
|
77
|
+
sheet_scale = data[:scale] || 1
|
|
78
|
+
|
|
79
|
+
@nine_slices = (data[:nine_slices] || {}).to_h do |id, spec|
|
|
80
|
+
[id, element(id) { build_nine_slice(image, spec, sheet_scale) }]
|
|
81
|
+
end
|
|
82
|
+
@images = (data[:images] || {}).to_h do |id, spec|
|
|
83
|
+
[id, element(id) { image.subimage(spec[:x], spec[:y], spec[:w], spec[:h]) }]
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def build_nine_slice(image, spec, sheet_scale)
|
|
90
|
+
NineSlice.new(image, x: spec[:x], y: spec[:y], w: spec[:w], h: spec[:h],
|
|
91
|
+
border: spec[:border], scale: spec[:scale] || sheet_scale)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def element(id)
|
|
95
|
+
yield
|
|
96
|
+
rescue StandardError => e
|
|
97
|
+
raise ArgumentError, "ui atlas element #{id.inspect}: #{e.message}"
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rgame/core_ext'
|
|
4
|
+
|
|
5
|
+
module RGame
|
|
6
|
+
module Core
|
|
7
|
+
# A synthetic game controller, for tests that need the real gamepad path
|
|
8
|
+
# with no hardware. It is C (`ext/rgame_core/input/virtual_gamepad.c`, bound
|
|
9
|
+
# in `ruby/virtual_gamepad_ext.c`), and it serves tests, not gameplay.
|
|
10
|
+
#
|
|
11
|
+
# pad = RGame::Core::VirtualGamepad.new # an App must be open
|
|
12
|
+
# pad.set_button(0, true) # SDL_CONTROLLER_BUTTON_A
|
|
13
|
+
# pad.set_axis(0, -32_768) # left stick fully left
|
|
14
|
+
# pad.detach # the App sees it unplugged
|
|
15
|
+
#
|
|
16
|
+
# SDL fabricates the device in-process, so the App seats it and raises its
|
|
17
|
+
# `gamepad_connected` and `gamepad_disconnected` callbacks as it would for a
|
|
18
|
+
# real pad. Buttons and axes take SDL's own numbers.
|
|
19
|
+
#
|
|
20
|
+
# It is part of the extension rather than a spec helper because only the
|
|
21
|
+
# extension is guaranteed to reach the SDL the App runs on, whether SDL is
|
|
22
|
+
# linked statically or dynamically.
|
|
23
|
+
class VirtualGamepad
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
data/lib/rgame/core.rb
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'version'
|
|
4
|
+
require_relative 'core/app'
|
|
5
|
+
require_relative 'core/locale'
|
|
6
|
+
require_relative 'core/input'
|
|
7
|
+
require_relative 'core/gamepad'
|
|
8
|
+
require_relative 'core/virtual_gamepad'
|
|
9
|
+
require_relative 'core/image'
|
|
10
|
+
require_relative 'core/renderer'
|
|
11
|
+
require_relative 'core/recording'
|
|
12
|
+
require_relative 'core/font'
|
|
13
|
+
require_relative 'core/audio'
|
|
14
|
+
require_relative 'core/sprite_sheet'
|
|
15
|
+
require_relative 'core/nine_slice'
|
|
16
|
+
require_relative 'core/ui_atlas'
|
|
17
|
+
require_relative 'core/tile_map_renderer'
|
|
18
|
+
require_relative 'core/asset_manager'
|
|
Binary file
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
# The registered colliders as a blocker source: the broadphase answers "what is near
|
|
6
|
+
# this box", and the same snapping arithmetic TileBlockers runs against a tile edge
|
|
7
|
+
# runs against a collider's edge instead. Pure — it needs no scene, no node and no
|
|
8
|
+
# tree, only something answering `query_box` and `reindex`.
|
|
9
|
+
#
|
|
10
|
+
# It answers the blocker-source question — "where does this box land moving dx" — that
|
|
11
|
+
# Engine::CollisionSystem asks of every source it holds; see that class's header for
|
|
12
|
+
# the protocol and for how the most restrictive answer wins across sources. This is
|
|
13
|
+
# the source that makes two actors stop each other the way a wall stops one.
|
|
14
|
+
#
|
|
15
|
+
# ## Per body, unlike the other sources
|
|
16
|
+
#
|
|
17
|
+
# A TileBlockers is the same grid for everybody, so one is shared by every actor on
|
|
18
|
+
# the map. This one takes an `owner` and a layer list, so it belongs to exactly one
|
|
19
|
+
# body: two actors declaring different `blocked_by` cannot share one. That is why
|
|
20
|
+
# Components::Mover builds its own resolver rather than borrowing the scene's.
|
|
21
|
+
#
|
|
22
|
+
# ## Three things a grid does not need
|
|
23
|
+
#
|
|
24
|
+
# - **The most restrictive candidate wins.** Several colliders may be in the way, so
|
|
25
|
+
# this keeps a running minimum (maximum, going the other way). That is
|
|
26
|
+
# dup-insensitive, which matters because the broadphase may offer the same collider
|
|
27
|
+
# once per cell the two share.
|
|
28
|
+
# - **An overlap that already exists is not resolved.** A step is blocked only if it
|
|
29
|
+
# *crosses* an edge the mover was on the near side of. Two actors that start
|
|
30
|
+
# overlapping stay overlapping and neither is teleported out — blocking stops the
|
|
31
|
+
# mover, it never moves the thing it hit.
|
|
32
|
+
# - **Boxes only.** A CircleCollider on a blocked layer is skipped, and still reports
|
|
33
|
+
# on_hit exactly as it does now. It is a documented limit rather than a raise at
|
|
34
|
+
# attach: layer membership is a runtime fact, so a check at attach would catch only
|
|
35
|
+
# the circles that already exist and quietly miss the ones spawned later.
|
|
36
|
+
class ActorBlockers
|
|
37
|
+
# `world` answers query_box/reindex (a Components::CollisionWorld); `owner` is the
|
|
38
|
+
# mover's own collider, excluded by identity so a body is never stopped by itself;
|
|
39
|
+
# `layers` is the set of collider layers that may stop it.
|
|
40
|
+
def initialize(world:, owner:, layers:)
|
|
41
|
+
@world = world
|
|
42
|
+
@owner = owner
|
|
43
|
+
@layers = Array(layers)
|
|
44
|
+
@blocker = nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Who produced the edge the last resolve_x / resolve_y returned, or nil when that
|
|
48
|
+
# axis was free. Per-move state, and safe because this source belongs to one body,
|
|
49
|
+
# which reads it inside its own update.
|
|
50
|
+
attr_reader :blocker
|
|
51
|
+
|
|
52
|
+
# Where the box's left edge lands moving dx, snapping flush against the nearest
|
|
53
|
+
# collider it would cross into.
|
|
54
|
+
def resolve_x(x, y, w, h, dx)
|
|
55
|
+
@blocker = nil
|
|
56
|
+
nx = x + dx
|
|
57
|
+
return nx if dx.zero?
|
|
58
|
+
|
|
59
|
+
sweep_x = dx.negative? ? nx : x
|
|
60
|
+
@best = nx
|
|
61
|
+
@world.query_box(sweep_x, y, w + dx.abs, h) do |other|
|
|
62
|
+
next unless candidate?(other)
|
|
63
|
+
next unless y < other.aabb_y + other.aabb_h && other.aabb_y < y + h
|
|
64
|
+
|
|
65
|
+
if dx.positive?
|
|
66
|
+
edge = other.aabb_x
|
|
67
|
+
take_min(edge - w, other) if x + w <= edge && nx + w > edge
|
|
68
|
+
else
|
|
69
|
+
edge = other.aabb_x + other.aabb_w
|
|
70
|
+
take_max(edge, other) if x >= edge && nx < edge
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
@best
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# The mirror of resolve_x. CollisionSystem feeds it the resolved x, which is what
|
|
77
|
+
# makes a diagonal push into a wall slide along it.
|
|
78
|
+
def resolve_y(x, y, w, h, dy)
|
|
79
|
+
@blocker = nil
|
|
80
|
+
ny = y + dy
|
|
81
|
+
return ny if dy.zero?
|
|
82
|
+
|
|
83
|
+
sweep_y = dy.negative? ? ny : y
|
|
84
|
+
@best = ny
|
|
85
|
+
@world.query_box(x, sweep_y, w, h + dy.abs) do |other|
|
|
86
|
+
next unless candidate?(other)
|
|
87
|
+
next unless x < other.aabb_x + other.aabb_w && other.aabb_x < x + w
|
|
88
|
+
|
|
89
|
+
if dy.positive?
|
|
90
|
+
edge = other.aabb_y
|
|
91
|
+
take_min(edge - h, other) if y + h <= edge && ny + h > edge
|
|
92
|
+
else
|
|
93
|
+
edge = other.aabb_y + other.aabb_h
|
|
94
|
+
take_max(edge, other) if y >= edge && ny < edge
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
@best
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Called by CollisionSystem#move once the resolved position is written back, with
|
|
101
|
+
# the box the step started from. Re-bucketing the owner here is what keeps the index
|
|
102
|
+
# exact for whoever resolves next this step; see CollisionWorld#reindex.
|
|
103
|
+
def moved(_actor, from_x, from_y, from_w, from_h)
|
|
104
|
+
@world.reindex(@owner, from_x, from_y, from_w, from_h)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
private
|
|
108
|
+
|
|
109
|
+
def take_min(landed, other)
|
|
110
|
+
return unless landed < @best
|
|
111
|
+
|
|
112
|
+
@best = landed
|
|
113
|
+
@blocker = other
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def take_max(landed, other)
|
|
117
|
+
return unless landed > @best
|
|
118
|
+
|
|
119
|
+
@best = landed
|
|
120
|
+
@blocker = other
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def candidate?(other)
|
|
124
|
+
!other.equal?(@owner) &&
|
|
125
|
+
other.is_a?(Components::BoxCollider) &&
|
|
126
|
+
@layers.include?(other.layer) &&
|
|
127
|
+
!other.node.freed?
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
# Pure animation math: given an atlas's animation table and an elapsed time,
|
|
6
|
+
# resolve which sprite-sheet cell (row, col) to show. No renderer, no images —
|
|
7
|
+
# fully testable.
|
|
8
|
+
#
|
|
9
|
+
# animations: { name => { row:, col:, frames:, fps:, flip_x: }, ... }
|
|
10
|
+
# row – sheet row
|
|
11
|
+
# col – starting column (default 0); lets an animation begin mid-row,
|
|
12
|
+
# e.g. a 1-frame "stand" pointing at the neutral middle pose
|
|
13
|
+
# frames – number of frames, advancing across columns from `col`
|
|
14
|
+
# fps – frames per second
|
|
15
|
+
# flip_x – draw mirrored horizontally
|
|
16
|
+
class AnimationSet
|
|
17
|
+
Anim = Struct.new(:row, :first_col, :frames, :frame_secs, :flip_x)
|
|
18
|
+
private_constant :Anim
|
|
19
|
+
|
|
20
|
+
def initialize(animations)
|
|
21
|
+
@anims = animations.each_with_object({}) do |(name, a), table|
|
|
22
|
+
table[name.to_sym] = Anim.new(a[:row], a[:col] || 0, a[:frames], 1.0 / a[:fps], a[:flip_x] || false)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Sheet row of `name` (fixed per animation).
|
|
27
|
+
def row(name)
|
|
28
|
+
@anims.fetch(name).row
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Sheet column of `name` at `elapsed` seconds — the only part that animates.
|
|
32
|
+
def col(name, elapsed)
|
|
33
|
+
anim = @anims.fetch(name)
|
|
34
|
+
anim.first_col + ((elapsed / anim.frame_secs).floor % anim.frames)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Whether `name` draws mirrored (fixed per animation).
|
|
38
|
+
def flip_x(name)
|
|
39
|
+
@anims.fetch(name).flip_x
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# [row, col, flip_x] for the given animation at `elapsed` seconds. A convenience
|
|
43
|
+
# for tests/inspection — it allocates an Array, so the per-frame draw path reads the
|
|
44
|
+
# row/col/flip_x accessors above directly instead.
|
|
45
|
+
def frame(name, elapsed)
|
|
46
|
+
[row(name), col(name, elapsed), flip_x(name)]
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
# Owns animation playback state for one actor: the AnimationSet plus the current
|
|
6
|
+
# animation name and elapsed time. Pure logic — the renderer asks it for the
|
|
7
|
+
# current [row, col, flip_x] cell. This is what makes an actor own its own
|
|
8
|
+
# animations instead of the scene passing an AnimationSet in every frame.
|
|
9
|
+
class Animator
|
|
10
|
+
def initialize(animation_set, initial: :stand)
|
|
11
|
+
@animation_set = animation_set
|
|
12
|
+
@current = initial
|
|
13
|
+
@elapsed = 0.0
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
attr_reader :current, :elapsed
|
|
17
|
+
|
|
18
|
+
# Switch to `name`; restarts elapsed time on an actual change (a no-op if the
|
|
19
|
+
# animation is already playing, so a continuing walk keeps cycling).
|
|
20
|
+
def play(name)
|
|
21
|
+
return if name == @current
|
|
22
|
+
|
|
23
|
+
@current = name
|
|
24
|
+
@elapsed = 0.0
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def update(dt)
|
|
28
|
+
@elapsed += dt
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Current animation's row/col/flip_x, resolved without allocating — the per-frame
|
|
32
|
+
# draw path reads these directly.
|
|
33
|
+
def row = @animation_set.row(@current)
|
|
34
|
+
def col = @animation_set.col(@current, @elapsed)
|
|
35
|
+
def flip_x = @animation_set.flip_x(@current)
|
|
36
|
+
|
|
37
|
+
# [row, col, flip_x] for the current animation at the current elapsed time. A
|
|
38
|
+
# convenience for tests/inspection; it allocates, so draw uses the accessors above.
|
|
39
|
+
def frame
|
|
40
|
+
@animation_set.frame(@current, @elapsed)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
# A global, always-present audio bus: gameplay emits audio *facts* here (decoupled
|
|
6
|
+
# from any actual playback), and an AudioDirector turns them into sound. A module
|
|
7
|
+
# rather than an instance so any node can reach it without wiring.
|
|
8
|
+
module AudioBus
|
|
9
|
+
@on_play_sound = Signal.define(:id).new
|
|
10
|
+
@on_play_music = Signal.define(:id).new
|
|
11
|
+
@on_stop_music = Signal.define.new
|
|
12
|
+
|
|
13
|
+
def self.on_play_sound = @on_play_sound
|
|
14
|
+
def self.on_play_music = @on_play_music
|
|
15
|
+
def self.on_stop_music = @on_stop_music
|
|
16
|
+
|
|
17
|
+
# Emit shims so callers say `AudioBus.play_sound(:boom)` rather than reaching
|
|
18
|
+
# through the signal. The signal is still the subscription seam (AudioDirector).
|
|
19
|
+
def self.play_sound(id) = @on_play_sound.emit(id)
|
|
20
|
+
def self.play_music(id) = @on_play_music.emit(id)
|
|
21
|
+
def self.stop_music = @on_stop_music.emit
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
# The registered handler that turns audio *events* into audio *playback*.
|
|
6
|
+
# Gameplay emits facts — `:play_sound`/`:play_music`/`:stop_music` with the asset
|
|
7
|
+
# id as payload — through the EventDispatcher, never touching the backend; this
|
|
8
|
+
# observer forwards each to an injected audio server (anything responding to
|
|
9
|
+
# #play_sound/#play_music/#stop_music: `RGame::Core::Audio` in a game, a recording
|
|
10
|
+
# fake in specs). Pure logic — it names no audio class, and the 'an audio server'
|
|
11
|
+
# contract in spec/support/shared_examples/ is the interface it calls, so a
|
|
12
|
+
# stand-in is checked against what the real device does.
|
|
13
|
+
class AudioDirector
|
|
14
|
+
def initialize(audio)
|
|
15
|
+
@audio = audio
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Subscribe to the audio events on a hub (the global AudioBus by default, but
|
|
19
|
+
# any object exposing the same three signals). Returns self, so a caller can
|
|
20
|
+
# build and subscribe in one expression.
|
|
21
|
+
#
|
|
22
|
+
# **The handles are kept, so `unsubscribe` can take them back off.** AudioBus
|
|
23
|
+
# is a module — one hub for the whole process — and it holds its listeners
|
|
24
|
+
# until something removes them. A connected director is therefore reachable
|
|
25
|
+
# from a global, and it keeps the audio device alive, along with the asset
|
|
26
|
+
# manager that device resolves paths through and the App that manager loads
|
|
27
|
+
# images for, down to the window.
|
|
28
|
+
#
|
|
29
|
+
# A game never notices, because its App lives as long as the process does.
|
|
30
|
+
# Anywhere an App is created and discarded — a spec, a tool,
|
|
31
|
+
# `tools/drive_test_project.rb` — it is the difference between a window
|
|
32
|
+
# closing and a window leaking. RGame::Game subscribes a director when it
|
|
33
|
+
# starts and releases it when the loop ends, which is why a game calls
|
|
34
|
+
# neither method itself.
|
|
35
|
+
def subscribe(audio_bus = Engine::AudioBus)
|
|
36
|
+
@bus = audio_bus
|
|
37
|
+
@handles = [
|
|
38
|
+
audio_bus.on_play_sound.connect { @audio.play_sound(it) },
|
|
39
|
+
audio_bus.on_play_music.connect { @audio.play_music(it) },
|
|
40
|
+
audio_bus.on_stop_music.connect { @audio.stop_music }
|
|
41
|
+
]
|
|
42
|
+
self
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Takes this director's listeners back off the hub. Safe to call twice, and
|
|
46
|
+
# safe on a director that was never subscribed — releasing something is not
|
|
47
|
+
# a good place to be strict about how many times it happens.
|
|
48
|
+
def unsubscribe
|
|
49
|
+
return self if @handles.nil?
|
|
50
|
+
|
|
51
|
+
@bus.on_play_sound.disconnect(@handles[0])
|
|
52
|
+
@bus.on_play_music.disconnect(@handles[1])
|
|
53
|
+
@bus.on_stop_music.disconnect(@handles[2])
|
|
54
|
+
@handles = nil
|
|
55
|
+
self
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|