game_2d 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,12 @@
1
1
  require 'securerandom'
2
2
  require 'delegate'
3
3
  require 'set'
4
+ require 'facets/kernel/try'
4
5
  require 'game_2d/wall'
5
6
  require 'game_2d/player'
6
7
  require 'game_2d/serializable'
8
+ require 'game_2d/entity_constants'
9
+ require 'game_2d/entity/owned_entity'
7
10
 
8
11
  # Common code between the server and client for maintaining the world.
9
12
  # This is a bounded space (walls on all sides).
@@ -50,6 +53,8 @@ end
50
53
 
51
54
 
52
55
  class GameSpace
56
+ include EntityConstants
57
+
53
58
  attr_reader :world_name, :world_id, :players, :npcs, :cell_width, :cell_height, :game
54
59
  attr_accessor :storage, :highest_id
55
60
 
@@ -60,6 +65,13 @@ class GameSpace
60
65
 
61
66
  @registry = {}
62
67
 
68
+ # Ownership registry needs to be here too. Each copy of the space must be
69
+ # separate. Otherwise you get duplicate entries whenever ClientEngine copies
70
+ # the GameSpace.
71
+ #
72
+ # owner.registry_id => [registry_id, ...]
73
+ @ownership = Hash.new {|h,k| h[k] = Array.new}
74
+
63
75
  # I create a @doomed array so we can remove entities after all collisions
64
76
  # have been processed, to avoid confusion
65
77
  @doomed = []
@@ -144,10 +156,10 @@ class GameSpace
144
156
  self
145
157
  end
146
158
 
147
- def pixel_width; @cell_width * Entity::CELL_WIDTH_IN_PIXELS; end
148
- def pixel_height; @cell_height * Entity::CELL_WIDTH_IN_PIXELS; end
149
- def width; @cell_width * Entity::WIDTH; end
150
- def height; @cell_height * Entity::HEIGHT; end
159
+ def pixel_width; @cell_width * CELL_WIDTH_IN_PIXELS; end
160
+ def pixel_height; @cell_height * CELL_WIDTH_IN_PIXELS; end
161
+ def width; @cell_width * WIDTH; end
162
+ def height; @cell_height * HEIGHT; end
151
163
 
152
164
  def next_id
153
165
  "R#{@highest_id += 1}".to_sym
@@ -189,6 +201,7 @@ class GameSpace
189
201
  end
190
202
  @registry[reg_id] = entity
191
203
  entity_list(entity) << entity
204
+ register_with_owner(entity)
192
205
  nil
193
206
  end
194
207
 
@@ -201,10 +214,34 @@ class GameSpace
201
214
 
202
215
  def deregister(entity)
203
216
  fail "#{entity} not registered" unless registered?(entity)
217
+ deregister_ownership(entity)
204
218
  entity_list(entity).delete entity
205
219
  @registry.delete entity.registry_id
206
220
  end
207
221
 
222
+ def register_with_owner(owned)
223
+ return unless owned.is_a?(Entity::OwnedEntity) && owned.owner_id
224
+ @ownership[owned.owner_id] << owned.registry_id
225
+ end
226
+
227
+ def deregister_ownership(entity)
228
+ if entity.is_a?(Entity::OwnedEntity) && entity.owner_id
229
+ @ownership[entity.owner_id].delete entity.registry_id
230
+ end
231
+ @ownership.delete entity.registry_id
232
+ end
233
+
234
+ def owner_change(owned_id, old_owner_id, new_owner_id)
235
+ return unless owned_id
236
+ return if old_owner_id == new_owner_id
237
+ @ownership[old_owner_id].delete(owned_id) if old_owner_id
238
+ @ownership[new_owner_id] << owned_id if new_owner_id
239
+ end
240
+
241
+ def possessions(entity)
242
+ @ownership[entity.registry_id].collect {|id| self[id]}
243
+ end
244
+
208
245
  # We can safely look up cell_x == -1, cell_x == @cell_width, cell_y == -1,
209
246
  # and/or cell_y == @cell_height -- any of these returns a Wall instance
210
247
  def assert_ok_coords(cell_x, cell_y)
@@ -234,14 +271,14 @@ class GameSpace
234
271
  end
235
272
 
236
273
  # Translate a subpixel point (X, Y) to a cell coordinate (cell_x, cell_y)
