module AdventureRL::Modifiers::Solid

This module is supposed to be included in Mask child classes. It will tag that Mask instance as 'solid', and check collision with other solid Masks when calling move_by. You can give it a specific solid_tag, which can be passed as the :solid_tag key's value upon initialization. Multiple solid tags may be passed as an array. Solid Masks will only collide with other Solid Masks that have a mutual solid tag. The default solid tag is :default.

Constants

DEFAULT_SOLID_SETTINGS

NOTE: possible :precision_over_performance values:

:low (or anything other than the higher values)

Lowest precision, highest performance. Never check every pixel between previous and new positions. If there is collision at new position, jump to previous position and return. The larger the movement steps, the more distance there will be to the colliding object.

  • – __CANNOT__ fully close gaps to Solids.

  • – __CAN__ phase through Solids at high speeds (especially when it lags).

  • – __CAN__ get stuck in place temporarily when moving on both axes but only colliding on one of them.

  • + Only __one collision check__ per call to move_by, highest performance.

:medium

Medium precision, medium (varying) performance. Only checks every pixel in path if the expected destination collides. Even then, collision checking is used somewhat sparingly.

  • – __CAN__ phase through Solids at high speeds (especially when it lags).

  • – __CAN__ get stuck in place temporarily when moving on both axes but only colliding on one of them.

  • + __CAN__ almost fully close gaps to Solids (no sub-pixel collision checks).

:high

High precision, low to medium (varying) performance. Only checks every pixel in path if the expected destination collides. When checking every pixel in path, check both axes separately, to improve precision.

  • – __CAN__ phase through Solids at high speeds (especially when it lags).

  • + __CANNOT__ get stuck in place temporarily when moving on both axes but only colliding on one of them.

  • + __CAN__ fully close gaps to Solids.

:highest

Highest precision, least performance. Always check every pixel between previous and new positions. Depending on the amount of (moving) Solid objects on screen,

  • – Depending on the amount of (moving) Solids, this can get very laggy at high speeds => lag produces larger steps (usually), because of Deltatime => larger steps produce more collision checks and more lag.

  • + __CANNOT__ phase through Solids, no matter what the speed is.

  • + __CANNOT__ get stuck in place temporarily when moving on both axes but only colliding on one of them.

  • + __CAN__ fully close gaps to Solids.

Public Class Methods

new(settings = {}) click to toggle source

Additionally to the Mask's settings Hash or Settings instance, you may pass the extra key :solid_tag, to define a custom solid tag (or multiple solid tags) upon initialization. They are used for collision checking with other Solid Mask objects that have a mutual solid tag.

Calls superclass method
# File lib/AdventureRL/Modifiers/Solid.rb, line 63
def initialize settings = {}
  @settings                   = DEFAULT_SOLID_SETTINGS.merge settings
  @solid_tags                 = [@settings.get(:solid_tag)].flatten.sort
  @solid_tags_collides_with   = [@settings.get(:solid_tag_collides_with) || @solid_tags].flatten.sort
  @solid_static               = @settings.get :static  # Basically disables #move_by
  @precision_over_performance = @settings.get :precision_over_performance
  assign_to_solids_manager  if (@settings.get :auto_update)
  super @settings
end

Public Instance Methods

add_to_solids_manager(solids_manager) click to toggle source
# File lib/AdventureRL/Modifiers/Solid.rb, line 73
def add_to_solids_manager solids_manager
  Helpers::Error.error(
    "Expected argument to be a SolidsManager, but got",
    "'#{solids_manager.inspect}:#{solids_manager.class.name}`."
  )  unless (solids_manager.is_a? SolidsManager)
  @solids_manager = solids_manager
  @solids_manager.add_object self, get_solid_tags
end
get_colliding_objects() click to toggle source

Returns all currently colliding objects (if any). TODO: Write documentation for callback method.

# File lib/AdventureRL/Modifiers/Solid.rb, line 166
def get_colliding_objects
  if (@solids_manager)
    colliding_objects = @solids_manager.get_colliding_objects(self, get_solid_tags_collides_with)
  else
    colliding_objects = []
  end
  is_colliding_with_objects colliding_objects  if (colliding_objects.any? && methods.include?(:is_colliding_with_objects))
  return colliding_objects
end
get_solid_tags() click to toggle source

Returns this Mask's solid tags, which other Masks use to check collision against this Mask.

# File lib/AdventureRL/Modifiers/Solid.rb, line 199
def get_solid_tags
  return @solid_tags
end
get_solid_tags_collides_with() click to toggle source

