Module: GameClient

Includes:
EntityConstants
Included in:
GameWindow
Defined in:
lib/game_2d/game_client.rb

Overview

We put as many methods here as possible, so we can test them without instantiating an actual Gosu window

Constant Summary collapse

SCREEN_WIDTH =

in pixels

640
SCREEN_HEIGHT =

in pixels

480
DEFAULT_PORT =
4321
DEFAULT_KEY_SIZE =
1024

Constants included from EntityConstants

EntityConstants::CELL_WIDTH_IN_PIXELS, EntityConstants::MAX_VELOCITY, EntityConstants::PIXEL_WIDTH, EntityConstants::WIDTH

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#animationObject (readonly)

Returns the value of attribute animation.



52
53
54
# File 'lib/game_2d/game_client.rb', line 52

def animation
  @animation
end

#fontObject (readonly)

Returns the value of attribute font.



52
53
54
# File 'lib/game_2d/game_client.rb', line 52

def font
  @font
end

#player_idObject

Returns the value of attribute player_id.



52
53
54
# File 'lib/game_2d/game_client.rb', line 52

def player_id
  @player_id
end

#player_nameObject (readonly)

Returns the value of attribute player_name.



52
53
54
# File 'lib/game_2d/game_client.rb', line 52

def player_name
  @player_name
end

#top_menuObject (readonly)

Returns the value of attribute top_menu.



52
53
54
# File 'lib/game_2d/game_client.rb', line 52

def top_menu
  @top_menu
end

Instance Method Details

#_make_client_connection(*args) ⇒ Object



84
85
86
# File 'lib/game_2d/game_client.rb', line 84

def _make_client_connection(*args)
  ClientConnection.new(*args)
end

#adjust_angle(adjustment) ⇒ Object



356
357
358
359
360
361
362
363
# File 'lib/game_2d/game_client.rb', line 356

def adjust_angle(adjustment)
  return unless target = selected_object
  @conn.send_update_entity(
    :registry_id => target.registry_id,
    :angle => target.a + adjustment,
    :moving => true # wake it up
  )
end

#build_top_menuObject



110
111
112
# File 'lib/game_2d/game_client.rb', line 110

def build_top_menu
  @top_menu = MenuItem.new('Click for menu', self, @font) { main_menu }
end

#button_down(id) ⇒ Object



218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
# File 'lib/game_2d/game_client.rb', line 218

def button_down(id)
  case id
    when Gosu::KbEnter, Gosu::KbReturn then
      if @dialog
        @dialog.enter
        @conn.start(@dialog.password_hash)
        @dialog = nil
      end
    when Gosu::KbP then
      @conn.send_ping unless @dialog
    when Gosu::KbEscape then @menu = @top_menu
    when Gosu::MsLeft then # left-click
      if new_menu = @menu.handle_click
        # If handle_click returned anything, the menu consumed the click
        # If it returned a menu, that's the new one we display
        @menu = (new_menu.respond_to?(:handle_click) ? new_menu : @top_menu)
      elsif @player_id
        generate_move_from_click
      end
    when Gosu::MsRight then # right-click
      if button_down?(Gosu::KbRightShift) || button_down?(Gosu::KbLeftShift)
        @create_npc_proc.call
      else
        toggle_grab
      end
    when Gosu::KbB then @create_npc_proc.call
    when Gosu::Kb1 then @create_npc_proc = make_block_npc_proc( 5).call
    when Gosu::Kb2 then @create_npc_proc = make_block_npc_proc(10).call
    when Gosu::Kb3 then @create_npc_proc = make_block_npc_proc(15).call
    when Gosu::Kb4 then @create_npc_proc = make_block_npc_proc(20).call
    when Gosu::Kb5 then @create_npc_proc = make_block_npc_proc(25).call
    when Gosu::Kb6 then @create_npc_proc = make_block_npc_proc( 0).call
    when Gosu::Kb7 then @create_npc_proc = make_teleporter_npc_proc.call
    when Gosu::Kb8 then @create_npc_proc = make_hole_npc_proc.call
    when Gosu::Kb9 then @create_npc_proc = make_base_npc_proc.call
    when Gosu::Kb0 then @create_npc_proc = make_slime_npc_proc.call
    when Gosu::KbDelete then send_delete_entity
    when Gosu::KbBracketLeft then rotate_left
    when Gosu::KbBracketRight then rotate_right
    else @pressed_buttons << id unless @dialog
  end