237
- def cell_at_point(x, y)
238
- [x / Entity::WIDTH, y / Entity::HEIGHT ]
274
+ def cell_location_at_point(x, y)
275
+ [x / WIDTH, y / HEIGHT ]
239
276
  end
240
277
 
241
278
  # Translate multiple subpixel points (X, Y) to a set of cell coordinates
242
279
  # (cell_x, cell_y)
243
- def cells_at_points(coords)
244
- coords.collect {|x, y| cell_at_point(x, y) }.to_set
280
+ def cell_locations_at_points(coords)
281
+ coords.collect {|x, y| cell_location_at_point(x, y) }.to_set
245
282
  end
246
283
 
247
284
  # Given the (X, Y) position of a theoretical entity, return the list of all
@@ -249,20 +286,32 @@ class GameSpace
249
286
  def corner_points_of_entity(x, y)
250
287
  [
251
288
  [x, y],
252
- [x + Entity::WIDTH - 1, y],
253
- [x, y + Entity::HEIGHT - 1],
254
- [x + Entity::WIDTH - 1, y + Entity::HEIGHT - 1],
289
+ [x + WIDTH - 1, y],
290
+ [x, y + HEIGHT - 1],
291
+ [x + WIDTH - 1, y + HEIGHT - 1],
255
292
  ]
256
293
  end
257
294
 
258
295
  # Return a list of the entities (if any) at a subpixel point (X, Y)
259
296
  def entities_at_point(x, y)
260
- at(*cell_at_point(x, y)).find_all do |e|
261
- e.x <= x && e.x > (x - Entity::WIDTH) &&
262
- e.y <= y && e.y > (y - Entity::HEIGHT)
297
+ at(*cell_location_at_point(x, y)).find_all do |e|
298
+ e.x <= x && e.x > (x - WIDTH) &&
299
+ e.y <= y && e.y > (y - HEIGHT)
263
300
  end
264
301
  end
265
302
 
303
+ # Return whichever entity's center is closest (or ties for closest)
304
+ def near_to(x, y)
305
+ entities_at_point(x, y).collect do |entity|
306
+ center_x = entity.x + WIDTH/2
307
+ center_y = entity.y + HEIGHT/2
308
+ delta_x = (center_x - x).abs
309
+ delta_y = (center_y - y).abs
310
+ distance = Math.sqrt(delta_x**2 + delta_y**2)
311
+ [distance, entity]
312
+ end.sort {|(d1, e1), (d2, e2)| d1 <=> d2}.first.try(:last)
313
+ end
314
+
266
315
  # Accepts a collection of (x, y)
267
316
  # Returns a Set of entities
268
317
  def entities_at_points(coords)
@@ -274,8 +323,8 @@ class GameSpace
274
323
  # This includes the coordinates of eight points just beyond the entity's
275
324
  # borders
276
325
  def entities_bordering_entity_at(x, y)
