Class: RGame::Game
- Inherits:
-
Core::App
- Object
- Core::App
- RGame::Game
- Defined in:
- lib/rgame/game.rb
Overview
The entry point of a game, and the one class that knows both halves.
class HelloScene < RGame::Engine::Node2D
def on_draw(renderer) = renderer.text('Hello world!', 250, 200)
end
RGame::Game.new(root: HelloScene.new, caption: 'Hello').start
A complete game is a root node plus that. Game assembles the pieces
around it — the window and its loop, the renderer, the asset manager, the
sound device, the input mapper, the debug overlay — and drives the root
node once per tick.
Why this class is allowed to name both layers
RGame::Engine holds game concepts and may not name RGame::Core;
RGame::Core owns handles and may not know Engine exists. Two RuboCop cops
say so. Something still has to introduce them, and this is that
something — see CLAUDE.md, "The rule points both ways". Keeping the
introduction to one file is what makes the rule checkable everywhere else,
so wiring belongs here and only here.
The tile-map loader is the clearest case. Parsing a .tmx is Engine's job
and drawing one is Core's, and neither may call the other, so Game
installs the loader that joins them.
The loop
Game is an App, so it inherits the fixed-timestep loop rather than
running one. Its hooks do three things: sample input once per frame, drive
the root once per tick, and draw. Nothing here counts steps or measures
time — frame_loop.c does that, and update is called once per whole tick.
Constant Summary collapse
Instance Attribute Summary collapse
-
#players ⇒ Object
readonly
The player registry, also reachable from any node as
node.system(RGame::Engine::Players)— which is how a scene gets at a camera to follow, without anything being threaded into its constructor. -
#renderer ⇒ Object
readonly
Returns the value of attribute renderer.
-
#root ⇒ Object
readonly
Returns the value of attribute root.
-
#viewports ⇒ Object
readonly
How the screen is divided.
Instance Method Summary collapse
-
#button_down(id) ⇒ Object
The two development keys, both function keys on purpose: Escape is deliberately not bound here, because it is the natural
cancel/backbutton for a game's own menus, and a debug shortcut has no business taking the one key every player expects to close a dialog. -
#draw ⇒ Object
The tree is drawn once, with the whole window as its view.
-
#gamepad_connected(slot) ⇒ Object
Hot-plug is bookkeeping, not seating: a controller arriving becomes a device the registry watches, and it is someone using it that gives it to a player.
- #gamepad_disconnected(slot) ⇒ Object
-
#initialize(root:, width: WIDTH, height: HEIGHT, caption: 'RGame', media_root: 'media', input_map: nil, device: Controls::KEYBOARD, players: 1, input: nil) ⇒ Game
constructor
input_map:is what physical inputs mean — one entry per action, naming ids from RGame::Util::Controls. -
#needs_redraw? ⇒ Boolean
Only the simulation advancing makes the frame stale.
-
#resize(width, height) ⇒ Object
The window changed size, so every rect and every camera clamp does too.
-
#start ⇒ Object
Brings the tree live and runs until the window closes.
-
#update(dt) ⇒ Object
One fixed simulation tick.
Constructor Details
#initialize(root:, width: WIDTH, height: HEIGHT, caption: 'RGame', media_root: 'media', input_map: nil, device: Controls::KEYBOARD, players: 1, input: nil) ⇒ Game
input_map: is what physical inputs mean — one entry per action, naming
ids from RGame::Util::Controls. It is merged over the universal UI set, so
ui_confirm and friends work whether or not a game declares them, and it
defaults to RGame::Engine::InputMap::DEFAULT_ACTIONS, so a game wanting
eight-way movement and a fire button declares nothing.
device: is which device drives it — the keyboard, or
Controls.gamepad(slot) for a controller.
input: overrides the input backend. It exists so a harness can drive a
game from a script instead of from hardware — see tools/drive_example.rb,
and CLAUDE.md's "The examples are the acceptance test for wiring", which
is why driving one has to be possible at all. A game passes nothing and
gets the real thing.
players: is how many seats the game has, and therefore the most people
who can play it. Player 0 starts on device:; the rest start empty and
are filled when someone uses a controller — see RGame::Engine::Players for
why that is a press rather than a plug.
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
# File 'lib/rgame/game.rb', line 66 def initialize(root:, width: WIDTH, height: HEIGHT, caption: 'RGame', media_root: 'media', input_map: nil, device: Controls::KEYBOARD, players: 1, input: nil) super(width: width, height: height, caption: caption, media_root: media_root) @root = root @renderer = RGame::Core::Renderer.new(self) @input = input || RGame::Core::Input.new(self) @players = RGame::Engine::Players.new( Array.new(players) do |id| RGame::Engine::Player.new(id: id, device: id.zero? ? device : nil, input_map: input_map) end ) @viewports = RGame::Engine::Viewports.new(@players, width: width, height: height) @debug = RGame::Engine::DebugOverlay.new # always wired up; F1 reveals it @dirty = true # draw the first frame install_asset_loaders end |
Instance Attribute Details
#players ⇒ Object (readonly)
The player registry, also reachable from any node as
node.system(RGame::Engine::Players) — which is how a scene gets at a
camera to follow, without anything being threaded into its constructor.
One player exists from the start, so a single-player game never mentions
players at all: it is players.primary that an unowned node reads from,
and players.primary.camera that a scene points at its hero.
94 95 96 |
# File 'lib/rgame/game.rb', line 94 def players @players end |
#renderer ⇒ Object (readonly)
Returns the value of attribute renderer.
46 47 48 |
# File 'lib/rgame/game.rb', line 46 def renderer @renderer end |
#root ⇒ Object (readonly)
Returns the value of attribute root.
46 47 48 |
# File 'lib/rgame/game.rb', line 46 def root @root end |
#viewports ⇒ Object (readonly)
How the screen is divided. Reachable as node.system(RGame::Engine::Viewports),
which is how a cutscene deep in a scene collapses the split without
anything being handed to it.
99 100 101 |
# File 'lib/rgame/game.rb', line 99 def @viewports end |
Instance Method Details
#button_down(id) ⇒ Object
The two development keys, both function keys on purpose: Escape is
deliberately not bound here, because it is the natural cancel/back
button for a game's own menus, and a debug shortcut has no business taking
the one key every player expects to close a dialog. F1 shows the overlay,
F2 quits.
175 176 177 178 |
# File 'lib/rgame/game.rb', line 175 def (id) close if id == Controls::KEY_F2 @debug.toggle if id == Controls::KEY_F1 end |
#draw ⇒ Object
The tree is drawn once, with the whole window as its view. Screen-space content — a HUD, a menu, a title card — lands there and is drawn exactly once, as it always was.
World content multiplies inside the tree, not here. An
RGame::Engine::WorldView draws its subtree once per viewport, clipping and
translating for each, so where the world begins is the game's choice
rather than a shape the platform imposes. That is also what keeps
node.root meaning the game's own root: nothing is inserted above it.
154 155 156 157 158 159 |
# File 'lib/rgame/game.rb', line 154 def draw @viewports.refresh # rects from the layout, then reclamp every camera @root.draw(@renderer, @viewports.screen) @debug.draw(@renderer, @viewports.screen, fps) # last, so it layers on top @dirty = false end |
#gamepad_connected(slot) ⇒ Object
Hot-plug is bookkeeping, not seating: a controller arriving becomes a device the registry watches, and it is someone using it that gives it to a player. Leaving takes it back off whoever had it.
167 |
# File 'lib/rgame/game.rb', line 167 def gamepad_connected(slot) = @players.device_connected(slot) |
#gamepad_disconnected(slot) ⇒ Object
168 |
# File 'lib/rgame/game.rb', line 168 def gamepad_disconnected(slot) = @players.device_disconnected(slot) |
#needs_redraw? ⇒ Boolean
Only the simulation advancing makes the frame stale. While the overlay is up, redraw anyway, so its numbers stay live even when nothing is moving.
143 |
# File 'lib/rgame/game.rb', line 143 def needs_redraw? = @dirty || @debug.visible? |
#resize(width, height) ⇒ Object
The window changed size, so every rect and every camera clamp does too.
162 |
# File 'lib/rgame/game.rb', line 162 def resize(width, height) = @viewports.resize(width, height) |
#start ⇒ Object
Brings the tree live and runs until the window closes.
The root gets this object as its context, which is how a node deep in
the tree reaches the asset manager (node.root.context.assets) without
anything being threaded through its constructor.
106 107 108 109 110 111 112 113 114 |
# File 'lib/rgame/game.rb', line 106 def start @root.context = self # Root-scoped systems, mounted before the tree comes alive so that an # on_add anywhere in it can already resolve node.system(...) for either. @root.add_component(@players) @root.add_component(@viewports) @root.enter_tree # components attach, then on_add run end |
#update(dt) ⇒ Object
One fixed simulation tick. dt is always the engine's fixed step, so the
tree never sees variable frame time.
Input is polled here, per tick, not in frame_begin per frame. That
is not where it started, and the reason is edge detection: pressed? is
"held now, not held at the previous poll", so whatever polls decides what
a press is. frame_begin runs once per rendered frame, and a loop that
renders faster than it simulates runs it many times between two ticks —
each one shifting the previous state, so the press is consumed by a poll
no tick ever reads. Menus stop responding, and only on fast machines.
Polling per tick costs nothing extra and loses nothing: the C layer
snapshots the keyboard once per frame, so several ticks inside one frame
read identical state, and the edge lands on the first of them — one press,
one pressed?, which is what a caller means.
131 132 133 134 135 136 137 138 139 |
# File 'lib/rgame/game.rb', line 131 def update(dt) @players.poll(@input) # The registry, not one player's snapshot: each node resolves the actions # of whoever owns it, and a node that claims nobody gets the primary. @root.control(@players) @root.update(dt) @root.sweep_freed # flush queue_free'd nodes outside the update traversal @dirty = true end |