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.
Files changed (265) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +228 -0
  3. data/README.md +142 -243
  4. data/docs/api/README.md +141 -90
  5. data/docs/api/app.md +125 -68
  6. data/docs/api/assets.md +203 -166
  7. data/docs/api/audio.md +130 -89
  8. data/docs/api/cli.md +259 -0
  9. data/docs/api/components.md +1045 -254
  10. data/docs/api/drawing.md +187 -119
  11. data/docs/api/examples.md +263 -0
  12. data/docs/api/game.md +165 -45
  13. data/docs/api/images.md +39 -39
  14. data/docs/api/input.md +344 -85
  15. data/docs/api/internals.md +241 -62
  16. data/docs/api/localization.md +285 -0
  17. data/docs/api/scene_graph.md +503 -123
  18. data/docs/api/signals.md +81 -76
  19. data/docs/api/systems.md +197 -64
  20. data/docs/api/text.md +58 -41
  21. data/docs/api/tile_maps.md +237 -0
  22. data/docs/api/toolbox.md +400 -147
  23. data/docs/api/ui.md +927 -0
  24. data/docs/api/values.md +234 -19
  25. data/examples/assets/README.md +322 -0
  26. data/examples/assets/blip.ogg +0 -0
  27. data/examples/assets/glyphs.json +5 -0
  28. data/examples/assets/glyphs.png +0 -0
  29. data/examples/assets/hero.json +12 -0
  30. data/examples/assets/hero.png +0 -0
  31. data/examples/assets/icons.json +13 -0
  32. data/examples/assets/icons.png +0 -0
  33. data/examples/assets/music.ogg +0 -0
  34. data/examples/assets/skills.json +10 -0
  35. data/examples/assets/skills.png +0 -0
  36. data/examples/assets/tileset.png +0 -0
  37. data/examples/assets/tileset.tsx +65 -0
  38. data/examples/assets/town.tmx +26 -0
  39. data/examples/assets/ui.json +11 -0
  40. data/examples/assets/ui.png +0 -0
  41. data/examples/collision/locales/en.yml +8 -0
  42. data/examples/collision/main.rb +316 -0
  43. data/examples/collision_tiles/locales/en.yml +9 -0
  44. data/examples/collision_tiles/main.rb +274 -0
  45. data/examples/fullscreen/locales/en.yml +10 -0
  46. data/examples/fullscreen/main.rb +216 -0
  47. data/examples/game_menu/locales/en.yml +8 -0
  48. data/examples/game_menu/main.rb +170 -0
  49. data/examples/input_glyphs/locales/en.yml +14 -0
  50. data/examples/input_glyphs/main.rb +213 -0
  51. data/examples/jump_topdown/locales/en.yml +9 -0
  52. data/examples/jump_topdown/main.rb +178 -0
  53. data/examples/localization/locales/de.yml +12 -0
  54. data/examples/localization/locales/en.yml +13 -0
  55. data/examples/localization/main.rb +158 -0
  56. data/examples/menu_navigation/locales/en.yml +23 -0
  57. data/examples/menu_navigation/main.rb +365 -0
  58. data/examples/music/locales/en.yml +7 -0
  59. data/examples/music/main.rb +134 -0
  60. data/examples/pathfinding/locales/en.yml +17 -0
  61. data/examples/pathfinding/main.rb +298 -0
  62. data/examples/pooling/locales/en.yml +7 -0
  63. data/examples/pooling/main.rb +259 -0
  64. data/examples/quick_wheel/locales/en.yml +16 -0
  65. data/examples/quick_wheel/main.rb +184 -0
  66. data/examples/radial_menu/locales/en.yml +16 -0
  67. data/examples/radial_menu/main.rb +184 -0
  68. data/examples/save_load/locales/en.yml +11 -0
  69. data/examples/save_load/main.rb +207 -0
  70. data/examples/save_load_ids/locales/en.yml +11 -0
  71. data/examples/save_load_ids/main.rb +322 -0
  72. data/examples/scroll_map/locales/en.yml +4 -0
  73. data/examples/scroll_map/main.rb +140 -0
  74. data/examples/signals/locales/en.yml +6 -0
  75. data/examples/signals/main.rb +278 -0
  76. data/examples/skill_bar/locales/en.yml +14 -0
  77. data/examples/skill_bar/main.rb +159 -0
  78. data/examples/sound/locales/en.yml +6 -0
  79. data/examples/sound/main.rb +122 -0
  80. data/examples/split_screen/locales/en.yml +9 -0
  81. data/examples/split_screen/main.rb +304 -0
  82. data/examples/sprite/locales/en.yml +8 -0
  83. data/examples/sprite/main.rb +180 -0
  84. data/examples/timer/locales/en.yml +12 -0
  85. data/examples/timer/main.rb +273 -0
  86. data/examples/velocity/locales/en.yml +6 -0
  87. data/examples/velocity/main.rb +196 -0
  88. data/examples/walk/locales/en.yml +4 -0
  89. data/examples/walk/main.rb +99 -0
  90. data/exe/rgame +9 -0
  91. data/ext/README.md +6 -5
  92. data/ext/rgame_core/app/app.c +215 -11
  93. data/ext/rgame_core/app/locale.c +67 -0
  94. data/ext/rgame_core/app/locale.h +28 -0
  95. data/ext/rgame_core/audio/audio.c +113 -2
  96. data/ext/rgame_core/example.rb +18 -56
  97. data/ext/rgame_core/extconf.rb +16 -113
  98. data/ext/rgame_core/graphics/canvas.c +45 -4
  99. data/ext/rgame_core/graphics/canvas.h +65 -10
  100. data/ext/rgame_core/graphics/clip.c +22 -13
  101. data/ext/rgame_core/include/rgame/core.h +151 -5
  102. data/ext/rgame_core/input/gamepad.c +57 -3
  103. data/ext/rgame_core/ruby/audio_ext.c +10 -5
  104. data/ext/rgame_core/ruby/core_ext.c +46 -7
  105. data/ext/rgame_core/ruby/core_ext.h +3 -0
  106. data/ext/rgame_core/ruby/locale_ext.c +44 -0
  107. data/ext/rgame_core/ruby/recording_ext.c +1 -1
  108. data/ext/rgame_core/ruby/renderer_ext.c +42 -19
  109. data/ext/rgame_util/color_ext.c +12 -3
  110. data/ext/rgame_util/extconf.rb +2 -20
  111. data/ext/rgame_util/route_search.c +305 -0
  112. data/ext/rgame_util/route_search.h +86 -0
  113. data/ext/rgame_util/route_search_ext.c +150 -0
  114. data/ext/rgame_util/solid_grid.c +58 -0
  115. data/ext/rgame_util/solid_grid.h +49 -0
  116. data/ext/rgame_util/solid_grid_ext.c +161 -0
  117. data/ext/rgame_util/tile_sweep.c +164 -0
  118. data/ext/rgame_util/tile_sweep.h +62 -0
  119. data/ext/rgame_util/tile_sweep_ext.c +155 -0
  120. data/ext/rgame_util/util_ext.c +3 -0
  121. data/ext/rgame_util/util_ext.h +15 -0
  122. data/lib/rgame/boot.rb +0 -10
  123. data/lib/rgame/cli/new_project.rb +139 -0
  124. data/lib/rgame/cli/templates/Gemfile.tt +23 -0
  125. data/lib/rgame/cli/templates/README.md.tt +93 -0
  126. data/lib/rgame/cli/templates/Rakefile.tt +9 -0
  127. data/lib/rgame/cli/templates/assets/locales/en.yml.tt +10 -0
  128. data/lib/rgame/cli/templates/game.rb.tt +23 -0
  129. data/lib/rgame/cli/templates/gitignore.tt +12 -0
  130. data/lib/rgame/cli/templates/main.rb.tt +11 -0
  131. data/lib/rgame/cli/templates/nodes/root.rb.tt +24 -0
  132. data/lib/rgame/cli/templates/rspec.tt +2 -0
  133. data/lib/rgame/cli/templates/rubocop.yml.tt +75 -0
  134. data/lib/rgame/cli/templates/ruby-version.tt +1 -0
  135. data/lib/rgame/cli/templates/spec/locales_spec.rb.tt +18 -0
  136. data/lib/rgame/cli/templates/spec/nodes/root_spec.rb.tt +18 -0
  137. data/lib/rgame/cli/templates/spec/spec_helper.rb.tt +37 -0
  138. data/lib/rgame/cli.rb +66 -0
  139. data/lib/rgame/core/app.rb +6 -42
  140. data/lib/rgame/core/asset_manager.rb +13 -31
  141. data/lib/rgame/core/audio.rb +37 -16
  142. data/lib/rgame/core/font.rb +0 -3
  143. data/lib/rgame/core/input.rb +35 -41
  144. data/lib/rgame/core/locale.rb +22 -0
  145. data/lib/rgame/core/nine_slice.rb +0 -21
  146. data/lib/rgame/core/recording.rb +3 -1
  147. data/lib/rgame/core/renderer.rb +75 -84
  148. data/lib/rgame/core/sprite_sheet.rb +0 -3
  149. data/lib/rgame/core/tile_map_renderer.rb +77 -65
  150. data/lib/rgame/core/ui_atlas.rb +28 -13
  151. data/lib/rgame/core.rb +1 -8
  152. data/lib/rgame/engine/actor_blockers.rb +131 -0
  153. data/lib/rgame/engine/animation_set.rb +1 -0
  154. data/lib/rgame/engine/audio_director.rb +36 -6
  155. data/lib/rgame/engine/bounds_blockers.rb +74 -0
  156. data/lib/rgame/engine/camera.rb +55 -10
  157. data/lib/rgame/engine/circle_collider.rb +4 -2
  158. data/lib/rgame/engine/collision_box.rb +26 -1
  159. data/lib/rgame/engine/collision_system.rb +110 -22
  160. data/lib/rgame/engine/component.rb +35 -1
  161. data/lib/rgame/engine/components/action_trigger.rb +0 -1
  162. data/lib/rgame/engine/components/animated_sprite.rb +31 -23
  163. data/lib/rgame/engine/components/box_collider.rb +99 -0
  164. data/lib/rgame/engine/components/camera_follow.rb +45 -0
  165. data/lib/rgame/engine/components/character_body.rb +21 -41
  166. data/lib/rgame/engine/components/circle_collider.rb +47 -11
  167. data/lib/rgame/engine/components/collision_world.rb +159 -31
  168. data/lib/rgame/engine/components/despawn_offscreen.rb +24 -8
  169. data/lib/rgame/engine/components/feet_collider.rb +61 -0
  170. data/lib/rgame/engine/components/hop.rb +76 -0
  171. data/lib/rgame/engine/components/identity.rb +73 -0
  172. data/lib/rgame/engine/components/mover.rb +285 -0
  173. data/lib/rgame/engine/components/navigator.rb +145 -0
  174. data/lib/rgame/engine/components/path_follow.rb +123 -31
  175. data/lib/rgame/engine/components/player_controller.rb +5 -2
  176. data/lib/rgame/engine/components/pool.rb +1 -1
  177. data/lib/rgame/engine/components/screen_wrap.rb +33 -11
  178. data/lib/rgame/engine/components/sprite.rb +22 -6
  179. data/lib/rgame/engine/components/targeting.rb +9 -11
  180. data/lib/rgame/engine/components/thrust_controller.rb +1 -1
  181. data/lib/rgame/engine/components/tile_world.rb +70 -29
  182. data/lib/rgame/engine/components/timer.rb +1 -1
  183. data/lib/rgame/engine/components/velocity.rb +23 -7
  184. data/lib/rgame/engine/components/wander_controller.rb +6 -2
  185. data/lib/rgame/engine/components/world.rb +133 -0
  186. data/lib/rgame/engine/contact_set.rb +74 -0
  187. data/lib/rgame/engine/culling.rb +45 -0
  188. data/lib/rgame/engine/debug_overlay.rb +23 -18
  189. data/lib/rgame/engine/i18n/plural.rb +45 -0
  190. data/lib/rgame/engine/i18n/plural_rules.rb +82 -0
  191. data/lib/rgame/engine/i18n/template.rb +59 -0
  192. data/lib/rgame/engine/i18n.rb +276 -51
  193. data/lib/rgame/engine/input/action_mapper.rb +76 -22
  194. data/lib/rgame/engine/input/actions.rb +63 -12
  195. data/lib/rgame/engine/input/input_map.rb +196 -0
  196. data/lib/rgame/engine/layout.rb +82 -0
  197. data/lib/rgame/engine/nav_grid.rb +87 -0
  198. data/lib/rgame/engine/node2d.rb +364 -80
  199. data/lib/rgame/engine/path.rb +4 -6
  200. data/lib/rgame/engine/player.rb +69 -0
  201. data/lib/rgame/engine/player_layer.rb +70 -0
  202. data/lib/rgame/engine/players.rb +205 -0
  203. data/lib/rgame/engine/presentation.rb +171 -0
  204. data/lib/rgame/engine/scene/scene_stack.rb +29 -7
  205. data/lib/rgame/engine/sealed_privates.rb +54 -0
  206. data/lib/rgame/engine/spatial_hash.rb +53 -8
  207. data/lib/rgame/engine/text.rb +194 -0
  208. data/lib/rgame/engine/tile_blockers.rb +63 -0
  209. data/lib/rgame/engine/tile_map.rb +2 -3
  210. data/lib/rgame/engine/tile_map_layer.rb +82 -0
  211. data/lib/rgame/engine/tileset.rb +2 -4
  212. data/lib/rgame/engine/timer.rb +2 -2
  213. data/lib/rgame/engine/ui/button.rb +248 -0
  214. data/lib/rgame/engine/ui/column.rb +20 -0
  215. data/lib/rgame/engine/ui/icon_button.rb +93 -0
  216. data/lib/rgame/engine/ui/menu.rb +290 -0
  217. data/lib/rgame/engine/ui/navigation.rb +57 -0
  218. data/lib/rgame/engine/ui/nine_slice_style.rb +50 -0
  219. data/lib/rgame/engine/ui/option_button.rb +163 -0
  220. data/lib/rgame/engine/ui/panel_button.rb +32 -0
  221. data/lib/rgame/engine/ui/panel_menu.rb +36 -0
  222. data/lib/rgame/engine/ui/pointing.rb +146 -0
  223. data/lib/rgame/engine/ui/radial_menu.rb +85 -0
  224. data/lib/rgame/engine/ui/ring.rb +55 -0
  225. data/lib/rgame/engine/ui/row.rb +21 -0
  226. data/lib/rgame/engine/ui/shape_style.rb +102 -0
  227. data/lib/rgame/engine/ui/stack.rb +58 -0
  228. data/lib/rgame/engine/ui/stepping.rb +93 -0
  229. data/lib/rgame/engine/ui/text_button.rb +59 -0
  230. data/lib/rgame/engine/view.rb +76 -0
  231. data/lib/rgame/engine/viewports.rb +171 -0
  232. data/lib/rgame/engine/world_view.rb +71 -0
  233. data/lib/rgame/engine.rb +43 -25
  234. data/lib/rgame/game.rb +164 -22
  235. data/lib/rgame/rubocop/cop/game/draw_in_local_space.rb +103 -0
  236. data/lib/rgame/rubocop/cop/game/hot_path.rb +36 -0
  237. data/lib/rgame/rubocop/cop/game/layer_boundary.rb +43 -0
  238. data/lib/rgame/rubocop/cop/game/no_core_in_engine_layer.rb +100 -0
  239. data/lib/rgame/rubocop/cop/game/no_engine_in_core_layer.rb +84 -0
  240. data/lib/rgame/rubocop/cop/game/no_interpolation_in_hot_path.rb +50 -0
  241. data/lib/rgame/rubocop/cop/game/no_literal_text.rb +41 -0
  242. data/lib/rgame/rubocop/cop/game/no_needless_allocation.rb +112 -0
  243. data/lib/rgame/rubocop/default.yml +39 -0
  244. data/lib/rgame/rubocop/plugin.rb +45 -0
  245. data/lib/rgame/rubocop.rb +11 -0
  246. data/lib/rgame/util/color.rb +20 -24
  247. data/lib/rgame/util/controls.rb +106 -44
  248. data/lib/rgame/util/route_search.rb +27 -0
  249. data/lib/rgame/util/save_file.rb +107 -0
  250. data/lib/rgame/util/solid_grid.rb +37 -0
  251. data/lib/rgame/util/tensor.rb +0 -9
  252. data/lib/rgame/util/tile_sweep.rb +36 -0
  253. data/lib/rgame/util/z.rb +123 -0
  254. data/lib/rgame/util.rb +5 -3
  255. data/lib/rgame/version.rb +1 -1
  256. data/lib/rgame.rb +0 -15
  257. metadata +176 -20
  258. data/lib/rgame/engine/actor.rb +0 -53
  259. data/lib/rgame/engine/body.rb +0 -49
  260. data/lib/rgame/engine/cached_label.rb +0 -33
  261. data/lib/rgame/engine/camera_view.rb +0 -28
  262. data/lib/rgame/engine/input/player_controller.rb +0 -14
  263. data/lib/rgame/engine/matrix.rb +0 -32
  264. data/lib/rgame/engine/resettable.rb +0 -67
  265. data/lib/rgame/engine/tile_collision.rb +0 -78
