Varias cosas

En las ultimas semanas hice varias cosas:

Juego

Tengo que hacer una entrada sobre las ultimas funcionalidades añadidas al juego. En las últimas iteraciones le he agregado animaciones, proyectiles, detección de daño, y soporte para varios jugadores. Igualmente falta mucho por hacer (menues, IA básico, scrolling de pantalla), aquí un video de como esta quedando:

Extensión de Nemo

Una de las cosas que me gustan del open source es que cuando encontramos cosas que no se adaptan exactamente a nuestro flujo de trabajo podemos ensuciarnos un poco las manos y tomar el código existente para lidiar con eso. En este caso encontré que Nemo estaba utilizando File-Roller de Gnome, mientras que yo suelo utilizar engrampa de MATE. Luego de buscar y ver que había gente que pedía esta funcionalidad decidí hacer un fork muy básico, ya que ambas herramientas utilizan los mismos flags en la linea de comandos (solo tuve que cambiár el nombre del binario)


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


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


Jueguitos en Ruby

Hace un tiempo en una de las meetups menos concurridas de Ruby Lit estuvimos viendo varios juegos hechos por @jjconti en Python. Ahí creo que surgió la pregunta si había algo parecido a PyGame en Ruby. Creería que no sabíamos de alternativas en su momento. Así que en un momento de aburrimiento me puse a investigar y me encontre con Gosu, una librería con la que es muy sencillo hacer juegos 2D en Ruby.

La creación de juegos no es ciertamente mi area de experiencia pero me resulto fácil crear un pequeño entorno, y un bastante precario sistema de gravedad. Aún estoy peleando un poco con como calcular y responder a las colisiones, pero eso lo hace aún más interesante para mí.

fit-width

enlace al ejemplo

Gosu

Edit: Por lo que veo en la página para física recomiendan Chipmunk, y hay también recomendaciones para frameworks sobre Gosu, los cuales seguramente voy a investigar.


Ruby and Crypto

Hace poco comencé un curso de criptografía en Coursera.org, la verdad ha sido todo un desafío encontrar el tiempo para ver los videos y hacer las tareas. Pero no obstante si bien estoy terminando cansadísimo he logrado más o menos entender e incorporar algunos conocimientos. Ayer por la noche terminé una de las tareas de programación que consistía en implementar un algoritmo para descifrar códigos de un One Time Pad mal utilizado . Decidí implementarlo en Ruby que es uno de los lenguajes que estoy aprendiendo.

One Time Pad

El One Time Pad es un método de criptografía que asegura perfect secrecy (secreto perfecto) entendiéndose por esto que si alguien tiene el texto cifrado no puede llegar al texto original. El One Time Pad es relativamente simple y se basa en generar una llave del mismo largo que el mensaje original y luego realizar una operación XOR entre el el texto original y la llave generada. El one time pad es rara vez implementado de manera pura en soluciones, ya que el componente teórico faltante es un verdadero generador de números aleatorios ( por lo cual recurrimos a los números pseudoaleatorios) en la vida real, por esto y por que generar llaves del mismo tamaño que el contenido original es en muchos casos poco práctico.


Errores en la implementación

Un error fatal en la implementación del One Time Pad es el uso de la misma llave generada para más de un mensaje, dado que siendo T el texto original y K la llave T XOR K da como resultado CT (mensaje cifrado), suponiendo que tenemos T1 y T2 cifrado con la misma llave por las propiedades de la operación XOR damos que:

T1 XOR K = CT1
T2 XOR K = CT2

CT1 XOR CT2 = T1 XOR T2

Esto no nos da de por si el mensaje original pero si el XOR de los mensajes originales, a partir de allí utilizaremos un método denominado cribbing que consiste en realizar una operación XOR con una frase que presupongamos se encuentra en el mensaje. ¿Que frase? es recomendable utilizar las más frecuentes en el idioma original del lenguaje, es así como si es inglés podremos probar utilizando "the" o "with" que son palabras frecuentemente encontradas en el idioma, si presuponemos que el mensaje se trata de un reporte del clima por ejemplo utilizaremos palabras relacionadas a esto.

Entonces utilizando esta palabra  denominada CRIB tenemos que al hacer ( CT1 XOR CT2 ) XOR CRIB (en cualquiera de las posiciones de CT1 XOR CT2) si la palabra existe en el segmento del mensaje original, se revelara el fragmento de carateres originales de uno de los mensajes.


Para realizar esto cree un pequeño programita que pueden ver en mi página de github. Estoy casi seguro que este programa contiene algunos errores, pero ya los depuraré y de momento me sirvió para descubrir el mensaje original.

