#-- # Rubygame -- Ruby bindings to SDL to facilitate game creation # Copyright (C) 2004 John 'jacius' Croisant # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA #++ #-- # Table of Contents: # # class Rect # GENERAL: # initialize # new_from_object # to_s # to_a, to_ary # [] # ATTRIBUTES: # x, y, w, h [<- accessors] # width, height, size # left, top, right, bottom # center, centerx, centery # topleft, topright # bottomleft, bottomright # midleft, midtop, midright, midbottom # UTILITY METHODS: # clamp, clamp! # clip, clip! # collide_hash, collide_hash_all # collide_array, collide_array_all # collide_point? # collide_rect? # contain? # inflate, inflate! # move, move! # normalize, normalize! # union, union! # union_all, union_all! # # class Surface # make_rect # #++ module Rubygame # A Rect is a representation of a rectangle, with four core attributes # (x offset, y offset, width, and height) and a variety of functions # for manipulating and accessing these attributes. # # Like all coordinates in Rubygame (and its base library, SDL), x and y # offsets are measured from the top-left corner of the screen, with greater # y offsets being lower. Thus, specifying the x and y offsets of the Rect # is equivalent to setting the location of its top-left corner. # # In Rubygame, Rects are used for collision detection and describing # the area of a Surface to operate on. class Rect < Array #-- # GENERAL #++ # Create a new Rect, attempting to extract its own information from # the given arguments. The arguments must fall into one of these cases: # # - 4 integers +(x, y, w, h)+. # - 1 Rect or Array containing 4 integers +([x, y, w, h])+. # - 2 Arrays containing 2 integers each +([x,y], [w,h])+. # - 1 object with a +rect+ attribute which is a valid Rect object. # # All rect core attributes (x,y,w,h) must be integers. # def initialize(*argv) case argv.length when 1 if argv[0].kind_of? Array; super(argv[0]) elsif argv[0].respond_to? :rect; super(argv[0]) end when 2 super(argv[0].concat(argv[1])) when 4 super(argv) end return self end # Extract or generate a Rect from the given object, if possible, using the # following process: # # 1. If it's a Rect already, return a duplicate Rect. # 2. Elsif it's an Array with at least 4 values, make a Rect from it. # 3. Elsif it has a +rect+ attribute., perform (1) and (2) on that. # 4. Otherwise, raise TypeError. # # See also Surface#make_rect() def Rect.new_from_object(object) case(object) when Rect return object.dup when Array if object.length >= 4 return Rect.new(object) else raise(ArgumentError,"Array does not have enough indices to be made into a Rect (%d for 4)."%object.length ) end else begin case(object.rect) when Rect return object.rect.dup when Array if object.rect.length >= 4 return Rect.new(object.rect) else raise(ArgumentError,"Array does not have enough indices to be made into a Rect (%d for 4)."%object.rect.length ) end end # case object.rect rescue NoMethodError # if no rect.rect raise(TypeError,"Object must be a Rect or Array [x,y,w,h], or have an attribute called 'rect'. (Got %s instance.)"%object.class) end end # case object end # Print the Rect in the form "+#+" def to_s; "#"%self; end # Print the Rect in the form "+#+" def inspect; "#"%self; end #-- # ATTRIBUTES #++ # Returns self[0] def x; return self[0]; end # Sets self[0] to +val+ def x=(val); self[0] = val; end alias left x alias left= x=; alias l x alias l= x=; # Returns self[1] def y; return self[1]; end # Sets self[1] to +val+ def y=(val); self[1] = val; end alias top y alias top= y=; alias t y alias t= y=; # Returns self[2] def w; return self[2]; end # Sets self[2] to +val+ def w=(val); self[2] = val; end alias width w alias width= w=; # Returns self[3] def h; return self[3]; end # Sets self[3] to +val+ def h=(val); self[3] = val; end alias height h alias height= h=; # Return the width and height of the Rect. def size; return self[2..3]; end # Set the width and height of the Rect. def size=(size); self[2..3] = size; return size; end # Return the x coordinate of the right side of the Rect. def right; return self[0]+self[2]; end # Set the x coordinate of the right side of the Rect by translating the # Rect (adjusting the x offset). def right=(r); self[0] = r - self[2]; return r; end alias r right alias r= right=; # Return the y coordinate of the bottom side of the Rect. def bottom; return self[1]+self[3]; end # Set the y coordinate of the bottom side of the Rect by translating the # Rect (adjusting the y offset). def bottom=(b); self[1] = b - self[3]; return b; end alias b bottom alias b= bottom=; # Return the x and y coordinates of the center of the Rect. def center; return self.centerx, self.centery; end # Set the x and y coordinates of the center of the Rect by translating the # Rect (adjusting the x and y offsets). def center=(center) self.centerx, self.centery = center return center end alias c center alias c= center=; # Return the x coordinate of the center of the Rect def centerx; return self[0]+(self[2]/2); end # Set the x coordinate of the center of the Rect by translating the # Rect (adjusting the x offset). def centerx=(x); self[0] = x - (self[2]/2); return x; end alias cx centerx alias cx= centerx=; # Return the y coordinate of the center of the Rect def centery; return self[1]+(self[3]/2); end # Set the y coordinate of the center of the Rect by translating the # Rect (adjusting the y offset). def centery=(y); self[1] = y- (self[3]/2); return y; end alias cy centery alias cy= centery=; # Return the x and y coordinates of the top-left corner of the Rect def topleft; return self[0..1]; end # Set the x and y coordinates of the top-left corner of the Rect by # translating the Rect (adjusting the x and y offsets). def topleft=(topleft) self[0..1] = topleft return topleft end alias tl topleft alias tl= topleft=; # Return the x and y coordinates of the top-right corner of the Rect def topright; return self.right, self[1]; end # Set the x and y coordinates of the top-right corner of the Rect by # translating the Rect (adjusting the x and y offsets). def topright=(topright) self.right, self[1] = topright return topright end alias tr topright alias tr= topright=; # Return the x and y coordinates of the bottom-left corner of the Rect def bottomleft; return self[0], self.bottom; end # Set the x and y coordinates of the bottom-left corner of the Rect by # translating the Rect (adjusting the x and y offsets). def bottomleft=(bottomleft) self[0], self.bottom = bottomleft return bottomleft end alias bl bottomleft alias bl= bottomleft=; # Return the x and y coordinates of the bottom-right corner of the Rect def bottomright; return self.right, self.bottom; end # Set the x and y coordinates of the bottom-right corner of the Rect by # translating the Rect (adjusting the x and y offsets). def bottomright=(bottomright) self.right, self.bottom = bottomright return bottomright end alias br bottomright alias br= bottomright=; # Return the x and y coordinates of the midpoint on the left side of the # Rect. def midleft; return self[0], self.centery; end # Set the x and y coordinates of the midpoint on the left side of the Rect # by translating the Rect (adjusting the x and y offsets). def midleft=(midleft) self[0], self.centery = midleft return midleft end alias ml midleft alias ml= midleft=; # Return the x and y coordinates of the midpoint on the left side of the # Rect. def midtop; return self.centerx, self[1]; end # Set the x and y coordinates of the midpoint on the top side of the Rect # by translating the Rect (adjusting the x and y offsets). def midtop=(midtop) self.centerx, self[1] = midtop return midtop end alias mt midtop alias mt= midtop=; # Return the x and y coordinates of the midpoint on the left side of the # Rect. def midright; return self.right, self.centery; end # Set the x and y coordinates of the midpoint on the right side of the Rect # by translating the Rect (adjusting the x and y offsets). def midright=(midright) self.right, self.centery = midright return midright end alias mr midright alias mr= midright=; # Return the x and y coordinates of the midpoint on the left side of the # Rect. def midbottom; return self.centerx, self.bottom; end # Set the x and y coordinates of the midpoint on the bottom side of the # Rect by translating the Rect (adjusting the x and y offsets). def midbottom=(midbottom) self.centerx, self.bottom = midbottom return midbottom end alias mb midbottom alias mb= midbottom=; #-- # UTILITY METHODS #++ # As #clamp!, but the original caller is not changed. def clamp(rect) self.dup.clamp!(rect) end # Translate the calling Rect to be entirely inside the given Rect. If # the caller is too large along either axis to fit in the given rect, # it is centered with respect to the given rect, along that axis. def clamp!(rect) nself = self.normalize rect = Rect.new_from_object(rect) #If self is inside given, there is no need to move self unless rect.contain?(nself) #If self is too wide: if nself[2] >= rect[2] self[0] = rect.centerx - nself[2]/2 #Else self is not too wide else #If self is to the left of arg if nself[0] < rect[0] self[0] = rect[0] #If self is to the right of arg elsif nself.right > rect.right self[0] = rect.right - nself[2] #Otherwise, leave x alone end end #If self is too tall: if nself[3] >= rect[3] self[1] = rect.centery - nself[3]/2 #Else self is not too tall else #If self is above arg if nself[1] < rect[1] self[1] = rect[1] #If self below arg elsif nself.bottom > rect.bottom self[1] = rect.bottom - nself[3] #Otherwise, leave y alone end end end return self end # As #clip!, but the original caller is not changed. def clip(rect) self.dup.clip!(rect) end # Crop the calling Rect to be entirely inside the given Rect. If the # caller does not intersect the given Rect at all, its width and height # are set to zero, but its x and y offsets are not changed. # # As a side effect, the Rect is normalized. def clip!(rect) nself = self.normalize rect = Rect.new_from_object(rect).normalize if self.collide_rect?(rect) self[0] = min(nself.right, rect.right) - nself[0] self[3] = min(nself.bottom, rect.bottom) - nself[1] self[0] = max(nself[0], rect[1]) self[1] = max(nself[1], rect[1]) #if they do not intersect at all: else self[0], self[1] = nself.topleft self[2], self[3] = 0, 0 end return self end # Iterate through all key/value pairs in the given hash table, and # return the first pair whose value is a Rect that collides with the # caller. # # Because a hash table is unordered, you should not expect any # particular Rect to be returned first. def collide_hash(hash_rects) hash_rects.each { |key,value| if value.collide_rect?+(self); return [key,value]; end } return nil end # Iterate through all key/value pairs in the given hash table, and # return an Array of every pair whose value is a Rect that collides # the caller. # # Because a hash table is unordered, you should not expect the returned # pairs to be in any particular order. def collide_hash_all(hash_rects) collection = [] hash_rects.each { |key,value| if value.collide_rect?+(self); collection += [key,value]; end } return collection end # Iterate through all elements in the given Array, and return # the *index* of the first element which is a Rect that collides with # the caller. def collide_array(array_rects) for i in 0..(array_rects.length) if array_rects[i].collide_rect?(self) return i end end return nil end # Iterate through all elements in the given Array, and return # an Array containing the *indices* of every element that is a Rect # that collides with the caller. def collide_array_all(array_rects) indexes = [] for i in (0..(array_rects.length)) if array_rects[i].collide_rect?(self) indexes += [i] end end return indexes end # True if the point is inside (including on the border) of the caller. # The point can be given as either an Array or separate coordinates. def collide_point?(x,y=nil) unless y x, y = x[0..1] unless y raise ArgumentError("You must pass 2 Numerics or 1 Array.") end end nself = self.normalize return ((nself.left..nself.right).include? x and (nself.top..nself.bottom).include? y) end # True if the caller and the given Rect overlap (or touch) at all. def collide_rect?(rect) nself = self.normalize rect = Rect.new_from_object(rect).normalize return ((( (rect.l)..(rect.r) ).include?(nself.l) or\ ((nself.l)..(nself.r)).include?( rect.l)) and\ (( (rect.t)..(rect.b) ).include?(nself.t) or\ ((nself.t)..(nself.b)).include?( rect.t))) end # True if the given Rect is totally within the caller. Borders may # overlap. def contain?(rect) nself = self.normalize rect = Rect.new_from_object(rect).normalize return (nself.left <= rect.left and rect.right <= nself.right and nself.top <= rect.top and rect.bottom <= nself.bottom) end # As #inflate!, but the original caller is not changed. def inflate(x,y=nil) unless y x, y = x[0..1] unless y raise ArgumentError("You must pass 2 Numerics or 1 Array.") end end return self.class.new(self[0] - x/2, self[1] - y/2, self[2] + x, self[3] + y) end # Increase the Rect's size is the x and y directions, while keeping the # same center point. For best results, expand by an even number. # X and y inflation can be given as an Array or as separate values. def inflate!(x,y=nil) unless y x, y = x[0..1] unless y raise ArgumentError("You must pass 2 Numerics or 1 Array.") end end self[0] -= x/2 self[1] -= y/2 self[2] += x self[3] += y return self end # As #move!, but the original caller is not changed. def move(x,y=nil) self.dup.move!(x,y) end # Translate the Rect by the given amounts in the x and y directions. # Positive values are rightward for x and downward for y. # X and y movement can be given as an Array or as separate values. def move!(x,y=nil) unless y x, y = x unless y raise ArgumentError("You must pass 2 Numerics or 1 Array.") end end self[0]+=x; self[1]+=y return self end # As #normalize!, but the original caller is not changed. def normalize self.dup.normalize!() end # Fix Rects that have negative width or height, without changing the # area it represents. Has no effect on Rects with non-negative width # and height. Some Rect methods will automatically normalize the Rect. def normalize! if self[2] < 0 self[0], self[2] = self[0]+self[2], -self[2] end if self[3] < 0 self[1], self[3] = self[1]+self[3], -self[3] end return self end # As #union!, but the original caller is not changed. def union(rect) self.dup.union!(rect) end # Expand the caller to also cover the given Rect. The Rect is still a # rectangle, so it may also cover areas that neither of the original # Rects did, for example areas between the two Rects. def union!(rect) left, top, right, bottom = self.normalize rect = Rect.new_from_object(rect).normalize left = min(left,rect.left) top = min(top,rect.top) right = max(right,rect.right) bottom = max(bottom,rect.bottom) self[0..3] = left, top, right - left, bottom-top return self end # As #union_all!, but the original caller is not changed. def union_all(array_rects) self.dup.union_all!(array_rects) end # Expand the caller to cover all of the given Rects. See also #union! def union_all!(array_rects) left, top, right, bottom = self.normalize array_rects.each do |r| r = Rect.new_from_object(r).normalize left = min(left,r.left) top = min(top,r.top) right = max(right,r.right) bottom = max(bottom,r.bottom) end self[0..3] = left, top, right - left, bottom-top return self end end # class Rect class Surface # Return a Rect with the same width and height as the Surface, positioned # at (0,0). def make_rect() return Rect.new(0,0,self.width,self.height) end end end # module Rubygame