Jogos em Ruby com Gosu

Alan Willms
7 min readMay 6, 2015

--

Jogo que você desenvolverá ao longo do tutorial

(Observação: essa é uma tradução do tutorial em inglês. Para seguir este tutorial, baixe os seguintes assets: Space.png, Starfighter.bmp, Star.png e Beep.wav. Para ver o código-fonte "colorido", acesse o tutorial no Github.)

Código fonte

Este e outros jogos de exemplo estão inclusos com o restante da biblioteca. Por exemplo, no Mac OS X 10.5, os exemplos podem ser encontrados em /Library/Ruby/Gems/1.8/gems/gosu-<version>/examples.

Se você não tem um editor que suporta a execução direta do arquivo (TextMate, SciTE…), então basta dar um cdnesse diretório e executar com ruby Tutorial.rb.

Vamos aos Negócios

1. Sobrescrevendo os callbacks da janela

A forma mais fácil de criar uma aplicação Gosu completa é escrever uma nova classe que deriva de Gosu::Window(consulte a documentação para uma descrição completa de sua interface). Uma classe GameWindow mínima se parece com a seguinte:

require 'gosu'class GameWindow < Gosu::Window
def initialize
super 640, 480, false
self.caption = "Gosu Tutorial Game"
end
def update
end
def draw
end
end
window = GameWindow.new
window.show

O construtor inicializa a classe base Gosu::Window. Os parâmetros passados criam uma janela de 640 por 480 pixels, sem estar em tela cheia. Ele também muda o título da janela, que até então estava vazio.

update() e draw() sobrescrevem os métodos de mesmo nome da Gosu::Window’. O método update() é chamado 60 vezes por segundo (por padrão) e deve conter a lógica principal do jogo: mover objetos, tratar colisões, etc.

O método draw() é chamado depois dele, e também em qualquer momento que a janela precisar redesenhar por outros motivos, e também eventualmente pode ser ignorado se a taxa de FPS (frames por segundo) baixar muito. Ele deve conter o código para redesenhar a tela inteira, e nenhuma lógica.

Então segue o programa principal. Uma janela é criada e seu método show() é chamado, que não retorna nada até que a janela tenha sido fechada pelo usuário ou pelo próprio código. Ta-dã — agora você tem uma pequena janela preta com o título que você escolheu!

Um diagrama do loop principal é explicado na página Loop Principal da Janela.

2. Usando Imagens

require 'gosu'class GameWindow < Gosu::Window
def initialize
super(640, 480, false)
self.caption = "Gosu Tutorial Game"
@background_image = Gosu::Image.new(self, "media/Space.png", true)
end
def update
end
def draw
@background_image.draw(0, 0, 0)
end
end
window = GameWindow.new
window.show

Gosu::Image#initialize recebe três argumentos. Primeiramente, como todos os recursos de mídia, ela está atrelada a uma janela (self). Todos os recursos Gosu precisam de uma Window para a inicialização e conterão uma referência interna à mesma. Em segundo lugar, o nome do arquivo da imagem é passado. O terceiro argumento especifica se a imagem deve ser criada com bordas opacas. Consulte a página Conceitos Básicos para uma explicação melhor.

Conforme mencionado na seção anterior, o método draw() da janela é o lugar certo para desenhar tudo, então é aqui que vamos desenhar nossa imagem de fundo.

A maioria dos argumentos é óbvia. A imagem é desenhada na posição (0;0) — o terceiro argumento é a posição Z; novamente, consulte a página Conceitos Básicos.

2.1 Jogador & movimento

Segue aqui uma classe Player (jogador) simples:

class Player
def initialize(window)
@image = Gosu::Image.new(window, "media/Starfighter.bmp", false)
@x = @y = @vel_x = @vel_y = @angle = 0.0
@score = 0
end
def warp(x, y)
@x, @y = x, y
end
def turn_left
@angle -= 4.5
end
def turn_right
@angle += 4.5
end
def accelerate
@vel_x += Gosu::offset_x(@angle, 0.5)
@vel_y += Gosu::offset_y(@angle, 0.5)
end
def move
@x += @vel_x
@y += @vel_y
@x %= 640
@y %= 480
@vel_x *= 0.95
@vel_y *= 0.95
end
def draw
@image.draw_rot(@x, @y, 1, @angle)
end
end

Há algumas observações a fazer sobre isso:

  • Player#accelerate utiliza os métodos offset_x e offset_y. Eles são semelhantes a aquilo para o qual muitas pessoas usam senos e cossenos: por exemplo, se alguma coisa se mover 100 pixels em um ângulo de 30°, se deslocaria offset_x(30, 100) pixels horizontalmente e offset_y(30, 100) pixels verticalmente.
  • Ao carregar arquivos BMP, o Gosu substitui a cor 0xff00ff (fúcsia/magenta; aquele cor-de-rosa feio) com pixels transparentes.
  • Perceba que o método draw_rot coloca o centro da imagem em (x; y) — e não o canto superior esquerdo como o método draw faz! Isso pode ser controlado pelos argumentos center_x e center_y se você quiser.
  • O jogador é desenhado em z=1, ou seja, por cima do fundo (é óbvio). Vamos substituir estes números fixos com algo melhor depois.
  • Além disso, consulte o rdoc para conhecer todos os métodos de desenho e seus argumentos.

2.2 Integrando o Jogador com a Janela

class GameWindow < Gosu::Window
def initialize
super(640, 480, false)
self.caption = "Gosu Tutorial Game"
@background_image = Gosu::Image.new(self, "media/Space.png", true) @player = Player.new(self)
@player.warp(320, 240)
end
def update
if button_down? Gosu::KbLeft or button_down? Gosu::GpLeft then
@player.turn_left
end
if button_down? Gosu::KbRight or button_down? Gosu::GpRight then
@player.turn_right
end
if button_down? Gosu::KbUp or button_down? Gosu::GpButton0 then
@player.accelerate
end
@player.move
end
def draw
@player.draw
@background_image.draw(0, 0, 0);
end
def button_down(id)
if id == Gosu::KbEscape
close
end
end
end
window = GameWindow.new
window.show

Como você pode ver, nós introduzimos o uso de um teclado ou joystick!

De forma semelhante aos métodos update() e draw(), a Gosu::Window fornece dois métodos: button_down(id) ebutton_up(id), que podem ser sobrescritos e não fazem nada por padrão. Aqui nós sobrescrevemos button_down(id)para fechar a janela com a tecla ESC. (Para ver uma lista com as constantes de teclas e botões, consulte o rdoc).

Embora obter feedback de botões pressionados é algo útil para eventos que acontecem uma vez só como interações com a interface, pulos e digitação, isso é meio inútil para ações que duram vários frames — por exemplo, mover-se segurando teclas pressionadas. É aqui que o método update() entra em campo, onde só chama os métodos de movimento do objeto player. Se você executar o código desse exemplo, você vai conseguir voar com sua nave por aí!

3. Animações Simples

Primeiramente, vamos nos livrar dos números que fixamos para as posições Z. De agora em diante vamos utilizar as seguintes constantes:

module ZOrder
Background, Stars, Player, UI = *0..3
end

O que é uma animação? É uma sequência de imagens — então vamos usar a classe Array do Ruby para armazená-las. (Para um jogo de verdade, não tem como escapar de escrever classes específicas para as necessidades individuais do jogo, mas nós vamos mostrar a solução simples por enquanto.)

Vamos introduzir as estrelas, que são o objeto principal deste tutorial. As estrelas aparecem aleatoriamente na tela e vivem sua vida animada até que o jogador as colete. A definição da classe Star é muito simples:

class Star
attr_reader :x, :y
def initialize(animation)
@animation = animation
@color = Gosu::Color.new(0xff000000)
@color.red = rand(256 - 40) + 40
@color.green = rand(256 - 40) + 40
@color.blue = rand(256 - 40) + 40
@x = rand * 640
@y = rand * 480
end
def draw
img = @animation[Gosu::milliseconds / 100 % @animation.size];
img.draw(@x - img.width / 2.0, @y - img.height / 2.0,
ZOrder::Stars, 1, 1, @color, :add)
end
end

Uma vez que não queremos que toda estrela carregue a animação novamente, nós não podemos fazer isso no construtor, então vamos passá-la de algum outro lugar. (A classe Window carregará a animação em cerca de três parágrafos).

Para mostrar um frame diferente da animação a cada 100 milisegundos, a hora retornada por Gosu::milliseconds é dividida por 100 e então encontramos seu módulo pelo número de frames da animação. Essa é própria imagem que será desenhada, centralizada na posição da estrela e alterada por uma cor aleatório que geramos no construtor.

Agora vamos adicionar um código simples para o jogador coletar as estrelas de um array:

class Player
...
def score
@score
end
def collect_stars(stars)
if stars.reject! {|star| Gosu::distance(@x, @y, star.x, star.y) < 35 } then
@score += 1
end
end
end

Agora vamos estender a classe Window para carregar a animação, criar as novas estrelas, fazer o jogador coletá-las e desenhar as que sobram:

...
class Window < Gosu::Window
def initialize
super(640, 480, false)
self.caption = "Gosu Tutorial Game"
@background_image = Gosu::Image.new(self, "media/Space.png", true) @player = Player.new(self)
@player.warp(320, 240)
@star_anim = Gosu::Image::load_tiles(self, "media/Star.png", 25, 25, false)
@stars = Array.new
end
def update
...
@player.move
@player.collect_stars(@stars)
if rand(100) < 4 and @stars.size < 25 then
@stars.push(Star.new(@star_anim))
end
end
def draw
@background_image.draw(0, 0, ZOrder::Background)
@player.draw
@stars.each { |star| star.draw }
end
...

Feito! Agora você pode coletar estrelas.

4. Som e Texto

Por fim, queremos escrever a pontuação atual usando uma fonte bitmap e tocar um som de “bipe” toda vez que o jogador coletar uma estrela. A classe Window se encarregará da parte do texto, carregando uma fonte com 20 pixels de altura:

class Window < Gosu::Window
def initialize
...
@font = Gosu::Font.new(self, Gosu::default_font_name, 20)
end
... def draw
@background_image.draw(0, 0, ZOrder::Background)
@player.draw
@stars.each { |star| star.draw }
@font.draw("Score: #{@player.score}", 10, 10, ZOrder::UI, 1.0, 1.0, 0xffffff00)
end
end

O que sobrou pro jogador? Ah é: um contador para a pontuação, carregar o som e reproduzí-lo.

class Player
attr_reader :score
def initialize(window)
@image = Gosu::Image.new(window, "media/Starfighter.bmp", false)
@beep = Gosu::Sample.new(window, "media/Beep.wav")
@x = @y = @vel_x = @vel_y = @angle = 0.0
@score = 0
end
... def collect_stars(stars)
stars.reject! do |star|
if Gosu::distance(@x, @y, star.x, star.y) < 35 then
@score += 10
@beep.play
true
else
false
end
end
end
end

Como você pode ver, carregar e tocar efeitos sonoros não poderia ser mais fácil! Consulte o rdoc para formas mais poderosas de reproduzir sons — brincar com o volume, posição e tom.

É isso! O resto só depende da sua imaginação. Se você não consegue imaginar como isso é o suficiente para criar jogos, então dê uma olhada nos exemplos do fórum.

--

--

Alan Willms
Alan Willms

Written by Alan Willms

Software development nerd. In 💙 with Ruby, PHP, JavaScript, Crystal, and other techy stuff.

No responses yet