@@ -8,21 +8,246 @@ module RGame
8
8
  # DSL to allow for easy signal usage.
9
9
  class Node2D
10
10
  extend Engine::Signal::DSL
11
-
12
- attr_accessor :x, :y, :z, :angle, :width, :height, :parent
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
13
44
  attr_writer :scene, :context
14
- attr_reader :children, :components, :abs_x, :abs_y, :abs_z, :abs_angle
15
45
 
16
- def initialize(x: 0, y: 0, z: 0, angle: 0, width: 0, height: 0)
17
- @x = x
18
- @y = y
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
19
236
  @z = z
20
- @angle = angle
237
+ self.band = band
238
+ @rel_angle = angle
21
239
  @width = width
22
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
23
246
  @children = []
247
+ @child_seq = 0
248
+ @children_sorted = true
24
249
  @components = []
25
- @component_slots = {} # slot (Class by default, or a Symbol name) => component
250
+ @component_slots = {}
26
251
  @parent = nil
27
252
  @scene = nil
28
253
  @in_tree = false
@@ -32,9 +257,8 @@ module RGame
32
257
  def add_node(node)
33
258
  @children << node
34
259
  node.parent = self
35
- # Defer the entered-tree cascade until this node is itself live; otherwise it
36
- # fires when an ancestor enters (see #enter_tree). This is the construct-vs-enter
37
- # split — a node built inside another node's initialize is not yet in the tree.
260
+ node._sibling_order = (@child_seq += 1)
261
+ @children_sorted = false
38
262
  node.enter_tree if @in_tree
