module AdventureRL
class Quadtree < Mask
include Helpers::MethodHelper
DEFAULT_SETTINGS = Settings.new(
max_objects: 4,
position: {
x: 0,
y: 0
},
size: {
width: 960,
height: 540
},
origin: {
x: :left,
y: :top
}
)
def self.get_default_settings
window = Window.get_window
return Settings.new(
position: (window ? window.get_position : DEFAULT_SETTINGS.get(:window, :position) || DEFAULT_SETTINGS[:position]),
size: (window ? window.get_size : DEFAULT_SETTINGS.get(:window, :size) || DEFAULT_SETTINGS[:size]),
origin: (window ? window.get_origin : DEFAULT_SETTINGS.get(:window, :origin) || DEFAULT_SETTINGS[:origin]),
)
end
def initialize settings = {}
@settings = DEFAULT_SETTINGS.merge(Quadtree.get_default_settings).merge(settings)
super @settings
@max_objects = @settings.get :max_objects
@quadtrees = {
top_left: nil,
top_right: nil,
bottom_left: nil,
bottom_right: nil
}
@objects = []
add_object [@settings.get(:objects)].flatten.compact
end
# Add the given Mask object(s) into the Quadtree,
# and split into smaller quadtrees if necessary.
def add_object object
objects = [object].flatten
objects.each do |obj|
validate_object_has_mask_or_point obj
add_object_to_quadtree obj
end
end
alias_method :add, :add_object
def add_object_to_quadtree object
return false unless (collides_with? object)
return false if (@objects.include? object)
if (@objects.size < @max_objects)
@objects << object
return true
end
split_quadtrees unless (has_quadtrees?)
return get_quadtrees.map do |quadtree|
next quadtree.add_object_to_quadtree(object)
end .any?
end
# Returns true if the given object
# collides with any other object and false if not.
def collides? object
validate_object_has_mask_or_point object
return collides_for?(object)
end
def collides_for? object
return false unless (collides_with? object)
return (
@objects.any? do |obj|
next obj != object && obj.collides_with?(object)
end ||
get_quadtrees.any? do |quadtree|
next quadtree.collides_for?(object)
end
)
end
# Returns all objects, that collide with object.
def get_colliding_objects object
validate_object_has_mask_or_point object
return get_colliding_objects_for(object)
end
def get_colliding_objects_for object
colliding_objects = []
return colliding_objects unless (collides_with? object)
colliding_objects.concat(@objects.select do |obj|
next obj != object && obj.collides_with?(object)
end)
get_quadtrees.each do |quadtree|
colliding_objects.concat quadtree.get_colliding_objects_for(object)
end
return colliding_objects
end
# Reset this and all child Quadtrees.
# Removes all stored objects.
def reset
@objects.clear
get_quadtrees.each &:reset
end
# Remove and (try to) re-add the given object(s) (single or multiple).
def reset_object object
objects = [object].flatten
objects.each do |obj|
@objects.delete obj
add_object_to_quadtree obj
end
end
# Remove the given object(s) (single or multiple) from the Quadtree (or any children).
def remove_object object
objects = [object].flatten
objects.each do |obj|
@objects.delete obj
get_quadtrees.each do |quadtree|
quadtree.remove_object obj
end
end
end
private
def validate_object_has_mask_or_point object
object.has_point? rescue error( # NOTE: #has_point? method must be available for both Point and Mask
"Expected an instance of Mask/Point or an object that has a Mask/Point, but got",
"`#{object.inspect}:#{object.class.name}'."
)
end
# Returns all the children Quadtrees.
def get_quadtrees
return @quadtrees.values.compact
end
# Returns true if this Quadtree has already been split
# and has children Quadtrees.
def has_quadtrees?
return @quadtrees.values.all?
end
def split_quadtrees
@quadtrees = @quadtrees.keys.map do |corner|
new_quadtree = get_split_quadtree_for_corner corner
next [corner, new_quadtree] if (new_quadtree)
next nil
end .compact.to_h
#move_objects_to_quadtrees if (@objects.any?) # NOTE: Doing this will break stuff.
end
def get_split_quadtree_for_corner corner
method_name = "get_split_quadtree_#{corner.to_s}".to_sym
error(
"Method `#{method_name.to_s}' doesn't exist for `#{self.inspect}:#{self.class}'."
) unless (method_exists? method_name)
return method(method_name).call
end
def get_split_quadtree_top_left
return Quadtree.new(Settings.new(
position: get_position,
size: get_size.map do |side, size|
next [side, (size.to_f * 0.5).round]
end .to_h,
origin: get_origin,
max_objects: @max_objects
))
end
def get_split_quadtree_top_right
return Quadtree.new(Settings.new(
position: get_position.map do |axis, pos|
next [axis, pos + (get_size(:width).to_f * 0.5).round] if (axis == :x)
next [axis, pos]
end .to_h,
size: get_size.map do |side, size|
next [side, (size.to_f * 0.5).round]
end .to_h,
origin: get_origin,
max_objects: @max_objects
))
end
def get_split_quadtree_bottom_left
return Quadtree.new(Settings.new(
position: get_position.map do |axis, pos|
next [axis, pos + (get_size(:height).to_f * 0.5).round] if (axis == :y)
next [axis, pos]
end .to_h,
size: get_size.map do |side, size|
next [side, (size.to_f * 0.5).round]
end .to_h,
origin: get_origin,
max_objects: @max_objects
))
end
def get_split_quadtree_bottom_right
return Quadtree.new(Settings.new(
position: get_position.map do |axis, pos|
next [axis, pos + (get_size(:width).to_f * 0.5).round] if (axis == :x)
next [axis, pos + (get_size(:height).to_f * 0.5).round] if (axis == :y)
end .to_h,
size: get_size.map do |side, size|
next [side, (size.to_f * 0.5).round]
end .to_h,
origin: get_origin,
max_objects: @max_objects
))
end
# NOTE: Shouldn't be used, breaks stuff currently.
# Life is easier without this method.
def move_objects_to_quadtrees
return if (@objects.empty?)
@objects.each do |object|
get_quadtrees.detect do |quadtree|
next quadtree.add_object_to_quadtree(object)
end
end
@objects.clear
end
end
end