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.
Files changed (253) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +144 -1
  3. data/README.md +67 -65
  4. data/docs/api/README.md +86 -82
  5. data/docs/api/app.md +125 -68
  6. data/docs/api/assets.md +203 -165
  7. data/docs/api/audio.md +130 -89
  8. data/docs/api/cli.md +259 -0
  9. data/docs/api/components.md +1032 -265
  10. data/docs/api/drawing.md +160 -160
  11. data/docs/api/examples.md +263 -0
  12. data/docs/api/game.md +149 -50
  13. data/docs/api/images.md +39 -39
  14. data/docs/api/input.md +226 -148
  15. data/docs/api/internals.md +241 -62
  16. data/docs/api/localization.md +285 -0
  17. data/docs/api/scene_graph.md +397 -244
  18. data/docs/api/signals.md +81 -76
  19. data/docs/api/systems.md +186 -73
  20. data/docs/api/text.md +58 -41
  21. data/docs/api/tile_maps.md +237 -0
  22. data/docs/api/toolbox.md +395 -146
  23. data/docs/api/ui.md +883 -54
  24. data/docs/api/values.md +216 -33
  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/rgame_core/app/app.c +33 -3
  92. data/ext/rgame_core/app/locale.c +67 -0
  93. data/ext/rgame_core/app/locale.h +28 -0
  94. data/ext/rgame_core/audio/audio.c +39 -2
  95. data/ext/rgame_core/example.rb +0 -49
  96. data/ext/rgame_core/extconf.rb +0 -125
  97. data/ext/rgame_core/include/rgame/core.h +38 -2
  98. data/ext/rgame_core/ruby/audio_ext.c +10 -5
  99. data/ext/rgame_core/ruby/core_ext.c +30 -7
  100. data/ext/rgame_core/ruby/core_ext.h +3 -0
  101. data/ext/rgame_core/ruby/locale_ext.c +44 -0
  102. data/ext/rgame_core/ruby/recording_ext.c +1 -1
  103. data/ext/rgame_core/ruby/renderer_ext.c +20 -20
  104. data/ext/rgame_util/extconf.rb +2 -20
  105. data/ext/rgame_util/route_search.c +305 -0
  106. data/ext/rgame_util/route_search.h +86 -0
  107. data/ext/rgame_util/route_search_ext.c +150 -0
  108. data/ext/rgame_util/solid_grid.c +58 -0
  109. data/ext/rgame_util/solid_grid.h +49 -0
  110. data/ext/rgame_util/solid_grid_ext.c +161 -0
  111. data/ext/rgame_util/tile_sweep.c +164 -0
  112. data/ext/rgame_util/tile_sweep.h +62 -0
  113. data/ext/rgame_util/tile_sweep_ext.c +155 -0
  114. data/ext/rgame_util/util_ext.c +3 -0
  115. data/ext/rgame_util/util_ext.h +15 -0
  116. data/lib/rgame/boot.rb +0 -10
  117. data/lib/rgame/cli/new_project.rb +139 -0
  118. data/lib/rgame/cli/templates/Gemfile.tt +23 -0
  119. data/lib/rgame/cli/templates/README.md.tt +93 -0
  120. data/lib/rgame/cli/templates/Rakefile.tt +9 -0
  121. data/lib/rgame/cli/templates/assets/locales/en.yml.tt +10 -0
  122. data/lib/rgame/cli/templates/game.rb.tt +23 -0
  123. data/lib/rgame/cli/templates/gitignore.tt +12 -0
  124. data/lib/rgame/cli/templates/main.rb.tt +11 -0
  125. data/lib/rgame/cli/templates/nodes/root.rb.tt +24 -0
  126. data/lib/rgame/cli/templates/rspec.tt +2 -0
  127. data/lib/rgame/cli/templates/rubocop.yml.tt +75 -0
  128. data/lib/rgame/cli/templates/ruby-version.tt +1 -0
  129. data/lib/rgame/cli/templates/spec/locales_spec.rb.tt +18 -0
  130. data/lib/rgame/cli/templates/spec/nodes/root_spec.rb.tt +18 -0
  131. data/lib/rgame/cli/templates/spec/spec_helper.rb.tt +37 -0
  132. data/lib/rgame/cli.rb +66 -0
  133. data/lib/rgame/core/app.rb +6 -44
  134. data/lib/rgame/core/asset_manager.rb +13 -31
  135. data/lib/rgame/core/audio.rb +37 -16
  136. data/lib/rgame/core/font.rb +0 -3
  137. data/lib/rgame/core/locale.rb +22 -0
  138. data/lib/rgame/core/nine_slice.rb +0 -21
  139. data/lib/rgame/core/renderer.rb +6 -63
  140. data/lib/rgame/core/sprite_sheet.rb +0 -3
  141. data/lib/rgame/core/tile_map_renderer.rb +2 -19
  142. data/lib/rgame/core/ui_atlas.rb +28 -13
  143. data/lib/rgame/core.rb +1 -8
  144. data/lib/rgame/engine/actor_blockers.rb +131 -0
  145. data/lib/rgame/engine/animation_set.rb +1 -0
  146. data/lib/rgame/engine/audio_director.rb +36 -6
  147. data/lib/rgame/engine/bounds_blockers.rb +74 -0
  148. data/lib/rgame/engine/camera.rb +3 -3
  149. data/lib/rgame/engine/circle_collider.rb +4 -2
  150. data/lib/rgame/engine/collision_box.rb +26 -1
  151. data/lib/rgame/engine/collision_system.rb +110 -22
  152. data/lib/rgame/engine/component.rb +34 -10
  153. data/lib/rgame/engine/components/action_trigger.rb +0 -1
  154. data/lib/rgame/engine/components/animated_sprite.rb +27 -25
  155. data/lib/rgame/engine/components/box_collider.rb +99 -0
  156. data/lib/rgame/engine/components/camera_follow.rb +6 -5
  157. data/lib/rgame/engine/components/character_body.rb +20 -61
  158. data/lib/rgame/engine/components/circle_collider.rb +47 -11
  159. data/lib/rgame/engine/components/collision_world.rb +159 -31
  160. data/lib/rgame/engine/components/despawn_offscreen.rb +24 -8
  161. data/lib/rgame/engine/components/feet_collider.rb +61 -0
  162. data/lib/rgame/engine/components/hop.rb +76 -0
  163. data/lib/rgame/engine/components/identity.rb +73 -0
  164. data/lib/rgame/engine/components/mover.rb +285 -0
  165. data/lib/rgame/engine/components/navigator.rb +145 -0
  166. data/lib/rgame/engine/components/path_follow.rb +123 -31
  167. data/lib/rgame/engine/components/player_controller.rb +5 -2
  168. data/lib/rgame/engine/components/pool.rb +1 -1
  169. data/lib/rgame/engine/components/screen_wrap.rb +33 -11
  170. data/lib/rgame/engine/components/sprite.rb +12 -6
  171. data/lib/rgame/engine/components/targeting.rb +9 -11
  172. data/lib/rgame/engine/components/thrust_controller.rb +1 -1
  173. data/lib/rgame/engine/components/tile_world.rb +41 -13
  174. data/lib/rgame/engine/components/timer.rb +1 -1
  175. data/lib/rgame/engine/components/velocity.rb +23 -7
  176. data/lib/rgame/engine/components/wander_controller.rb +6 -2
  177. data/lib/rgame/engine/components/world.rb +133 -0
  178. data/lib/rgame/engine/contact_set.rb +74 -0
  179. data/lib/rgame/engine/culling.rb +4 -6
  180. data/lib/rgame/engine/debug_overlay.rb +5 -11
  181. data/lib/rgame/engine/i18n/plural.rb +45 -0
  182. data/lib/rgame/engine/i18n/plural_rules.rb +82 -0
  183. data/lib/rgame/engine/i18n/template.rb +59 -0
  184. data/lib/rgame/engine/i18n.rb +276 -51
  185. data/lib/rgame/engine/input/action_mapper.rb +0 -26
  186. data/lib/rgame/engine/input/actions.rb +2 -8
  187. data/lib/rgame/engine/input/input_map.rb +39 -21
  188. data/lib/rgame/engine/nav_grid.rb +87 -0
  189. data/lib/rgame/engine/node2d.rb +248 -133
  190. data/lib/rgame/engine/path.rb +4 -6
  191. data/lib/rgame/engine/players.rb +6 -13
  192. data/lib/rgame/engine/presentation.rb +171 -0
  193. data/lib/rgame/engine/scene/scene_stack.rb +4 -4
  194. data/lib/rgame/engine/sealed_privates.rb +54 -0
  195. data/lib/rgame/engine/spatial_hash.rb +51 -19
  196. data/lib/rgame/engine/text.rb +194 -0
  197. data/lib/rgame/engine/tile_blockers.rb +63 -0
  198. data/lib/rgame/engine/tile_map.rb +3 -5
  199. data/lib/rgame/engine/tile_map_layer.rb +0 -2
  200. data/lib/rgame/engine/tileset.rb +2 -4
  201. data/lib/rgame/engine/timer.rb +2 -2
  202. data/lib/rgame/engine/ui/button.rb +248 -0
  203. data/lib/rgame/engine/ui/column.rb +20 -0
  204. data/lib/rgame/engine/ui/icon_button.rb +93 -0
  205. data/lib/rgame/engine/ui/menu.rb +246 -71
  206. data/lib/rgame/engine/ui/navigation.rb +57 -0
  207. data/lib/rgame/engine/ui/nine_slice_style.rb +50 -0
  208. data/lib/rgame/engine/ui/option_button.rb +163 -0
  209. data/lib/rgame/engine/ui/panel_button.rb +32 -0
  210. data/lib/rgame/engine/ui/panel_menu.rb +36 -0
  211. data/lib/rgame/engine/ui/pointing.rb +146 -0
  212. data/lib/rgame/engine/ui/radial_menu.rb +85 -0
  213. data/lib/rgame/engine/ui/ring.rb +55 -0
  214. data/lib/rgame/engine/ui/row.rb +21 -0
  215. data/lib/rgame/engine/ui/shape_style.rb +102 -0
  216. data/lib/rgame/engine/ui/stack.rb +58 -0
  217. data/lib/rgame/engine/ui/stepping.rb +93 -0
  218. data/lib/rgame/engine/ui/text_button.rb +59 -0
  219. data/lib/rgame/engine/viewports.rb +2 -5
  220. data/lib/rgame/engine/world_view.rb +5 -4
  221. data/lib/rgame/engine.rb +31 -25
  222. data/lib/rgame/game.rb +99 -27
  223. data/lib/rgame/rubocop/cop/game/draw_in_local_space.rb +103 -0
  224. data/lib/rgame/rubocop/cop/game/hot_path.rb +36 -0
  225. data/lib/rgame/rubocop/cop/game/layer_boundary.rb +43 -0
  226. data/lib/rgame/rubocop/cop/game/no_core_in_engine_layer.rb +100 -0
  227. data/lib/rgame/rubocop/cop/game/no_engine_in_core_layer.rb +84 -0
  228. data/lib/rgame/rubocop/cop/game/no_interpolation_in_hot_path.rb +50 -0
  229. data/lib/rgame/rubocop/cop/game/no_literal_text.rb +41 -0
  230. data/lib/rgame/rubocop/cop/game/no_needless_allocation.rb +112 -0
  231. data/lib/rgame/rubocop/default.yml +39 -0
  232. data/lib/rgame/rubocop/plugin.rb +45 -0
  233. data/lib/rgame/rubocop.rb +11 -0
  234. data/lib/rgame/util/color.rb +20 -24
  235. data/lib/rgame/util/controls.rb +14 -28
  236. data/lib/rgame/util/route_search.rb +27 -0
  237. data/lib/rgame/util/save_file.rb +107 -0
  238. data/lib/rgame/util/solid_grid.rb +37 -0
  239. data/lib/rgame/util/tensor.rb +0 -9
  240. data/lib/rgame/util/tile_sweep.rb +36 -0
  241. data/lib/rgame/util/z.rb +4 -14
  242. data/lib/rgame/util.rb +4 -3
  243. data/lib/rgame/version.rb +1 -1
  244. data/lib/rgame.rb +0 -15
  245. metadata +167 -12
  246. data/lib/rgame/engine/actor.rb +0 -53
  247. data/lib/rgame/engine/body.rb +0 -49
  248. data/lib/rgame/engine/cached_label.rb +0 -33
  249. data/lib/rgame/engine/input/player_controller.rb +0 -14
  250. data/lib/rgame/engine/matrix.rb +0 -32
  251. data/lib/rgame/engine/resettable.rb +0 -67
  252. data/lib/rgame/engine/tile_collision.rb +0 -78
  253. data/lib/rgame/engine/ui/menu_item.rb +0 -84
