rgame 0.4.0-x86_64-linux-gnu
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +251 -0
- data/LICENSE +26 -0
- data/README.md +325 -0
- data/docs/api/README.md +224 -0
- data/docs/api/app.md +249 -0
- data/docs/api/assets.md +463 -0
- data/docs/api/audio.md +249 -0
- data/docs/api/cli.md +259 -0
- data/docs/api/components.md +1112 -0
- data/docs/api/drawing.md +398 -0
- data/docs/api/examples.md +263 -0
- data/docs/api/game.md +219 -0
- data/docs/api/images.md +118 -0
- data/docs/api/input.md +476 -0
- data/docs/api/internals.md +289 -0
- data/docs/api/localization.md +285 -0
- data/docs/api/scene_graph.md +539 -0
- data/docs/api/signals.md +147 -0
- data/docs/api/systems.md +231 -0
- data/docs/api/text.md +133 -0
- data/docs/api/tile_maps.md +237 -0
- data/docs/api/toolbox.md +493 -0
- data/docs/api/ui.md +927 -0
- data/docs/api/values.md +316 -0
- data/examples/assets/README.md +322 -0
- data/examples/assets/blip.ogg +0 -0
- data/examples/assets/glyphs.json +5 -0
- data/examples/assets/glyphs.png +0 -0
- data/examples/assets/hero.json +12 -0
- data/examples/assets/hero.png +0 -0
- data/examples/assets/icons.json +13 -0
- data/examples/assets/icons.png +0 -0
- data/examples/assets/music.ogg +0 -0
- data/examples/assets/skills.json +10 -0
- data/examples/assets/skills.png +0 -0
- data/examples/assets/tileset.png +0 -0
- data/examples/assets/tileset.tsx +65 -0
- data/examples/assets/town.tmx +26 -0
- data/examples/assets/ui.json +11 -0
- data/examples/assets/ui.png +0 -0
- data/examples/collision/locales/en.yml +8 -0
- data/examples/collision/main.rb +316 -0
- data/examples/collision_tiles/locales/en.yml +9 -0
- data/examples/collision_tiles/main.rb +274 -0
- data/examples/fullscreen/locales/en.yml +10 -0
- data/examples/fullscreen/main.rb +216 -0
- data/examples/game_menu/locales/en.yml +8 -0
- data/examples/game_menu/main.rb +170 -0
- data/examples/input_glyphs/locales/en.yml +14 -0
- data/examples/input_glyphs/main.rb +213 -0
- data/examples/jump_topdown/locales/en.yml +9 -0
- data/examples/jump_topdown/main.rb +178 -0
- data/examples/localization/locales/de.yml +12 -0
- data/examples/localization/locales/en.yml +13 -0
- data/examples/localization/main.rb +158 -0
- data/examples/menu_navigation/locales/en.yml +23 -0
- data/examples/menu_navigation/main.rb +365 -0
- data/examples/music/locales/en.yml +7 -0
- data/examples/music/main.rb +134 -0
- data/examples/pathfinding/locales/en.yml +17 -0
- data/examples/pathfinding/main.rb +298 -0
- data/examples/pooling/locales/en.yml +7 -0
- data/examples/pooling/main.rb +259 -0
- data/examples/quick_wheel/locales/en.yml +16 -0
- data/examples/quick_wheel/main.rb +184 -0
- data/examples/radial_menu/locales/en.yml +16 -0
- data/examples/radial_menu/main.rb +184 -0
- data/examples/save_load/locales/en.yml +11 -0
- data/examples/save_load/main.rb +207 -0
- data/examples/save_load_ids/locales/en.yml +11 -0
- data/examples/save_load_ids/main.rb +322 -0
- data/examples/scroll_map/locales/en.yml +4 -0
- data/examples/scroll_map/main.rb +140 -0
- data/examples/signals/locales/en.yml +6 -0
- data/examples/signals/main.rb +278 -0
- data/examples/skill_bar/locales/en.yml +14 -0
- data/examples/skill_bar/main.rb +159 -0
- data/examples/sound/locales/en.yml +6 -0
- data/examples/sound/main.rb +122 -0
- data/examples/split_screen/locales/en.yml +9 -0
- data/examples/split_screen/main.rb +304 -0
- data/examples/sprite/locales/en.yml +8 -0
- data/examples/sprite/main.rb +180 -0
- data/examples/timer/locales/en.yml +12 -0
- data/examples/timer/main.rb +273 -0
- data/examples/velocity/locales/en.yml +6 -0
- data/examples/velocity/main.rb +196 -0
- data/examples/walk/locales/en.yml +4 -0
- data/examples/walk/main.rb +99 -0
- data/exe/rgame +9 -0
- data/lib/rgame/boot.rb +3 -0
- data/lib/rgame/cli/new_project.rb +139 -0
- data/lib/rgame/cli/templates/Gemfile.tt +23 -0
- data/lib/rgame/cli/templates/README.md.tt +93 -0
- data/lib/rgame/cli/templates/Rakefile.tt +9 -0
- data/lib/rgame/cli/templates/assets/locales/en.yml.tt +10 -0
- data/lib/rgame/cli/templates/game.rb.tt +23 -0
- data/lib/rgame/cli/templates/gitignore.tt +12 -0
- data/lib/rgame/cli/templates/main.rb.tt +11 -0
- data/lib/rgame/cli/templates/nodes/root.rb.tt +24 -0
- data/lib/rgame/cli/templates/rspec.tt +2 -0
- data/lib/rgame/cli/templates/rubocop.yml.tt +75 -0
- data/lib/rgame/cli/templates/ruby-version.tt +1 -0
- data/lib/rgame/cli/templates/spec/locales_spec.rb.tt +18 -0
- data/lib/rgame/cli/templates/spec/nodes/root_spec.rb.tt +18 -0
- data/lib/rgame/cli/templates/spec/spec_helper.rb.tt +37 -0
- data/lib/rgame/cli.rb +66 -0
- data/lib/rgame/core/app.rb +46 -0
- data/lib/rgame/core/asset_manager.rb +206 -0
- data/lib/rgame/core/audio.rb +145 -0
- data/lib/rgame/core/font.rb +46 -0
- data/lib/rgame/core/gamepad.rb +55 -0
- data/lib/rgame/core/image.rb +55 -0
- data/lib/rgame/core/input.rb +71 -0
- data/lib/rgame/core/locale.rb +22 -0
- data/lib/rgame/core/nine_slice.rb +142 -0
- data/lib/rgame/core/recording.rb +54 -0
- data/lib/rgame/core/renderer.rb +354 -0
- data/lib/rgame/core/sprite_sheet.rb +105 -0
- data/lib/rgame/core/tile_map_renderer.rb +172 -0
- data/lib/rgame/core/ui_atlas.rb +101 -0
- data/lib/rgame/core/virtual_gamepad.rb +26 -0
- data/lib/rgame/core.rb +18 -0
- data/lib/rgame/core_ext.so +0 -0
- data/lib/rgame/engine/actor_blockers.rb +131 -0
- data/lib/rgame/engine/animation_set.rb +50 -0
- data/lib/rgame/engine/animator.rb +44 -0
- data/lib/rgame/engine/audio_bus.rb +24 -0
- data/lib/rgame/engine/audio_director.rb +59 -0
- data/lib/rgame/engine/bounds_blockers.rb +74 -0
- data/lib/rgame/engine/camera.rb +78 -0
- data/lib/rgame/engine/circle_collider.rb +34 -0
- data/lib/rgame/engine/collision_box.rb +59 -0
- data/lib/rgame/engine/collision_system.rb +132 -0
- data/lib/rgame/engine/component.rb +64 -0
- data/lib/rgame/engine/components/action_trigger.rb +40 -0
- data/lib/rgame/engine/components/animated_sprite.rb +71 -0
- data/lib/rgame/engine/components/box_collider.rb +99 -0
- data/lib/rgame/engine/components/camera_follow.rb +45 -0
- data/lib/rgame/engine/components/character_body.rb +50 -0
- data/lib/rgame/engine/components/circle_collider.rb +80 -0
- data/lib/rgame/engine/components/collision_world.rb +231 -0
- data/lib/rgame/engine/components/despawn_offscreen.rb +42 -0
- data/lib/rgame/engine/components/feet_collider.rb +61 -0
- data/lib/rgame/engine/components/hop.rb +76 -0
- data/lib/rgame/engine/components/identity.rb +73 -0
- data/lib/rgame/engine/components/mover.rb +285 -0
- data/lib/rgame/engine/components/navigator.rb +145 -0
- data/lib/rgame/engine/components/path_follow.rb +176 -0
- data/lib/rgame/engine/components/player_controller.rb +27 -0
- data/lib/rgame/engine/components/pool.rb +53 -0
- data/lib/rgame/engine/components/screen_wrap.rb +49 -0
- data/lib/rgame/engine/components/sprite.rb +47 -0
- data/lib/rgame/engine/components/targeting.rb +52 -0
- data/lib/rgame/engine/components/thrust_controller.rb +65 -0
- data/lib/rgame/engine/components/tile_world.rb +109 -0
- data/lib/rgame/engine/components/timer.rb +75 -0
- data/lib/rgame/engine/components/velocity.rb +43 -0
- data/lib/rgame/engine/components/wander_controller.rb +64 -0
- data/lib/rgame/engine/components/world.rb +133 -0
- data/lib/rgame/engine/contact_set.rb +74 -0
- data/lib/rgame/engine/culling.rb +45 -0
- data/lib/rgame/engine/debug_overlay.rb +111 -0
- data/lib/rgame/engine/i18n/plural.rb +45 -0
- data/lib/rgame/engine/i18n/plural_rules.rb +82 -0
- data/lib/rgame/engine/i18n/template.rb +59 -0
- data/lib/rgame/engine/i18n.rb +322 -0
- data/lib/rgame/engine/input/action_mapper.rb +100 -0
- data/lib/rgame/engine/input/actions.rb +92 -0
- data/lib/rgame/engine/input/input_map.rb +196 -0
- data/lib/rgame/engine/layout.rb +82 -0
- data/lib/rgame/engine/nav_grid.rb +87 -0
- data/lib/rgame/engine/node2d.rb +555 -0
- data/lib/rgame/engine/path.rb +76 -0
- data/lib/rgame/engine/player.rb +69 -0
- data/lib/rgame/engine/player_layer.rb +70 -0
- data/lib/rgame/engine/players.rb +205 -0
- data/lib/rgame/engine/pool.rb +51 -0
- data/lib/rgame/engine/presentation.rb +171 -0
- data/lib/rgame/engine/scene/scene_stack.rb +87 -0
- data/lib/rgame/engine/sealed_privates.rb +54 -0
- data/lib/rgame/engine/signal.rb +75 -0
- data/lib/rgame/engine/spatial_hash.rb +116 -0
- data/lib/rgame/engine/text.rb +194 -0
- data/lib/rgame/engine/tile_blockers.rb +63 -0
- data/lib/rgame/engine/tile_map.rb +147 -0
- data/lib/rgame/engine/tile_map_layer.rb +82 -0
- data/lib/rgame/engine/tileset.rb +99 -0
- data/lib/rgame/engine/timer.rb +51 -0
- data/lib/rgame/engine/ui/button.rb +248 -0
- data/lib/rgame/engine/ui/column.rb +20 -0
- data/lib/rgame/engine/ui/icon_button.rb +93 -0
- data/lib/rgame/engine/ui/menu.rb +290 -0
- data/lib/rgame/engine/ui/navigation.rb +57 -0
- data/lib/rgame/engine/ui/nine_slice_style.rb +50 -0
- data/lib/rgame/engine/ui/option_button.rb +163 -0
- data/lib/rgame/engine/ui/panel_button.rb +32 -0
- data/lib/rgame/engine/ui/panel_menu.rb +36 -0
- data/lib/rgame/engine/ui/pointing.rb +146 -0
- data/lib/rgame/engine/ui/radial_menu.rb +85 -0
- data/lib/rgame/engine/ui/ring.rb +55 -0
- data/lib/rgame/engine/ui/row.rb +21 -0
- data/lib/rgame/engine/ui/shape_style.rb +102 -0
- data/lib/rgame/engine/ui/stack.rb +58 -0
- data/lib/rgame/engine/ui/stepping.rb +93 -0
- data/lib/rgame/engine/ui/text_button.rb +59 -0
- data/lib/rgame/engine/view.rb +76 -0
- data/lib/rgame/engine/viewports.rb +171 -0
- data/lib/rgame/engine/world_view.rb +71 -0
- data/lib/rgame/engine.rb +86 -0
- data/lib/rgame/fonts/LiberationSans-Regular.ttf +0 -0
- data/lib/rgame/fonts/OFL.txt +102 -0
- data/lib/rgame/game.rb +271 -0
- data/lib/rgame/rubocop/cop/game/draw_in_local_space.rb +103 -0
- data/lib/rgame/rubocop/cop/game/hot_path.rb +36 -0
- data/lib/rgame/rubocop/cop/game/layer_boundary.rb +43 -0
- data/lib/rgame/rubocop/cop/game/no_core_in_engine_layer.rb +100 -0
- data/lib/rgame/rubocop/cop/game/no_engine_in_core_layer.rb +84 -0
- data/lib/rgame/rubocop/cop/game/no_interpolation_in_hot_path.rb +50 -0
- data/lib/rgame/rubocop/cop/game/no_literal_text.rb +41 -0
- data/lib/rgame/rubocop/cop/game/no_needless_allocation.rb +112 -0
- data/lib/rgame/rubocop/default.yml +39 -0
- data/lib/rgame/rubocop/plugin.rb +45 -0
- data/lib/rgame/rubocop.rb +11 -0
- data/lib/rgame/util/color.rb +23 -0
- data/lib/rgame/util/controls.rb +169 -0
- data/lib/rgame/util/route_search.rb +27 -0
- data/lib/rgame/util/save_file.rb +107 -0
- data/lib/rgame/util/solid_grid.rb +37 -0
- data/lib/rgame/util/tensor.rb +3 -0
- data/lib/rgame/util/tile_sweep.rb +36 -0
- data/lib/rgame/util/z.rb +123 -0
- data/lib/rgame/util.rb +10 -0
- data/lib/rgame/util_ext.so +0 -0
- data/lib/rgame/version.rb +12 -0
- data/lib/rgame.rb +5 -0
- data/licenses/SDL2/LICENSE.txt +18 -0
- metadata +309 -0
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
# A node in a scene graph. We currently have only 2D nodes, but the
|
|
6
|
+
# name reflects this should there ever be a 3D space. Nodes are
|
|
7
|
+
# containers for both containers and nodes. They extend the signal
|
|
8
|
+
# DSL to allow for easy signal usage.
|
|
9
|
+
class Node2D
|
|
10
|
+
extend Engine::Signal::DSL
|
|
11
|
+
extend Engine::SealedPrivates
|
|
12
|
+
|
|
13
|
+
# This node's transform **in its parent's space** — where it sits inside
|
|
14
|
+
# whatever contains it, and the only position a node ever sets. `x`, `y`
|
|
15
|
+
# and `angle` are the short spelling of the same three, because that is
|
|
16
|
+
# what reads well where a node moves itself:
|
|
17
|
+
#
|
|
18
|
+
# def on_update(dt) = self.x += @speed * dt
|
|
19
|
+
#
|
|
20
|
+
# Not for drawing, though it is tempting: `on_draw` runs with the renderer
|
|
21
|
+
# already on this node (see #_in_local_space), so drawing at `x` offsets by
|
|
22
|
+
# this node's own position a second time. `Game/DrawInLocalSpace` says so.
|
|
23
|
+
#
|
|
24
|
+
# The ivar carries the longer name so that `@x` does not exist. Reaching
|
|
25
|
+
# for a parent-relative coordinate where a world one was meant is the
|
|
26
|
+
# mistake this whole design is arranged against, and the spelling that used
|
|
27
|
+
# to make it silent is simply not there any more.
|
|
28
|
+
attr_reader :rel_x, :rel_y, :rel_angle
|
|
29
|
+
alias x rel_x
|
|
30
|
+
alias y rel_y
|
|
31
|
+
alias angle rel_angle
|
|
32
|
+
|
|
33
|
+
attr_accessor :width, :height
|
|
34
|
+
|
|
35
|
+
# Height above the ground in pixels, for a top-down view: how far this
|
|
36
|
+
# node's picture is drawn above the spot it stands on. Positive is up.
|
|
37
|
+
#
|
|
38
|
+
# It is **not** part of the transform. `y`, `world_y`, colliders, cameras
|
|
39
|
+
# and children all ignore it, which is what lets a character leave the
|
|
40
|
+
# ground without its feet box leaving too. Components::Sprite and
|
|
41
|
+
# Components::AnimatedSprite draw lifted by it, in the node's local space;
|
|
42
|
+
# Components::Hop is one thing that writes it.
|
|
43
|
+
attr_accessor :elevation
|
|
44
|
+
attr_writer :scene, :context
|
|
45
|
+
|
|
46
|
+
# The other way a node moves, and the only one that does not go through a
|
|
47
|
+
# coordinate writer: its `x`/`y` do not change, but they are now an offset
|
|
48
|
+
# from somewhere else. Set by #add_node and #remove_node — a game does not
|
|
49
|
+
# call this — and it invalidates the subtree for the same reason a move
|
|
50
|
+
# does.
|
|
51
|
+
def parent=(value)
|
|
52
|
+
@parent = value
|
|
53
|
+
_soil
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Moving a node invalidates the world transform of the node *and its whole
|
|
57
|
+
# subtree* — every one of them is now somewhere else — but computes none of
|
|
58
|
+
# them. Whoever reads one next pays for that one.
|
|
59
|
+
#
|
|
60
|
+
# `node.x += dx` from a component (Components::Velocity does exactly that)
|
|
61
|
+
# is two writes and so two invalidations, which is why #_soil returns
|
|
62
|
+
# immediately on a subtree that is already stale.
|
|
63
|
+
def rel_x=(value)
|
|
64
|
+
@rel_x = value
|
|
65
|
+
_soil
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def rel_y=(value)
|
|
69
|
+
@rel_y = value
|
|
70
|
+
_soil
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def rel_angle=(value)
|
|
74
|
+
@rel_angle = value
|
|
75
|
+
_soil
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
alias x= rel_x=
|
|
79
|
+
alias y= rel_y=
|
|
80
|
+
alias angle= rel_angle=
|
|
81
|
+
|
|
82
|
+
# This node's transform **in world space**, accumulated from its whole
|
|
83
|
+
# ancestry. A node lives in its parent's space and is moved by setting
|
|
84
|
+
# `x`/`y`/`angle`; `world_x=`/`world_y=` below are that same move, worked
|
|
85
|
+
# out from a world position for the code that thinks in one.
|
|
86
|
+
#
|
|
87
|
+
# `world_` rather than `abs_`, because the name should say which space the
|
|
88
|
+
# value is in. `abs_band` and `abs_input_owner` keep theirs deliberately —
|
|
89
|
+
# those are *inherited* from the nearest ancestor that declares one rather
|
|
90
|
+
# than expressed in a space, and a band is not in world coordinates.
|
|
91
|
+
#
|
|
92
|
+
# **Computed on demand and cached**, which is how Godot and Unity do it and
|
|
93
|
+
# why no phase resolves this any more. Moving a node marks it and its whole
|
|
94
|
+
# subtree stale (see #_soil); the next read walks up to the nearest node
|
|
95
|
+
# still current, recomputing on the way back down. Two consequences worth
|
|
96
|
+
# knowing:
|
|
97
|
+
#
|
|
98
|
+
# - It is never stale. There is no "resolved at the top of the phase" value
|
|
99
|
+
# to go out of date, so a node that moved, a node whose *ancestor* moved,
|
|
100
|
+
# and a paused node under a moving ancestor all answer correctly, at any
|
|
101
|
+
# point in any phase.
|
|
102
|
+
# - Nothing is computed for a node nobody asks about. A still frame costs
|
|
103
|
+
# nothing at all, where resolving the tree eagerly cost a full pass per
|
|
104
|
+
# phase whether or not anything had moved.
|
|
105
|
+
# hot-path
|
|
106
|
+
def world_x
|
|
107
|
+
_resolve_transform unless @world_current
|
|
108
|
+
@world_x
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# hot-path
|
|
112
|
+
def world_y
|
|
113
|
+
_resolve_transform unless @world_current
|
|
114
|
+
@world_y
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Place the node at a world coordinate, leaving the other one where it is.
|
|
118
|
+
#
|
|
119
|
+
# For code that decides where a node goes in world space — the edge of the
|
|
120
|
+
# world, a resolved collision — and must still write the node's own, local
|
|
121
|
+
# position. The target is turned back into the parent's frame, so the
|
|
122
|
+
# answer is exact under a rotated ancestor too: there it moves the local
|
|
123
|
+
# position along *both* axes, which is what one world axis looks like from
|
|
124
|
+
# inside a turned frame.
|
|
125
|
+
#
|
|
126
|
+
# A node with no parent is pinned to the origin (see #_resolve_transform),
|
|
127
|
+
# so there is no local position that would put it anywhere else, and this
|
|
128
|
+
# changes nothing.
|
|
129
|
+
# hot-path
|
|
130
|
+
def world_x=(value)
|
|
131
|
+
return if @parent.nil?
|
|
132
|
+
|
|
133
|
+
pa = @parent.world_angle
|
|
134
|
+
if pa.zero?
|
|
135
|
+
self.rel_x = value - @parent.world_x
|
|
136
|
+
else
|
|
137
|
+
_place_in_rotated_parent(value - @parent.world_x, world_y - @parent.world_y, pa)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# hot-path
|
|
142
|
+
def world_y=(value)
|
|
143
|
+
return if @parent.nil?
|
|
144
|
+
|
|
145
|
+
pa = @parent.world_angle
|
|
146
|
+
if pa.zero?
|
|
147
|
+
self.rel_y = value - @parent.world_y
|
|
148
|
+
else
|
|
149
|
+
_place_in_rotated_parent(world_x - @parent.world_x, value - @parent.world_y, pa)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# hot-path
|
|
154
|
+
def world_angle
|
|
155
|
+
_resolve_transform unless @world_current
|
|
156
|
+
@world_angle
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
attr_reader :children, :components, :parent, :abs_input_owner, :z, :band, :abs_band
|
|
160
|
+
|
|
161
|
+
# Where this node sits among its **siblings**, and nowhere else.
|
|
162
|
+
#
|
|
163
|
+
# The tree is drawn depth-first with siblings in `z` order, so a node's
|
|
164
|
+
# whole subtree is drawn before or after a sibling's whole subtree —
|
|
165
|
+
# never interleaved with it. Clouds over birds over people is three
|
|
166
|
+
# children of one node at `z` 2, 1 and 0, and each of them may be built
|
|
167
|
+
# out of as many parts as it likes without any of those parts escaping.
|
|
168
|
+
#
|
|
169
|
+
# Only the *comparison* matters. `z` is never added to anything and never
|
|
170
|
+
# reaches the renderer, so its magnitude means nothing: 1 and 1_000_000
|
|
171
|
+
# behave identically if they are the only two children, and a negative is
|
|
172
|
+
# ordinary. Equal `z` keeps the order the nodes were added in.
|
|
173
|
+
#
|
|
174
|
+
# This is deliberately unlike the additive relative z it replaces
|
|
175
|
+
# (`abs_z = parent.abs_z + z`), where a node at z 2 with a child at z 5
|
|
176
|
+
# resolved to 7 and overtook a sibling at 4 — some of a node's parts in
|
|
177
|
+
# front of something the node itself was behind. See RGame::Util::Z.
|
|
178
|
+
def z=(value)
|
|
179
|
+
@z = value
|
|
180
|
+
@parent&._children_unsorted!
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Which band this node and everything under it draws in — `:world` (the
|
|
184
|
+
# default), `:hud`, `:overlay` or `:debug`. A band beats every `z` in the
|
|
185
|
+
# tree: nothing in `:world` can draw over anything in `:hud`.
|
|
186
|
+
#
|
|
187
|
+
# Inherited like `input_owner`, and normally set by a node that exists to
|
|
188
|
+
# mark one: WorldView is `:world`, PlayerLayer is `:hud`. Setting it
|
|
189
|
+
# directly is the escape hatch — a node inside the world that must draw
|
|
190
|
+
# over the HUD says `band: :overlay` and does, still clipped to whatever
|
|
191
|
+
# its ancestors allowed. That is explicit and named, which is the whole
|
|
192
|
+
# difference from the Integer bases this replaces.
|
|
193
|
+
def band=(value)
|
|
194
|
+
Util::Z.band!(value) unless value.nil?
|
|
195
|
+
@band = value
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Whose input drives this node: an RGame::Engine::Player, or nil.
|
|
199
|
+
#
|
|
200
|
+
# Inherited down the tree exactly like the transform. Set it on a node and
|
|
201
|
+
# its whole subtree reads that player, so `ship.input_owner = players[1]`
|
|
202
|
+
# is all it takes for everything under the ship to answer to player two. A
|
|
203
|
+
# node that sets none inherits its parent's, and a tree that sets none
|
|
204
|
+
# anywhere reads the primary player — which is why single player needs no
|
|
205
|
+
# mention of this at all.
|
|
206
|
+
#
|
|
207
|
+
# **Not `player`**, deliberately, and not `controller` either. `@player` is
|
|
208
|
+
# what a game's own code naturally calls its hero node, so an
|
|
209
|
+
# `attr_accessor :player` here would quietly claim that ivar out from
|
|
210
|
+
# under every scene that has one — which it did, and the symptom
|
|
211
|
+
# was the input system being handed a Node2D. `controller` is taken too:
|
|
212
|
+
# a controller is the component that produces movement intent — see
|
|
213
|
+
# Components::PlayerController — which is a different idea entirely. This
|
|
214
|
+
# name says exactly what it decides and collides with neither.
|
|
215
|
+
attr_accessor :input_owner
|
|
216
|
+
|
|
217
|
+
# A paused node skips `control` and `update` — and so does everything
|
|
218
|
+
# under it, because a subtree is only ever reached through its parent.
|
|
219
|
+
# It still **draws**: pausing is about time, not visibility, which is what
|
|
220
|
+
# lets a frozen world sit under a cutscene overlay that keeps animating.
|
|
221
|
+
#
|
|
222
|
+
# world_view.paused = true # the world stops; the overlay above it does not
|
|
223
|
+
#
|
|
224
|
+
# There is no `abs_paused` to go with `abs_input_owner`. Ownership has to
|
|
225
|
+
# be resolved because a node needs to know whose input it reads even when
|
|
226
|
+
# its parent claims nobody; pausing needs no resolution at all, because a
|
|
227
|
+
# paused node simply never descends.
|
|
228
|
+
attr_accessor :paused
|
|
229
|
+
|
|
230
|
+
def initialize(x: 0, y: 0, z: 0, angle: 0, width: 0, height: 0, input_owner: nil,
|
|
231
|
+
band: nil)
|
|
232
|
+
@input_owner = input_owner
|
|
233
|
+
@paused = false
|
|
234
|
+
@rel_x = x
|
|
235
|
+
@rel_y = y
|
|
236
|
+
@z = z
|
|
237
|
+
self.band = band
|
|
238
|
+
@rel_angle = angle
|
|
239
|
+
@width = width
|
|
240
|
+
@height = height
|
|
241
|
+
@elevation = 0
|
|
242
|
+
@world_current = false
|
|
243
|
+
@world_x = @world_y = @world_angle = 0
|
|
244
|
+
@abs_input_owner = @input_owner
|
|
245
|
+
@abs_band = @band || Util::Z::DEFAULT
|
|
246
|
+
@children = []
|
|
247
|
+
@child_seq = 0
|
|
248
|
+
@children_sorted = true
|
|
249
|
+
@components = []
|
|
250
|
+
@component_slots = {}
|
|
251
|
+
@parent = nil
|
|
252
|
+
@scene = nil
|
|
253
|
+
@in_tree = false
|
|
254
|
+
@freed = false
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def add_node(node)
|
|
258
|
+
@children << node
|
|
259
|
+
node.parent = self
|
|
260
|
+
node._sibling_order = (@child_seq += 1)
|
|
261
|
+
@children_sorted = false
|
|
262
|
+
node.enter_tree if @in_tree
|
|
263
|
+
node
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def remove_node(node)
|
|
267
|
+
node.exit_tree if @in_tree
|
|
268
|
+
@children.delete(node)
|
|
269
|
+
node.parent = nil
|
|
270
|
+
node
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Look a component up by its slot. A Class/Module is matched by ancestry across every
|
|
274
|
+
# component (so a base class finds a subclass instance); a Symbol names a specific slot
|
|
275
|
+
# (see #add_component's `as:`). A class lookup raises when it's ambiguous — two
|
|
276
|
+
# components share that type — so the caller reaches for the name instead. The scan is
|
|
277
|
+
# allocation-free, so it's safe to call on the per-frame path.
|
|
278
|
+
def get_component(key)
|
|
279
|
+
return @component_slots[key] unless key.is_a?(Module)
|
|
280
|
+
|
|
281
|
+
found = nil
|
|
282
|
+
@components.each do |component|
|
|
283
|
+
next unless component.is_a?(key)
|
|
284
|
+
raise ArgumentError, "Multiple components match #{key}; look one up by name" if found
|
|
285
|
+
|
|
286
|
+
found = component
|
|
287
|
+
end
|
|
288
|
+
found
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# Attach a component in a named slot. The slot defaults to the component's class, so a
|
|
292
|
+
# node still holds at most one component per class — until you give them distinct
|
|
293
|
+
# names: `add_component(Timer.new, as: :spawn)` / `add_component(Timer.new, as: :wave)`.
|
|
294
|
+
# A taken slot raises, so an accidental duplicate is still caught.
|
|
295
|
+
def add_component(component, as: nil)
|
|
296
|
+
slot = as || component.class
|
|
297
|
+
raise ArgumentError, "Node already has a component in slot #{slot.inspect}" if @component_slots.key?(slot)
|
|
298
|
+
|
|
299
|
+
@components << component
|
|
300
|
+
@component_slots[slot] = component
|
|
301
|
+
component.node = self
|
|
302
|
+
component.on_attach if @in_tree
|
|
303
|
+
component
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def remove_component(key)
|
|
307
|
+
component = get_component(key)
|
|
308
|
+
return nil unless component
|
|
309
|
+
|
|
310
|
+
component.on_detach if @in_tree
|
|
311
|
+
@components.delete(component)
|
|
312
|
+
@component_slots.delete(@component_slots.key(component))
|
|
313
|
+
component.node = nil
|
|
314
|
+
component
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# The top-most node — a node with no parent is its own root. Global,
|
|
318
|
+
# program-lifetime systems live here as components.
|
|
319
|
+
def root
|
|
320
|
+
@parent ? @parent.root : self
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# The nearest enclosing scene node, marked as a boundary by SceneStack
|
|
324
|
+
# (#scene= self). Scene-lifetime systems live on it as components.
|
|
325
|
+
def scene
|
|
326
|
+
@scene || @parent&.scene
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def context
|
|
330
|
+
@context ||= root.context
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# Nearest system of a class: scene scope first, then the global root.
|
|
334
|
+
def system(klass)
|
|
335
|
+
scene&.get_component(klass) || root.get_component(klass)
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# `input` is an input *source*, not one player's snapshot: an
|
|
339
|
+
# RGame::Engine::Players registry, or a bare Actions when there is only
|
|
340
|
+
# ever one answer (which is what a spec usually passes).
|
|
341
|
+
#
|
|
342
|
+
# Each node asks the source for the actions of whichever player owns it,
|
|
343
|
+
# and hands its components and its own hook that plain Actions. So a
|
|
344
|
+
# component never learns there is more than one player — `control(actions)`
|
|
345
|
+
# means the same thing it always did — while two subtrees under one tick
|
|
346
|
+
# can read two different controllers.
|
|
347
|
+
#
|
|
348
|
+
# The source is what descends, not the resolved snapshot, because
|
|
349
|
+
# ownership can change further down.
|
|
350
|
+
def control(input)
|
|
351
|
+
return if @paused
|
|
352
|
+
|
|
353
|
+
_resolve_inherited
|
|
354
|
+
actions = input.actions_for(@abs_input_owner)
|
|
355
|
+
@components.each { it.control(actions) }
|
|
356
|
+
on_control(actions)
|
|
357
|
+
_children_in_order.each { it.control(input) }
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# update game logic and physics (might become two calls with
|
|
361
|
+
# time, but for now works in one step). This runs second in a
|
|
362
|
+
# game tick
|
|
363
|
+
def update(dt)
|
|
364
|
+
return if @paused
|
|
365
|
+
|
|
366
|
+
@components.each { it.update(dt) }
|
|
367
|
+
on_update(dt)
|
|
368
|
+
_children_in_order.each { it.update(dt) }
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
# update visual game state, drawing the node. This runs last in
|
|
372
|
+
# a game tick
|
|
373
|
+
# `view` is the viewport being drawn into: its rectangle, and the camera
|
|
374
|
+
# (if any) it is seen through. Every node gets it, because a node cannot
|
|
375
|
+
# otherwise know where the edges of its own region are — a HUD laying out
|
|
376
|
+
# against the whole window is wrong the moment the window is one player's
|
|
377
|
+
# half of it — and because culling needs it once the world is drawn more
|
|
378
|
+
# than once. Most nodes ignore it and simply draw.
|
|
379
|
+
def draw(renderer, view)
|
|
380
|
+
_resolve_inherited
|
|
381
|
+
_in_local_space(renderer) do
|
|
382
|
+
renderer.layered(@abs_band) { _draw_content(renderer, view) }
|
|
383
|
+
draw_children(renderer, view)
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def in_tree? = @in_tree
|
|
388
|
+
|
|
389
|
+
# Deferred removal (à la Godot's queue_free): mark this node for removal
|
|
390
|
+
# instead of detaching it now. A node that removes itself or a sibling
|
|
391
|
+
# mid-traversal would mutate the parent's @children while it's being iterated;
|
|
392
|
+
# marking instead and sweeping once after the tick (see #sweep_freed, flushed by
|
|
393
|
+
# the platform loop) keeps removal safe and allocation-free.
|
|
394
|
+
def queue_free = @freed = true
|
|
395
|
+
def freed? = @freed
|
|
396
|
+
|
|
397
|
+
# Detach every node marked by #queue_free, depth-first, from a point outside the
|
|
398
|
+
# update traversal. Components get a hook too, so a container-style component
|
|
399
|
+
# (e.g. SceneStack) can flush the subtree it owns off the normal child list.
|
|
400
|
+
def sweep_freed
|
|
401
|
+
@components.each(&:sweep_freed)
|
|
402
|
+
i = 0
|
|
403
|
+
while i < @children.size
|
|
404
|
+
child = @children[i]
|
|
405
|
+
if child.freed?
|
|
406
|
+
remove_node(child)
|
|
407
|
+
else
|
|
408
|
+
child.sweep_freed
|
|
409
|
+
i += 1
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
# Entered-tree cascade: anchors (root/scene) and sibling systems are now
|
|
415
|
+
# reachable, so components attach (register with systems) before this node's
|
|
416
|
+
# own on_add, and the whole subtree enters depth-first. The engine fires this
|
|
417
|
+
# — the user never calls it — so registration can't be forgotten. Idempotent.
|
|
418
|
+
def enter_tree
|
|
419
|
+
return if @in_tree
|
|
420
|
+
|
|
421
|
+
@in_tree = true
|
|
422
|
+
@freed = false
|
|
423
|
+
@components.each(&:on_attach)
|
|
424
|
+
on_add
|
|
425
|
+
_children_in_order.each(&:enter_tree)
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
# Leaving-tree cascade: mirror of #enter_tree (children first, then this
|
|
429
|
+
# node's on_remove, then component on_detach to release registrations).
|
|
430
|
+
def exit_tree
|
|
431
|
+
return unless @in_tree
|
|
432
|
+
|
|
433
|
+
_children_in_order.each(&:exit_tree)
|
|
434
|
+
on_remove
|
|
435
|
+
@components.each(&:on_detach)
|
|
436
|
+
@in_tree = false
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
def on_control(actions); end
|
|
440
|
+
def on_update(dt); end
|
|
441
|
+
def on_draw(renderer, view); end
|
|
442
|
+
def on_add; end
|
|
443
|
+
def on_remove; end
|
|
444
|
+
|
|
445
|
+
private
|
|
446
|
+
|
|
447
|
+
# hot-path
|
|
448
|
+
# rubocop:disable Style/ExplicitBlockArgument -- an explicit &block would
|
|
449
|
+
# allocate a Proc for every node, every frame, per viewport. `yield` is
|
|
450
|
+
# what keeps this path allocation-free, which culling_spec asserts.
|
|
451
|
+
def _in_local_space(renderer)
|
|
452
|
+
return yield if @parent.nil?
|
|
453
|
+
return yield if @rel_x.zero? && @rel_y.zero? && @rel_angle.zero?
|
|
454
|
+
|
|
455
|
+
renderer.translated(@rel_x, @rel_y) do
|
|
456
|
+
if @rel_angle.zero?
|
|
457
|
+
yield
|
|
458
|
+
else
|
|
459
|
+
renderer.rotated(@rel_angle * 180.0 / Math::PI, 0, 0) { yield }
|
|
460
|
+
end
|
|
461
|
+
end
|
|
462
|
+
end
|
|
463
|
+
# rubocop:enable Style/ExplicitBlockArgument
|
|
464
|
+
|
|
465
|
+
# hot-path
|
|
466
|
+
def _draw_content(renderer, view)
|
|
467
|
+
@components.each { it.draw(renderer, view) }
|
|
468
|
+
on_draw(renderer, view)
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
# hot-path
|
|
472
|
+
def draw_children(renderer, view)
|
|
473
|
+
_children_in_order.each { it.draw(renderer, view) }
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
# hot-path
|
|
477
|
+
def _children_in_order
|
|
478
|
+
_sort_children unless @children_sorted
|
|
479
|
+
@children
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
def _sort_children
|
|
483
|
+
@children_sorted = true
|
|
484
|
+
return if @children.size < 2
|
|
485
|
+
|
|
486
|
+
@children.sort! do |a, b|
|
|
487
|
+
order = a.z <=> b.z
|
|
488
|
+
order.zero? ? a._sibling_order <=> b._sibling_order : order
|
|
489
|
+
end
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
# hot-path
|
|
493
|
+
def _resolve_transform
|
|
494
|
+
@world_current = true
|
|
495
|
+
if @parent.nil?
|
|
496
|
+
@world_x = @world_y = 0
|
|
497
|
+
@world_angle = 0
|
|
498
|
+
return
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
pa = @parent.world_angle
|
|
502
|
+
if pa.zero?
|
|
503
|
+
@world_x = @parent.world_x + @rel_x
|
|
504
|
+
@world_y = @parent.world_y + @rel_y
|
|
505
|
+
else
|
|
506
|
+
cos = Math.cos(pa)
|
|
507
|
+
sin = Math.sin(pa)
|
|
508
|
+
@world_x = @parent.world_x + (@rel_x * cos) - (@rel_y * sin)
|
|
509
|
+
@world_y = @parent.world_y + (@rel_x * sin) + (@rel_y * cos)
|
|
510
|
+
end
|
|
511
|
+
@world_angle = pa + @rel_angle
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
def _place_in_rotated_parent(offset_x, offset_y, pa)
|
|
515
|
+
cos = Math.cos(pa)
|
|
516
|
+
sin = Math.sin(pa)
|
|
517
|
+
self.rel_x = (offset_x * cos) + (offset_y * sin)
|
|
518
|
+
self.rel_y = (offset_y * cos) - (offset_x * sin)
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
protected
|
|
522
|
+
|
|
523
|
+
attr_accessor :_sibling_order
|
|
524
|
+
|
|
525
|
+
def _children_unsorted! = @children_sorted = false
|
|
526
|
+
|
|
527
|
+
# hot-path
|
|
528
|
+
def _soil
|
|
529
|
+
return unless @world_current
|
|
530
|
+
|
|
531
|
+
@world_current = false
|
|
532
|
+
# rubocop:disable Style/SymbolProc -- `&:_soil` would call through
|
|
533
|
+
# Symbol#to_proc, which dispatches publicly and so cannot reach a
|
|
534
|
+
# protected method. An explicit receiver is the only form that works
|
|
535
|
+
# here, and it allocates no more than the symbol would.
|
|
536
|
+
@children.each { it._soil }
|
|
537
|
+
# rubocop:enable Style/SymbolProc
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
private
|
|
541
|
+
|
|
542
|
+
# hot-path
|
|
543
|
+
def _resolve_inherited
|
|
544
|
+
if @parent.nil?
|
|
545
|
+
@abs_input_owner = @input_owner
|
|
546
|
+
@abs_band = @band || Util::Z::DEFAULT
|
|
547
|
+
return
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
@abs_input_owner = @input_owner || @parent.abs_input_owner
|
|
551
|
+
@abs_band = @band || @parent.abs_band
|
|
552
|
+
end
|
|
553
|
+
end
|
|
554
|
+
end
|
|
555
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
# An ordered polyline of waypoints an entity walks along — a road, a patrol
|
|
6
|
+
# route, a track. Pure data: it holds the waypoints and the precomputed per-segment
|
|
7
|
+
# lengths, so a follower walking it at runtime allocates nothing.
|
|
8
|
+
#
|
|
9
|
+
# Waypoints are stored flat (x0, y0, x1, y1, …) in one contiguous array rather than a
|
|
10
|
+
# pair-object per point, and read back through scalar accessors (`x_at`/`y_at`), so
|
|
11
|
+
# neither construction shape nor traversal leaks per-waypoint Arrays onto the hot path.
|
|
12
|
+
# A follower (see Components::PathFollow) reads segments by index and interpolates
|
|
13
|
+
# itself; Path never returns a coordinate pair.
|
|
14
|
+
class Path
|
|
15
|
+
attr_reader :length, :count
|
|
16
|
+
|
|
17
|
+
# `points` is an Array of [x, y] waypoint pairs in walk order (construction-time, so
|
|
18
|
+
# the pair Arrays are fine here). At least two are required — a path with one point
|
|
19
|
+
# has nowhere to walk.
|
|
20
|
+
def initialize(points)
|
|
21
|
+
raise ArgumentError, 'a Path needs at least two waypoints' if points.length < 2
|
|
22
|
+
|
|
23
|
+
@coords = points.flatten.freeze
|
|
24
|
+
@count = points.length
|
|
25
|
+
@segment_lengths, @length = build_segments
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# World coordinates of waypoint `i` (0-based), as scalars (no allocation).
|
|
29
|
+
def x_at(index) = @coords[index * 2]
|
|
30
|
+
def y_at(index) = @coords[(index * 2) + 1]
|
|
31
|
+
|
|
32
|
+
# Length of the segment from waypoint `i` to waypoint `i + 1`. There are
|
|
33
|
+
# `count - 1` segments, indexed 0..count-2.
|
|
34
|
+
def segment_length(index) = @segment_lengths[index]
|
|
35
|
+
|
|
36
|
+
# Shortest distance from the point (x, y) to the polyline — e.g. how far a spot is
|
|
37
|
+
# from the road, so a level can keep things from being placed on or beside it.
|
|
38
|
+
# Pure scalar maths, allocation-free.
|
|
39
|
+
def distance_to(x, y)
|
|
40
|
+
min = Float::INFINITY
|
|
41
|
+
(@count - 1).times do |i|
|
|
42
|
+
d = segment_distance(x, y, x_at(i), y_at(i), x_at(i + 1), y_at(i + 1))
|
|
43
|
+
min = d if d < min
|
|
44
|
+
end
|
|
45
|
+
min
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def segment_distance(px, py, ax, ay, bx, by)
|
|
51
|
+
abx = bx - ax
|
|
52
|
+
aby = by - ay
|
|
53
|
+
len2 = ((abx * abx) + (aby * aby)).to_f
|
|
54
|
+
t = len2.zero? ? 0.0 : (((px - ax) * abx) + ((py - ay) * aby)) / len2
|
|
55
|
+
t = 0.0 if t < 0.0
|
|
56
|
+
t = 1.0 if t > 1.0
|
|
57
|
+
dx = px - (ax + (t * abx))
|
|
58
|
+
dy = py - (ay + (t * aby))
|
|
59
|
+
Math.sqrt((dx * dx) + (dy * dy))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def build_segments
|
|
63
|
+
lengths = []
|
|
64
|
+
total = 0.0
|
|
65
|
+
(@count - 1).times do |i|
|
|
66
|
+
dx = x_at(i + 1) - x_at(i)
|
|
67
|
+
dy = y_at(i + 1) - y_at(i)
|
|
68
|
+
len = Math.sqrt((dx * dx) + (dy * dy))
|
|
69
|
+
lengths << len
|
|
70
|
+
total += len
|
|
71
|
+
end
|
|
72
|
+
[lengths.freeze, total]
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
# One person playing: their device, their bindings, their camera, and their
|
|
6
|
+
# own corner of the screen.
|
|
7
|
+
#
|
|
8
|
+
# player = Player.new(id: 0, device: Controls.gamepad(0))
|
|
9
|
+
# player.actions.held?(:fire) # what they are doing this tick
|
|
10
|
+
# player.camera # where they are looking
|
|
11
|
+
# player.ui # their HUD and menus, in screen space
|
|
12
|
+
#
|
|
13
|
+
# ## Why the player owns these and the scene does not
|
|
14
|
+
#
|
|
15
|
+
# A world has one simulation and any number of viewers. Two players share
|
|
16
|
+
# every NPC and every tile, but not a camera, not a set of bindings, and not
|
|
17
|
+
# a menu — so those belong to the viewer. Putting the camera on the scene
|
|
18
|
+
# works exactly until there are two of them, and putting it on a node in the
|
|
19
|
+
# world is worse: it forces the world to know how many times it is drawn.
|
|
20
|
+
#
|
|
21
|
+
# This is the model Unreal calls a LocalPlayer and Unity spreads across
|
|
22
|
+
# PlayerInput plus a camera.
|
|
23
|
+
#
|
|
24
|
+
# ## The action *names* are shared, the bindings are not
|
|
25
|
+
#
|
|
26
|
+
# Every player reads `:fire`. What triggers it is per player: one on Space,
|
|
27
|
+
# one on a pad's X button, and the same InputMap can serve both because a
|
|
28
|
+
# device only answers for its own kind of input. Each player gets their own
|
|
29
|
+
# ActionMapper, so their edge queries are independent — one player's press
|
|
30
|
+
# cannot consume another's.
|
|
31
|
+
class Player
|
|
32
|
+
Controls = RGame::Util::Controls
|
|
33
|
+
|
|
34
|
+
attr_reader :id, :camera, :ui, :mapper
|
|
35
|
+
attr_accessor :name
|
|
36
|
+
|
|
37
|
+
# `device` may be nil, meaning "nobody is driving this player yet" — a
|
|
38
|
+
# seat waiting for a controller. Polling one reads as nothing held rather
|
|
39
|
+
# than raising, so a game can show "Player 2: press a button" without a
|
|
40
|
+
# special case.
|
|
41
|
+
def initialize(id: 0, device: Controls::KEYBOARD, input_map: nil, camera: nil)
|
|
42
|
+
@id = id
|
|
43
|
+
@camera = camera || Camera.new
|
|
44
|
+
@mapper = ActionMapper.new(input_map || InputMap.default, device: device)
|
|
45
|
+
@ui = Node2D.new
|
|
46
|
+
@name = nil
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def device = @mapper.device
|
|
50
|
+
def active? = !@mapper.device.nil?
|
|
51
|
+
|
|
52
|
+
# Reassigning is how a hot-plug lands: the pad that just arrived in a slot
|
|
53
|
+
# becomes this player's, and their bindings and camera carry on unchanged.
|
|
54
|
+
def device=(value)
|
|
55
|
+
@mapper.device = value
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# This player's input for the current tick. Set by #poll, and a reused
|
|
59
|
+
# object — hold the Player, never this.
|
|
60
|
+
def actions = @mapper.actions
|
|
61
|
+
|
|
62
|
+
def poll(backend) = @mapper.poll(backend)
|
|
63
|
+
|
|
64
|
+
# What this player's map can answer for. The vocabulary is the game's, so
|
|
65
|
+
# it is the same for every player; the bindings behind it are not.
|
|
66
|
+
def input_map = @mapper.map
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|