Returns the solid tags, which this Mask uses to check collision against other Masks.

# File lib/AdventureRL/Modifiers/Solid.rb, line 205
def get_solid_tags_collides_with
  return @solid_tags_collides_with || @solid_tags
end
in_collision?() click to toggle source

Returns true if this Mask is currently in collision with another solid Mask which has a mutual solid tag. TODO: Write documentation for callback method.

# File lib/AdventureRL/Modifiers/Solid.rb, line 154
def in_collision?
  if (@solids_manager)
    is_colliding = @solids_manager.collides?(self, get_solid_tags_collides_with)
  else
    is_colliding = false
  end
  is_colliding  if (is_colliding && methods.include?(:is_colliding))
  return is_colliding
end
is_static?() click to toggle source

Returns true if this is a static solid Mask, which means it cannot be moved with move_by.

# File lib/AdventureRL/Modifiers/Solid.rb, line 183
def is_static?
  return !!@solid_static
end
make_static() click to toggle source

Makes this Solid Mask static.

# File lib/AdventureRL/Modifiers/Solid.rb, line 177
def make_static
  @solid_static = true
end
move_by(*args) click to toggle source

Overwrite move_by method, so that collision checking with other objects with a mutual solid tag is done, and movement prevented if necessary.

Calls superclass method
# File lib/AdventureRL/Modifiers/Solid.rb, line 103
def move_by *args
  return false  if     (is_static?)
  return super  unless (@solids_manager)

  @real_point = nil
  previous_position = get_position.dup
  incremental_position = parse_position(*args)
  expected_position = {
    x: (previous_position[:x] + (incremental_position.key?(:x) ? incremental_position[:x] : 0)),
    y: (previous_position[:y] + (incremental_position.key?(:y) ? incremental_position[:y] : 0))
  }

  # NOTE:
  # This is a bit of a hacky workaround for some
  # weird Pusher behavior with Velocity and Gravity.
  previous_precision_over_performance = @precision_over_performance.dup
  opts = args.last.is_a?(Hash) ? args.last : nil

  @precision_over_performance = opts[:precision_over_performance]  if (opts && opts.key?(:precision_over_performance))

  if ([:highest].include? @precision_over_performance)
    move_by_steps incremental_position
  else
    @position[:x] += incremental_position[:x]  if (incremental_position.key? :x)
    @position[:y] += incremental_position[:y]  if (incremental_position.key? :y)

    # TODO
    #puts 'PUSHING'  if (is_a?(Player) && opts && opts[:pushed_by_pusher])

    unless (move_by_handle_collision_with_previous_position previous_position)
      move_by_steps incremental_position  if ([:medium, :high].include? @precision_over_performance)
    end
  end

  @precision_over_performance = previous_precision_over_performance
  @solids_manager.reset_object self, get_solid_tags  unless (@position == previous_position)
  return @position == expected_position
end
move_to(*args) click to toggle source

Overwrite the move_to method, so we can reset the object for the solids_manager if necessary.

Calls superclass method
# File lib/AdventureRL/Modifiers/Solid.rb, line 144
def move_to *args
  previous_position = get_position.dup
  super
  return  unless (@solids_manager)
  @solids_manager.reset_object self, get_solid_tags  if (@position != previous_position)
end
remove_from_solids_manager() click to toggle source

When it is removed, also remove it from the SolidsManager. TODO: Do this properly.

# File lib/AdventureRL/Modifiers/Solid.rb, line 96
def remove_from_solids_manager
  #@solids_manager.remove_object self, [get_solid_tags, get_solid_tags_collides_with].flatten  if (@solids_manager)
  @solids_manager.remove_object_from_all_quadtrees self  if (@solids_manager)
end
removed() click to toggle source

This method is called when this object is removed from an Inventory.

# File lib/AdventureRL/Modifiers/Solid.rb, line 90
def removed
  remove_from_solids_manager
end
set_layer(layer) click to toggle source

Overwrite set_layer method, so we can get the SolidsManager from the Layer, if it has one.

Calls superclass method
# File lib/AdventureRL/Modifiers/Solid.rb, line 84
def set_layer layer
  super
  assign_to_solids_manager  if (@settings.get :auto_update)
end
set_solid_tags(*new_solid_tags) click to toggle source
# File lib/AdventureRL/Modifiers/Solid.rb, line 187
def set_solid_tags *new_solid_tags
  @solids_manager.remove_object self, get_solid_tags  if (@solids_manager)
  @solid_tags = [new_solid_tags].flatten.compact
  @solids_manager.add_object self,    get_solid_tags  if (@solids_manager)
