rgame 0.2.0 → 0.3.1
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 +144 -1
- data/README.md +67 -65
- data/docs/api/README.md +86 -82
- data/docs/api/app.md +125 -68
- data/docs/api/assets.md +203 -165
- data/docs/api/audio.md +130 -89
- data/docs/api/cli.md +259 -0
- data/docs/api/components.md +1032 -265
- data/docs/api/drawing.md +160 -160
- data/docs/api/examples.md +263 -0
- data/docs/api/game.md +149 -50
- data/docs/api/images.md +39 -39
- data/docs/api/input.md +226 -148
- data/docs/api/internals.md +241 -62
- data/docs/api/localization.md +285 -0
- data/docs/api/scene_graph.md +397 -244
- data/docs/api/signals.md +81 -76
- data/docs/api/systems.md +186 -73
- data/docs/api/text.md +58 -41
- data/docs/api/tile_maps.md +237 -0
- data/docs/api/toolbox.md +395 -146
- data/docs/api/ui.md +883 -54
- data/docs/api/values.md +216 -33
- data/examples/assets/README.md +322 -0
- data/examples/assets/blip.ogg +0 -0
- data/examples/assets/glyphs.json +5 -0
- data/examples/assets/glyphs.png +0 -0
- data/examples/assets/hero.json +12 -0
- data/examples/assets/hero.png +0 -0
- data/examples/assets/icons.json +13 -0
- data/examples/assets/icons.png +0 -0
- data/examples/assets/music.ogg +0 -0
- data/examples/assets/skills.json +10 -0
- data/examples/assets/skills.png +0 -0
- data/examples/assets/tileset.png +0 -0
- data/examples/assets/tileset.tsx +65 -0
- data/examples/assets/town.tmx +26 -0
- data/examples/assets/ui.json +11 -0
- data/examples/assets/ui.png +0 -0
- data/examples/collision/locales/en.yml +8 -0
- data/examples/collision/main.rb +316 -0
- data/examples/collision_tiles/locales/en.yml +9 -0
- data/examples/collision_tiles/main.rb +274 -0
- data/examples/fullscreen/locales/en.yml +10 -0
- data/examples/fullscreen/main.rb +216 -0
- data/examples/game_menu/locales/en.yml +8 -0
- data/examples/game_menu/main.rb +170 -0
- data/examples/input_glyphs/locales/en.yml +14 -0
- data/examples/input_glyphs/main.rb +213 -0
- data/examples/jump_topdown/locales/en.yml +9 -0
- data/examples/jump_topdown/main.rb +178 -0
- data/examples/localization/locales/de.yml +12 -0
- data/examples/localization/locales/en.yml +13 -0
- data/examples/localization/main.rb +158 -0
- data/examples/menu_navigation/locales/en.yml +23 -0
- data/examples/menu_navigation/main.rb +365 -0
- data/examples/music/locales/en.yml +7 -0
- data/examples/music/main.rb +134 -0
- data/examples/pathfinding/locales/en.yml +17 -0
- data/examples/pathfinding/main.rb +298 -0
- data/examples/pooling/locales/en.yml +7 -0
- data/examples/pooling/main.rb +259 -0
- data/examples/quick_wheel/locales/en.yml +16 -0
- data/examples/quick_wheel/main.rb +184 -0
- data/examples/radial_menu/locales/en.yml +16 -0
- data/examples/radial_menu/main.rb +184 -0
- data/examples/save_load/locales/en.yml +11 -0
- data/examples/save_load/main.rb +207 -0
- data/examples/save_load_ids/locales/en.yml +11 -0
- data/examples/save_load_ids/main.rb +322 -0
- data/examples/scroll_map/locales/en.yml +4 -0
- data/examples/scroll_map/main.rb +140 -0
- data/examples/signals/locales/en.yml +6 -0
- data/examples/signals/main.rb +278 -0
- data/examples/skill_bar/locales/en.yml +14 -0
- data/examples/skill_bar/main.rb +159 -0
- data/examples/sound/locales/en.yml +6 -0
- data/examples/sound/main.rb +122 -0
- data/examples/split_screen/locales/en.yml +9 -0
- data/examples/split_screen/main.rb +304 -0
- data/examples/sprite/locales/en.yml +8 -0
- data/examples/sprite/main.rb +180 -0
- data/examples/timer/locales/en.yml +12 -0
- data/examples/timer/main.rb +273 -0
- data/examples/velocity/locales/en.yml +6 -0
- data/examples/velocity/main.rb +196 -0
- data/examples/walk/locales/en.yml +4 -0
- data/examples/walk/main.rb +99 -0
- data/exe/rgame +9 -0
- data/ext/rgame_core/app/app.c +33 -3
- data/ext/rgame_core/app/locale.c +67 -0
- data/ext/rgame_core/app/locale.h +28 -0
- data/ext/rgame_core/audio/audio.c +39 -2
- data/ext/rgame_core/example.rb +0 -49
- data/ext/rgame_core/extconf.rb +0 -125
- data/ext/rgame_core/include/rgame/core.h +38 -2
- data/ext/rgame_core/ruby/audio_ext.c +10 -5
- data/ext/rgame_core/ruby/core_ext.c +30 -7
- data/ext/rgame_core/ruby/core_ext.h +3 -0
- data/ext/rgame_core/ruby/locale_ext.c +44 -0
- data/ext/rgame_core/ruby/recording_ext.c +1 -1
- data/ext/rgame_core/ruby/renderer_ext.c +20 -20
- data/ext/rgame_util/extconf.rb +2 -20
- data/ext/rgame_util/route_search.c +305 -0
- data/ext/rgame_util/route_search.h +86 -0
- data/ext/rgame_util/route_search_ext.c +150 -0
- data/ext/rgame_util/solid_grid.c +58 -0
- data/ext/rgame_util/solid_grid.h +49 -0
- data/ext/rgame_util/solid_grid_ext.c +161 -0
- data/ext/rgame_util/tile_sweep.c +164 -0
- data/ext/rgame_util/tile_sweep.h +62 -0
- data/ext/rgame_util/tile_sweep_ext.c +155 -0
- data/ext/rgame_util/util_ext.c +3 -0
- data/ext/rgame_util/util_ext.h +15 -0
- data/lib/rgame/boot.rb +0 -10
- data/lib/rgame/cli/new_project.rb +139 -0
- data/lib/rgame/cli/templates/Gemfile.tt +23 -0
- data/lib/rgame/cli/templates/README.md.tt +93 -0
- data/lib/rgame/cli/templates/Rakefile.tt +9 -0
- data/lib/rgame/cli/templates/assets/locales/en.yml.tt +10 -0
- data/lib/rgame/cli/templates/game.rb.tt +23 -0
- data/lib/rgame/cli/templates/gitignore.tt +12 -0
- data/lib/rgame/cli/templates/main.rb.tt +11 -0
- data/lib/rgame/cli/templates/nodes/root.rb.tt +24 -0
- data/lib/rgame/cli/templates/rspec.tt +2 -0
- data/lib/rgame/cli/templates/rubocop.yml.tt +75 -0
- data/lib/rgame/cli/templates/ruby-version.tt +1 -0
- data/lib/rgame/cli/templates/spec/locales_spec.rb.tt +18 -0
- data/lib/rgame/cli/templates/spec/nodes/root_spec.rb.tt +18 -0
- data/lib/rgame/cli/templates/spec/spec_helper.rb.tt +37 -0
- data/lib/rgame/cli.rb +66 -0
- data/lib/rgame/core/app.rb +6 -44
- data/lib/rgame/core/asset_manager.rb +13 -31
- data/lib/rgame/core/audio.rb +37 -16
- data/lib/rgame/core/font.rb +0 -3
- data/lib/rgame/core/locale.rb +22 -0
- data/lib/rgame/core/nine_slice.rb +0 -21
- data/lib/rgame/core/renderer.rb +6 -63
- data/lib/rgame/core/sprite_sheet.rb +0 -3
- data/lib/rgame/core/tile_map_renderer.rb +2 -19
- data/lib/rgame/core/ui_atlas.rb +28 -13
- data/lib/rgame/core.rb +1 -8
- data/lib/rgame/engine/actor_blockers.rb +131 -0
- data/lib/rgame/engine/animation_set.rb +1 -0
- data/lib/rgame/engine/audio_director.rb +36 -6
- data/lib/rgame/engine/bounds_blockers.rb +74 -0
- data/lib/rgame/engine/camera.rb +3 -3
- data/lib/rgame/engine/circle_collider.rb +4 -2
- data/lib/rgame/engine/collision_box.rb +26 -1
- data/lib/rgame/engine/collision_system.rb +110 -22
- data/lib/rgame/engine/component.rb +34 -10
- data/lib/rgame/engine/components/action_trigger.rb +0 -1
- data/lib/rgame/engine/components/animated_sprite.rb +27 -25
- data/lib/rgame/engine/components/box_collider.rb +99 -0
- data/lib/rgame/engine/components/camera_follow.rb +6 -5
- data/lib/rgame/engine/components/character_body.rb +20 -61
- data/lib/rgame/engine/components/circle_collider.rb +47 -11
- data/lib/rgame/engine/components/collision_world.rb +159 -31
- data/lib/rgame/engine/components/despawn_offscreen.rb +24 -8
- data/lib/rgame/engine/components/feet_collider.rb +61 -0
- data/lib/rgame/engine/components/hop.rb +76 -0
- data/lib/rgame/engine/components/identity.rb +73 -0
- data/lib/rgame/engine/components/mover.rb +285 -0
- data/lib/rgame/engine/components/navigator.rb +145 -0
- data/lib/rgame/engine/components/path_follow.rb +123 -31
- data/lib/rgame/engine/components/player_controller.rb +5 -2
- data/lib/rgame/engine/components/pool.rb +1 -1
- data/lib/rgame/engine/components/screen_wrap.rb +33 -11
- data/lib/rgame/engine/components/sprite.rb +12 -6
- data/lib/rgame/engine/components/targeting.rb +9 -11
- data/lib/rgame/engine/components/thrust_controller.rb +1 -1
- data/lib/rgame/engine/components/tile_world.rb +41 -13
- data/lib/rgame/engine/components/timer.rb +1 -1
- data/lib/rgame/engine/components/velocity.rb +23 -7
- data/lib/rgame/engine/components/wander_controller.rb +6 -2
- data/lib/rgame/engine/components/world.rb +133 -0
- data/lib/rgame/engine/contact_set.rb +74 -0
- data/lib/rgame/engine/culling.rb +4 -6
- data/lib/rgame/engine/debug_overlay.rb +5 -11
- data/lib/rgame/engine/i18n/plural.rb +45 -0
- data/lib/rgame/engine/i18n/plural_rules.rb +82 -0
- data/lib/rgame/engine/i18n/template.rb +59 -0
- data/lib/rgame/engine/i18n.rb +276 -51
- data/lib/rgame/engine/input/action_mapper.rb +0 -26
- data/lib/rgame/engine/input/actions.rb +2 -8
- data/lib/rgame/engine/input/input_map.rb +39 -21
- data/lib/rgame/engine/nav_grid.rb +87 -0
- data/lib/rgame/engine/node2d.rb +248 -133
- data/lib/rgame/engine/path.rb +4 -6
- data/lib/rgame/engine/players.rb +6 -13
- data/lib/rgame/engine/presentation.rb +171 -0
- data/lib/rgame/engine/scene/scene_stack.rb +4 -4
- data/lib/rgame/engine/sealed_privates.rb +54 -0
- data/lib/rgame/engine/spatial_hash.rb +51 -19
- data/lib/rgame/engine/text.rb +194 -0
- data/lib/rgame/engine/tile_blockers.rb +63 -0
- data/lib/rgame/engine/tile_map.rb +3 -5
- data/lib/rgame/engine/tile_map_layer.rb +0 -2
- data/lib/rgame/engine/tileset.rb +2 -4
- data/lib/rgame/engine/timer.rb +2 -2
- data/lib/rgame/engine/ui/button.rb +248 -0
- data/lib/rgame/engine/ui/column.rb +20 -0
- data/lib/rgame/engine/ui/icon_button.rb +93 -0
- data/lib/rgame/engine/ui/menu.rb +246 -71
- data/lib/rgame/engine/ui/navigation.rb +57 -0
- data/lib/rgame/engine/ui/nine_slice_style.rb +50 -0
- data/lib/rgame/engine/ui/option_button.rb +163 -0
- data/lib/rgame/engine/ui/panel_button.rb +32 -0
- data/lib/rgame/engine/ui/panel_menu.rb +36 -0
- data/lib/rgame/engine/ui/pointing.rb +146 -0
- data/lib/rgame/engine/ui/radial_menu.rb +85 -0
- data/lib/rgame/engine/ui/ring.rb +55 -0
- data/lib/rgame/engine/ui/row.rb +21 -0
- data/lib/rgame/engine/ui/shape_style.rb +102 -0
- data/lib/rgame/engine/ui/stack.rb +58 -0
- data/lib/rgame/engine/ui/stepping.rb +93 -0
- data/lib/rgame/engine/ui/text_button.rb +59 -0
- data/lib/rgame/engine/viewports.rb +2 -5
- data/lib/rgame/engine/world_view.rb +5 -4
- data/lib/rgame/engine.rb +31 -25
- data/lib/rgame/game.rb +99 -27
- data/lib/rgame/rubocop/cop/game/draw_in_local_space.rb +103 -0
- data/lib/rgame/rubocop/cop/game/hot_path.rb +36 -0
- data/lib/rgame/rubocop/cop/game/layer_boundary.rb +43 -0
- data/lib/rgame/rubocop/cop/game/no_core_in_engine_layer.rb +100 -0
- data/lib/rgame/rubocop/cop/game/no_engine_in_core_layer.rb +84 -0
- data/lib/rgame/rubocop/cop/game/no_interpolation_in_hot_path.rb +50 -0
- data/lib/rgame/rubocop/cop/game/no_literal_text.rb +41 -0
- data/lib/rgame/rubocop/cop/game/no_needless_allocation.rb +112 -0
- data/lib/rgame/rubocop/default.yml +39 -0
- data/lib/rgame/rubocop/plugin.rb +45 -0
- data/lib/rgame/rubocop.rb +11 -0
- data/lib/rgame/util/color.rb +20 -24
- data/lib/rgame/util/controls.rb +14 -28
- data/lib/rgame/util/route_search.rb +27 -0
- data/lib/rgame/util/save_file.rb +107 -0
- data/lib/rgame/util/solid_grid.rb +37 -0
- data/lib/rgame/util/tensor.rb +0 -9
- data/lib/rgame/util/tile_sweep.rb +36 -0
- data/lib/rgame/util/z.rb +4 -14
- data/lib/rgame/util.rb +4 -3
- data/lib/rgame/version.rb +1 -1
- data/lib/rgame.rb +0 -15
- metadata +167 -12
- data/lib/rgame/engine/actor.rb +0 -53
- data/lib/rgame/engine/body.rb +0 -49
- data/lib/rgame/engine/cached_label.rb +0 -33
- data/lib/rgame/engine/input/player_controller.rb +0 -14
- data/lib/rgame/engine/matrix.rb +0 -32
- data/lib/rgame/engine/resettable.rb +0 -67
- data/lib/rgame/engine/tile_collision.rb +0 -78
- data/lib/rgame/engine/ui/menu_item.rb +0 -84
data/lib/rgame/engine/node2d.rb
CHANGED
|
@@ -8,16 +8,155 @@ module RGame
|
|
|
8
8
|
# DSL to allow for easy signal usage.
|
|
9
9
|
class Node2D
|
|
10
10
|
extend Engine::Signal::DSL
|
|
11
|
+
extend Engine::SealedPrivates
|
|
11
12
|
|
|
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
|
|
13
44
|
attr_writer :scene, :context
|
|
14
45
|
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
21
160
|
|
|
22
161
|
# Where this node sits among its **siblings**, and nowhere else.
|
|
23
162
|
#
|
|
@@ -38,7 +177,7 @@ module RGame
|
|
|
38
177
|
# front of something the node itself was behind. See RGame::Util::Z.
|
|
39
178
|
def z=(value)
|
|
40
179
|
@z = value
|
|
41
|
-
@parent&.
|
|
180
|
+
@parent&._children_unsorted!
|
|
42
181
|
end
|
|
43
182
|
|
|
44
183
|
# Which band this node and everything under it draws in — `:world` (the
|
|
@@ -66,13 +205,13 @@ module RGame
|
|
|
66
205
|
# mention of this at all.
|
|
67
206
|
#
|
|
68
207
|
# **Not `player`**, deliberately, and not `controller` either. `@player` is
|
|
69
|
-
# what a game's own code calls its hero node
|
|
70
|
-
#
|
|
71
|
-
#
|
|
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
|
|
72
211
|
# was the input system being handed a Node2D. `controller` is taken too:
|
|
73
|
-
#
|
|
74
|
-
#
|
|
75
|
-
# neither.
|
|
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.
|
|
76
215
|
attr_accessor :input_owner
|
|
77
216
|
|
|
78
217
|
# A paused node skips `control` and `update` — and so does everything
|
|
@@ -92,30 +231,23 @@ module RGame
|
|
|
92
231
|
band: nil)
|
|
93
232
|
@input_owner = input_owner
|
|
94
233
|
@paused = false
|
|
95
|
-
@
|
|
96
|
-
@
|
|
234
|
+
@rel_x = x
|
|
235
|
+
@rel_y = y
|
|
97
236
|
@z = z
|
|
98
237
|
self.band = band
|
|
99
|
-
@
|
|
238
|
+
@rel_angle = angle
|
|
100
239
|
@width = width
|
|
101
240
|
@height = height
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
# unparented node, and saves every reader of abs_* from a NoMethodError
|
|
106
|
-
# on a node built but not yet ticked.
|
|
107
|
-
@abs_x = @abs_y = @abs_angle = 0
|
|
241
|
+
@elevation = 0
|
|
242
|
+
@world_current = false
|
|
243
|
+
@world_x = @world_y = @world_angle = 0
|
|
108
244
|
@abs_input_owner = @input_owner
|
|
109
245
|
@abs_band = @band || Util::Z::DEFAULT
|
|
110
246
|
@children = []
|
|
111
|
-
# Siblings are drawn in `z` order, and in insertion order within one
|
|
112
|
-
# `z`. Ruby's sort is not stable, so insertion order is carried as a
|
|
113
|
-
# number rather than relied on — the same reason the C draw queue
|
|
114
|
-
# compares (z, order) instead of trusting qsort.
|
|
115
247
|
@child_seq = 0
|
|
116
248
|
@children_sorted = true
|
|
117
249
|
@components = []
|
|
118
|
-
@component_slots = {}
|
|
250
|
+
@component_slots = {}
|
|
119
251
|
@parent = nil
|
|
120
252
|
@scene = nil
|
|
121
253
|
@in_tree = false
|
|
@@ -125,21 +257,12 @@ module RGame
|
|
|
125
257
|
def add_node(node)
|
|
126
258
|
@children << node
|
|
127
259
|
node.parent = self
|
|
128
|
-
node.
|
|
260
|
+
node._sibling_order = (@child_seq += 1)
|
|
129
261
|
@children_sorted = false
|
|
130
|
-
# Defer the entered-tree cascade until this node is itself live; otherwise it
|
|
131
|
-
# fires when an ancestor enters (see #enter_tree). This is the construct-vs-enter
|
|
132
|
-
# split — a node built inside another node's initialize is not yet in the tree.
|
|
133
262
|
node.enter_tree if @in_tree
|
|
134
263
|
node
|
|
135
264
|
end
|
|
136
265
|
|
|
137
|
-
# A child was added, or one changed its `z`, so the child order is stale.
|
|
138
|
-
# The sort is deferred to the next traversal rather than done here, so
|
|
139
|
-
# building a scene of a thousand nodes costs one sort rather than a
|
|
140
|
-
# thousand. Called by the engine; a game only ever assigns `z`.
|
|
141
|
-
def children_unsorted! = @children_sorted = false
|
|
142
|
-
|
|
143
266
|
def remove_node(node)
|
|
144
267
|
node.exit_tree if @in_tree
|
|
145
268
|
@children.delete(node)
|
|
@@ -191,11 +314,6 @@ module RGame
|
|
|
191
314
|
component
|
|
192
315
|
end
|
|
193
316
|
|
|
194
|
-
# Anchors, resolved by walking parents so they can never go stale (a cached
|
|
195
|
-
# back-link set at add-time breaks when children are built before the node is
|
|
196
|
-
# in the tree). Shared systems live as components on an anchor node and are
|
|
197
|
-
# reached through these, not threaded through constructors.
|
|
198
|
-
|
|
199
317
|
# The top-most node — a node with no parent is its own root. Global,
|
|
200
318
|
# program-lifetime systems live here as components.
|
|
201
319
|
def root
|
|
@@ -217,13 +335,6 @@ module RGame
|
|
|
217
335
|
scene&.get_component(klass) || root.get_component(klass)
|
|
218
336
|
end
|
|
219
337
|
|
|
220
|
-
# updates input, both from player (readings actions) as well as
|
|
221
|
-
# AI-driven node control. This run first in a game tick
|
|
222
|
-
# Each phase settles this node first (components, then the node's own
|
|
223
|
-
# hook), then descends into children. Self-before-subtree keeps the
|
|
224
|
-
# transform flowing downward: a component or hook that moves this node
|
|
225
|
-
# does so before children resolve their origin from it.
|
|
226
|
-
|
|
227
338
|
# `input` is an input *source*, not one player's snapshot: an
|
|
228
339
|
# RGame::Engine::Players registry, or a bare Actions when there is only
|
|
229
340
|
# ever one answer (which is what a spec usually passes).
|
|
@@ -239,11 +350,11 @@ module RGame
|
|
|
239
350
|
def control(input)
|
|
240
351
|
return if @paused
|
|
241
352
|
|
|
242
|
-
|
|
353
|
+
_resolve_inherited
|
|
243
354
|
actions = input.actions_for(@abs_input_owner)
|
|
244
355
|
@components.each { it.control(actions) }
|
|
245
356
|
on_control(actions)
|
|
246
|
-
|
|
357
|
+
_children_in_order.each { it.control(input) }
|
|
247
358
|
end
|
|
248
359
|
|
|
249
360
|
# update game logic and physics (might become two calls with
|
|
@@ -252,10 +363,9 @@ module RGame
|
|
|
252
363
|
def update(dt)
|
|
253
364
|
return if @paused
|
|
254
365
|
|
|
255
|
-
resolve_origin
|
|
256
366
|
@components.each { it.update(dt) }
|
|
257
367
|
on_update(dt)
|
|
258
|
-
|
|
368
|
+
_children_in_order.each { it.update(dt) }
|
|
259
369
|
end
|
|
260
370
|
|
|
261
371
|
# update visual game state, drawing the node. This runs last in
|
|
@@ -267,27 +377,11 @@ module RGame
|
|
|
267
377
|
# half of it — and because culling needs it once the world is drawn more
|
|
268
378
|
# than once. Most nodes ignore it and simply draw.
|
|
269
379
|
def draw(renderer, view)
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
# reaches nodes, draw order *is* tree order — and because a slot is
|
|
275
|
-
# narrow, nothing a node draws can reach past itself. The node never
|
|
276
|
-
# asks for this and cannot forget it; see RGame::Util::Z.
|
|
277
|
-
renderer.layered(@abs_band) do
|
|
278
|
-
# Draw this node's own visuals oriented by its absolute angle, then descend.
|
|
279
|
-
# Children resolve their own world transform (resolve_origin already baked this
|
|
280
|
-
# node's rotation into their abs_x/abs_y), so they draw in flat world space and
|
|
281
|
-
# must NOT be nested inside this node's rotation — nesting would apply that
|
|
282
|
-
# rotation to them a second time. Unrotated nodes skip the wrapper entirely.
|
|
283
|
-
if abs_angle.zero?
|
|
284
|
-
draw_content(renderer, view)
|
|
285
|
-
else
|
|
286
|
-
renderer.rotated(abs_angle * 180.0 / Math::PI, abs_x, abs_y) { draw_content(renderer, view) }
|
|
287
|
-
end
|
|
380
|
+
_resolve_inherited
|
|
381
|
+
_in_local_space(renderer) do
|
|
382
|
+
renderer.layered(@abs_band) { _draw_content(renderer, view) }
|
|
383
|
+
draw_children(renderer, view)
|
|
288
384
|
end
|
|
289
|
-
# Outside the block: a child takes a slot of its own, after this one.
|
|
290
|
-
draw_children(renderer, view)
|
|
291
385
|
end
|
|
292
386
|
|
|
293
387
|
def in_tree? = @in_tree
|
|
@@ -309,7 +403,7 @@ module RGame
|
|
|
309
403
|
while i < @children.size
|
|
310
404
|
child = @children[i]
|
|
311
405
|
if child.freed?
|
|
312
|
-
remove_node(child)
|
|
406
|
+
remove_node(child)
|
|
313
407
|
else
|
|
314
408
|
child.sweep_freed
|
|
315
409
|
i += 1
|
|
@@ -325,10 +419,10 @@ module RGame
|
|
|
325
419
|
return if @in_tree
|
|
326
420
|
|
|
327
421
|
@in_tree = true
|
|
328
|
-
@freed = false
|
|
422
|
+
@freed = false
|
|
329
423
|
@components.each(&:on_attach)
|
|
330
424
|
on_add
|
|
331
|
-
|
|
425
|
+
_children_in_order.each(&:enter_tree)
|
|
332
426
|
end
|
|
333
427
|
|
|
334
428
|
# Leaving-tree cascade: mirror of #enter_tree (children first, then this
|
|
@@ -336,17 +430,12 @@ module RGame
|
|
|
336
430
|
def exit_tree
|
|
337
431
|
return unless @in_tree
|
|
338
432
|
|
|
339
|
-
|
|
433
|
+
_children_in_order.each(&:exit_tree)
|
|
340
434
|
on_remove
|
|
341
435
|
@components.each(&:on_detach)
|
|
342
436
|
@in_tree = false
|
|
343
437
|
end
|
|
344
438
|
|
|
345
|
-
# Lifecycle hooks: Subclasses should implement these instead of
|
|
346
|
-
# overwriting the public interface draw/update/add etc. on_add/on_remove
|
|
347
|
-
# fire when the node enters/leaves the live tree (see #enter_tree), not at
|
|
348
|
-
# construction — so anchors and systems are available inside them.
|
|
349
|
-
|
|
350
439
|
def on_control(actions); end
|
|
351
440
|
def on_update(dt); end
|
|
352
441
|
def on_draw(renderer, view); end
|
|
@@ -355,85 +444,111 @@ module RGame
|
|
|
355
444
|
|
|
356
445
|
private
|
|
357
446
|
|
|
358
|
-
# This node's own drawing: its components and its draw hook, in that order.
|
|
359
|
-
# Wrapped in renderer.rotated by #draw when the node carries an absolute angle.
|
|
360
447
|
# hot-path
|
|
361
|
-
|
|
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)
|
|
362
467
|
@components.each { it.draw(renderer, view) }
|
|
363
468
|
on_draw(renderer, view)
|
|
364
469
|
end
|
|
365
470
|
|
|
366
|
-
# Draw the child subtrees. Its own method so a node can wrap the whole subtree's
|
|
367
|
-
# draw in a transform without each child knowing about it.
|
|
368
471
|
# hot-path
|
|
369
472
|
def draw_children(renderer, view)
|
|
370
|
-
|
|
473
|
+
_children_in_order.each { it.draw(renderer, view) }
|
|
371
474
|
end
|
|
372
475
|
|
|
373
|
-
# The children, in the order every phase visits them: by `z`, then by when
|
|
374
|
-
# they were added. Sorted lazily — a scene that never touches `z` after
|
|
375
|
-
# building sorts once and then pays one boolean per phase.
|
|
376
476
|
# hot-path
|
|
377
|
-
def
|
|
378
|
-
|
|
477
|
+
def _children_in_order
|
|
478
|
+
_sort_children unless @children_sorted
|
|
379
479
|
@children
|
|
380
480
|
end
|
|
381
481
|
|
|
382
|
-
|
|
383
|
-
# explicitly. Without it two same-z siblings would swap places between
|
|
384
|
-
# frames, which reads on screen as flicker rather than as a sort problem.
|
|
385
|
-
def sort_children
|
|
482
|
+
def _sort_children
|
|
386
483
|
@children_sorted = true
|
|
387
|
-
# Nothing to order, and the overwhelmingly common case for a leaf or a
|
|
388
|
-
# node with one visual — worth skipping before touching the array.
|
|
389
484
|
return if @children.size < 2
|
|
390
485
|
|
|
391
486
|
@children.sort! do |a, b|
|
|
392
487
|
order = a.z <=> b.z
|
|
393
|
-
order.zero? ? a.
|
|
488
|
+
order.zero? ? a._sibling_order <=> b._sibling_order : order
|
|
394
489
|
end
|
|
395
490
|
end
|
|
396
491
|
|
|
397
|
-
#
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
# angle before being added to the parent's origin.
|
|
401
|
-
#
|
|
402
|
-
# `z` is **not** among them, and that is the point: depth is decided by
|
|
403
|
-
# where the traversal reaches a node, not by summing what its ancestors
|
|
404
|
-
# picked. See #z= and RGame::Util::Z.
|
|
405
|
-
# TODO: Do not recalculate every time, but use a @dirty flag
|
|
406
|
-
def resolve_origin
|
|
492
|
+
# hot-path
|
|
493
|
+
def _resolve_transform
|
|
494
|
+
@world_current = true
|
|
407
495
|
if @parent.nil?
|
|
408
|
-
@
|
|
409
|
-
@
|
|
410
|
-
@abs_input_owner = @input_owner
|
|
411
|
-
@abs_band = @band || Util::Z::DEFAULT
|
|
496
|
+
@world_x = @world_y = 0
|
|
497
|
+
@world_angle = 0
|
|
412
498
|
return
|
|
413
499
|
end
|
|
414
500
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
# player's corner wants the same answer `control` used.
|
|
420
|
-
@abs_input_owner = @input_owner || @parent.abs_input_owner
|
|
421
|
-
# The band inherits the same way. A node that declares one overrides it
|
|
422
|
-
# for its whole subtree, which is the only way out of a band and is
|
|
423
|
-
# spelled with a name rather than a number.
|
|
424
|
-
@abs_band = @band || @parent.abs_band
|
|
425
|
-
|
|
426
|
-
pa = @parent.abs_angle
|
|
427
|
-
if pa.zero? # fast path: parent unrotated -> plain translation, no trig
|
|
428
|
-
@abs_x = @parent.abs_x + @x
|
|
429
|
-
@abs_y = @parent.abs_y + @y
|
|
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
|
|
430
505
|
else
|
|
431
506
|
cos = Math.cos(pa)
|
|
432
507
|
sin = Math.sin(pa)
|
|
433
|
-
@
|
|
434
|
-
@
|
|
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
|
|
435
548
|
end
|
|
436
|
-
|
|
549
|
+
|
|
550
|
+
@abs_input_owner = @input_owner || @parent.abs_input_owner
|
|
551
|
+
@abs_band = @band || @parent.abs_band
|
|
437
552
|
end
|
|
438
553
|
end
|
|
439
554
|
end
|
data/lib/rgame/engine/path.rb
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module RGame
|
|
4
4
|
module Engine
|
|
5
|
-
# An ordered polyline of waypoints an entity walks along —
|
|
6
|
-
#
|
|
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
7
|
# lengths, so a follower walking it at runtime allocates nothing.
|
|
8
8
|
#
|
|
9
9
|
# Waypoints are stored flat (x0, y0, x1, y1, …) in one contiguous array rather than a
|
|
@@ -34,7 +34,7 @@ module RGame
|
|
|
34
34
|
def segment_length(index) = @segment_lengths[index]
|
|
35
35
|
|
|
36
36
|
# Shortest distance from the point (x, y) to the polyline — e.g. how far a spot is
|
|
37
|
-
# from the road, so a
|
|
37
|
+
# from the road, so a level can keep things from being placed on or beside it.
|
|
38
38
|
# Pure scalar maths, allocation-free.
|
|
39
39
|
def distance_to(x, y)
|
|
40
40
|
min = Float::INFINITY
|
|
@@ -47,12 +47,10 @@ module RGame
|
|
|
47
47
|
|
|
48
48
|
private
|
|
49
49
|
|
|
50
|
-
# Distance from (px, py) to the segment (ax, ay)-(bx, by): project the point onto the
|
|
51
|
-
# segment, clamp the projection to the segment's ends, and measure to that foot.
|
|
52
50
|
def segment_distance(px, py, ax, ay, bx, by)
|
|
53
51
|
abx = bx - ax
|
|
54
52
|
aby = by - ay
|
|
55
|
-
len2 = (abx * abx) + (aby * aby)
|
|
53
|
+
len2 = ((abx * abx) + (aby * aby)).to_f
|
|
56
54
|
t = len2.zero? ? 0.0 : (((px - ax) * abx) + ((py - ay) * aby)) / len2
|
|
57
55
|
t = 0.0 if t < 0.0
|
|
58
56
|
t = 1.0 if t > 1.0
|
data/lib/rgame/engine/players.rb
CHANGED
|
@@ -62,9 +62,6 @@ module RGame
|
|
|
62
62
|
def initialize(players = [])
|
|
63
63
|
super()
|
|
64
64
|
@list = players
|
|
65
|
-
# One seat means there is no second player to become, so an unassigned
|
|
66
|
-
# device is that player picking up a controller. More than one means the
|
|
67
|
-
# game expects company.
|
|
68
65
|
@on_unassigned_input = players.size > 1 ? :join : :takeover
|
|
69
66
|
@accepting_joins = true
|
|
70
67
|
@connected = []
|
|
@@ -96,6 +93,8 @@ module RGame
|
|
|
96
93
|
# Nobody in particular means the primary player, which is what makes the
|
|
97
94
|
# single-player path free: no node claims ownership, every node resolves
|
|
98
95
|
# to nil, and every nil resolves to the one player there is.
|
|
96
|
+
#
|
|
97
|
+
# @api private
|
|
99
98
|
# hot-path
|
|
100
99
|
def actions_for(player)
|
|
101
100
|
owner = player || primary
|
|
@@ -119,6 +118,8 @@ module RGame
|
|
|
119
118
|
|
|
120
119
|
# A controller arrived in a slot. Recorded, not seated: this is what makes
|
|
121
120
|
# the slot *scannable*, and someone using it is what seats it.
|
|
121
|
+
#
|
|
122
|
+
# @api private
|
|
122
123
|
def device_connected(slot)
|
|
123
124
|
@connected << slot unless @connected.include?(slot)
|
|
124
125
|
self
|
|
@@ -130,6 +131,8 @@ module RGame
|
|
|
130
131
|
#
|
|
131
132
|
# Under `:takeover` there is no second player to become, so the seat falls
|
|
132
133
|
# back to the keyboard rather than the game going dead in someone's hands.
|
|
134
|
+
#
|
|
135
|
+
# @api private
|
|
133
136
|
def device_disconnected(slot)
|
|
134
137
|
@connected.delete(slot)
|
|
135
138
|
device = Controls.gamepad(slot)
|
|
@@ -157,10 +160,6 @@ module RGame
|
|
|
157
160
|
|
|
158
161
|
private
|
|
159
162
|
|
|
160
|
-
# Watch the devices nobody is holding, and seat one when it is used.
|
|
161
|
-
#
|
|
162
|
-
# Costs nothing when there is nobody to seat: with every seat full, or the
|
|
163
|
-
# policy set to ignore, there is no candidate and no device is looked at.
|
|
164
163
|
def admit(backend)
|
|
165
164
|
return if @on_unassigned_input == :ignore || candidate.nil?
|
|
166
165
|
|
|
@@ -172,8 +171,6 @@ module RGame
|
|
|
172
171
|
end
|
|
173
172
|
end
|
|
174
173
|
|
|
175
|
-
# Who the next unassigned device would go to, and therefore whose bindings
|
|
176
|
-
# decide what counts as a press. Nil when nobody could take one.
|
|
177
174
|
def candidate
|
|
178
175
|
return primary if @on_unassigned_input == :takeover
|
|
179
176
|
|
|
@@ -191,10 +188,6 @@ module RGame
|
|
|
191
188
|
buttons.any? { |id| backend.down?(id, device: device) }
|
|
192
189
|
end
|
|
193
190
|
|
|
194
|
-
# Connected pads nobody holds — plus the keyboard, but only while taking
|
|
195
|
-
# over. The keyboard is always "connected", so under `:join` it would sit
|
|
196
|
-
# waiting to seat whoever pressed Return, which is right for some games and
|
|
197
|
-
# a surprise in most; one that wants a keyboard player seats it explicitly.
|
|
198
191
|
def each_unassigned_device
|
|
199
192
|
@connected.each do |slot|
|
|
200
193
|
device = Controls.gamepad(slot)
|