@@ -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
- attr_accessor :x, :y, :angle, :width, :height, :parent
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
- # Insertion order among siblings, the tie-breaker for equal `z`. Engine
16
- # bookkeeping, set by the parent's #add_node the way `parent` is not for
17
- # game code, and meaningless on a node with no parent.
18
- attr_accessor :sibling_order
19
- attr_reader :children, :components, :abs_x, :abs_y, :abs_angle, :abs_input_owner, :z,
20
- :band, :abs_band
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&.children_unsorted!
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 (`examples/15_tiled_world`
70
- # does), so an `attr_accessor :player` here would quietly claim that ivar
71
- # out from under every scene that has one — which it did, and the symptom
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
- # Actor#controller is the thing that produces movement intent, a different
74
- # idea entirely. This name says exactly what it decides and collides with
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
- @x = x
96
- @y = y
234
+ @rel_x = x
235
+ @rel_y = y
97
236
  @z = z
98
237
  self.band = band
99
- @angle = angle
238
+ @rel_angle = angle
100
239
  @width = width
101
240
  @height = height
102
- # Resolved by #resolve_origin at the top of every phase. Seeded here so
103
- # a node that has not been driven yet reads as being at the origin
104
- # rather than as nil which is the same answer resolve_origin gives an
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 = {} # slot (Class by default, or a Symbol name) => component
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.sibling_order = (@child_seq += 1)
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
- resolve_origin
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
- children_in_order.each { it.control(input) }
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
- children_in_order.each { it.update(dt) }
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
- resolve_origin
271
- # This node's own drawing goes in its own layer: the renderer hands out
272
- # the next slot in the node's band, and every `z:` the node passes is an
273
- # offset inside it. Because the traversal takes slots in the order it
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) # detaches + exit_tree; @children shrinks, so don't advance i
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 # revive: a pooled node reacquired after death re-enters here
422
+ @freed = false
329
423
  @components.each(&:on_attach)
