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í
Comments
Comments powered by Disqus