rgame 0.2.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +135 -1
- data/README.md +67 -65
- data/docs/api/README.md +86 -82
- data/docs/api/app.md +125 -68
- data/docs/api/assets.md +203 -165
- data/docs/api/audio.md +130 -89
- data/docs/api/cli.md +259 -0
- data/docs/api/components.md +1032 -265
- data/docs/api/drawing.md +160 -160
- data/docs/api/examples.md +263 -0
- data/docs/api/game.md +149 -50
- data/docs/api/images.md +39 -39
- data/docs/api/input.md +226 -148
- data/docs/api/internals.md +241 -62
- data/docs/api/localization.md +285 -0
- data/docs/api/scene_graph.md +397 -244
- data/docs/api/signals.md +81 -76
- data/docs/api/systems.md +186 -73
- data/docs/api/text.md +58 -41
- data/docs/api/tile_maps.md +237 -0
- data/docs/api/toolbox.md +395 -146
- data/docs/api/ui.md +883 -54
- data/docs/api/values.md +216 -33
- data/examples/assets/README.md +322 -0
- data/examples/assets/blip.ogg +0 -0
- data/examples/assets/glyphs.json +5 -0
- data/examples/assets/glyphs.png +0 -0
- data/examples/assets/hero.json +12 -0
- data/examples/assets/hero.png +0 -0
- data/examples/assets/icons.json +13 -0
- data/examples/assets/icons.png +0 -0
- data/examples/assets/music.ogg +0 -0
- data/examples/assets/skills.json +10 -0
- data/examples/assets/skills.png +0 -0
- data/examples/assets/tileset.png +0 -0
- data/examples/assets/tileset.tsx +65 -0
- data/examples/assets/town.tmx +26 -0
- data/examples/assets/ui.json +11 -0
- data/examples/assets/ui.png +0 -0
- data/examples/collision/locales/en.yml +8 -0
- data/examples/collision/main.rb +316 -0
- data/examples/collision_tiles/locales/en.yml +9 -0
- data/examples/collision_tiles/main.rb +274 -0
- data/examples/fullscreen/locales/en.yml +10 -0
- data/examples/fullscreen/main.rb +216 -0
- data/examples/game_menu/locales/en.yml +8 -0
- data/examples/game_menu/main.rb +170 -0
- data/examples/input_glyphs/locales/en.yml +14 -0
- data/examples/input_glyphs/main.rb +213 -0
- data/examples/jump_topdown/locales/en.yml +9 -0
- data/examples/jump_topdown/main.rb +178 -0
- data/examples/localization/locales/de.yml +12 -0
- data/examples/localization/locales/en.yml +13 -0
- data/examples/localization/main.rb +158 -0
- data/examples/menu_navigation/locales/en.yml +23 -0
- data/examples/menu_navigation/main.rb +365 -0
- data/examples/music/locales/en.yml +7 -0
- data/examples/music/main.rb +134 -0
- data/examples/pathfinding/locales/en.yml +17 -0
- data/examples/pathfinding/main.rb +298 -0
- data/examples/pooling/locales/en.yml +7 -0
- data/examples/pooling/main.rb +259 -0
- data/examples/quick_wheel/locales/en.yml +16 -0
- data/examples/quick_wheel/main.rb +184 -0
- data/examples/radial_menu/locales/en.yml +16 -0
- data/examples/radial_menu/main.rb +184 -0
- data/examples/save_load/locales/en.yml +11 -0
- data/examples/save_load/main.rb +207 -0
- data/examples/save_load_ids/locales/en.yml +11 -0
- data/examples/save_load_ids/main.rb +322 -0
- data/examples/scroll_map/locales/en.yml +4 -0
- data/examples/scroll_map/main.rb +140 -0
- data/examples/signals/locales/en.yml +6 -0
- data/examples/signals/main.rb +278 -0
- data/examples/skill_bar/locales/en.yml +14 -0
- data/examples/skill_bar/main.rb +159 -0
- data/examples/sound/locales/en.yml +6 -0
- data/examples/sound/main.rb +122 -0
- data/examples/split_screen/locales/en.yml +9 -0
- data/examples/split_screen/main.rb +304 -0
- data/examples/sprite/locales/en.yml +8 -0
- data/examples/sprite/main.rb +180 -0
- data/examples/timer/locales/en.yml +12 -0
- data/examples/timer/main.rb +273 -0
- data/examples/velocity/locales/en.yml +6 -0
- data/examples/velocity/main.rb +196 -0
- data/examples/walk/locales/en.yml +4 -0
- data/examples/walk/main.rb +99 -0
- data/exe/rgame +9 -0
- data/ext/rgame_core/app/app.c +33 -3
- data/ext/rgame_core/app/locale.c +67 -0
- data/ext/rgame_core/app/locale.h +28 -0
- data/ext/rgame_core/audio/audio.c +39 -2
- data/ext/rgame_core/example.rb +0 -49
- data/ext/rgame_core/extconf.rb +0 -125
- data/ext/rgame_core/include/rgame/core.h +38 -2
- data/ext/rgame_core/ruby/audio_ext.c +10 -5
- data/ext/rgame_core/ruby/core_ext.c +30 -7
- data/ext/rgame_core/ruby/core_ext.h +3 -0
- data/ext/rgame_core/ruby/locale_ext.c +44 -0
- data/ext/rgame_core/ruby/recording_ext.c +1 -1
- data/ext/rgame_core/ruby/renderer_ext.c +20 -20
- data/ext/rgame_util/extconf.rb +2 -20
- data/ext/rgame_util/route_search.c +305 -0
- data/ext/rgame_util/route_search.h +86 -0
- data/ext/rgame_util/route_search_ext.c +150 -0
- data/ext/rgame_util/solid_grid.c +58 -0
- data/ext/rgame_util/solid_grid.h +49 -0
- data/ext/rgame_util/solid_grid_ext.c +161 -0
- data/ext/rgame_util/tile_sweep.c +164 -0
- data/ext/rgame_util/tile_sweep.h +62 -0
- data/ext/rgame_util/tile_sweep_ext.c +155 -0
- data/ext/rgame_util/util_ext.c +3 -0
- data/ext/rgame_util/util_ext.h +15 -0
- data/lib/rgame/boot.rb +0 -10
- data/lib/rgame/cli/new_project.rb +139 -0
- data/lib/rgame/cli/templates/Gemfile.tt +23 -0
- data/lib/rgame/cli/templates/README.md.tt +93 -0
- data/lib/rgame/cli/templates/Rakefile.tt +9 -0
- data/lib/rgame/cli/templates/assets/locales/en.yml.tt +10 -0
- data/lib/rgame/cli/templates/game.rb.tt +23 -0
- data/lib/rgame/cli/templates/gitignore.tt +12 -0
- data/lib/rgame/cli/templates/main.rb.tt +11 -0
- data/lib/rgame/cli/templates/nodes/root.rb.tt +24 -0
- data/lib/rgame/cli/templates/rspec.tt +2 -0
- data/lib/rgame/cli/templates/rubocop.yml.tt +75 -0
- data/lib/rgame/cli/templates/ruby-version.tt +1 -0
- data/lib/rgame/cli/templates/spec/locales_spec.rb.tt +18 -0
- data/lib/rgame/cli/templates/spec/nodes/root_spec.rb.tt +18 -0
- data/lib/rgame/cli/templates/spec/spec_helper.rb.tt +37 -0
- data/lib/rgame/cli.rb +66 -0
- data/lib/rgame/core/app.rb +6 -44
- data/lib/rgame/core/asset_manager.rb +13 -31
- data/lib/rgame/core/audio.rb +37 -16
- data/lib/rgame/core/font.rb +0 -3
- data/lib/rgame/core/locale.rb +22 -0
- data/lib/rgame/core/nine_slice.rb +0 -21
- data/lib/rgame/core/renderer.rb +6 -63
- data/lib/rgame/core/sprite_sheet.rb +0 -3
- data/lib/rgame/core/tile_map_renderer.rb +2 -19
- data/lib/rgame/core/ui_atlas.rb +28 -13
- data/lib/rgame/core.rb +1 -8
- data/lib/rgame/engine/actor_blockers.rb +131 -0
- data/lib/rgame/engine/animation_set.rb +1 -0
- data/lib/rgame/engine/audio_director.rb +36 -6
- data/lib/rgame/engine/bounds_blockers.rb +74 -0
- data/lib/rgame/engine/camera.rb +3 -3
- data/lib/rgame/engine/circle_collider.rb +4 -2
- data/lib/rgame/engine/collision_box.rb +26 -1
- data/lib/rgame/engine/collision_system.rb +110 -22
- data/lib/rgame/engine/component.rb +34 -10
- data/lib/rgame/engine/components/action_trigger.rb +0 -1
- data/lib/rgame/engine/components/animated_sprite.rb +27 -25
- data/lib/rgame/engine/components/box_collider.rb +99 -0
- data/lib/rgame/engine/components/camera_follow.rb +6 -5
- data/lib/rgame/engine/components/character_body.rb +20 -61
- data/lib/rgame/engine/components/circle_collider.rb +47 -11
- data/lib/rgame/engine/components/collision_world.rb +159 -31
- data/lib/rgame/engine/components/despawn_offscreen.rb +24 -8
- data/lib/rgame/engine/components/feet_collider.rb +61 -0
- data/lib/rgame/engine/components/hop.rb +76 -0
- data/lib/rgame/engine/components/identity.rb +73 -0
- data/lib/rgame/engine/components/mover.rb +285 -0
- data/lib/rgame/engine/components/navigator.rb +145 -0
- data/lib/rgame/engine/components/path_follow.rb +123 -31
- data/lib/rgame/engine/components/player_controller.rb +5 -2
- data/lib/rgame/engine/components/pool.rb +1 -1
- data/lib/rgame/engine/components/screen_wrap.rb +33 -11
- data/lib/rgame/engine/components/sprite.rb +12 -6
- data/lib/rgame/engine/components/targeting.rb +9 -11
- data/lib/rgame/engine/components/thrust_controller.rb +1 -1
- data/lib/rgame/engine/components/tile_world.rb +41 -13
- data/lib/rgame/engine/components/timer.rb +1 -1
- data/lib/rgame/engine/components/velocity.rb +23 -7
- data/lib/rgame/engine/components/wander_controller.rb +6 -2
- data/lib/rgame/engine/components/world.rb +133 -0
- data/lib/rgame/engine/contact_set.rb +74 -0
- data/lib/rgame/engine/culling.rb +4 -6
- data/lib/rgame/engine/debug_overlay.rb +5 -11
- data/lib/rgame/engine/i18n/plural.rb +45 -0
- data/lib/rgame/engine/i18n/plural_rules.rb +82 -0
- data/lib/rgame/engine/i18n/template.rb +59 -0
- data/lib/rgame/engine/i18n.rb +276 -51
- data/lib/rgame/engine/input/action_mapper.rb +0 -26
- data/lib/rgame/engine/input/actions.rb +2 -8
- data/lib/rgame/engine/input/input_map.rb +39 -21
- data/lib/rgame/engine/nav_grid.rb +87 -0
- data/lib/rgame/engine/node2d.rb +248 -133
- data/lib/rgame/engine/path.rb +4 -6
- data/lib/rgame/engine/players.rb +6 -13
- data/lib/rgame/engine/presentation.rb +171 -0
- data/lib/rgame/engine/scene/scene_stack.rb +4 -4
- data/lib/rgame/engine/sealed_privates.rb +54 -0
- data/lib/rgame/engine/spatial_hash.rb +51 -19
- data/lib/rgame/engine/text.rb +194 -0
- data/lib/rgame/engine/tile_blockers.rb +63 -0
- data/lib/rgame/engine/tile_map.rb +2 -3
- data/lib/rgame/engine/tile_map_layer.rb +0 -2
- data/lib/rgame/engine/tileset.rb +2 -4
- data/lib/rgame/engine/timer.rb +2 -2
- data/lib/rgame/engine/ui/button.rb +248 -0
- data/lib/rgame/engine/ui/column.rb +20 -0
- data/lib/rgame/engine/ui/icon_button.rb +93 -0
- data/lib/rgame/engine/ui/menu.rb +246 -71
- data/lib/rgame/engine/ui/navigation.rb +57 -0
- data/lib/rgame/engine/ui/nine_slice_style.rb +50 -0
- data/lib/rgame/engine/ui/option_button.rb +163 -0
- data/lib/rgame/engine/ui/panel_button.rb +32 -0
- data/lib/rgame/engine/ui/panel_menu.rb +36 -0
- data/lib/rgame/engine/ui/pointing.rb +146 -0
- data/lib/rgame/engine/ui/radial_menu.rb +85 -0
- data/lib/rgame/engine/ui/ring.rb +55 -0
- data/lib/rgame/engine/ui/row.rb +21 -0
- data/lib/rgame/engine/ui/shape_style.rb +102 -0
- data/lib/rgame/engine/ui/stack.rb +58 -0
- data/lib/rgame/engine/ui/stepping.rb +93 -0
- data/lib/rgame/engine/ui/text_button.rb +59 -0
- data/lib/rgame/engine/viewports.rb +2 -5
- data/lib/rgame/engine/world_view.rb +5 -4
- data/lib/rgame/engine.rb +31 -25
- data/lib/rgame/game.rb +99 -27
- data/lib/rgame/rubocop/cop/game/draw_in_local_space.rb +103 -0
- data/lib/rgame/rubocop/cop/game/hot_path.rb +36 -0
- data/lib/rgame/rubocop/cop/game/layer_boundary.rb +43 -0
- data/lib/rgame/rubocop/cop/game/no_core_in_engine_layer.rb +100 -0
- data/lib/rgame/rubocop/cop/game/no_engine_in_core_layer.rb +84 -0
- data/lib/rgame/rubocop/cop/game/no_interpolation_in_hot_path.rb +50 -0
- data/lib/rgame/rubocop/cop/game/no_literal_text.rb +41 -0
- data/lib/rgame/rubocop/cop/game/no_needless_allocation.rb +112 -0
- data/lib/rgame/rubocop/default.yml +39 -0
- data/lib/rgame/rubocop/plugin.rb +45 -0
- data/lib/rgame/rubocop.rb +11 -0
- data/lib/rgame/util/color.rb +20 -24
- data/lib/rgame/util/controls.rb +14 -28
- data/lib/rgame/util/route_search.rb +27 -0
- data/lib/rgame/util/save_file.rb +107 -0
- data/lib/rgame/util/solid_grid.rb +37 -0
- data/lib/rgame/util/tensor.rb +0 -9
- data/lib/rgame/util/tile_sweep.rb +36 -0
- data/lib/rgame/util/z.rb +4 -14
- data/lib/rgame/util.rb +4 -3
- data/lib/rgame/version.rb +1 -1
- data/lib/rgame.rb +0 -15
- metadata +152 -11
- data/lib/rgame/engine/actor.rb +0 -53
- data/lib/rgame/engine/body.rb +0 -49
- data/lib/rgame/engine/cached_label.rb +0 -33
- data/lib/rgame/engine/input/player_controller.rb +0 -14
- data/lib/rgame/engine/matrix.rb +0 -32
- data/lib/rgame/engine/resettable.rb +0 -67
- data/lib/rgame/engine/tile_collision.rb +0 -78
- data/lib/rgame/engine/ui/menu_item.rb +0 -84
|
@@ -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
|