class GameSpace

Attributes

cell_height[R]
cell_width[R]
game[R]
highest_id[RW]
npcs[R]
players[R]
storage[RW]
world_id[R]
world_name[R]

Public Class Methods

load(game, storage) click to toggle source
# File lib/game_2d/game_space.rb, line 134
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
new(game=nil) click to toggle source
# File lib/game_2d/game_space.rb, line 63
def initialize(game=nil)
  @game = game
  @grid = @storage = nil
  @highest_id = 0

  @registry = {}

  # Ownership registry needs to be here too.  Each copy of the space must be
  # separate.  Otherwise you get duplicate entries whenever ClientEngine copies
  # the GameSpace.
  #
  # owner.registry_id => [registry_id, ...]
  @ownership = Hash.new {|h,k| h[k] = Array.new}

  # I create a @doomed array so we can remove entities after all collisions
  # have been processed, to avoid confusion
  @doomed = []

  @players = []
  @npcs = []
  @bases = []
  @gravities = []
end

Public Instance Methods

<<(entity) click to toggle source

Add an entity. Will wake neighboring entities

# File lib/game_2d/game_space.rb, line 483
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?
    entities_bordering_entity_at(entity.x, entity.y).each(&:wake!)
    add_entity(entity)
  else
    entity.space = nil
    # TODO: Convey error to user somehow
    warn "Can't create #{entity}, occupied by #{conflicts.inspect}"
  end
end
==(other) click to toggle source
# File lib/game_2d/game_space.rb, line 633
def ==(other)
  other.class.equal?(self.class) && other.all_state == self.all_state
end
[](registry_id) click to toggle source

Retrieve entity by ID

# File lib/game_2d/game_space.rb, line 176
def [](registry_id)
  return nil unless registry_id
  @registry[registry_id.to_sym]
end
add_entity(entity) click to toggle source

Add the entity to the grid, and register it For use only during copies and registry syncs – some checks are skipped, and neighbors aren’t woken up

# File lib/game_2d/game_space.rb, line 475
def add_entity(entity)
  entity.space = self
  register(entity)
  add_entity_to_grid(entity)
  entity
end
add_entity_to_grid(entity) click to toggle source

Add the entity to the grid

# File lib/game_2d/game_space.rb, line 422
def add_entity_to_grid(entity)
  cells_overlapping(entity.x, entity.y).each {|s| s << entity }
end
all_registered() click to toggle source
# File lib/game_2d/game_space.rb, line 181
def all_registered
  @registry.values
end
all_state() click to toggle source
# File lib/game_2d/game_space.rb, line 636
def all_state
  [@world_name, @world_id, @registry, @grid, @highest_id]
end
assert_ok_coords(cell_x, cell_y) click to toggle source

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

# File lib/game_2d/game_space.rb, line 292
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) click to toggle source

Retrieve set of entities falling (partly) within cell coordinates, zero-based

# File lib/game_2d/game_space.rb, line 303
def at(cell_x, cell_y)
  assert_ok_coords(cell_x, cell_y)
  @grid[cell_x + 1][cell_y + 1]
end
available_base() click to toggle source

Return a “randomly” chosen available base, or nil

# File lib/game_2d/game_space.rb, line 269
def available_base
  choices = available_bases
  choices.empty? ? nil : choices[game.tick % choices.size]
end
available_base_near(x, y) click to toggle source

Return the available base closest to the coordinates, or nil

# File lib/game_2d/game_space.rb, line 275
def available_base_near(x, y)
  nearest_to(available_bases, x, y)
end
available_bases() click to toggle source

Return a list of available bases, for a player to (re)spawn

# File lib/game_2d/game_space.rb, line 264
def available_bases
  @bases.collect {|id| self[id] }.find_all(&:available?)
end
cell_location_at_point(x, y) click to toggle source

Translate a subpixel point (X, Y) to a cell coordinate (cell_x, cell_y)

# File lib/game_2d/game_space.rb, line 319
def cell_location_at_point(x, y)
  [x / WIDTH, y / HEIGHT ]
end
cell_locations_at_points(coords) click to toggle source

Translate multiple subpixel points (X, Y) to a set of cell coordinates (cell_x, cell_y)

