require_relative 'global'
module MiniGL
# Represents an object with a rectangular bounding box and the +passable+
# property. It is the simplest structure that can be passed as an element of
# the +obst+ array parameter of the +move+ method.
class Block
# The x-coordinate of the top left corner of the bounding box.
attr_reader :x
# The y-coordinate of the top left corner of the bounding box.
attr_reader :y
# The width of the bounding box.
attr_reader :w
# The height of the bounding box.
attr_reader :h
# Whether a moving object can pass through this block when coming from
# below. This is a common feature of platforms in platform games.
attr_reader :passable
# Creates a new block.
#
# Parameters:
# [x] The x-coordinate of the top left corner of the bounding box.
# [y] The y-coordinate of the top left corner of the bounding box.
# [w] The width of the bounding box.
# [h] The height of the bounding box.
# [passable] Whether a moving object can pass through this block when
# coming from below. This is a common feature of platforms in platform
# games. Default is +false+.
def initialize(x, y, w, h, passable = false)
@x = x; @y = y; @w = w; @h = h
@passable = passable
end
# Returns the bounding box of this block as a Rectangle.
def bounds
Rectangle.new @x, @y, @w, @h
end
end
# Represents a ramp, i.e., an inclined structure which allows walking over
# it while automatically going up or down. It can be imagined as a right
# triangle, with a side parallel to the x axis and another one parallel to
# the y axis. You must provide instances of this class (or derived classes)
# to the +ramps+ array parameter of the +move+ method.
class Ramp
# The x-coordinate of the top left corner of a rectangle that completely
# (and precisely) encloses the ramp (thought of as a right triangle).
attr_reader :x
# The y-coordinate of the top left corner of the rectangle described in
# the +x+ attribute.
attr_reader :y
# The width of the ramp.
attr_reader :w
# The height of the ramp.
attr_reader :h
# Whether the height of the ramp increases from left to right (decreases
# from left to right when +false+).
attr_reader :left
attr_reader :ratio # :nodoc:
attr_reader :factor # :nodoc:
# Creates a new ramp.
#
# Parameters:
# [x] The x-coordinate of the top left corner of a rectangle that
# completely (and precisely) encloses the ramp (thought of as a right
# triangle).
# [y] The y-coordinate of the top left corner of the rectangle described
# above.
# [w] The width of the ramp (which corresponds to the width of the
# rectangle described above).
# [h] The height of the ramp (which corresponds to the height of the
# rectangle described above, and to the difference between the lowest
# point of the ramp, where it usually meets the floor, and the
# highest).
# [left] Whether the height of the ramp increases from left to right. Use
# +false+ for a ramp that goes down from left to right.
def initialize(x, y, w, h, left)
@x = x
@y = y
@w = w
@h = h
@left = left
@ratio = @h.to_f / @w
@factor = @w / Math.sqrt(@w**2 + @h**2)
end
# Checks if an object is in contact with this ramp (standing over it).
#
# Parameters:
# [obj] The object to check contact with. It must have the +x+, +y+, +w+
# and +h+ accessible attributes determining its bounding box.
def contact?(obj)
obj.x + obj.w > @x && obj.x < @x + @w && obj.x.round(6) == get_x(obj).round(6) && obj.y.round(6) == get_y(obj).round(6)
end
# Checks if an object is intersecting this ramp (inside the corresponding
# right triangle and at the floor level or above).
#
# Parameters:
# [obj] The object to check intersection with. It must have the +x+, +y+,
# +w+ and +h+ accessible attributes determining its bounding box.
def intersect?(obj)
obj.x + obj.w > @x && obj.x < @x + @w && obj.y > get_y(obj) && obj.y <= @y + @h - obj.h
end
# :nodoc:
def check_can_collide(m)
y = get_y(m) + m.h
@can_collide = m.x + m.w > @x && @x + @w > m.x && m.y < y && m.y + m.h > y
end
def check_intersection(obj)
if @can_collide and intersect? obj
counter = @left && obj.prev_speed.x > 0 || !@left && obj.prev_speed.x < 0
if obj.prev_speed.y > 0 && counter
dx = get_x(obj) - obj.x
s = (obj.prev_speed.y.to_f / obj.prev_speed.x).abs
dx /= s + @ratio
obj.x += dx
end
obj.y = get_y obj
if counter && obj.bottom != self
obj.speed.x *= @factor
end
obj.speed.y = 0
end
end
def get_x(obj)
return obj.x if @left && obj.x + obj.w > @x + @w
return @x + (1.0 * (@y + @h - obj.y - obj.h) * @w / @h) - obj.w if @left
return obj.x if obj.x < @x
@x + (1.0 * (obj.y + obj.h - @y) * @w / @h)
end
def get_y(obj)
return @y - obj.h if @left && obj.x + obj.w > @x + @w
return @y + (1.0 * (@x + @w - obj.x - obj.w) * @h / @w) - obj.h if @left
return @y - obj.h if obj.x < @x
@y + (1.0 * (obj.x - @x) * @h / @w) - obj.h
end
end
# This module provides objects with physical properties and methods for
# moving. It allows moving with or without collision checking (based on
# rectangular bounding boxes), including a method to behave as an elevator,
# affecting other objects' positions as it moves.
module Movement
# The mass of the object, in arbitrary units. The default value for
# GameObject instances, for example, is 1. The larger the mass (i.e., the
# heavier the object), the more intense the forces applied to the object
# have to be in order to move it.
attr_reader :mass
# A Vector with the current speed of the object (x: horizontal component,
# y: vertical component).
attr_reader :speed
# A Vector with the speed limits for the object (x: horizontal component,
# y: vertical component).
attr_reader :max_speed
# Width of the bounding box.
attr_reader :w
# Height of the bounding box.
attr_reader :h
# The object that is making contact with this from above. If there's no
# contact, returns +nil+.
attr_reader :top
# The object that is making contact with this from below. If there's no
# contact, returns +nil+.
attr_reader :bottom
# The object that is making contact with this from the left. If there's no
# contact, returns +nil+.
attr_reader :left
# The object that is making contact with this from the right. If there's
# no contact, returns +nil+.
attr_reader :right
# The x-coordinate of the top left corner of the bounding box.
attr_accessor :x
# The y-coordinate of the top left corner of the bounding box.
attr_accessor :y
# Whether a moving object can pass through this block when coming from
# below. This is a common feature of platforms in platform games.
attr_accessor :passable
# A Vector with the horizontal and vertical components of a force that
# be applied in the next time +move+ is called.
attr_accessor :stored_forces
# A Vector containing the speed of the object in the previous frame.
attr_reader :prev_speed
# Returns the bounding box as a Rectangle.
def bounds
Rectangle.new @x, @y, @w, @h
end
# Moves this object, based on the forces being applied to it, and
# performing collision checking.
#
# Parameters:
# [forces] A Vector where x is the horizontal component of the resulting
# force and y is the vertical component.
# [obst] An array of obstacles to be considered in the collision checking.
# Obstacles must be instances of Block (or derived classes), or
# objects that include Movement
.
# [ramps] An array of ramps to be considered in the collision checking.
# Ramps must be instances of Ramp (or derived classes).
# [set_speed] Set this flag to +true+ to cause the +forces+ vector to be
# treated as a speed vector, i.e., the object's speed will be
# directly set to the given values. The force of gravity will
# also be ignored in this case.
def move(forces, obst, ramps, set_speed = false)
if set_speed
@speed.x = forces.x
@speed.y = forces.y
else
forces.x += G.gravity.x; forces.y += G.gravity.y
forces.x += @stored_forces.x; forces.y += @stored_forces.y
@stored_forces.x = @stored_forces.y = 0
forces.x = 0 if (forces.x < 0 and @left) or (forces.x > 0 and @right)
forces.y = 0 if (forces.y < 0 and @top) or (forces.y > 0 and @bottom)
if @bottom.is_a? Ramp
if @bottom.ratio > G.ramp_slip_threshold
forces.x += (@bottom.left ? -1 : 1) * (@bottom.ratio - G.ramp_slip_threshold) * G.ramp_slip_force / G.ramp_slip_threshold
elsif forces.x > 0 && @bottom.left || forces.x < 0 && !@bottom.left
forces.x *= @bottom.factor
end
end
@speed.x += forces.x / @mass; @speed.y += forces.y / @mass
end
@speed.x = 0 if @speed.x.abs < G.min_speed.x
@speed.y = 0 if @speed.y.abs < G.min_speed.y
@speed.x = (@speed.x <=> 0) * @max_speed.x if @speed.x.abs > @max_speed.x
@speed.y = (@speed.y <=> 0) * @max_speed.y if @speed.y.abs > @max_speed.y
@prev_speed = @speed.clone
x = @speed.x < 0 ? @x + @speed.x : @x
y = @speed.y < 0 ? @y + @speed.y : @y
w = @w + (@speed.x < 0 ? -@speed.x : @speed.x)
h = @h + (@speed.y < 0 ? -@speed.y : @speed.y)
move_bounds = Rectangle.new x, y, w, h
coll_list = []
obst.each do |o|
coll_list << o if o != self && move_bounds.intersect?(o.bounds)
end
ramps.each do |r|
r.check_can_collide move_bounds
end
if coll_list.length > 0
up = @speed.y < 0; rt = @speed.x > 0; dn = @speed.y > 0; lf = @speed.x < 0
if @speed.x == 0 || @speed.y == 0
# Ortogonal
if rt; x_lim = find_right_limit coll_list
elsif lf; x_lim = find_left_limit coll_list
elsif dn; y_lim = find_down_limit coll_list
elsif up; y_lim = find_up_limit coll_list
end
if rt && @x + @w + @speed.x > x_lim
@x = x_lim - @w
@speed.x = 0
elsif lf && @x + @speed.x < x_lim
@x = x_lim
@speed.x = 0
elsif dn && @y + @h + @speed.y > y_lim; @y = y_lim - @h; @speed.y = 0
elsif up && @y + @speed.y < y_lim; @y = y_lim; @speed.y = 0
end
else
# Diagonal
x_aim = @x + @speed.x + (rt ? @w : 0); x_lim_def = x_aim
y_aim = @y + @speed.y + (dn ? @h : 0); y_lim_def = y_aim
coll_list.each do |c|
if c.passable; x_lim = x_aim
elsif rt; x_lim = c.x
else; x_lim = c.x + c.w
end
if dn; y_lim = c.y
elsif c.passable; y_lim = y_aim
else; y_lim = c.y + c.h
end
if c.passable
y_lim_def = y_lim if dn && @y + @h <= y_lim && y_lim < y_lim_def
elsif (rt && @x + @w > x_lim) || (lf && @x < x_lim)
# Can't limit by x, will limit by y
y_lim_def = y_lim if (dn && y_lim < y_lim_def) || (up && y_lim > y_lim_def)
elsif (dn && @y + @h > y_lim) || (up && @y < y_lim)
# Can't limit by y, will limit by x
x_lim_def = x_lim if (rt && x_lim < x_lim_def) || (lf && x_lim > x_lim_def)
else
x_time = 1.0 * (x_lim - @x - (@speed.x < 0 ? 0 : @w)) / @speed.x
y_time = 1.0 * (y_lim - @y - (@speed.y < 0 ? 0 : @h)) / @speed.y
if x_time > y_time
# Will limit by x
x_lim_def = x_lim if (rt && x_lim < x_lim_def) || (lf && x_lim > x_lim_def)
elsif (dn && y_lim < y_lim_def) || (up && y_lim > y_lim_def)
y_lim_def = y_lim
end
end
end
if x_lim_def != x_aim
@speed.x = 0
if lf; @x = x_lim_def
else; @x = x_lim_def - @w
end
end
if y_lim_def != y_aim
@speed.y = 0
if up; @y = y_lim_def
else; @y = y_lim_def - @h
end
end
end
end
@x += @speed.x
@y += @speed.y
# Keeping contact with ramp
# if @speed.y == 0 and @speed.x.abs <= G.ramp_contact_threshold and @bottom.is_a? Ramp
# @y = @bottom.get_y(self)
# puts 'aqui'
# end
ramps.each do |r|
r.check_intersection self
end
check_contact obst, ramps
end
# Moves this object as an elevator (i.e., potentially carrying other
# objects) with the specified forces or towards a given point.
#
# Parameters:
# [arg] A Vector specifying either the forces acting on this object or a
# point towards the object should move.
# [speed] If the first argument is a forces vector, then this should be
# +nil+. If it is a point, then this is the constant speed at which
# the object will move (provided as a scalar, not a vector).
# [carried_objs] An array of objects that can potentially be carried by
# this object while it moves. The objects must respond to
# +x+, +y+, +w+ and +h+.
# [obstacles] Obstacles that should be considered for collision checking
# with the carried objects, if they include the +Movement+
# module, and with this object too, if moving with forces and
# the +ignore_collision+ flag is false.
# [ramps] Ramps that should be considered for the carried objects, if they
# include the +Movement+ module, and for this object too, if moving
# with forces and +ignore_collision+ is false.
# [ignore_collision] Set to true to make this object ignore collision even
# when moving with forces.
def move_carrying(arg, speed, carried_objs, obstacles, ramps, ignore_collision = false)
if speed
x_d = arg.x - @x; y_d = arg.y - @y
distance = Math.sqrt(x_d**2 + y_d**2)
if distance == 0
@speed.x = @speed.y = 0
return
end
@speed.x = 1.0 * x_d * speed / distance
@speed.y = 1.0 * y_d * speed / distance
x_aim = @x + @speed.x; y_aim = @y + @speed.y
else
x_aim = @x + @speed.x + G.gravity.x + arg.x
y_aim = @y + @speed.y + G.gravity.y + arg.y
end
passengers = []
carried_objs.each do |o|
if @x + @w > o.x && o.x + o.w > @x
foot = o.y + o.h
if foot.round(6) == @y.round(6) || @speed.y < 0 && foot < @y && foot > y_aim
passengers << o
end
end
end
prev_x = @x; prev_y = @y
if speed
if @speed.x > 0 && x_aim >= arg.x || @speed.x < 0 && x_aim <= arg.x
@x = arg.x; @speed.x = 0
else
@x = x_aim
end
if @speed.y > 0 && y_aim >= arg.y || @speed.y < 0 && y_aim <= arg.y
@y = arg.y; @speed.y = 0
else
@y = y_aim
end
else
move(arg, ignore_collision ? [] : obstacles, ignore_collision ? [] : ramps)
end
forces = Vector.new @x - prev_x, @y - prev_y
prev_g = G.gravity.clone
G.gravity.x = G.gravity.y = 0
passengers.each do |p|
if p.class.included_modules.include?(Movement)
prev_speed = p.speed.clone
prev_forces = p.stored_forces.clone
prev_bottom = p.bottom
p.speed.x = p.speed.y = 0
p.stored_forces.x = p.stored_forces.y = 0
p.instance_exec { @bottom = nil }
p.move(forces * p.mass, obstacles, ramps)
p.speed.x = prev_speed.x
p.speed.y = prev_speed.y
p.stored_forces.x = prev_forces.x
p.stored_forces.y = prev_forces.y
p.instance_exec(prev_bottom) { |b| @bottom = b }
else
p.x += forces.x
p.y += forces.y
end
end
G.gravity = prev_g
end
# Moves this object, without performing any collision checking, towards
# a specified point or in a specified direction.
#
# Parameters:
# [aim] A +Vector+ specifying where the object will move to or an angle (in
# degrees) indicating the direction of the movement. Angles are
# measured starting from the right (i.e., to move to the right, the
# angle must be 0) and raising clockwise.
# [speed] The constant speed at which the object will move. This must be
# provided as a scalar, not a vector.
def move_free(aim, speed)
if aim.is_a? Vector
x_d = aim.x - @x; y_d = aim.y - @y
distance = Math.sqrt(x_d**2 + y_d**2)
if distance == 0
@speed.x = @speed.y = 0
return
end
@speed.x = 1.0 * x_d * speed / distance
@speed.y = 1.0 * y_d * speed / distance
if (@speed.x < 0 and @x + @speed.x <= aim.x) or (@speed.x >= 0 and @x + @speed.x >= aim.x)
@x = aim.x
@speed.x = 0
else
@x += @speed.x
end
if (@speed.y < 0 and @y + @speed.y <= aim.y) or (@speed.y >= 0 and @y + @speed.y >= aim.y)
@y = aim.y
@speed.y = 0
else
@y += @speed.y
end
else
rads = aim * Math::PI / 180
@speed.x = speed * Math.cos(rads)
@speed.y = speed * Math.sin(rads)
@x += @speed.x
@y += @speed.y
end
end
# Causes the object to move in cycles across multiple given points (the
# first point in the array is the first point the object will move towards,
# so it doesn't need to be equal to the current/initial position). If
# obstacles are provided, it will behave as an elevator (as in
# +move_carrying+).
#
# Parameters:
# [points] An array of Vectors representing the path that the object will
# perform.
# [speed] The constant speed at which the object will move. This must be
# provided as a scalar, not a vector.
# [obstacles] An array of obstacles to be considered in the collision
# checking, and carried along when colliding from above.
# Obstacles must be instances of Block (or derived classes),
# or objects that include Movement
.
# [obst_obstacles] Obstacles that should be considered when moving objects
# from the +obstacles+ array, i.e., these obstacles won't
# interfere in the elevator's movement, but in the movement
# of the objects being carried.
# [obst_ramps] Ramps to consider when moving objects from the +obstacles+
# array, as described for +obst_obstacles+.
# [stop_time] Optional stop time (in frames) when the object reaches each of
# the points.
def cycle(points, speed, obstacles = nil, obst_obstacles = nil, obst_ramps = nil, stop_time = 0)
unless @cycle_setup
@cur_point = 0 if @cur_point.nil?
if obstacles
obst_obstacles = [] if obst_obstacles.nil?
obst_ramps = [] if obst_ramps.nil?
move_carrying points[@cur_point], speed, obstacles, obst_obstacles, obst_ramps
else
move_free points[@cur_point], speed
end
end
if @speed.x == 0 and @speed.y == 0
unless @cycle_setup
@cycle_timer = 0
@cycle_setup = true
end
if @cycle_timer >= stop_time
if @cur_point == points.length - 1
@cur_point = 0
else
@cur_point += 1
end
@cycle_setup = false
else
@cycle_timer += 1
end
end
end
private
def check_contact(obst, ramps)
prev_bottom = @bottom
@top = @bottom = @left = @right = nil
obst.each do |o|
x2 = @x + @w; y2 = @y + @h; x2o = o.x + o.w; y2o = o.y + o.h
@right = o if !o.passable && x2.round(6) == o.x.round(6) && y2 > o.y && @y < y2o
@left = o if !o.passable && @x.round(6) == x2o.round(6) && y2 > o.y && @y < y2o
@bottom = o if y2.round(6) == o.y.round(6) && x2 > o.x && @x < x2o
@top = o if !o.passable && @y.round(6) == y2o.round(6) && x2 > o.x && @x < x2o
end
if @bottom.nil?
ramps.each do |r|
if r.contact? self
@bottom = r
break
end
end
if @bottom.nil?
ramps.each do |r|
if r == prev_bottom && @x + @w > r.x && r.x + r.w > @x &&
@prev_speed.x.abs <= G.ramp_contact_threshold &&
@prev_speed.y >= 0
@y = r.get_y self
@bottom = r
break
end
end
end
end
end
def find_right_limit(coll_list)
limit = @x + @w + @speed.x
coll_list.each do |c|
limit = c.x if !c.passable && c.x < limit
end
limit
end
def find_left_limit(coll_list)
limit = @x + @speed.x
coll_list.each do |c|
limit = c.x + c.w if !c.passable && c.x + c.w > limit
end
limit
end
def find_down_limit(coll_list)
limit = @y + @h + @speed.y
coll_list.each do |c|
limit = c.y if c.y < limit && c.y >= @y + @h
end
limit
end
def find_up_limit(coll_list)
limit = @y + @speed.y
coll_list.each do |c|
limit = c.y + c.h if !c.passable && c.y + c.h > limit
end
limit
end
end
end