Class: ClientEngine

Inherits:
Object
  • Object
show all
Defined in:
lib/game_2d/client_engine.rb

Overview

Server sends authoritative copy of GameSpace for tick T0. We store that, along with pending moves generated by our player, and pending moves for other players sent to us by the server. Then we calculate further ticks of action. These are predictions and might well be wrong.

When the user requests an action at T0, we delay it by 100ms (T6). We tell the server about it immediately, but advise it not to perform the action until T6 arrives. The server rebroadcasts this information to other players. Hopefully, everyone receives all players’ actions before T6.

We render one tick after another, 60 per second, the same speed at which the server calculates them. But because we may get out of sync, we also watch for full server updates at, e.g., T15. When we get a new full update, we can discard all information about older ticks. Anything we’ve calculated past the new update must now be recalculated, applying again whatever pending player actions we have heard about.

Constant Summary collapse

MAX_LEAD_TICKS =

If we haven’t received a full update from the server in this many ticks, stop guessing. We’re almost certainly wrong by this point.

30

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(game_window) ⇒ ClientEngine

Returns a new instance of ClientEngine.



32
33
34
35
36
37
# File 'lib/game_2d/client_engine.rb', line 32

def initialize(game_window)
  @game_window, @width, @height = game_window, 0, 0
  @spaces = {}
  @deltas = Hash.new {|h,tick| h[tick] = Array.new}
  @earliest_tick = @tick = @preprocessed = nil
end

Instance Attribute Details

#tickObject (readonly) Also known as: world_established?

Returns the value of attribute tick.



30
31
32
# File 'lib/game_2d/client_engine.rb', line 30

def tick
  @tick
end

Instance Method Details

#add_delta(delta) ⇒ Object



103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/game_2d/client_engine.rb', line 103

def add_delta(delta)
  at_tick = delta.delete :at_tick
  fail "Received delta without at_tick: #{delta.inspect}" unless at_tick
  if at_tick < @tick
    $stderr.puts "Received delta #{@tick - at_tick} ticks late"
    if at_tick <= @earliest_tick
      $stderr.puts "Discarding it - we've received registry sync at <#{@earliest_tick}>"
      return
    end
    # Invalidate old spaces that were generated without this information
    at_tick.upto(@tick) {|old_tick| @spaces.delete old_tick}
  end
  @deltas[at_tick] << delta
end

#add_entity(space, json) ⇒ Object



176
177
178
# File 'lib/game_2d/client_engine.rb', line 176

def add_entity(space, json)
  space << Serializable.from_json(json)
end

#add_npcs(space, npcs) ⇒ Object



168
169
170
171
172
173
174
# File 'lib/game_2d/client_engine.rb', line 168

def add_npcs(space, npcs)
  npcs.each do |json|
    on_create = json.delete :on_create
    space << (entity = Serializable.from_json(json))
    on_create.call(entity) if on_create
  end
end

#add_player(space, hash) ⇒ Object



151
152
153
154
155
156
# File 'lib/game_2d/client_engine.rb', line 151

def add_player(space, hash)
  player = Serializable.from_json(hash)
  puts "Added player #{player}"
  space << player
  player.registry_id
end

#add_players(space, players) ⇒ Object



158
159
160
# File 'lib/game_2d/client_engine.rb', line 158

def add_players(space, players)
  players.each {|json| add_player(space, json) }
end

#apply_deltas(at_tick) ⇒ Object



118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/game_2d/client_engine.rb', line 118

def apply_deltas(at_tick)
  space = space_at(at_tick)

  @deltas[at_tick].each do |hash|
    players = hash.delete :add_players
    add_players(space, players) if players

    doomed = hash.delete :delete_entities
    delete_entities(space, doomed) if doomed

    updated = hash.delete :update_entities
    update_entities(space, updated) if updated

    snap = hash.delete :snap_to_grid
    space.snap_to_grid(snap.to_sym) if snap

    npcs = hash.delete :add_npcs
    add_npcs(space, npcs) if npcs

    move = hash.delete :move
    player_id = hash.delete :player_id
    if move
      fail "No player_id sent with move #{move.inspect}" unless player_id
      apply_move(space, move, player_id.to_sym)
    end

    score_update = hash.delete :update_score
    update_score(space, score_update) if score_update

    $stderr.puts "Unprocessed deltas: #{hash.keys.join(', ')}" unless hash.empty?
  end
end

#apply_move(space, move, player_id) ⇒ Object



162
163
164
165
166
# File 'lib/game_2d/client_engine.rb', line 162

