#!/usr/bin/env ruby
## gosu_chipmunk_demo.rb
##
## I'm posting this in the hope that it will be useful to someone, but I make no
## guarantee that it works and won't fail your system up. (BSD 4 life)
##
## Andy B - Tasty Sandwiches - http://www.neorab.com
# I tend to distribute the compiled libraries with any game that I write that
# uses gosu or chipmunk. Or rather I would if I ever finished one and managed
# to get it out to anyone. This script expects to find the libraries on the
# search path. If you installed from rubygems, be sure to require that first.
require 'gosu'
require 'chipmunk'
# Define the size in an easy to see place. Here.
SCREEN_WIDTH, SCREEN_HEIGHT = 640, 480
# Here we define the number of physics steps that will be done during each call
# to the update method. If you want to see how this can affect performance and
# collision detection, fiddle with it. DTIME is basically how long each substep
# is. Lower values will increates the accuracy while higher help performance.
SUBSTEPS, DTIME = 6, (1.0 / 60.0)
# This defines the shape of the Box that we draw later.
BOX_SHAPE_ARRAY = [
CP::Vec2.new(-8.0, -8.0),
CP::Vec2.new(-8.0, 8.0),
CP::Vec2.new(8.0, 8.0),
CP::Vec2.new(8.0, -8.0)
]
# All of the groups of boxes is set up at the begining of the scene, we are just
# starting them all off the screen and dropping them from different heights. So
# instead of having magic numbers all over the place, we'll set them here. For
# some of them, it's not a specific point, but a rand range. Play around with
# these, it's a bucket of little box fun. Whooo!
def first_bread_height
-rand(600)
end
MEAT_HEIGHT = [-900, -1300, -1700]
def meat_height(layer)
MEAT_HEIGHT[layer]
end
CHEESE_HEIGHT = [-2200, -2300]
def cheese_height(layer)
CHEESE_HEIGHT[layer]
end
def lettuce_height
-3000
end
def top_bread_height
-rand(400) - 3500
end
# Convenience method for converting from radians to a Vec2 vector.
class Numeric
def radians_to_vec2
CP::Vec2.new(Math::cos(self), Math::sin(self))
end
end
# This is a Container for a Gosu Image and a Chupmunk Body/Shape pair. It is
# largely just for a coninient method to create this item and a wrapper around
# it's draw routine.
class GosuMunk_Box
attr_reader :body, :shape
def initialize(space, image, x, y, color)
# Create a small body and the shape around it.
@body = CP::Body.new(1.0, 1.0)
@shape = CP::Shape::Poly.new(@body, BOX_SHAPE_ARRAY, CP::Vec2.new(0,0))
@image = image
@color = color
@shape.body.p = CP::Vec2.new(x, y) # position
@shape.body.v = CP::Vec2.new(0.0, 0.0) # velocity
@shape.body.a = (3 * Math::PI / 2.0) # angle in radians
space.add_body(@body)
space.add_shape(@shape)
end
def draw
@image.draw_rot(@shape.body.p.x, @shape.body.p.y, 1, @shape.body.a.radians_to_gosu, 0.5, 0.5, 1, 1, @color)
end
end
# Gosu::Window is exactly that, a window. It's the top level container for our
# scene environment. Normally I only use this for a basic state holder and pass
# the scene building, updating and drawing to another class for readability but
# for a single scene demo, this will work just as well.
class GameWindow < Gosu::Window
# When we create the window, we need to set all of the options for Gosu and
# the physics environment. While we don't strictly have to build our scene
# here, it makes a great deal more sense than doing it anywhere else.
def initialize
super(SCREEN_WIDTH, SCREEN_HEIGHT, false)
self.caption = "Tasty Sandwich Software"
# Some values for keeping track of FPS
@prev_frames = @frames = 0
@yester_second = Time.now.sec
# Font for easily drawing text
@font = Gosu::Font.new(self, "monospace", 40)
setup_scene
end
# Instead of filling up the initialize with method with all of this, I like to
# pull it out into a method by itself. For one, it makes the initialize much
# easier to read. Also, I am working on an external level format for a game
# and have found that a function that does the set up from a data file is much
# easier to write and maintain that filling everything into a single method.
def setup_scene
# We're going to be using a single image for all of the boxes.
# It's a 16x16 png of a white box with a black border.
@box_image = Gosu::Image.new(self, "crumb.png", true)
# Collection of Boxes
@boxes = Array.new
# Create our Space with a bit of resistance and simple gravity.
@space = CP::Space.new
@space.damping = 0.8
@space.gravity = CP::Vec2.new(0.0, 25.0)
# A static body with an infinite mass and inertia is handy for pinning
# objects to the window. We are not going to allow this body to move, so
# if we need something to stay put, we can attach it to this. Also, we can
# attach some shapes that can give us a non-mobile object to collide with.
static_body = CP::Body.new((1.0 / 0.0), (1.0 / 0.0));
# Here we are going to use line segments to create a "cup" of sorts to hold
# all of the little boxes we are going to drop down from the sky. When
# creating line segments, the vectors that are passed to the creation method
# are really just points on the screen. So first we create a line left to
# right across the window near the middle. Then on the left we create a
# line that slopes into the center just slightly. The we create a similar
# one on the right. All of these shapes are attached to the static_body.
@space.add_shape CP::Shape::Segment.new(static_body, CP::Vec2.new(0.0, 350.0), CP::Vec2.new(640.0, 350.0), 1.0)
@space.add_shape CP::Shape::Segment.new(static_body, CP::Vec2.new(25.0, 0.0), CP::Vec2.new(30.0, 350.0), 1.0)
@space.add_shape CP::Shape::Segment.new(static_body, CP::Vec2.new(615.0, 0.0), CP::Vec2.new(610.0, 350.0), 1.0)
# First Layer of Bread
color = Gosu::Color.new(0xffffffff)
(0..155).each do |i|
box = GosuMunk_Box.new(@space, @box_image, rand(20) + 300, first_bread_height, color)
@boxes.push box
end
# A few slices of "Meat" substance
color = Gosu::Color.new(0x33ffffff)
min, max = 10.0, 25.0
(0..2).each do |i|
# Create the starter box
last_box = GosuMunk_Box.new(@space, @box_image, 40 + rand(20), meat_height(i), color)
@boxes.push last_box
# String the boxes together with Slide Joints
(0..45).each do |j|
box = GosuMunk_Box.new(@space, @box_image, 40 + (j * 12) + rand(20), meat_height(i), color)
@boxes.push box
joint = CP::Joint::Slide.new(last_box.body, box.body, CP::Vec2.new(0, 0), CP::Vec2.new(0, 0), min, max)
@space.add_joint(joint)
last_box = box
end
end
# Double Cheese for Me, Please!
color = Gosu::Color.new(0xddff3311)
(0..1).each do |i|
# Create the starter box
last_box = GosuMunk_Box.new(@space, @box_image, 45 + (i * 50), cheese_height(i), color)
@boxes.push last_box
# String the boxes together with Pin Joints
(0..50).each do |j|
box = GosuMunk_Box.new(@space, @box_image, 55.0 + (j * 10.0) + (i * 50), cheese_height(i), color)
@boxes.push box
joint = CP::Joint::Pin.new(last_box.body, box.body, CP::Vec2.new(0, 0), CP::Vec2.new(0, 0))
@space.add_joint(joint)
last_box = box
end
end
# Lettuce... Or big green booger worm, whatever...
color = Gosu::Color.new(0xff00ff00)
# Create the starter box
last_box = GosuMunk_Box.new(@space, @box_image, 60.0, lettuce_height, color)
@boxes.push last_box
# String the boxes together with Groove Joints
(0..60).each do |i|
box = GosuMunk_Box.new(@space, @box_image, 68.0 + (i * 8.0), lettuce_height, color)
@boxes.push box
joint = CP::Joint::Groove.new(last_box.body, box.body, CP::Vec2.new(-8, 1), CP::Vec2.new(8, -1), CP::Vec2.new(-8, 0))
@space.add_joint(joint)
last_box = box
end
# Top Layer of Bread
color = Gosu::Color.new(0xffffffff)
(0..155).each do |i|
box = GosuMunk_Box.new(@space, @box_image, rand(580) + 40, top_bread_height, color)
@boxes.push box
end
end
# By default this is called 60 times per second. This is kind of broken into
# two sections, the Gosu section and the Chipmunk section. We need to handle
# our physics faster than our game logic. In fact, this scene has no game
# logic, but you should fiddle with a SUBSTEPS = 1 and understand why.
def update
# Perform the physics steps first
SUBSTEPS.times do
## Do Physics tasks here. Collisions, forces from the environment, etc.
# This tells chipmunk to calculate everything in the space for DTIME time.
@space.step(DTIME)
end
## Do Game Logic tasks here. See the Gosu Wiki (RubyChipmunkIntegration) for
## more details on what you might want to put here instead of above.
end
# Very Simple Draw Routine
def draw
@boxes.each { |box| box.draw }
# Write our Little Strings and the FPS indicator.
@font.draw("Tasty Sandwich Software", 50, 350, 2, 1.0, 1.0, 0xffffffff)
@font.draw("Ruby + Gosu + Chipmunk = Awesome", 50, 400, 2, 0.75, 0.75, 0xff00ffff)
@font.draw("fps: #{@prev_frames}", 0, 460, 2, 0.5, 0.5, 0xffff00ff)
# Update the FPS counters
if Time.now.sec != @yester_second
@prev_frames = @frames
@frames = 0
@yester_second = Time.new.sec
else
@frames += 1
end
end
# Give us a way out of this demo
def button_down(id)
if id == Gosu::Button::KbEscape
close
end
end
end
# Ztart zee zimulation!
window = GameWindow.new
window.show