# File lib/game_2d/game_space.rb, line 325
def cell_locations_at_points(coords)
  coords.collect {|x, y| cell_location_at_point(x, y) }.to_set
end
cell_locations_overlapping(x, y) click to toggle source

Retrieve list of cell-coordinates (expressed as [cell_x, cell_y] arrays), coinciding with position [x, y] (expressed in subpixels).

# File lib/game_2d/game_space.rb, line 411
def cell_locations_overlapping(x, y)
  cell_locations_at_points(corner_points_of_entity(x, y))
end
cells_overlapping(x, y) click to toggle source

Retrieve list of cells that overlap with a theoretical entity at position [x, y] (in subpixels).

# File lib/game_2d/game_space.rb, line 417
def cells_overlapping(x, y)
  cell_locations_overlapping(x, y).collect {|cx, cy| at(cx, cy) }
end
center() click to toggle source

Where to place an entity if you want it dead-center

# File lib/game_2d/game_space.rb, line 169
def center; [(width - Entity::WIDTH) / 2, (height - Entity::HEIGHT) / 2]; end
check_for_grid_corruption() click to toggle source

Assertion

# File lib/game_2d/game_space.rb, line 578
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() click to toggle source

Assertion. Useful server-side only

# File lib/game_2d/game_space.rb, line 600
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) click to toggle source
# File lib/game_2d/game_space.rb, line 121
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| add_entity ent.clone }

  self
end
corner_points_of_entity(x, y) click to toggle source

Given the (X, Y) position of a theoretical entity, return the list of all the coordinates of its corners

# File lib/game_2d/game_space.rb, line 331
def corner_points_of_entity(x, y)
  [
    [x, y],
    [x + WIDTH - 1, y],
    [x, y + HEIGHT - 1],
    [x + WIDTH - 1, y + HEIGHT - 1],
  ]
end
cut(cell_x, cell_y, entity) click to toggle source

Low-level remover

# File lib/game_2d/game_space.rb, line 314
def cut(cell_x, cell_y, entity)
  at(cell_x, cell_y).delete entity
end
deregister(entity) click to toggle source
# File lib/game_2d/game_space.rb, line 224
def deregister(entity)
  fail "#{entity} not registered" unless registered?(entity)
  deregister_base(entity)
  deregister_gravity(entity)
  deregister_ownership(entity)
  entity_list(entity).delete entity
  @registry.delete entity.registry_id
end
deregister_base(entity) click to toggle source
# File lib/game_2d/game_space.rb, line 259
def deregister_base(entity)
  @bases.delete entity.registry_id
end
deregister_gravity(entity) click to toggle source
# File lib/game_2d/game_space.rb, line 255
def deregister_gravity(entity)
  @gravities.delete entity.registry_id
end
deregister_ownership(entity) click to toggle source
# File lib/game_2d/game_space.rb, line 238
def deregister_ownership(entity)
  if entity.is_a?(Entity::OwnedEntity) && entity.owner_id
    @ownership[entity.owner_id].delete entity.registry_id
  end
  @ownership.delete entity.registry_id
end
distance_between(x1, y1, x2, y2) click to toggle source
# File lib/game_2d/game_space.rb, line 359
def distance_between(x1, y1, x2, y2)
  delta_x = (x1 - x2).abs
  delta_y = (y1 - y2).abs
  Math.sqrt(delta_x**2 + delta_y**2)
end
doom(entity) click to toggle source

Doom an entity (mark it to be deleted but don’t remove it yet)

# File lib/game_2d/game_space.rb, line 530
def doom(entity)
  return unless entity && registered?(entity)
  return if doomed?(entity)
  @doomed << entity
  entity.destroy!
end
doomed?(entity) click to toggle source
# File lib/game_2d/game_space.rb, line 537
def doomed?(entity); @doomed.include?(entity); end
entities_at_point(x, y) click to toggle source

Return a list of the entities (if any) intersecting with the subpixel point (X, Y). That is, the point falls somewhere within the entity

# File lib/game_2d/game_space.rb, line 352
def entities_at_point(x, y)
  at(*cell_location_at_point(x, y)).find_all do |e|
    e.x <= x && e.x > (x - WIDTH) &&
    e.y <= y && e.y > (y - HEIGHT)
  end
