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



90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/game_2d/client_engine.rb', line 90

def add_delta(delta)
  at_tick = delta[:at_tick]
  fail "Received delta without at_tick: #{delta.inspect}" unless at_tick
  if at_tick < @tick
    warn "Received delta #{@tick - at_tick} ticks late"
    if at_tick <= @earliest_tick
      warn "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



170
171
172
173
174
175
176
177
178
179
180
# File 'lib/game_2d/client_engine.rb', line 170

def add_entity(space, json)
  space.add_entity (o = Serializable.from_json(json))
  if o.is_a?(Player) && @game_window.player_name == o.player_name
    # This can be news, if the server is ahead of us.  The server
    # promises that we always have exactly one entity assigned to
    # each authenticated player, and each connected player must
    # have a unique name at any time -- so this entity has to be
    # ours.
    @game_window.player_id = o.registry_id
  end
end

#add_npcs(space, npcs) ⇒ Object



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

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



140
141
142
143
144
145
# File 'lib/game_2d/client_engine.rb', line 140

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



147
148
149
# File 'lib/game_2d/client_engine.rb', line 147

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

#apply_deltas(at_tick) ⇒ Object



105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/game_2d/client_engine.rb', line 105

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_name = hash.delete :player_name
    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

    leftovers = hash.keys - [:at_tick]
    warn "Unprocessed deltas: #{leftovers.join(', ')}" unless leftovers.empty?
  end
end

#apply_move(space, move, player_id) ⇒ Object



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

def apply_move(space, move, player_id)
  player = space[player_id]
  if player
    player.add_move move
  else
    # This can happen if, say, the player sent an action just before
    # death or disconnection.
    warn "No such player #{player_id}, can't apply #{move.inspect}"
  end
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

#delete_entities(space, doomed) ⇒ Object



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

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

#spaceObject



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

def space
  @spaces[@tick]
end

#space_at(tick) ⇒ Object



57
58
59
60
61
62
63
64
65
66
67
68
# 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



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

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



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

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
    warn "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



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

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



211
212
213
214
215
# File 'lib/game_2d/client_engine.rb', line 211

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