Class: RGame::Engine::Players

Inherits:
Component show all
Extended by:
Signal::DSL
Includes:
Enumerable
Defined in:
lib/rgame/engine/players.rb

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

Controls =
RGame::Util::Controls

Instance Attribute Summary collapse

Attributes inherited from Component

#node

Instance Method Summary collapse

Methods included from Signal::DSL

signal

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_joinsObject

Returns the value of attribute accepting_joins.



60
61
62
# File 'lib/rgame/engine/players.rb', line 60

def accepting_joins
  @accepting_joins
end

#listObject (readonly)

Returns the value of attribute list.



59
60
61
# File 'lib/rgame/engine/players.rb', line 59

def list
  @list
end

#on_unassigned_inputObject

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_countObject



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

#eachObject



79
# File 'lib/rgame/engine/players.rb', line 79

def each(&) = @list.each(&)

#each_activeObject

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

#primaryObject

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