end
set_solid_tags_collides_with(*new_solid_tags_collides_with) click to toggle source
# File lib/AdventureRL/Modifiers/Solid.rb, line 193
def set_solid_tags_collides_with *new_solid_tags_collides_with
  @solid_tags_collides_with = [new_solid_tags_collides_with].flatten.compact
end

Private Instance Methods

assign_to_solids_manager() click to toggle source
# File lib/AdventureRL/Modifiers/Solid.rb, line 211
def assign_to_solids_manager
  layer = get_layer
  return  unless (layer && layer.has_solids_manager?)
  @solids_manager = layer.get_solids_manager
  @solids_manager.add_object self, get_solid_tags  if (@solids_manager)
end
move_by_handle_collision_with_previous_position(previous_position) click to toggle source

Returns true if there was no collision, and returns false if there was and it had to reset to the previous_position.

# File lib/AdventureRL/Modifiers/Solid.rb, line 293
def move_by_handle_collision_with_previous_position previous_position
  if (in_collision?)
    @position = previous_position
    return false
  end
  return true
end
move_by_steps(incremental_position) click to toggle source

This is the ugliest method in the project. I can live with there being __one__ ugly method. Also, it does do some complicated stuff, so cut it some slack. It didn't ask to be this way.

# File lib/AdventureRL/Modifiers/Solid.rb, line 222
def move_by_steps incremental_position
  incremental_position[:x] ||= 0
  incremental_position[:y] ||= 0

  larger_axis = :x  if (incremental_position[:x].abs >= incremental_position[:y].abs)
  larger_axis = :y  if (incremental_position[:y].abs >  incremental_position[:x].abs)
  smaller_axis = (larger_axis == :x) ? :y : :x
  larger_axis_sign  = incremental_position[larger_axis].sign
  smaller_axis_sign = incremental_position[smaller_axis].sign
  smaller_axis_increment_at = (incremental_position[larger_axis].abs.to_f / incremental_position[smaller_axis].abs.to_f).round  rescue nil
  remaining_values = {
    larger_axis  => ((incremental_position[larger_axis].abs  % 1) * larger_axis_sign),
    smaller_axis => ((incremental_position[smaller_axis].abs % 1) * smaller_axis_sign),
  }

  return  unless (move_by_steps_for_remaining_values remaining_values)

  # NOTE
  # We use #to_i here, because a negative float's #floor method decreases its value. Example:
  #   1.75.floor   # =>  1.0
  #   -1.75.floor  # => -2.0
  #   1.75.to_i    # =>  1.0
  #   -1.75.to_i   # => -1.0
  incremental_position[larger_axis].to_i.abs.times do |axis_index|
    initial_previous_position = @position.dup

    tmp_in_collision_count = 0

    previous_position = @position.dup
    @position[larger_axis] += larger_axis_sign
    tmp_in_collision_count += 1  unless (
      move_by_handle_collision_with_previous_position(previous_position)
    )  if ([:high, :highest].include? @precision_over_performance)

    if (smaller_axis_increment_at &&
        (((axis_index + 1) % smaller_axis_increment_at) == 0)
       )
      previous_position = @position.dup
      @position[smaller_axis] += smaller_axis_sign
      tmp_in_collision_count += 1  unless (
        move_by_handle_collision_with_previous_position(previous_position)
      )  if ([:high, :highest].include? @precision_over_performance)
    end

    return  unless (tmp_in_collision_count < 2)
    if ([:medium].include? @precision_over_performance)
      return  unless (move_by_handle_collision_with_previous_position initial_previous_position)
    end
  end
end
move_by_steps_for_remaining_values(remaining_values) click to toggle source
# File lib/AdventureRL/Modifiers/Solid.rb, line 273
def move_by_steps_for_remaining_values remaining_values
  return true  unless ([:high, :highest].include? @precision_over_performance)
  return true  if     (remaining_values.values.all? { |val| val == 0 })
  tmp_in_collision_count = 0
  remaining_values.each do |remaining_axis, remaining_value|
    next  if (remaining_value == 0)
    previous_position = @position.dup
    @position[remaining_axis] += remaining_value
    remaining_values[remaining_axis] = 0
    unless (move_by_handle_collision_with_previous_position previous_position)
      tmp_in_collision_count += 1
      next  # break
    end
  end
  return false  if (tmp_in_collision_count == 2)  # NOTE: Slight performance improvement
  return true
end