39
263
  node
40
264
  end
@@ -90,11 +314,6 @@ module RGame
90
314
  component
91
315
  end
92
316
 
93
- # Anchors, resolved by walking parents so they can never go stale (a cached
94
- # back-link set at add-time breaks when children are built before the node is
95
- # in the tree). Shared systems live as components on an anchor node and are
96
- # reached through these, not threaded through constructors.
97
-
98
317
  # The top-most node — a node with no parent is its own root. Global,
99
318
  # program-lifetime systems live here as components.
100
319
  def root
@@ -116,45 +335,53 @@ module RGame
116
335
  scene&.get_component(klass) || root.get_component(klass)
117
336
  end
118
337
 
119
- # updates input, both from player (readings actions) as well as
120
- # AI-driven node control. This run first in a game tick
121
- # Each phase settles this node first (components, then the node's own
122
- # hook), then descends into children. Self-before-subtree keeps the
123
- # transform flowing downward: a component or hook that moves this node
124
- # does so before children resolve their origin from it.
125
-
126
- def control(actions)
127
- resolve_origin
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)
128
355
  @components.each { it.control(actions) }
129
356
  on_control(actions)
130
- @children.each { it.control(actions) }
357
+ _children_in_order.each { it.control(input) }
131
358
  end
132
359
 
