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
|
@@ -3,23 +3,30 @@
|
|
|
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
|
|
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
|
|
28
|
+
include Engine::Culling
|
|
29
|
+
|
|
23
30
|
def initialize(sheet:, z: 0)
|
|
24
31
|
super()
|
|
25
32
|
@sheet = sheet
|
|
@@ -31,30 +38,31 @@ module RGame
|
|
|
31
38
|
@animator = Engine::Animator.new(Engine::AnimationSet.new(sheet.animations))
|
|
32
39
|
node.width = sheet.frame_width
|
|
33
40
|
node.height = sheet.frame_height
|
|
34
|
-
@
|
|
41
|
+
@mover = require_sibling(Mover)
|
|
35
42
|
end
|
|
36
43
|
|
|
37
44
|
def update(dt)
|
|
38
|
-
@animator.play(walk_animation(@
|
|
45
|
+
@animator.play(walk_animation(@mover.heading_x, @mover.heading_y))
|
|
39
46
|
@animator.update(dt)
|
|
40
47
|
end
|
|
41
48
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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.
|
|
52
|
+
def draw(renderer, view)
|
|
53
|
+
lift = node.elevation
|
|
54
|
+
return if culled?(view, node.world_x, node.world_y - lift, node.width, node.height)
|
|
55
|
+
|
|
56
|
+
renderer.sprite(@sheet, @animator.row, @animator.col, 0, -lift,
|
|
46
57
|
flip_x: @animator.flip_x, z: @layer)
|
|
47
58
|
end
|
|
48
59
|
|
|
49
60
|
private
|
|
50
61
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
elsif move_y.negative? then :walk_up
|
|
56
|
-
elsif move_y.positive? then :walk_down
|
|
57
|
-
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
|
|
58
66
|
end
|
|
59
67
|
end
|
|
60
68
|
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
module Components
|
|
6
|
+
# A rectangular collision shape on a node — the sibling of CircleCollider for
|
|
7
|
+
# entities that are honestly box-shaped (a crate, a platform, a wall segment).
|
|
8
|
+
# It registers itself with the scene's CollisionWorld when it enters the tree
|
|
9
|
+
# and unregisters on leaving, so a spawned/despawned entity can't leak a
|
|
10
|
+
# registration. A scene with no world mounted leaves it a bare shape (see
|
|
11
|
+
# on_attach).
|
|
12
|
+
#
|
|
13
|
+
# The rectangle is an Engine::CollisionBox: an offset + size relative to the
|
|
14
|
+
# node's origin, so a 32x32 sprite can carry a small box at its feet —
|
|
15
|
+
# FeetCollider is the subclass that works that offset out for you. The `layer`
|
|
16
|
+
# is an opaque tag the game reads in its on_hit handler to decide what a contact
|
|
17
|
+
# means. See docs/api/systems.md.
|
|
18
|
+
#
|
|
19
|
+
# The box stays axis-aligned in world space: it does not rotate with the node.
|
|
20
|
+
# That is what an AABB buys — a spinning entity wants a CircleCollider, which
|
|
21
|
+
# is rotation-invariant, rather than a per-frame box recompute.
|
|
22
|
+
class BoxCollider < Engine::Component
|
|
23
|
+
# The two edges of a contact, fired by CollisionWorld: on_hit on the step this
|
|
24
|
+
# collider starts overlapping another, on_separated on the step it stops. Each
|
|
25
|
+
# fires once per pair, so a handler may count, play a sound or spend a life.
|
|
26
|
+
# The listener gets the other collider and reads its #layer / #node to react.
|
|
27
|
+
signal :on_hit, Engine::Signal.define(:other)
|
|
28
|
+
signal :on_separated, Engine::Signal.define(:other)
|
|
29
|
+
|
|
30
|
+
# box is writable so a pooled entity can retune its shape on reset — assign any
|
|
31
|
+
# CollisionBox, CollisionBox.bottom_anchored(...) included. CollisionWorld reads
|
|
32
|
+
# it fresh each frame, so no re-registration.
|
|
33
|
+
attr_accessor :box
|
|
34
|
+
attr_reader :layer
|
|
35
|
+
|
|
36
|
+
# CollisionWorld's per-collider bookkeeping: who this was touching this step and
|
|
37
|
+
# last. The world owns what goes in it; nothing else should write to it.
|
|
38
|
+
attr_reader :contacts
|
|
39
|
+
|
|
40
|
+
def initialize(width:, height:, offset_x: 0, offset_y: 0, layer: :default)
|
|
41
|
+
super()
|
|
42
|
+
@box = Engine::CollisionBox.new(width:, height:, offset_x:, offset_y:)
|
|
43
|
+
@layer = layer
|
|
44
|
+
@contacts = Engine::ContactSet.new
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# A collider is a *shape*; a CollisionWorld is what turns shapes into contacts. So
|
|
48
|
+
# a scene with no world mounted leaves this a bare shape instead of raising, which
|
|
49
|
+
# is what a tile-only game wants: its character's feet box stops its steps (that is
|
|
50
|
+
# a Mover's `blocked_by`) and there are no pairs to report to anyone.
|
|
51
|
+
#
|
|
52
|
+
# The cost is that an on_hit handler in such a scene never fires and nothing says
|
|
53
|
+
# so. That is the trade taken deliberately: what declares "I expect to be stopped"
|
|
54
|
+
# is `blocked_by`, and that *does* raise for a system it cannot find.
|
|
55
|
+
def on_attach = node.system(CollisionWorld)&.register(self)
|
|
56
|
+
def on_detach = node.system(CollisionWorld)&.unregister(self)
|
|
57
|
+
|
|
58
|
+
# The broadphase AABB in world space, one component per call rather than
|
|
59
|
+
# CollisionBox#aabb's Array: CollisionWorld reads these for every collider
|
|
60
|
+
# every frame, and that path may not allocate.
|
|
61
|
+
#
|
|
62
|
+
# These go through `box` rather than @box so a subclass that builds its shape
|
|
63
|
+
# lazily — FeetCollider, which cannot know the node's size until the tree is
|
|
64
|
+
# live — is seen by the broadphase too. An attr_reader call allocates nothing.
|
|
65
|
+
def aabb_x = node.world_x + box.offset_x
|
|
66
|
+
def aabb_y = node.world_y + box.offset_y
|
|
67
|
+
def aabb_w = box.width
|
|
68
|
+
def aabb_h = box.height
|
|
69
|
+
|
|
70
|
+
# World-space centre of the box — what CollisionWorld's range queries measure
|
|
71
|
+
# from, so a box collider is targetable on the same terms as a circle. Note it
|
|
72
|
+
# is the box's centre, not the node's origin, which is where a circle's is.
|
|
73
|
+
def cx = aabb_x + (box.width / 2.0)
|
|
74
|
+
def cy = aabb_y + (box.height / 2.0)
|
|
75
|
+
|
|
76
|
+
# Narrowphase, first half of the double dispatch: hand this shape's numbers to
|
|
77
|
+
# the *other* collider and let it pick the test, so neither side has to ask what
|
|
78
|
+
# kind the other is. CircleCollider#overlap? is the mirror image.
|
|
79
|
+
def overlap?(other) = other.overlap_box?(aabb_x, aabb_y, aabb_w, aabb_h)
|
|
80
|
+
|
|
81
|
+
# Second half: the two tests another collider dispatches into.
|
|
82
|
+
#
|
|
83
|
+
# @api private
|
|
84
|
+
def overlap_box?(x, y, w, h)
|
|
85
|
+
Engine::CollisionBox.overlap?(aabb_x, aabb_y, aabb_w, aabb_h, x, y, w, h)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# @api private
|
|
89
|
+
def overlap_circle?(x, y, r)
|
|
90
|
+
Engine::CollisionBox.overlap_circle?(aabb_x, aabb_y, aabb_w, aabb_h, x, y, r)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Called by CollisionWorld on each edge (the signals' emit is otherwise private).
|
|
94
|
+
def emit_hit(other) = on_hit_signal.emit(other)
|
|
95
|
+
def emit_separated(other) = on_separated_signal.emit(other)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
module Components
|
|
6
|
+
# Points a camera at the node it is attached to.
|
|
7
|
+
#
|
|
8
|
+
# player_node.add_component(CameraFollow.new(camera: players.primary.camera))
|
|
9
|
+
#
|
|
10
|
+
# ## Ownership and behaviour are different questions
|
|
11
|
+
#
|
|
12
|
+
# The camera cannot be *owned* by a node in the world — with several
|
|
13
|
+
# viewers there are several cameras, and a world that holds one has to know
|
|
14
|
+
# how many times it is being drawn. But deciding *where a camera points* is
|
|
15
|
+
# exactly a per-node concern, so it belongs here: the player owns the
|
|
16
|
+
# camera, and a component in the world moves it.
|
|
17
|
+
#
|
|
18
|
+
# That also makes "player two's camera follows player two" nothing more
|
|
19
|
+
# than attaching this to their node with their camera.
|
|
20
|
+
#
|
|
21
|
+
# `offset_x` / `offset_y` shift the point being centred on, for a node
|
|
22
|
+
# whose origin is not what should be in the middle of the screen — a
|
|
23
|
+
# bottom-anchored sprite usually wants its feet, not its head.
|
|
24
|
+
class CameraFollow < Engine::Component
|
|
25
|
+
def initialize(camera:, offset_x: 0.0, offset_y: 0.0)
|
|
26
|
+
super()
|
|
27
|
+
@camera = camera
|
|
28
|
+
@offset_x = offset_x
|
|
29
|
+
@offset_y = offset_y
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# A node runs its components before its own `on_update`, so this reads
|
|
33
|
+
# the node's world position from before whatever moves it this tick, and
|
|
34
|
+
# the camera trails the node's own movement by one step (a couple of
|
|
35
|
+
# pixels at walking speed). That is deliberate and uniform: everything
|
|
36
|
+
# drawn through this camera trails equally, so nothing drifts apart on
|
|
37
|
+
# screen. Reading it later — from the node's own hook, say — would put
|
|
38
|
+
# this component's ordering among its siblings on show instead.
|
|
39
|
+
def update(_dt)
|
|
40
|
+
@camera.center_on(node.world_x + @offset_x, node.world_y + @offset_y)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -3,42 +3,33 @@
|
|
|
3
3
|
module RGame
|
|
4
4
|
module Engine
|
|
5
5
|
module Components
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# integrates blindly): here every step is collision-checked.
|
|
6
|
+
# Direct, per-step movement for a walking actor (player or NPC). A controller writes a
|
|
7
|
+
# movement intent — each axis in -1..1 — and this component turns it into a real move
|
|
8
|
+
# each update, at a fixed speed and with no inertia (unlike Velocity, which integrates
|
|
9
|
+
# a velocity the controller sets, and ThrustController, which accelerates one).
|
|
11
10
|
#
|
|
12
|
-
# The intent doubles as the
|
|
11
|
+
# The intent doubles as the mover's heading, which is what AnimatedSprite faces by, so
|
|
13
12
|
# a character is just CharacterBody + a controller + AnimatedSprite.
|
|
14
13
|
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
|
|
14
|
+
# What may stop a step — `blocked_by:`, `on_blocked` / `on_unblocked`, and the
|
|
15
|
+
# `apply_move` seam — is Mover's, and shared with every other component that moves a
|
|
16
|
+
# node; see its header. What is this class's own is the intent and the speed.
|
|
17
|
+
#
|
|
18
|
+
# CharacterBody.new(speed: 80) # walks wherever the intent points
|
|
19
|
+
# CharacterBody.new(speed: 80, blocked_by: %i[tiles npc]) # stopped by the map and by NPCs
|
|
20
|
+
class CharacterBody < Mover
|
|
21
21
|
attr_reader :move_x, :move_y
|
|
22
22
|
|
|
23
|
-
def initialize(
|
|
24
|
-
super()
|
|
25
|
-
@feet_width = feet_width
|
|
26
|
-
@feet_height = feet_height
|
|
23
|
+
def initialize(speed:, blocked_by: [])
|
|
24
|
+
super(blocked_by: blocked_by)
|
|
27
25
|
@speed = speed
|
|
28
26
|
@move_x = 0.0
|
|
29
27
|
@move_y = 0.0
|
|
30
|
-
@collision_box = nil
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def collision_box
|
|
34
|
-
@collision_box ||= Engine::CollisionBox.bottom_anchored(
|
|
35
|
-
sprite_width: node.width, sprite_height: node.height,
|
|
36
|
-
width: @feet_width, height: @feet_height
|
|
37
|
-
)
|
|
38
28
|
end
|
|
39
29
|
|
|
40
|
-
# The
|
|
41
|
-
def
|
|
30
|
+
# The heading is the intent as set, blocked or not.
|
|
31
|
+
def heading_x = @move_x
|
|
32
|
+
def heading_y = @move_y
|
|
42
33
|
|
|
43
34
|
# Set this step's movement intent; each axis is in -1..1.
|
|
44
35
|
def set_intent(intent_x, intent_y)
|
|
@@ -46,23 +37,12 @@ module RGame
|
|
|
46
37
|
@move_y = intent_y
|
|
47
38
|
end
|
|
48
39
|
|
|
49
|
-
|
|
50
|
-
return if @move_x.zero? && @move_y.zero?
|
|
40
|
+
private
|
|
51
41
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
# CollisionSystem#move drives an "actor" through x/y/collision_box and writes the
|
|
56
|
-
# resolved position back — back those by the owning node's transform.
|
|
57
|
-
def x = node.x
|
|
58
|
-
def y = node.y
|
|
59
|
-
|
|
60
|
-
def x=(value)
|
|
61
|
-
node.x = value
|
|
62
|
-
end
|
|
42
|
+
def take_step(dt)
|
|
43
|
+
return if @move_x.zero? && @move_y.zero?
|
|
63
44
|
|
|
64
|
-
|
|
65
|
-
node.y = value
|
|
45
|
+
apply_move(@move_x * @speed * dt, @move_y * @speed * dt)
|
|
66
46
|
end
|
|
67
47
|
end
|
|
68
48
|
end
|
|
@@ -6,38 +6,74 @@ module RGame
|
|
|
6
6
|
# A circular collision shape on a node. It registers itself with the scene's
|
|
7
7
|
# CollisionWorld system when it enters the tree and unregisters on leaving —
|
|
8
8
|
# the engine fires both hooks, so a spawned/despawned entity can't leak a
|
|
9
|
-
# registration. Its world centre is the node's
|
|
9
|
+
# registration. Its world centre is the node's world origin; the
|
|
10
10
|
# `layer` is an opaque tag the game reads in its on_hit handler to decide what a
|
|
11
|
-
# contact means
|
|
11
|
+
# contact means. See docs/api/systems.md.
|
|
12
|
+
#
|
|
13
|
+
# BoxCollider is the rectangular sibling, and the two collide with each other:
|
|
14
|
+
# both answer the same broadphase (#aabb_*) and narrowphase (#overlap?) protocol.
|
|
12
15
|
class CircleCollider < Engine::Component
|
|
13
|
-
#
|
|
14
|
-
#
|
|
16
|
+
# The two edges of a contact, fired by CollisionWorld: on_hit on the step this
|
|
17
|
+
# collider starts overlapping another, on_separated on the step it stops. Each
|
|
18
|
+
# fires once per pair, so a handler may count, play a sound or spend a life.
|
|
19
|
+
# The listener gets the other collider and reads its #layer / #node to react.
|
|
15
20
|
signal :on_hit, Engine::Signal.define(:other)
|
|
21
|
+
signal :on_separated, Engine::Signal.define(:other)
|
|
16
22
|
|
|
17
23
|
# radius is writable so a pooled entity (e.g. a multi-tier rock) can retune its
|
|
18
24
|
# shape on reset; CollisionWorld reads it fresh each frame, so no re-registration.
|
|
19
25
|
attr_accessor :radius
|
|
20
26
|
attr_reader :layer
|
|
21
27
|
|
|
28
|
+
# CollisionWorld's per-collider bookkeeping: who this was touching this step and
|
|
29
|
+
# last. The world owns what goes in it; nothing else should write to it.
|
|
30
|
+
attr_reader :contacts
|
|
31
|
+
|
|
22
32
|
def initialize(radius:, layer: :default)
|
|
23
33
|
super()
|
|
24
34
|
@radius = radius
|
|
25
35
|
@layer = layer
|
|
36
|
+
@contacts = Engine::ContactSet.new
|
|
26
37
|
end
|
|
27
38
|
|
|
28
|
-
|
|
39
|
+
# A collider is a *shape*; a CollisionWorld is what turns shapes into contacts.
|
|
40
|
+
# A scene with no world mounted therefore leaves this a bare shape rather than
|
|
41
|
+
# raising — BoxCollider#on_attach says why, and it is the same trade here.
|
|
42
|
+
def on_attach = node.system(CollisionWorld)&.register(self)
|
|
29
43
|
def on_detach = node.system(CollisionWorld)&.unregister(self)
|
|
30
44
|
|
|
31
|
-
# World-space centre — the node's
|
|
32
|
-
def cx = node.
|
|
33
|
-
def cy = node.
|
|
45
|
+
# World-space centre — the node's own origin, in world coordinates.
|
|
46
|
+
def cx = node.world_x
|
|
47
|
+
def cy = node.world_y
|
|
48
|
+
|
|
49
|
+
# The circle's bounding box, one component per call rather than an Array:
|
|
50
|
+
# CollisionWorld reads these for every collider every frame, and that path may
|
|
51
|
+
# not allocate.
|
|
52
|
+
def aabb_x = cx - @radius
|
|
53
|
+
def aabb_y = cy - @radius
|
|
54
|
+
def aabb_w = @radius * 2
|
|
55
|
+
def aabb_h = @radius * 2
|
|
56
|
+
|
|
57
|
+
# Narrowphase, first half of the double dispatch: hand this shape's numbers to
|
|
58
|
+
# the *other* collider and let it pick the test, so neither side has to ask what
|
|
59
|
+
# kind the other is. BoxCollider#overlap? is the mirror image.
|
|
60
|
+
def overlap?(other) = other.overlap_circle?(cx, cy, @radius)
|
|
61
|
+
|
|
62
|
+
# Second half: the two tests another collider dispatches into.
|
|
63
|
+
#
|
|
64
|
+
# @api private
|
|
65
|
+
def overlap_circle?(x, y, r)
|
|
66
|
+
Engine::CircleCollider.overlap?(cx, cy, @radius, x, y, r)
|
|
67
|
+
end
|
|
34
68
|
|
|
35
|
-
|
|
36
|
-
|
|
69
|
+
# @api private
|
|
70
|
+
def overlap_box?(x, y, w, h)
|
|
71
|
+
Engine::CollisionBox.overlap_circle?(x, y, w, h, cx, cy, @radius)
|
|
37
72
|
end
|
|
38
73
|
|
|
39
|
-
# Called by CollisionWorld on
|
|
74
|
+
# Called by CollisionWorld on each edge (the signals' emit is otherwise private).
|
|
40
75
|
def emit_hit(other) = on_hit_signal.emit(other)
|
|
76
|
+
def emit_separated(other) = on_separated_signal.emit(other)
|
|
41
77
|
end
|
|
42
78
|
end
|
|
43
79
|
end
|
|
@@ -5,10 +5,31 @@ module RGame
|
|
|
5
5
|
module Components
|
|
6
6
|
# Scene-scoped broadphase collision system: a Component that lives on the scene
|
|
7
7
|
# node (so it is born and torn down with the scene, and rides the normal update
|
|
8
|
-
# traversal).
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
8
|
+
# traversal). Colliders register/unregister with it via their tree lifecycle;
|
|
9
|
+
# each update it buckets them in a SpatialHash and reports every overlapping
|
|
10
|
+
# pair. It is layer-agnostic — it reports contacts and lets the colliders'
|
|
11
|
+
# owners decide meaning. See docs/api/systems.md.
|
|
12
|
+
#
|
|
13
|
+
# **A contact is reported as two edges, not as a state.** `on_hit` fires on the
|
|
14
|
+
# step a pair starts overlapping and `on_separated` on the step it stops, each
|
|
15
|
+
# once, on both colliders. Nothing fires in between, so a handler is free to
|
|
16
|
+
# count, to play a sound, or to do anything else that must happen once — which
|
|
17
|
+
# is the whole reason the world keeps a ContactSet per collider rather than
|
|
18
|
+
# simply forwarding what the broadphase found.
|
|
19
|
+
#
|
|
20
|
+
# It is also *shape*-agnostic, which is what lets CircleCollider and BoxCollider
|
|
21
|
+
# share it (and collide with each other). A collider is anything answering:
|
|
22
|
+
#
|
|
23
|
+
# aabb_x, aabb_y, aabb_w, aabb_h its world-space bounding box, for bucketing
|
|
24
|
+
# cx, cy its centre, for the range queries below
|
|
25
|
+
# overlap?(other) the narrowphase, which the two colliders
|
|
26
|
+
# settle between themselves by double dispatch
|
|
27
|
+
# layer, node the tag and the owner
|
|
28
|
+
# contacts an Engine::ContactSet, which this drives
|
|
29
|
+
# emit_hit(other) the contact's two edges
|
|
30
|
+
# emit_separated(other)
|
|
31
|
+
#
|
|
32
|
+
# None of those may allocate: they run per collider per frame.
|
|
12
33
|
class CollisionWorld < Engine::Component
|
|
13
34
|
def initialize(cell_size:)
|
|
14
35
|
super()
|
|
@@ -16,13 +37,22 @@ module RGame
|
|
|
16
37
|
@colliders = []
|
|
17
38
|
end
|
|
18
39
|
|
|
19
|
-
|
|
40
|
+
# Reset rather than merely add: the collider may be a pooled one coming back
|
|
41
|
+
# from the dead, still carrying the contacts it held when it was freed.
|
|
42
|
+
def register(collider)
|
|
43
|
+
collider.contacts.reset
|
|
44
|
+
@colliders << collider
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# The partners of a collider that leaves are told on their next step, by the
|
|
48
|
+
# ordinary separation pass — it is gone from the index, so the pair no longer
|
|
49
|
+
# overlaps. Nothing has to be emitted here.
|
|
20
50
|
def unregister(collider) = @colliders.delete(collider)
|
|
21
51
|
|
|
22
52
|
# Yield every registered collider whose centre lies within `r` of (x, y), using
|
|
23
53
|
# the spatial index built by the most recent #update. The narrowphase is a
|
|
24
|
-
# centre-distance test — the query is a point + range (a
|
|
25
|
-
# the collider's own
|
|
54
|
+
# centre-distance test — the query is a point + range (a turret's range ring), so
|
|
55
|
+
# the collider's own size isn't added in. Colliders whose node is queued for
|
|
26
56
|
# removal are skipped. As with SpatialHash#query a collider spanning several cells
|
|
27
57
|
# may be yielded more than once, so callers that *select* (e.g. #nearest) are
|
|
28
58
|
# written dup-insensitively. Layer-agnostic — filter by `collider.layer` in the
|
|
@@ -38,8 +68,47 @@ module RGame
|
|
|
38
68
|
end
|
|
39
69
|
end
|
|
40
70
|
|
|
71
|
+
# Yield every registered collider bucketed in a cell the region covers, skipping
|
|
72
|
+
# those whose node is queued for removal. The rectangular counterpart to
|
|
73
|
+
# #query_circle, and what a blocker source asks when it resolves a step: "what is
|
|
74
|
+
# near this box".
|
|
75
|
+
#
|
|
76
|
+
# It inherits #query_circle's dedup contract — a collider spanning several cells
|
|
77
|
+
# may be yielded more than once — so a caller that *selects* is written
|
|
78
|
+
# dup-insensitively, the way #nearest and Engine::ActorBlockers both are. Unlike
|
|
79
|
+
# #query_circle there is no narrowphase here at all: the bucket walk is the whole
|
|
80
|
+
# answer, and refining it is the caller's business. Layer-agnostic; filter by
|
|
81
|
+
# `collider.layer` in the block. Allocation-free.
|
|
82
|
+
def query_box(x, y, w, h)
|
|
83
|
+
@hash.query(x, y, w, h) do |collider|
|
|
84
|
+
next if collider.node.freed?
|
|
85
|
+
|
|
86
|
+
yield collider
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Re-bucket a collider that has moved since #update built the index, so a query
|
|
91
|
+
# later in the same step still finds it where it now is. `from_*` is the box it
|
|
92
|
+
# was bucketed at — the caller knows it, which is what lets the index keep no
|
|
93
|
+
# per-item state of its own.
|
|
94
|
+
#
|
|
95
|
+
# This is what makes a mid-step query exact rather than nearly right. Buckets are
|
|
96
|
+
# filled once per step and a collider that moves afterwards is still bucketed
|
|
97
|
+
# where it was, so a query over cells it has left does not reach it. Measured over
|
|
98
|
+
# 60,000 queries at two hundred actors: 116 misses left stale, 0 re-indexed, and
|
|
99
|
+
# padding the query instead is not exact at any pad — it compensates for the
|
|
100
|
+
# *other* actor's staleness by inflating the *mover's* step. Engine::CollisionSystem
|
|
101
|
+
# calls this through a blocker source's #moved, so nothing a game writes has to
|
|
102
|
+
# remember it; anything else that moves a collider mid-step may call it directly.
|
|
103
|
+
#
|
|
104
|
+
# Allocation-free, so a resolver may call it every step.
|
|
105
|
+
def reindex(collider, from_x, from_y, from_w, from_h)
|
|
106
|
+
@hash.remove(collider, from_x, from_y, from_w, from_h)
|
|
107
|
+
insert(collider)
|
|
108
|
+
end
|
|
109
|
+
|
|
41
110
|
# The registered collider nearest to (x, y) within range `r`, or nil when none
|
|
42
|
-
# qualifies. Restrict to a single `layer:` (the common case: a
|
|
111
|
+
# qualifies. Restrict to a single `layer:` (the common case: a turret targeting only
|
|
43
112
|
# :enemy). Dup-safe — it keeps the running minimum, so #query_circle's possible
|
|
44
113
|
# multi-cell repeats don't matter. Allocation-free.
|
|
45
114
|
def nearest(x, y, r, layer: nil)
|
|
@@ -59,15 +128,64 @@ module RGame
|
|
|
59
128
|
best
|
|
60
129
|
end
|
|
61
130
|
|
|
131
|
+
# Is the cell containing the *world* point (x, y) free — "may a pickup spawn on
|
|
132
|
+
# this square?" A point, not a region: pass any coordinate inside the square you
|
|
133
|
+
# mean. The cells are the broadphase's own, so set `cell_size` to the game's
|
|
134
|
+
# square and the two grids line up.
|
|
135
|
+
#
|
|
136
|
+
# Two coordinate gotchas, both easy to get wrong from a node that has neither in
|
|
137
|
+
# mind:
|
|
138
|
+
#
|
|
139
|
+
# - The index holds **world** coordinates, because that is what a collider
|
|
140
|
+
# reports. A node whose own grid starts somewhere else adds its world origin
|
|
141
|
+
# before asking (`node.world_x + col * cell_size`) — and puts that origin on a
|
|
142
|
+
# multiple of `cell_size`, since the cells are the hash's own lattice anchored
|
|
143
|
+
# at the world origin. A board off that lattice has each square straddling two
|
|
144
|
+
# cells, and both read occupied.
|
|
145
|
+
# - It answers about the index the most recent #update built, exactly as
|
|
146
|
+
# #query_circle and #nearest do. Before the first update every cell is empty.
|
|
147
|
+
#
|
|
148
|
+
# The bucket is the answer, geometry and all: the broadphase's cell walk is
|
|
149
|
+
# half-open exactly like CollisionBox.overlap?, so a collider is bucketed in a
|
|
150
|
+
# cell if and only if it overlaps that cell's area. All this adds is the one
|
|
151
|
+
# thing the index cannot know — that a collider queued for removal no longer
|
|
152
|
+
# occupies anything, the same rule the queries above follow, so a corpse cannot
|
|
153
|
+
# reserve a square.
|
|
154
|
+
#
|
|
155
|
+
# Allocation-free, including the miss: SpatialHash#cell_empty? is asked first
|
|
156
|
+
# because it is the only way to look at a cell without materialising a bucket for
|
|
157
|
+
# it, and a caller scanning a board for a free square asks mostly about empty
|
|
158
|
+
# ones.
|
|
159
|
+
def cell_empty?(x, y)
|
|
160
|
+
return true if @hash.cell_empty?(x, y)
|
|
161
|
+
|
|
162
|
+
free = true
|
|
163
|
+
@hash.query(x, y, 0, 0) { |collider| free &&= collider.node.freed? }
|
|
164
|
+
free
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Rebuild the index, then report the step's two kinds of edge. Starting edges
|
|
168
|
+
# come first and every one of them for the step is emitted before the first
|
|
169
|
+
# ending one, because a separation is only knowable once every pair has been
|
|
170
|
+
# looked at.
|
|
62
171
|
def update(_dt)
|
|
63
172
|
@hash.clear
|
|
64
|
-
@colliders.each
|
|
173
|
+
@colliders.each do |collider|
|
|
174
|
+
collider.contacts.begin_frame
|
|
175
|
+
insert(collider)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
report_contacts
|
|
179
|
+
report_separations
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
private
|
|
65
183
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
184
|
+
def insert(collider)
|
|
185
|
+
@hash.insert(collider, collider.aabb_x, collider.aabb_y, collider.aabb_w, collider.aabb_h)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def report_contacts
|
|
71
189
|
count = @colliders.size
|
|
72
190
|
i = 0
|
|
73
191
|
while i < count
|
|
@@ -75,27 +193,37 @@ module RGame
|
|
|
75
193
|
i += 1
|
|
76
194
|
next if a.node.freed?
|
|
77
195
|
|
|
78
|
-
|
|
79
|
-
d = r * 2
|
|
80
|
-
@hash.query(a.cx - r, a.cy - r, d, d) do |b|
|
|
81
|
-
# object_id ordering visits each unordered pair once (and skips self);
|
|
82
|
-
# the freed? guards skip nodes already queued for removal, so a dead
|
|
83
|
-
# entity stops colliding and duplicate (multi-cell) yields are ignored.
|
|
84
|
-
next if a.node.freed? || b.node.freed? || a.object_id >= b.object_id
|
|
85
|
-
next unless a.overlap?(b)
|
|
86
|
-
|
|
87
|
-
a.emit_hit(b)
|
|
88
|
-
b.emit_hit(a)
|
|
89
|
-
end
|
|
196
|
+
pair_up(a)
|
|
90
197
|
end
|
|
91
198
|
end
|
|
92
199
|
|
|
93
|
-
|
|
200
|
+
def pair_up(a)
|
|
201
|
+
contacts = a.contacts
|
|
202
|
+
@hash.query(a.aabb_x, a.aabb_y, a.aabb_w, a.aabb_h) do |b|
|
|
203
|
+
next if a.node.freed? || b.node.freed? || a.object_id >= b.object_id
|
|
204
|
+
next if contacts.touching?(b)
|
|
205
|
+
next unless a.overlap?(b)
|
|
94
206
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
207
|
+
started = contacts.started?(b)
|
|
208
|
+
contacts.add(b)
|
|
209
|
+
b.contacts.add(a)
|
|
210
|
+
next unless started
|
|
211
|
+
|
|
212
|
+
a.emit_hit(b)
|
|
213
|
+
b.emit_hit(a)
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def report_separations
|
|
218
|
+
count = @colliders.size
|
|
219
|
+
i = 0
|
|
220
|
+
while i < count
|
|
221
|
+
collider = @colliders[i]
|
|
222
|
+
i += 1
|
|
223
|
+
next if collider.node.freed?
|
|
224
|
+
|
|
225
|
+
collider.contacts.each_ended { |other| collider.emit_separated(other) }
|
|
226
|
+
end
|
|
99
227
|
end
|
|
100
228
|
end
|
|
101
229
|
end
|