Class: GameSpace
- Inherits:
-
Object
- Object
- GameSpace
- Defined in:
- lib/game_2d/game_space.rb
Instance Attribute Summary collapse
-
#cell_height ⇒ Object
readonly
Returns the value of attribute cell_height.
-
#cell_width ⇒ Object
readonly
Returns the value of attribute cell_width.
-
#game ⇒ Object
readonly
Returns the value of attribute game.
-
#highest_id ⇒ Object
Returns the value of attribute highest_id.
-
#npcs ⇒ Object
readonly
Returns the value of attribute npcs.
-
#players ⇒ Object
readonly
Returns the value of attribute players.
-
#storage ⇒ Object
Returns the value of attribute storage.
-
#world_id ⇒ Object
readonly
Returns the value of attribute world_id.
-
#world_name ⇒ Object
readonly
Returns the value of attribute world_name.
Class Method Summary collapse
Instance Method Summary collapse
-
#<<(entity) ⇒ Object
Add an entity.
- #==(other) ⇒ Object
-
#[](registry_id) ⇒ Object
Retrieve entity by ID.
-
#add_entity_to_grid(entity) ⇒ Object
Add the entity to the grid.
- #all_registered ⇒ Object
- #all_state ⇒ Object
-
#assert_ok_coords(cell_x, cell_y) ⇒ Object
We can safely look up cell_x == -1, cell_x == @cell_width, cell_y == -1, and/or cell_y == @cell_height – any of these returns a Wall instance.
-
#at(cell_x, cell_y) ⇒ Object
Retrieve set of entities falling (partly) within cell coordinates, zero-based.
-
#cell_at_point(x, y) ⇒ Object
Translate a subpixel point (X, Y) to a cell coordinate (cell_x, cell_y).
-
#cells_at_points(coords) ⇒ Object
Translate multiple subpixel points (X, Y) to a set of cell coordinates (cell_x, cell_y).
-
#cells_overlapping(x, y) ⇒ Object
Retrieve list of cells that overlap with a theoretical entity at position [x, y] (in subpixels).
-
#check_for_grid_corruption ⇒ Object
Assertion.
-
#check_for_registry_leaks ⇒ Object
Assertion.
- #copy_from(original) ⇒ Object
-
#corner_points_of_entity(x, y) ⇒ Object
Given the (X, Y) position of a theoretical entity, return the list of all the coordinates of its corners.
-
#cut(cell_x, cell_y, entity) ⇒ Object
Low-level remover.
- #deregister(entity) ⇒ Object
-
#doom(entity) ⇒ Object
Doom an entity (mark it to be deleted but don’t remove it yet).
- #doomed?(entity) ⇒ Boolean
-
#entities_at_point(x, y) ⇒ Object
Return a list of the entities (if any) at a subpixel point (X, Y).
-
#entities_at_points(coords) ⇒ Object
Accepts a collection of (x, y) Returns a Set of entities.
-
#entities_bordering_entity_at(x, y) ⇒ Object
The set of entities that may be affected by an entity moving to (or from) the specified (x, y) coordinates This includes the coordinates of eight points just beyond the entity’s borders.
-
#entities_overlapping(x, y) ⇒ Object
Retrieve set of entities that overlap with a theoretical entity created at position [x, y] (in subpixels).
-
#entity_list(entity) ⇒ Object
List of entities by type matching the specified entity.
-
#establish_world(name, id, cell_width, cell_height) ⇒ Object
Width and height, measured in cells.
-
#fire_duplicate_id(old_entity, new_entity) ⇒ Object
Override to be informed when trying to add an entity that we already have (registry ID clash).
-
#fire_entity_not_found(entity) ⇒ Object
Override to be informed when trying to purge an entity that turns out not to exist.
-
#good_camera_position_for(entity, screen_width, screen_height) ⇒ Object
Used client-side only.
- #height ⇒ Object
-
#initialize(game = nil) ⇒ GameSpace
constructor
A new instance of GameSpace.
-
#load ⇒ Object
TODO: Handle this while server is running and players are connected TODO: Handle resizing the space.
- #next_id ⇒ Object
- #pixel_height ⇒ Object
- #pixel_width ⇒ Object
-
#process_moving_entity(entity) ⇒ Object
Execute a block during which an entity may move If it did, we will update the grid appropriately, and wake nearby entities.
-
#purge_doomed_entities ⇒ Object
Actually remove all previously-marked entities.
-
#put(cell_x, cell_y, entity) ⇒ Object
Low-level adder.
-
#register(entity) ⇒ Object
Returns nil if registration worked, or the exact same object was already registered If another object was registered, calls fire_duplicate_id and then returns the previously-registered object.
- #registered?(entity) ⇒ Boolean
-
#remove_entity_from_grid(entity) ⇒ Object
Remove the entity from the grid.
- #save ⇒ Object
- #update ⇒ Object
-
#update_grid_for_moved_entity(entity, old_x, old_y) ⇒ Object
Update grid after an entity moves.
- #width ⇒ Object
Constructor Details
#initialize(game = nil) ⇒ GameSpace
Returns a new instance of GameSpace.
56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
# File 'lib/game_2d/game_space.rb', line 56 def initialize(game=nil) @game = game @grid = @storage = nil @highest_id = 0 @registry = {} # I create a @doomed array so we can remove entities after all collisions # have been processed, to avoid confusion @doomed = [] @players = [] @npcs = [] end |
Instance Attribute Details
#cell_height ⇒ Object (readonly)
Returns the value of attribute cell_height.
53 54 55 |
# File 'lib/game_2d/game_space.rb', line 53 def cell_height @cell_height end |
#cell_width ⇒ Object (readonly)
Returns the value of attribute cell_width.
53 54 55 |
# File 'lib/game_2d/game_space.rb', line 53 def cell_width @cell_width end |
#game ⇒ Object (readonly)
Returns the value of attribute game.
53 54 55 |
# File 'lib/game_2d/game_space.rb', line 53 def game @game end |
#highest_id ⇒ Object
Returns the value of attribute highest_id.
54 55 56 |
# File 'lib/game_2d/game_space.rb', line 54 def highest_id @highest_id end |
#npcs ⇒ Object (readonly)
Returns the value of attribute npcs.
53 54 55 |
# File 'lib/game_2d/game_space.rb', line 53 def npcs @npcs end |
#players ⇒ Object (readonly)
Returns the value of attribute players.
53 54 55 |
# File 'lib/game_2d/game_space.rb', line 53 def players @players end |
#storage ⇒ Object
Returns the value of attribute storage.
54 55 56 |
# File 'lib/game_2d/game_space.rb', line 54 def storage @storage end |
#world_id ⇒ Object (readonly)
Returns the value of attribute world_id.
53 54 55 |
# File 'lib/game_2d/game_space.rb', line 53 def world_id @world_id end |
#world_name ⇒ Object (readonly)
Returns the value of attribute world_name.
53 54 55 |
# File 'lib/game_2d/game_space.rb', line 53 def world_name @world_name end |
Class Method Details
.load(game, storage) ⇒ Object
118 119 120 121 122 123 124 125 |
# File 'lib/game_2d/game_space.rb', line 118 def self.load(game, storage) name, id, cell_width, cell_height = storage[:world_name], storage[:world_id], storage[:cell_width], storage[:cell_height] space = GameSpace.new(game).establish_world(name, id, cell_width, cell_height) space.storage = storage space.load end |
Instance Method Details
#<<(entity) ⇒ Object
Add an entity. Will wake neighboring entities
351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 |
# File 'lib/game_2d/game_space.rb', line 351 def <<(entity) entity.registry_id = next_id unless entity.registry_id? fail "Already registered: #{entity}" if registered?(entity) # Need to assign the space before entities_obstructing() entity.space = self conflicts = entity.entities_obstructing(entity.x, entity.y) if conflicts.empty? register(entity) add_entity_to_grid(entity) entities_bordering_entity_at(entity.x, entity.y).each(&:wake!) entity else entity.space = nil # TODO: Convey error to user somehow $stderr.puts "Can't create #{entity}, occupied by #{conflicts.inspect}" end end |
#==(other) ⇒ Object
456 457 458 |
# File 'lib/game_2d/game_space.rb', line 456 def ==(other) other.class.equal?(self.class) && other.all_state == self.all_state end |
#[](registry_id) ⇒ Object
Retrieve entity by ID
157 158 159 160 |
# File 'lib/game_2d/game_space.rb', line 157 def [](registry_id) return nil unless registry_id @registry[registry_id.to_sym] end |
#add_entity_to_grid(entity) ⇒ Object
Add the entity to the grid
300 301 302 |
# File 'lib/game_2d/game_space.rb', line 300 def add_entity_to_grid(entity) cells_overlapping(entity.x, entity.y).each {|s| s << entity } end |
#all_registered ⇒ Object
162 163 164 |
# File 'lib/game_2d/game_space.rb', line 162 def all_registered @registry.values end |
#all_state ⇒ Object
459 460 461 |
# File 'lib/game_2d/game_space.rb', line 459 def all_state [@world_name, @world_id, @registry, @grid, @highest_id] end |
#assert_ok_coords(cell_x, cell_y) ⇒ Object
We can safely look up cell_x == -1, cell_x == @cell_width, cell_y == -1, and/or cell_y == @cell_height – any of these returns a Wall instance
210 211 212 213 214 215 216 217 |
# File 'lib/game_2d/game_space.rb', line 210 def assert_ok_coords(cell_x, cell_y) raise "Illegal coordinate #{cell_x}x#{cell_y}" if ( cell_x < -1 || cell_y < -1 || cell_x > @cell_width || cell_y > @cell_height ) end |
#at(cell_x, cell_y) ⇒ Object
Retrieve set of entities falling (partly) within cell coordinates, zero-based
221 222 223 224 |
# File 'lib/game_2d/game_space.rb', line 221 def at(cell_x, cell_y) assert_ok_coords(cell_x, cell_y) @grid[cell_x + 1][cell_y + 1] end |
#cell_at_point(x, y) ⇒ Object
Translate a subpixel point (X, Y) to a cell coordinate (cell_x, cell_y)
237 238 239 |
# File 'lib/game_2d/game_space.rb', line 237 def cell_at_point(x, y) [x / Entity::WIDTH, y / Entity::HEIGHT ] end |
#cells_at_points(coords) ⇒ Object
Translate multiple subpixel points (X, Y) to a set of cell coordinates (cell_x, cell_y)
243 244 245 |
# File 'lib/game_2d/game_space.rb', line 243 def cells_at_points(coords) coords.collect {|x, y| cell_at_point(x, y) }.to_set end |
#cells_overlapping(x, y) ⇒ Object
Retrieve list of cells that overlap with a theoretical entity at position [x, y] (in subpixels).
295 296 297 |
# File 'lib/game_2d/game_space.rb', line 295 def cells_overlapping(x, y) cells_at_points(corner_points_of_entity(x, y)).collect {|cx, cy| at(cx, cy) } end |
#check_for_grid_corruption ⇒ Object
Assertion
401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 |
# File 'lib/game_2d/game_space.rb', line 401 def check_for_grid_corruption 0.upto(@cell_height - 1) do |cell_y| 0.upto(@cell_width - 1) do |cell_x| cell = at(cell_x, cell_y) cell.each do |entity| ok = cells_overlapping(entity.x, entity.y) unless ok.include? cell raise "#{entity} shouldn't be in cell #{cell}" end end end end @registry.values.each do |entity| cells_overlapping(entity.x, entity.y).each do |cell| unless cell.include? entity raise "Expected #{entity} to be in cell #{cell}" end end end end |
#check_for_registry_leaks ⇒ Object
Assertion. Useful server-side only
423 424 425 426 427 428 429 |
# File 'lib/game_2d/game_space.rb', line 423 def check_for_registry_leaks expected = @players.size + @npcs.size actual = @registry.size if expected != actual raise "We have #{expected} game entities, #{actual} in registry (delta: #{actual - expected})" end end |
#copy_from(original) ⇒ Object
105 106 107 108 109 110 111 112 113 114 115 116 |
# File 'lib/game_2d/game_space.rb', line 105 def copy_from(original) establish_world(original.world_name, original.world_id, original.cell_width, original.cell_height) @highest_id = original.highest_id # @game and @storage should point to the same object (no clone) @game, @storage = original.game, original.storage # Registry should contain all objects - clone those original.all_registered.each {|ent| self << ent.clone } self end |
#corner_points_of_entity(x, y) ⇒ Object
Given the (X, Y) position of a theoretical entity, return the list of all the coordinates of its corners
249 250 251 252 253 254 255 256 |
# File 'lib/game_2d/game_space.rb', line 249 def corner_points_of_entity(x, y) [ [x, y], [x + Entity::WIDTH - 1, y], [x, y + Entity::HEIGHT - 1], [x + Entity::WIDTH - 1, y + Entity::HEIGHT - 1], ] end |
#cut(cell_x, cell_y, entity) ⇒ Object
Low-level remover
232 233 234 |
# File 'lib/game_2d/game_space.rb', line 232 def cut(cell_x, cell_y, entity) at(cell_x, cell_y).delete entity end |
#deregister(entity) ⇒ Object
202 203 204 205 206 |
# File 'lib/game_2d/game_space.rb', line 202 def deregister(entity) fail "#{entity} not registered" unless registered?(entity) entity_list(entity).delete entity @registry.delete entity.registry_id end |
#doom(entity) ⇒ Object
Doom an entity (mark it to be deleted but don’t remove it yet)
372 |
# File 'lib/game_2d/game_space.rb', line 372 def doom(entity); @doomed << entity; end |
#doomed?(entity) ⇒ Boolean
374 |
# File 'lib/game_2d/game_space.rb', line 374 def doomed?(entity); @doomed.include?(entity); end |
#entities_at_point(x, y) ⇒ Object
Return a list of the entities (if any) at a subpixel point (X, Y)
259 260 261 262 263 264 |
# File 'lib/game_2d/game_space.rb', line 259 def entities_at_point(x, y) at(*cell_at_point(x, y)).find_all do |e| e.x <= x && e.x > (x - Entity::WIDTH) && e.y <= y && e.y > (y - Entity::HEIGHT) end end |
#entities_at_points(coords) ⇒ Object
Accepts a collection of (x, y) Returns a Set of entities
268 269 270 |
# File 'lib/game_2d/game_space.rb', line 268 def entities_at_points(coords) coords.collect {|x, y| entities_at_point(x, y) }.flatten.to_set end |
#entities_bordering_entity_at(x, y) ⇒ Object
The set of entities that may be affected by an entity moving to (or from) the specified (x, y) coordinates This includes the coordinates of eight points just beyond the entity’s borders
276 277 278 279 280 281 282 283 284 285 |
# File 'lib/game_2d/game_space.rb', line 276 def entities_bordering_entity_at(x, y) r = x + Entity::WIDTH - 1 b = y + Entity::HEIGHT - 1 entities_at_points([ [x - 1, y], [x, y - 1], # upper-left corner [r + 1, y], [r, y - 1], # upper-right corner [x - 1, b], [x, b + 1], # lower-left corner [r + 1, b], [r, b + 1], # lower-right corner ]) end |
#entities_overlapping(x, y) ⇒ Object
Retrieve set of entities that overlap with a theoretical entity created at position [x, y] (in subpixels)
289 290 291 |
# File 'lib/game_2d/game_space.rb', line 289 def entities_overlapping(x, y) entities_at_points(corner_points_of_entity(x, y)) end |
#entity_list(entity) ⇒ Object
List of entities by type matching the specified entity
167 168 169 170 171 172 |
# File 'lib/game_2d/game_space.rb', line 167 def entity_list(entity) case entity when Player then @players else @npcs end end |
#establish_world(name, id, cell_width, cell_height) ⇒ Object
Width and height, measured in cells
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 |
# File 'lib/game_2d/game_space.rb', line 72 def establish_world(name, id, cell_width, cell_height) @world_name = name @world_id = (id || SecureRandom.uuid).to_sym @cell_width, @cell_height = cell_width, cell_height # Outer array is X-indexed; inner arrays are Y-indexed # Therefore you can look up @grid[cell_x][cell_y] ... # However, for convenience, we make the grid two cells wider, two cells # taller. Then we can populate the edge with Wall instances, and treat (0, # 0) as a usable coordinate. (-1, -1) contains a Wall, for example. The # at(), put(), and cut() methods do the translation, so only they should # access @grid directly @grid = Array.new(cell_width + 2) do |cx| Array.new(cell_height + 2) do |cy| Cell.new(cx-1, cy-1) end.freeze end.freeze # Top and bottom, including corners (-1 .. cell_width).each do |cell_x| put(cell_x, -1, Wall.new(self, cell_x, -1)) # top put(cell_x, cell_height, Wall.new(self, cell_x, cell_height)) # bottom end # Left and right, skipping corners (0 .. cell_height - 1).each do |cell_y| put(-1, cell_y, Wall.new(self, -1, cell_y)) # left put(cell_width, cell_y, Wall.new(self, cell_width, cell_y)) # right end self end |
#fire_duplicate_id(old_entity, new_entity) ⇒ Object
Override to be informed when trying to add an entity that we already have (registry ID clash)
176 |
# File 'lib/game_2d/game_space.rb', line 176 def fire_duplicate_id(old_entity, new_entity); end |
#fire_entity_not_found(entity) ⇒ Object
Override to be informed when trying to purge an entity that turns out not to exist
378 |
# File 'lib/game_2d/game_space.rb', line 378 def fire_entity_not_found(entity); end |
#good_camera_position_for(entity, screen_width, screen_height) ⇒ Object
Used client-side only. Determine an appropriate camera position, given the specified window size, and preferring that the specified entity be in the center. Inputs and outputs are in pixels
434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 |
# File 'lib/game_2d/game_space.rb', line 434 def good_camera_position_for(entity, screen_width, screen_height) # Given plenty of room, put the entity in the middle of the screen # If doing so would expose the area outside the world, move the camera just enough # to avoid that # If the world is smaller than the window, center it # puts "Screen in pixels is #{screen_width}x#{screen_height}; world in pixels is #{pixel_width}x#{pixel_height}" camera_x = if screen_width > pixel_width (pixel_width - screen_width) / 2 # negative else [[entity.pixel_x - screen_width/2, pixel_width - screen_width].min, 0].max end camera_y = if screen_height > pixel_height (pixel_height - screen_height) / 2 # negative else [[entity.pixel_y - screen_height/2, pixel_height - screen_height].min, 0].max end # puts "Camera at #{camera_x}x#{camera_y}" [ camera_x, camera_y ] end |
#height ⇒ Object
150 |
# File 'lib/game_2d/game_space.rb', line 150 def height; @cell_height * Entity::HEIGHT; end |
#load ⇒ Object
TODO: Handle this while server is running and players are connected TODO: Handle resizing the space
138 139 140 141 142 143 144 145 |
# File 'lib/game_2d/game_space.rb', line 138 def load @highest_id = @storage[:highest_id] @storage[:npcs].each do |json| puts "Loading #{json.inspect}" self << Serializable.from_json(json) end self end |
#next_id ⇒ Object
152 153 154 |
# File 'lib/game_2d/game_space.rb', line 152 def next_id "R#{@highest_id += 1}".to_sym end |
#pixel_height ⇒ Object
148 |
# File 'lib/game_2d/game_space.rb', line 148 def pixel_height; @cell_height * Entity::CELL_WIDTH_IN_PIXELS; end |
#pixel_width ⇒ Object
147 |
# File 'lib/game_2d/game_space.rb', line 147 def pixel_width; @cell_width * Entity::CELL_WIDTH_IN_PIXELS; end |
#process_moving_entity(entity) ⇒ Object
Execute a block during which an entity may move If it did, we will update the grid appropriately, and wake nearby entities
All entity motion should be passed through this method
326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 |
# File 'lib/game_2d/game_space.rb', line 326 def process_moving_entity(entity) unless registered?(entity) puts "#{entity} not in registry yet, no move to process" yield return end before_x, before_y = entity.x, entity.y yield if moved = (entity.x != before_x || entity.y != before_y) update_grid_for_moved_entity(entity, before_x, before_y) # Note: Maybe we should only wake entities in either set # and not both. For now we'll wake them all ( entities_bordering_entity_at(before_x, before_y) + entities_bordering_entity_at(entity.x, entity.y) ).each(&:wake!) end moved end |
#purge_doomed_entities ⇒ Object
Actually remove all previously-marked entities. Wakes neighbors
381 382 383 384 385 386 387 388 389 390 391 392 393 |
# File 'lib/game_2d/game_space.rb', line 381 def purge_doomed_entities @doomed.each do |entity| if registered?(entity) entity.destroy! deregister(entity) entities_bordering_entity_at(entity.x, entity.y).each(&:wake!) remove_entity_from_grid(entity) else fire_entity_not_found(entity) end end @doomed.clear end |
#put(cell_x, cell_y, entity) ⇒ Object
Low-level adder
227 228 229 |
# File 'lib/game_2d/game_space.rb', line 227 def put(cell_x, cell_y, entity) at(cell_x, cell_y) << entity end |
#register(entity) ⇒ Object
Returns nil if registration worked, or the exact same object was already registered If another object was registered, calls fire_duplicate_id and then returns the previously-registered object
182 183 184 185 186 187 188 189 190 191 192 193 |
# File 'lib/game_2d/game_space.rb', line 182 def register(entity) reg_id = entity.registry_id old = @registry[reg_id] return nil if old.equal? entity if old fire_duplicate_id(old, entity) return old end @registry[reg_id] = entity entity_list(entity) << entity nil end |
#registered?(entity) ⇒ Boolean
195 196 197 198 199 200 |
# File 'lib/game_2d/game_space.rb', line 195 def registered?(entity) return false unless old = @registry[entity.registry_id] return true if old.equal? entity fail("Registered entity #{old} has ID #{old.object_id}; " + "passed entity #{entity} has ID #{entity.object_id}") end |
#remove_entity_from_grid(entity) ⇒ Object
Remove the entity from the grid
305 306 307 308 309 |
# File 'lib/game_2d/game_space.rb', line 305 def remove_entity_from_grid(entity) cells_overlapping(entity.x, entity.y).each do |s| raise "#{entity} not where expected" unless s.delete entity end end |
#save ⇒ Object
127 128 129 130 131 132 133 134 |
# File 'lib/game_2d/game_space.rb', line 127 def save @storage[:world_name] = @world_name @storage[:world_id] = @world_id @storage[:cell_width], @storage[:cell_height] = @cell_width, @cell_height @storage[:highest_id] = @highest_id @storage[:npcs] = @npcs @storage.save end |
#update ⇒ Object
395 396 397 398 |
# File 'lib/game_2d/game_space.rb', line 395 def update @registry.values.find_all(&:moving?).each(&:update) purge_doomed_entities end |
#update_grid_for_moved_entity(entity, old_x, old_y) ⇒ Object
Update grid after an entity moves
312 313 314 315 316 317 318 319 320 |
# File 'lib/game_2d/game_space.rb', line 312 def update_grid_for_moved_entity(entity, old_x, old_y) cells_before = cells_overlapping(old_x, old_y) cells_after = cells_overlapping(entity.x, entity.y) (cells_before - cells_after).each do |s| raise "#{entity} not where expected" unless s.delete entity end (cells_after - cells_before).each {|s| s << entity } end |