133
360
  # update game logic and physics (might become two calls with
134
361
  # time, but for now works in one step). This runs second in a
135
362
  # game tick
136
363
  def update(dt)
137
- resolve_origin
364
+ return if @paused
365
+
138
366
  @components.each { it.update(dt) }
139
367
  on_update(dt)
140
- @children.each { it.update(dt) }
368
+ _children_in_order.each { it.update(dt) }
141
369
  end
142
370
 
143
371
  # update visual game state, drawing the node. This runs last in
144
372
  # a game tick
145
- def draw(renderer)
146
- resolve_origin
147
- # Draw this node's own visuals oriented by its absolute angle, then descend.
148
- # Children resolve their own world transform (resolve_origin already baked this
149
- # node's rotation into their abs_x/abs_y), so they draw in flat world space and
150
- # must NOT be nested inside this node's rotation — nesting would apply that
151
- # rotation to them a second time. Unrotated nodes skip the wrapper entirely.
152
- if abs_angle.zero?
153
- draw_content(renderer)
154
- else
155
- renderer.rotated(abs_angle * 180.0 / Math::PI, abs_x, abs_y) { draw_content(renderer) }
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)
156
384
  end
157
- draw_children(renderer)
158
385
  end
159
386
 
160
387
  def in_tree? = @in_tree
