#-- # 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 #++ module Rubygame # The Sprites module provides classes and mix-in modules for managing # sprites. A Sprite is, fundamentally, a Surface with an associated # Rect which defines the position of that Surface on the Screen. # Additionally, sprites can have some behavior associated with them # via the Sprite#update method, including movement and collision detection; # because of this, sprites can be the foundation of most on-screen objects, # such as characters, items, missiles, and even user-interface elements. # # There are several classes/modules under the Sprites module: # Sprite:: mix-in module to turn an individual object or class into # a Sprite. # Group:: class for containing and manipulating many Sprite objects. # UpdateGroup:: mix-in module for Group to allow undrawing/redrawing of # member Sprite objects. # LimitGroup:: mix-in module for Group to limit the number of Sprite objects # the Group can hold at once. # # More mix-in modules to extend the functionality of Group or Sprite are # planned for the future. Do not hesitate to change either (or both) Group # or Sprite within your application to fit your needs! That's what they are # here for! # module Sprites # The Sprite mix-in module (not to be confused with its parent module, # Sprites) can be used to extend a class or object to behave as a # sprite. Specifically, a sprite can: # - #draw (blit) itself onto a Surface in its proper position # - detect bounding-box collision with Groups (#collide_group) and/or other # sprites (#collide_sprite). # - #update its own state based on arbitrary rules. # # A Sprite is, fundamentally, a Surface with an associated # Rect which defines the position of that Surface on the Screen. # Additionally, sprites can have some behavior associated with them # via the #update method, including movement and collision detection; # because of this, sprites can be the foundation of most on-screen objects, # such as characters, items, missiles, and even user-interface elements. # # In order to work properly as a Sprite, the extended object or class must # have two methods defined (by default, these are defined as accessors to # attributes of the same names): # image:: return a Surface with the sprite's image. # rect:: returns a Rect with the position and dimensions of the sprite. # # Normally, the value returned by rect is used to draw the sprite # onto a Surface as well as to detect collision between sprites. However, # if @col_rect will be used for collision detection instead, if it is # defined. See also #col_rect. # # Additionally, if you are extending an already-existing instance (rather # than a class), that instance must have an attribute @groups, which is # an Array containing all Groups to which the sprite belongs. # # module Sprite attr_reader :groups attr_accessor :image, :rect # Initialize the Sprite, defining @groups. def initialize @groups = [] end # Add the Sprite to each given Group. def add(*groups) groups.each { |group| unless @groups.include? group @groups.push(group) group.push(self) end } end # call-seq: alive? -> true or false # # True if the Sprite belongs to at least one Group. def alive? return @groups.length > 0 end # Set an alternative Rect to use for collision detection. If undefined or # set to nil, the Sprite's #rect is used instead. attr_writer :col_rect # call-seq: col_rect -> Rect # # Returns @col_rect if it is defined, otherwise calls #rect. # This method is used by #collide, #collide_group, and #collide_sprite # to get the bounding box for collision detection. def col_rect if defined? @col_rect return (@col_rect or rect) else return rect end end # call-seq: collide(other) -> Array # # Check collision between the caller and a Group or another Sprite. See # also #collide_group and #collide_sprite. This method uses the value # of #col_rect as the bounding box when detecting collision. # # If +other+ is a Group, returns an Array of every Sprite in that # Group that collides with the caller. If +other+ is a Sprite, returns # an Array containing +other+ if it collides with the caller. Otherwise, # returns an empty Array. def collide(other) if other.class.is_a? Group return collide_group(other) elsif collide_sprite?(other) return [other] else return [] end end # call-seq: collide_group(group) -> Array # # Check collision between the caller and all members of a Group. This # method uses the value of #col_rect as the bounding box when detecting # collision. # # Returns an Array of every Sprite in +group+ that collides with the # caller. If none collide, returns an empty Array. def collide_group(group) sprites = [] group.each { |sprite| if self.collide_sprite?(sprite) and (not sprites.include?(sprite)) sprites.push(sprite) end } return sprites end # call-seq: collide_sprite?(sprite) -> true or false or nil # # Check collision between the caller and another Sprite. This method uses # the value of #col_rect as the bounding box when detecting collision. # Returns true if +sprite+ collides with the caller, false if they do # not, or nil if +sprite+ does not respond to either :col_rect or :rect # (meaning it was not possible to detect collision). def collide_sprite?(sprite) if sprite.respond_to?(:col_rect) return self.col_rect.collide_rect?(sprite.col_rect) elsif sprite.respond_to?(:rect) return self.col_rect.collide_rect?(sprite.rect) else return nil end end # call-seq: draw(surface) -> Rect # # Blit the Surface returned by #image onto +surface+ at the position # given by the Rect returned by #rect. Returns a Rect representing # the area of +surface+ which was affected by the draw. def draw(destination) self.image.blit(destination, self.rect) end # Remove the caller from every Group that it belongs to. def kill @groups.each { |group| group.delete(self) } @groups = [] end # Remove the caller from each given Group. def remove(*groups) groups.each { |group| if @groups.include? group @groups.delete(group) group.delete(self) end } end # This method is meant to be overwritten by Sprite-based classes to # define meaningful behavior. It can take any number of arguments you # want, be called however often you want, and do whatever you want. # It may return something, but Group#update will not (by default) use, # or even collect, return values from this method. # # An example definition might take the amount of time that has passed # since the last update; the Sprite could then update its position # accordingly. Game logic, collision detection, and animation would also # fit in here, if appropriate to your class. def update( *args ) super end end # module Sprite # The Group class is a special container, based on Array, with supplemental # methods for handling multiple Sprite objects. Group can draw, update, # and check collision for all its member sprites with one call. # # All members of a Group must be unique (duplicates will be refused), and # should be a Sprite (or functionally equivalent). class Group < Array # Add +sprite+ to the Group. +sprite+ is notified so that it can # add this Group to its list of parent Groups. See also #push. def <<(sprite) unless self.include? sprite super(sprite) sprite.add(self) end return self end # Call the method represented by +symbol+ for every member sprite, # giving +args+ as the arguments to the method. This method uses # Object#send, which does not hesitate to call private methods, so # use this wisely! See also #draw and #update. def call(symbol,*args) self.each { |sprite| sprite.send(symbol,*args) } end # Remove every member sprite from the Group. Each sprite is notified # so that it can remove this Group from its list of parent Groups. # See also #delete. def clear self.each { |sprite| sprite.remove(self) } end # call-seq: collide_sprite(sprite) -> Array # # Check collision between each member of the Group and +sprite+. Returns # an Array of all member sprites that collided with +sprite+. If none # collided, returns an empty Array. def collide_sprite(sprite) sprite.collide_group(self) end # call-seq: collide_group(group, klla=false, killb=false) -> Hash # # Check collision between each member of the calling Group and each # member of +group+. Returns a Hash table with each member of the calling # Group as a key, and as a value an Array of all members of +group+ that # it collided with. def collide_group(group, killa=false, killb=false) sprites = {} self.each { |sprite| sprites[sprite] = sprite.collide_group(group) } if killa sprites.each_key { |sprite| sprite.kill } end if killb sprites.each_value { |sprite| sprite.kill } end end # Remove each sprite in +sprites+ from the Group. Each sprite is notified # so that it can remove this Group from its list of parent Groups. def delete(*sprites) sprites.each { |sprite| if self.include? sprite super(sprite) sprite.remove(self) end } return self end # Draw every sprite on Surface +dest+. Calls Sprite#draw for every member # sprite, passing +dest+ as the argument. See also #call and #update. def draw(dest) self.each { |sprite| sprite.draw(dest) } end # Add each sprite in +sprites+ to the Group. Each sprite is notified so # that it can add this Group to its list of parent Groups. See also #<<. def push(*sprites) sprites.each { |sprite| self << sprite } return self end # Update every member sprite. Calls Sprite#update for every member # sprite, passing on all arguments. See also #call and #draw. def update(*args) self.each { |sprite| sprite.update(*args) } end end #class Group # UpdateGroup is a mix-in module that extends Group to allow it to "erase" # (i.e. draw over with a background Surface) and re-draw each sprite in its # new position. This eliminates the "trail" of images that sprites would # otherwise leave as they move around. # # UpdateGroup adds a new attribute, @dirty_rects, which is an Array storing # all Rects which need to be updated, including the old positions of the # sprites. This attribute is returned when UpdateGroup#draw is called, # so that it can be used to update the parts of the Screen that have # changed. # # The general order of calls each frame should be: # 1. #undraw; clear the old positions of the sprites. # 2. #update; update the sprites to their new positions. # 3. #draw; draw the sprites in their new positions. # # This module can extend either a class or an already-existing Group # instance (either empty or with members) without any special preparation. module UpdateGroup attr_accessor :dirty_rects # Defines @dirty_rects when an existing object is extended. def UpdateGroup.extend_object(obj) super obj.dirty_rects = [] end # Initialize the Group, calling +super+ and defining @dirty_rects. def initialize super @dirty_rects = [] end # call-seq: draw(dest) -> Array # # Draw every sprite on Surface +dest+. See Group#draw. # Returns an Array of Rects representing the portions of +dest+ which # were affected by the last #undraw and this #draw. def draw(dest) self.each { |sprite| @dirty_rects.push( sprite.draw(dest) ) } rects = @dirty_rects @dirty_rects = [] return rects end # Draw over part of +dest+ with image data from the corresponding part # of +background+. For best results, +background+ should be at least as # big as +dest+ (or, rather, the part of +dest+ that will ever be drawn # over). def undraw(dest,background) self.each { |sprite| background.blit(dest,sprite.rect,sprite.rect) @dirty_rects.push(sprite.rect) } end end # module UpdateGroup # LimitGroup is a mix-in module that extends Group to limit the number # of sprites it can contain. If the limit has been reached, each new sprite # will "push out" the oldest sprite, on a First-In-First-Out basis. # # The limit can be set either when the LimitGroup is created, or # at any time during execution. However, if you reduce the limit, # excess sprites will not be removed until the next time a sprite is # added. (You can work around this by pushing the last sprite in the # group again; the duplicate will be removed.) # # Please note that, because Group#push is defined in terms of Group#<<, # both LimitGroup#<< and LimitGroup#push work properly, even though only # LimitGroup#<< is defined. module LimitGroup attr_accessor :limit # Defines and sets @limit = 1 when an existing object is extended. def LimitGroup.extend_object(obj) super obj.limit = 1 end # Initialize the LimitGroup and define @limit (as 1, by default). def initialize(limit=1) @limit = limit end # Add +sprite+ to the LimitGroup, removing the oldest member if # necessary to keep under the limit. If +sprite+ is already in # the LimitGroup, it will be moved to the top of the queue. def <<(sprite) if not include? sprite super(sprite) while length > @limit self.slice!(0) end else # move sprite to the back of the queue self.delete(sprite) super(sprite) end end end # module LimitGroup end #module Sprite end # module Rubygame