end

#clear_messageObject



106
107
108
# File 'lib/game_2d/game_client.rb', line 106

def clear_message
  @message = nil
end

#display_message(*lines) ⇒ Object



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

def display_message(*lines)
  if @message
    @message.lines = lines
  else
    @message = Message.new(self, @font, lines)
  end
end

#display_message!(*lines) ⇒ Object

Ensure the message is drawn at least once



101
102
103
104
# File 'lib/game_2d/game_client.rb', line 101

def display_message!(*lines)
  display_message(*lines)
  sleep 0.01 until message_drawn?
end

#generate_move_from_clickObject



261
262
263
264
# File 'lib/game_2d/game_client.rb', line 261

def generate_move_from_click
  move = player.generate_move_from_click(*mouse_coords)
  @conn.send_move(player_id, *move) if move
end

#grab_specific(registry_id) ⇒ Object



332
333
334
# File 'lib/game_2d/game_client.rb', line 332

def grab_specific(registry_id)
  @grabbed_entity_id = registry_id
end

#handle_inputObject

Dequeue an input event



379
380
381
382
383
384
# File 'lib/game_2d/game_client.rb', line 379

def handle_input
  return unless player # can happen when spawning
  return if player.should_fall? || @dialog
  move = move_for_keypress
  @conn.send_move player_id, move # also creates a delta in the engine
end

#initialize_from_hash(opts = {}) ⇒ Object



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/game_2d/game_client.rb', line 54

def initialize_from_hash(opts = {})
  @player_name = opts[:name]
  hostname = opts[:hostname]
  port = opts[:port] || DEFAULT_PORT
  key_size = opts[:key_size] || DEFAULT_KEY_SIZE
  profile = opts[:profile] || false

  @conn_update_total = @engine_update_total = 0.0
  @conn_update_count = @engine_update_count = 0
  @profile = profile

  self.caption = "Game 2D - #{@player_name} on #{hostname}"

  @pressed_buttons = []

  @snap_to_grid = false

  @create_npc_proc = make_block_npc_proc(5)

  @grabbed_entity_id = nil

  @run_start = Time.now.to_f
  @update_count = 0

  @conn = _make_client_connection(hostname, port, self, @player_name, key_size)
  @engine = @conn.engine = ClientEngine.new(self)
  @menu = build_top_menu
  @dialog = PasswordDialog.new(self, @font)
end


114
115
116
117
118
119
# File 'lib/game_2d/game_client.rb', line 114

def main_menu
  Menu.new('Main menu', self, @font,
    MenuItem.new('Object creation', self, @font) { object_creation_menu },
    MenuItem.new('Quit!', self, @font) { shutdown }
  )
end

#make_base_npc_procObject



315
# File 'lib/game_2d/game_client.rb', line 315

def make_base_npc_proc; make_simple_npc_proc 'Base'; end

#make_block_npc_proc(hp) ⇒ Object



288
289
290
291
# File 'lib/game_2d/game_client.rb', line 288

def make_block_npc_proc(hp)
  type = hp.zero? ? 'Entity::Titanium' : 'Entity::Block'
  proc { send_create_npc type, :hp => hp }
end

#make_destination_npc_procObject



299
300
301
302
303
304
# File 'lib/game_2d/game_client.rb', line 299

def make_destination_npc_proc
  proc do |teleporter|
    send_create_npc 'Entity::Destination', :owner => teleporter.registry_id,
      :on_create => make_grab_destination_proc
  end
end

#make_grab_destination_procObject



306
307
308
309
310
# File 'lib/game_2d/game_client.rb', line 306

def make_grab_destination_proc
  proc do |destination|
    grab_specific destination.registry_id
  end
end

#make_hole_npc_procObject



314
# File 'lib/game_2d/game_client.rb', line 314

def make_hole_npc_proc; make_simple_npc_proc 'Hole'; end

#make_simple_npc_proc(type) ⇒ Object



