Ir al contenido principal

Agregando un Popover a una aplicación Ruby Gtk

La aplicación Pomarola es un poquito distinta a los demás relojes pomodoro que he visto, básicamente la idea era generar un diario de trabajo basado en la cantidad de pomodoros trabajados por día. Es por eso que al fin me puse manos a la obra para poder guardarlos en un formato JSON. Para ello quería añadir un menu a mi aplicación. Resulta que los nuevos menues gtk son algo poco intuitivo de cara al desarrollo y por eso me costo bastante encontrarle la vuelta. Es por eso que aquí hay un pequeño tutorial de como agregar un menú tipo popover a nuestra aplicación.

![fit-width](/static/uploads/1471723826_Pantallazo-2016-08-20 17-06-14.png )

El boton de menu

Es conveniente utilizar el botón de menu provisto por GTK (GtkMenuButton), esto nos permitirá definir si lo que queremos es un menu popover. Para ello en las opciones del MenuButton seleccionamos "Ventana Emergente" y haciendo click en el pequeño icono de la entrada de texto creamos una nueva ventana emergente (nuestro popover). Este nos servirá en el código, más no podremos editarlo directamente desde aquí.

fit-width

Los archivos UI

Un popover no es simplemente un menú tradicional (en cuyo caso simplemente utilizariamos un TreeModel y lo poblaríamos. Sino más bien un widget contenedor con widgets hijos. Es por eso que debemos diseñar y guardar en un archivo ui aparte el contenedor de la siguiente manera:

fit-width

El código

Creamos nuestro popover en Glade, así como el archivo UI necesario, es por eso que ahora deberemos indicar en nuestro código que queremos utilizar este archivo para poblar los contenidos de nuestro popover:

  def generate_popover()
    @popover = @ui.get_object "popover1"
    @popover_ui = Gtk::Builder.new(:file => "./ui/menu.ui")
    @popover_contents = @popover_ui["popover"]

    @popover_ui.connect_signals do |handler|
        method(handler)
    end


    @popover.add @popover_contents
  end

Paso a paso

Obtenemos de nuestro archivo UI principal el objeto con el ID "popover1", este es el widget que mostrará el GtkMenuButton al pulsarlo, pero ahora mismo se encuentra vacío

    @popover = @ui.get_object "popover1"

Cargamos nuestro archivo de menu, y seleccionamos el widget con el ID "popover" dentro de el (quizás debería haber utilizado nombres distintos para evitar confusión)

    @popover_ui = Gtk::Builder.new(:file => "./ui/menu.ui")
    @popover_contents = @popover_ui["popover"]

Conectamos las señales de los widgets del popover (no es necesario hacerlo de esta forma)

    @popover_ui.connect_signals do |handler|
        method(handler)
    end

Añadimos los contenidos de este nuevo archivo UI a nuestro popover.

    @popover.add @popover_contents

Y listo! Como dije anteriormente el contenido de los popover es básicamente un Container Gtk (creo que más especificamente un Gtk::bin), por lo tanto podremos añadir dentro de el grillas, cajas, barras de progreso y botones esto posibilita menus complejos como los nuevos menues de gEdit y los de otras nuevas aplicaciones de Gnome.

Pomarola, evil-mode y otras cosas

Hola!

Despues de tanto tiempo vuelvo a bloguear. Sinceramente extrañaba mucho escribir por acá (ahora en nuevo dominio), ya que es una herramienta que me mantiene motivado a seguir aprendiendo cosas y mantiene un buen registro de las cosas que he aprendido previamente (lo cual me ha servido más de una vez).

Emacs, I3, RSI y Evil

Tanto joder con atajos de teclado, hace un tiempo me empezó a molestar la mano izquierda y me di cuenta que entre Tmux, i3 y Emacs estaba haciendo mucho uso de atajos de teclado con esta misma. Así que lamentablemente (a pesar de que me encontraba muy a gusto con ese manejador de ventanas) tuve que abandonar i3 y volví al tradicional escritorio MATE. No pensaba dejar Emacs tan fácilmente, pero si tengo que admitir que el lado oscuro (VIM) tiene atajos que no cansan tanto las muñecas, y que los modos, una vez que te acostumbrás son más fáciles. Es por eso que ahora mismo me encuentro utilizando evil-mode y god-mode que me emulan bastante el workflow de Vim sin perder todas las ventajas de Emacs (la indentación, paquetes, org-mode, etc)

Pomarola

Hace un tiempo que quiero implementar la técnica pomodoro y vi que la app para linux solo soporta el escritorio GNOME. Es así que me decidi a hacer una aplicación en Ruby para aplicar esta técnica a mi trabajo (inicialmente iba a ser en Go, pero la librería esta rota para gtk3.20). No es la gran cosa, pero lo bueno es que al finalizar un pomodoro, guarda la entrada debajo así podemos editar el nombre. La idea es eventualmente poder exportarla a diversos formatos y hacer una suerte de work log. Ahora mismo está muy verde (no se puede guardar permanentemente), pero como timer pomodoro básico funciona bastante bien (con notificaciones de escritorio y toda la cosa).

fit-width

Libros

Estoy leyendo la Trilogía del Asesino de Robin Hobb, y debo decir que me gusta bastante. Es muy similar (pero anterior) a Canción de Hielo y Fuego de GRRM, aunque bastante más centrado en un solo personaje. También comencé la saga de la Torre Obscura, pero realmente debo admitir que me cuesta muchísimo seguir la escritura de Stephen King.

Una canción

Esta canción la escuche en el CD Incompleto de Callejeros y la verdad me encantó:

Un Minuto

Frase #3

Que es un buen hombre sino un maestro para el hombre malo?

Que es un hombre malo sino un trabajo para el hombre bueno?

Si no entiendes esto te perderás, sin importar lo inteligente que seas

--Tao Te Ching, ch. 27

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
  • 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.

fit-width

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í

Haciendo un juego de plataformas con Ruby y Gosu

Como les comentaba en el post anterior, comencé a programar un pequeño juego en mis ratos libres. Para esto utilicé la librería Gosu, que hace que esto sea una tarea bastante sencilla.

Comenzando

Para mi juego dividí la aplicación en 5 clases, una para definir el entorno o sea el juego en sí, otra el mundo donde esta el código encargado de manejar la gravedad y eventualmente las colisiones entre objetos; luego esta la clase actor, donde defino cosas comunes a todos los actores que aparecen en el juego (bloques, personajes, NPCs? ) y dos clases que derivan de esta, player y block. La clase esta comentada explicando que hace cada cosa:

require "gosu"

require_relative "./lib/player"
require_relative "./lib/block"
require_relative "./lib/world"

class GameWindow < Gosu::Window

  def initialize
    super 800, 600
    self.caption =  "Game test"

    @world = World.new() # aquí inicializamos la clase mundo y le asignamos un par de valores 
    @world.viewport_height = self.height
    @world.viewport_width = self.width

    #Creamos un jugador y lo añadimos a nuestro mundo

    @player = Player.new
    @player.warp(200,@world.horizon ) 
    @world.add_actor(@player)    

    # Creamos un bloque y lo añadimos a nuestro mundo
    @block = Block.new
    @block.place(300,@world.horizon + @block.height)
    @world.add_actor(@block)

    # seteamos un fondo 
    @background_image = Gosu::Image.new("assets/images/bg.png", :tileable => true)
  end

 # El método update se encarga de capturar los distintos eventos de teclado
 # también llamamos al metodo de mundo gravity, que se encargará de que los objetos caigan
 # por ultimo el método move de la clase player se encargara de que el jugador se mueva

  def update
    if Gosu::button_down? Gosu::KbLeft 
       @player.accelerate :left
    end

    if Gosu::button_down? Gosu::KbRight 
        @player.accelerate :right
    end

    if Gosu::button_down? Gosu::KbUp 

        if !@player.falling
          @player.jump
        end

    end

    @world.gravity

    @player.move    
  end

# El método draw se encarga de dibujar todos los actores del mundo así como la imágen de fondo que hemos escogido

  def draw
    @world.show
    @background_image.draw(0, 0, 0)    
  end
end

#Instanciamos la ventana de juego y mostramos 
window = GameWindow.new

window.show

La clase actor

La clase actor define propiedades comunes de los actores del juego. Por ahora no hace mucho más que proveer un método comun de acceder a las propiedades del ancho y alto de la sprite de cada actor

class Actor
  attr_accessor :sprite, :x, :y, :angle, :mass, :falling, :mid_air, :height

  def width
    @sprite.width
  end

  def height
    @sprite.height
  end 

end

La clase player

Nuestra clase player hereda de actor, tiene los métodos necesarios para: posicionar el jugador, iniciar la aceleración cuando el jugador realiza un movimiento

require_relative "./actor"
require "pp"

class Player < Actor
  attr_accessor :vel_y, :vel_x, :acc, :x,:y
  def initialize
    super 
    @sprite = Gosu::Image.new("assets/images/player.png")

    @x = @y = @vel_x = @vel_y =  0.0

    @acc = 0.5
    @mass = 50
  end

  def warp(x,y)
    @x,@y = x,y
  end

  # este metodo se encarga de acelerar el jugador, dado que queremos disminuir la velocidad de aceleracion cuando estamos en el aire
  # el flag @midair determina si estamos en el aire o no

  def accelerate(angle)
    acc =  @mid_air ? 0.2 : @acc

    case angle
    when :right
      @vel_x += Gosu::offset_x(90, acc)
    when :left
      @vel_x += Gosu::offset_x(-90, acc)
    end

  end


  # Movemos el actor a las coordenadas deseadas
  def move
    @x += @vel_x
    @y += @vel_y


    @vel_x *= 0.95
    @vel_y *= 0.95

  end

  # Al saltar, definimos que estamos en el aire, y en tanto no llegemos a cierto punto permitimos acelerar el actor
  # esto permite graduar la fuerza del salto sin que el jugador pueda volar y permite activar la gravedad una vez que se llega al punto máximo

  def jump
    @mid_air = true
    if @vel_y.abs < 6.0
      @vel_y += Gosu::offset_y(1, 3.5)
    else
      @falling = true
    end
  end  

  # dibujamos el actor en la posicion indicada
  def draw
    @sprite.draw(@x,@y, 1 )
  end
end

La clase World

La clase world controla la interacción entre los actores del juego, asi como efectos como la gravedad que afectan a los actores

require "json"


class World
  attr_reader :actors,:gravity,:friction, :horizon
  attr_accessor :viewport_height, :viewport_width
  def initialize
    @actors = [] #arreglo que contiene los actores del juego
    @gravitational_force = 0.85 # constante de la fuerza de gravedad
    @gravity_acceleration = 0.0 # aceleración generada por fuerza de gravedad
  end

  # el horizonte sería nuestro suelo ¿tal vez debería llamarlo suelo?
  def horizon
    @viewport_height - 140    
  end

 # agrega un actor
  def add_actor(actor)
    @actors << actor
  end


  #define nuestras reglas de gravedad
  def gravity

    @actors.each {|actor|

        if actor.y >= horizon #Si nuestro actor se encuentra en el suelo detenemos la caida y toda aceleración vertical.
          if actor.falling  
            actor.vel_y = 0
          end
          @gravity_acceleration = 0
          actor.y = horizon
          actor.falling = false
          actor.mid_air = false
        elsif actor.vel_y.abs > 0.0 # aplicamos la fuerza de gravedad siempre y cuando el jugador no se encuentre en el suelo
          @gravity_acceleration = Gosu::offset_y(1, @gravitational_force)
          actor.vel_y -= @gravity_acceleration
        end

    }

  end

  def show
    @actors.each { |actor|
      actor.draw
    }


  end

end

El juego en acción

Como ya mencioné el código de este programa se puede descargar de github. Lo que quedaría es agregar un bloque, y programar las colisiones, aunque quizás no lo haga manualmente sino que cambie mi sistema de física por Chipmunk que parece algo mucho más completo y bien hecho