Haciendo un juego de plataformas con Ruby y Gosu (Parte II )

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

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

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.

fit-width

Consideraciones

Hay ciertas cosas a recordar trabajando con chipmunk:

Recuerden que pueden descargar el juego aquí

  • LinkedIn
  • Tumblr
  • Reddit
  • Google+
  • Pinterest
  • Pocket