#!/usr/bin/env ruby
# This program is released to the PUBLIC DOMAIN.
# It 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.
#
# This demo shows a simple example of game structure.
# It demonstrates how to use (or, one of many ways to use):
#
# * Clock to limit the maximum framerate (to keep CPU usage low)
#
# * Sprites to display player characters on the screen
#
# * EventQueue, EventHandler, and the HasEventHandler mixin to
# receive events from the keyboard, joystick, etc.
#
# * A custom Game class to integrate it all and provide the game
# structure and main loop.
#
require "rubygame"
# Include these modules so we can type "Surface" instead of
# "Rubygame::Surface", etc. Purely for convenience/readability.
include Rubygame
include Rubygame::Events
include Rubygame::EventActions
include Rubygame::EventTriggers
# Make text we output appear on the console right away.
$stdout.sync = true
# Use smooth scaling/rotating? You can toggle this with S key
$smooth = false
# Make sure everything is set up properly.
Rubygame.init()
# SDL_gfx is required for drawing shapes and rotating/zooming Surfaces.
$gfx_ok = (VERSIONS[:sdl_gfx] != nil)
unless ( $gfx_ok )
raise "You must have SDL_gfx support to run this demo!"
end
# Activate all joysticks so that their button press
# events, etc. appear in the event queue.
Joystick.activate_all
########################
# CUSTOM EVENT CLASSES #
########################
# Signals sprites to draw themselves on the screen
class DrawSprites
attr_accessor :screen
def initialize( screen )
@screen = screen
end
end
# Signals sprites to erase themselves from the screen
class UndrawSprites
attr_accessor :screen, :background
def initialize( screen, background )
@screen, @background = screen, background
end
end
######################
# AUTOLOADING IMAGES #
######################
# Set up autoloading for Surfaces. Surfaces will be loaded automatically
# the first time you use Surface["filename"]. Check out the docs for
# Rubygame::NamedResource for more info about that.
#
Surface.autoload_dirs = [ File.dirname(__FILE__) ]
#################
# PANDA CLASSES #
#################
# Base class for our panda sprites. This provides the core
# logic for initialization and movement of the sprites.
class Panda
include Sprites::Sprite
include EventHandler::HasEventHandler
# Autoload the "panda.png" image and set its colorkey
@@pandapic = Surface["panda.png"]
@@pandapic.set_colorkey(@@pandapic.get_at(0,0))
attr_accessor :vx, :vy, :speed
def initialize(x,y)
super()
@vx, @vy = 0,0
@speed = 40
@image = @@pandapic
@rect = Rect.new(x,y,*@@pandapic.size)
end
def update_image(time)
# do nothing in base class, rotate/zoom image in subs
end
def update( tick_event )
x,y = @rect.center
self.update_image( tick_event.seconds * 1000.0 )
@rect.size = @image.size
base = @speed * tick_event.seconds
@rect.centerx = x + @vx * base
@rect.centery = y + @vy * base
end
end
# A panda that spins around and around. The update_image
# method is called once per frame to generate the new
# image (in this case by rotating the original image).
class SpinnyPanda < Panda
attr_accessor :rate
def initialize(x,y,rate=0.1)
super(x,y)
@rate = rate
@angle = 0
end
def update_image(time)
@angle += (@rate * time) % 360
@image = @@pandapic.rotozoom(@angle,1,$smooth)
end
end
# A panda that grows and shrinks in size. Like the other
# panda classes, it updates its image every frame.
class ExpandaPanda < Panda
attr_accessor :rate
def initialize(x,y,rate=0.1)
super(x,y)
@rate = rate
@delta = 0
end
def update_image(time)
@delta = (@delta + time*@rate/36) % (Math::PI*2)
zoom = 1 + Math.sin(@delta)/2
@image = @@pandapic.zoom(zoom,$smooth)
end
end
# A panda that wobbles and jiggles. Like the other
# panda classes, it updates its image every frame.
class WobblyPanda < Panda
attr_accessor :rate
def initialize(x,y,rate=0.1)
super(x,y)
@rate = rate
@delta = 0
end
def update_image(time)
@delta = (@delta + time*@rate/36) % (Math::PI*2)
zoomx = (1.5 + Math.sin(@delta)/6) * @@pandapic.width
zoomy = (1.5 + Math.cos(@delta)/5) * @@pandapic.height
@image = @@pandapic.zoom_to(zoomx,zoomy,$smooth)
end
end
# Create the very cute panda objects!
panda1 = SpinnyPanda.new(100,50)
panda2 = ExpandaPanda.new(150,50)
panda3 = WobblyPanda.new(200,50,0.5)
# Set their depths. This affects which one appears in front
# of the other in case they overlap.
panda1.depth = 0 # in between the others
panda2.depth = 10 # behind both of the others
panda3.depth = -10 # in front of both of the others
###############
# PANDA GROUP #
###############
# Create a spritegroup to manage the pandas.
pandas = Sprites::Group.new
pandas.extend(Sprites::UpdateGroup)
pandas.extend(Sprites::DepthSortGroup)
# Add the pandas to the group.
pandas.push(panda1,panda2,panda3)
# Extend the pandas group with event hooks.
class << pandas
include EventHandler::HasEventHandler
# Draw all the sprites and refresh
# those parts of the screen
def do_draw( event )
dirty_rects = draw( event.screen )
event.screen.update_rects(dirty_rects)
end
# Erase the sprites from the screen by
# drawing over them with the background.
def do_undraw( event )
undraw( event.screen, event.background )
end
end
pandas.make_magic_hooks( :tick => :update,
DrawSprites => :do_draw,
UndrawSprites => :do_undraw )
##########
# SCREEN #
##########
# Create the SDL window
screen = Screen.set_mode([320,240])
screen.title = "Rubygame test"
screen.show_cursor = false;
###############
# BACKGROUND #
###############
# Make the background surface. We'll draw on this, then blit (copy) it
# onto the screen. We'll also use it as the background for "erasing"
# the pandas from their old positions each frame.
background = Surface.new( screen.size )
# Fill the background with a nice blue color.
background.fill( Color::ColorRGB.new([0.1, 0.2, 0.35]) )
# Render instructions with TTF (TrueType Font)
TTF.setup()
ttfont_path = File.join(File.dirname(__FILE__),"FreeSans.ttf")
ttfont = TTF.new( ttfont_path, 14 )
ttfont.render( "Use arrow keys or joystick to move pandas.",
true, [250,250,250] ).blit( background, [20,160] )
ttfont.render( "Press escape or q to quit.",
true, [250,250,250] ).blit( background, [20,180] )
############################
# EVENT HOOKS AND HANDLING #
############################
# Factory methods for creating event triggers
# Returns a trigger that matches the released key event
def released( key )
return KeyReleaseTrigger.new( key )
end
# Returns a trigger that matches the joystick axis event.
# There are no built-in joystick event triggers in Rubygame
# yet, sorry.
def joyaxis( axis )
return AndTrigger.new( InstanceOfTrigger.new( JoystickAxisMoved ),
AttrTrigger.new(:joystick_id => 0,
:axis => axis))
end
# Returns a trigger that matches the joystick button press event.
def joypressed( button )
return AndTrigger.new( InstanceOfTrigger.new( JoystickButtonPressed ),
AttrTrigger.new(:joystick_id => 0,
:button => button))
end
# Returns a trigger that matches the joystick button press event.
def joyreleased( button )
return AndTrigger.new( InstanceOfTrigger.new( JoystickButtonReleased ),
AttrTrigger.new(:joystick_id => 0,
:button => button))
end
#######################
# PANDA 1 EVENT HOOKS #
#######################
hooks = {
# Start moving when an arrow key is pressed
:up => proc { |owner, event| owner.vy = -1 },
:down => proc { |owner, event| owner.vy = 1 },
:left => proc { |owner, event| owner.vx = -1 },
:right => proc { |owner, event| owner.vx = 1 },
# Stop moving when the arrow key is released
released( :up ) => proc { |owner, event| owner.vy = 0 },
released( :down ) => proc { |owner, event| owner.vy = 0 },
released( :left ) => proc { |owner, event| owner.vx = 0 },
released( :right ) => proc { |owner, event| owner.vx = 0 },
# Move according to how far the joystick axis is moved
joyaxis( 0 ) => proc { |owner, event| owner.vx = event.value },
joyaxis( 1 ) => proc { |owner, event| owner.vy = event.value },
# Fast speed when button is pressed, normal speed when released
joypressed( 4 ) => proc { |owner, event| owner.speed *= 2.0 },
joyreleased( 4 ) => proc { |owner, event| owner.speed *= 0.5 }
}
panda1.make_magic_hooks( hooks )
#######################
# PANDA 2 EVENT HOOKS #
#######################
hooks = {
# Move according to how far the joystick axis is moved
joyaxis( 2 ) => proc { |owner, event| owner.vx = event.value },
joyaxis( 3 ) => proc { |owner, event| owner.vy = event.value },
# Fast speed when button is pressed, normal speed when released
joypressed( 5 ) => proc { |owner, event| owner.speed *= 2.0 },
joyreleased( 5 ) => proc { |owner, event| owner.speed *= 0.5 }
}
panda2.make_magic_hooks( hooks )
##############
# GAME CLASS #
##############
# The Game class helps organize thing. It takes events
# from the queue and handles them, sometimes performing
# its own action (e.g. Escape key = quit), but also
# passing the events to the pandas to handle.
#
class Game
include EventHandler::HasEventHandler
attr_reader :clock, :queue
def initialize( screen, background )
@screen = screen
@background = background
setup_clock()
setup_queue()
setup_event_hooks()
# Now blit the background onto the screen and update the screen
# once. During the loop, we'll use 'dirty rect' updating to
# refresh only the parts of the screen that have changed.
@background.blit(screen,[0,0])
@screen.update()
end
# The "main loop". Repeat the #step method
# over and over and over until the user quits.
def go
catch(:quit) do
loop do
step
end
end
end
# Register the object to receive all events.
# Events will be passed to the object's #handle method.
def register( *objects )
objects.each do |object|
append_hook( :owner => object,
:trigger => YesTrigger.new,
:action => MethodAction.new(:handle) )
end
end
private
# Quit the game
def quit
puts "Quitting!"
throw :quit
end
# Create a new Clock to manage the game framerate
# so it doesn't use 100% of the CPU
def setup_clock
@clock = Clock.new()
@clock.target_framerate = 50
# Adjust the assumed granularity to match the system.
# This helps minimize CPU usage on systems with clocks
# that are more accurate than the default granularity.
@clock.calibrate
# Make Clock#tick return a ClockTicked event.
@clock.enable_tick_events
end
# Set up the event hooks to perform actions in
# response to certain events.
def setup_event_hooks
hooks = {
:escape => :quit,
:q => :quit,
:s => :toggle_smooth,
QuitRequested => :quit,
# Tell the user where they clicked.
MousePressed => proc { |owner, event|
puts "click: [%d,%d]"%event.pos
},
# These help to ensure everything is refreshed after the
# Rubygame window has been covered up by a different window.
InputFocusGained => :update_screen,
WindowUnminimized => :update_screen,
WindowExposed => :update_screen,
# Refresh the window title.
:tick => :update_framerate
}
make_magic_hooks( hooks )
end
# Create an EventQueue to take events from the keyboard, etc.
# The events are taken from the queue and passed to objects
# as part of the main loop.
def setup_queue
# Create EventQueue with new-style events (added in Rubygame 2.4)
@queue = EventQueue.new()
@queue.enable_new_style_events
# Don't care about mouse movement, so let's ignore it.
@queue.ignore = [MouseMoved]
end
# Do everything needed for one frame.
def step
@queue << UndrawSprites.new( @screen, @background )
@queue.fetch_sdl_events
@queue << DrawSprites.new( @screen )
@queue << $game.clock.tick
@queue.each do |event|
handle( event )
end
end
# Toggle smooth effects
def toggle_smooth
$smooth = !$smooth
puts "#{$smooth?'En':'Dis'}abling smooth scale/rotate."
end
# Update the window title to display the current framerate.
def update_framerate( event )
new_framerate = @clock.framerate.to_i
unless @old_framerate == new_framerate
@screen.title = "Rubygame test [%d fps]"%new_framerate
@old_framerate = new_framerate
end
end
# Refresh the whole screen.
def update_screen
@screen.update()
end
end
$game = Game.new( screen, background )
# Register the pandas to receive events.
$game.register( pandas, panda1, panda2 )
# Start the main game loop. It will repeat forever
# until the user quits the game!
$game.go
# Make sure everything is cleaned up properly.
Rubygame.quit()