def apply_move(space, move, player_id)
  player = space[player_id]
  fail "No such player #{player_id}, can't apply #{move.inspect}" unless player
  player.add_move move
end

#create_initial_space(at_tick, highest_id) ⇒ Object



49
50
51
52
53
54
55
# File 'lib/game_2d/client_engine.rb', line 49

def create_initial_space(at_tick, highest_id)
  @earliest_tick = @tick = at_tick
  space = @spaces[@tick] = GameSpace.new(@game_window).
    establish_world(@world_name, @world_id, @width, @height)
  space.highest_id = highest_id
  space
end

#create_local_player(player_id) ⇒ Object



91
92
93
94
95
96
97
# File 'lib/game_2d/client_engine.rb', line 91

def create_local_player(player_id)
  old_player_id = @game_window.player_id
  fail "Already have player #{old_player_id}!?" if old_player_id

  @game_window.player_id = player_id
  puts "I am player #{player_id}"
end

#delete_entities(space, doomed) ⇒ Object



199
200
201
202
203
204
205
206
207
# File 'lib/game_2d/client_engine.rb', line 199

def delete_entities(space, doomed)
  doomed.each do |registry_id|
    dead = space[registry_id]
    next unless dead
    puts "Disconnected: #{dead}" if dead.is_a? Player
    space.doom dead
  end
  space.purge_doomed_entities
end

#establish_world(world, at_tick) ⇒ Object



39
40
41
42
43
44
45
# File 'lib/game_2d/client_engine.rb', line 39

def establish_world(world, at_tick)
  @world_name, @world_id = world[:world_name], world[:world_id]
  @width, @height = world[:cell_width], world[:cell_height]
  highest_id = world[:highest_id]
  create_initial_space(at_tick, highest_id)
  @preprocessed = at_tick
end

#player_idObject



99
100
101
# File 'lib/game_2d/client_engine.rb', line 99

def player_id
  @game_window.player_id
end

#spaceObject



87
88
89
# File 'lib/game_2d/client_engine.rb', line 87

def space
  @spaces[@tick]
end

#space_at(tick) ⇒ Object



57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/game_2d/client_engine.rb', line 57

def space_at(tick)
  return @spaces[tick] if @spaces[tick]

  fail "Can't create space at #{tick}; earliest space we know about is #{@earliest_tick}" if tick < @earliest_tick

  last_space = space_at(tick - 1)
  @spaces[tick] = new_space = GameSpace.new(@game_window).copy_from(last_space)

  apply_deltas(tick)
  new_space.update

  new_space
end

#sync_registry(server_registry, highest_id, at_tick) ⇒ Object

Discard anything we think we know, in favor of the registry we just got from the server



217
218
219
220
221
222
223
224
225
226
227
# File 'lib/game_2d/client_engine.rb', line 217

def sync_registry(server_registry, highest_id, at_tick)
  return unless world_established?
  @spaces.clear
  # Any older deltas are now irrelevant
  @earliest_tick.upto(at_tick - 1) {|old_tick| @deltas.delete old_tick}
  update_entities(create_initial_space(at_tick, highest_id), server_registry)

  # The server has given us a complete, finished frame.  Don't
  # create a new one until this one has been displayed once.
  @preprocessed = at_tick
end

#updateObject



71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/game_2d/client_engine.rb', line 71

def update
  return unless world_established?

  # Display the frame we received from the server as-is
  if @preprocessed == @tick
    @preprocessed = nil
    return space_at(@tick)
  end

  if @tick - @earliest_tick >= MAX_LEAD_TICKS
    $stderr.puts "Lost connection?  Running ahead of server?"
    return space_at(@tick)
  end
  space_at(@tick += 1)
end

#update_entities(space, updated) ⇒ Object

Returns the set of registry IDs updated or added



181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# File 'lib/game_2d/client_engine.rb', line 181

def update_entities(space, updated)
  registry_ids = Set.new
  updated.each do |json|
    registry_id = json[:registry_id]
    fail "Can't update #{entity.inspect}, no registry_id!" unless registry_id
    registry_ids << registry_id

    if my_obj = space[registry_id]
      my_obj.update_from_json(json)
      my_obj.grab!
    else
      add_entity(space, json)
    end
  end

  registry_ids
end

#update_score(space, update) ⇒ Object



209
210
211
212
213
# File 'lib/game_2d/client_engine.rb', line 209

def update_score(space, update)
  registry_id, score = update.to_a.first
  return unless player = space[registry_id]
  player.score = score
end