Para realizar el programa aprendi bastante sobre arrays, y sobre la operación pack/unpack de Ruby que permite convertir cadenas ASCII a distintos formatos (binario, hex, etc), la operación zip, el operador XOR ^ y varias otras cosas, entre ellas que no se debe utilizar nunca más de una vez la misma llave de OTP.


Aplicaciones de escritorio con Ruby + Gtk

Hace poco en las reuniones mensuales de RubyLit, surgió el tema de la inexistencia de aplicaciones como Delphi o C++ Builder para hacer aplicaciones gráficas en GNU/Linux puntualmente con Ruby o Python. La realidad es que cuando uno se introduce al mundo del SL ve que las cosas están pensadas de una manera bastante distinta. Las suites integradas a veces resultan hasta antipáticas, y muchas de las aplicaciones estan pensadas no para ser utilizadas con un solo lenguaje, sino para varios. Sacrificando quizás las comodidades circunstanciales ( doble click en un widget para editar el código del evento ) por ventajas más interesantes como la utilización de una API similar y bindings para docenas de lenguajes ( como es el caso de GTK+).

A veces la ambición parece extrema, pero el caso es que eventualmente siempre resulta ventajosa.

Investigando un poco, me puse a ver si realmente era mucho más trabajoso hacer una aplicación GUI con Ruby en Linux y resulta que no solo no lo es, sino que el resultado es muy bueno y la capacidad de poder editar la interfaz como se nos venga la gana ( o aún más, permitir al usuario editarla como se le venga la gana sin modificar código) es genial.

A continuación un breve ejemplo de crear una aplicación con glade (es un cliente del api rest de redmine en que estoy trabajando):

necesitaremos la gema gtk3 y Glade

Primero creamos un nuevo archivo con glade y definimos los handlers que determinaran que hacer cada vez que el widget emite una señal, es importante guardar el archivo como .ui ya que así podremos utilizar directamente Gtk::Builder para leerlo, de otro modo necesitaríamos utilizar la librería glade

 Luego en nuestro programa:

#!/usr/bin/env ruby

require 'gtk3'

 

class MinerApp

  def initialize

    @ui = Gtk::Builder.new  

    @ui.add_from_file "ui/blueminer.ui"

    @ui.connect_signals {|handler| method(handler)}

 

    @main_window = @ui.get_object "mainwindow"

    @main_window.signal_connect "destroy" do

      Gtk.main_quit

    end

 

    @main_window.show_all 

  end

 

  def about_menuitem_activate_cb  

    dialog = @ui.get_object "aboutdialog1" 

     dialog.run #ejecutamos el dialogo

    dialog.hide

  end

end

Aquí lo que hacen algunas lineas:

    @ui = Gtk::Builder.new  

    @ui.add_from_file "ui/blueminer.ui"

instanciamos el objeto UI, y cargamos el archivo .ui este nos permite obtener instancias de nuestro widget ya definido.

    @ui.connect_signals {|handler| method(handler)}

esta es una linea interesante, aquí le pasamos un bloque que asigna a cada handler el método o función correspondiente, en este caso le estamos diciendo que llame un método de la clase actual que se llame igual que el handler, nótese que esto puede no ser lo ideal, ya que hace todos los metodos de la clase activables a través del nombre del handler. Pero resulta conveniente para este caso.

   @main_window = @ui.get_object "mainwindow"

   @main_window.show_all

Acá obtenemos el objeto por el identificador (ID) que le pusimos en el archivo glade, por defecto este es "window1". Luego llamamos el método show_all que muestra la ventana y redibuja todos sus contenidos.

 

 

  def about_menuitem_activate_cb  

    dialog = @ui.get_object "aboutdialog1" 

     dialog.run #ejecutamos el dialogo

     dialog.hide

  end

 

Si vemos la imagen, veremos que tengo definido un handler "about_menuitem_activate_cb" para el item de menu "Acerca de" y la señal "activate", este es el método que responde a dicho evento. Los dialogos Gtk se suelen ejecutar con el método run, que devuelve una respuesta de tipo Gtk::ResponseType al cerrar el dialogo, en este caso no queremos evaluar la respuesta ya que es solo un dialogo de información.

 

Finalmente instanciamos nuestra aplicación e inicializamos el loop Gtk

 

Gtk.init

miner = MinerApp.new

Gtk.main

 

El resto es simplemente continuar definiendo handlers para las señales en glade 

 Les dejo además estos tutoriales muy completos para Ruby y otros lenguajes

 

Pronto voy a subir esta app en mi repo github

 

Edit: Cuando usamos Gtk::Builder deberemos ocultar los dialogos luego de su ejecución en vez de destruirlos con destroy(), ya que a diferencia de al crearlos con Gtk::Dialog.new estamos destruyendo una referencia y al intentar abrir el diálogo nuevamente el programa lanzará una excepción.