Class: Doom::Net::GameServer
- Inherits:
-
Object
- Object
- Doom::Net::GameServer
- Defined in:
- lib/doom/net/game_server.rb
Overview
The authoritative game server for large, chaotic sessions.
Peer-to-peer lockstep does not survive a conference: with mobile clients behind CGNAT they cannot reach each other, and lockstep makes everyone wait for the slowest, so the first person to close their laptop freezes the match. This server fixes both. Clients speak only to it (outbound UDP, so NAT is a non-issue), and it never waits: on every tic it takes whatever input has arrived, fills the gaps with neutral commands, runs the tic, and broadcasts the finalized result. A player whose phone dies simply stops acting -- nobody else is held up.
It is still deterministic underneath: it holds the one canonical world, every client runs the identical command stream, and joins and leaves are tic events (a deathmatch spawn draws the shared RNG, so it must happen on the same tic everywhere). Because it holds the canonical world, it can hand a late joiner a snapshot of the exact current state instead of making them replay the match from the start.
Nothing here blocks. poll drains the socket, advance runs whatever
tics are due against an injected clock; a host loops them.
Defined Under Namespace
Classes: Member
Constant Summary collapse
- TICRATE =
35- TIC_SECONDS =
1.0 / TICRATE
- FRAME_REDUNDANCY =
Recent frames repeated per packet against loss
6- CLIENT_TIMEOUT =
Drop a client silent this long
5.0
Instance Attribute Summary collapse
-
#clients ⇒ Object
readonly
Returns the value of attribute clients.
-
#tic ⇒ Object
readonly
Returns the value of attribute tic.
-
#transport ⇒ Object
readonly
Returns the value of attribute transport.
-
#world ⇒ Object
readonly
Returns the value of attribute world.
Instance Method Summary collapse
-
#advance(now = @clock.call, limit: 8) ⇒ Object
Run every tic whose deadline has passed, capping the catch-up so one long stall cannot make a single call run thousands of tics.
-
#initialize(world:, transport:, max_players: 16, clock: -> { Time.now }, client_timeout: CLIENT_TIMEOUT) ⇒ GameServer
constructor
A new instance of GameServer.
- #player_count ⇒ Object
-
#poll ⇒ Object
Drain and dispatch whatever has arrived.
-
#reap(now = @clock.call) ⇒ Object
Drop clients that have gone silent, so their stale bodies stop being simulated and stop holding a player slot.
Constructor Details
#initialize(world:, transport:, max_players: 16, clock: -> { Time.now }, client_timeout: CLIENT_TIMEOUT) ⇒ GameServer
Returns a new instance of GameServer.
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
# File 'lib/doom/net/game_server.rb', line 39 def initialize(world:, transport:, max_players: 16, clock: -> { Time.now }, client_timeout: CLIENT_TIMEOUT) @world = world @transport = transport @max_players = max_players @clock = clock @client_timeout = client_timeout @tic = world.leveltime @start_tic = world.leveltime @start_time = @clock.call @clients = {} # addr key => Client @by_id = {} # player_id => Client @inputs = Hash.new { |h, k| h[k] = {} } # tic => { player_id => Ticcmd } @pending_joins = [] # player ids to add on the next tic @pending_leaves = [] # player ids to remove on the next tic @frame_log = [] # recent finalized frames, for redundancy end |
Instance Attribute Details
#clients ⇒ Object (readonly)
Returns the value of attribute clients.
37 38 39 |
# File 'lib/doom/net/game_server.rb', line 37 def clients @clients end |
#tic ⇒ Object (readonly)
Returns the value of attribute tic.
37 38 39 |
# File 'lib/doom/net/game_server.rb', line 37 def tic @tic end |
#transport ⇒ Object (readonly)
Returns the value of attribute transport.
37 38 39 |
# File 'lib/doom/net/game_server.rb', line 37 def transport @transport end |
#world ⇒ Object (readonly)
Returns the value of attribute world.
37 38 39 |
# File 'lib/doom/net/game_server.rb', line 37 def world @world end |
Instance Method Details
#advance(now = @clock.call, limit: 8) ⇒ Object
Run every tic whose deadline has passed, capping the catch-up so one long stall cannot make a single call run thousands of tics. Returns how many ran.
80 81 82 83 84 85 86 87 88 |
# File 'lib/doom/net/game_server.rb', line 80 def advance(now = @clock.call, limit: 8) target = @start_tic + ((now - @start_time) * TICRATE).floor ran = 0 while @tic < target && ran < limit step_one_tic ran += 1 end ran end |
#player_count ⇒ Object
90 |
# File 'lib/doom/net/game_server.rb', line 90 def player_count = @world.players.size |
#poll ⇒ Object
Drain and dispatch whatever has arrived. Safe every loop iteration.
59 60 61 |
# File 'lib/doom/net/game_server.rb', line 59 def poll @transport.poll.each { |msg, host, port| handle(msg, host, port) } end |
#reap(now = @clock.call) ⇒ Object
Drop clients that have gone silent, so their stale bodies stop being simulated and stop holding a player slot. Called from the host loop. A leave is a tic event like a join, applied on the next tic.
66 67 68 69 70 71 72 73 74 75 |
# File 'lib/doom/net/game_server.rb', line 66 def reap(now = @clock.call) @clients.values.each do |c| next unless c.joined next if now - c.last_seen < @client_timeout @clients.delete(c.key) @by_id.delete(c.player_id) @pending_leaves << c.player_id end end |