rgame 0.1.0 → 0.2.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 +94 -0
- data/README.md +130 -233
- data/docs/api/README.md +116 -69
- data/docs/api/assets.md +11 -12
- data/docs/api/components.md +58 -34
- data/docs/api/drawing.md +77 -9
- data/docs/api/game.md +34 -13
- data/docs/api/input.md +232 -51
- data/docs/api/scene_graph.md +242 -15
- data/docs/api/systems.md +20 -0
- data/docs/api/toolbox.md +19 -15
- data/docs/api/ui.md +98 -0
- data/docs/api/values.md +32 -0
- data/ext/README.md +6 -5
- data/ext/rgame_core/app/app.c +182 -8
- data/ext/rgame_core/audio/audio.c +74 -0
- data/ext/rgame_core/example.rb +17 -6
- data/ext/rgame_core/extconf.rb +52 -24
- 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 +113 -3
- data/ext/rgame_core/input/gamepad.c +57 -3
- data/ext/rgame_core/ruby/core_ext.c +16 -0
- data/ext/rgame_core/ruby/renderer_ext.c +23 -0
- data/ext/rgame_util/color_ext.c +12 -3
- data/lib/rgame/core/app.rb +2 -0
- data/lib/rgame/core/input.rb +35 -41
- data/lib/rgame/core/recording.rb +3 -1
- data/lib/rgame/core/renderer.rb +76 -28
- data/lib/rgame/core/tile_map_renderer.rb +84 -55
- data/lib/rgame/engine/camera.rb +55 -10
- data/lib/rgame/engine/component.rb +11 -1
- data/lib/rgame/engine/components/animated_sprite.rb +9 -3
- data/lib/rgame/engine/components/camera_follow.rb +44 -0
- data/lib/rgame/engine/components/character_body.rb +25 -4
- data/lib/rgame/engine/components/sprite.rb +11 -1
- data/lib/rgame/engine/components/tile_world.rb +31 -18
- data/lib/rgame/engine/culling.rb +47 -0
- data/lib/rgame/engine/debug_overlay.rb +20 -9
- data/lib/rgame/engine/input/action_mapper.rb +101 -21
- data/lib/rgame/engine/input/actions.rb +69 -12
- data/lib/rgame/engine/input/input_map.rb +178 -0
- data/lib/rgame/engine/layout.rb +82 -0
- data/lib/rgame/engine/node2d.rb +205 -36
- data/lib/rgame/engine/player.rb +69 -0
- data/lib/rgame/engine/player_layer.rb +70 -0
- data/lib/rgame/engine/players.rb +212 -0
- data/lib/rgame/engine/scene/scene_stack.rb +25 -3
- data/lib/rgame/engine/spatial_hash.rb +17 -4
- data/lib/rgame/engine/tile_map_layer.rb +84 -0
- data/lib/rgame/engine/ui/menu.rb +115 -0
- data/lib/rgame/engine/ui/menu_item.rb +84 -0
- data/lib/rgame/engine/view.rb +76 -0
- data/lib/rgame/engine/viewports.rb +174 -0
- data/lib/rgame/engine/world_view.rb +70 -0
- data/lib/rgame/engine.rb +13 -1
- data/lib/rgame/game.rb +81 -11
- data/lib/rgame/util/controls.rb +117 -41
- data/lib/rgame/util/z.rb +133 -0
- data/lib/rgame/util.rb +1 -0
- data/lib/rgame/version.rb +1 -1
- metadata +26 -11
- data/lib/rgame/engine/camera_view.rb +0 -28
|
@@ -0,0 +1,174 @@
|
|
|
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
|
+
# One View per possible player for the world, one more each for their own
|
|
42
|
+
# screen space, and one for the whole window. Built once and mutated in
|
|
43
|
+
# place. See View: these are reused, never rebuilt.
|
|
44
|
+
@pool = []
|
|
45
|
+
@screen_pool = []
|
|
46
|
+
@views = []
|
|
47
|
+
@screen = View.new
|
|
48
|
+
refresh
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# The window changed size. Rects are recomputed from it on the next
|
|
52
|
+
# refresh, and every camera reclamps against its new rect — which is why
|
|
53
|
+
# a camera does not carry one.
|
|
54
|
+
def resize(width, height)
|
|
55
|
+
@width = width
|
|
56
|
+
@height = height
|
|
57
|
+
refresh
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def solo? = !@solo_camera.nil?
|
|
61
|
+
|
|
62
|
+
# The screen-space region belonging to `player`: the same rectangle their
|
|
63
|
+
# world view is drawn into, with **no camera**, so its contents are laid
|
|
64
|
+
# out against their own corner rather than the window's. Their HUD and
|
|
65
|
+
# their menus live here.
|
|
66
|
+
#
|
|
67
|
+
# `nil` when they have no region to draw into, which is two cases and one
|
|
68
|
+
# answer. An **empty seat** has no viewport at all. And while the split is
|
|
69
|
+
# **collapsed**, nobody has a half of the screen to own: a cutscene is
|
|
70
|
+
# everyone looking at one thing, so per-player UI has no place to be, and
|
|
71
|
+
# a game wanting something on screen through it draws in the global
|
|
72
|
+
# overlay band instead.
|
|
73
|
+
#
|
|
74
|
+
# Returning nil rather than an empty rectangle is deliberate: one check at
|
|
75
|
+
# the one caller that needs it beats every caller relying on a zero-sized
|
|
76
|
+
# clip happening to draw nothing.
|
|
77
|
+
def screen_for(player)
|
|
78
|
+
return nil if player.nil? || @solo_camera
|
|
79
|
+
|
|
80
|
+
world = @views.find { |view| view.player.equal?(player) }
|
|
81
|
+
return nil if world.nil?
|
|
82
|
+
|
|
83
|
+
pooled_screen(@players.list.index(player))
|
|
84
|
+
.set(world.x, world.y, world.width, world.height, player: player)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Collapse to a single screen-wide view through `camera`.
|
|
88
|
+
#
|
|
89
|
+
# The camera is **required**: promoting one player's would silently give
|
|
90
|
+
# everyone else their view, and deciding what is on screen is what a
|
|
91
|
+
# cutscene is for. A game points an ordinary Camera wherever it likes —
|
|
92
|
+
# with a CameraFollow on a cutscene actor, or its own component framing
|
|
93
|
+
# every player at once — and hands it here.
|
|
94
|
+
def solo!(camera)
|
|
95
|
+
raise ArgumentError, 'solo! needs a camera to look through' if camera.nil?
|
|
96
|
+
|
|
97
|
+
@pending = camera
|
|
98
|
+
self
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Back to one view per player.
|
|
102
|
+
def split! = @pending = :split
|
|
103
|
+
|
|
104
|
+
# Applies a pending mode change, then rebuilds the rects. Runs in the
|
|
105
|
+
# root's component phase, so a change requested during a tick lands on the
|
|
106
|
+
# next one.
|
|
107
|
+
def update(_dt)
|
|
108
|
+
apply_pending
|
|
109
|
+
refresh
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Recompute every rect from the current mode and window, and reclamp each
|
|
113
|
+
# camera against the rect it is about to be drawn into.
|
|
114
|
+
#
|
|
115
|
+
# Allocation-free once the pool has grown: Layout yields its rects rather
|
|
116
|
+
# than building them, and the Views are mutated in place.
|
|
117
|
+
def refresh
|
|
118
|
+
@screen.set(0, 0, @width, @height)
|
|
119
|
+
@solo_camera ? refresh_solo : refresh_split
|
|
120
|
+
@views.each { |view| view.camera&.resolve(view.width, view.height) }
|
|
121
|
+
self
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
private
|
|
125
|
+
|
|
126
|
+
def apply_pending
|
|
127
|
+
return if @pending.nil?
|
|
128
|
+
|
|
129
|
+
@solo_camera = @pending == :split ? nil : @pending
|
|
130
|
+
@pending = nil
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def refresh_solo
|
|
134
|
+
@views.clear
|
|
135
|
+
@views << pooled(0).set(0, 0, @width, @height, camera: @solo_camera)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def refresh_split
|
|
139
|
+
@views.clear
|
|
140
|
+
count = @players.active_count
|
|
141
|
+
return if count.zero?
|
|
142
|
+
|
|
143
|
+
index = 0
|
|
144
|
+
Layout.each_rect(count, @width, @height) do |i, x, y, w, h|
|
|
145
|
+
player = active_at(i)
|
|
146
|
+
@views << pooled(index).set(x, y, w, h, camera: player.camera, player: player)
|
|
147
|
+
index += 1
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# `each_active` is a filter, so it cannot be indexed without building an
|
|
152
|
+
# Array. Walking it costs nothing and this runs once per frame.
|
|
153
|
+
def active_at(index)
|
|
154
|
+
found = nil
|
|
155
|
+
i = 0
|
|
156
|
+
@players.each do |player|
|
|
157
|
+
next unless player.active?
|
|
158
|
+
|
|
159
|
+
found = player if i == index
|
|
160
|
+
i += 1
|
|
161
|
+
end
|
|
162
|
+
found
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def pooled(index)
|
|
166
|
+
@pool[index] ||= View.new
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def pooled_screen(index)
|
|
170
|
+
@screen_pool[index] ||= View.new
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
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 at their own absolute origin and never see a camera. This
|
|
22
|
+
# node supplies the view, so the same world serves one player or four with
|
|
23
|
+
# nothing below it changing — which is exactly what a camera owned by a node
|
|
24
|
+
# *inside* the world could not do, and why cameras belong to players.
|
|
25
|
+
#
|
|
26
|
+
# ## Only `draw` multiplies, and deliberately
|
|
27
|
+
#
|
|
28
|
+
# `control` and `update` still run **once** for this subtree, however many
|
|
29
|
+
# viewports draw it. That is what keeps simulation cost independent of player
|
|
30
|
+
# count, and it is also a safety property: a world node's `on_update` is
|
|
31
|
+
# where mutation belongs, so per-view updating would move an actor at twice
|
|
32
|
+
# the speed with two players — correctly in single player, silently wrong the
|
|
33
|
+
# moment somebody joins.
|
|
34
|
+
#
|
|
35
|
+
# It does make the standing "draw renders state" rule load-bearing rather
|
|
36
|
+
# than stylistic: a `draw` with a side effect now happens once per player,
|
|
37
|
+
# and an allocation in one costs that many times.
|
|
38
|
+
#
|
|
39
|
+
# A node that genuinely needs per-view work at draw time can read
|
|
40
|
+
# `system(Viewports).views` itself. Anything needing per-view state *over
|
|
41
|
+
# time* — a screen shake, a hit flash — is per-*player* rather than per-view,
|
|
42
|
+
# and belongs on that player's camera or their own subtree, which tick once.
|
|
43
|
+
class WorldView < Node2D
|
|
44
|
+
# World content, which is the default anyway — stated because this is the
|
|
45
|
+
# node that marks where world space begins, and a reader looking for
|
|
46
|
+
# "which band is the world in" should find the answer here.
|
|
47
|
+
def initialize(**)
|
|
48
|
+
super(band: :world, **)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Drawn once per viewport, so this overrides the whole of `draw` rather
|
|
52
|
+
# than just `draw_children`: the node's own visuals belong inside the
|
|
53
|
+
# viewport too, not once outside all of them.
|
|
54
|
+
#
|
|
55
|
+
# `view` is the screen-space view this node was reached with, and is
|
|
56
|
+
# deliberately ignored — a WorldView asks the system which viewports exist
|
|
57
|
+
# rather than being told, so a game can place one anywhere in the tree.
|
|
58
|
+
def draw(renderer, _view = nil)
|
|
59
|
+
viewports = system(Viewports)
|
|
60
|
+
viewports.views.each do |world_view|
|
|
61
|
+
renderer.clipped(world_view.x, world_view.y, world_view.width, world_view.height) do
|
|
62
|
+
renderer.translated(world_view.offset_x, world_view.offset_y) do
|
|
63
|
+
super(renderer, world_view)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
data/lib/rgame/engine.rb
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
# three-layer rule allows and this file therefore has to declare.
|
|
20
20
|
require_relative 'util'
|
|
21
21
|
|
|
22
|
+
require_relative 'engine/culling'
|
|
22
23
|
require_relative 'engine/matrix'
|
|
23
24
|
require_relative 'engine/signal'
|
|
24
25
|
require_relative 'engine/node2d'
|
|
@@ -26,7 +27,12 @@ require_relative 'engine/component'
|
|
|
26
27
|
require_relative 'engine/animation_set'
|
|
27
28
|
require_relative 'engine/animator'
|
|
28
29
|
require_relative 'engine/camera'
|
|
29
|
-
require_relative 'engine/
|
|
30
|
+
require_relative 'engine/layout'
|
|
31
|
+
require_relative 'engine/view'
|
|
32
|
+
require_relative 'engine/world_view'
|
|
33
|
+
require_relative 'engine/player_layer'
|
|
34
|
+
require_relative 'engine/ui/menu_item'
|
|
35
|
+
require_relative 'engine/ui/menu'
|
|
30
36
|
require_relative 'engine/path'
|
|
31
37
|
require_relative 'engine/timer'
|
|
32
38
|
require_relative 'engine/tileset'
|
|
@@ -46,8 +52,12 @@ require_relative 'engine/debug_overlay'
|
|
|
46
52
|
require_relative 'engine/body'
|
|
47
53
|
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
57
|
require_relative 'engine/input/player_controller'
|
|
58
|
+
require_relative 'engine/player'
|
|
59
|
+
require_relative 'engine/players'
|
|
60
|
+
require_relative 'engine/viewports'
|
|
51
61
|
require_relative 'engine/scene/scene_stack'
|
|
52
62
|
require_relative 'engine/components/velocity'
|
|
53
63
|
require_relative 'engine/components/pool'
|
|
@@ -66,3 +76,5 @@ require_relative 'engine/components/character_body'
|
|
|
66
76
|
require_relative 'engine/components/player_controller'
|
|
67
77
|
require_relative 'engine/components/wander_controller'
|
|
68
78
|
require_relative 'engine/components/animated_sprite'
|
|
79
|
+
require_relative 'engine/components/camera_follow'
|
|
80
|
+
require_relative 'engine/tile_map_layer'
|
data/lib/rgame/game.rb
CHANGED
|
@@ -43,22 +43,61 @@ 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 tools/drive_example.rb,
|
|
59
|
+
# and CLAUDE.md's "The examples are the acceptance test for wiring", which
|
|
60
|
+
# is why driving one has to be possible at all. A game passes nothing and
|
|
61
|
+
# 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.
|
|
48
66
|
def initialize(root:, width: WIDTH, height: HEIGHT, caption: 'RGame',
|
|
49
|
-
media_root: 'media',
|
|
67
|
+
media_root: 'media', input_map: nil, device: Controls::KEYBOARD,
|
|
68
|
+
players: 1, input: nil)
|
|
50
69
|
super(width: width, height: height, caption: caption, media_root: media_root)
|
|
51
70
|
|
|
52
71
|
@root = root
|
|
53
72
|
@renderer = RGame::Core::Renderer.new(self)
|
|
54
|
-
@input = RGame::Core::Input.new(self)
|
|
55
|
-
@
|
|
56
|
-
|
|
73
|
+
@input = input || RGame::Core::Input.new(self)
|
|
74
|
+
@players = RGame::Engine::Players.new(
|
|
75
|
+
Array.new(players) do |id|
|
|
76
|
+
RGame::Engine::Player.new(id: id, device: id.zero? ? device : nil,
|
|
77
|
+
input_map: input_map)
|
|
78
|
+
end
|
|
79
|
+
)
|
|
80
|
+
@viewports = RGame::Engine::Viewports.new(@players, width: width, height: height)
|
|
81
|
+
@debug = RGame::Engine::DebugOverlay.new # always wired up; F1 reveals it
|
|
57
82
|
@dirty = true # draw the first frame
|
|
58
83
|
|
|
59
84
|
install_asset_loaders
|
|
60
85
|
end
|
|
61
86
|
|
|
87
|
+
# The player registry, also reachable from any node as
|
|
88
|
+
# `node.system(RGame::Engine::Players)` — which is how a scene gets at a
|
|
89
|
+
# camera to follow, without anything being threaded into its constructor.
|
|
90
|
+
#
|
|
91
|
+
# One player exists from the start, so a single-player game never mentions
|
|
92
|
+
# players at all: it is `players.primary` that an unowned node reads from,
|
|
93
|
+
# and `players.primary.camera` that a scene points at its hero.
|
|
94
|
+
attr_reader :players
|
|
95
|
+
|
|
96
|
+
# How the screen is divided. Reachable as `node.system(RGame::Engine::Viewports)`,
|
|
97
|
+
# which is how a cutscene deep in a scene collapses the split without
|
|
98
|
+
# anything being handed to it.
|
|
99
|
+
attr_reader :viewports
|
|
100
|
+
|
|
62
101
|
# Brings the tree live and runs until the window closes.
|
|
63
102
|
#
|
|
64
103
|
# The root gets this object as its `context`, which is how a node deep in
|
|
@@ -66,6 +105,10 @@ module RGame
|
|
|
66
105
|
# anything being threaded through its constructor.
|
|
67
106
|
def start
|
|
68
107
|
@root.context = self
|
|
108
|
+
# Root-scoped systems, mounted before the tree comes alive so that an
|
|
109
|
+
# on_add anywhere in it can already resolve node.system(...) for either.
|
|
110
|
+
@root.add_component(@players)
|
|
111
|
+
@root.add_component(@viewports)
|
|
69
112
|
@root.enter_tree # components attach, then on_add
|
|
70
113
|
run
|
|
71
114
|
end
|
|
@@ -86,7 +129,10 @@ module RGame
|
|
|
86
129
|
# read identical state, and the edge lands on the first of them — one press,
|
|
87
130
|
# one `pressed?`, which is what a caller means.
|
|
88
131
|
def update(dt)
|
|
89
|
-
@
|
|
132
|
+
@players.poll(@input)
|
|
133
|
+
# The registry, not one player's snapshot: each node resolves the actions
|
|
134
|
+
# of whoever owns it, and a node that claims nobody gets the primary.
|
|
135
|
+
@root.control(@players)
|
|
90
136
|
@root.update(dt)
|
|
91
137
|
@root.sweep_freed # flush queue_free'd nodes outside the update traversal
|
|
92
138
|
@dirty = true
|
|
@@ -94,17 +140,41 @@ module RGame
|
|
|
94
140
|
|
|
95
141
|
# Only the simulation advancing makes the frame stale. While the overlay is
|
|
96
142
|
# up, redraw anyway, so its numbers stay live even when nothing is moving.
|
|
97
|
-
def needs_redraw? = @dirty || @
|
|
143
|
+
def needs_redraw? = @dirty || @debug.visible?
|
|
98
144
|
|
|
145
|
+
# The tree is drawn once, with the whole window as its view. Screen-space
|
|
146
|
+
# content — a HUD, a menu, a title card — lands there and is drawn exactly
|
|
147
|
+
# once, as it always was.
|
|
148
|
+
#
|
|
149
|
+
# **World content multiplies inside the tree, not here.** An
|
|
150
|
+
# RGame::Engine::WorldView draws its subtree once per viewport, clipping and
|
|
151
|
+
# translating for each, so where the world begins is the game's choice
|
|
152
|
+
# rather than a shape the platform imposes. That is also what keeps
|
|
153
|
+
# `node.root` meaning the game's own root: nothing is inserted above it.
|
|
99
154
|
def draw
|
|
100
|
-
@
|
|
101
|
-
@
|
|
155
|
+
@viewports.refresh # rects from the layout, then reclamp every camera
|
|
156
|
+
@root.draw(@renderer, @viewports.screen)
|
|
157
|
+
@debug.draw(@renderer, @viewports.screen, fps) # last, so it layers on top
|
|
102
158
|
@dirty = false
|
|
103
159
|
end
|
|
104
160
|
|
|
161
|
+
# The window changed size, so every rect and every camera clamp does too.
|
|
162
|
+
def resize(width, height) = @viewports.resize(width, height)
|
|
163
|
+
|
|
164
|
+
# Hot-plug is bookkeeping, not seating: a controller arriving becomes a
|
|
165
|
+
# device the registry watches, and it is someone *using* it that gives it to
|
|
166
|
+
# a player. Leaving takes it back off whoever had it.
|
|
167
|
+
def gamepad_connected(slot) = @players.device_connected(slot)
|
|
168
|
+
def gamepad_disconnected(slot) = @players.device_disconnected(slot)
|
|
169
|
+
|
|
170
|
+
# The two development keys, both function keys on purpose: **Escape is
|
|
171
|
+
# deliberately not bound here**, because it is the natural `cancel`/`back`
|
|
172
|
+
# button for a game's own menus, and a debug shortcut has no business taking
|
|
173
|
+
# the one key every player expects to close a dialog. F1 shows the overlay,
|
|
174
|
+
# F2 quits.
|
|
105
175
|
def button_down(id)
|
|
106
|
-
close if id == Controls::
|
|
107
|
-
@
|
|
176
|
+
close if id == Controls::KEY_F2
|
|
177
|
+
@debug.toggle if id == Controls::KEY_F1
|
|
108
178
|
end
|
|
109
179
|
|
|
110
180
|
private
|
data/lib/rgame/util/controls.rb
CHANGED
|
@@ -10,8 +10,14 @@ module RGame
|
|
|
10
10
|
# layer and a game's own configuration name a physical input without
|
|
11
11
|
# touching RGame::Core, which they may not do:
|
|
12
12
|
#
|
|
13
|
-
#
|
|
14
|
-
# RGame::
|
|
13
|
+
# controls = RGame::Util::Controls
|
|
14
|
+
# map = RGame::Engine::InputMap.default.merge(
|
|
15
|
+
# fire: { buttons: [controls::KEY_SPACE, controls::PAD_A] }
|
|
16
|
+
# )
|
|
17
|
+
#
|
|
18
|
+
# This module is the **vocabulary** only. What an id *means* to a game — the
|
|
19
|
+
# binding table — is RGame::Engine::InputMap, one per player, because two
|
|
20
|
+
# players share a game's actions but not the buttons that trigger them.
|
|
15
21
|
#
|
|
16
22
|
# The same numbers exist as `#define`s in
|
|
17
23
|
# ext/rgame_core/include/rgame/core.h, because the C engine and the
|
|
@@ -20,17 +26,118 @@ module RGame
|
|
|
20
26
|
# by _Static_assert at compile time, and spec/rgame/util/controls_spec.rb
|
|
21
27
|
# parses that header and compares every value here against it.
|
|
22
28
|
module Controls
|
|
23
|
-
# --- Keyboard. Values are SDL scancodes
|
|
29
|
+
# --- Keyboard. Values are SDL scancodes, which name a physical *position*
|
|
30
|
+
# rather than a letter: KEY_A is the key marked A on a QWERTY board and Q
|
|
31
|
+
# on AZERTY. A game rebinding controls shows the player what their layout
|
|
32
|
+
# calls it; the engine only ever compares numbers. ---
|
|
33
|
+
#
|
|
34
|
+
# The set is what a Western keyboard can be relied on to have. No numpad
|
|
35
|
+
# (most laptops have none), no GUI/Windows/Command key, no print-screen
|
|
36
|
+
# cluster, and nothing whose position depends on the layout.
|
|
37
|
+
|
|
38
|
+
# Letters. Scancodes are physical *positions*, so KEY_A is the key marked A on
|
|
39
|
+
# a QWERTY board and Q on AZERTY.
|
|
40
|
+
KEY_A = 4
|
|
41
|
+
KEY_B = 5
|
|
42
|
+
KEY_C = 6
|
|
43
|
+
KEY_D = 7
|
|
44
|
+
KEY_E = 8
|
|
45
|
+
KEY_F = 9
|
|
46
|
+
KEY_G = 10
|
|
47
|
+
KEY_H = 11
|
|
48
|
+
KEY_I = 12
|
|
49
|
+
KEY_J = 13
|
|
50
|
+
KEY_K = 14
|
|
51
|
+
KEY_L = 15
|
|
52
|
+
KEY_M = 16
|
|
53
|
+
KEY_N = 17
|
|
54
|
+
KEY_O = 18
|
|
55
|
+
KEY_P = 19
|
|
56
|
+
KEY_Q = 20
|
|
57
|
+
KEY_R = 21
|
|
58
|
+
KEY_S = 22
|
|
59
|
+
KEY_T = 23
|
|
60
|
+
KEY_U = 24
|
|
61
|
+
KEY_V = 25
|
|
62
|
+
KEY_W = 26
|
|
63
|
+
KEY_X = 27
|
|
64
|
+
KEY_Y = 28
|
|
65
|
+
KEY_Z = 29
|
|
66
|
+
|
|
67
|
+
# Digits along the top row.
|
|
68
|
+
KEY_1 = 30
|
|
69
|
+
KEY_2 = 31
|
|
70
|
+
KEY_3 = 32
|
|
71
|
+
KEY_4 = 33
|
|
72
|
+
KEY_5 = 34
|
|
73
|
+
KEY_6 = 35
|
|
74
|
+
KEY_7 = 36
|
|
75
|
+
KEY_8 = 37
|
|
76
|
+
KEY_9 = 38
|
|
77
|
+
KEY_0 = 39
|
|
78
|
+
|
|
79
|
+
# Editing and whitespace.
|
|
24
80
|
KEY_RETURN = 40
|
|
25
81
|
KEY_ESCAPE = 41
|
|
82
|
+
KEY_BACKSPACE = 42
|
|
83
|
+
KEY_TAB = 43
|
|
26
84
|
KEY_SPACE = 44
|
|
85
|
+
|
|
86
|
+
# Punctuation, by position on a US board.
|
|
87
|
+
KEY_MINUS = 45
|
|
88
|
+
KEY_EQUALS = 46
|
|
89
|
+
KEY_LEFTBRACKET = 47
|
|
90
|
+
KEY_RIGHTBRACKET = 48
|
|
91
|
+
KEY_BACKSLASH = 49
|
|
92
|
+
KEY_SEMICOLON = 51
|
|
93
|
+
KEY_APOSTROPHE = 52
|
|
94
|
+
KEY_GRAVE = 53
|
|
95
|
+
KEY_COMMA = 54
|
|
96
|
+
KEY_PERIOD = 55
|
|
97
|
+
KEY_SLASH = 56
|
|
98
|
+
|
|
99
|
+
# Function row and caps lock.
|
|
100
|
+
KEY_CAPSLOCK = 57
|
|
27
101
|
KEY_F1 = 58
|
|
102
|
+
KEY_F2 = 59
|
|
103
|
+
KEY_F3 = 60
|
|
104
|
+
KEY_F4 = 61
|
|
105
|
+
KEY_F5 = 62
|
|
106
|
+
KEY_F6 = 63
|
|
107
|
+
KEY_F7 = 64
|
|
108
|
+
KEY_F8 = 65
|
|
109
|
+
KEY_F9 = 66
|
|
110
|
+
KEY_F10 = 67
|
|
111
|
+
KEY_F11 = 68
|
|
112
|
+
KEY_F12 = 69
|
|
113
|
+
|
|
114
|
+
# The navigation cluster.
|
|
115
|
+
KEY_INSERT = 73
|
|
116
|
+
KEY_HOME = 74
|
|
117
|
+
KEY_PAGEUP = 75
|
|
118
|
+
KEY_DELETE = 76
|
|
119
|
+
KEY_END = 77
|
|
120
|
+
KEY_PAGEDOWN = 78
|
|
121
|
+
|
|
122
|
+
# Arrows.
|
|
28
123
|
KEY_RIGHT = 79
|
|
29
124
|
KEY_LEFT = 80
|
|
30
125
|
KEY_DOWN = 81
|
|
31
126
|
KEY_UP = 82
|
|
32
127
|
|
|
33
|
-
#
|
|
128
|
+
# Modifiers. No GUI key: that is Windows on a PC and Command on a Mac, which
|
|
129
|
+
# is exactly the platform-specific territory this list stays out of.
|
|
130
|
+
KEY_LCTRL = 224
|
|
131
|
+
KEY_LSHIFT = 225
|
|
132
|
+
KEY_LALT = 226
|
|
133
|
+
KEY_RCTRL = 228
|
|
134
|
+
KEY_RSHIFT = 229
|
|
135
|
+
KEY_RALT = 230
|
|
136
|
+
|
|
137
|
+
# --- Gamepad buttons. The gamepad range plus SDL's own controller button
|
|
138
|
+
# number. The first fifteen are on every controller; MISC1, the paddles
|
|
139
|
+
# and TOUCHPAD are hardware the id space describes but most pads do not
|
|
140
|
+
# have, and read as never pressed on one that does not. ---
|
|
34
141
|
PAD_A = 4096
|
|
35
142
|
PAD_B = 4097
|
|
36
143
|
PAD_X = 4098
|
|
@@ -46,6 +153,12 @@ module RGame
|
|
|
46
153
|
PAD_DPAD_DOWN = 4108
|
|
47
154
|
PAD_DPAD_LEFT = 4109
|
|
48
155
|
PAD_DPAD_RIGHT = 4110
|
|
156
|
+
PAD_MISC1 = 4111
|
|
157
|
+
PAD_PADDLE1 = 4112
|
|
158
|
+
PAD_PADDLE2 = 4113
|
|
159
|
+
PAD_PADDLE3 = 4114
|
|
160
|
+
PAD_PADDLE4 = 4115
|
|
161
|
+
PAD_TOUCHPAD = 4116
|
|
49
162
|
|
|
50
163
|
# --- Analog axes. Their own small space: they are float-valued and read
|
|
51
164
|
# through a different call, so folding them into the button space would
|
|
@@ -65,43 +178,6 @@ module RGame
|
|
|
65
178
|
|
|
66
179
|
# The device id for a player slot: gamepad(0) is the first controller.
|
|
67
180
|
def self.gamepad(slot) = GAMEPAD_FIRST + slot
|
|
68
|
-
|
|
69
|
-
# --- Default bindings ---
|
|
70
|
-
#
|
|
71
|
-
# Symbolic action => physical input. Games override these to rebind; they
|
|
72
|
-
# are values, so a config screen can build its own table from the
|
|
73
|
-
# constants above and hand it to the input layer.
|
|
74
|
-
#
|
|
75
|
-
# Two button tables rather than one, because the same action is a
|
|
76
|
-
# different physical input per device class: :fire is the space bar on a
|
|
77
|
-
# keyboard and the A button on a pad.
|
|
78
|
-
DEFAULT_KEYBOARD = {
|
|
79
|
-
left: KEY_LEFT,
|
|
80
|
-
right: KEY_RIGHT,
|
|
81
|
-
up: KEY_UP,
|
|
82
|
-
down: KEY_DOWN,
|
|
83
|
-
confirm: KEY_RETURN,
|
|
84
|
-
fire: KEY_SPACE
|
|
85
|
-
}.freeze
|
|
86
|
-
|
|
87
|
-
DEFAULT_PAD = {
|
|
88
|
-
left: PAD_DPAD_LEFT,
|
|
89
|
-
right: PAD_DPAD_RIGHT,
|
|
90
|
-
up: PAD_DPAD_UP,
|
|
91
|
-
down: PAD_DPAD_DOWN,
|
|
92
|
-
confirm: PAD_A,
|
|
93
|
-
fire: PAD_A
|
|
94
|
-
}.freeze
|
|
95
|
-
|
|
96
|
-
# Analog axes exist only on pads, so there is one table.
|
|
97
|
-
DEFAULT_AXES = {
|
|
98
|
-
move_x: AXIS_LEFT_X,
|
|
99
|
-
move_y: AXIS_LEFT_Y,
|
|
100
|
-
aim_x: AXIS_RIGHT_X,
|
|
101
|
-
aim_y: AXIS_RIGHT_Y,
|
|
102
|
-
trigger_left: AXIS_TRIGGER_LEFT,
|
|
103
|
-
trigger_right: AXIS_TRIGGER_RIGHT
|
|
104
|
-
}.freeze
|
|
105
181
|
end
|
|
106
182
|
end
|
|
107
183
|
end
|