rgame 0.1.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 +228 -0
- data/README.md +142 -243
- data/docs/api/README.md +141 -90
- data/docs/api/app.md +125 -68
- data/docs/api/assets.md +203 -166
- data/docs/api/audio.md +130 -89
- data/docs/api/cli.md +259 -0
- data/docs/api/components.md +1045 -254
- data/docs/api/drawing.md +187 -119
- data/docs/api/examples.md +263 -0
- data/docs/api/game.md +165 -45
- data/docs/api/images.md +39 -39
- data/docs/api/input.md +344 -85
- data/docs/api/internals.md +241 -62
- data/docs/api/localization.md +285 -0
- data/docs/api/scene_graph.md +503 -123
- data/docs/api/signals.md +81 -76
- data/docs/api/systems.md +197 -64
- data/docs/api/text.md +58 -41
- data/docs/api/tile_maps.md +237 -0
- data/docs/api/toolbox.md +400 -147
- data/docs/api/ui.md +927 -0
- data/docs/api/values.md +234 -19
- 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/README.md +6 -5
- data/ext/rgame_core/app/app.c +215 -11
- 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 +113 -2
- data/ext/rgame_core/example.rb +18 -56
- data/ext/rgame_core/extconf.rb +16 -113
- data/ext/rgame_core/graphics/canvas.c +45 -4
- data/ext/rgame_core/graphics/canvas.h +65 -10
- data/ext/rgame_core/graphics/clip.c +22 -13
- data/ext/rgame_core/include/rgame/core.h +151 -5
- data/ext/rgame_core/input/gamepad.c +57 -3
- data/ext/rgame_core/ruby/audio_ext.c +10 -5
- data/ext/rgame_core/ruby/core_ext.c +46 -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 +42 -19
- data/ext/rgame_util/color_ext.c +12 -3
- 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 -42
- 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/input.rb +35 -41
- data/lib/rgame/core/locale.rb +22 -0
- data/lib/rgame/core/nine_slice.rb +0 -21
- data/lib/rgame/core/recording.rb +3 -1
- data/lib/rgame/core/renderer.rb +75 -84
- data/lib/rgame/core/sprite_sheet.rb +0 -3
- data/lib/rgame/core/tile_map_renderer.rb +77 -65
- 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 +55 -10
- 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 +35 -1
- data/lib/rgame/engine/components/action_trigger.rb +0 -1
- data/lib/rgame/engine/components/animated_sprite.rb +31 -23
- 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 +21 -41
- 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 +22 -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 +70 -29
- 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 +45 -0
- data/lib/rgame/engine/debug_overlay.rb +23 -18
- 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 +76 -22
- data/lib/rgame/engine/input/actions.rb +63 -12
- 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 +364 -80
- data/lib/rgame/engine/path.rb +4 -6
- 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/presentation.rb +171 -0
- data/lib/rgame/engine/scene/scene_stack.rb +29 -7
- data/lib/rgame/engine/sealed_privates.rb +54 -0
- data/lib/rgame/engine/spatial_hash.rb +53 -8
- 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 +82 -0
- 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 +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 +43 -25
- data/lib/rgame/game.rb +164 -22
- 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 +106 -44
- 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 +123 -0
- data/lib/rgame/util.rb +5 -3
- data/lib/rgame/version.rb +1 -1
- data/lib/rgame.rb +0 -15
- metadata +176 -20
- 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/camera_view.rb +0 -28
- 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
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
module Components
|
|
6
|
+
# A PathFollow that makes its own paths: `go_to(world_x, world_y)` plans a route
|
|
7
|
+
# over the scene's tile map to the cell containing that point, and walks it.
|
|
8
|
+
#
|
|
9
|
+
# Planning is three steps. The scene's TileWorld#nav_grid finds the cheapest route
|
|
10
|
+
# of cells; the route is **string-pulled** into as few straight segments as the
|
|
11
|
+
# node's own collider can travel; and those corners become the Engine::Path this
|
|
12
|
+
# component walks, exactly as a PathFollow walks one it was handed.
|
|
13
|
+
#
|
|
14
|
+
# ## The route is checked against what will stop the walk
|
|
15
|
+
#
|
|
16
|
+
# A segment is kept only if the map's own blocker source — TileWorld#blockers, the
|
|
17
|
+
# object a mover declaring `:tiles` resolves its steps against — lets the collider
|
|
18
|
+
# box travel it. A test on cells alone would keep a diagonal past a solid tile's
|
|
19
|
+
# corner that a point clears and a 12x6 feet box clips, and a PathFollow does not
|
|
20
|
+
# slide: the walker would stand at that corner.
|
|
21
|
+
#
|
|
22
|
+
# The question is TileBlockers#travel?, which holds for a walker stepping under a
|
|
23
|
+
# quarter tile at a time — 240 px/s at 60 ticks a second on 16 px tiles.
|
|
24
|
+
#
|
|
25
|
+
# Smoothing is greedy: from each corner it extends the segment cell by cell until the
|
|
26
|
+
# next cell would not be clear, and turns there. The next cell after a corner is taken
|
|
27
|
+
# without asking, which is sound only for a collider **no larger than a tile** on
|
|
28
|
+
# either axis — so `go_to` refuses a larger one rather than plan a route it may stall on.
|
|
29
|
+
#
|
|
30
|
+
# So what is walked is measured from the **anchor** — the centre of the sibling
|
|
31
|
+
# BoxCollider's box, or the node's origin on a node with none — and a route ends
|
|
32
|
+
# with the anchor on the centre of the target cell.
|
|
33
|
+
#
|
|
34
|
+
# ## What it does not do
|
|
35
|
+
#
|
|
36
|
+
# It does not replan. A route is planned against the map, and the map is all it
|
|
37
|
+
# knows: a walker declaring other colliders in `blocked_by` waits behind one on its
|
|
38
|
+
# route, the way any PathFollow waits, and a game that wants it to go round calls
|
|
39
|
+
# `go_to` again.
|
|
40
|
+
#
|
|
41
|
+
# The route is planned in world space and walked in the node's parent's space, which
|
|
42
|
+
# agree under an unrotated ancestor chain — the same limit a blocked Mover has.
|
|
43
|
+
class Navigator < PathFollow
|
|
44
|
+
# The cells of the last route `go_to` planned, start to target, before smoothing —
|
|
45
|
+
# `[[col, row], ...]`, or nil before the first one. For drawing; `path` is what is
|
|
46
|
+
# walked.
|
|
47
|
+
attr_reader :cells
|
|
48
|
+
|
|
49
|
+
def initialize(speed:, blocked_by: [])
|
|
50
|
+
super
|
|
51
|
+
@cells = nil
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def on_attach
|
|
55
|
+
super
|
|
56
|
+
@world = node.system(TileWorld) ||
|
|
57
|
+
raise("#{mover_name} plans routes over the scene's TileWorld, and the scene " \
|
|
58
|
+
'has none. Mount one, or use a PathFollow and hand it a path.')
|
|
59
|
+
@anchor = node.get_component(BoxCollider)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Plan from where the node stands to the cell containing (world_x, world_y), and
|
|
63
|
+
# start walking it from the node's current position — so a walker already on a
|
|
64
|
+
# route turns onto the new one without a jump. Returns true.
|
|
65
|
+
#
|
|
66
|
+
# Returns false, and leaves whatever the node was doing alone, when there is no
|
|
67
|
+
# route: the target is solid, outside the map, or cut off from the node.
|
|
68
|
+
#
|
|
69
|
+
# Raises ArgumentError when the node's collider is wider or taller than a tile:
|
|
70
|
+
# pathfinding for such a collider is not supported.
|
|
71
|
+
#
|
|
72
|
+
# rubocop:disable Naming/PredicateMethod -- a command that reports whether it could be
|
|
73
|
+
# carried out, not a question; `go_to?` would read as "may I go there?".
|
|
74
|
+
def go_to(world_x, world_y)
|
|
75
|
+
unless @world
|
|
76
|
+
raise "#{mover_name}#go_to plans over the scene's TileWorld, so the node has to be " \
|
|
77
|
+
'in the tree first.'
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
measure_anchor
|
|
81
|
+
refuse_a_box_larger_than_a_tile
|
|
82
|
+
cells = @world.nav_grid.find(*cell_at(@anchor_x, @anchor_y), *cell_at(world_x, world_y))
|
|
83
|
+
return false unless cells
|
|
84
|
+
|
|
85
|
+
@cells = cells
|
|
86
|
+
follow(Engine::Path.new(waypoints(corners(cells))))
|
|
87
|
+
true
|
|
88
|
+
end
|
|
89
|
+
# rubocop:enable Naming/PredicateMethod
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def measure_anchor
|
|
94
|
+
box = @anchor&.box
|
|
95
|
+
@box_w = box ? box.width : 0
|
|
96
|
+
@box_h = box ? box.height : 0
|
|
97
|
+
@anchor_x = node.world_x + (box ? box.offset_x + (@box_w / 2.0) : 0.0)
|
|
98
|
+
@anchor_y = node.world_y + (box ? box.offset_y + (@box_h / 2.0) : 0.0)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def refuse_a_box_larger_than_a_tile
|
|
102
|
+
return if @box_w <= @world.tile_width && @box_h <= @world.tile_height
|
|
103
|
+
|
|
104
|
+
raise ArgumentError, "#{mover_name}#go_to plans for a collider no larger than a tile, and this " \
|
|
105
|
+
"node's is #{@box_w}x#{@box_h} over #{@world.tile_width}x" \
|
|
106
|
+
"#{@world.tile_height} tiles. Pathfinding for a larger collider is not supported."
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def cell_at(x, y) = [(x / @world.tile_width).floor, (y / @world.tile_height).floor]
|
|
110
|
+
|
|
111
|
+
def centre_x(cell) = (cell[0] + 0.5) * @world.tile_width
|
|
112
|
+
def centre_y(cell) = (cell[1] + 0.5) * @world.tile_height
|
|
113
|
+
|
|
114
|
+
def corners(cells)
|
|
115
|
+
corners = [[@anchor_x, @anchor_y]]
|
|
116
|
+
turn = -1
|
|
117
|
+
while turn < cells.length - 1
|
|
118
|
+
turn = furthest_clear(*corners.last, cells, turn + 1)
|
|
119
|
+
corners << [centre_x(cells[turn]), centre_y(cells[turn])]
|
|
120
|
+
end
|
|
121
|
+
corners
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def furthest_clear(x, y, cells, index)
|
|
125
|
+
index += 1 while index < cells.length - 1 &&
|
|
126
|
+
box_travels?(x, y, centre_x(cells[index + 1]), centre_y(cells[index + 1]))
|
|
127
|
+
index
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def box_travels?(from_x, from_y, to_x, to_y)
|
|
131
|
+
@world.blockers.travel?(from_x - (@box_w / 2.0), from_y - (@box_h / 2.0), @box_w, @box_h,
|
|
132
|
+
to_x - from_x, to_y - from_y)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def waypoints(corners)
|
|
136
|
+
shift_x = node.x - @anchor_x
|
|
137
|
+
shift_y = node.y - @anchor_y
|
|
138
|
+
points = corners.map { |(x, y)| [x + shift_x, y + shift_y] }
|
|
139
|
+
points[0] = [node.x, node.y]
|
|
140
|
+
points
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
@@ -4,26 +4,58 @@ module RGame
|
|
|
4
4
|
module Engine
|
|
5
5
|
module Components
|
|
6
6
|
# Walks the owning node along an Engine::Path at a constant speed, segment by
|
|
7
|
-
# segment, and emits `on_finished` once it reaches the final waypoint — the seam
|
|
8
|
-
#
|
|
7
|
+
# segment, and emits `on_finished` once it reaches the final waypoint — the seam for
|
|
8
|
+
# whatever should happen when a walker arrives.
|
|
9
9
|
#
|
|
10
10
|
# The walk is allocation-free: it tracks the current segment and the distance into
|
|
11
11
|
# it, advancing through as many segments as one step crosses (so a fast mover over
|
|
12
12
|
# short segments still lands correctly), then interpolates the node's position from
|
|
13
13
|
# the segment endpoints. Movement is driven purely by `speed * dt`; this is not a
|
|
14
14
|
# Velocity integrator and ignores the node's angle.
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
#
|
|
16
|
+
# ## Blocked, the walk waits
|
|
17
|
+
#
|
|
18
|
+
# A Mover, so `blocked_by:` stops it the way it stops a CharacterBody — see Mover's
|
|
19
|
+
# header. Declaring nothing keeps the walk exactly as described above: the node is
|
|
20
|
+
# *placed* on the path each step.
|
|
21
|
+
#
|
|
22
|
+
# Declaring something turns the placement into a move: the walk still works out
|
|
23
|
+
# where the node should be, and then gets there through `apply_move`. **A step that
|
|
24
|
+
# is stopped short does not advance the walk** — progress goes back to where the
|
|
25
|
+
# step began. The obvious alternative, letting progress run on and the node catch up,
|
|
26
|
+
# is wrong twice over: a follower held behind something for a second would race
|
|
27
|
+
# ahead to where it "should" be once let go, and `on_finished` could fire for a
|
|
28
|
+
# walker still standing in front of the obstacle.
|
|
29
|
+
#
|
|
30
|
+
# One consequence is easy to miss: **a held follower does not slide along what stopped
|
|
31
|
+
# it** the way a Velocity does. A step's free axis still moves, but the rewind aims the
|
|
32
|
+
# next step at the same point on the path, from a little closer, so a follower pressed
|
|
33
|
+
# diagonally against a wall creeps to rest instead of sliding to the corner. A walker
|
|
34
|
+
# meant to get round something replans its path rather than relying on the slide.
|
|
35
|
+
#
|
|
36
|
+
# `on_attach` still places the node on the first waypoint absolutely, blocked or
|
|
37
|
+
# not: where a walker starts is a placement, not a step.
|
|
38
|
+
#
|
|
39
|
+
# ## A new route, and none
|
|
40
|
+
#
|
|
41
|
+
# `path: nil` builds an idle follower, which moves nothing and never finishes, and
|
|
42
|
+
# `follow(path)` hands any follower a route to walk from its start — the same restart
|
|
43
|
+
# entering the tree gives, so a walker that finished one route walks the next and
|
|
44
|
+
# finishes again, and one still walking drops its old route at once.
|
|
45
|
+
#
|
|
46
|
+
# Its heading is the unit direction of the segment it is on, worked out when the walk
|
|
47
|
+
# crosses into a segment rather than on every read.
|
|
48
|
+
class PathFollow < Mover
|
|
49
|
+
signal :on_finished
|
|
17
50
|
|
|
18
51
|
attr_accessor :speed
|
|
52
|
+
attr_reader :path, :heading_x, :heading_y
|
|
19
53
|
|
|
20
|
-
def initialize(
|
|
21
|
-
super()
|
|
54
|
+
def initialize(speed:, path: nil, blocked_by: [])
|
|
55
|
+
super(blocked_by: blocked_by)
|
|
22
56
|
@path = path
|
|
23
57
|
@speed = speed
|
|
24
|
-
|
|
25
|
-
@distance = 0.0 # distance travelled into the current segment
|
|
26
|
-
@finished = false
|
|
58
|
+
restart
|
|
27
59
|
end
|
|
28
60
|
|
|
29
61
|
def finished? = @finished
|
|
@@ -32,52 +64,112 @@ module RGame
|
|
|
32
64
|
# progress cleared — so a pooled follower reacquired and re-added begins a fresh walk
|
|
33
65
|
# rather than resuming (or staying finished) where its previous life ended.
|
|
34
66
|
def on_attach
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
@finished = false
|
|
38
|
-
place_at(0)
|
|
67
|
+
super
|
|
68
|
+
restart
|
|
39
69
|
end
|
|
40
70
|
|
|
41
|
-
|
|
42
|
-
|
|
71
|
+
# Walk `path` from its first waypoint, where the node is placed, whatever this
|
|
72
|
+
# follower was doing — idle, finished, or halfway along another route. `nil` stops
|
|
73
|
+
# it where it stands.
|
|
74
|
+
def follow(path)
|
|
75
|
+
@path = path
|
|
76
|
+
restart
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def take_step(dt)
|
|
82
|
+
return if @finished || @path.nil?
|
|
83
|
+
|
|
84
|
+
from_segment = @segment
|
|
85
|
+
from_distance = @distance
|
|
86
|
+
|
|
87
|
+
if advance_to_end?(@speed * dt)
|
|
88
|
+
last = @path.count - 1
|
|
89
|
+
return finish if moved_to?(@path.x_at(last), @path.y_at(last))
|
|
90
|
+
|
|
91
|
+
rewind(from_segment, from_distance)
|
|
92
|
+
else
|
|
93
|
+
seg_len = @path.segment_length(@segment)
|
|
94
|
+
t = seg_len.zero? ? 0.0 : @distance / seg_len
|
|
95
|
+
sx = @path.x_at(@segment)
|
|
96
|
+
sy = @path.y_at(@segment)
|
|
97
|
+
reached = moved_to?(sx + ((@path.x_at(@segment + 1) - sx) * t),
|
|
98
|
+
sy + ((@path.y_at(@segment + 1) - sy) * t))
|
|
99
|
+
rewind(from_segment, from_distance) unless reached
|
|
100
|
+
end
|
|
101
|
+
aim
|
|
102
|
+
end
|
|
43
103
|
|
|
44
|
-
|
|
104
|
+
def advance_to_end?(remaining)
|
|
45
105
|
while remaining.positive?
|
|
46
106
|
left = @path.segment_length(@segment) - @distance
|
|
47
107
|
if remaining < left
|
|
48
108
|
@distance += remaining
|
|
49
|
-
|
|
109
|
+
return false
|
|
50
110
|
end
|
|
51
|
-
# Consume the rest of this segment and step onto the next waypoint.
|
|
52
111
|
remaining -= left
|
|
53
112
|
@segment += 1
|
|
54
113
|
@distance = 0.0
|
|
55
|
-
return
|
|
114
|
+
return true if @segment >= @path.count - 1
|
|
56
115
|
end
|
|
57
|
-
|
|
116
|
+
false
|
|
58
117
|
end
|
|
59
118
|
|
|
60
|
-
|
|
119
|
+
def moved_to?(x, y)
|
|
120
|
+
unless blocking?
|
|
121
|
+
node.x = x
|
|
122
|
+
node.y = y
|
|
123
|
+
return true
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
apply_move(x - node.x, y - node.y)
|
|
127
|
+
!last_move_blocked?
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def rewind(segment, distance)
|
|
131
|
+
@segment = segment
|
|
132
|
+
@distance = distance
|
|
133
|
+
end
|
|
61
134
|
|
|
62
135
|
def finish
|
|
63
|
-
place_at(@path.count - 1)
|
|
64
136
|
@finished = true
|
|
137
|
+
head_nowhere
|
|
65
138
|
on_finished_signal.emit
|
|
66
139
|
end
|
|
67
140
|
|
|
141
|
+
def restart
|
|
142
|
+
@segment = 0
|
|
143
|
+
@distance = 0.0
|
|
144
|
+
@finished = false
|
|
145
|
+
head_nowhere
|
|
146
|
+
return unless @path
|
|
147
|
+
|
|
148
|
+
aim
|
|
149
|
+
place_at(0) if node
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def aim
|
|
153
|
+
return if @segment == @aimed_segment
|
|
154
|
+
|
|
155
|
+
@aimed_segment = @segment
|
|
156
|
+
length = @path.segment_length(@segment)
|
|
157
|
+
return head_nowhere if length.zero?
|
|
158
|
+
|
|
159
|
+
@heading_x = (@path.x_at(@segment + 1) - @path.x_at(@segment)) / length
|
|
160
|
+
@heading_y = (@path.y_at(@segment + 1) - @path.y_at(@segment)) / length
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def head_nowhere
|
|
164
|
+
@aimed_segment = nil
|
|
165
|
+
@heading_x = 0.0
|
|
166
|
+
@heading_y = 0.0
|
|
167
|
+
end
|
|
168
|
+
|
|
68
169
|
def place_at(waypoint)
|
|
69
170
|
node.x = @path.x_at(waypoint)
|
|
70
171
|
node.y = @path.y_at(waypoint)
|
|
71
172
|
end
|
|
72
|
-
|
|
73
|
-
def place_along_segment
|
|
74
|
-
seg_len = @path.segment_length(@segment)
|
|
75
|
-
t = seg_len.zero? ? 0.0 : @distance / seg_len
|
|
76
|
-
sx = @path.x_at(@segment)
|
|
77
|
-
sy = @path.y_at(@segment)
|
|
78
|
-
node.x = sx + ((@path.x_at(@segment + 1) - sx) * t)
|
|
79
|
-
node.y = sy + ((@path.y_at(@segment + 1) - sy) * t)
|
|
80
|
-
end
|
|
81
173
|
end
|
|
82
174
|
end
|
|
83
175
|
end
|
|
@@ -5,7 +5,10 @@ module RGame
|
|
|
5
5
|
module Components
|
|
6
6
|
# Drives a CharacterBody sibling from two input axes — direct 8-way walking, no
|
|
7
7
|
# inertia (unlike ThrustController). Each frame it copies the axis snapshot into
|
|
8
|
-
# the body's movement intent; the body does the
|
|
8
|
+
# the body's movement intent; what the body then does with it is the body's
|
|
9
|
+
# business — an unblocked CharacterBody moves the node, a blocked one resolves
|
|
10
|
+
# the step against the map. The lookup names the base class and get_component
|
|
11
|
+
# matches by ancestry, so this needs to know nothing about which is there.
|
|
9
12
|
class PlayerController < Engine::Component
|
|
10
13
|
def initialize(x_axis: :move_x, y_axis: :move_y)
|
|
11
14
|
super()
|
|
@@ -13,7 +16,7 @@ module RGame
|
|
|
13
16
|
@y_axis = y_axis
|
|
14
17
|
end
|
|
15
18
|
|
|
16
|
-
def on_attach = @body =
|
|
19
|
+
def on_attach = @body = require_sibling(CharacterBody)
|
|
17
20
|
|
|
18
21
|
def control(actions)
|
|
19
22
|
@body.set_intent(actions.axis(@x_axis), actions.axis(@y_axis))
|
|
@@ -3,23 +3,45 @@
|
|
|
3
3
|
module RGame
|
|
4
4
|
module Engine
|
|
5
5
|
module Components
|
|
6
|
-
# Wraps the node's position toroidally within
|
|
7
|
-
# entity leaving one edge reappears on the opposite one.
|
|
8
|
-
#
|
|
9
|
-
#
|
|
6
|
+
# Wraps the node's position toroidally within the world bounds (plus margin), so
|
|
7
|
+
# an entity leaving one edge reappears on the opposite one.
|
|
8
|
+
#
|
|
9
|
+
# The bounds come from the scene's world system — anything answering the
|
|
10
|
+
# WorldBounds contract, so Components::World or Components::TileWorld — and are
|
|
11
|
+
# resolved when the node enters the tree. Passing `width:`/`height:` overrides
|
|
12
|
+
# that for a node whose wrap region is not the whole world.
|
|
13
|
+
#
|
|
14
|
+
# The node is tested and placed in **world space**, which is the frame WorldBounds
|
|
15
|
+
# is stated in: a node under an offset container wraps at the world's edge, not at
|
|
16
|
+
# an edge shifted by wherever the container sits. The write goes through
|
|
17
|
+
# Node2D#world_x=, so the node's local position is what actually changes.
|
|
18
|
+
#
|
|
19
|
+
# A wrap is a placement, not a step: it does not ask a sibling Mover's `blocked_by`
|
|
20
|
+
# whether the far edge is free, so a node wrapped onto a blocker is left pressed
|
|
21
|
+
# against it.
|
|
10
22
|
class ScreenWrap < Engine::Component
|
|
11
|
-
def initialize(width
|
|
23
|
+
def initialize(width: nil, height: nil, margin: 0.0)
|
|
12
24
|
super()
|
|
13
|
-
@
|
|
14
|
-
@
|
|
25
|
+
@given_width = width
|
|
26
|
+
@given_height = height
|
|
15
27
|
@margin = margin
|
|
16
28
|
end
|
|
17
29
|
|
|
30
|
+
# Re-resolved on every entry rather than cached from the first, so a pooled
|
|
31
|
+
# entity recycled into a differently sized scene wraps against that scene.
|
|
32
|
+
def on_attach
|
|
33
|
+
WorldBounds.one_response!(node)
|
|
34
|
+
@width, @height = WorldBounds.resolve(node, @given_width, @given_height)
|
|
35
|
+
end
|
|
36
|
+
|
|
18
37
|
def update(_dt)
|
|
19
|
-
|
|
20
|
-
node.
|
|
21
|
-
node.
|
|
22
|
-
|
|
38
|
+
x = node.world_x
|
|
39
|
+
node.world_x = @width + @margin if x < -@margin
|
|
40
|
+
node.world_x = -@margin if x > @width + @margin
|
|
41
|
+
|
|
42
|
+
y = node.world_y
|
|
43
|
+
node.world_y = @height + @margin if y < -@margin
|
|
44
|
+
node.world_y = -@margin if y > @height + @margin
|
|
23
45
|
end
|
|
24
46
|
end
|
|
25
47
|
end
|
|
@@ -3,16 +3,18 @@
|
|
|
3
3
|
module RGame
|
|
4
4
|
module Engine
|
|
5
5
|
module Components
|
|
6
|
-
# Draws a single registered image
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
6
|
+
# Draws a single registered image centred on the node's own origin. It passes
|
|
7
|
+
# NO angle and NO position to the renderer: Node2D#draw has already pushed the
|
|
8
|
+
# node's transform, so drawing at (0, 0) *is* drawing at the node, correctly
|
|
9
|
+
# rotated. Passing either would apply it a second time.
|
|
10
10
|
#
|
|
11
11
|
# `scale` is writable so a pooled entity (e.g. a multi-tier rock) can retune it on
|
|
12
12
|
# reset. `z` is the render layer (NOT the node's transform z / abs_z) — kept under
|
|
13
13
|
# @layer to say so. For an animated sprite (sheet + frame index driving
|
|
14
14
|
# renderer.sprite), add a sibling AnimatedSprite component later.
|
|
15
15
|
class Sprite < Engine::Component
|
|
16
|
+
include Engine::Culling
|
|
17
|
+
|
|
16
18
|
attr_accessor :scale
|
|
17
19
|
|
|
18
20
|
def initialize(id:, scale: 1.0, z: 0)
|
|
@@ -22,8 +24,22 @@ module RGame
|
|
|
22
24
|
@layer = z
|
|
23
25
|
end
|
|
24
26
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
# Centred on the node's origin and scaled, so the footprint to cull
|
|
28
|
+
# against is the node's box scaled and offset back by half of itself. A
|
|
29
|
+
# node that never set a size is never culled — see Culling.
|
|
30
|
+
#
|
|
31
|
+
# Culling is the one thing here still stated in **world** coordinates:
|
|
32
|
+
# it compares against the camera, which is nowhere near this node's
|
|
33
|
+
# local space. Drawing is local, culling is world, and the two are
|
|
34
|
+
# deliberately different arguments.
|
|
35
|
+
def draw(renderer, view)
|
|
36
|
+
width = node.width * @scale
|
|
37
|
+
height = node.height * @scale
|
|
38
|
+
lift = node.elevation
|
|
39
|
+
return if culled?(view, node.world_x - (width / 2.0), node.world_y - lift - (height / 2.0),
|
|
40
|
+
width, height)
|
|
41
|
+
|
|
42
|
+
renderer.image(@id, 0, -lift, scale: @scale, z: @layer)
|
|
27
43
|
end
|
|
28
44
|
end
|
|
29
45
|
end
|
|
@@ -3,17 +3,17 @@
|
|
|
3
3
|
module RGame
|
|
4
4
|
module Engine
|
|
5
5
|
module Components
|
|
6
|
-
# Picks
|
|
7
|
-
#
|
|
8
|
-
#
|
|
6
|
+
# Picks a node for the owning node to aim at: each update it queries the scene's
|
|
7
|
+
# CollisionWorld around the node's world origin and exposes the chosen target via
|
|
8
|
+
# #target (the candidate's node, or nil). The owner reads it to act — Targeting only
|
|
9
9
|
# selects, it never moves or shoots.
|
|
10
10
|
#
|
|
11
|
-
# The CollisionWorld it queries is the same broadphase
|
|
12
|
-
#
|
|
13
|
-
#
|
|
11
|
+
# The CollisionWorld it queries is the same broadphase every collider registers with,
|
|
12
|
+
# so targeting needs no candidate list of its own. The `layer` restricts candidates
|
|
13
|
+
# (pass :enemy to pick only colliders on that layer, not allies or projectiles).
|
|
14
14
|
#
|
|
15
15
|
# Policies (how to choose among the candidates in range):
|
|
16
|
-
# - :nearest — the closest
|
|
16
|
+
# - :nearest — the closest candidate (the default; one broadphase nearest-lookup).
|
|
17
17
|
# More policies (e.g. furthest-along-path) arrive with the state they need.
|
|
18
18
|
class Targeting < Engine::Component
|
|
19
19
|
POLICIES = %i[nearest].freeze
|
|
@@ -28,7 +28,7 @@ module RGame
|
|
|
28
28
|
@target = nil
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
-
# The chosen
|
|
31
|
+
# The chosen node, or nil when nothing is in range. Refreshed every update.
|
|
32
32
|
attr_reader :target
|
|
33
33
|
|
|
34
34
|
# Pull the broadphase once it's reachable (scene scope, resolved up the tree).
|
|
@@ -41,11 +41,9 @@ module RGame
|
|
|
41
41
|
|
|
42
42
|
private
|
|
43
43
|
|
|
44
|
-
# Allocation-free: a symbol dispatch over a known-small policy set, each delegating
|
|
45
|
-
# to an allocation-free CollisionWorld query.
|
|
46
44
|
def pick
|
|
47
45
|
case @policy
|
|
48
|
-
when :nearest then @world.nearest(node.
|
|
46
|
+
when :nearest then @world.nearest(node.world_x, node.world_y, @range, layer: @layer)
|
|
49
47
|
end
|
|
50
48
|
end
|
|
51
49
|
end
|
|
@@ -26,7 +26,7 @@ module RGame
|
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
# The Velocity sibling is only guaranteed present once attached to a node.
|
|
29
|
-
def on_attach = @velocity =
|
|
29
|
+
def on_attach = @velocity = require_sibling(Velocity)
|
|
30
30
|
|
|
31
31
|
def control(actions)
|
|
32
32
|
@velocity.spin = actions.axis(@turn_action) * @turn_speed
|