lib/minigl/movement.rb in minigl-2.2.3 vs lib/minigl/movement.rb in minigl-2.2.4

- old
+ new

@@ -1,585 +1,585 @@ -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 <code>include Movement</code>. - # [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). - # [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 <code>include Movement</code>. - # [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+. - def move_carrying(arg, speed, obstacles, obst_obstacles, obst_ramps) - 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 - else - arg += G.gravity - @speed.x += arg.x / @mass; @speed.y += arg.y / @mass - @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 - end - - x_aim = @x + @speed.x; y_aim = @y + @speed.y - passengers = [] - obstacles.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 - @x = x_aim; @y = y_aim - 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| - 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, obst_obstacles, obst_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 } - 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 <code>include Movement</code>. - # [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+. - def cycle(points, speed, obstacles = nil, obst_obstacles = nil, obst_ramps = nil) - @cur_point = 0 if @cur_point.nil? - if obstacles - move_carrying points[@cur_point], speed, obstacles, obst_obstacles, obst_ramps - else - move_free points[@cur_point], speed - end - if @speed.x == 0 and @speed.y == 0 - if @cur_point == points.length - 1; @cur_point = 0 - else; @cur_point += 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 +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 <code>include Movement</code>. + # [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). + # [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 <code>include Movement</code>. + # [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+. + def move_carrying(arg, speed, obstacles, obst_obstacles, obst_ramps) + 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 + else + arg += G.gravity + @speed.x += arg.x / @mass; @speed.y += arg.y / @mass + @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 + end + + x_aim = @x + @speed.x; y_aim = @y + @speed.y + passengers = [] + obstacles.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 + @x = x_aim; @y = y_aim + 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| + 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, obst_obstacles, obst_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 } + 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 <code>include Movement</code>. + # [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+. + def cycle(points, speed, obstacles = nil, obst_obstacles = nil, obst_ramps = nil) + @cur_point = 0 if @cur_point.nil? + if obstacles + move_carrying points[@cur_point], speed, obstacles, obst_obstacles, obst_ramps + else + move_free points[@cur_point], speed + end + if @speed.x == 0 and @speed.y == 0 + if @cur_point == points.length - 1; @cur_point = 0 + else; @cur_point += 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