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,322 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Save and load, part two — when the save has to name things.
|
|
4
|
+
#
|
|
5
|
+
# Run it:
|
|
6
|
+
#
|
|
7
|
+
# ruby examples/save_load_ids/main.rb
|
|
8
|
+
#
|
|
9
|
+
# Arrows walk the dog. **Tab** points it at the next sheep, **Space** shears the
|
|
10
|
+
# one it is pointing at, **N** brings in a new one. **F5** saves, **F9** loads,
|
|
11
|
+
# **Delete** discards. Every sheep is labelled with its id, and the line from the
|
|
12
|
+
# dog shows which one it is watching. It exercises:
|
|
13
|
+
# - Components::Identity — a stable name for a node;
|
|
14
|
+
# - Util::SaveFile — the same one `examples/save_load` uses;
|
|
15
|
+
# - a save that holds records rather than a bare list of positions.
|
|
16
|
+
#
|
|
17
|
+
# ## Read `examples/save_load` first
|
|
18
|
+
#
|
|
19
|
+
# That one restores a dog by the variable holding it and a flock by array order,
|
|
20
|
+
# and both are correct for what they are. Nothing there needs an id, and adding
|
|
21
|
+
# one would have been ceremony.
|
|
22
|
+
#
|
|
23
|
+
# This is the case it does not cover.
|
|
24
|
+
#
|
|
25
|
+
# ## A collection is not what forces ids — a reference is
|
|
26
|
+
#
|
|
27
|
+
# It is tempting to say "the flock changes, so it needs ids", and that is not
|
|
28
|
+
# quite true. A collection can always be saved as *records* — each sheep writing
|
|
29
|
+
# its own position and wool — and rebuilt from them. Order stops mattering
|
|
30
|
+
# because nothing is being matched up: every sheep in the file becomes a sheep in
|
|
31
|
+
# the world, and that works whether three died or thirty arrived.
|
|
32
|
+
#
|
|
33
|
+
# What cannot be written that way is **the line from the dog to one particular
|
|
34
|
+
# sheep**. A node reference is a pointer into this run of the program; the next
|
|
35
|
+
# run has different objects at different addresses. The only thing that survives
|
|
36
|
+
# the gap is a name both runs agree on, and that is what `Identity` is:
|
|
37
|
+
#
|
|
38
|
+
# target: Identity.of(@target) # an id, or nil
|
|
39
|
+
#
|
|
40
|
+
# On load the flock is rebuilt from records, and then the dog is re-linked by
|
|
41
|
+
# looking for that id among the new sheep. That is the shape `Identity.of` is
|
|
42
|
+
# built for: it takes a node and answers an id, because what a game holds is
|
|
43
|
+
# always the node and what a file can hold is only ever the name.
|
|
44
|
+
#
|
|
45
|
+
# ## Why this is not `Components::Targeting`
|
|
46
|
+
#
|
|
47
|
+
# The engine already has a component that holds a target node, and it is the
|
|
48
|
+
# wrong tool here — which is worth saying, because "there is a component for
|
|
49
|
+
# that" is the first thing a reader will think.
|
|
50
|
+
#
|
|
51
|
+
# `Targeting` *derives* its target: every update it asks the scene's
|
|
52
|
+
# `CollisionWorld` for the nearest collider in range. So it holds a node without
|
|
53
|
+
# ever needing to write one down — a save that omitted it entirely would arrive
|
|
54
|
+
# at the same answer on the next frame. The dog here is the opposite: **Tab**
|
|
55
|
+
# chose this sheep, nothing can recompute that choice, and losing it is the thing
|
|
56
|
+
# a player would notice.
|
|
57
|
+
#
|
|
58
|
+
# Targeting proximity picks the target, every frame — derived
|
|
59
|
+
# this example the player picks it, and it has to survive quitting — chosen
|
|
60
|
+
#
|
|
61
|
+
# **Derived state is never saved; chosen state always is.** Sorting a game's
|
|
62
|
+
# facts into those two piles is most of designing a save file, and the question
|
|
63
|
+
# is cheaper than it looks: could the next frame work this out again? The flock's
|
|
64
|
+
# positions are chosen (the sheep wandered there), the wool is chosen (it grew),
|
|
65
|
+
# the tether is derived from the target — which is why the save holds a target
|
|
66
|
+
# and not a line.
|
|
67
|
+
#
|
|
68
|
+
# ## The allocator is part of the save, and forgetting it is the classic bug
|
|
69
|
+
#
|
|
70
|
+
# `@next_id` is saved and restored with everything else. It has to be: a counter
|
|
71
|
+
# that restarts at 1 on load hands out ids that the restored sheep are already
|
|
72
|
+
# using, and nothing complains at the time. What you see is a dog that follows
|
|
73
|
+
# the wrong animal, hours later, once a duplicate finally gets picked as a
|
|
74
|
+
# target. One number, and the whole scheme rests on it.
|
|
75
|
+
#
|
|
76
|
+
# Try it: save with a few sheep, quit, run again, press **N**. The new sheep gets
|
|
77
|
+
# the next number rather than a duplicate.
|
|
78
|
+
#
|
|
79
|
+
# ## An id that is no longer there
|
|
80
|
+
#
|
|
81
|
+
# `restore_target` falls back to any sheep when the saved id matches none of
|
|
82
|
+
# them. That is not defensive habit — a save file is the one input a game did not
|
|
83
|
+
# produce this run, and it can be hand-edited, copied from another machine, or
|
|
84
|
+
# written by an older version of the game with a different flock. Pointing at
|
|
85
|
+
# nothing is a legitimate state to arrive in, and picking a new target is a
|
|
86
|
+
# better answer than refusing to load.
|
|
87
|
+
|
|
88
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', __dir__)
|
|
89
|
+
require 'rgame/game'
|
|
90
|
+
|
|
91
|
+
Controls = RGame::Util::Controls
|
|
92
|
+
Identity = RGame::Engine::Components::Identity
|
|
93
|
+
|
|
94
|
+
WIDTH = 640
|
|
95
|
+
HEIGHT = 480
|
|
96
|
+
ASSETS = File.expand_path('../assets', __dir__)
|
|
97
|
+
LOCALES = File.expand_path('locales', __dir__) # the text on screen: locales/en.yml
|
|
98
|
+
|
|
99
|
+
DOG_SPEED = 150.0
|
|
100
|
+
SHEEP_SPEED = 35.0
|
|
101
|
+
STARTING_FLOCK = 5
|
|
102
|
+
DEFAULT_SEED = 0x5EED2
|
|
103
|
+
|
|
104
|
+
# One sheep: a position, an id, and wool that grows while it stands there.
|
|
105
|
+
#
|
|
106
|
+
# The wool is what makes this a *record* rather than a coordinate pair. A save
|
|
107
|
+
# that wrote only positions would restore a flock of freshly shorn sheep, and
|
|
108
|
+
# nobody would notice until they wondered why shearing never seemed to pay.
|
|
109
|
+
class Sheep < RGame::Engine::Node2D
|
|
110
|
+
FLEECE = RGame::Util::Color.new(240, 240, 235)
|
|
111
|
+
INK = RGame::Util::Color.new(30, 40, 30)
|
|
112
|
+
MIN_R = 7.0
|
|
113
|
+
MAX_WOOL = 9.0
|
|
114
|
+
GROWTH = 1.4 # radius per second
|
|
115
|
+
|
|
116
|
+
attr_reader :id
|
|
117
|
+
attr_accessor :wool
|
|
118
|
+
|
|
119
|
+
def initialize(id:, wool: 0.0, **)
|
|
120
|
+
super(**)
|
|
121
|
+
@id = id
|
|
122
|
+
@wool = wool
|
|
123
|
+
# Built once, on the way in. `renderer.text(@id.to_s, …)` would allocate a
|
|
124
|
+
# String every frame for every sheep, which is what
|
|
125
|
+
# Game/NoInterpolationInHotPath is about even though `to_s` is not
|
|
126
|
+
# interpolation.
|
|
127
|
+
@label = id.to_s
|
|
128
|
+
add_component(Identity.new(id: id))
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def on_update(dt)
|
|
132
|
+
@wool = [@wool + (dt * GROWTH), MAX_WOOL].min
|
|
133
|
+
self.x = x.clamp(0, WIDTH)
|
|
134
|
+
self.y = y.clamp(40, HEIGHT)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def on_draw(renderer, _view)
|
|
138
|
+
renderer.circle(0, 0, MIN_R + @wool, color: FLEECE)
|
|
139
|
+
renderer.text(@label, MIN_R + MAX_WOOL + 4, -8, color: INK)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
class Pasture < RGame::Engine::Node2D
|
|
144
|
+
GRASS = RGame::Util::Color.new(96, 140, 84)
|
|
145
|
+
DOG = RGame::Util::Color.new(70, 50, 40)
|
|
146
|
+
TETHER = RGame::Util::Color.new(250, 240, 180)
|
|
147
|
+
DOG_R = 11
|
|
148
|
+
|
|
149
|
+
STATUS = { fresh: RGame::Engine::Text.new('status.fresh'),
|
|
150
|
+
restored: RGame::Engine::Text.new('status.restored'),
|
|
151
|
+
saved: RGame::Engine::Text.new('status.saved'),
|
|
152
|
+
loaded: RGame::Engine::Text.new('status.loaded'),
|
|
153
|
+
deleted: RGame::Engine::Text.new('status.deleted') }.freeze
|
|
154
|
+
|
|
155
|
+
def initialize(save:, **)
|
|
156
|
+
super(**)
|
|
157
|
+
@save = save
|
|
158
|
+
@help = RGame::Engine::Text.new('help.keys')
|
|
159
|
+
@save_keys = RGame::Engine::Text.new('help.save')
|
|
160
|
+
@rng = Random.new(ENV.fetch('RGAME_SEED', DEFAULT_SEED).to_i)
|
|
161
|
+
@flock = []
|
|
162
|
+
@next_id = 1
|
|
163
|
+
@status = :fresh
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def on_add
|
|
167
|
+
@dog = add_node(build_dog)
|
|
168
|
+
STARTING_FLOCK.times { spawn_sheep }
|
|
169
|
+
@target = @flock.first
|
|
170
|
+
|
|
171
|
+
return unless @save.exist?
|
|
172
|
+
|
|
173
|
+
load_state
|
|
174
|
+
@status = :restored
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def on_control(actions)
|
|
178
|
+
target_next if actions.pressed?(:target_next)
|
|
179
|
+
shear if actions.pressed?(:shear)
|
|
180
|
+
spawn_sheep if actions.pressed?(:spawn)
|
|
181
|
+
|
|
182
|
+
save_state if actions.pressed?(:save)
|
|
183
|
+
load_state if actions.pressed?(:load)
|
|
184
|
+
drop_save if actions.pressed?(:drop)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def on_draw(renderer, view)
|
|
188
|
+
renderer.rect(0, 0, view.width, view.height, color: GRASS)
|
|
189
|
+
# Between two nodes, so the scene draws it: it is the only thing that knows
|
|
190
|
+
# both. The sheep draw themselves, over this.
|
|
191
|
+
renderer.line(@dog.x, @dog.y, @target.x, @target.y, color: TETHER) if @target
|
|
192
|
+
renderer.circle(@dog.x, @dog.y, DOG_R, color: DOG)
|
|
193
|
+
|
|
194
|
+
renderer.text(@help, 12, 12)
|
|
195
|
+
renderer.text(@save_keys, 12, 34)
|
|
196
|
+
renderer.text(STATUS.fetch(@status), 12, 56)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
private
|
|
200
|
+
|
|
201
|
+
# --- the save ----------------------------------------------------------
|
|
202
|
+
#
|
|
203
|
+
# Records, not coordinates: each sheep writes everything about itself that the
|
|
204
|
+
# next run cannot work out for itself.
|
|
205
|
+
|
|
206
|
+
def save_state
|
|
207
|
+
@save.write(
|
|
208
|
+
next_id: @next_id,
|
|
209
|
+
dog: [@dog.x, @dog.y],
|
|
210
|
+
target: Identity.of(@target), # a node becomes an id, here and nowhere else
|
|
211
|
+
sheep: @flock.map { |sheep| { id: sheep.id, x: sheep.x, y: sheep.y, wool: sheep.wool } }
|
|
212
|
+
)
|
|
213
|
+
@status = :saved
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def load_state
|
|
217
|
+
state = @save.read
|
|
218
|
+
return if state.empty?
|
|
219
|
+
|
|
220
|
+
@flock.each(&:queue_free)
|
|
221
|
+
@flock = []
|
|
222
|
+
state.fetch(:sheep, []).each { |record| restore_sheep(record) }
|
|
223
|
+
# After the flock, because the target has to be found among the sheep that
|
|
224
|
+
# now exist rather than the ones that used to.
|
|
225
|
+
restore_target(state[:target])
|
|
226
|
+
@dog.x, @dog.y = state[:dog] if state[:dog]
|
|
227
|
+
# Restored, not recomputed. A counter that started again from the flock size
|
|
228
|
+
# would collide with any id belonging to a sheep that has since been sheared.
|
|
229
|
+
@next_id = state.fetch(:next_id, @flock.size + 1)
|
|
230
|
+
@status = :loaded
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def restore_sheep(record)
|
|
234
|
+
@flock << add_node(build_sheep(record.fetch(:id), record.fetch(:x), record.fetch(:y),
|
|
235
|
+
wool: record.fetch(:wool, 0.0)))
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# The re-link, and the whole reason ids are here. `find` rather than an index:
|
|
239
|
+
# the flock that comes back is a different set of objects in whatever order the
|
|
240
|
+
# file listed them.
|
|
241
|
+
def restore_target(id)
|
|
242
|
+
@target = @flock.find { |sheep| sheep.id == id } || @flock.first
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def drop_save
|
|
246
|
+
@save.delete
|
|
247
|
+
@status = :deleted
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# --- the game ----------------------------------------------------------
|
|
251
|
+
|
|
252
|
+
def target_next
|
|
253
|
+
return if @flock.empty?
|
|
254
|
+
|
|
255
|
+
index = @flock.index(@target) || -1
|
|
256
|
+
@target = @flock[(index + 1) % @flock.size]
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def shear
|
|
260
|
+
return if @target.nil?
|
|
261
|
+
|
|
262
|
+
@target.wool = 0.0
|
|
263
|
+
sheared = @target
|
|
264
|
+
@flock.delete(sheared)
|
|
265
|
+
sheared.queue_free
|
|
266
|
+
# The removed sheep leaves a hole in the middle of the flock, and every id
|
|
267
|
+
# after it keeps the number it had. That is the property an array index
|
|
268
|
+
# cannot give you.
|
|
269
|
+
@target = @flock.first
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def spawn_sheep
|
|
273
|
+
id = @next_id
|
|
274
|
+
@next_id += 1
|
|
275
|
+
@flock << add_node(build_sheep(id, @rng.rand(WIDTH - 80) + 40, @rng.rand(HEIGHT - 120) + 80))
|
|
276
|
+
@target = @flock.first if @target.nil?
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def build_sheep(id, x, y, wool: 0.0)
|
|
280
|
+
sheep = Sheep.new(id: id, wool: wool, x: x, y: y)
|
|
281
|
+
sheep.add_component(RGame::Engine::Components::CharacterBody.new(speed: SHEEP_SPEED))
|
|
282
|
+
sheep.add_component(RGame::Engine::Components::WanderController.new(rng: @rng))
|
|
283
|
+
sheep
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def build_dog
|
|
287
|
+
dog = RGame::Engine::Node2D.new(x: WIDTH / 2, y: HEIGHT / 2)
|
|
288
|
+
dog.add_component(RGame::Engine::Components::CharacterBody.new(speed: DOG_SPEED))
|
|
289
|
+
dog.add_component(RGame::Engine::Components::PlayerController.new)
|
|
290
|
+
dog
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
save = RGame::Util::SaveFile.new('flock.json', game: 'rgame-examples',
|
|
295
|
+
dir: ENV.fetch('RGAME_SAVE_DIR', nil))
|
|
296
|
+
|
|
297
|
+
game = RGame::Game.new(
|
|
298
|
+
root: Pasture.new(save: save),
|
|
299
|
+
caption: 'Save and load with ids',
|
|
300
|
+
width: WIDTH,
|
|
301
|
+
height: HEIGHT,
|
|
302
|
+
media_root: ASSETS,
|
|
303
|
+
locales: LOCALES,
|
|
304
|
+
# F5 and F9 rather than S and L: **a key already in the default map keeps
|
|
305
|
+
# doing its default job too.** `move_y` is bound to W and S, so a save action
|
|
306
|
+
# on S would save *and* walk the dog downwards — two actions may read one key,
|
|
307
|
+
# both fire, and nothing warns. F1 and F2 are not free either; RGame::Game
|
|
308
|
+
# keeps them for the debug overlay and quit.
|
|
309
|
+
#
|
|
310
|
+
# Space is a deliberate exception: the default map has it on `fire` and
|
|
311
|
+
# `ui_confirm`, and nothing here reads either, so sharing it costs nothing.
|
|
312
|
+
input_map: RGame::Engine::InputMap.default.merge(
|
|
313
|
+
target_next: { buttons: [Controls::KEY_TAB] },
|
|
314
|
+
shear: { buttons: [Controls::KEY_SPACE] },
|
|
315
|
+
spawn: { buttons: [Controls::KEY_N] },
|
|
316
|
+
save: { buttons: [Controls::KEY_F5] },
|
|
317
|
+
load: { buttons: [Controls::KEY_F9] },
|
|
318
|
+
drop: { buttons: [Controls::KEY_DELETE] }
|
|
319
|
+
)
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
game.start
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Scroll map — a Tiled map, larger than the window, that a player scrolls.
|
|
4
|
+
#
|
|
5
|
+
# Run it:
|
|
6
|
+
#
|
|
7
|
+
# ruby examples/scroll_map/main.rb
|
|
8
|
+
#
|
|
9
|
+
# Arrow keys, WASD, a d-pad or a left stick pan the view; the map stops at its
|
|
10
|
+
# own edges. It exercises:
|
|
11
|
+
# - the `:tilemap` asset loader — a .tmx becomes a parsed map plus a renderer;
|
|
12
|
+
# - Components::TileWorld — the scene-scoped system that owns the map and the
|
|
13
|
+
# world's size;
|
|
14
|
+
# - TileMapLayer.mount — one node per Tiled layer, drawn as world content;
|
|
15
|
+
# - WorldView — where world space begins, and where the camera is applied;
|
|
16
|
+
# - Camera — clamped to the world, so the view never shows past the edge;
|
|
17
|
+
# - Components::CameraFollow — what points a camera at something.
|
|
18
|
+
#
|
|
19
|
+
# ## Moving a camera means moving a node
|
|
20
|
+
#
|
|
21
|
+
# There is no "pan the camera" call, and the absence is the design. A camera is
|
|
22
|
+
# owned by a *player* — a scene may have any number of viewers, so it cannot own
|
|
23
|
+
# one — and where it points is decided by a CameraFollow component on some node
|
|
24
|
+
# in the world. Scrolling is therefore the same thing as walking: build a node,
|
|
25
|
+
# drive it with the usual controller, and hang a camera off it.
|
|
26
|
+
#
|
|
27
|
+
# That node is the Rig below. It is `examples/walk`'s hero with the sprite left
|
|
28
|
+
# off — same CharacterBody, same PlayerController — which is the point worth
|
|
29
|
+
# taking away: the camera is not a special case, and a game that follows its
|
|
30
|
+
# player is this file with a sprite added back.
|
|
31
|
+
|
|
32
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', __dir__)
|
|
33
|
+
require 'rgame/game'
|
|
34
|
+
|
|
35
|
+
WIDTH = 640
|
|
36
|
+
HEIGHT = 480
|
|
37
|
+
ASSETS = File.expand_path('../assets', __dir__)
|
|
38
|
+
LOCALES = File.expand_path('locales', __dir__) # the text on screen: locales/en.yml
|
|
39
|
+
|
|
40
|
+
MAP = 'town.tmx'
|
|
41
|
+
SPEED = 220.0 # px/s — a camera pans faster than a character walks
|
|
42
|
+
|
|
43
|
+
# The node the camera follows: no sprite, no collision, just a position that
|
|
44
|
+
# input moves and a camera that trails it.
|
|
45
|
+
#
|
|
46
|
+
# It draws a crosshair so the centre of the view is visible, which is the only
|
|
47
|
+
# reason it is a class rather than a bare Node2D with three components on it.
|
|
48
|
+
class Rig < RGame::Engine::Node2D
|
|
49
|
+
ARM = 7
|
|
50
|
+
# A Color rather than an array literal, and a constant rather than either: a
|
|
51
|
+
# drawing call coerces `nil`, `[r, g, b]` or a Color, and building the array
|
|
52
|
+
# inline would allocate one every frame. RGame::Util::Color is a value type —
|
|
53
|
+
# frozen, comparable, and safe to share.
|
|
54
|
+
TINT = RGame::Util::Color.new(250, 250, 255)
|
|
55
|
+
|
|
56
|
+
def initialize(world_width:, world_height:, **)
|
|
57
|
+
super(**)
|
|
58
|
+
@world_width = world_width
|
|
59
|
+
@world_height = world_height
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Keep the rig on the map. The camera already refuses to show past the world's
|
|
63
|
+
# edges, but the rig is not the camera: without this it walks off into
|
|
64
|
+
# nothing, the view stays pinned to the edge, and the player is left pushing a
|
|
65
|
+
# key that does nothing visible. Clamping both to the same bounds keeps the
|
|
66
|
+
# crosshair and the view agreeing.
|
|
67
|
+
def on_update(_dt)
|
|
68
|
+
self.x = x.clamp(0, @world_width)
|
|
69
|
+
self.y = y.clamp(0, @world_height)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# (0, 0) is this node: Node2D#draw has already pushed its transform, and the
|
|
73
|
+
# WorldView above has already applied the camera. Reading `x` or `world_x`
|
|
74
|
+
# here would apply one of those a second time.
|
|
75
|
+
def on_draw(renderer, _view)
|
|
76
|
+
renderer.line(-ARM, 0, ARM, 0, color: TINT)
|
|
77
|
+
renderer.line(0, -ARM, 0, ARM, color: TINT)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# The scene: mount the map, mount the world, put the rig in it.
|
|
82
|
+
class Scene < RGame::Engine::Node2D
|
|
83
|
+
def initialize
|
|
84
|
+
super
|
|
85
|
+
@help = RGame::Engine::Text.new('help.scroll')
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def on_add
|
|
89
|
+
# `.tilemap` is a loader RGame::Game installs, because building one needs
|
|
90
|
+
# both layers at once: Engine::TileMap reads the .tmx, and the renderer that
|
|
91
|
+
# draws it is Core. What comes back holds both; `.map` is the grid half.
|
|
92
|
+
map = root.context.assets.tilemap(MAP).map
|
|
93
|
+
players = root.system(RGame::Engine::Players)
|
|
94
|
+
|
|
95
|
+
# The system every actor asks about the world: how big it is, what is solid.
|
|
96
|
+
# Handing it the cameras is what bounds them — a camera left unbounded
|
|
97
|
+
# follows its target exactly and will happily show the void past the edge.
|
|
98
|
+
add_component(RGame::Engine::Components::TileWorld.new(
|
|
99
|
+
map: map, tilemap_id: MAP, cameras: players.map(&:camera)
|
|
100
|
+
))
|
|
101
|
+
|
|
102
|
+
# World space begins here. Everything under it is drawn in world
|
|
103
|
+
# coordinates, once per viewport, through that viewport's camera.
|
|
104
|
+
view = add_node(RGame::Engine::WorldView.new)
|
|
105
|
+
# One node per Tiled layer. The node handed back is the gap between the
|
|
106
|
+
# ground layers and any layer Tiled flags `above`, which is where things
|
|
107
|
+
# that walk around go — the rig included, so a canopy layer would pass over
|
|
108
|
+
# it without this file choosing a single z.
|
|
109
|
+
actors = RGame::Engine::TileMapLayer.mount(view)
|
|
110
|
+
actors.add_node(build_rig(map, players.primary.camera))
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def on_draw(renderer, _view)
|
|
114
|
+
# Outside the WorldView, so this is screen space: it stays put while the
|
|
115
|
+
# world scrolls under it.
|
|
116
|
+
renderer.text(@help, 12, 12)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
private
|
|
120
|
+
|
|
121
|
+
def build_rig(map, camera)
|
|
122
|
+
rig = Rig.new(x: map.pixel_width / 2.0, y: map.pixel_height / 2.0,
|
|
123
|
+
world_width: map.pixel_width, world_height: map.pixel_height)
|
|
124
|
+
rig.add_component(RGame::Engine::Components::CharacterBody.new(speed: SPEED))
|
|
125
|
+
rig.add_component(RGame::Engine::Components::PlayerController.new)
|
|
126
|
+
rig.add_component(RGame::Engine::Components::CameraFollow.new(camera: camera))
|
|
127
|
+
rig
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
game = RGame::Game.new(
|
|
132
|
+
root: Scene.new,
|
|
133
|
+
caption: 'Scroll map',
|
|
134
|
+
width: WIDTH,
|
|
135
|
+
height: HEIGHT,
|
|
136
|
+
media_root: ASSETS,
|
|
137
|
+
locales: LOCALES
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
game.start
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# The English table: every string this example draws.
|
|
2
|
+
en:
|
|
3
|
+
help:
|
|
4
|
+
plate: Enter stands on the plate — the door and the lamp both answer
|
|
5
|
+
unnamed: Neither of them is named anywhere inside the plate
|
|
6
|
+
cooldowns: "Hold Space and hold E: one ActionTrigger, two cooldowns"
|