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,298 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Pathfinding — pick a tile, and the hero works out how to get there.
|
|
4
|
+
#
|
|
5
|
+
# Run it:
|
|
6
|
+
#
|
|
7
|
+
# ruby examples/pathfinding/main.rb
|
|
8
|
+
#
|
|
9
|
+
# The arrow keys or a d-pad move the tile cursor, and holding one keeps it moving.
|
|
10
|
+
# Return, Space or the pad's A button sends the hero there. The trees and the fence
|
|
11
|
+
# are solid, and the fence has one gap. It exercises:
|
|
12
|
+
# - Components::Navigator — `go_to(world_x, world_y)`: a route over the map,
|
|
13
|
+
# smoothed against the hero's own feet box, and walked;
|
|
14
|
+
# - Components::TileWorld#nav_grid — the search grid, built from the same
|
|
15
|
+
# solidity the collision uses, and asked whether a tile can be stood on;
|
|
16
|
+
# - Components::AnimatedSprite — facing the way the route goes, with nobody
|
|
17
|
+
# pressing a direction;
|
|
18
|
+
# - Components::ActionTrigger — the cursor's held-key repeat;
|
|
19
|
+
# - Components::CameraFollow — on the cursor rather than on the hero;
|
|
20
|
+
# - Engine::Text.computed — a status line assembled from several translations,
|
|
21
|
+
# with plurals, and built only when the route or the language changes;
|
|
22
|
+
# - the TileWorld, FeetCollider and TileMapLayer scene from `examples/collision_tiles`.
|
|
23
|
+
#
|
|
24
|
+
# ## Two drawings of one route
|
|
25
|
+
#
|
|
26
|
+
# The small dots are the route the search found: one per tile, start to target,
|
|
27
|
+
# each a step to a neighbouring tile. The lines are the route the hero walks. They
|
|
28
|
+
# are the same route, and the difference between them is the point of this file.
|
|
29
|
+
#
|
|
30
|
+
# A tile-by-tile route walked as it stands zig-zags. It turns wherever the grid
|
|
31
|
+
# does, which is at every tile a diagonal meets a straight, and a character doing
|
|
32
|
+
# that looks like it is being steered by a spreadsheet. So the navigator pulls the
|
|
33
|
+
# route tight: from each corner it keeps going straight for as long as it can, and
|
|
34
|
+
# turns only where it has to. Send the hero across open ground and there is one
|
|
35
|
+
# line; send it through the fence and there are a handful, against dozens of dots.
|
|
36
|
+
#
|
|
37
|
+
# ## "As long as it can" means as long as the feet fit
|
|
38
|
+
#
|
|
39
|
+
# A straight line is kept only if the hero's feet box — the red box in
|
|
40
|
+
# `examples/collision_tiles`, twelve by six — can travel it without touching a
|
|
41
|
+
# solid tile, and the thing asked is the same `TileWorld` that stops the hero when
|
|
42
|
+
# it walks. A line tested against tiles alone would cut past a tree's corner that a
|
|
43
|
+
# point clears and a feet box clips. The hero does not slide along the tree the way
|
|
44
|
+
# a player-steered character would: it is on a route, so it would stand at that
|
|
45
|
+
# corner pressing into it. The search and the collision cannot disagree about what
|
|
46
|
+
# fits, because they are asking one map.
|
|
47
|
+
#
|
|
48
|
+
# ## The cursor, and where the camera looks
|
|
49
|
+
#
|
|
50
|
+
# The camera follows the cursor, not the hero. A target is picked by looking at it,
|
|
51
|
+
# and a cursor that could be walked off the screen would be lost; the hero is the
|
|
52
|
+
# thing that is allowed to leave the view, and it walks back into it on its way to
|
|
53
|
+
# the tile you picked.
|
|
54
|
+
#
|
|
55
|
+
# The outline is green on a tile the hero can stand on and red on a solid one. That
|
|
56
|
+
# is read from the grid once each time the cursor moves, not every frame. Confirming
|
|
57
|
+
# on a red tile does nothing to the hero — there is no route to a tree, and the
|
|
58
|
+
# navigator says so by returning `false` — so a hero already walking carries on.
|
|
59
|
+
#
|
|
60
|
+
# ## What this scene does not solve
|
|
61
|
+
#
|
|
62
|
+
# The hero plans against the map and nothing else. Give it `blocked_by: %i[tiles
|
|
63
|
+
# npc]` and it waits behind a villager standing on its route rather than going
|
|
64
|
+
# round; a game that wants it to go round calls `go_to` again. The map never
|
|
65
|
+
# changes, so the grid is built once, the first time it is asked for — a door that
|
|
66
|
+
# opens would need a grid that is told. And there is one hero: many walkers heading
|
|
67
|
+
# for one place want a different search from many separate ones.
|
|
68
|
+
|
|
69
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', __dir__)
|
|
70
|
+
require 'rgame/game'
|
|
71
|
+
|
|
72
|
+
WIDTH = 640
|
|
73
|
+
HEIGHT = 480
|
|
74
|
+
ASSETS = File.expand_path('../assets', __dir__)
|
|
75
|
+
LOCALES = File.expand_path('locales', __dir__) # the text on screen: locales/en.yml
|
|
76
|
+
|
|
77
|
+
MAP = 'town.tmx'
|
|
78
|
+
TILE = 16
|
|
79
|
+
SPEED = 90.0 # px/s
|
|
80
|
+
|
|
81
|
+
FEET_WIDTH = 12
|
|
82
|
+
FEET_HEIGHT = 6
|
|
83
|
+
|
|
84
|
+
# The tile the hero starts on: north of the fence and well east of its gap, so any
|
|
85
|
+
# route south goes the long way.
|
|
86
|
+
START_COL = 24
|
|
87
|
+
START_ROW = 18
|
|
88
|
+
|
|
89
|
+
# The middle of the feet box, from the hero's origin: centred across the 16px-wide
|
|
90
|
+
# sprite, and half the box up from the bottom of its 22px height. Standing the hero
|
|
91
|
+
# with this point on a tile's centre is standing it on that tile.
|
|
92
|
+
FEET_CENTRE_X = 8
|
|
93
|
+
FEET_CENTRE_Y = 19
|
|
94
|
+
|
|
95
|
+
# How often a held direction moves the cursor another tile.
|
|
96
|
+
CURSOR_REPEAT = 0.12 # s
|
|
97
|
+
|
|
98
|
+
# A sprite, a feet box and a navigator. Where `examples/collision_tiles` has a
|
|
99
|
+
# CharacterBody and a PlayerController, this hero has one component that plans
|
|
100
|
+
# its own movement, and nothing reads the keyboard on its behalf.
|
|
101
|
+
class Hero < RGame::Engine::Node2D
|
|
102
|
+
attr_reader :navigator, :collider
|
|
103
|
+
|
|
104
|
+
def initialize(**)
|
|
105
|
+
super
|
|
106
|
+
add_component(RGame::Engine::Components::AnimatedSprite.new(sheet: 'hero.json'))
|
|
107
|
+
@collider = add_component(RGame::Engine::Components::FeetCollider.new(
|
|
108
|
+
width: FEET_WIDTH, height: FEET_HEIGHT
|
|
109
|
+
))
|
|
110
|
+
@navigator = add_component(RGame::Engine::Components::Navigator.new(
|
|
111
|
+
speed: SPEED, blocked_by: [:tiles]
|
|
112
|
+
))
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# A tile outline, moved a tile at a time. It knows which tile it is on and whether
|
|
117
|
+
# that tile can be stood on; what confirming *means* is the scene's business, so it
|
|
118
|
+
# only reports the tile's centre.
|
|
119
|
+
class Cursor < RGame::Engine::Node2D
|
|
120
|
+
WALKABLE = RGame::Util::Color.rgba(120, 230, 120, 255)
|
|
121
|
+
SOLID = RGame::Util::Color.rgba(240, 90, 90, 255)
|
|
122
|
+
STEPS = { ui_left: [-1, 0], ui_right: [1, 0], ui_up: [0, -1], ui_down: [0, 1] }.freeze
|
|
123
|
+
|
|
124
|
+
signal :on_confirmed, RGame::Engine::Signal.define(:world_x, :world_y)
|
|
125
|
+
|
|
126
|
+
def initialize(col:, row:, camera:)
|
|
127
|
+
super(x: col * TILE, y: row * TILE)
|
|
128
|
+
@col = col
|
|
129
|
+
@row = row
|
|
130
|
+
# ActionTrigger fires on the tick a direction goes down and again every
|
|
131
|
+
# CURSOR_REPEAT while it stays down — which is a key repeat, per action.
|
|
132
|
+
repeat = add_component(RGame::Engine::Components::ActionTrigger.new(
|
|
133
|
+
STEPS.keys.to_h { |action| [action, CURSOR_REPEAT] }
|
|
134
|
+
))
|
|
135
|
+
repeat.on_triggered { |action| step(*STEPS.fetch(action)) }
|
|
136
|
+
add_component(RGame::Engine::Components::CameraFollow.new(
|
|
137
|
+
camera: camera, offset_x: TILE / 2, offset_y: TILE / 2
|
|
138
|
+
))
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def on_add
|
|
142
|
+
@world = system(RGame::Engine::Components::TileWorld)
|
|
143
|
+
step(0, 0)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def on_control(actions)
|
|
147
|
+
return unless actions.pressed?(:ui_confirm)
|
|
148
|
+
|
|
149
|
+
on_confirmed_signal.emit(world_x: (@col + 0.5) * TILE, world_y: (@row + 0.5) * TILE)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def on_draw(renderer, _view)
|
|
153
|
+
renderer.line(0, 0, TILE, 0, thickness: 2.0, color: @color)
|
|
154
|
+
renderer.line(TILE, 0, TILE, TILE, thickness: 2.0, color: @color)
|
|
155
|
+
renderer.line(TILE, TILE, 0, TILE, thickness: 2.0, color: @color)
|
|
156
|
+
renderer.line(0, TILE, 0, 0, thickness: 2.0, color: @color)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
private
|
|
160
|
+
|
|
161
|
+
def step(dcol, drow)
|
|
162
|
+
grid = @world.nav_grid
|
|
163
|
+
@col = (@col + dcol).clamp(0, grid.width - 1)
|
|
164
|
+
@row = (@row + drow).clamp(0, grid.height - 1)
|
|
165
|
+
self.x = @col * TILE
|
|
166
|
+
self.y = @row * TILE
|
|
167
|
+
@color = grid.walkable?(@col, @row) ? WALKABLE : SOLID
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Both drawings of the hero's current route, in world space: a dot per tile the
|
|
172
|
+
# search returned, and a line per segment of the path being walked.
|
|
173
|
+
#
|
|
174
|
+
# The path's waypoints are where the hero's *origin* goes — its sprite's top-left
|
|
175
|
+
# corner — while the route was planned for the middle of its feet. Adding the feet
|
|
176
|
+
# box's centre back puts the lines on the ground the dots are on.
|
|
177
|
+
class Route < RGame::Engine::Node2D
|
|
178
|
+
DOT = RGame::Util::Color.rgba(255, 255, 255, 200)
|
|
179
|
+
LINE = RGame::Util::Color.rgba(255, 220, 60, 255)
|
|
180
|
+
DOT_SIZE = 4
|
|
181
|
+
|
|
182
|
+
def initialize(hero:)
|
|
183
|
+
super()
|
|
184
|
+
@hero = hero
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def on_draw(renderer, _view)
|
|
188
|
+
navigator = @hero.navigator
|
|
189
|
+
draw_cells(renderer, navigator.cells) if navigator.cells
|
|
190
|
+
draw_path(renderer, navigator.path) if navigator.path
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
private
|
|
194
|
+
|
|
195
|
+
def draw_cells(renderer, cells)
|
|
196
|
+
offset = (TILE - DOT_SIZE) / 2
|
|
197
|
+
index = 0
|
|
198
|
+
while index < cells.length
|
|
199
|
+
col, row = cells[index]
|
|
200
|
+
renderer.rect((col * TILE) + offset, (row * TILE) + offset, DOT_SIZE, DOT_SIZE, color: DOT)
|
|
201
|
+
index += 1
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def draw_path(renderer, path)
|
|
206
|
+
box = @hero.collider.box
|
|
207
|
+
feet_x = box.offset_x + (box.width / 2.0)
|
|
208
|
+
feet_y = box.offset_y + (box.height / 2.0)
|
|
209
|
+
index = 1
|
|
210
|
+
while index < path.count
|
|
211
|
+
renderer.line(path.x_at(index - 1) + feet_x, path.y_at(index - 1) + feet_y,
|
|
212
|
+
path.x_at(index) + feet_x, path.y_at(index) + feet_y,
|
|
213
|
+
thickness: 2.0, color: LINE)
|
|
214
|
+
index += 1
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# The scene: the map and its world, the hero, the route drawn under the hero and
|
|
220
|
+
# the cursor over it — and the one line that connects a confirmed tile to a walk.
|
|
221
|
+
class Scene < RGame::Engine::Node2D
|
|
222
|
+
def on_add
|
|
223
|
+
map = root.context.assets.tilemap(MAP).map
|
|
224
|
+
players = root.system(RGame::Engine::Players)
|
|
225
|
+
add_component(RGame::Engine::Components::TileWorld.new(
|
|
226
|
+
map: map, tilemap_id: MAP, cameras: players.map(&:camera)
|
|
227
|
+
))
|
|
228
|
+
|
|
229
|
+
view = add_node(RGame::Engine::WorldView.new)
|
|
230
|
+
actors = RGame::Engine::TileMapLayer.mount(view)
|
|
231
|
+
@hero = Hero.new(x: ((START_COL + 0.5) * TILE) - FEET_CENTRE_X,
|
|
232
|
+
y: ((START_ROW + 0.5) * TILE) - FEET_CENTRE_Y)
|
|
233
|
+
actors.add_node(Route.new(hero: @hero))
|
|
234
|
+
actors.add_node(@hero)
|
|
235
|
+
cursor = actors.add_node(Cursor.new(col: START_COL, row: START_ROW, camera: players.primary.camera))
|
|
236
|
+
|
|
237
|
+
cursor.on_confirmed { |world_x, world_y| send_hero(world_x, world_y) }
|
|
238
|
+
@hero.navigator.on_finished do
|
|
239
|
+
@arrived = true
|
|
240
|
+
report
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
@arrived = false
|
|
244
|
+
@status = nil
|
|
245
|
+
@status_label = RGame::Engine::Text.computed(:status) { |status:| describe(status) }
|
|
246
|
+
@help_cursor = RGame::Engine::Text.new('help.cursor')
|
|
247
|
+
@help_route = RGame::Engine::Text.new('help.route')
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def on_draw(renderer, _view)
|
|
251
|
+
renderer.text(@help_cursor, 12, 12)
|
|
252
|
+
renderer.text(@help_route, 12, 34)
|
|
253
|
+
renderer.text(@status_label.with(status: @status), 12, 56)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
private
|
|
257
|
+
|
|
258
|
+
def send_hero(world_x, world_y)
|
|
259
|
+
routed = @hero.navigator.go_to(world_x, world_y)
|
|
260
|
+
@arrived = false if routed
|
|
261
|
+
report(refused: !routed)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# The status is rebuilt on an event, never per frame, so the label's value only
|
|
265
|
+
# changes when there is something new to say.
|
|
266
|
+
def report(refused: false)
|
|
267
|
+
navigator = @hero.navigator
|
|
268
|
+
@status = [navigator.cells&.length, navigator.path&.count, @arrived, refused].freeze
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# The status line, in whatever language is current. It is several keys rather
|
|
272
|
+
# than one, because a count needs its own plural and a refusal can wrap a
|
|
273
|
+
# route: `computed` is for text put together like this, and its block runs
|
|
274
|
+
# only when the status or the language changes.
|
|
275
|
+
def describe(status)
|
|
276
|
+
i18n = RGame::Engine::I18n
|
|
277
|
+
return i18n.t('status.idle') unless status
|
|
278
|
+
|
|
279
|
+
cells, waypoints, arrived, refused = status
|
|
280
|
+
route = cells && i18n.t(arrived ? 'status.arrived' : 'status.walking',
|
|
281
|
+
waypoints: i18n.t('status.waypoints', count: waypoints),
|
|
282
|
+
cells: i18n.t('status.cells', count: cells))
|
|
283
|
+
return route unless refused
|
|
284
|
+
|
|
285
|
+
route ? i18n.t('status.refused_after', route: route) : i18n.t('status.refused')
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
game = RGame::Game.new(
|
|
290
|
+
root: Scene.new,
|
|
291
|
+
caption: 'Pathfinding',
|
|
292
|
+
width: WIDTH,
|
|
293
|
+
height: HEIGHT,
|
|
294
|
+
media_root: ASSETS,
|
|
295
|
+
locales: LOCALES
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
game.start
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Pooling — spawning a lot of things without building any of them.
|
|
4
|
+
#
|
|
5
|
+
# Run it:
|
|
6
|
+
#
|
|
7
|
+
# ruby examples/pooling/main.rb
|
|
8
|
+
#
|
|
9
|
+
# **Space** switches between taking motes from a pool and building a fresh one
|
|
10
|
+
# every time. Watch the number. It exercises:
|
|
11
|
+
# - Components::Pool — acquire, add as a child, reclaim on free;
|
|
12
|
+
# - Engine::Pool — the free list underneath it;
|
|
13
|
+
# - Components::DespawnOffscreen — retiring a mote that has left the world;
|
|
14
|
+
# - Components::Timer — the spawn cadence, from `examples/timer`;
|
|
15
|
+
# - Engine::Text — a readout with a variable that changes once a second, not per frame.
|
|
16
|
+
#
|
|
17
|
+
# ## The number on screen is the whole argument
|
|
18
|
+
#
|
|
19
|
+
# Everything else here is prose. The readout samples
|
|
20
|
+
# `GC.stat(:total_allocated_objects)` once a second and shows the difference, and
|
|
21
|
+
# switching modes moves it within a second or two of the press. Measured over a
|
|
22
|
+
# steady eighty motes a second:
|
|
23
|
+
#
|
|
24
|
+
# pooled about 140 objects a second
|
|
25
|
+
# fresh about 750
|
|
26
|
+
#
|
|
27
|
+
# Same spawn rate, same motes on screen, same draw calls. The gap is around seven
|
|
28
|
+
# and a half objects per mote — the node, its two components, and the few arrays
|
|
29
|
+
# and hashes a node keeps for them — and it is *entirely* what the pool removes.
|
|
30
|
+
#
|
|
31
|
+
# A steady sixty-frame-per-second loop that allocates is a GC pause waiting to
|
|
32
|
+
# happen, and a pause is not a slow frame: it is a visible hitch at a moment
|
|
33
|
+
# nobody chose. Bullets and particles are where it starts, because they are the
|
|
34
|
+
# things there are suddenly two hundred of.
|
|
35
|
+
#
|
|
36
|
+
# **The readout only means anything in a plain run.** Under
|
|
37
|
+
# `tools/drive_test_project.rb` both numbers land near eighty-five thousand,
|
|
38
|
+
# because the harness records every draw call it sees and that recording dwarfs
|
|
39
|
+
# everything the game does. Measure the thing you changed, in the configuration
|
|
40
|
+
# you care about.
|
|
41
|
+
#
|
|
42
|
+
# ## A game writes two lines and no bookkeeping
|
|
43
|
+
#
|
|
44
|
+
# @pool.spawn { |mote| mote.reset(...) } # acquire, initialise, add as a child
|
|
45
|
+
# mote.queue_free # → back on the free list
|
|
46
|
+
#
|
|
47
|
+
# There is no acquire/add/reclaim bridge to write, because
|
|
48
|
+
# `Components::Pool#update` rides the tick and reclaims everything freed since
|
|
49
|
+
# the last one. Nothing in this file asks for a mote back, and
|
|
50
|
+
# `Components::DespawnOffscreen` — which knows nothing about pools — retires them
|
|
51
|
+
# by calling the same `queue_free` any other node would.
|
|
52
|
+
#
|
|
53
|
+
# ## A pooled node is an ordinary child
|
|
54
|
+
#
|
|
55
|
+
# It is in the tree, the traversal updates and draws it, its components run. The
|
|
56
|
+
# pool manages membership and nothing else. Pooling is not a parallel world with
|
|
57
|
+
# its own rules — which is worth saying, because the one rule it *does* add is
|
|
58
|
+
# easy to miss.
|
|
59
|
+
#
|
|
60
|
+
# ## The one rule: components go in `initialize`, not `on_add`
|
|
61
|
+
#
|
|
62
|
+
# `on_add` fires on **every** entry into the tree, and a pooled node enters it
|
|
63
|
+
# again on each spawn. A component added there is added a second time, and
|
|
64
|
+
# `add_component` refuses a slot that is already taken — so a pool that worked
|
|
65
|
+
# for one lifetime raises on the next. Mote below adds its components in
|
|
66
|
+
# `initialize`, where a node built by a factory outside the tree collects them
|
|
67
|
+
# once.
|
|
68
|
+
#
|
|
69
|
+
# The mirror of that rule is what makes reuse safe: `Components::Timer` re-arms
|
|
70
|
+
# itself in `on_attach`, and `Components::DespawnOffscreen` re-resolves its world
|
|
71
|
+
# bounds there, so a recycled node inherits neither a spent countdown nor the
|
|
72
|
+
# dimensions of the scene it came from.
|
|
73
|
+
#
|
|
74
|
+
# ## `reset` is the other half of a factory that builds blanks
|
|
75
|
+
#
|
|
76
|
+
# `Engine::Pool.new { Mote.new }` builds a blank, because it has no idea where the
|
|
77
|
+
# next mote will be or where it is going. `spawn` takes a block and runs it
|
|
78
|
+
# *before* the node enters the tree, which is where those answers get filled in.
|
|
79
|
+
#
|
|
80
|
+
# ## What it does not solve
|
|
81
|
+
#
|
|
82
|
+
# A cap. The pool grows to the high-water mark of how many are live at once and
|
|
83
|
+
# never shrinks, which is the intent — the memory is the point of the exercise —
|
|
84
|
+
# but a spawner that can outrun its despawn has no brake here.
|
|
85
|
+
|
|
86
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', __dir__)
|
|
87
|
+
require 'rgame/game'
|
|
88
|
+
|
|
89
|
+
WIDTH = 640
|
|
90
|
+
HEIGHT = 480
|
|
91
|
+
ASSETS = File.expand_path('../assets', __dir__)
|
|
92
|
+
LOCALES = File.expand_path('locales', __dir__) # the text on screen: locales/en.yml
|
|
93
|
+
|
|
94
|
+
SPAWN_EVERY = 0.05 # seconds
|
|
95
|
+
PER_BURST = 4
|
|
96
|
+
SPEED_MIN = 70.0
|
|
97
|
+
SPEED_MAX = 210.0
|
|
98
|
+
SAMPLE = 1.0 # how often the allocation readout is taken
|
|
99
|
+
DEFAULT_SEED = 0x9001
|
|
100
|
+
|
|
101
|
+
# Far enough out that a mote is well off the screen before it is retired, so the
|
|
102
|
+
# despawn is never something you can see happening.
|
|
103
|
+
DESPAWN_MARGIN = 30.0
|
|
104
|
+
|
|
105
|
+
# A blank when the factory builds it, a particular mote when `reset` is done with
|
|
106
|
+
# it. Both of its components are added here rather than in `on_add`, because a
|
|
107
|
+
# pooled node re-enters the tree on every spawn and `on_add` fires every time.
|
|
108
|
+
class Mote < RGame::Engine::Node2D
|
|
109
|
+
SIZE = 7
|
|
110
|
+
|
|
111
|
+
def initialize
|
|
112
|
+
super(width: SIZE, height: SIZE)
|
|
113
|
+
@velocity = add_component(RGame::Engine::Components::Velocity.new)
|
|
114
|
+
add_component(RGame::Engine::Components::DespawnOffscreen.new(margin: DESPAWN_MARGIN))
|
|
115
|
+
@color = RGame::Util::Color.new(255, 255, 255)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Run by the spawner before this node enters the tree, which is the seam a
|
|
119
|
+
# factory that builds blanks needs. Returns self so a fresh mote can be built
|
|
120
|
+
# and placed in one expression.
|
|
121
|
+
def reset(x, y, vx, vy, color)
|
|
122
|
+
self.x = x
|
|
123
|
+
self.y = y
|
|
124
|
+
@velocity.vx = vx
|
|
125
|
+
@velocity.vy = vy
|
|
126
|
+
@color = color
|
|
127
|
+
self
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def on_draw(renderer, _view) = renderer.rect(-SIZE / 2, -SIZE / 2, SIZE, SIZE, color: @color)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Emits motes on a cadence, either from a pool or by building each one, and the
|
|
134
|
+
# only difference between the two paths is the line that gets the node.
|
|
135
|
+
class Spawner < RGame::Engine::Node2D
|
|
136
|
+
POOLED = RGame::Util::Color.new(120, 210, 150)
|
|
137
|
+
FRESH = RGame::Util::Color.new(235, 120, 100)
|
|
138
|
+
|
|
139
|
+
def initialize(rng:, **)
|
|
140
|
+
super(**)
|
|
141
|
+
@rng = rng
|
|
142
|
+
@pooled = true
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def pooled? = @pooled
|
|
146
|
+
def toggle = @pooled = !@pooled
|
|
147
|
+
# Children, not `@pool.size`: both paths attach the mote as a child of this
|
|
148
|
+
# node, and only one of them involves a pool. Counting the pool would make the
|
|
149
|
+
# bar vanish in fresh mode and hide the thing being compared.
|
|
150
|
+
def live = children.size
|
|
151
|
+
|
|
152
|
+
def on_add
|
|
153
|
+
@pool = add_component(RGame::Engine::Components::Pool.new { Mote.new })
|
|
154
|
+
add_component(RGame::Engine::Components::Timer.new(SPAWN_EVERY))
|
|
155
|
+
.on_timeout { PER_BURST.times { emit } }
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
private
|
|
159
|
+
|
|
160
|
+
def emit
|
|
161
|
+
if @pooled
|
|
162
|
+
# Acquire, initialise, attach — one call, and the reclaim is already
|
|
163
|
+
# arranged for.
|
|
164
|
+
@pool.spawn { |mote| place(mote) }
|
|
165
|
+
else
|
|
166
|
+
# The same three steps written out, and a new object every single time.
|
|
167
|
+
# Nothing else about the mote or the frame differs.
|
|
168
|
+
add_node(place(Mote.new))
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def place(mote)
|
|
173
|
+
angle = @rng.rand * Math::PI * 2
|
|
174
|
+
speed = SPEED_MIN + (@rng.rand * (SPEED_MAX - SPEED_MIN))
|
|
175
|
+
mote.reset(0, 0, Math.cos(angle) * speed, Math.sin(angle) * speed,
|
|
176
|
+
@pooled ? POOLED : FRESH)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Samples the allocation counter once a second and shows the difference.
|
|
181
|
+
#
|
|
182
|
+
# A per-frame readout is the one thing a cached Text cannot help with — a value
|
|
183
|
+
# that changes every frame has to be rebuilt every frame. Sampling on a slow
|
|
184
|
+
# timer makes it a value that changes once a second, which is exactly what the
|
|
185
|
+
# cache is for, and it is also the only way the number means anything: a single
|
|
186
|
+
# frame's allocations are noise.
|
|
187
|
+
class Meter < RGame::Engine::Node2D
|
|
188
|
+
INK = RGame::Util::Color.new(255, 255, 255)
|
|
189
|
+
|
|
190
|
+
def initialize(**)
|
|
191
|
+
super
|
|
192
|
+
@label = RGame::Engine::Text.new('hud.allocated', :count)
|
|
193
|
+
@count = 0
|
|
194
|
+
@last = 0
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def on_add
|
|
198
|
+
@last = GC.stat(:total_allocated_objects)
|
|
199
|
+
add_component(RGame::Engine::Components::Timer.new(SAMPLE)).on_timeout { sample }
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def on_draw(renderer, _view) = renderer.text(@label.with(count: @count), 0, 0, color: INK)
|
|
203
|
+
|
|
204
|
+
private
|
|
205
|
+
|
|
206
|
+
def sample
|
|
207
|
+
now = GC.stat(:total_allocated_objects)
|
|
208
|
+
@count = now - @last
|
|
209
|
+
@last = now
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
class Scene < RGame::Engine::Node2D
|
|
214
|
+
BACKDROP = RGame::Util::Color.new(26, 30, 38)
|
|
215
|
+
BAR = RGame::Util::Color.new(90, 100, 124)
|
|
216
|
+
BAR_X = 12
|
|
217
|
+
BAR_Y = 86
|
|
218
|
+
BAR_H = 14
|
|
219
|
+
BAR_SCALE = 2.4 # pixels per live mote
|
|
220
|
+
|
|
221
|
+
MODE = { true => RGame::Engine::Text.new('status.pooled'),
|
|
222
|
+
false => RGame::Engine::Text.new('status.fresh') }.freeze
|
|
223
|
+
|
|
224
|
+
def initialize
|
|
225
|
+
super
|
|
226
|
+
@rng = Random.new(ENV.fetch('RGAME_SEED', DEFAULT_SEED).to_i)
|
|
227
|
+
# Mounted outside the tree, so every DespawnOffscreen that attaches later
|
|
228
|
+
# finds it — the same ordering the wrap in `examples/velocity` depends on.
|
|
229
|
+
add_component(RGame::Engine::Components::World.new(width: WIDTH, height: HEIGHT))
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def on_add
|
|
233
|
+
@spawner = add_node(Spawner.new(rng: @rng, x: WIDTH / 2, y: HEIGHT / 2))
|
|
234
|
+
@meter = add_node(Meter.new(x: 12, y: 34))
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def on_control(actions)
|
|
238
|
+
@spawner.toggle if actions.pressed?(:fire)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def on_draw(renderer, view)
|
|
242
|
+
renderer.rect(0, 0, view.width, view.height, color: BACKDROP)
|
|
243
|
+
renderer.text(MODE.fetch(@spawner.pooled?), 12, 12)
|
|
244
|
+
# The live count as a bar rather than a number: it changes most frames, and
|
|
245
|
+
# a label rebuilt every frame is the allocation this example is about.
|
|
246
|
+
renderer.rect(BAR_X, BAR_Y, @spawner.live * BAR_SCALE, BAR_H, color: BAR)
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
game = RGame::Game.new(
|
|
251
|
+
root: Scene.new,
|
|
252
|
+
caption: 'Pooling',
|
|
253
|
+
width: WIDTH,
|
|
254
|
+
height: HEIGHT,
|
|
255
|
+
media_root: ASSETS,
|
|
256
|
+
locales: LOCALES
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
game.start
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# The English table: every string this example draws.
|
|
2
|
+
en:
|
|
3
|
+
help:
|
|
4
|
+
wheel: Hold Tab (or LB), point with the arrows (or stick), let go to choose
|
|
5
|
+
status:
|
|
6
|
+
nothing: "Chosen: nothing yet"
|
|
7
|
+
chosen: "Chosen: %{item}"
|
|
8
|
+
items:
|
|
9
|
+
home: Home
|
|
10
|
+
settings: Settings
|
|
11
|
+
save: Save
|
|
12
|
+
favourite: Favourite
|
|
13
|
+
trophies: Trophies
|
|
14
|
+
sound: Sound
|
|
15
|
+
music: Music
|
|
16
|
+
locked: Locked
|