Class: RGame::Engine::Players
Overview
Who is playing, as a root-scoped system.
players = node.system(Players)
players.primary.camera
players.each_active { |player| ... }
A Component on the root node, so any node reaches it by walking the tree rather than having it threaded through a constructor — the same shape CollisionWorld and TileWorld use (see docs/api/systems.md).
It owns the list, polls every player's mapper once per tick, and decides who a newly used controller belongs to. It does not own the screen rects — those come from the layout, because they depend on how many players are active and change without the players doing so.
Seats, and how a device comes to occupy one
Every seat exists from the start; the unfilled ones are inactive and draw no viewport. So the number of seats is also the maximum number of players, rather than a separate cap that could disagree with the list.
A device is seated when someone uses it, not when it is plugged in. A connect says something about hardware; seating a player creates a camera, a viewport and a screen split, and that should follow a statement of intent. Seating on connect drops a pad a solo player plugs in (no seat is free), splits the screen when a spare pad wakes up, and cannot be refused during a cutscene.
players.on_unassigned_input = :join # :join | :takeover | :ignore
players.accepting_joins = false # temporarily refuse either
:join— a press on an unassigned device fills the next free seat. Couch co-op, and the default when a game asks for more than one seat.:takeover— it becomes the primary player's device instead. Single player, where picking up a controller is not a second person arriving, and the default when there is one seat.:ignore— the game seats devices itself, with #seat.
The trigger is a ui_confirm press, read through the map of whoever
would receive the device. One action rather than "any input", because a
stick resting slightly off centre must never seat a player, and an edge
rather than held so one press does one thing.
Constant Summary collapse
Instance Attribute Summary collapse
-
#accepting_joins ⇒ Object
Returns the value of attribute accepting_joins.
-
#list ⇒ Object
readonly
Returns the value of attribute list.
-
#on_unassigned_input ⇒ Object
Returns the value of attribute on_unassigned_input.
Attributes inherited from Component
Instance Method Summary collapse
- #[](id) ⇒ Object
-
#actions_for(player) ⇒ Object
The input a node owned by
playershould read this tick. - #active_count ⇒ Object
- #add(player) ⇒ Object
-
#device_connected(slot) ⇒ Object
A controller arrived in a slot.
-
#device_disconnected(slot) ⇒ Object
A controller left its slot.
- #each ⇒ Object
-
#each_active ⇒ Object
Players with a device driving them.
-
#initialize(players = []) ⇒ Players
constructor
A new instance of Players.
-
#poll(backend) ⇒ Object
Every player's input for this tick, in one call.
-
#primary ⇒ Object
The player a single-player game means, and the one an unowned node reads from.
-
#seat(device) ⇒ Object
Give
deviceto whoever should have it, and say who that was.
Methods included from Signal::DSL
Methods inherited from Component
#context, #control, #draw, #on_attach, #on_detach, #sweep_freed, #update
Constructor Details
#initialize(players = []) ⇒ Players
Returns a new instance of Players.
62 63 64 65 66 67 68 69 70 71 72 |
# File 'lib/rgame/engine/players.rb', line 62 def initialize(players = []) super() @list = players # One seat means there is no second player to become, so an unassigned # device is that player picking up a controller. More than one means the # game expects company. @on_unassigned_input = players.size > 1 ? :join : :takeover @accepting_joins = true @connected = [] @confirm_held = {} end |
Instance Attribute Details
#accepting_joins ⇒ Object
Returns the value of attribute accepting_joins.
60 61 62 |
# File 'lib/rgame/engine/players.rb', line 60 def accepting_joins @accepting_joins end |
#list ⇒ Object (readonly)
Returns the value of attribute list.
59 60 61 |
# File 'lib/rgame/engine/players.rb', line 59 def list @list end |
#on_unassigned_input ⇒ Object
Returns the value of attribute on_unassigned_input.
60 61 62 |
# File 'lib/rgame/engine/players.rb', line 60 def on_unassigned_input @on_unassigned_input end |
Instance Method Details
#[](id) ⇒ Object
87 |
# File 'lib/rgame/engine/players.rb', line 87 def [](id) = @list.find { |player| player.id == id } |
#actions_for(player) ⇒ Object
The input a node owned by player should read this tick.
Nobody in particular means the primary player, which is what makes the single-player path free: no node claims ownership, every node resolves to nil, and every nil resolves to the one player there is. hot-path
100 101 102 103 104 105 |
# File 'lib/rgame/engine/players.rb', line 100 def actions_for(player) owner = player || primary raise 'no players are registered, so nothing can read input' if owner.nil? owner.actions end |
#active_count ⇒ Object
85 |
# File 'lib/rgame/engine/players.rb', line 85 def active_count = @list.count(&:active?) |
#add(player) ⇒ Object
89 90 91 92 |
# File 'lib/rgame/engine/players.rb', line 89 def add(player) @list << player player end |
#device_connected(slot) ⇒ Object
A controller arrived in a slot. Recorded, not seated: this is what makes the slot scannable, and someone using it is what seats it.
122 123 124 125 |
# File 'lib/rgame/engine/players.rb', line 122 def device_connected(slot) @connected << slot unless @connected.include?(slot) self end |
#device_disconnected(slot) ⇒ Object
A controller left its slot. Whoever was on it loses it; their camera, bindings and UI stay exactly as they were, so plugging back in and pressing confirm resumes rather than restarts.
Under :takeover there is no second player to become, so the seat falls
back to the keyboard rather than the game going dead in someone's hands.
133 134 135 136 137 138 139 |
# File 'lib/rgame/engine/players.rb', line 133 def device_disconnected(slot) @connected.delete(slot) device = Controls.gamepad(slot) seated = @list.find { |player| player.device == device } seated&.device = @on_unassigned_input == :takeover ? Controls::KEYBOARD : nil seated end |
#each ⇒ Object
79 |
# File 'lib/rgame/engine/players.rb', line 79 def each(&) = @list.each(&) |
#each_active ⇒ Object
Players with a device driving them. An empty seat waiting for a
controller is in list but not here, so a viewport loop skips it.
83 |
# File 'lib/rgame/engine/players.rb', line 83 def each_active(&) = @list.select(&:active?).each(&) |
#poll(backend) ⇒ Object
Every player's input for this tick, in one call. Each has their own mapper and their own previous-frame state, so one player's press cannot consume another's edge. Then the devices nobody holds are checked for someone starting to use one. Here rather than in a hot-plug hook because a press is a per-tick idea, and this is the one place that already has the backend and runs once a tick.
114 115 116 117 118 |
# File 'lib/rgame/engine/players.rb', line 114 def poll(backend) @list.each { |player| player.poll(backend) } admit(backend) self end |
#primary ⇒ Object
The player a single-player game means, and the one an unowned node reads from. Always present: a game with no players declared still has this one, which is what keeps single-player free of ceremony.
77 |
# File 'lib/rgame/engine/players.rb', line 77 def primary = @list.first |
#seat(device) ⇒ Object
Give device to whoever should have it, and say who that was. The join
path's own last step, and the one call a game running :ignore uses to
seat devices on its own terms.
Refused while accepting_joins is false — which covers taking over as
well as joining, since both change who is holding what.
147 148 149 150 151 152 153 154 155 156 |
# File 'lib/rgame/engine/players.rb', line 147 def seat(device) return nil unless @accepting_joins player = candidate return nil if player.nil? player.device = device on_joined_signal.emit(player) player end |