end
entities_at_points(coords) click to toggle source

Accepts a collection of (x, y) Returns a Set of entities

# File lib/game_2d/game_space.rb, line 384
def entities_at_points(coords)
  coords.collect {|x, y| entities_at_point(x, y) }.flatten.to_set
end
entities_bordering_entity_at(x, y) click to toggle source

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

# File lib/game_2d/game_space.rb, line 392
def entities_bordering_entity_at(x, y)
  r = x + WIDTH - 1
  b = y + 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_exactly_at_point(x, y) click to toggle source

Return a list of the entities (if any) exactly at the subpixel point (X, Y). That is, the point is the entity’s upper-left corner

# File lib/game_2d/game_space.rb, line 343
def entities_exactly_at_point(x, y)
  at(*cell_location_at_point(x, y)).find_all do |e|
    e.x == x && e.y == y
  end
end
entities_overlapping(x, y) click to toggle source

Retrieve set of entities that overlap with a theoretical entity created at position [x, y] (in subpixels)

# File lib/game_2d/game_space.rb, line 405
def entities_overlapping(x, y)
  entities_at_points(corner_points_of_entity(x, y))
end
entity_list(entity) click to toggle source

List of entities by type matching the specified entity

# File lib/game_2d/game_space.rb, line 186
def entity_list(entity)
  case entity
  when Player then @players
  else @npcs
  end
end
establish_world(name, id, cell_width, cell_height) click to toggle source

Width and height, measured in cells

# File lib/game_2d/game_space.rb, line 88
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
fall(entity) click to toggle source
# File lib/game_2d/game_space.rb, line 524
def fall(entity)
  return if @gravities.find {|g| self[g].apply_gravity_to?(entity)}
  entity.accelerate(0, 1)
end
fire_duplicate_id(old_entity, new_entity) click to toggle source

Override to be informed when trying to add an entity that we already have (registry ID clash)

# File lib/game_2d/game_space.rb, line 195
def fire_duplicate_id(old_entity, new_entity); end
fire_entity_not_found(entity) click to toggle source

Override to be informed when trying to purge an entity that turns out not to exist

# File lib/game_2d/game_space.rb, line 541
def fire_entity_not_found(entity); end
good_camera_position_for(entity, screen_width, screen_height) click to toggle source

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

# File lib/game_2d/game_space.rb, line 611
  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() click to toggle source
# File lib/game_2d/game_space.rb, line 166
def height; @cell_height * HEIGHT; end
load() click to toggle source

TODO: Handle this while server is running and players are connected TODO: Handle resizing the space

# File lib/game_2d/game_space.rb, line 154
def load
  @highest_id = @storage[:highest_id]
  @storage[:npcs].each do |json|
    puts "Loading #{json.inspect}"
    self << Serializable.from_json(json)
  end
  self
end
near_to(x, y) click to toggle source

Consider all entities intersecting with (x, y) Return whichever entity’s center is closest (or ties for closest)

# File lib/game_2d/game_space.rb, line 376
def near_to(x, y)
  entities_at_point(x, y).collect do |entity|
    [distance_between(entity.cx, entity.cy, x, y), entity]
  end.sort {|(d1, e1), (d2, e2)| d1 <=> d2}.first.try(:last)
end
nearest_to(entities, x, y) click to toggle source

Consider a given list of entities Return whichever entity’s center is closest (or ties for closest) to the given coordinates

# File lib/game_2d/game_space.rb, line 368
def nearest_to(entities, x, y)
  entities.collect do |entity|
    [distance_between(entity.cx, entity.cy, x, y), entity]
  end.sort {|(d1, e1), (d2, e2)| d1 <=> d2}.first.try(:last)
end
next_id() click to toggle source
# File lib/game_2d/game_space.rb, line 171
def next_id
  "R#{@highest_id += 1}".to_sym
end
owner_change(owned_id, old_owner_id, new_owner_id) click to toggle source
# File lib/game_2d/game_space.rb, line 279
def owner_change(owned_id, old_owner_id, new_owner_id)
  return unless owned_id
  return if old_owner_id == new_owner_id
  @ownership[old_owner_id].delete(owned_id) if old_owner_id
  @ownership[new_owner_id] << owned_id if new_owner_id
