require_relative 'movement'
module MiniGL
# This class represents an (optionally animated) image inside the game screen.
class Sprite
# The index of the current sprite in the spritesheet being drawn.
attr_reader :img_index
# The x-coordinate of the image in the screen.
attr_accessor :x
# The y-coordinate of the image in the screen.
attr_accessor :y
# Creates a new sprite.
#
# Parameters:
# [x] The x-coordinate in the screen (or map) where the sprite will be
# drawn. This can be modified later via the +x+ attribute.
# [y] The y-coordinate in the screen (or map) where the sprite will be
# drawn. This can be modified later via the +y+ attribute.
# [img] The path to a PNG image or spritesheet, following the MiniGL
# convention: images must be inside a 'data/img' directory, relative
# to the code file, and you must only provide the file name, without
# extension, in this case. If the image is inside a subdirectory of
# 'data/img', you must prefix the file name with each subdirectory
# name, followed by an underscore (so the file and directories names
# must not contain underscores). For example, if your image is
# 'data/img/sprite/1.png', you must provide "sprite_1"
# or +:sprite_1+.
# [sprite_cols] The number of columns in the spritesheet. Use +nil+ if the
# image is not a spritesheet.
# [sprite_rows] The number of rows in the spritesheet. Use +nil+ if the
# image is not a spritesheet.
def initialize(x, y, img, sprite_cols = nil, sprite_rows = nil, retro = nil)
@x = x; @y = y
retro = Res.retro_images if retro.nil?
@img =
if sprite_cols.nil?
[Res.img(img, false, false, '.png', retro)]
else
Res.imgs img, sprite_cols, sprite_rows, false, '.png', retro
end
@anim_counter = 0
@img_index = 0
@index_index = 0
@animate_once_control = 0
end
# Performs time checking to update the image index according to the
# sequence of indices and the interval.
#
# Parameters:
# [indices] The sequence of image indices used in the animation. The
# indices are determined from left to right, and from top to
# bottom, inside the spritesheet. All indices must be in the
# interval 0..(sprite_cols * sprite_rows)
.
# [interval] The amount of frames between each change in the image index.
# A frame will usually represent 1/60 second (roughly 17
# milliseconds).
def animate(indices, interval)
@animate_once_control = 0 if @animate_once_control != 0
@anim_counter += 1
if @anim_counter >= interval
@index_index += 1
@index_index = 0 if @index_index >= indices.length
@img_index = indices[@index_index]
@anim_counter = 0
end
end
# Causes the sprite to animate through the +indices+ array exactly once,
# so that the animation stops once it reaches the last index in the array.
# Subsequent calls with the same parameters will have no effect, but if
# the index or interval changes, or if +set_animation+ is called, then a
# new animation cycle will begin.
#
# Parameters:
# [indices] The sequence of image indices used in the animation. See
# +animate+ for details.
# [interval] The amount of frames between each change in the image index.
# See +animate+ for details.
def animate_once(indices, interval)
if @animate_once_control == 2
return if indices == @animate_once_indices && interval == @animate_once_interval
@animate_once_control = 0
end
unless @animate_once_control == 1
@anim_counter = 0
@img_index = indices[0]
@index_index = 0
@animate_once_indices = indices
@animate_once_interval = interval
@animate_once_control = 1
return
end
@anim_counter += 1
return unless @anim_counter >= interval
@index_index += 1
@img_index = indices[@index_index]
@anim_counter = 0
@animate_once_control = 2 if @index_index == indices.length - 1
end
# Resets the animation timer and immediately changes the image index to
# the specified value.
#
# Parameters:
# [index] The image index to be set.
def set_animation(index)
@anim_counter = 0
@img_index = index
@index_index = 0
@animate_once_control = 0
end
# Draws the sprite in the screen
#
# Parameters:
# [map] A Map object, relative to which the sprite will be drawn (the x
# and y coordinates of the sprite will be changed according to the
# position of the camera).
# [scale_x] A scale factor to be applied horizontally to the image.
# [scale_y] A scale factor to be applied vertically to the image.
# [alpha] The opacity with which the image will be drawn. Valid values
# vary from 0 (fully transparent) to 255 (fully opaque).
# [color] A color filter to apply to the image. A white (0xffffff) filter
# will keep all colors unchanged, while a black (0x000000) filter
# will turn all colors to black. A red (0xff0000) filter will keep
# reddish colors with slight or no change, whereas bluish colors
# will be darkened, for example.
# [angle] A rotation, in degrees, to be applied to the image, relative to
# its center.
# [flip] Specify +:horiz+ to draw the image horizontally flipped or +:vert+
# to draw it vertically flipped.
# [z_index] The z-order to draw the object. Objects with larger z-orders
# will be drawn on top of the ones with smaller z-orders.
# [round] Specify whether the drawing coordinates should be rounded to an
# integer before drawing, to avoid little distortions of the image.
# Only applies when the image is not rotated.
#
# *Obs.:* This method accepts named parameters.
def draw(map = nil, scale_x = 1, scale_y = 1, alpha = 0xff, color = 0xffffff, angle = nil, flip = nil, z_index = 0, round = false)
if map.is_a? Hash
scale_x = map.fetch(:scale_x, 1)
scale_y = map.fetch(:scale_y, 1)
alpha = map.fetch(:alpha, 0xff)
color = map.fetch(:color, 0xffffff)
angle = map.fetch(:angle, nil)
flip = map.fetch(:flip, nil)
z_index = map.fetch(:z_index, 0)
round = map.fetch(:round, false)
map = map.fetch(:map, nil)
end
color = (alpha << 24) | color
if angle
@img[@img_index].draw_rot @x - (map ? map.cam.x : 0) + @img[0].width * scale_x * 0.5,
@y - (map ? map.cam.y : 0) + @img[0].height * scale_y * 0.5,
z_index, angle, 0.5, 0.5, (flip == :horiz ? -scale_x : scale_x),
(flip == :vert ? -scale_y : scale_y), color
else
x = @x - (map ? map.cam.x : 0) + (flip == :horiz ? scale_x * @img[0].width : 0)
y = @y - (map ? map.cam.y : 0) + (flip == :vert ? scale_y * @img[0].height : 0)
@img[@img_index].draw (round ? x.round : x), (round ? y.round : y),
z_index, (flip == :horiz ? -scale_x : scale_x),
(flip == :vert ? -scale_y : scale_y), color
end
end
# Returns whether this sprite is visible in the given map (i.e., in the
# viewport determined by the camera of the given map). If no map is given,
# returns whether the sprite is visible on the screen.
def visible?(map = nil)
r = Rectangle.new @x, @y, @img[0].width, @img[0].height
return Rectangle.new(0, 0, G.window.width, G.window.height).intersect? r if map.nil?
map.cam.intersect? r
end
end
# This class represents an object with a set of properties and methods
# commonly used in games. It defines an object with a rectangular bounding
# box, and having all the attributes required for using the Movement module.
class GameObject < Sprite
include Movement
# Creates a new game object.
#
# Parameters:
# [x] The x-coordinate of the object's bounding box. This can be modified
# later via the +x+ attribute.
# [y] The y-coordinate of the object's bounding box. This can be modified
# later via the +y+ attribute.
# [w] The width of the object's bounding box.
# [h] The height of the object's bounding box.
# [img] The image or spritesheet for the object.
# [img_gap] A Vector object representing the difference between the top
# left corner of the bounding box and the coordinates of the
# image. For example, an object with x = 100
,
# y = 50
and img_gap = Vector.new(-5, -5)
# will be drawn at position (95, 45) of the screen.
# [sprite_cols] The number of columns in the spritesheet. Use +nil+ if the
# image is not a spritesheet.
# [sprite_rows] The number of rows in the spritesheet. Use +nil+ if the
# image is not a spritesheet.
# [mass] The mass of the object. Details on how it is used can be found
# in the Movement module.
def initialize(x, y, w, h, img, img_gap = nil, sprite_cols = nil, sprite_rows = nil, mass = 1.0, retro = nil)
super x, y, img, sprite_cols, sprite_rows, retro
@w = w; @h = h
@img_gap =
if img_gap.nil?
Vector.new 0, 0
else
img_gap
end
@mass = mass
@speed = Vector.new 0, 0
@max_speed = Vector.new 15, 15
@stored_forces = Vector.new 0, 0
end
# Draws the game object in the screen.
#
# Parameters:
# [map] A Map object, relative to which the object will be drawn (the x
# and y coordinates of the image will be changed according to the
# position of the camera).
# [scale_x] A scale factor to be applied horizontally to the image.
# [scale_y] A scale factor to be applied vertically to the image.
# [alpha] The opacity with which the image will be drawn. Valid values
# vary from 0 (fully transparent) to 255 (fully opaque).
# [color] A color filter to apply to the image. A white (0xffffff) filter
# will keep all colors unchanged, while a black (0x000000) filter
# will turn all colors to black. A red (0xff0000) filter will keep
# reddish colors with slight or no change, whereas bluish colors
# will be darkened, for example.
# [angle] A rotation, in degrees, to be applied to the image, relative to
# its center.
# [flip] Specify +:horiz+ to draw the image horizontally flipped or +:vert+
# to draw it vertically flipped.
# [z_index] The z-order to draw the object. Objects with larger z-orders
# will be drawn on top of the ones with smaller z-orders.
# [round] Specify whether the drawing coordinates should be rounded to an
# integer before drawing, to avoid little distortions of the image.
# Only applies when the image is not rotated.
#
# *Obs.:* This method accepts named parameters.
def draw(map = nil, scale_x = 1, scale_y = 1, alpha = 0xff, color = 0xffffff, angle = nil, flip = nil, z_index = 0, round = false)
if map.is_a? Hash
scale_x = map.fetch(:scale_x, 1)
scale_y = map.fetch(:scale_y, 1)
alpha = map.fetch(:alpha, 0xff)
color = map.fetch(:color, 0xffffff)
angle = map.fetch(:angle, nil)
flip = map.fetch(:flip, nil)
z_index = map.fetch(:z_index, 0)
round = map.fetch(:round, false)
map = map.fetch(:map, nil)
end
color = (alpha << 24) | color
if angle
center_x = @x + @w * 0.5
center_y = @y + @h * 0.5
o_x = center_x - @x - @img_gap.x
o_y = center_y - @y - @img_gap.y
@img[@img_index].draw_rot @x + (flip == :horiz ? -1 : 1) * (@img_gap.x + o_x) - (map ? map.cam.x : 0),
@y + (flip == :vert ? -1 : 1) * (@img_gap.y + o_y) - (map ? map.cam.y : 0),
z_index, angle, o_x.to_f / @img[0].width, o_y.to_f / @img[0].height,
(flip == :horiz ? -scale_x : scale_x), (flip == :vert ? -scale_y : scale_y), color
else
x = @x + (flip == :horiz ? -1 : 1) * @img_gap.x - (map ? map.cam.x : 0) + (flip == :horiz ? @w : 0)
y = @y + (flip == :vert ? -1 : 1) * @img_gap.y - (map ? map.cam.y : 0) + (flip == :vert ? @h : 0)
@img[@img_index].draw (round ? x.round : x), (round ? y.round : y),
z_index, (flip == :horiz ? -scale_x : scale_x),
(flip == :vert ? -scale_y : scale_y), color
end
end
# Returns whether this object is visible in the given map (i.e., in the
# viewport determined by the camera of the given map). If no map is given,
# returns whether the object is visible on the screen.
def visible?(map = nil)
r = Rectangle.new @x.round + @img_gap.x, @y.round + @img_gap.y, @img[0].width, @img[0].height
return Rectangle.new(0, 0, G.window.width, G.window.height).intersect? r if map.nil?
map.cam.intersect? r
end
end
# Represents a visual effect, i.e., a graphic - usually animated - that shows
# up in the screen, lasts for a given time and "disappears". You should
# explicitly dispose of references to effects whose attribute +dead+ is set
# to +true+.
class Effect < Sprite
# This is +true+ when the effect's lifetime has already passed.
attr_reader :dead
# Creates a new Effect.
#
# Parameters:
# [x] The x-coordinate in the screen (or map) where the effect will be
# drawn. This can be modified later via the +x+ attribute.
# [y] The y-coordinate in the screen (or map) where the effect will be
# drawn. This can be modified later via the +y+ attribute.
# [img] The image or spritesheet to use for this effect (see Sprite for
# details on spritesheets).
# [sprite_cols] (see Sprite)
# [sprite_rows] (see Sprite)
# [interval] The interval between steps of the animation, in updates.
# [indices] The indices to use in the animation. See Sprite#animate for
# details. If +nil+, it will be the sequence from 0 to
# sprite_cols * sprite_rows - 1
.
# [lifetime] The lifetime of the effect, in updates. After +update+ is
# called this number of times, the effect will no longer
# be visible, even when +draw+ is called, and the +dead+ flag
# will be set to +true+, so you get to know when to dispose
# of the Effect object. If +nil+, it will be set to
# @indices.length * interval
, i.e., the exact time
# needed for one animation cycle to complete.
# [sound] The id of a sound to be played when the effect is created (id must
# be given in the format specified for the +Res.sound+ method).
# [sound_ext] Extension of the sound file, if a sound is given. Default is
# '.wav'.
# [sound_volume] The volume (from 0 to 1) to play the sound, if any. Default
# is 1.
#
# *Obs.:* This method accepts named parameters, but +x+, +y+ and +img+ are
# mandatory.
def initialize(x, y = nil, img = nil, sprite_cols = nil, sprite_rows = nil, interval = 10, indices = nil, lifetime = nil,
sound = nil, sound_ext = '.wav', sound_volume = 1)
if x.is_a? Hash
y = x[:y]
img = x[:img]
sprite_cols = x.fetch(:sprite_cols, nil)
sprite_rows = x.fetch(:sprite_rows, nil)
interval = x.fetch(:interval, 10)
indices = x.fetch(:indices, nil)
lifetime = x.fetch(:lifetime, nil)
sound = x.fetch(:sound, nil)
sound_ext = x.fetch(:sound_ext, '.wav')
sound_volume = x.fetch(:sound_volume, 1)
x = x[:x]
end
super x, y, img, sprite_cols, sprite_rows
@timer = 0
if indices
@indices = indices
else
@indices = *(0..(@img.length - 1))
end
@interval = interval
if lifetime
@lifetime = lifetime
else
@lifetime = @indices.length * interval
end
Res.sound(sound, false, sound_ext).play(sound_volume) if sound
end
# Updates the effect, animating and counting its remaining lifetime.
def update
unless @dead
animate @indices, @interval
@timer += 1
@dead = true if @timer == @lifetime
end
end
def draw(map = nil, scale_x = 1, scale_y = 1, alpha = 0xff, color = 0xffffff, angle = nil, z_index = 0)
super unless @dead
end
end
end