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
@@ -0,0 +1,304 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Split screen — two players, one world.
4
+ #
5
+ # Run it:
6
+ #
7
+ # ruby examples/split_screen/main.rb
8
+ #
9
+ # Player one walks with the arrow keys or WASD. Press **A** on a controller and
10
+ # player two joins: the screen splits, a second walker appears, and each half
11
+ # follows its own player. Space (or A) waves, and only the waver's own badge
12
+ # counts it. It exercises:
13
+ # - Game.new(players: 2) — two seats, the second of them empty at the start;
14
+ # - Engine::Players — who is playing, and the `on_joined` signal that says
15
+ # somebody now is;
16
+ # - Engine::WorldView — the world, drawn once per viewport through that
17
+ # viewport's camera;
18
+ # - Engine::PlayerLayer — one player's own corner of the screen;
19
+ # - Engine::Camera and Components::CameraFollow — one camera per player;
20
+ # - `input_owner` — which player a subtree answers to, inherited like a
21
+ # transform.
22
+ #
23
+ # ## The world does not know how many times it is drawn
24
+ #
25
+ # That is the sentence the whole design exists to make true, and this example is
26
+ # the only place it can be shown rather than asserted. `Ground` draws the floor
27
+ # once in its `on_draw`; with two players that method runs twice a frame, with
28
+ # one it runs once, and nothing in it can tell. The walkers are the same: each is
29
+ # a node in the world, so each is drawn in every viewport that can see it —
30
+ # player two walks around inside player one's view, because there is one world
31
+ # and two windows onto it.
32
+ #
33
+ # The update is not doubled. A frame is one `update(dt)` over the whole tree and
34
+ # then one `draw` per viewport, so nothing moves twice when the screen splits.
35
+ #
36
+ # ## What a viewport cannot see, it does not draw
37
+ #
38
+ # The two walkers start side by side and each appears in both halves. Walk them
39
+ # apart and each half is down to one sprite, because `AnimatedSprite` compares
40
+ # the node's world box against the view's camera rectangle and skips the draw —
41
+ # `Engine::Culling`, which split-screen is the reason for: with one viewport a
42
+ # skipped draw saved a little, and with one per player it saves that much per
43
+ # player.
44
+ #
45
+ # Only components that know a node's footprint cull. The banner `Walker` draws
46
+ # itself is not culled, just clipped away with the rest of what is outside the
47
+ # region — at one rectangle a check would cost more than the draw, and that is
48
+ # the trade rather than an oversight.
49
+ #
50
+ # ## Three kinds of content, and a node's place decides which it is
51
+ #
52
+ # A frame here holds all three, and no node is told which it is in:
53
+ #
54
+ # - **world content** — under the `WorldView`, drawn once per viewport, through
55
+ # that viewport's camera, so its coordinates are places in the world;
56
+ # - **a player's own screen space** — under a `PlayerLayer`, drawn once for that
57
+ # player, clipped to their region and laid out from *its* corner, so a badge
58
+ # at (16, 16) is sixteen pixels inside whichever half they were given;
59
+ # - **a global overlay** — a plain child of the scene, like the two lines of
60
+ # instructions at the bottom. Drawn once across the whole window whatever the
61
+ # split is doing.
62
+ #
63
+ # ## The screen splits when somebody joins, not when a pad is plugged in
64
+ #
65
+ # The game opens with two seats and one player. The second seat is empty, so
66
+ # there is one viewport and it is the whole window — an ordinary single-player
67
+ # game, which is what a two-player game should look like until the second player
68
+ # turns up.
69
+ #
70
+ # A device is seated when somebody *uses* it. Pressing A on an unassigned
71
+ # controller fills the empty seat, `Players#on_joined` fires with the player who
72
+ # got it, and this scene spawns their walker and their badge in the handler. The
73
+ # split follows from there: `Viewports` gives a region to each active player, and
74
+ # one more active player is one more region. Nothing here counts anything or
75
+ # asks how the screen is divided.
76
+ #
77
+ # ## Ownership is inherited, so neither subtree mentions players
78
+ #
79
+ # `walker.input_owner = player` is the only line about ownership in this file,
80
+ # and `PlayerLayer` sets the same thing for its subtree. Everything below either
81
+ # one reads that player's device: `PlayerController` moves the right walker,
82
+ # `Badge#on_control` counts the right waves, and neither class names a player or
83
+ # knows that there is more than one.
84
+ #
85
+ # That is what keeps a HUD written for one player working for two. The badge
86
+ # under player two's layer is the same class, with nothing configured, drawn in
87
+ # their half because their layer is where it hangs.
88
+ #
89
+ # ## What it does not solve
90
+ #
91
+ # Anything about the world itself — there is a floor, some landmarks and two
92
+ # walkers, and they pass through each other. Every other example puts its subject
93
+ # in the world; this one's subject is the plumbing around it, and a busier world
94
+ # would only hide the thing being shown.
95
+ #
96
+ # `Ground` also draws the whole floor every time, which is thirty-eight
97
+ # rectangles and not worth thinking about. A world big enough for that to matter
98
+ # draws what the `view` it is handed can actually show — the same camera
99
+ # rectangle the sprites are culled against.
100
+
101
+ $LOAD_PATH.unshift File.expand_path('../../lib', __dir__)
102
+ require 'rgame/game'
103
+
104
+ WIDTH = 640
105
+ HEIGHT = 480
106
+ ASSETS = File.expand_path('../assets', __dir__)
107
+ LOCALES = File.expand_path('locales', __dir__) # the text on screen: locales/en.yml
108
+
109
+ # Bigger than the window on both axes, so a camera has somewhere to go and the
110
+ # two halves can be looking at genuinely different places.
111
+ WORLD_W = 1280
112
+ WORLD_H = 960
113
+
114
+ SPEED = 120.0
115
+
116
+ # Where the camera looks: the middle of the 16x22 sprite rather than its
117
+ # top-left origin.
118
+ CAMERA_OFFSET_X = 8
119
+ CAMERA_OFFSET_Y = 11
120
+
121
+ # One player, one colour. Their walker carries it as a banner and their badge
122
+ # prints their name in it, which is the only way to tell two halves apart at a
123
+ # glance.
124
+ TINTS = [RGame::Util::Color.new(255, 190, 90), RGame::Util::Color.new(120, 200, 255)].freeze
125
+
126
+ # Where each player's walker starts: side by side, so each of them is in the
127
+ # other's half of the screen until the two of them walk apart.
128
+ STARTS = [[520.0, 460.0], [700.0, 500.0]].freeze
129
+
130
+ # The world: a floor, a grid to make movement visible, and landmarks to tell one
131
+ # part of it from another. It is a single node under the WorldView, so this
132
+ # `on_draw` runs once per viewport and knows nothing about that.
133
+ class Ground < RGame::Engine::Node2D
134
+ FLOOR = RGame::Util::Color.new(38, 54, 44)
135
+ GRID = RGame::Util::Color.new(50, 70, 58)
136
+ CELL = 64
137
+
138
+ # x, y and a colour: four blocks in four quarters of the world, so which half
139
+ # of the screen is looking where is obvious.
140
+ LANDMARKS = [
141
+ [200, 200, RGame::Util::Color.new(196, 92, 72)],
142
+ [980, 240, RGame::Util::Color.new(92, 148, 196)],
143
+ [260, 700, RGame::Util::Color.new(214, 196, 96)],
144
+ [1000, 740, RGame::Util::Color.new(140, 110, 196)]
145
+ ].freeze
146
+ BLOCK = 64
147
+
148
+ def on_draw(renderer, _view)
149
+ renderer.rect(0, 0, WORLD_W, WORLD_H, color: FLOOR)
150
+ draw_grid(renderer)
151
+ LANDMARKS.each { |x, y, color| renderer.rect(x, y, BLOCK, BLOCK, color: color) }
152
+ end
153
+
154
+ private
155
+
156
+ # While loops rather than ranges: this runs once per viewport per frame, and a
157
+ # fresh Range every time is the allocation the hot-path cops refuse.
158
+ #
159
+ # hot-path
160
+ def draw_grid(renderer)
161
+ x = CELL
162
+ while x < WORLD_W
163
+ renderer.rect(x, 0, 1, WORLD_H, color: GRID)
164
+ x += CELL
165
+ end
166
+
167
+ y = CELL
168
+ while y < WORLD_H
169
+ renderer.rect(0, y, WORLD_W, 1, color: GRID)
170
+ y += CELL
171
+ end
172
+ end
173
+ end
174
+
175
+ # A player's avatar: `examples/walk`'s hero with a camera on it and a banner in
176
+ # their colour. Nothing in it refers to a player — the scene sets `input_owner`
177
+ # once, and the controller below reads whatever that resolves to.
178
+ class Walker < RGame::Engine::Node2D
179
+ BANNER_H = 4
180
+
181
+ def initialize(tint:, camera:, **)
182
+ super(**)
183
+ @tint = tint
184
+ add_component(RGame::Engine::Components::AnimatedSprite.new(sheet: 'hero.json'))
185
+ add_component(RGame::Engine::Components::CharacterBody.new(speed: SPEED))
186
+ add_component(RGame::Engine::Components::PlayerController.new)
187
+ add_component(RGame::Engine::Components::CameraFollow.new(
188
+ camera: camera, offset_x: CAMERA_OFFSET_X, offset_y: CAMERA_OFFSET_Y
189
+ ))
190
+ end
191
+
192
+ # Keep them on the floor. A plain CharacterBody walks wherever the intent
193
+ # points; the cameras stop at the world's edges on their own, and a walker that
194
+ # kept going would leave its half of the screen showing a player pushing a key
195
+ # with nothing happening.
196
+ def on_update(_dt)
197
+ self.x = x.clamp(0, WORLD_W - width)
198
+ self.y = y.clamp(0, WORLD_H - height)
199
+ end
200
+
201
+ def on_draw(renderer, _view) = renderer.rect(0, -BANNER_H - 2, width, BANNER_H, color: @tint)
202
+ end
203
+
204
+ # One player's badge, in their own corner of the screen.
205
+ #
206
+ # Written as though there were one player, because from in here there is. It
207
+ # hangs under a PlayerLayer, so it is drawn inside that player's region, laid out
208
+ # from that region's corner, and `on_control` is handed that player's actions.
209
+ class Badge < RGame::Engine::Node2D
210
+ PANEL = RGame::Util::Color.new(20, 26, 34, 190)
211
+ INK = RGame::Util::Color.new(226, 230, 238)
212
+ W = 108
213
+ H = 46
214
+
215
+ def initialize(number:, tint:, **)
216
+ super(**)
217
+ @name = RGame::Engine::Text.new('hud.player', :number)
218
+ @name.with(number: number)
219
+ @tint = tint
220
+ @waves = 0
221
+ @label = RGame::Engine::Text.new('hud.waves', :count)
222
+ end
223
+
224
+ def on_control(actions)
225
+ @waves += 1 if actions.pressed?(:fire)
226
+ end
227
+
228
+ def on_draw(renderer, _view)
229
+ renderer.rect(0, 0, W, H, color: PANEL)
230
+ renderer.rect(0, 0, W, 3, color: @tint)
231
+ renderer.text(@name, 8, 8, color: @tint)
232
+ renderer.text(@label.with(count: @waves), 8, 26, color: INK)
233
+ end
234
+ end
235
+
236
+ class Scene < RGame::Engine::Node2D
237
+ MARGIN = 16
238
+
239
+ def initialize
240
+ super
241
+ @help_keys = RGame::Engine::Text.new('help.keys')
242
+ @help_join = RGame::Engine::Text.new('help.join')
243
+ end
244
+
245
+ def on_add
246
+ @players = root.system(RGame::Engine::Players)
247
+ # Every camera, including the empty seat's: a camera that does not know how
248
+ # big the world is will happily show the void past its edge, and the seat is
249
+ # filled later by somebody who should not have to remember this.
250
+ @players.each { |player| bound(player.camera) }
251
+
252
+ # World space begins here. Everything under it is drawn once per viewport,
253
+ # through that viewport's camera.
254
+ @view = add_node(RGame::Engine::WorldView.new)
255
+ @view.add_node(Ground.new)
256
+
257
+ # One walker and one badge per player who is already playing, and one more
258
+ # when somebody joins. The scene never asks whether anybody has — the
259
+ # registry tells it.
260
+ @players.each_active { |player| spawn(player) }
261
+ @players.on_joined { |player| spawn(player) }
262
+ end
263
+
264
+ # A plain child of the scene, so this is the global overlay: once across the
265
+ # whole window, wherever the split is.
266
+ def on_draw(renderer, view)
267
+ renderer.text(@help_keys, MARGIN, view.height - 52)
268
+ renderer.text(@help_join, MARGIN, view.height - 30)
269
+ end
270
+
271
+ private
272
+
273
+ def bound(camera)
274
+ camera.world_width = WORLD_W
275
+ camera.world_height = WORLD_H
276
+ end
277
+
278
+ def spawn(player)
279
+ x, y = STARTS[player.id]
280
+ walker = Walker.new(x: x, y: y, tint: TINTS[player.id], camera: player.camera)
281
+ # The only line in this file about who owns what. It is inherited by the
282
+ # whole subtree, which is why nothing under it needs telling.
283
+ walker.input_owner = player
284
+ @view.add_node(walker)
285
+
286
+ layer = add_node(RGame::Engine::PlayerLayer.new(player: player))
287
+ layer.add_node(Badge.new(number: player.id + 1, tint: TINTS[player.id],
288
+ x: MARGIN, y: MARGIN))
289
+ end
290
+ end
291
+
292
+ game = RGame::Game.new(
293
+ root: Scene.new,
294
+ caption: 'Split screen',
295
+ width: WIDTH,
296
+ height: HEIGHT,
297
+ media_root: ASSETS,
298
+ locales: LOCALES,
299
+ # Two seats. The second stays empty until somebody picks up a controller and
300
+ # presses confirm, and until then this is an ordinary one-player game.
301
+ players: 2
302
+ )
303
+
304
+ game.start
@@ -0,0 +1,8 @@
1
+ # The English table: every string this example draws.
2
+ en:
3
+ help:
4
+ keys: Left / right turn it — up / down resize it
5
+ captions:
6
+ whole: "hero.png whole: a String id is a path"
7
+ frame: one registered frame — the node holds the angle and the scale
8
+ above: the sprite asks for a z above the panel its own node draws
@@ -0,0 +1,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Sprite — one frame, drawn at a node, with no animation behind it.
4
+ #
5
+ # Run it:
6
+ #
7
+ # ruby examples/sprite/main.rb
8
+ #
9
+ # Left and right turn the middle sprite; up and down resize it. It exercises:
10
+ # - Components::Sprite — a picture drawn at the node it is attached to;
11
+ # - Image#subimage — one cell of a sheet, as a view costing no second decode;
12
+ # - renderer.register_image — binding a Symbol to an image the game chose;
13
+ # - the two id spaces, a String path and a registered Symbol, in one file.
14
+ #
15
+ # ## Most things in a game are this
16
+ #
17
+ # `examples/walk` uses Components::AnimatedSprite, which carries an AnimationSet,
18
+ # a current animation name and an elapsed time. Take all of that away and what is
19
+ # left is this: a picture, at a node. A crate, a pickup, a rock and a tree want
20
+ # exactly that and nothing more.
21
+ #
22
+ # ## It passes no position and no angle, and that is the whole trick
23
+ #
24
+ # The component's draw is one line, and both coordinates in it are zero:
25
+ #
26
+ # renderer.image(@id, 0, 0, scale: @scale, z: @layer)
27
+ #
28
+ # `Node2D#draw` has already pushed this node's transform onto the renderer, so
29
+ # drawing at the origin *is* drawing at the node, turned by however much the node
30
+ # is turned. Hold left and watch: nothing inside Components::Sprite knows that an
31
+ # angle exists, and the sprite turns anyway.
32
+ #
33
+ # Writing `node.x` there would apply the position a second time, which is what
34
+ # `Game/DrawInLocalSpace` refuses — `world_x` included, and for the same reason.
35
+ #
36
+ # ## Two numbers called z, and the bottom row separates them
37
+ #
38
+ # A **node's** `z` orders it against its siblings: which of two nodes is drawn
39
+ # first. A **component's** `z:` is an offset inside that one node's own slot, so
40
+ # it can only ever settle the order of what a single node draws — against its own
41
+ # other components, and against its own `on_draw`. It cannot reach the next node.
42
+ #
43
+ # The bottom node draws a panel from `on_draw` and hangs a sprite on itself, and
44
+ # they land in one slot together. The default offsets are not equal: a shape is
45
+ # drawn at 50 and an image at 0, so the panel covers the sprite unless the sprite
46
+ # asks for more. `z: ABOVE_PANEL` is that ask, and the node did not move.
47
+ #
48
+ # ## What this does not show
49
+ #
50
+ # Culling, though Components::Sprite does it. It skips a draw whose footprint is
51
+ # outside the view — but this scene is screen space with everything well inside
52
+ # the window, so the check answers "draw" every frame. `examples/scroll_map` has
53
+ # the camera that makes the other answer visible.
54
+
55
+ $LOAD_PATH.unshift File.expand_path('../../lib', __dir__)
56
+ require 'rgame/game'
57
+
58
+ WIDTH = 640
59
+ HEIGHT = 480
60
+ ASSETS = File.expand_path('../assets', __dir__)
61
+ LOCALES = File.expand_path('locales', __dir__) # the text on screen: locales/en.yml
62
+
63
+ SHEET = 'hero.png'
64
+ # The grid examples/assets/hero.json describes, which is what makes cell (0, 0)
65
+ # the standing frame. Written out rather than parsed: this example is about the
66
+ # image, and reading the descriptor would be borrowing AnimatedSprite's job.
67
+ FRAME_W = 16
68
+ FRAME_H = 22
69
+
70
+ STILL = :hero_still # the Symbol the frame is registered under, below
71
+
72
+ TURN_SPEED = 120.0 # degrees per second
73
+ GROW_SPEED = 3.0 # scale steps per second
74
+ MIN_SCALE = 1.0
75
+ MAX_SCALE = 9.0
76
+ START_SCALE = 4.0
77
+
78
+ # The one you can play with. It holds an angle and a scale, and hands neither to
79
+ # the renderer — the component draws at the origin and the transform does the
80
+ # rest.
81
+ class Turntable < RGame::Engine::Node2D
82
+ def initialize(**)
83
+ super(width: FRAME_W, height: FRAME_H, **)
84
+ @turn = 0.0
85
+ @resize = 0.0
86
+ end
87
+
88
+ def on_add
89
+ @sprite = add_component(RGame::Engine::Components::Sprite.new(id: STILL, scale: START_SCALE))
90
+ end
91
+
92
+ # Axes, not buttons: `move_x` and `move_y` come from the default map already
93
+ # bound to the arrows, WASD and a left stick, so this example declares no input
94
+ # of its own. Screen y grows downward, so up reads negative and is subtracted.
95
+ def on_control(actions)
96
+ @turn = actions.axis(:move_x)
97
+ @resize = actions.axis(:move_y)
98
+ end
99
+
100
+ def on_update(dt)
101
+ self.angle += @turn * TURN_SPEED * dt
102
+ @sprite.scale = (@sprite.scale - (@resize * GROW_SPEED * dt)).clamp(MIN_SCALE, MAX_SCALE)
103
+ end
104
+ end
105
+
106
+ # The bottom row: one node, two things drawn, and a z that decides which wins.
107
+ class Layered < RGame::Engine::Node2D
108
+ PANEL = RGame::Util::Color.new(196, 92, 64)
109
+ PANEL_W = 132
110
+ PANEL_H = 78
111
+ SCALE = 3.0
112
+ # Above the 50 a shape is drawn at, inside this node's slot. Drop it to 0 and
113
+ # the sprite disappears behind the panel; nothing else about the frame changes.
114
+ ABOVE_PANEL = 60
115
+
116
+ def initialize(**)
117
+ super(width: FRAME_W, height: FRAME_H, **)
118
+ end
119
+
120
+ def on_add
121
+ add_component(RGame::Engine::Components::Sprite.new(id: STILL, scale: SCALE, z: ABOVE_PANEL))
122
+ end
123
+
124
+ # Centred on this node's own origin, which is where the traversal has already
125
+ # put the renderer — the same reason the component draws at (0, 0).
126
+ def on_draw(renderer, _view)
127
+ renderer.rect(-PANEL_W / 2, -PANEL_H / 2, PANEL_W, PANEL_H, color: PANEL)
128
+ end
129
+ end
130
+
131
+ class Scene < RGame::Engine::Node2D
132
+ BACKDROP = RGame::Util::Color.new(38, 42, 52)
133
+ SHEET_X = 24
134
+ SHEET_Y = 60
135
+
136
+ def initialize
137
+ super
138
+ @help = RGame::Engine::Text.new('help.keys')
139
+ @whole = RGame::Engine::Text.new('captions.whole')
140
+ @frame = RGame::Engine::Text.new('captions.frame')
141
+ @above = RGame::Engine::Text.new('captions.above')
142
+ end
143
+
144
+ def on_add
145
+ add_node(Turntable.new(x: WIDTH / 2, y: 230))
146
+ add_node(Layered.new(x: WIDTH / 2, y: 400))
147
+ end
148
+
149
+ def on_draw(renderer, view)
150
+ renderer.rect(0, 0, view.width, view.height, color: BACKDROP)
151
+
152
+ # A String is a path, resolved through the asset manager on first use and
153
+ # remembered. Drawn at its natural size, so this is the file itself — six
154
+ # columns of walk frames over three rows, and the frame below is the first
155
+ # cell of it.
156
+ renderer.image_at(SHEET, SHEET_X, SHEET_Y)
157
+
158
+ renderer.text(@help, 12, 12)
159
+ renderer.text(@whole, SHEET_X, SHEET_Y - 18)
160
+ renderer.text(@frame, SHEET_X, 170)
161
+ renderer.text(@above, SHEET_X, 330)
162
+ end
163
+ end
164
+
165
+ game = RGame::Game.new(
166
+ root: Scene.new,
167
+ caption: 'Sprite',
168
+ width: WIDTH,
169
+ height: HEIGHT,
170
+ media_root: ASSETS,
171
+ locales: LOCALES
172
+ )
173
+
174
+ # The other id space. A Symbol is a name this game chose, so nothing can resolve
175
+ # it and it has to be bound — and what it is bound to here is not a file at all
176
+ # but a *view* into one. `subimage` shares the uploaded texture rather than
177
+ # decoding the PNG again, so slicing a sheet into frames costs nothing.
178
+ game.renderer.register_image(STILL, game.assets.image(SHEET).subimage(0, 0, FRAME_W, FRAME_H))
179
+
180
+ game.start
@@ -0,0 +1,12 @@
1
+ # The English table: every string this example draws.
2
+ en:
3
+ help:
4
+ beat: Both bars beat every 0.07s. One of them is right
5
+ captions:
6
+ timer: Components::Timer — the remainder carries forward
7
+ naive: "@elapsed = 0.0 — the overshoot is thrown away"
8
+ mark: the white mark is where this bar was when the other finished
9
+ blink: The square blinks on a second, slower timer on the same node
10
+ fuse: Space sets off a one-shot
11
+ fuse:
12
+ banner: one-shot — this removes itself