En el post anterior comente como comenzar a hacer juegos con la librería Gosu y Ruby. El ejemplo mostraba como implementar una función muy primitiva para simular la gravedad, pero dejaba a resolver cuestiones como cálculo de colisiones lo cual es muy importante. Para simplificar el trabajo encontré la librería Chipmunk que básicamente se encarga de hacer el trabajo pesado de calcular la física de nuestro juego, y además nos permite detectar y responder a distintas colisiones de una manera muy sencilla.
En este tutorial voy a mostrar como añadir Chipmunk al ejemplo anterior ( y ya que estamos cambie el tileset del juego con la ayuda de opengameart )
Conceptos de Chipmunk
Chipmunk añade varias abstracciones a nuestro juego en este ejempo hacemos uso de:
- Cuerpos
- Figuras de colisión
- Espacios
- Vectores
Cuerpos
El cuerpo es una representación de la estructura física de una entidad o actor de nuestro juego, básicamente contiene información como la posicion, fuerza y velocidad de un objeto, así como otras propiedades como masa, momento, elasticidad, fricción y demás.
Figuras de colisión
Las figuras de colisión básicamente manejan la forma en la que chipmunk representará nuestra figura estas pueden ser
- Circulos
- Segmentos
- Poligonos
Espacio
El espacio cumple una función muy similar a nuestra clase mundo, básicamente se encarga de hacer interactuar las distintas figuras y cuerpos entre si, también permite manipular las colisiones y activar callbacks o bloques en caso de producirse colisiones específicas.
Revisando lo que ya se hizo
Estos son los cambios a realizar:
La clase Actor pierde gran parte de sus propiedades que estarán ahora manejadas por CP::Body y CP::Shape, he escrito también algunos metodos de conveniencia como vec_from_size que permite establecer una forma (CP::Shape) CP::Vec2 a partir de un tamaño arbitrario o bien del tamaño del sprite . Le agregamos además la función draw, que dibuja el sprite a partir de un cuerpo, warp también ha cambiado, directamente alterando la posición del cuerpo.
require 'chipmunk'
class Actor
attr_accessor :sprite, :angle, :mass, :falling, :mid_air, :height
attr_reader :shape, :body
def vec_from_size
@width = @width ? width : @sprite.width
@height = @height ? height : @sprite.height
half_width = @width / 2
half_height = @height / 2
[CP::Vec2.new(-half_width,-half_height), CP::Vec2.new(-half_width, half_height), CP::Vec2.new(half_width, half_height), CP::Vec2.new(half_width,-half_height)]
end
def width
@width ? @width : @sprite.width
end
def height
@height ? @height: @sprite.height
end
def draw
@sprite.draw_rot(@body.p.x , @body.p.y , 1, @shape.body.a)
end
def mid_air
@body.v.y.abs > 0
end
def warp(x,y)
@body.p.x = x
@body.p.y = y
end
end
La clase player
La clase player cambia bastante, el constructor se encarga de establecer un cuerpo y una forma a partir de nuestro sprite ( que ha cambiado por este simpatico amigo por
cierto!). El método accelerate ahora solo incrementa un poco la velocidad del cuerpo hacia la izquierda o derecha. Y saltar hace lo mismo detectando que el actor no este en el aire (para evitar el doble salto)
require_relative "./actor"
require 'chipmunk'
require 'pp'
class Player < Actor
def initialize
@sprite = Gosu::Image.new("assets/images/player.png")
# agregamos un cuerpo dandole masa y
# momento le damos CP::INFINITY ya que no queremos que gire
@body = CP::Body.new(10, CP::INFINITY)
# Creamos la forma
@shape = CP::Shape::Poly.new(@body,vec_from_size,CP::Vec2.new(0,0) )
@shape.collision_type = :player #el tipo de colisión servirá para determinar que accion tomar ante distintas colisiones
@shape.e = 0.0 # Le quitamos elasticidad así nuestro personaje no rebota por todos lados
@shape.u = 1 # Le damos friccion
@shape.surface_v = CP::Vec2.new(1.0,1.0) #Velocidad de superficie
@body.w_limit = 0.5
end
def accelerate(angle)
case angle
when :right
@body.v.x = 3 * 0.85
when :left
@body.v.x = -3 * 0.85
end
end
def jump
if !mid_air
@body.v.y = -20 * 0.95
end
end
end
Mundo
El mundo ahora tiene menos atributos, conserva los actores, y añade uno nuevo, :space, lo inicializa determinando el damping ( una fuerza global de desaceleración, que evitara que nuestros objetos se aceleren indefinidamente ) y la gravedad
El método add actor ahora agrega la capacidad de añadir "rogue bodies", básicamente cuerpos que no serán manipulados por el espacio, esto es util para hacer cosas como el suelo o plataformas fijas
require "chipmunk"
class World
attr_reader :actors, :space
def initialize
@space = CP::Space.new()
@actors = []
@space.damping = 0.9
@space.gravity.y = 0.5
end
def add_actor(actor, rogue = false)
@actors << actor
if rogue #adds static shape to have a rogue body
@space.add_static_shape(actor.shape)
else
@space.add_body(actor.body)
@space.add_shape(actor.shape)
end
end
def show
@actors.each { |actor|
actor.draw
}
end
end
Clase Platform
Esta clase la cree para crear plataformas donde nuestro personaje se pueda subir, básicamente es igual a las demas solo que cuenta con 3 sprites para definir inicio, medio y final. También es una de las únicas done definimos arbitrariamente el tamaño en vez de tomarlo del tamaño del sprite, es por ello que sobrecargamos luego el metodo draw para poder dibujar correctamente la plataforma completa.
require_relative "./actor.rb"
require "chipmunk"
class Platform < Actor
attr_accessor :height
def initialize(width, height, angle = nil)
@body = CP::Body.new_static()
@width = width
@height = height
@sprite_start = Gosu::Image.new("assets/images/platform_start.png")
@sprite = Gosu::Image.new("assets/images/platform_body.png")
@sprite_end = Gosu::Image.new("assets/images/platform_end.png")
@shape = CP::Shape::Poly.new(@body,vec_from_size,CP::Vec2.new(0,0) )
if angle
@body.a = angle
end
@shape.collision_type = :platform
end
def draw
tiles = (@width / @sprite.width) / 2
(-tiles..tiles).each do |i|
if i == -tiles
@sprite_start.draw_rot(@body.p.x + (@sprite.width * i ) + 32 ,@body.p.y , 1, @body.a)
elsif i > -tiles && i < tiles -1
@sprite.draw_rot(@body.p.x + (@sprite.width * i ) + 32 ,@body.p.y , 1, @body.a)
elsif i == tiles -1
@sprite_end.draw_rot(@body.p.x + (@sprite.width * i ) + 32 ,@body.p.y , 1, @body.a)
end
end
end
end
Clase Ground
Ahora que tenemos física necesitamos un lugar a donde caer. La clase ground es muy similar a platform aunque un poco más simple. (quizas platform la hace obsoleta)
require_relative "./actor.rb"
require "chipmunk"
class Ground < Actor
attr_accessor :height
def initialize
@body = CP::Body.new_static()
@sprite = Gosu::Image.new("assets/images/ground.png")
@width = 1200
@height = 84
@shape = CP::Shape::Poly.new(@body,vec_from_size,CP::Vec2.new(0,0) )
@shape.collision_type = :ground
end
def draw
tiles = (@width / @sprite.width) / 2
(-tiles..tiles).each do |i|
@sprite.draw_rot(@body.p.x + (@sprite.width * i ) ,@body.p.y , 1, @body.a)
end
end
end
Actualizando nuestro juego
Ahora es momento de editar nuestro archivo principal game.rb y hacer que las cosas interactuen entre sí. Afortunadamente ahora esto es muy sencillo ya que la mayoría de nuestras clases manejan todo lo necesario, lo único que cambia es que ahora en vez de llamar a distintos metodos de World para la gravedad y demás, simplemente llamamos al método step de @world.space con parametro 1, lo cual avanzara la simulación una unidad de tiempo.
Ah dado que cambiamos el tileset, la funcion de dibujar el fondo también cambia un poquito.
require "gosu"
require_relative "./lib/player"
require_relative "./lib/crate"
require_relative "./lib/platform"
require_relative "./lib/world"
require_relative "./lib/ground"
class GameWindow < Gosu::Window
def initialize
super 1024, 768
self.caption = "Game test"
@world = World.new()
@player = Player.new
@player.warp(200,128) #position the player
@world.add_actor(@player)
@ground = Ground.new
@ground.warp(600,726) #position the ground
@world.add_actor(@ground,true)
@platform = Platform.new(256,64)
@platform.warp(256,128)
@world.add_actor(@platform,true)
@platform = Platform.new(256,64)
@platform.warp(640,128)
@world.add_actor(@platform,true)
@platform = Platform.new(256,64)
@platform.warp(512,256)
@world.add_actor(@platform,true)
@platform = Platform.new(256,64)
@platform.warp(256,512)
@world.add_actor(@platform,true)
@platform = Platform.new(256,64)
@platform.warp(512,640)
@world.add_actor(@platform,true)
@crate = Crate.new
@crate.warp(640,128)
@world.add_actor(@crate)
@crate = Crate.new 3
@crate.warp(256,128)
@world.add_actor(@crate)
@crate = Crate.new 2
@crate.warp(600,350)
@world.add_actor(@crate)
@background_image = Gosu::Image.new("assets/images/bg.png", :tileable => true)
end
def update
if Gosu::button_down? Gosu::KbLeft #or Gosu::button_down? Gosu::GpLeft then
@player.accelerate :left
end
if Gosu::button_down? Gosu::KbRight #or Gosu::button_down? Gosu::GpRight then
@player.accelerate :right
end
if Gosu::button_down? Gosu::KbUp #or Gosu::button_down? Gosu::GpRight then
@player.jump
end
@world.space.step 1
end
def draw
@world.show
tiles_x = 1024 / @background_image.width
tiles_y = 768 / @background_image.height
tiles_x.times { |i|
tiles_y.times {|j|
@background_image.draw(i * @background_image.width, j * @background_image.height, 0)
}
}
end
end
window = GameWindow.new
window.show
El resultado un simpático robot en una fábrica que puede empujar cajas y otros objetos. También subi un video de etapas más tempranas del desarrollo usando el antiguo tileset.

Consideraciones
Hay ciertas cosas a recordar trabajando con chipmunk:
- Chipmunk y gosu expresan los angulos y vectores de manera distinta, chipmunk simplemente indica puntos en un eje relativo al cuerpo, gosu lo expresa en función de un angulo y distancia.
- CP::INFINITY es un valor que representa infinito, y es útil en algunos casos como por ejemplo cuando no queremos que un actor gire sobre si mismo.
- Chipmunk no maneja fricción con objetos en rotación, eso hace que sea más importante CP::INFINITY
- Si añadis un cuerpo al espacio simulado este va a ser afectado por la simulación, si solo añadís la forma, esta va a afectar a los demás pero el propio cuerpo no se vera afectado ( a menos que arbitrariamente se modifique como en caso de ascensores y demás) esto sería un "rogue body"
Recuerden que pueden descargar el juego aquí