rgame 0.1.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 +228 -0
- data/README.md +142 -243
- data/docs/api/README.md +141 -90
- data/docs/api/app.md +125 -68
- data/docs/api/assets.md +203 -166
- data/docs/api/audio.md +130 -89
- data/docs/api/cli.md +259 -0
- data/docs/api/components.md +1045 -254
- data/docs/api/drawing.md +187 -119
- data/docs/api/examples.md +263 -0
- data/docs/api/game.md +165 -45
- data/docs/api/images.md +39 -39
- data/docs/api/input.md +344 -85
- data/docs/api/internals.md +241 -62
- data/docs/api/localization.md +285 -0
- data/docs/api/scene_graph.md +503 -123
- data/docs/api/signals.md +81 -76
- data/docs/api/systems.md +197 -64
- data/docs/api/text.md +58 -41
- data/docs/api/tile_maps.md +237 -0
- data/docs/api/toolbox.md +400 -147
- data/docs/api/ui.md +927 -0
- data/docs/api/values.md +234 -19
- 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/README.md +6 -5
- data/ext/rgame_core/app/app.c +215 -11
- 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 +113 -2
- data/ext/rgame_core/example.rb +18 -56
- data/ext/rgame_core/extconf.rb +16 -113
- data/ext/rgame_core/graphics/canvas.c +45 -4
- data/ext/rgame_core/graphics/canvas.h +65 -10
- data/ext/rgame_core/graphics/clip.c +22 -13
- data/ext/rgame_core/include/rgame/core.h +151 -5
- data/ext/rgame_core/input/gamepad.c +57 -3
- data/ext/rgame_core/ruby/audio_ext.c +10 -5
- data/ext/rgame_core/ruby/core_ext.c +46 -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 +42 -19
- data/ext/rgame_util/color_ext.c +12 -3
- 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 -42
- 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/input.rb +35 -41
- data/lib/rgame/core/locale.rb +22 -0
- data/lib/rgame/core/nine_slice.rb +0 -21
- data/lib/rgame/core/recording.rb +3 -1
- data/lib/rgame/core/renderer.rb +75 -84
- data/lib/rgame/core/sprite_sheet.rb +0 -3
- data/lib/rgame/core/tile_map_renderer.rb +77 -65
- 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 +55 -10
- 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 +35 -1
- data/lib/rgame/engine/components/action_trigger.rb +0 -1
- data/lib/rgame/engine/components/animated_sprite.rb +31 -23
- data/lib/rgame/engine/components/box_collider.rb +99 -0
- data/lib/rgame/engine/components/camera_follow.rb +45 -0
- data/lib/rgame/engine/components/character_body.rb +21 -41
- 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 +22 -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 +70 -29
- 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 +45 -0
- data/lib/rgame/engine/debug_overlay.rb +23 -18
- 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 +76 -22
- data/lib/rgame/engine/input/actions.rb +63 -12
- data/lib/rgame/engine/input/input_map.rb +196 -0
- data/lib/rgame/engine/layout.rb +82 -0
- data/lib/rgame/engine/nav_grid.rb +87 -0
- data/lib/rgame/engine/node2d.rb +364 -80
- data/lib/rgame/engine/path.rb +4 -6
- data/lib/rgame/engine/player.rb +69 -0
- data/lib/rgame/engine/player_layer.rb +70 -0
- data/lib/rgame/engine/players.rb +205 -0
- data/lib/rgame/engine/presentation.rb +171 -0
- data/lib/rgame/engine/scene/scene_stack.rb +29 -7
- data/lib/rgame/engine/sealed_privates.rb +54 -0
- data/lib/rgame/engine/spatial_hash.rb +53 -8
- 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 +82 -0
- 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 +290 -0
- 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/view.rb +76 -0
- data/lib/rgame/engine/viewports.rb +171 -0
- data/lib/rgame/engine/world_view.rb +71 -0
- data/lib/rgame/engine.rb +43 -25
- data/lib/rgame/game.rb +164 -22
- 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 +106 -44
- 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 +123 -0
- data/lib/rgame/util.rb +5 -3
- data/lib/rgame/version.rb +1 -1
- data/lib/rgame.rb +0 -15
- metadata +176 -20
- 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/camera_view.rb +0 -28
- 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
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
# One viewport being drawn: a rectangle of the screen, and (for a world
|
|
6
|
+
# view) the camera to look through it with.
|
|
7
|
+
#
|
|
8
|
+
# view.x, view.y, view.width, view.height # the screen rect
|
|
9
|
+
# view.camera # nil in screen space
|
|
10
|
+
# view.player # whose view this is, or nil
|
|
11
|
+
# view.visible?(x, y, w, h) # is this worth drawing
|
|
12
|
+
#
|
|
13
|
+
# ## What a view is for
|
|
14
|
+
#
|
|
15
|
+
# `draw(renderer, view)` hands every node the view it is being drawn into,
|
|
16
|
+
# which answers two questions nothing else can. **Where the edges are**: a
|
|
17
|
+
# HUD laying itself out against the whole window is wrong the moment the
|
|
18
|
+
# window is a player's half of one, and until this existed there was nothing
|
|
19
|
+
# else to ask. And **what is worth drawing**: with the world drawn once per
|
|
20
|
+
# player, culling stops being an optimisation and starts being the
|
|
21
|
+
# difference between one frame's work and four.
|
|
22
|
+
#
|
|
23
|
+
# ## It is reused, not rebuilt
|
|
24
|
+
#
|
|
25
|
+
# Viewports owns one of these per viewport and mutates it in place each
|
|
26
|
+
# frame, the way ActionMapper reuses its Actions. Building fresh ones would
|
|
27
|
+
# allocate a handful of objects every frame — invisible by every measure
|
|
28
|
+
# except the frame that stutters. **Hold the player or the viewports, never
|
|
29
|
+
# this**: the object a node was handed last frame is the same one, with
|
|
30
|
+
# different numbers in it.
|
|
31
|
+
class View
|
|
32
|
+
attr_reader :x, :y, :width, :height, :camera, :player
|
|
33
|
+
|
|
34
|
+
def initialize(x: 0, y: 0, width: 0, height: 0, camera: nil, player: nil)
|
|
35
|
+
set(x, y, width, height, camera: camera, player: player)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Mutated in place by Viewports once per frame. Not for game code.
|
|
39
|
+
def set(x, y, width, height, camera: nil, player: nil)
|
|
40
|
+
@x = x
|
|
41
|
+
@y = y
|
|
42
|
+
@width = width
|
|
43
|
+
@height = height
|
|
44
|
+
@camera = camera
|
|
45
|
+
@player = player
|
|
46
|
+
self
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Where this view's contents start, in the space its nodes draw in: the
|
|
50
|
+
# camera's offset for a world view, and the origin for a screen-space one,
|
|
51
|
+
# whose nodes draw relative to the view's own corner.
|
|
52
|
+
# hot-path
|
|
53
|
+
def origin_x = @camera ? @camera.x : 0
|
|
54
|
+
# hot-path
|
|
55
|
+
def origin_y = @camera ? @camera.y : 0
|
|
56
|
+
|
|
57
|
+
# Does a rectangle overlap what this view shows? Coordinates are in the
|
|
58
|
+
# space the caller draws in — world coordinates under a camera, view-local
|
|
59
|
+
# ones in screen space — which is the same space `origin_x` is in.
|
|
60
|
+
# hot-path
|
|
61
|
+
def visible?(x, y, width, height)
|
|
62
|
+
left = origin_x
|
|
63
|
+
top = origin_y
|
|
64
|
+
x + width > left && x < left + @width &&
|
|
65
|
+
y + height > top && y < top + @height
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# The offset to translate by so that content at `origin` lands at this
|
|
69
|
+
# view's corner on screen. The whole of split-screen, in two numbers.
|
|
70
|
+
# hot-path
|
|
71
|
+
def offset_x = @x - origin_x
|
|
72
|
+
# hot-path
|
|
73
|
+
def offset_y = @y - origin_y
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
# How the screen is divided, as a root-scoped system.
|
|
6
|
+
#
|
|
7
|
+
# node.system(Viewports).views # one View per active player
|
|
8
|
+
# node.system(Viewports).screen # the whole window, no camera
|
|
9
|
+
# node.system(Viewports).solo!(cam) # collapse to one view — cutscene
|
|
10
|
+
#
|
|
11
|
+
# It holds the mutable half of the question — which mode is current, who is
|
|
12
|
+
# playing, how big the window is — while Layout holds the arithmetic. That
|
|
13
|
+
# split is deliberate: the rect maths is pure and gets specced with no tree
|
|
14
|
+
# and no window, and only the state that genuinely changes lives in a
|
|
15
|
+
# component.
|
|
16
|
+
#
|
|
17
|
+
# It is a Component on the root, so any node reaches it by walking the tree
|
|
18
|
+
# rather than having it threaded through a constructor — the same shape
|
|
19
|
+
# CollisionWorld and Players use.
|
|
20
|
+
#
|
|
21
|
+
# ## Mode changes are deferred
|
|
22
|
+
#
|
|
23
|
+
# `solo!` and `split!` record a request; it is applied in `update`, which
|
|
24
|
+
# runs in the root's component phase. That matters because this is reachable
|
|
25
|
+
# from anywhere, including from a `draw` — and a `draw` now runs once per
|
|
26
|
+
# view, so a mode change made there would fire several times and tear the
|
|
27
|
+
# frame it was made in. Deferring is the same shape `queue_free` uses, and it
|
|
28
|
+
# means a change takes effect on the next tick.
|
|
29
|
+
class Viewports < Component
|
|
30
|
+
# `views` is the list drawn through this frame — one per active player
|
|
31
|
+
# while split, exactly one while solo. Reused, like the Views in it.
|
|
32
|
+
attr_reader :screen, :width, :height, :views
|
|
33
|
+
|
|
34
|
+
def initialize(players, width: 0, height: 0)
|
|
35
|
+
super()
|
|
36
|
+
@players = players
|
|
37
|
+
@width = width
|
|
38
|
+
@height = height
|
|
39
|
+
@solo_camera = nil
|
|
40
|
+
@pending = nil
|
|
41
|
+
@pool = []
|
|
42
|
+
@screen_pool = []
|
|
43
|
+
@views = []
|
|
44
|
+
@screen = View.new
|
|
45
|
+
refresh
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# The window changed size. Rects are recomputed from it on the next
|
|
49
|
+
# refresh, and every camera reclamps against its new rect — which is why
|
|
50
|
+
# a camera does not carry one.
|
|
51
|
+
def resize(width, height)
|
|
52
|
+
@width = width
|
|
53
|
+
@height = height
|
|
54
|
+
refresh
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def solo? = !@solo_camera.nil?
|
|
58
|
+
|
|
59
|
+
# The screen-space region belonging to `player`: the same rectangle their
|
|
60
|
+
# world view is drawn into, with **no camera**, so its contents are laid
|
|
61
|
+
# out against their own corner rather than the window's. Their HUD and
|
|
62
|
+
# their menus live here.
|
|
63
|
+
#
|
|
64
|
+
# `nil` when they have no region to draw into, which is two cases and one
|
|
65
|
+
# answer. An **empty seat** has no viewport at all. And while the split is
|
|
66
|
+
# **collapsed**, nobody has a half of the screen to own: a cutscene is
|
|
67
|
+
# everyone looking at one thing, so per-player UI has no place to be, and
|
|
68
|
+
# a game wanting something on screen through it draws in the global
|
|
69
|
+
# overlay band instead.
|
|
70
|
+
#
|
|
71
|
+
# Returning nil rather than an empty rectangle is deliberate: one check at
|
|
72
|
+
# the one caller that needs it beats every caller relying on a zero-sized
|
|
73
|
+
# clip happening to draw nothing.
|
|
74
|
+
def screen_for(player)
|
|
75
|
+
return nil if player.nil? || @solo_camera
|
|
76
|
+
|
|
77
|
+
world = @views.find { |view| view.player.equal?(player) }
|
|
78
|
+
return nil if world.nil?
|
|
79
|
+
|
|
80
|
+
pooled_screen(@players.list.index(player))
|
|
81
|
+
.set(world.x, world.y, world.width, world.height, player: player)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Collapse to a single screen-wide view through `camera`.
|
|
85
|
+
#
|
|
86
|
+
# The camera is **required**: promoting one player's would silently give
|
|
87
|
+
# everyone else their view, and deciding what is on screen is what a
|
|
88
|
+
# cutscene is for. A game points an ordinary Camera wherever it likes —
|
|
89
|
+
# with a CameraFollow on a cutscene actor, or its own component framing
|
|
90
|
+
# every player at once — and hands it here.
|
|
91
|
+
def solo!(camera)
|
|
92
|
+
raise ArgumentError, 'solo! needs a camera to look through' if camera.nil?
|
|
93
|
+
|
|
94
|
+
@pending = camera
|
|
95
|
+
self
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Back to one view per player.
|
|
99
|
+
def split! = @pending = :split
|
|
100
|
+
|
|
101
|
+
# Applies a pending mode change, then rebuilds the rects. Runs in the
|
|
102
|
+
# root's component phase, so a change requested during a tick lands on the
|
|
103
|
+
# next one.
|
|
104
|
+
def update(_dt)
|
|
105
|
+
apply_pending
|
|
106
|
+
refresh
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Recompute every rect from the current mode and window, and reclamp each
|
|
110
|
+
# camera against the rect it is about to be drawn into.
|
|
111
|
+
#
|
|
112
|
+
# Allocation-free once the pool has grown: Layout yields its rects rather
|
|
113
|
+
# than building them, and the Views are mutated in place.
|
|
114
|
+
#
|
|
115
|
+
# @api private
|
|
116
|
+
def refresh
|
|
117
|
+
@screen.set(0, 0, @width, @height)
|
|
118
|
+
@solo_camera ? refresh_solo : refresh_split
|
|
119
|
+
@views.each { |view| view.camera&.resolve(view.width, view.height) }
|
|
120
|
+
self
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
def apply_pending
|
|
126
|
+
return if @pending.nil?
|
|
127
|
+
|
|
128
|
+
@solo_camera = @pending == :split ? nil : @pending
|
|
129
|
+
@pending = nil
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def refresh_solo
|
|
133
|
+
@views.clear
|
|
134
|
+
@views << pooled(0).set(0, 0, @width, @height, camera: @solo_camera)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def refresh_split
|
|
138
|
+
@views.clear
|
|
139
|
+
count = @players.active_count
|
|
140
|
+
return if count.zero?
|
|
141
|
+
|
|
142
|
+
index = 0
|
|
143
|
+
Layout.each_rect(count, @width, @height) do |i, x, y, w, h|
|
|
144
|
+
player = active_at(i)
|
|
145
|
+
@views << pooled(index).set(x, y, w, h, camera: player.camera, player: player)
|
|
146
|
+
index += 1
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def active_at(index)
|
|
151
|
+
found = nil
|
|
152
|
+
i = 0
|
|
153
|
+
@players.each do |player|
|
|
154
|
+
next unless player.active?
|
|
155
|
+
|
|
156
|
+
found = player if i == index
|
|
157
|
+
i += 1
|
|
158
|
+
end
|
|
159
|
+
found
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def pooled(index)
|
|
163
|
+
@pool[index] ||= View.new
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def pooled_screen(index)
|
|
167
|
+
@screen_pool[index] ||= View.new
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
# The node that marks where world space begins.
|
|
6
|
+
#
|
|
7
|
+
# view = scene.add_node(WorldView.new)
|
|
8
|
+
# view.add_node(player) # draws at its own world coordinates
|
|
9
|
+
#
|
|
10
|
+
# Its children live in **world** coordinates and are drawn once per active
|
|
11
|
+
# viewport: for each one, clip to that viewport's rectangle and translate by
|
|
12
|
+
# its camera, then run the same subtree again. Everything outside a
|
|
13
|
+
# WorldView is screen space and draws once.
|
|
14
|
+
#
|
|
15
|
+
# That is the whole of split-screen, and it is why the transform and clip
|
|
16
|
+
# stacks in Core are proper push/pop stacks — a clip always narrows, so a
|
|
17
|
+
# child can never draw outside the region its parent allowed.
|
|
18
|
+
#
|
|
19
|
+
# ## The subtree does not know how many times it is drawn
|
|
20
|
+
#
|
|
21
|
+
# Children draw in their own local space and never see a camera. This node
|
|
22
|
+
# supplies the view — one clip and one translate around the whole subtree —
|
|
23
|
+
# so the same world serves one player or four with nothing below it changing,
|
|
24
|
+
# which is exactly what a camera owned by a node *inside* the world could not
|
|
25
|
+
# do, and why cameras belong to players.
|
|
26
|
+
#
|
|
27
|
+
# ## Only `draw` multiplies, and deliberately
|
|
28
|
+
#
|
|
29
|
+
# `control` and `update` still run **once** for this subtree, however many
|
|
30
|
+
# viewports draw it. That is what keeps simulation cost independent of player
|
|
31
|
+
# count, and it is also a safety property: a world node's `on_update` is
|
|
32
|
+
# where mutation belongs, so per-view updating would move an actor at twice
|
|
33
|
+
# the speed with two players — correctly in single player, silently wrong the
|
|
34
|
+
# moment somebody joins.
|
|
35
|
+
#
|
|
36
|
+
# It does make the standing "draw renders state" rule load-bearing rather
|
|
37
|
+
# than stylistic: a `draw` with a side effect now happens once per player,
|
|
38
|
+
# and an allocation in one costs that many times.
|
|
39
|
+
#
|
|
40
|
+
# A node that genuinely needs per-view work at draw time can read
|
|
41
|
+
# `system(Viewports).views` itself. Anything needing per-view state *over
|
|
42
|
+
# time* — a screen shake, a hit flash — is per-*player* rather than per-view,
|
|
43
|
+
# and belongs on that player's camera or their own subtree, which tick once.
|
|
44
|
+
class WorldView < Node2D
|
|
45
|
+
# World content, which is the default anyway — stated because this is the
|
|
46
|
+
# node that marks where world space begins, and a reader looking for
|
|
47
|
+
# "which band is the world in" should find the answer here.
|
|
48
|
+
def initialize(**)
|
|
49
|
+
super(band: :world, **)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Drawn once per viewport, so this overrides the whole of `draw` rather
|
|
53
|
+
# than just `draw_children`: the node's own visuals belong inside the
|
|
54
|
+
# viewport too, not once outside all of them.
|
|
55
|
+
#
|
|
56
|
+
# `view` is the screen-space view this node was reached with, and is
|
|
57
|
+
# deliberately ignored — a WorldView asks the system which viewports exist
|
|
58
|
+
# rather than being told, so a game can place one anywhere in the tree.
|
|
59
|
+
def draw(renderer, _view = nil)
|
|
60
|
+
viewports = system(Viewports)
|
|
61
|
+
viewports.views.each do |world_view|
|
|
62
|
+
renderer.clipped(world_view.x, world_view.y, world_view.width, world_view.height) do
|
|
63
|
+
renderer.translated(world_view.offset_x, world_view.offset_y) do
|
|
64
|
+
super(renderer, world_view)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
data/lib/rgame/engine.rb
CHANGED
|
@@ -1,61 +1,76 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# RGame::Engine — the scene graph a game is written against: nodes, components,
|
|
4
|
-
# signals, scenes, collision, tile maps, pathing, pooling.
|
|
5
|
-
#
|
|
6
|
-
# Pure Ruby, and it links nothing. It may hold `RGame::Util` values but may not
|
|
7
|
-
# name `RGame::Core` at all, reaching the platform only through objects handed
|
|
8
|
-
# to it — a `renderer` passed into `draw`, an audio server behind
|
|
9
|
-
# `AudioDirector`. See CLAUDE.md, "The three layers, and who may talk to whom".
|
|
10
|
-
#
|
|
11
|
-
# That is what lets the whole layer be specified with no window, no GPU and no
|
|
12
|
-
# clock: `spec/` drives `update(dt)` directly and can run a simulated hour in
|
|
13
|
-
# milliseconds.
|
|
14
|
-
#
|
|
15
|
-
# Loaded by `require "rgame"` along with Util, since between them they are
|
|
16
|
-
# everything that runs without a window. Requirable on its own too — hence the
|
|
17
|
-
# `util` require below rather than relying on `rgame.rb` to have got there
|
|
18
|
-
# first: `TileMap` holds a `Util::Tensor`, which is exactly the dependency the
|
|
19
|
-
# three-layer rule allows and this file therefore has to declare.
|
|
20
3
|
require_relative 'util'
|
|
21
4
|
|
|
22
|
-
require_relative 'engine/
|
|
5
|
+
require_relative 'engine/culling'
|
|
23
6
|
require_relative 'engine/signal'
|
|
7
|
+
require_relative 'engine/sealed_privates'
|
|
24
8
|
require_relative 'engine/node2d'
|
|
25
9
|
require_relative 'engine/component'
|
|
26
10
|
require_relative 'engine/animation_set'
|
|
27
11
|
require_relative 'engine/animator'
|
|
28
12
|
require_relative 'engine/camera'
|
|
29
|
-
require_relative 'engine/
|
|
13
|
+
require_relative 'engine/layout'
|
|
14
|
+
require_relative 'engine/presentation'
|
|
15
|
+
require_relative 'engine/view'
|
|
16
|
+
require_relative 'engine/world_view'
|
|
17
|
+
require_relative 'engine/player_layer'
|
|
18
|
+
require_relative 'engine/ui/button'
|
|
19
|
+
require_relative 'engine/ui/nine_slice_style'
|
|
20
|
+
require_relative 'engine/ui/shape_style'
|
|
21
|
+
require_relative 'engine/ui/text_button'
|
|
22
|
+
require_relative 'engine/ui/panel_button'
|
|
23
|
+
require_relative 'engine/ui/option_button'
|
|
24
|
+
require_relative 'engine/ui/icon_button'
|
|
25
|
+
require_relative 'engine/ui/stack'
|
|
26
|
+
require_relative 'engine/ui/column'
|
|
27
|
+
require_relative 'engine/ui/row'
|
|
28
|
+
require_relative 'engine/ui/ring'
|
|
29
|
+
require_relative 'engine/ui/navigation'
|
|
30
|
+
require_relative 'engine/ui/stepping'
|
|
31
|
+
require_relative 'engine/ui/pointing'
|
|
32
|
+
require_relative 'engine/ui/menu'
|
|
33
|
+
require_relative 'engine/ui/panel_menu'
|
|
34
|
+
require_relative 'engine/ui/radial_menu'
|
|
30
35
|
require_relative 'engine/path'
|
|
31
36
|
require_relative 'engine/timer'
|
|
32
37
|
require_relative 'engine/tileset'
|
|
33
38
|
require_relative 'engine/tile_map'
|
|
34
|
-
require_relative 'engine/
|
|
39
|
+
require_relative 'engine/tile_blockers'
|
|
40
|
+
require_relative 'engine/nav_grid'
|
|
41
|
+
require_relative 'engine/actor_blockers'
|
|
42
|
+
require_relative 'engine/bounds_blockers'
|
|
35
43
|
require_relative 'engine/collision_box'
|
|
36
44
|
require_relative 'engine/collision_system'
|
|
37
45
|
require_relative 'engine/circle_collider'
|
|
38
46
|
require_relative 'engine/spatial_hash'
|
|
47
|
+
require_relative 'engine/contact_set'
|
|
39
48
|
require_relative 'engine/pool'
|
|
40
|
-
require_relative 'engine/resettable'
|
|
41
49
|
require_relative 'engine/audio_bus'
|
|
42
50
|
require_relative 'engine/audio_director'
|
|
43
51
|
require_relative 'engine/i18n'
|
|
44
|
-
require_relative 'engine/
|
|
52
|
+
require_relative 'engine/text'
|
|
45
53
|
require_relative 'engine/debug_overlay'
|
|
46
|
-
require_relative 'engine/body'
|
|
47
|
-
require_relative 'engine/actor'
|
|
48
54
|
require_relative 'engine/input/actions'
|
|
55
|
+
require_relative 'engine/input/input_map'
|
|
49
56
|
require_relative 'engine/input/action_mapper'
|
|
50
|
-
require_relative 'engine/
|
|
57
|
+
require_relative 'engine/player'
|
|
58
|
+
require_relative 'engine/players'
|
|
59
|
+
require_relative 'engine/viewports'
|
|
51
60
|
require_relative 'engine/scene/scene_stack'
|
|
61
|
+
require_relative 'engine/components/identity'
|
|
62
|
+
require_relative 'engine/components/mover'
|
|
52
63
|
require_relative 'engine/components/velocity'
|
|
53
64
|
require_relative 'engine/components/pool'
|
|
54
65
|
require_relative 'engine/components/path_follow'
|
|
66
|
+
require_relative 'engine/components/navigator'
|
|
55
67
|
require_relative 'engine/components/timer'
|
|
68
|
+
require_relative 'engine/components/world'
|
|
56
69
|
require_relative 'engine/components/screen_wrap'
|
|
57
70
|
require_relative 'engine/components/despawn_offscreen'
|
|
58
71
|
require_relative 'engine/components/circle_collider'
|
|
72
|
+
require_relative 'engine/components/box_collider'
|
|
73
|
+
require_relative 'engine/components/feet_collider'
|
|
59
74
|
require_relative 'engine/components/collision_world'
|
|
60
75
|
require_relative 'engine/components/targeting'
|
|
61
76
|
require_relative 'engine/components/sprite'
|
|
@@ -64,5 +79,8 @@ require_relative 'engine/components/action_trigger'
|
|
|
64
79
|
require_relative 'engine/components/tile_world'
|
|
65
80
|
require_relative 'engine/components/character_body'
|
|
66
81
|
require_relative 'engine/components/player_controller'
|
|
82
|
+
require_relative 'engine/components/hop'
|
|
67
83
|
require_relative 'engine/components/wander_controller'
|
|
68
84
|
require_relative 'engine/components/animated_sprite'
|
|
85
|
+
require_relative 'engine/components/camera_follow'
|
|
86
|
+
require_relative 'engine/tile_map_layer'
|
data/lib/rgame/game.rb
CHANGED
|
@@ -43,22 +43,93 @@ module RGame
|
|
|
43
43
|
WIDTH = 640
|
|
44
44
|
HEIGHT = 480
|
|
45
45
|
|
|
46
|
-
attr_reader :root, :renderer
|
|
46
|
+
attr_reader :root, :renderer
|
|
47
47
|
|
|
48
|
+
# `input_map:` is what physical inputs mean — one entry per action, naming
|
|
49
|
+
# ids from RGame::Util::Controls. It is merged over the universal UI set, so
|
|
50
|
+
# `ui_confirm` and friends work whether or not a game declares them, and it
|
|
51
|
+
# defaults to RGame::Engine::InputMap::DEFAULT_ACTIONS, so a game wanting
|
|
52
|
+
# eight-way movement and a fire button declares nothing.
|
|
53
|
+
#
|
|
54
|
+
# `device:` is which device drives it — the keyboard, or
|
|
55
|
+
# `Controls.gamepad(slot)` for a controller.
|
|
56
|
+
#
|
|
57
|
+
# `input:` overrides the input backend. It exists so a harness can drive a
|
|
58
|
+
# game from a script instead of from hardware — see
|
|
59
|
+
# tools/drive_test_project.rb, and CLAUDE.md's "The test projects are the
|
|
60
|
+
# acceptance test for wiring", which is why driving one has to be possible
|
|
61
|
+
# at all. A game passes nothing and gets the real thing.
|
|
62
|
+
# `players:` is how many seats the game has, and therefore the most people
|
|
63
|
+
# who can play it. Player 0 starts on `device:`; the rest start empty and
|
|
64
|
+
# are filled when someone uses a controller — see RGame::Engine::Players for
|
|
65
|
+
# why that is a press rather than a plug.
|
|
66
|
+
# `fullscreen:` opens the window fullscreen rather than switching after it is
|
|
67
|
+
# already up, so a game that always runs fullscreen never flashes a windowed
|
|
68
|
+
# frame at startup. `width` and `height` still matter: they are the size the
|
|
69
|
+
# window takes when it leaves fullscreen, whether or not this game offers a
|
|
70
|
+
# way to do that.
|
|
71
|
+
#
|
|
72
|
+
# `scale_mode:` decides what `width` and `height` *mean*, and the default
|
|
73
|
+
# answer is "the resolution the game is designed in". Under `:letterbox` the
|
|
74
|
+
# view a node draws into is always that size, whatever the window is doing,
|
|
75
|
+
# so a layout written against fixed numbers keeps working at any window size
|
|
76
|
+
# and in fullscreen — which is what almost every game wants and what nothing
|
|
77
|
+
# in the engine can supply for it afterwards.
|
|
78
|
+
#
|
|
79
|
+
# `:disabled` is the opt-out, and it is the right answer for something that
|
|
80
|
+
# should genuinely use whatever space it is given: a tool, a HUD-shaped
|
|
81
|
+
# program, an editor. It hands the window straight through as the view, and
|
|
82
|
+
# skips the clip, translate and scale that every other mode pushes. See
|
|
83
|
+
# RGame::Engine::Presentation.
|
|
84
|
+
#
|
|
85
|
+
# `locales:` is the directory the translation tables are in, relative to
|
|
86
|
+
# `media_root` unless absolute. Every `.yml` under it is loaded through the
|
|
87
|
+
# asset manager here, in sorted order, and the language is chosen from the
|
|
88
|
+
# player's OS preferences — so a game writes no i18n setup, and a language
|
|
89
|
+
# the player saved is set after `new` and before `start`. A directory that
|
|
90
|
+
# does not exist loads nothing, and every key shows as itself.
|
|
48
91
|
def initialize(root:, width: WIDTH, height: HEIGHT, caption: 'RGame',
|
|
49
|
-
media_root: 'media',
|
|
50
|
-
|
|
92
|
+
media_root: 'media', input_map: nil, device: Controls::KEYBOARD,
|
|
93
|
+
players: 1, input: nil, fullscreen: false, scale_mode: :letterbox,
|
|
94
|
+
locales: 'locales')
|
|
95
|
+
super(width: width, height: height, caption: caption, media_root: media_root,
|
|
96
|
+
fullscreen: fullscreen)
|
|
51
97
|
|
|
52
98
|
@root = root
|
|
53
99
|
@renderer = RGame::Core::Renderer.new(self)
|
|
54
|
-
@input = RGame::Core::Input.new(self)
|
|
55
|
-
@
|
|
56
|
-
|
|
57
|
-
|
|
100
|
+
@input = input || RGame::Core::Input.new(self)
|
|
101
|
+
@players = RGame::Engine::Players.new(
|
|
102
|
+
Array.new(players) do |id|
|
|
103
|
+
RGame::Engine::Player.new(id: id, device: id.zero? ? device : nil,
|
|
104
|
+
input_map: input_map)
|
|
105
|
+
end
|
|
106
|
+
)
|
|
107
|
+
@presentation = RGame::Engine::Presentation.new(width: width, height: height,
|
|
108
|
+
mode: scale_mode)
|
|
109
|
+
@presentation.fit(self.width, self.height)
|
|
110
|
+
@viewports = RGame::Engine::Viewports.new(@players, width: @presentation.width,
|
|
111
|
+
height: @presentation.height)
|
|
112
|
+
@debug = RGame::Engine::DebugOverlay.new
|
|
113
|
+
@dirty = true
|
|
58
114
|
|
|
59
115
|
install_asset_loaders
|
|
116
|
+
load_locales(locales)
|
|
60
117
|
end
|
|
61
118
|
|
|
119
|
+
# The player registry, also reachable from any node as
|
|
120
|
+
# `node.system(RGame::Engine::Players)` — which is how a scene gets at a
|
|
121
|
+
# camera to follow, without anything being threaded into its constructor.
|
|
122
|
+
#
|
|
123
|
+
# One player exists from the start, so a single-player game never mentions
|
|
124
|
+
# players at all: it is `players.primary` that an unowned node reads from,
|
|
125
|
+
# and `players.primary.camera` that a scene points at its hero.
|
|
126
|
+
attr_reader :players
|
|
127
|
+
|
|
128
|
+
# How the screen is divided. Reachable as `node.system(RGame::Engine::Viewports)`,
|
|
129
|
+
# which is how a cutscene deep in a scene collapses the split without
|
|
130
|
+
# anything being handed to it.
|
|
131
|
+
attr_reader :viewports
|
|
132
|
+
|
|
62
133
|
# Brings the tree live and runs until the window closes.
|
|
63
134
|
#
|
|
64
135
|
# The root gets this object as its `context`, which is how a node deep in
|
|
@@ -66,8 +137,13 @@ module RGame
|
|
|
66
137
|
# anything being threaded through its constructor.
|
|
67
138
|
def start
|
|
68
139
|
@root.context = self
|
|
69
|
-
@root.
|
|
140
|
+
@root.add_component(@players)
|
|
141
|
+
@root.add_component(@viewports)
|
|
142
|
+
@audio_director = Engine::AudioDirector.new(audio).subscribe
|
|
143
|
+
@root.enter_tree
|
|
70
144
|
run
|
|
145
|
+
ensure
|
|
146
|
+
@audio_director&.unsubscribe
|
|
71
147
|
end
|
|
72
148
|
|
|
73
149
|
# One fixed simulation tick. `dt` is always the engine's fixed step, so the
|
|
@@ -86,37 +162,103 @@ module RGame
|
|
|
86
162
|
# read identical state, and the edge lands on the first of them — one press,
|
|
87
163
|
# one `pressed?`, which is what a caller means.
|
|
88
164
|
def update(dt)
|
|
89
|
-
@
|
|
165
|
+
@players.poll(@input)
|
|
166
|
+
@root.control(@players)
|
|
90
167
|
@root.update(dt)
|
|
91
|
-
@root.sweep_freed
|
|
168
|
+
@root.sweep_freed
|
|
92
169
|
@dirty = true
|
|
93
170
|
end
|
|
94
171
|
|
|
95
172
|
# Only the simulation advancing makes the frame stale. While the overlay is
|
|
96
173
|
# up, redraw anyway, so its numbers stay live even when nothing is moving.
|
|
97
|
-
def needs_redraw? = @dirty || @
|
|
174
|
+
def needs_redraw? = @dirty || @debug.visible?
|
|
98
175
|
|
|
176
|
+
# The tree is drawn once, with the whole window as its view. Screen-space
|
|
177
|
+
# content — a HUD, a menu, a title card — lands there and is drawn exactly
|
|
178
|
+
# once, as it always was.
|
|
179
|
+
#
|
|
180
|
+
# **World content multiplies inside the tree, not here.** An
|
|
181
|
+
# RGame::Engine::WorldView draws its subtree once per viewport, clipping and
|
|
182
|
+
# translating for each, so where the world begins is the game's choice
|
|
183
|
+
# rather than a shape the platform imposes. That is also what keeps
|
|
184
|
+
# `node.root` meaning the game's own root: nothing is inserted above it.
|
|
99
185
|
def draw
|
|
100
|
-
@
|
|
101
|
-
@
|
|
186
|
+
@viewports.refresh
|
|
187
|
+
if @presentation.scaled?
|
|
188
|
+
presented { draw_tree }
|
|
189
|
+
else
|
|
190
|
+
draw_tree
|
|
191
|
+
end
|
|
102
192
|
@dirty = false
|
|
103
193
|
end
|
|
104
194
|
|
|
195
|
+
# How `width` and `height` are mapped onto the window: `:disabled`,
|
|
196
|
+
# `:stretch`, `:letterbox` or `:integer`. See RGame::Engine::Presentation.
|
|
197
|
+
def scale_mode = @presentation.mode
|
|
198
|
+
|
|
199
|
+
# Switchable while the game runs, for a settings screen.
|
|
200
|
+
#
|
|
201
|
+
# The viewports are resized as well as the presentation refitted, because
|
|
202
|
+
# the two modes disagree about what the logical size *is*: leaving `:integer`
|
|
203
|
+
# for `:disabled` turns a fixed 640x480 back into the window's own size, and
|
|
204
|
+
# a viewport still holding the old one would lay out into a corner.
|
|
205
|
+
def scale_mode=(mode)
|
|
206
|
+
@presentation.mode = mode
|
|
207
|
+
@viewports.resize(@presentation.width, @presentation.height)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# The window changed size, so the presentation is refitted and every rect and
|
|
211
|
+
# camera clamp follows. Under a scaling mode the logical size does not move,
|
|
212
|
+
# so the viewports are handed the same numbers again and only the transform
|
|
213
|
+
# in `draw` changes — which is the whole point of the mode.
|
|
214
|
+
def resize(width, height)
|
|
215
|
+
@presentation.fit(width, height)
|
|
216
|
+
@viewports.resize(@presentation.width, @presentation.height)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Hot-plug is bookkeeping, not seating: a controller arriving becomes a
|
|
220
|
+
# device the registry watches, and it is someone *using* it that gives it to
|
|
221
|
+
# a player. Leaving takes it back off whoever had it.
|
|
222
|
+
def gamepad_connected(slot) = @players.device_connected(slot)
|
|
223
|
+
def gamepad_disconnected(slot) = @players.device_disconnected(slot)
|
|
224
|
+
|
|
225
|
+
# The two development keys, both function keys on purpose: **Escape is
|
|
226
|
+
# deliberately not bound here**, because it is the natural `cancel`/`back`
|
|
227
|
+
# button for a game's own menus, and a debug shortcut has no business taking
|
|
228
|
+
# the one key every player expects to close a dialog. F1 shows the overlay,
|
|
229
|
+
# F2 quits.
|
|
105
230
|
def button_down(id)
|
|
106
|
-
close if id == Controls::
|
|
107
|
-
@
|
|
231
|
+
close if id == Controls::KEY_F2
|
|
232
|
+
@debug.toggle if id == Controls::KEY_F1
|
|
108
233
|
end
|
|
109
234
|
|
|
110
235
|
private
|
|
111
236
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
237
|
+
def draw_tree
|
|
238
|
+
@root.draw(@renderer, @viewports.screen)
|
|
239
|
+
@debug.draw(@renderer, @viewports.screen, fps)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def presented(&)
|
|
243
|
+
@renderer.clipped(@presentation.offset_x, @presentation.offset_y,
|
|
244
|
+
(@presentation.width * @presentation.scale_x).round,
|
|
245
|
+
(@presentation.height * @presentation.scale_y).round) do
|
|
246
|
+
@renderer.translated(@presentation.offset_x, @presentation.offset_y) do
|
|
247
|
+
@renderer.scaled(@presentation.scale_x, @presentation.scale_y, &)
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def load_locales(directory)
|
|
253
|
+
assets.glob(File.join(directory, '**', '*.yml')).each { |path| assets.locale(path) }
|
|
254
|
+
RGame::Engine::I18n.locale = RGame::Engine::I18n.choose(RGame::Core.preferred_locales)
|
|
255
|
+
end
|
|
256
|
+
|
|
119
257
|
def install_asset_loaders
|
|
258
|
+
assets.add_loader(:locale) do |path|
|
|
259
|
+
RGame::Engine::I18n.load(File.read(path), source: path)
|
|
260
|
+
end
|
|
261
|
+
|
|
120
262
|
game = self
|
|
121
263
|
assets.add_loader(:tilemap) do |path|
|
|
122
264
|
map, image_path = RGame::Engine::TileMap.load(path)
|