312
# File 'lib/game_2d/game_client.rb', line 312

def make_simple_npc_proc(type); proc { send_create_npc "Entity::#{type}" }; end

#make_slime_npc_procObject



316
317
318
319
320
# File 'lib/game_2d/game_client.rb', line 316

def make_slime_npc_proc
  proc do
    send_create_npc 'Entity::Slime', :angle => 270
  end
end

#make_teleporter_npc_procObject



293
294
295
296
297
# File 'lib/game_2d/game_client.rb', line 293

def make_teleporter_npc_proc
  proc do
    send_create_npc 'Entity::Teleporter', :on_create => make_destination_npc_proc
  end
end

#media(filename) ⇒ Object



156
157
158
# File 'lib/game_2d/game_client.rb', line 156

def media(filename)
  "#{File.dirname __FILE__}/../../media/#{filename}"
end

#message_drawn?Boolean

Returns:

  • (Boolean)


98
# File 'lib/game_2d/game_client.rb', line 98

def message_drawn?; @message.try(:drawn?); end

#mouse_coordsObject

X/Y position of the mouse (center of the crosshairs), adjusted for camera



267
268
269
270
271
272
273
# File 'lib/game_2d/game_client.rb', line 267

def mouse_coords
  # For some reason, Gosu's mouse_x/mouse_y return Floats, so round it off
  [
    (mouse_x.round + @camera_x) * PIXEL_WIDTH,
    (mouse_y.round + @camera_y) * PIXEL_WIDTH
  ]
end

#mouse_entity_locationObject



275
276
277
278
279
280
281
282
283
284
285
286
# File 'lib/game_2d/game_client.rb', line 275

def mouse_entity_location
  x, y = mouse_coords

  if @snap_to_grid
    # When snap is on, we want the upper-left corner of the cell we point at
    return (x / WIDTH) * WIDTH, (y / HEIGHT) * HEIGHT
  else
    # When snap is off, we are pointing at the entity's center, not
    # its upper-left corner
    return x - WIDTH / 2, y - HEIGHT / 2
  end
end

#move_for_keypressObject

Check keyboard, mouse, and pressed-button queue Return a motion symbol or nil



388
389
390
391
392
393
394
395
396
397
398
399
400
401
# File 'lib/game_2d/game_client.rb', line 388

def move_for_keypress
  # Generated once for each keypress
  until @pressed_buttons.empty?
    move = player.move_for_keypress(@pressed_buttons.shift)
    return move if move
  end

  # Continuously-generated when key held down
  player.moves_for_key_held.each do |key, move|
    return move if button_down?(key)
  end

  nil
end

#move_grabbed_entity(divide_by = ClientConnection::ACTION_DELAY) ⇒ Object



206
207
208
209
210
211
212
213
214
215
216
# File 'lib/game_2d/game_client.rb', line 206

def move_grabbed_entity(divide_by = ClientConnection::ACTION_DELAY)
  return unless @grabbed_entity_id
  return unless grabbed = space[@grabbed_entity_id]
  dest_x, dest_y = mouse_entity_location
  vel_x = Entity.constrain_velocity((dest_x - grabbed.x) / divide_by)
  vel_y = Entity.constrain_velocity((dest_y - grabbed.y) / divide_by)
  @conn.send_update_entity(
    :registry_id => grabbed.registry_id,
    :velocity => [vel_x, vel_y],
    :moving => true)
end

#object_creation_menuObject



121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/game_2d/game_client.rb', line 121

def object_creation_menu
  snap_text = lambda do |item|
    @snap_to_grid ? "Turn snap off" : "Turn snap on"
  end

  Menu.new('Object creation', self, @font,
    MenuItem.new('Object type', self, @font) { object_type_menu },
    MenuItem.new(snap_text, self, @font) do
      @snap_to_grid = !@snap_to_grid
    end,
    MenuItem.new('Save!', self, @font) { @conn.send_save }
  )
end

#object_type_menuObject



135
136
137
# File 'lib/game_2d/game_client.rb', line 135

def object_type_menu
  Menu.new('Object type', self, @font, *object_type_submenus)
end

#object_type_submenusObject



