rgame 0.2.0 → 0.3.0
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 +135 -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 +2 -3
- 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 +152 -11
- 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
|
@@ -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
|
|
@@ -15,6 +15,7 @@ module RGame
|
|
|
15
15
|
# flip_x – draw mirrored horizontally
|
|
16
16
|
class AnimationSet
|
|
17
17
|
Anim = Struct.new(:row, :first_col, :frames, :frame_secs, :flip_x)
|
|
18
|
+
private_constant :Anim
|
|
18
19
|
|
|
19
20
|
def initialize(animations)
|
|
20
21
|
@anims = animations.each_with_object({}) do |(name, a), table|
|
|
@@ -15,13 +15,43 @@ module RGame
|
|
|
15
15
|
@audio = audio
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
# Subscribe to the audio events on a hub (the global
|
|
19
|
-
#
|
|
20
|
-
#
|
|
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.
|
|
21
35
|
def subscribe(audio_bus = Engine::AudioBus)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
25
55
|
self
|
|
26
56
|
end
|
|
27
57
|
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
# The edges of the world as a blocker source: a box may not leave the region the
|
|
6
|
+
# scene's Components::WorldBounds describes. Pure — it is four numbers and the same
|
|
7
|
+
# snapping arithmetic every other source uses.
|
|
8
|
+
#
|
|
9
|
+
# It answers the blocker-source question Engine::CollisionSystem asks of every source
|
|
10
|
+
# it holds; see that class's header for the protocol.
|
|
11
|
+
#
|
|
12
|
+
# ## Why the world edge is declared rather than automatic
|
|
13
|
+
#
|
|
14
|
+
# Stopping at the edge is one of three responses to it. ScreenWrap and DespawnOffscreen
|
|
15
|
+
# are the other two, and a node may carry only one of them
|
|
16
|
+
# (Components::WorldBounds.one_response!). A clamp applied to every step would be a
|
|
17
|
+
# response nobody chose, contradicting a wrap or a despawn on every node that has one.
|
|
18
|
+
# So a game that wants the world edge to stop an actor says so, and gets an ordinary
|
|
19
|
+
# blocker with a layer like any other.
|
|
20
|
+
class BoundsBlockers
|
|
21
|
+
# What a BoundsBlockers reports as having stopped a step: one object for the life of
|
|
22
|
+
# the process, answering the same two questions a collider does, so a handler reads
|
|
23
|
+
# `by.layer` whatever stopped it. The world's edge has no node.
|
|
24
|
+
class Bounds
|
|
25
|
+
def layer = :bounds
|
|
26
|
+
def node = nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
BOUNDS = Bounds.new.freeze
|
|
30
|
+
|
|
31
|
+
# `bounds` is anything answering world_width / world_height — a Components::World or
|
|
32
|
+
# a Components::TileWorld. The two numbers are read once, here, because WorldBounds
|
|
33
|
+
# is documented immutable: a world that genuinely changes size is a new scene.
|
|
34
|
+
def initialize(bounds:)
|
|
35
|
+
@world_width = bounds.world_width
|
|
36
|
+
@world_height = bounds.world_height
|
|
37
|
+
@blocker = nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# The world's edge stopped the last resolve on this axis, or nil when it did not.
|
|
41
|
+
attr_reader :blocker
|
|
42
|
+
|
|
43
|
+
# The far axis is nothing to a rectangle: a box leaves the world on one axis
|
|
44
|
+
# independently of where it is on the other, so `_y` and `_h` go unread here and
|
|
45
|
+
# `_x` and `_w` in the mirror. The five arguments are the protocol's, not this
|
|
46
|
+
# source's need.
|
|
47
|
+
def resolve_x(x, _y, w, _h, dx) = resolve(x + dx, w, @world_width, dx)
|
|
48
|
+
def resolve_y(_x, y, _w, h, dy) = resolve(y + dy, h, @world_height, dy)
|
|
49
|
+
|
|
50
|
+
# The world does not move, so there is nothing to re-index.
|
|
51
|
+
def moved(_actor, _from_x, _from_y, _w, _h) = nil
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def resolve(landed, span, extent, delta)
|
|
56
|
+
@blocker = nil
|
|
57
|
+
return landed if delta.zero?
|
|
58
|
+
|
|
59
|
+
limit = extent - span
|
|
60
|
+
limit = 0.0 if limit.negative?
|
|
61
|
+
|
|
62
|
+
if landed.negative?
|
|
63
|
+
@blocker = BOUNDS
|
|
64
|
+
0.0
|
|
65
|
+
elsif landed > limit
|
|
66
|
+
@blocker = BOUNDS
|
|
67
|
+
limit.to_f
|
|
68
|
+
else
|
|
69
|
+
landed
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
data/lib/rgame/engine/camera.rb
CHANGED
|
@@ -7,7 +7,7 @@ module RGame
|
|
|
7
7
|
# transform, never baked into a node.
|
|
8
8
|
#
|
|
9
9
|
# camera = Camera.new(world_width: map.pixel_width, world_height: map.pixel_height)
|
|
10
|
-
# camera.center_on(player.
|
|
10
|
+
# camera.center_on(player.world_x, player.world_y) # in update, every tick
|
|
11
11
|
# camera.resolve(view_width, view_height) # at draw, for one viewport
|
|
12
12
|
# camera.x, camera.y # the offset to translate by
|
|
13
13
|
#
|
|
@@ -66,10 +66,10 @@ module RGame
|
|
|
66
66
|
private
|
|
67
67
|
|
|
68
68
|
def clamp(value, world_size, view_size)
|
|
69
|
-
return value.to_f if world_size.nil?
|
|
69
|
+
return value.to_f if world_size.nil?
|
|
70
70
|
|
|
71
71
|
max = world_size - view_size
|
|
72
|
-
return 0.0 if max <= 0
|
|
72
|
+
return 0.0 if max <= 0
|
|
73
73
|
|
|
74
74
|
value.clamp(0.0, max.to_f)
|
|
75
75
|
end
|
|
@@ -9,12 +9,14 @@ module RGame
|
|
|
9
9
|
class CircleCollider
|
|
10
10
|
attr_reader :radius
|
|
11
11
|
|
|
12
|
-
# Squared-distance overlap test (no sqrt): true when two circles
|
|
12
|
+
# Squared-distance overlap test (no sqrt): true when two circles overlap in area.
|
|
13
|
+
# Circles that exactly graze are apart, the same half-open convention CollisionBox
|
|
14
|
+
# uses — one meaning of "contact" across both shapes and the pairs that mix them.
|
|
13
15
|
def self.overlap?(x1, y1, r1, x2, y2, r2)
|
|
14
16
|
dx = x2 - x1
|
|
15
17
|
dy = y2 - y1
|
|
16
18
|
sum = r1 + r2
|
|
17
|
-
(dx * dx) + (dy * dy)
|
|
19
|
+
(dx * dx) + (dy * dy) < sum * sum
|
|
18
20
|
end
|
|
19
21
|
|
|
20
22
|
def initialize(radius:)
|
|
@@ -5,6 +5,10 @@ module RGame
|
|
|
5
5
|
# A character's collision rectangle, expressed as an offset + size relative to
|
|
6
6
|
# the actor's top-left origin (the sprite's top-left). Decoupled from sprite
|
|
7
7
|
# size: a 32x32 sprite can have a small 16x16 box at its feet.
|
|
8
|
+
#
|
|
9
|
+
# It is also where rectangle geometry lives, the way CircleCollider owns circle
|
|
10
|
+
# geometry: the overlap tests below are class methods over plain numbers, so the
|
|
11
|
+
# per-frame collision path can call them without building a box first.
|
|
8
12
|
class CollisionBox
|
|
9
13
|
attr_reader :offset_x, :offset_y, :width, :height
|
|
10
14
|
|
|
@@ -18,7 +22,28 @@ module RGame
|
|
|
18
22
|
)
|
|
19
23
|
end
|
|
20
24
|
|
|
21
|
-
|
|
25
|
+
# Rect-vs-rect: true when two world-space AABBs overlap in *area*. A shape spans
|
|
26
|
+
# the half-open box [x, x + w), so shapes that merely share an edge are apart —
|
|
27
|
+
# the same convention SDL_HasIntersection uses, and the one thing that makes a
|
|
28
|
+
# grid work: pieces on neighbouring squares border each other constantly, and an
|
|
29
|
+
# inclusive test would report every one of those as a contact.
|
|
30
|
+
def self.overlap?(x1, y1, w1, h1, x2, y2, w2, h2)
|
|
31
|
+
x1 < x2 + w2 && x2 < x1 + w1 && y1 < y2 + h2 && y2 < y1 + h1
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Rect-vs-circle: true when the circle at (cx, cy) overlaps the rect. Clamping
|
|
35
|
+
# the centre into the rect gives the rect's nearest point to it, so the corner
|
|
36
|
+
# case and the edge case are the same two lines — no per-region analysis, and
|
|
37
|
+
# no sqrt (compare squared distances). Grazing is not overlapping, as above.
|
|
38
|
+
def self.overlap_circle?(x, y, w, h, cx, cy, r)
|
|
39
|
+
nx = cx.clamp(x, x + w)
|
|
40
|
+
ny = cy.clamp(y, y + h)
|
|
41
|
+
dx = cx - nx
|
|
42
|
+
dy = cy - ny
|
|
43
|
+
(dx * dx) + (dy * dy) < r * r
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def initialize(width:, height:, offset_x: 0, offset_y: 0)
|
|
22
47
|
@offset_x = offset_x
|
|
23
48
|
@offset_y = offset_y
|
|
24
49
|
@width = width
|
|
@@ -3,41 +3,129 @@
|
|
|
3
3
|
module RGame
|
|
4
4
|
module Engine
|
|
5
5
|
# The actor-facing collision system: moves any actor by a delta, resolving its
|
|
6
|
-
# *collision box* (not its sprite) against
|
|
7
|
-
#
|
|
8
|
-
# any NPC.
|
|
6
|
+
# *collision box* (not its sprite) against every blocker source it holds. Reusable by
|
|
7
|
+
# the player and any NPC.
|
|
9
8
|
#
|
|
9
|
+
# **Everything that can stop a step is a source**, including the edge of the world —
|
|
10
|
+
# see Engine::BoundsBlockers, and its header for why that used to be an unconditional
|
|
11
|
+
# clamp here and is not any more. Nothing holds an actor anywhere it did not ask to
|
|
12
|
+
# be held.
|
|
13
|
+
#
|
|
14
|
+
# ## What a blocker source is
|
|
15
|
+
#
|
|
16
|
+
# A blocker source answers one question, on one axis, over plain numbers:
|
|
17
|
+
#
|
|
18
|
+
# source.resolve_x(x, y, w, h, dx) # -> where the box's left edge lands moving dx
|
|
19
|
+
# source.resolve_y(x, y, w, h, dy) # -> where the box's top edge lands moving dy
|
|
20
|
+
#
|
|
21
|
+
# Engine::TileBlockers divides by the tile size and snaps flush against a solid tile;
|
|
22
|
+
# Engine::ActorBlockers queries a broadphase and snaps flush against another
|
|
23
|
+
# collider's edge. Neither needs to know the other exists, because this system takes
|
|
24
|
+
# the **most restrictive** answer on each axis: a box hemmed in by a wall on one side
|
|
25
|
+
# and something else nearer stops at whichever is nearer.
|
|
26
|
+
#
|
|
27
|
+
# Two more questions a source answers, and both are asked here so that no caller has
|
|
28
|
+
# anything to remember:
|
|
29
|
+
#
|
|
30
|
+
# source.blocker # -> what produced the edge the last resolve returned, or nil
|
|
31
|
+
# source.moved(actor, from_x, from_y, w, h) # -> the step has been written back
|
|
32
|
+
#
|
|
33
|
+
# `blocker` is how #blocked_x / #blocked_y name what stopped a step; `moved` is how a
|
|
34
|
+
# source over a moving index re-buckets the mover, with the box the step started from.
|
|
35
|
+
#
|
|
36
|
+
# A fourth question is optional, and only Engine::TileBlockers answers it today:
|
|
37
|
+
#
|
|
38
|
+
# source.travel?(x, y, w, h, dx, dy) # -> can the box move (dx, dy) without being stopped?
|
|
39
|
+
#
|
|
40
|
+
# "Stopped" means what it means here: a resolve landing *short* of where the step was
|
|
41
|
+
# heading. A landing past it is ignored below, so it does not stop a travel either. Read
|
|
42
|
+
# that way the question splits by source — a box travels past several sources exactly when
|
|
43
|
+
# it travels past each one alone — so no source needs to know the others exist for this
|
|
44
|
+
# question any more than for the other three. The shared example group "a blocker source
|
|
45
|
+
# answering travel?" states it.
|
|
46
|
+
#
|
|
47
|
+
# Holding the sources here rather than inside any one of them is what puts the
|
|
48
|
+
# axis-separated order — resolve X, then resolve Y fed the resolved X — in exactly
|
|
49
|
+
# one place. That order is what produces wall-sliding (a diagonal push into a wall
|
|
50
|
+
# keeps the component that is still free), so every source shares one feel rather
|
|
51
|
+
# than each implementing it.
|
|
10
52
|
class CollisionSystem
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
53
|
+
# What stopped the last #move on each axis, or nil when that axis was free. Whatever
|
|
54
|
+
# a source reports, so a handler reads `blocked_x.layer` without asking which kind
|
|
55
|
+
# of thing it was. Set by every #move, so read it straight after one.
|
|
56
|
+
attr_reader :blocked_x, :blocked_y
|
|
57
|
+
|
|
58
|
+
def initialize(blockers: [])
|
|
59
|
+
@blockers = Array(blockers)
|
|
60
|
+
@blocked_x = nil
|
|
61
|
+
@blocked_y = nil
|
|
15
62
|
end
|
|
16
63
|
|
|
17
64
|
# Move `actor` by (dx, dy), writing the resolved position back to it.
|
|
18
65
|
def move(actor, dx, dy)
|
|
19
66
|
box = actor.collision_box
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
bx = actor.x + box.offset_x
|
|
23
|
-
by = actor.y + box.offset_y
|
|
67
|
+
from_x = actor.x + box.offset_x
|
|
68
|
+
from_y = actor.y + box.offset_y
|
|
24
69
|
bw = box.width
|
|
25
70
|
bh = box.height
|
|
26
71
|
|
|
27
|
-
bx =
|
|
28
|
-
by =
|
|
29
|
-
|
|
30
|
-
# Clamp the box inside the world. Floor each upper bound at 0 without a [span, 0]
|
|
31
|
-
# array (this runs per actor per frame).
|
|
32
|
-
max_x = @world_width - bw
|
|
33
|
-
max_x = 0 if max_x.negative?
|
|
34
|
-
max_y = @world_height - bh
|
|
35
|
-
max_y = 0 if max_y.negative?
|
|
36
|
-
bx = bx.clamp(0.0, max_x.to_f)
|
|
37
|
-
by = by.clamp(0.0, max_y.to_f)
|
|
72
|
+
bx = resolve_x(from_x, from_y, bw, bh, dx)
|
|
73
|
+
by = resolve_y(bx, from_y, bw, bh, dy)
|
|
38
74
|
|
|
39
75
|
actor.x = bx - box.offset_x
|
|
40
76
|
actor.y = by - box.offset_y
|
|
77
|
+
|
|
78
|
+
i = 0
|
|
79
|
+
while i < @blockers.size
|
|
80
|
+
@blockers[i].moved(actor, from_x, from_y, bw, bh)
|
|
81
|
+
i += 1
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Where the box's left edge lands moving dx, against every source at once. The most
|
|
86
|
+
# restrictive answer is the *smallest* landing for a rightward step and the largest
|
|
87
|
+
# for a leftward one; with no sources at all the box simply moves.
|
|
88
|
+
#
|
|
89
|
+
# A plain index loop rather than @blockers.map/min: this runs per actor per axis per
|
|
90
|
+
# frame, and both the block and the intermediate array would allocate.
|
|
91
|
+
def resolve_x(x, y, w, h, dx)
|
|
92
|
+
@blocked_x = nil
|
|
93
|
+
nx = x + dx
|
|
94
|
+
return nx if dx.zero?
|
|
95
|
+
|
|
96
|
+
winner = nil
|
|
97
|
+
i = 0
|
|
98
|
+
while i < @blockers.size
|
|
99
|
+
source = @blockers[i]
|
|
100
|
+
landed = source.resolve_x(x, y, w, h, dx)
|
|
101
|
+
if dx.positive? ? landed < nx : landed > nx
|
|
102
|
+
nx = landed
|
|
103
|
+
winner = source
|
|
104
|
+
end
|
|
105
|
+
i += 1
|
|
106
|
+
end
|
|
107
|
+
@blocked_x = winner&.blocker
|
|
108
|
+
nx
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def resolve_y(x, y, w, h, dy)
|
|
112
|
+
@blocked_y = nil
|
|
113
|
+
ny = y + dy
|
|
114
|
+
return ny if dy.zero?
|
|
115
|
+
|
|
116
|
+
winner = nil
|
|
117
|
+
i = 0
|
|
118
|
+
while i < @blockers.size
|
|
119
|
+
source = @blockers[i]
|
|
120
|
+
landed = source.resolve_y(x, y, w, h, dy)
|
|
121
|
+
if dy.positive? ? landed < ny : landed > ny
|
|
122
|
+
ny = landed
|
|
123
|
+
winner = source
|
|
124
|
+
end
|
|
125
|
+
i += 1
|
|
126
|
+
end
|
|
127
|
+
@blocked_y = winner&.blocker
|
|
128
|
+
ny
|
|
41
129
|
end
|
|
42
130
|
end
|
|
43
131
|
end
|
|
@@ -4,6 +4,7 @@ module RGame
|
|
|
4
4
|
module Engine
|
|
5
5
|
class Component
|
|
6
6
|
extend Signal::DSL
|
|
7
|
+
extend SealedPrivates
|
|
7
8
|
|
|
8
9
|
attr_accessor :node
|
|
9
10
|
|
|
@@ -18,6 +19,39 @@ module RGame
|
|
|
18
19
|
def on_attach; end
|
|
19
20
|
def on_detach; end
|
|
20
21
|
|
|
22
|
+
# The sibling component this one drives, or a raise naming both — for the
|
|
23
|
+
# `@body = require_sibling(CharacterBody)` line an `on_attach` opens with.
|
|
24
|
+
#
|
|
25
|
+
# Worth a helper rather than a bare `get_component` because the nil it returns
|
|
26
|
+
# is silent, and stays silent until the first frame calls a method on it: the
|
|
27
|
+
# error you see is a NoMethodError on nil, in `control`, naming neither the
|
|
28
|
+
# component that is missing nor the one that wanted it.
|
|
29
|
+
#
|
|
30
|
+
# And the cause is nearly always the same, which is why the message says it.
|
|
31
|
+
# A node assembled *outside* the tree collects every component before any
|
|
32
|
+
# on_attach runs (Node2D#enter_tree), so add order does not matter there. A
|
|
33
|
+
# node that adds components from its own `on_add` is already in the tree, so
|
|
34
|
+
# each one attaches as it arrives and can only see the ones before it. Same
|
|
35
|
+
# two lines, opposite outcome, depending on where they were written.
|
|
36
|
+
#
|
|
37
|
+
# A class several components answer to — `Components::Mover` has three — can match
|
|
38
|
+
# twice on one node, and then there is no telling which one was meant. That raises
|
|
39
|
+
# too, naming each match, rather than quietly taking the first.
|
|
40
|
+
def require_sibling(klass)
|
|
41
|
+
matches = node.components.grep(klass)
|
|
42
|
+
if matches.length > 1
|
|
43
|
+
raise ArgumentError, "#{self.class} reads one #{klass} on the same node, and this node has " \
|
|
44
|
+
"#{matches.length}: #{matches.map(&:class).join(', ')}. " \
|
|
45
|
+
'Keep one of them on this node.'
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
matches.first ||
|
|
49
|
+
raise("#{self.class} needs a #{klass} on the same node, and there is none. If you did " \
|
|
50
|
+
'add one, add it before this component: a node that is already in the tree ' \
|
|
51
|
+
'attaches each component as it arrives, so a sibling added after this one is ' \
|
|
52
|
+
'not there yet when this one attaches.')
|
|
53
|
+
end
|
|
54
|
+
|
|
21
55
|
def control(actions); end
|
|
22
56
|
def update(dt); end
|
|
23
57
|
def draw(renderer, view); end
|
|
@@ -25,16 +59,6 @@ module RGame
|
|
|
25
59
|
# Container components (e.g. SceneStack) that hold nodes off the normal child
|
|
26
60
|
# list override this to forward the deferred-free sweep into them.
|
|
27
61
|
def sweep_freed; end
|
|
28
|
-
|
|
29
|
-
# `control` above receives **one player's** Actions — the actions of
|
|
30
|
-
# whoever owns this component's node — which is what a component wants,
|
|
31
|
-
# since it belongs to exactly one node.
|
|
32
|
-
#
|
|
33
|
-
# A container component holding a whole subtree needs the input *source*
|
|
34
|
-
# instead, so the nodes inside it can each resolve their own owner. There
|
|
35
|
-
# is no hook for that on purpose: it is a system lookup like any other, so
|
|
36
|
-
# such a component pulls `node.system(Players)` in `on_attach` and passes
|
|
37
|
-
# that down. SceneStack is the worked example.
|
|
38
62
|
end
|
|
39
63
|
end
|
|
40
64
|
end
|
|
@@ -3,21 +3,26 @@
|
|
|
3
3
|
module RGame
|
|
4
4
|
module Engine
|
|
5
5
|
module Components
|
|
6
|
-
# Draws a sprite-sheet animation for a walking actor, picking the animation from
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
6
|
+
# Draws a sprite-sheet animation for a walking actor, picking the animation from its
|
|
7
|
+
# Mover sibling's heading: walk_left/right/up/down while moving, stand when still. The
|
|
8
|
+
# larger axis of the heading picks the direction and a tie goes horizontal, so a
|
|
9
|
+
# keyboard diagonal walks sideways and a route running mostly downhill walks down.
|
|
10
|
+
# Any mover will do — a CharacterBody faces its intent, a PathFollow the road it is
|
|
11
|
+
# on — and a node with two movers raises at attach, since there would be no telling
|
|
12
|
+
# which way it faces. Owns its Animator + the pure AnimationSet built from the sheet's
|
|
13
|
+
# animation table.
|
|
10
14
|
#
|
|
11
|
-
# Like Sprite, it passes NO angle and draws at
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
# layer (kept as @layer, distinct from the node's transform z); it must
|
|
15
|
-
# the tile map's ground and canopy z bands, so canopies draw in
|
|
15
|
+
# Like Sprite, it passes NO angle and NO position: it draws at (0, 0), which
|
|
16
|
+
# Node2D#draw has already made mean "at this node, correctly rotated", and a
|
|
17
|
+
# WorldView ancestor has already made mean "through the camera". `z` is the
|
|
18
|
+
# render layer (kept as @layer, distinct from the node's transform z); it must
|
|
19
|
+
# sit between the tile map's ground and canopy z bands, so canopies draw in
|
|
20
|
+
# front.
|
|
16
21
|
#
|
|
17
22
|
# `sheet` is the asset's relative path. The component resolves it from the game's
|
|
18
23
|
# asset manager on attach — via node.root.context.assets (the platform seam) — to
|
|
19
24
|
# build its animation table and to size the node to the sprite's frame, so siblings
|
|
20
|
-
# like
|
|
25
|
+
# like FeetCollider can read node.width/height. The renderer resolves the same
|
|
21
26
|
# symbol when drawing, so nothing is registered or passed in by hand.
|
|
22
27
|
class AnimatedSprite < Engine::Component
|
|
23
28
|
include Engine::Culling
|
|
@@ -33,34 +38,31 @@ module RGame
|
|
|
33
38
|
@animator = Engine::Animator.new(Engine::AnimationSet.new(sheet.animations))
|
|
34
39
|
node.width = sheet.frame_width
|
|
35
40
|
node.height = sheet.frame_height
|
|
36
|
-
@
|
|
41
|
+
@mover = require_sibling(Mover)
|
|
37
42
|
end
|
|
38
43
|
|
|
39
44
|
def update(dt)
|
|
40
|
-
@animator.play(walk_animation(@
|
|
45
|
+
@animator.play(walk_animation(@mover.heading_x, @mover.heading_y))
|
|
41
46
|
@animator.update(dt)
|
|
42
47
|
end
|
|
43
48
|
|
|
44
|
-
# Top-left anchored,
|
|
45
|
-
# to cull against is
|
|
49
|
+
# Top-left anchored, sized by the sheet's frame and lifted by the node's
|
|
50
|
+
# elevation — so the footprint to cull against is the node's box, raised
|
|
51
|
+
# by the same amount the picture is.
|
|
46
52
|
def draw(renderer, view)
|
|
47
|
-
|
|
53
|
+
lift = node.elevation
|
|
54
|
+
return if culled?(view, node.world_x, node.world_y - lift, node.width, node.height)
|
|
48
55
|
|
|
49
|
-
|
|
50
|
-
# every call) to keep the draw path allocation-free.
|
|
51
|
-
renderer.sprite(@sheet, @animator.row, @animator.col, node.abs_x, node.abs_y,
|
|
56
|
+
renderer.sprite(@sheet, @animator.row, @animator.col, 0, -lift,
|
|
52
57
|
flip_x: @animator.flip_x, z: @layer)
|
|
53
58
|
end
|
|
54
59
|
|
|
55
60
|
private
|
|
56
61
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
elsif move_y.negative? then :walk_up
|
|
62
|
-
elsif move_y.positive? then :walk_down
|
|
63
|
-
else :stand
|
|
62
|
+
def walk_animation(heading_x, heading_y)
|
|
63
|
+
if heading_x.zero? && heading_y.zero? then :stand
|
|
64
|
+
elsif heading_x.abs >= heading_y.abs then heading_x.negative? ? :walk_left : :walk_right
|
|
65
|
+
else heading_y.negative? ? :walk_up : :walk_down
|
|
64
66
|
end
|
|
65
67
|
end
|
|
66
68
|
end
|