require 'gosu' # The main module of the library, used only as a namespace. module AGL # A Struct with two attributes, x and y (in this order), representing a point # in a bidimensional space. Vector = Struct.new :x, :y # This class represents a rectangle by its x and y coordinates and width and # height. class Rectangle # The x-coordinate of the rectangle. attr_accessor :x # The y-coordinate of the rectangle. attr_accessor :y # The width of the rectangle. attr_accessor :w # The height of the rectangle. attr_accessor :h # Creates a new rectangle. # # Parameters: # [x] The x-coordinate of the rectangle. # [y] The y-coordinate of the rectangle. # [w] The width of the rectangle. # [h] The height of the rectangle. def initialize x, y, w, h @x = x; @y = y; @w = w; @h = h end # Returns whether this rectangle intersects another. # # Parameters: # [r] The rectangle to check intersection with. def intersects r @x < r.x + r.w && @x + @w > r.x && @y < r.y + r.h && @y + @h > r.y end end # The main class for a MiniGL game, holds references to globally accessible # objects and constants. class Game # Initializes a MiniGL game. This method must be called before any feature # provided by the library can be used. # # Parameters: # [window] An instance of a class which inherits from # Gosu::Window. This will be the game window, used # to draw everything and capture user input. # [gravity] A Vector object representing the horizontal and vertical # components of the force of gravity. Essentially, this force # will be applied to every object which calls +move+, from the # Movement module. # [kb_held_delay] The number of frames a key must be held by the user # before the "held" event (that can be checked with # KB.key_held?) starts to trigger. # [kb_held_interval] The interval, in frames, between each triggering of # the "held" event, after the key has been held for # more than +kb_held_delay+ frames. # [double_click_delay] The maximum interval, in frames, between two # clicks, to trigger the "double click" event # (checked with Mouse.double_click?). def self.initialize window, gravity = Vector.new(0, 1), kb_held_delay = 40, kb_held_interval = 5, double_click_delay = 8 @@window = window @@gravity = gravity @@kb_held_delay = kb_held_delay @@kb_held_interval = kb_held_interval @@double_click_delay = double_click_delay KB.initialize Mouse.initialize Res.initialize end # Returns a reference to the game window. def self.window; @@window; end # Returns a Vector representing the force of gravity. See +initialize+ for # details. def self.gravity; @@gravity; end # Returns the value of kb_held_delay. See +initialize+ for details. def self.kb_held_delay; @@kb_held_delay; end # Returns the value of kb_held_interval. See +initialize+ for details. def self.kb_held_interval; @@kb_held_interval; end # Returns the value of double_click_delay. See +initialize+ for details. def self.double_click_delay; @@double_click_delay; end end #class JSHelper # Exposes methods for controlling keyboard events. class KB # This is called by Game.initialize. Don't call it # explicitly. def self.initialize @@keys = [ Gosu::KbUp, Gosu::KbDown, Gosu::KbReturn, Gosu::KbEscape, Gosu::KbLeftControl, Gosu::KbRightControl, Gosu::KbLeftAlt, Gosu::KbRightAlt, Gosu::KbA, Gosu::KbB, Gosu::KbC, Gosu::KbD, Gosu::KbE, Gosu::KbF, Gosu::KbG, Gosu::KbH, Gosu::KbI, Gosu::KbJ, Gosu::KbK, Gosu::KbL, Gosu::KbM, Gosu::KbN, Gosu::KbO, Gosu::KbP, Gosu::KbQ, Gosu::KbR, Gosu::KbS, Gosu::KbT, Gosu::KbU, Gosu::KbV, Gosu::KbW, Gosu::KbX, Gosu::KbY, Gosu::KbZ, Gosu::Kb1, Gosu::Kb2, Gosu::Kb3, Gosu::Kb4, Gosu::Kb5, Gosu::Kb6, Gosu::Kb7, Gosu::Kb8, Gosu::Kb9, Gosu::Kb0, Gosu::KbNumpad1, Gosu::KbNumpad2, Gosu::KbNumpad3, Gosu::KbNumpad4, Gosu::KbNumpad5, Gosu::KbNumpad6, Gosu::KbNumpad7, Gosu::KbNumpad8, Gosu::KbNumpad9, Gosu::KbNumpad0, Gosu::KbSpace, Gosu::KbBackspace, Gosu::KbDelete, Gosu::KbLeft, Gosu::KbRight, Gosu::KbHome, Gosu::KbEnd, Gosu::KbLeftShift, Gosu::KbRightShift, Gosu::KbTab, Gosu::KbBacktick, Gosu::KbMinus, Gosu::KbEqual, Gosu::KbBracketLeft, Gosu::KbBracketRight, Gosu::KbBackslash, Gosu::KbApostrophe, Gosu::KbComma, Gosu::KbPeriod, Gosu::KbSlash ] @@down = [] @@prev_down = [] @@held_timer = {} @@held_interval = {} end # Updates the state of all keys. def self.update @@held_timer.each do |k, v| if v < Game.kb_held_delay; @@held_timer[k] += 1 else @@held_interval[k] = 0 @@held_timer.delete k end end @@held_interval.each do |k, v| if v < Game.kb_held_interval; @@held_interval[k] += 1 else; @@held_interval[k] = 0; end end @@prev_down = @@down.clone @@down.clear @@keys.each do |k| if Game.window.button_down? k @@down << k @@held_timer[k] = 0 if @@prev_down.index(k).nil? elsif @@prev_down.index(k) @@held_timer.delete k @@held_interval.delete k end end end # Returns whether the given key is down in the current frame and was not # down in the frame before. # # Parameters: # [key] Code of the key to be checked. The available codes are # Gosu::KbUp, Gosu::KbDown, Gosu::KbReturn, Gosu::KbEscape, # Gosu::KbLeftControl, Gosu::KbRightControl, # Gosu::KbLeftAlt, Gosu::KbRightAlt, # Gosu::KbA, Gosu::KbB, Gosu::KbC, Gosu::KbD, Gosu::KbE, Gosu::KbF, # Gosu::KbG, Gosu::KbH, Gosu::KbI, Gosu::KbJ, Gosu::KbK, Gosu::KbL, # Gosu::KbM, Gosu::KbN, Gosu::KbO, Gosu::KbP, Gosu::KbQ, Gosu::KbR, # Gosu::KbS, Gosu::KbT, Gosu::KbU, Gosu::KbV, Gosu::KbW, Gosu::KbX, # Gosu::KbY, Gosu::KbZ, Gosu::Kb1, Gosu::Kb2, Gosu::Kb3, Gosu::Kb4, # Gosu::Kb5, Gosu::Kb6, Gosu::Kb7, Gosu::Kb8, Gosu::Kb9, Gosu::Kb0, # Gosu::KbNumpad1, Gosu::KbNumpad2, Gosu::KbNumpad3, Gosu::KbNumpad4, # Gosu::KbNumpad5, Gosu::KbNumpad6, Gosu::KbNumpad7, Gosu::KbNumpad8, # Gosu::KbNumpad9, Gosu::KbNumpad0, Gosu::KbSpace, Gosu::KbBackspace, # Gosu::KbDelete, Gosu::KbLeft, Gosu::KbRight, Gosu::KbHome, # Gosu::KbEnd, Gosu::KbLeftShift, Gosu::KbRightShift, Gosu::KbTab, # Gosu::KbBacktick, Gosu::KbMinus, Gosu::KbEqual, Gosu::KbBracketLeft, # Gosu::KbBracketRight, Gosu::KbBackslash, Gosu::KbApostrophe, # Gosu::KbComma, Gosu::KbPeriod, Gosu::KbSlash. def self.key_pressed? key @@prev_down.index(key).nil? and @@down.index(key) end # Returns whether the given key is down in the current frame. # # Parameters: # [key] Code of the key to be checked. See +key_pressed?+ for details. def self.key_down? key @@down.index(key) end # Returns whether the given key is not down in the current frame but was # down in the frame before. # # Parameters: # [key] Code of the key to be checked. See +key_pressed?+ for details. def self.key_released? key @@prev_down.index(key) and @@down.index(key).nil? end # Returns whether the given key is being held down. See # Game.initialize for details. # # Parameters: # [key] Code of the key to be checked. See +key_pressed?+ for details. def self.key_held? key @@held_interval[key] == Game.kb_held_interval end end # Exposes methods for controlling mouse events. class Mouse # This is called by Game.initialize. Don't call it # explicitly. def self.initialize @@down = {} @@prev_down = {} @@dbl_click = {} @@dbl_click_timer = {} end # Updates the mouse position and the state of all buttons. def self.update @@prev_down = @@down.clone @@down.clear @@dbl_click.clear @@dbl_click_timer.each do |k, v| if v < Game.double_click_delay; @@dbl_click_timer[k] += 1 else; @@dbl_click_timer.delete k; end end k1 = [Gosu::MsLeft, Gosu::MsMiddle, Gosu::MsRight] k2 = [:left, :middle, :right] for i in 0..2 if Game.window.button_down? k1[i] @@down[k2[i]] = true @@dbl_click[k2[i]] = true if @@dbl_click_timer[k2[i]] @@dbl_click_timer.delete k2[i] elsif @@prev_down[k2[i]] @@dbl_click_timer[k2[i]] = 0 end end @@x = Game.window.mouse_x.round @@y = Game.window.mouse_y.round end # Returns the x-coordinate of the mouse cursor in the screen. def self.x; @@x; end # Returns the y-coordinate of the mouse cursor in the screen. def self.y; @@y; end # Returns whether the given button is down in the current frame and was # not down in the frame before. # # Parameters: # [btn] Button to be checked. Valid values are +:left+, +:middle+ and # +:right+ def self.button_pressed? btn @@down[btn] and not @@prev_down[btn] end # Returns whether the given button is down in the current frame. # # Parameters: # [btn] Button to be checked. Valid values are +:left+, +:middle+ and # +:right+ def self.button_down? btn @@down[btn] end # Returns whether the given button is not down in the current frame, but # was down in the frame before. # # Parameters: # [btn] Button to be checked. Valid values are +:left+, +:middle+ and # +:right+ def self.button_released? btn @@prev_down[btn] and not @@down[btn] end # Returns whether the given button has just been double clicked. # # Parameters: # [btn] Button to be checked. Valid values are +:left+, +:middle+ and # +:right+ def self.double_click? btn @@dbl_click[btn] end # Returns whether the mouse cursor is currently inside the given area. # # Parameters: # [x] The x-coordinate of the top left corner of the area. # [y] The y-coordinate of the top left corner of the area. # [w] The width of the area. # [h] The height of the area. def self.over? x, y, w, h @@x >= x and @@x < x + w and @@y >= y and @@y < y + h end end # This class is responsible for resource management. It keeps references to # all loaded resources until a call to +clear+ is made. Resources can be # loaded as global, so that their references won't be removed even when # +clear+ is called. # # It also provides an easier syntax for loading resources, assuming a # particular folder structure. All resources must be inside subdirectories # of a 'data' directory, so that you will only need to specify the type of # resource being loaded and the file name (either as string or as symbol). # There are default extensions for each type of resource, so the extension # must be specified only if the file is in a format other than the default. class Res # This is called by Game.initialize. Don't call it # explicitly. def self.initialize @@imgs = Hash.new @@global_imgs = Hash.new @@tilesets = Hash.new @@global_tilesets = Hash.new @@sounds = Hash.new @@global_sounds = Hash.new @@songs = Hash.new @@global_songs = Hash.new @@fonts = Hash.new @@global_fonts = Hash.new end # Returns a Gosu::Image object. # # Parameters: # [id] A string or symbol representing the path to the image. If the file # is inside 'data/img', only the file name is needed. If it's inside # a subdirectory, the id must be prefixed by each subdirectory name # followed by an underscore. Example: to load # 'data/img/sprite/1.png', provide +:sprite_1+ or "sprite_1". # [global] Set to true if you want to keep the image in memory until the # game execution is finished. If false, the image will be # released when you call +clear+. # [tileable] Whether the image should be loaded in tileable mode, which is # proper for images that will be used as a tile, i.e., that # will be drawn repeated times, side by side, forming a # continuous composition. # [ext] The extension of the file being loaded. Specify only if it is # other than ".png". def self.img id, global = false, tileable = false, ext = ".png" if global; a = @@global_imgs; else; a = @@imgs; end return a[id] if a[id] s = "data/img/" + id.to_s.split('_').join('/') + ext img = Gosu::Image.new Game.window, s, tileable a[id] = img end # Returns an array of Gosu::Image objects, using the image as # a spritesheet. The image with index 0 will be the top left sprite, and # the following indices raise first from left to right and then from top # to bottom. # # Parameters: # [id] A string or symbol representing the path to the image. See +img+ # for details. # [sprite_cols] Number of columns in the spritesheet. # [sprite_rows] Number of rows in the spritesheet. # [global] Set to true if you want to keep the image in memory until the # game execution is finished. If false, the image will be # released when you call +clear+. # [ext] The extension of the file being loaded. Specify only if it is # other than ".png". def self.imgs id, sprite_cols, sprite_rows, global = false, ext = ".png" if global; a = @@global_imgs; else; a = @@imgs; end return a[id] if a[id] s = "data/img/" + id.to_s.split('_').join('/') + ext imgs = Gosu::Image.load_tiles Game.window, s, -sprite_cols, -sprite_rows, false a[id] = imgs end # Returns an array of Gosu::Image objects, using the image as # a tileset. Works the same as +imgs+, except you must provide the tile # size instead of the number of columns and rows, and that the images will # be loaded as tileable. # # Parameters: # [id] A string or symbol representing the path to the image. It must be # specified the same way as in +img+, but the base directory is # 'data/tileset'. # [tile_width] Width of each tile, in pixels. # [tile_height] Height of each tile, in pixels. # [global] Set to true if you want to keep the image in memory until the # game execution is finished. If false, the image will be # released when you call +clear+. # [ext] The extension of the file being loaded. Specify only if it is # other than ".png". def self.tileset id, tile_width = 32, tile_height = 32, global = false, ext = ".png" if global; a = @@global_tilesets; else; a = @@tilesets; end return a[id] if a[id] s = "data/tileset/" + id.to_s.split('_').join('/') + ext tileset = Gosu::Image.load_tiles Game.window, s, tile_width, tile_height, true a[id] = tileset end # Returns a Gosu::Sample object. This should be used for # simple and short sound effects. # # Parameters: # [id] A string or symbol representing the path to the sound. It must be # specified the same way as in +img+, but the base directory is # 'data/sound'. # [global] Set to true if you want to keep the sound in memory until the # game execution is finished. If false, the sound will be # released when you call +clear+. # [ext] The extension of the file being loaded. Specify only if it is # other than ".wav". def self.sound id, global = false, ext = ".wav" if global; a = @@global_sounds; else; a = @@sounds; end return a[id] if a[id] s = "data/sound/" + id.to_s.split('_').join('/') + ext sound = Gosu::Sample.new Game.window, s a[id] = sound end # Returns a Gosu::Song object. This should be used for the # background musics of your game. # # Parameters: # [id] A string or symbol representing the path to the song. It must be # specified the same way as in +img+, but the base directory is # 'data/song'. # [global] Set to true if you want to keep the song in memory until the # game execution is finished. If false, the song will be released # when you call +clear+. # [ext] The extension of the file being loaded. Specify only if it is # other than ".ogg". def self.song id, global = false, ext = ".ogg" if global; a = @@global_songs; else; a = @@songs; end return a[id] if a[id] s = "data/song/" + id.to_s.split('_').join('/') + ext song = Gosu::Song.new Game.window, s a[id] = song end # Returns a Gosu::Font object. Fonts are needed to draw text # and used by MiniGL elements like buttons, text fields and TextHelper # objects. # # Parameters: # [id] A string or symbol representing the path to the song. It must be # specified the same way as in +img+, but the base directory is # 'data/font'. # [size] The size of the font, in pixels. This will correspond, # approximately, to the height of the tallest character when drawn. # [global] Set to true if you want to keep the font in memory until the # game execution is finished. If false, the font will be released # when you call +clear+. # [ext] The extension of the file being loaded. Specify only if it is # other than ".ttf". def self.font id, size, global = true, ext = ".ttf" if global; a = @@global_fonts; else; a = @@fonts; end id_size = "#{id}_#{size}" return a[id_size] if a[id_size] s = "data/font/" + id.to_s.split('_').join('/') + ext font = Gosu::Font.new Game.window, s, size a[id_size] = font end # Releases the memory used by all non-global resources. def self.clear @@imgs.clear @@tilesets.clear @@sounds.clear @@songs.clear @@fonts.clear end end end