@@ -176,7 +403,7 @@ module RGame
176
403
  while i < @children.size
177
404
  child = @children[i]
178
405
  if child.freed?
179
- remove_node(child) # detaches + exit_tree; @children shrinks, so don't advance i
406
+ remove_node(child)
180
407
  else
181
408
  child.sweep_freed
182
409
  i += 1
@@ -192,10 +419,10 @@ module RGame
192
419
  return if @in_tree
193
420
 
194
421
  @in_tree = true
195
- @freed = false # revive: a pooled node reacquired after death re-enters here
422
+ @freed = false
196
423
  @components.each(&:on_attach)
197
424
  on_add
198
- @children.each(&:enter_tree)
425
+ _children_in_order.each(&:enter_tree)
199
426
  end
200
427
 
201
428
  # Leaving-tree cascade: mirror of #enter_tree (children first, then this
@@ -203,68 +430,125 @@ module RGame
203
430
  def exit_tree
204
431
  return unless @in_tree
205
432
 
206
- @children.each(&:exit_tree)
433
+ _children_in_order.each(&:exit_tree)
207
434
  on_remove
208
435
  @components.each(&:on_detach)
209
436
  @in_tree = false
210
437
  end
211
438
 
212
- # Lifecycle hooks: Subclasses should implement these instead of
213
- # overwriting the public interface draw/update/add etc. on_add/on_remove
214
- # fire when the node enters/leaves the live tree (see #enter_tree), not at
215
- # construction — so anchors and systems are available inside them.
216
-
217
439
  def on_control(actions); end