330
424
  on_add
331
- children_in_order.each(&:enter_tree)
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
- children_in_order.each(&:exit_tree)
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
- def draw_content(renderer, view)
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
- children_in_order.each { it.draw(renderer, view) }
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 children_in_order
378
- sort_children unless @children_sorted
477
+ def _children_in_order
478
+ _sort_children unless @children_sorted
379
479
  @children
380
480
  end
381
481
 
382
- # Ruby's sort is not stable, so the insertion counter is compared
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.sibling_order <=> b.sibling_order : order
488
+ order.zero? ? a._sibling_order <=> b._sibling_order : order
394
489
  end
395
490
  end
396
491
 
397
- # Resolve this node's absolute transform from the parent origin passed down by the
398
- # traversal. Relative x/y/angle accumulate, so a nested Node offsets and rotates
399
- # its whole subtree: a child's local (x, y) is rotated by the parent's accumulated
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
- @abs_x = @abs_y = 0
409
- @abs_angle = 0 # root pinned to identity, like its position
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
- # Ownership accumulates the same way the transform does: this node's own
416
- # if it has one, otherwise whatever it inherits. Resolved here rather
417
- # than walked on demand so it costs one assignment per phase, and so it
418
- # is equally available in update and draw — a HUD node drawing in its
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
- @abs_x = @parent.abs_x + (@x * cos) - (@y * sin)
434
- @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
435
548
  end
436
- @abs_angle = pa + @angle
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
@@ -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
@@ -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)