end
pixel_height() click to toggle source
# File lib/game_2d/game_space.rb, line 164
def pixel_height; @cell_height * CELL_WIDTH_IN_PIXELS; end
pixel_width() click to toggle source
# File lib/game_2d/game_space.rb, line 163
def pixel_width; @cell_width * CELL_WIDTH_IN_PIXELS; end
possessions(entity) click to toggle source
# File lib/game_2d/game_space.rb, line 286
def possessions(entity)
  @ownership[entity.registry_id].collect {|id| self[id]}
end
process_moving_entity(entity) { || ... } click to toggle source

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

# File lib/game_2d/game_space.rb, line 448
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() click to toggle source

Actually remove all previously-marked entities. Wakes neighbors

# File lib/game_2d/game_space.rb, line 544
def purge_doomed_entities
  @doomed.each do |entity|
    if registered?(entity)
      remove_entity_from_grid(entity)
      entities_bordering_entity_at(entity.x, entity.y).each(&:wake!)
      deregister(entity)
    else
      fire_entity_not_found(entity)
    end
  end
  @doomed.clear
end
put(cell_x, cell_y, entity) click to toggle source

Low-level adder

# File lib/game_2d/game_space.rb, line 309
def put(cell_x, cell_y, entity)
  at(cell_x, cell_y) << entity
end
register(entity) click to toggle source

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

# File lib/game_2d/game_space.rb, line 201
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
  register_with_owner(entity)
  register_gravity(entity)
  register_base(entity)
  nil
end
register_base(entity) click to toggle source
# File lib/game_2d/game_space.rb, line 250
def register_base(entity)
  return unless entity.is_a? Entity::Base
  @bases.unshift entity.registry_id
end
register_gravity(entity) click to toggle source
# File lib/game_2d/game_space.rb, line 245
def register_gravity(entity)
  return unless entity.respond_to? :apply_gravity_to?
  @gravities.unshift entity.registry_id
end
register_with_owner(owned) click to toggle source
# File lib/game_2d/game_space.rb, line 233
def register_with_owner(owned)
  return unless owned.is_a?(Entity::OwnedEntity) && owned.owner_id
  @ownership[owned.owner_id] << owned.registry_id
end
registered?(entity) click to toggle source
# File lib/game_2d/game_space.rb, line 217
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) click to toggle source

Remove the entity from the grid

# File lib/game_2d/game_space.rb, line 427
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() click to toggle source
# File lib/game_2d/game_space.rb, line 143
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
snap_to_grid(entity_id) click to toggle source
# File lib/game_2d/game_space.rb, line 501
def snap_to_grid(entity_id)
  unless entity = self[entity_id]
    warn "Can't snap #{entity_id}, doesn't exist"
    return
  end

  candidates = cell_locations_overlapping(entity.x, entity.y).collect do |cell_x, cell_y|
    [cell_x * WIDTH, cell_y * HEIGHT]
  end
  sorted = candidates.to_a.sort do |(ax, ay), (bx, by)|
    ((entity.x - ax).abs + (entity.y - ay).abs) <=>
    ((entity.x - bx).abs + (entity.y - by).abs)
  end
  sorted.each do |dx, dy|
    if entity.entities_obstructing(dx, dy).empty?
      entity.warp(dx, dy)
      entity.wake!
      return
    end
  end
  warn "Couldn't snap #{entity} to grid"
end
update() click to toggle source
# File lib/game_2d/game_space.rb, line 557
def update
  grabbed_entities = []
  @registry.values.each do |ent|
    if ent.grabbed?
      grabbed_entities << ent
    elsif ent.moving?
      ent.update
    end
  end
  # Update these, and clear their flag, last
  # Gives other entities (e.g. teleporters) a chance to
  # consider their grabbed-state
  grabbed_entities.each do |ent|
    ent.move
    ent.release!
    ent.x_vel = ent.y_vel = 0
  end
  purge_doomed_entities
end
update_grid_for_moved_entity(entity, old_x, old_y) click to toggle source

Update grid after an entity moves

# File lib/game_2d/game_space.rb, line 434
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
width() click to toggle source
# File lib/game_2d/game_space.rb, line 165
def width; @cell_width * WIDTH; end