139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/game_2d/game_client.rb', line 139

def object_type_submenus
  [
    ['Dirt',        make_block_npc_proc( 5) ],
    ['Brick',       make_block_npc_proc(10) ],
    ['Cement',      make_block_npc_proc(15) ],
    ['Steel',       make_block_npc_proc(20) ],
    ['Unlikelium',  make_block_npc_proc(25) ],
    ['Titanium',    make_block_npc_proc( 0) ],
    ['Teleporter',  make_teleporter_npc_proc],
    ['Hole',        make_hole_npc_proc      ],
    ['Base',        make_base_npc_proc      ],
    ['Slime',       make_slime_npc_proc     ],
  ].collect do |type_name, p|
    MenuItem.new(type_name, self, @font) { @create_npc_proc = p }
  end
end

#playerObject



168
169
170
171
172
# File 'lib/game_2d/game_client.rb', line 168

def player
  return unless space
  warn "GameClient#player(): No such entity #{@player_id}" unless space[@player_id]
  space[@player_id]
end

#rotate_leftObject



365
# File 'lib/game_2d/game_client.rb', line 365

def rotate_left; adjust_angle(-90); end

#rotate_rightObject



366
# File 'lib/game_2d/game_client.rb', line 366

def rotate_right; adjust_angle(+90); end

#selected_objectObject

Actions that modify or delete an existing entity will affect:

  • The grabbed entity, if there is one, or

  • The entity under the mouse whose center is closest to the mouse



339
340
341
342
343
344
345
# File 'lib/game_2d/game_client.rb', line 339

def selected_object
  if @grabbed_entity_id
    grabbed = space[@grabbed_entity_id]
    return grabbed if grabbed
  end
  space.near_to(*mouse_coords)
end

#send_create_npc(type, args = {}) ⇒ Object



322
323
324
325
326
327
328
329
330
# File 'lib/game_2d/game_client.rb', line 322

def send_create_npc(type, args={})
  @conn.send_create_npc({
    :class => type,
    :position => mouse_entity_location,
    :velocity => [0, 0],
    :angle => 0,
    :moving => true
  }.merge(args))
end

#send_delete_entityObject



368
369
370
371
# File 'lib/game_2d/game_client.rb', line 368

def send_delete_entity
  return unless target = selected_object
  @conn.send_delete_entity target
end

#shutdownObject



373
374
375
376
# File 'lib/game_2d/game_client.rb', line 373

def shutdown
  @conn.disconnect
  close
end

#spaceObject



160
161
162
# File 'lib/game_2d/game_client.rb', line 160

def space
  @engine.space
end

#tickObject



164
165
166
# File 'lib/game_2d/game_client.rb', line 164

def tick
  @engine.tick
end

#toggle_grabObject



347
348
349
350
351
352
353
354
# File 'lib/game_2d/game_client.rb', line 347

def toggle_grab
  if @grabbed_entity_id
    @conn.send_snap_to_grid(space[@grabbed_entity_id]) if @snap_to_grid
    return @grabbed_entity_id = nil
  end

  @grabbed_entity_id = space.near_to(*mouse_coords).nullsafe_registry_id
end

#updateObject



174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/game_2d/game_client.rb', line 174

def update
  @update_count += 1

  return unless @conn.online?

  # Handle any pending ENet events
  before_t = Time.now.to_f
  @conn.update
  if @profile
    @conn_update_total += (Time.now.to_f - before_t)
    @conn_update_count += 1
    warn "@conn.update() averages #{@conn_update_total / @conn_update_count} seconds each" if (@conn_update_count % 60) == 0
  end
  return unless @engine.world_established?

  before_t = Time.now.to_f
  @engine.update
  if @profile
    @engine_update_total += (Time.now.to_f - before_t)
    @engine_update_count += 1
    warn "@engine.update() averages #{@engine_update_total / @engine_update_count} seconds" if (@engine_update_count % 60) == 0
  end

  # Player at the keyboard queues up a command
  # @pressed_buttons is emptied by handle_input
  handle_input if @player_id

  move_grabbed_entity

  warn "Updates per second: #{@update_count / (Time.now.to_f - @run_start)}" if @profile
end