277
- r = x + Entity::WIDTH - 1
278
- b = y + Entity::HEIGHT - 1
326
+ r = x + WIDTH - 1
327
+ b = y + HEIGHT - 1
279
328
  entities_at_points([
280
329
  [x - 1, y], [x, y - 1], # upper-left corner
281
330
  [r + 1, y], [r, y - 1], # upper-right corner
@@ -290,10 +339,16 @@ class GameSpace
290
339
  entities_at_points(corner_points_of_entity(x, y))
291
340
  end
292
341
 
342
+ # Retrieve list of cell-coordinates (expressed as [cell_x, cell_y]
343
+ # arrays), coinciding with position [x, y] (expressed in subpixels).
344
+ def cell_locations_overlapping(x, y)
345
+ cell_locations_at_points(corner_points_of_entity(x, y))
346
+ end
347
+
293
348
  # Retrieve list of cells that overlap with a theoretical entity
294
349
  # at position [x, y] (in subpixels).
295
350
  def cells_overlapping(x, y)
296
- cells_at_points(corner_points_of_entity(x, y)).collect {|cx, cy| at(cx, cy) }
351
+ cell_locations_overlapping(x, y).collect {|cx, cy| at(cx, cy) }
297
352
  end
298
353
 
299
354
  # Add the entity to the grid
@@ -368,6 +423,29 @@ class GameSpace
368
423
  end
369
424
  end
370
425
 
426
+ def snap_to_grid(entity_id)
427
+ unless entity = self[entity_id]
428
+ $stderr.puts "Can't snap #{entity_id}, doesn't exist"
429
+ return
430
+ end
431
+
432
+ candidates = cell_locations_overlapping(entity.x, entity.y).collect do |cell_x, cell_y|
433
+ [cell_x * WIDTH, cell_y * HEIGHT]
434
+ end
435
+ sorted = candidates.to_a.sort do |(ax, ay), (bx, by)|
436
+ ((entity.x - ax).abs + (entity.y - ay).abs) <=>
437
+ ((entity.x - bx).abs + (entity.y - by).abs)
438
+ end
439
+ sorted.each do |dx, dy|
440
+ if entity.entities_obstructing(dx, dy).empty?
441
+ entity.warp(dx, dy)
442
+ entity.wake!
443
+ return
444
+ end
445
+ end
446
+ $stderr.puts "Couldn't snap #{entity} to grid"
447
+ end
448
+
371
449
  # Doom an entity (mark it to be deleted but don't remove it yet)
372
450
  def doom(entity); @doomed << entity; end
373
451
 
@@ -393,7 +471,15 @@ class GameSpace
393
471
  end
394
472
 
395
473
  def update
396
- @registry.values.find_all(&:moving?).each(&:update)
474
+ @registry.values.each do |ent|
475
+ if ent.grabbed?
476
+ ent.move
477
+ ent.release!
478
+ ent.x_vel = ent.y_vel = 0
479
+ elsif ent.moving?
480
+ ent.update
481
+ end
482
+ end
397
483
  purge_doomed_entities
398
484
  end
399
485
 
@@ -2,41 +2,23 @@
2
2
  ## License: Same as for Gosu (MIT)
3
3
 
4
4
  require 'rubygems'
5
+ require 'facets/kernel/try'
5
6
  require 'gosu'
6
7
 
7
- require 'game_2d/client_connection'
8
- require 'game_2d/client_engine'
9
- require 'game_2d/game_space'
10
- require 'game_2d/entity'
11
- require 'game_2d/player'
12
- require 'game_2d/menu'
13
- require 'game_2d/zorder'
14
-
15
- SCREEN_WIDTH = 640 # in pixels
16
- SCREEN_HEIGHT = 480 # in pixels
17
-
18
- DEFAULT_PORT = 4321
8
+ require 'game_2d/game_client'
19
9
 
20
10
  # The Gosu::Window is always the "environment" of our game
21
11
  # It also provides the pulse of our game
22
12
  class GameWindow < Gosu::Window
23
- attr_reader :animation, :font
24
- attr_accessor :player_id
25
-
26
- def initialize(player_name, hostname, port=DEFAULT_PORT, profile=false)
27
- @conn_update_total = @engine_update_total = 0.0
28
- @conn_update_count = @engine_update_count = 0
29
- @profile = profile
13
+ include GameClient
30
14
 
15
+ def initialize(opts = {})
31
16
  super(SCREEN_WIDTH, SCREEN_HEIGHT, false, 16)
32
- self.caption = "Ruby Gosu Game"
33
-
34
- @pressed_buttons = []
35
17
 
36
18
  @background_image = Gosu::Image.new(self, media("Space.png"), true)
37
19
  @animation = Hash.new do |h, k|
38
20
  h[k] = Gosu::Image::load_tiles(
39
- self, k, Entity::CELL_WIDTH_IN_PIXELS, Entity::CELL_WIDTH_IN_PIXELS, false)
21
+ self, k, CELL_WIDTH_IN_PIXELS, CELL_WIDTH_IN_PIXELS, false)
40
22
  end
41
23
 
42
24
  @cursor_anim = @animation[media("crosshair.gif")]
@@ -45,109 +27,14 @@ class GameWindow < Gosu::Window
45
27
 
46
28
  @font = Gosu::Font.new(self, Gosu::default_font_name, 20)
47
29
 
48
- # Local settings
49
- @local = {
50
- :create_npc => {
51
- :type => 'Entity::Block',
52
- :hp => 5,
53
- :snap => false,
54
- },
55
- }
56
- snap_text = lambda do |item|
57
- @local[:create_npc][:snap] ? "Turn snap off" : "Turn snap on"
58
- end
59
-
60
- object_type_submenus = [
61
- ['Dirt', 'Entity::Block', 5],
62
- ['Brick', 'Entity::Block', 10],
63
- ['Cement', 'Entity::Block', 15],
64
- ['Steel', 'Entity::Block', 20],
65
- ['Unlikelium', 'Entity::Block', 25],
66
- ['Titanium', 'Entity::Titanium', 0]
67
- ].collect do |type_name, class_name, hp|
68
- MenuItem.new(type_name, self, @font) do |item|
69
- @local[:create_npc][:type] = class_name
70
- @local[:create_npc][:hp] = hp
71
- end
72
- end
73
- object_type_menu = Menu.new('Object type', self, @font,
74
- *object_type_submenus)
75
-
76
- object_creation_menu = Menu.new('Object creation', self, @font,
77
- MenuItem.new('Object type', self, @font) { object_type_menu },
78
- MenuItem.new(snap_text, self, @font) do
79
- @local[:create_npc][:snap] = !@local[:create_npc][:snap]
80
- end,
81
- MenuItem.new('Save!', self, @font) { @conn.send_save }
82
- )
83
- main_menu = Menu.new('Main menu', self, @font,
84
- MenuItem.new('Object creation', self, @font) { object_creation_menu },
85
- MenuItem.new('Quit!', self, @font) { shutdown }
86
- )
87
- @menu = @top_menu = MenuItem.new('Click for menu', self, @font) { main_menu }
88
-
89
- # Connect to server and kick off handshaking
90
- # We will create our player object only after we've been accepted by the server
91
- # and told our starting position
92
- @conn = ClientConnection.new(hostname, port, self, player_name)
93
- @engine = @conn.engine = ClientEngine.new(self)
94
- @run_start = Time.now.to_f
95
- @update_count = 0
96
- end
97
-
98
- def media(filename)
99
- "#{File.dirname __FILE__}/../../media/#{filename}"
100
- end
101
-
102
- def space
103
- @engine.space
104
- end
105
-
106
- def player
107
- space[@player_id]
108
- end
109
-
110
- def update
111
- @update_count += 1
112
-
113
- # Handle any pending ENet events
114
- before_t = Time.now.to_f
115
- @conn.update
116
- if @profile
117
- @conn_update_total += (Time.now.to_f - before_t)
118
- @conn_update_count += 1
119
- $stderr.puts "@conn.update() averages #{@conn_update_total / @conn_update_count} seconds each" if (@conn_update_count % 60) == 0
120
- end
121
- return unless @conn.online? && @engine
122
-
123
- before_t = Time.now.to_f
124
- @engine.update
125
- if @profile
126
- @engine_update_total += (Time.now.to_f - before_t)
127
- @engine_update_count += 1
128
- $stderr.puts "@engine.update() averages #{@engine_update_total / @engine_update_count} seconds" if (@engine_update_count % 60) == 0
129
- end
130
-
131
- # Player at the keyboard queues up a command
132
- # @pressed_buttons is emptied by handle_input
133
- handle_input if @player_id
134
-
135
- $stderr.puts "Updates per second: #{@update_count / (Time.now.to_f - @run_start)}" if @profile
30
+ initialize_from_hash(opts)
136
31
  end
137
32
 
138
33
  def draw
139
34
  @background_image.draw(0, 0, ZOrder::Background)
140
- return unless @player_id
141
- @camera_x, @camera_y = space.good_camera_position_for(player, SCREEN_WIDTH, SCREEN_HEIGHT)
142
- translate(-@camera_x, -@camera_y) do
143
- (space.players + space.npcs).each {|entity| entity.draw(self) }
144
- end
145
-
146
- space.players.sort.each_with_index do |player, num|
147
- @font.draw("#{player.player_name}: #{player.score}", 10, 10 * (num * 2 + 1), ZOrder::Text, 1.0, 1.0, Gosu::Color::YELLOW)
148
- end
149
-
150
- @menu.draw
35
+ @dialog.draw if @dialog
36
+ @message.draw if @message
37
+ @menu.draw if @menu
151
38
 
152
39
  cursor_img = @cursor_anim[Gosu::milliseconds / 50 % @cursor_anim.size]
153
40
  cursor_img.draw(
@@ -155,106 +42,20 @@ class GameWindow < Gosu::Window
155
42
  mouse_y - cursor_img.height / 2.0,
156
43
  ZOrder::Cursor,
157
44
  1, 1, Gosu::Color::WHITE, :add)
158
- end
159
-
160
- def draw_box_at(x1, y1, x2, y2, c)
161
- draw_quad(x1, y1, c, x2, y1, c, x2, y2, c, x1, y2, c, ZOrder::Highlight)
162
- end
163
-
164
- def button_down(id)
165
- case id
166
- when Gosu::KbP then @conn.send_ping
167
- when Gosu::KbEscape then @menu = @top_menu
168
- when Gosu::MsLeft then # left-click
169
- if new_menu = @menu.handle_click
170
- # If handle_click returned anything, the menu consumed the click
171
- # If it returned a menu, that's the new one we display
172
- @menu = (new_menu.respond_to?(:handle_click) ? new_menu : @top_menu)
173
- else
174
- send_fire
175
- end
176
- when Gosu::MsRight then # right-click
177
- send_create_npc
178
- else @pressed_buttons << id
179
- end
180
- end
181
45
 
182
- def send_fire
183
46
  return unless @player_id
184
- x, y = mouse_coords
185
- x_vel = (x - (player.x + Entity::WIDTH / 2)) / Entity::PIXEL_WIDTH
186
- y_vel = (y - (player.y + Entity::WIDTH / 2)) / Entity::PIXEL_WIDTH
187
- @conn.send_move :fire, :x_vel => x_vel, :y_vel => y_vel
188
- end
189
-
190
- # X/Y position of the mouse (center of the crosshairs), adjusted for camera
191
- def mouse_coords
192
- # For some reason, Gosu's mouse_x/mouse_y return Floats, so round it off
193
- [
194
- (mouse_x.round + @camera_x) * Entity::PIXEL_WIDTH,
195
- (mouse_y.round + @camera_y) * Entity::PIXEL_WIDTH
196
- ]
197
- end
198
47
 
199
- def send_create_npc
200
- x, y = mouse_coords
201
-
202
- if @local[:create_npc][:snap]
203
- # When snap is on, we want the upper-left corner of the cell we clicked in
204
- x = (x / Entity::WIDTH) * Entity::WIDTH
205
- y = (y / Entity::HEIGHT) * Entity::HEIGHT
206
- else
207
- # When snap is off, we want the click to be the new entity's center, not
208
- # its upper-left corner
209
- x -= Entity::WIDTH / 2
210
- y -= Entity::HEIGHT / 2
48
+ @camera_x, @camera_y = space.good_camera_position_for(player, SCREEN_WIDTH, SCREEN_HEIGHT)
49
+ translate(-@camera_x, -@camera_y) do
50
+ (space.players + space.npcs).each {|entity| entity.draw(self) }
211
51
  end
212
52
 
213
- @conn.send_create_npc(
214
- :class => @local[:create_npc][:type],
215
- :position => [x, y],
216
- :velocity => [0, 0],
217
- :angle => 0,
218
- :moving => true,
219
- :hp => @local[:create_npc][:hp]
220
- )
221
- end
222
-
223
- def shutdown
224
- @conn.disconnect
225
- close
226
- end
227
-
228
- # Dequeue an input event
229
- def handle_input
230
- return if player.falling?
231
- move = move_for_keypress
232
- @conn.send_move move # also creates a delta in the engine
233
- end
234
-
235
- # Check keyboard, return a motion symbol or nil
236
- #
237
- #
238
- def move_for_keypress
239
- # Generated once for each keypress
240
- until @pressed_buttons.empty?
241
- button = @pressed_buttons.shift
242
- case button
243
- when Gosu::KbUp, Gosu::KbW
244
- return (player.building?) ? :rise_up : :flip
245
- end
53
+ space.entities_at_point(*mouse_coords).each_with_index do |entity, line|
54
+ @font.draw(entity.to_s, 10, 10 * (line * 2 + 1), ZOrder::Text, 1.0, 1.0, Gosu::Color::YELLOW)
246
55
  end
56
+ end
247
57
 
248
- # Continuously-generated when key held down
249
- case
250
- when button_down?(Gosu::KbLeft), button_down?(Gosu::KbA)
251
- :slide_left
252
- when button_down?(Gosu::KbRight), button_down?(Gosu::KbD)
253
- :slide_right
254
- when button_down?(Gosu::KbRightControl), button_down?(Gosu::KbLeftControl)
255
- :brake
256
- when button_down?(Gosu::KbDown), button_down?(Gosu::KbS)
257
- :build
258
- end
58
+ def draw_box_at(x1, y1, x2, y2, c)
59
+ draw_quad(x1, y1, c, x2, y1, c, x2, y2, c, x1, y2, c, ZOrder::Highlight)
259
60
  end
260
61
  end