require 'gosu' # The main module of the library, used only as a namespace. module MiniGL # This class represents a point or vector in a bidimensional space. class Vector # The x coordinate of the vector attr_accessor :x # The y coordinate of the vector attr_accessor :y # Creates a new bidimensional vector. # # Parameters: # [x] The x coordinate of the vector # [y] The y coordinate of the vector def initialize(x = 0, y = 0) @x = x @y = y end # Returns +true+ if both coordinates of this vector are equal to the # corresponding coordinates of +other_vector+, with +precision+ decimal # places of precision. def ==(other_vector, precision = 6) @x.round(precision) == other_vector.x.round(precision) and @y.round(precision) == other_vector.y.round(precision) end # Returns +true+ if at least one coordinate of this vector is different from # the corresponding coordinate of +other_vector+, with +precision+ decimal # places of precision. def !=(other_vector, precision = 6) @x.round(precision) != other_vector.x.round(precision) or @y.round(precision) != other_vector.y.round(precision) end # Sums this vector with +other_vector+, i.e., sums each coordinate of this # vector with the corresponding coordinate of +other_vector+. def +(other_vector) Vector.new @x + other_vector.x, @y + other_vector.y end # Subtracts +other_vector+ from this vector, i.e., subtracts from each # coordinate of this vector the corresponding coordinate of +other_vector+. def -(other_vector) Vector.new @x - other_vector.x, @y - other_vector.y end # Multiplies this vector by a scalar, i.e., each coordinate is multiplied by # the given number. def *(scalar) Vector.new @x * scalar, @y * scalar end # Divides this vector by a scalar, i.e., each coordinate is divided by the # given number. def /(scalar) Vector.new @x / scalar.to_f, @y / scalar.to_f end # Returns the euclidean distance between this vector and +other_vector+. def distance(other_vector) dx = @x - other_vector.x dy = @y - other_vector.y Math.sqrt(dx ** 2 + dy ** 2) end # Returns a vector corresponding to the rotation of this vector around the # origin (0, 0) by +radians+ radians. def rotate(radians) sin = Math.sin radians cos = Math.cos radians Vector.new cos * @x - sin * @y, sin * @x + cos * @y end # Rotates this vector by +radians+ radians around the origin (0, 0). def rotate!(radians) sin = Math.sin radians cos = Math.cos radians prev_x = @x @x = cos * @x - sin * @y @y = sin * prev_x + cos * @y end end # 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 intersect?(r) @x < r.x + r.w && @x + @w > r.x && @y < r.y + r.h && @y + @h > r.y end end # This module contains references to global objects/constants used by MiniGL. module G class << self # A reference to the game window. attr_accessor :window # Gets or sets the value of gravity. See # GameWindow#initialize for details. attr_accessor :gravity # Gets or sets the value of min_speed. See # GameWindow#initialize for details. attr_accessor :min_speed # Gets or sets the value of ramp_contact_threshold. See # GameWindow#initialize for details. attr_accessor :ramp_contact_threshold # Gets or sets the value of ramp_slip_threshold. See # GameWindow#initialize for details. attr_accessor :ramp_slip_threshold # Gets or sets the value of ramp_slip_force. See # GameWindow#initialize for details. attr_accessor :ramp_slip_force # Gets or sets the value of kb_held_delay. See # GameWindow#initialize for details. attr_accessor :kb_held_delay # Gets or sets the value of kb_held_interval. See # GameWindow#initialize for details. attr_accessor :kb_held_interval # Gets or sets the value of double_click_delay. See # GameWindow#initialize for details. attr_accessor :double_click_delay end end # The main class for a MiniGL game, holds references to globally accessible # objects and constants. class GameWindow < Gosu::Window # Creates a game window (initializing a game with all MiniGL features # enabled). # # Parameters: # [scr_w] Width of the window, in pixels. # [scr_h] Height of the window, in pixels. # [fullscreen] Whether the window must be initialized in full screen mode. # [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. # [min_speed] A Vector with the minimum speed for moving objects, i.e., the # value below which the speed will be rounded to zero. # [ramp_contact_threshold] The maximum horizontal movement an object can # perform in a single frame and keep contact with a # ramp when it's above one. # [ramp_slip_threshold] The maximum ratio between height and width of a ramp # above which the objects will always slip down when # trying to 'climb' that ramp. # [ramp_slip_force] The force that will be applied in the horizontal # direction when the object is slipping from a steep ramp. # [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?). # # *Obs.:* This method accepts named parameters, but +scr_w+ and +scr_h+ are # mandatory. def initialize(scr_w, scr_h = nil, fullscreen = true, gravity = Vector.new(0, 1), min_speed = Vector.new(0.01, 0.01), ramp_contact_threshold = 4, ramp_slip_threshold = 1.1, ramp_slip_force = 0.1, kb_held_delay = 40, kb_held_interval = 5, double_click_delay = 8) if scr_w.is_a? Hash scr_h = scr_w[:scr_h] fullscreen = scr_w.fetch(:fullscreen, true) gravity = scr_w.fetch(:gravity, Vector.new(0, 1)) min_speed = scr_w.fetch(:min_speed, Vector.new(0.01, 0.01)) ramp_contact_threshold = scr_w.fetch(:ramp_contact_threshold, 4) ramp_slip_threshold = scr_w.fetch(:ramp_slip_threshold, 1.1) ramp_slip_force = scr_w.fetch(:ramp_slip_force, 0.1) kb_held_delay = scr_w.fetch(:kb_held_delay, 40) kb_held_interval = scr_w.fetch(:kb_held_interval, 5) double_click_delay = scr_w.fetch(:double_click_delay, 8) scr_w = scr_w[:scr_w] end super scr_w, scr_h, fullscreen G.window = self G.gravity = gravity G.min_speed = min_speed G.ramp_contact_threshold = ramp_contact_threshold G.ramp_slip_threshold = ramp_slip_threshold G.ramp_slip_force = ramp_slip_force G.kb_held_delay = kb_held_delay G.kb_held_interval = kb_held_interval G.double_click_delay = double_click_delay KB.initialize Mouse.initialize Res.initialize end # Draws a rectangle with the size of the entire screen, in the given color. # # Parameters: # [color] Color of the rectangle to be drawn, in hexadecimal RRGGBB format. def clear(color) color |= 0xff000000 draw_quad 0, 0, color, width, 0, color, width, height, color, 0, height, color, 0 end # def toggle_fullscreen # # TODO # end end #class JSHelper # Exposes methods for controlling keyboard events. module KB class << self # This is called by GameWindow.initialize. Don't call it # explicitly. def 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 update @held_timer.each do |k, v| if v < G.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 < G.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 G.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 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 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 key_released?(key) @prev_down.index(key) and @down.index(key).nil? end # Returns whether the given key is being held down. See # GameWindow.initialize for details. # # Parameters: # [key] Code of the key to be checked. See +key_pressed?+ for details. def key_held?(key) @held_interval[key] == G.kb_held_interval end end end # Exposes methods for controlling mouse events. module Mouse class << self # The current x-coordinate of the mouse cursor in the screen. attr_reader :x # The current y-coordinate of the mouse cursor in the screen. attr_reader :y # This is called by GameWindow.initialize. Don't call it # explicitly. def initialize @down = {} @prev_down = {} @dbl_click = {} @dbl_click_timer = {} end # Updates the mouse position and the state of all buttons. def update @prev_down = @down.clone @down.clear @dbl_click.clear @dbl_click_timer.each do |k, v| if v < G.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] (0..2).each do |i| if G.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 = G.window.mouse_x.round @y = G.window.mouse_y.round 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 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 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 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 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. # # Alternate syntax # # over?(rectangle) # # Parameters: # [rectangle] A rectangle representing the area to be checked. def over?(x, y = nil, w = nil, h = nil) return @x >= x.x && @x < x.x + x.w && @y >= x.y && @y < x.y + x.h if x.is_a? Rectangle @x >= x && @x < x + w && @y >= y && @y < y + h end 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. module Res class << self # Get the current prefix for searching data files. This is the directory # under which 'img', 'sound', 'song', etc. folders are located. attr_reader :prefix # Gets the current path to image files (under +prefix+). Default is 'img'. attr_reader :img_dir # Gets the current path to tileset files (under +prefix+). Default is # 'tileset'. attr_reader :tileset_dir # Gets the current path to sound files (under +prefix+). Default is 'sound'. attr_reader :sound_dir # Gets the current path to song files (under +prefix+). Default is 'song'. attr_reader :song_dir # Gets the current path to font files (under +prefix+). Default is 'font'. attr_reader :font_dir # Gets or sets the character that is currently being used in the +id+ # parameter of the loading methods as a folder separator. Default is '_'. # Note that if you want to use symbols to specify paths, this separator # should be a valid character in a Ruby symbol. On the other hand, if you # want to use only slashes in Strings, you can specify a 'weird' character # that won't appear in any file name. attr_accessor :separator # This is called by GameWindow.initialize. Don't call it # explicitly. def initialize @imgs = {} @global_imgs = {} @tilesets = {} @global_tilesets = {} @sounds = {} @global_sounds = {} @songs = {} @global_songs = {} @fonts = {} @global_fonts = {} @prefix = File.expand_path(File.dirname($0)) + '/data/' @img_dir = 'img/' @tileset_dir = 'tileset/' @sound_dir = 'sound/' @song_dir = 'song/' @font_dir = 'font/' @separator = '_' end # Set a custom prefix for loading resources. By default, the prefix is the # directory of the game script. The prefix is the directory under which # 'img', 'sound', 'song', etc. folders are located. def prefix=(value) value += '/' if value != '' and value[-1] != '/' @prefix = value end # Sets the path to image files (under +prefix+). Default is 'img'. def img_dir=(value) value += '/' if value != '' and value[-1] != '/' @img_dir = value end # Sets the path to tilset files (under +prefix+). Default is 'tileset'. def tileset_dir=(value) value += '/' if value != '' and value[-1] != '/' @tileset_dir = value end # Sets the path to sound files (under +prefix+). Default is 'sound'. def sound_dir=(value) value += '/' if value != '' and value[-1] != '/' @sound_dir = value end # Sets the path to song files (under +prefix+). Default is 'song'. def song_dir=(value) value += '/' if value != '' and value[-1] != '/' @song_dir = value end # Sets the path to font files (under +prefix+). Default is 'font'. def font_dir=(value) value += '/' if value != '' and value[-1] != '/' @font_dir = value end # Returns a Gosu::Image object. # # Parameters: # [id] A string or symbol representing the path to the image. If the file # is inside +prefix+/+img_dir+, only the file name is needed. If it's # inside a subdirectory of +prefix+/+img_dir+, the id must be # prefixed by each subdirectory name followed by +separator+. Example: # to load 'data/img/sprite/1.png', with the default values of +prefix+, # +img_dir+ and +separator+, 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 img(id, global = false, tileable = false, ext = '.png') if global; a = @global_imgs; else; a = @imgs; end return a[id] if a[id] s = @prefix + @img_dir + id.to_s.split(@separator).join('/') + ext img = Gosu::Image.new G.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 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 = @prefix + @img_dir + id.to_s.split(@separator).join('/') + ext imgs = Gosu::Image.load_tiles G.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 # +prefix+/+tileset_dir+. # [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 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 = @prefix + @tileset_dir + id.to_s.split(@separator).join('/') + ext tileset = Gosu::Image.load_tiles G.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 # +prefix+/+sound_dir+. # [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 sound(id, global = false, ext = '.wav') if global; a = @global_sounds; else; a = @sounds; end return a[id] if a[id] s = @prefix + @sound_dir + id.to_s.split(@separator).join('/') + ext sound = Gosu::Sample.new G.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 # +prefix+/+song_dir+. # [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 song(id, global = false, ext = '.ogg') if global; a = @global_songs; else; a = @songs; end return a[id] if a[id] s = @prefix + @song_dir + id.to_s.split(@separator).join('/') + ext song = Gosu::Song.new G.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 # +prefix+/+font_dir+. # [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 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 = @prefix + @font_dir + id.to_s.split(@separator).join('/') + ext font = Gosu::Font.new G.window, s, size a[id_size] = font end # Releases the memory used by all non-global resources. def clear @imgs.clear @tilesets.clear @sounds.clear @songs.clear @fonts.clear end end end end