218
440
  def on_update(dt); end
219
- def on_draw(renderer); end
441
+ def on_draw(renderer, view); end
220
442
  def on_add; end
221
443
  def on_remove; end
222
444
 
223
445
  private
224
446
 
225
- # This node's own drawing: its components and its draw hook, in that order.
226
- # Wrapped in renderer.rotated by #draw when the node carries an absolute angle.
227
- def draw_content(renderer)
228
- @components.each { it.draw(renderer) }
229
- on_draw(renderer)
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
230
462
  end
463
+ # rubocop:enable Style/ExplicitBlockArgument
231
464
 
232
- # Draw the child subtrees. Its own method so a node can wrap the whole subtree's
233
- # draw in a transform — e.g. CameraView wraps it in renderer.translated to apply a
234
- # camera offset, without each child knowing about the camera.
235
- def draw_children(renderer)
236
- @children.each { it.draw(renderer) }
465
+ # hot-path
466
+ def _draw_content(renderer, view)
467
+ @components.each { it.draw(renderer, view) }
468
+ on_draw(renderer, view)
237
469
  end
238
470
 
239
- # TODO: Calculate (and cache?) depth
240
- # depth = z + highest z of children, maybe +1?
471
+ # hot-path
472
+ def draw_children(renderer, view)
473
+ _children_in_order.each { it.draw(renderer, view) }
474
+ end
241
475
 
242
- # Resolve this node's absolute transform from the parent origin passed down by the
243
- # traversal. Relative x/y/z/angle accumulate, so a nested Node offsets, re-layers
244
- # and rotates its whole subtree: a child's local (x, y) is rotated by the parent's
245
- # accumulated angle before being added to the parent's origin.
246
- # TODO: Do not recalculate every time, but use a @dirty flag
247
- # TODO: Handle z better - offset not by Z of the parent, but their
248
- # depth
249
- def resolve_origin
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
250
495
  if @parent.nil?
251
- @abs_x = @abs_y = @abs_z = 0
252
- @abs_angle = 0 # root pinned to identity, like its position
496
+ @world_x = @world_y = 0
497
+ @world_angle = 0
253
498
  return
254
499
  end
255
500
 
256
- pa = @parent.abs_angle
257
- if pa.zero? # fast path: parent unrotated -> plain translation, no trig
258
- @abs_x = @parent.abs_x + @x
259
- @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
260
505
  else
261
506
  cos = Math.cos(pa)
262
507
  sin = Math.sin(pa)
263
- @abs_x = @parent.abs_x + (@x * cos) - (@y * sin)
264
- @abs_y = @parent.abs_y + (@x * sin) + (@y * cos)
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
265
548
  end
266
- @abs_z = @parent.abs_z + @z
267
- @abs_angle = pa + @angle
549
+
550
+ @abs_input_owner = @input_owner || @parent.abs_input_owner
551
+ @abs_band = @band || @parent.abs_band
268
552
  end
269
553
  end
270
554
  end
@@ -2,8 +2,8 @@
2
2
 
3
3
  module RGame
4
4
  module Engine
5
- # An ordered polyline of waypoints an entity walks along — the "road" of a tower
6
- # defense level. Pure data: it holds the waypoints and the precomputed per-segment
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 tower-defense level can mask placement cells that sit on it.
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
@@ -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