Desenvolvendo games com Ruby e MiniGL: Parte 6 - movimento baseado em física
Olá meus caros! Peço desculpas pelo “hiato”, eu tive umas semanas muito corridas nestes últimos tempos - mudei de trabalho, agora estou trabalhando numa empresa de desenvolvimento de games! -, mas estamos de volta!
No post de hoje vamos voltar a falar de movimentação, mas agora explorando o outro tipo de movimento disponibilizado na classe GameObject
, conforme prometido na parte 3: movimento baseado em forças e leis da física!
Basicamente, a ideia é que, em vez de passar uma velocidade fixa para o objeto se mover, vamos passar um vetor das forças atuantes neste objeto, e a biblioteca calculará a velocidade resultante. O resultado disso serão movimentos mais “suaves”, dando a sensação de aceleração e desaceleração. Além disso, o objeto passará a estar sujeito à força da gravidade, caindo quando não houver algo sólido abaixo.
Tomemos o arquivo “game.rb” como foi deixado ao final do último post, e vamos às primeiras alterações:
...
class MyGame < GameWindow
def initialize
...
@sprite = GameObject.new(10, 10, 80, 90, :face, Vector.new(-10, -10), 2, 3)
@sprite.max_speed.x = 15
@sprite.max_speed.y = 50
...
end
...
end
...
Fizemos uma pequena modificação no tamanho físico da carinha, agora com 90 de altura, para que ela toque o chão de maneira precisa. Além disso, estamos definindo as velocidades máximas em cada direção (alterando as coordenadas x
e y
do vetor max_speed
). Estes números (e outros que veremos mais à frente) são resultado de testes para calibração do movimento, é necessário ir testando diversos valores para encontrar um que resulte num movimento balanceado e suave.
O tipo de movimentação que queremos fazer não se encaixa bem num jogo de labirinto, mas sim num jogo de plataforma, por exemplo. Assim, vamos alterar o mapa para que nos permita explorar melhor esses movimentos:
############################################
# # #
# ### # !#
# ## # ######
###### # #
# # ##### #
# # #
# ### # ######### ###
# @ ### # # #
############################################
Salve o conteúdo acima no arquivo “level.txt” (sobrescrevendo o do último post). Agora, vamos também ajustar a maneira como o mapa é carregado:
...
class MyGame < GameWindow
TILES_X = 45
TILES_Y = 10
def initialize
...
@map = Map.new(100, 100, TILES_X, TILES_Y)
@walls_matrix = Array.new(TILES_X) { Array.new(TILES_Y) { nil } }
...
end
...
end
...
Note que definimos as constantes TILES_X
e TILES_Y
para a quantidade de tiles (ou células da grid) na horizontal e na vertical, respectivamente, pois estes valores são usados mais de uma vez. Então redefinimos a contagem de tiles do mapa e também a quantidade de colunas e linhas da matriz de paredes (@walls_matrix
).
Agora, vamos para o que realmente importa para o objetivo de hoje: a lógica de movimentação. Localize no “game.rb”, dentro do método update
, o bloco abaixo:
v = Vector.new(0, 0)
v.x += 3 if KB.key_down?(Gosu::KB_RIGHT)
v.x -= 3 if KB.key_down?(Gosu::KB_LEFT)
v.y += 3 if KB.key_down?(Gosu::KB_DOWN)
v.y -= 3 if KB.key_down?(Gosu::KB_UP)
E substitua-o por isto:
v = Vector.new(0, 0)
v.x += 1.5 if KB.key_down?(Gosu::KB_RIGHT)
v.x -= 1.5 if KB.key_down?(Gosu::KB_LEFT)
if @sprite.bottom
v.y -= 15 + 0.7 * @sprite.speed.x.abs if KB.key_down?(Gosu::KB_UP)
v.x -= 0.15 * @sprite.speed.x
end
Analisemos ponto a ponto as alterações:
- Para o movimento na horizontal (segunda e terceira linhas), nós apenas ajustamos o valor de 3 para 1.5. Este valor representará agora a força aplicada na horizontal, ou seja, uma aceleração. Se o valor for muito alto, o objeto chegará em velocidades absurdamente altas em pouco tempo (a aceleração é aplicada a cada frame, ou seja, aproximadamente 60 vezes por segundo).
- Para a vertical, as mudanças são maiores. Primeiramente, não mais aumentamos a coordenada y do vetor ao pressionar a seta para baixo, pois o movimento para baixo ficará por conta da gravidade. Segundo, apenas aplicamos força na vertical (para cima) quando o objeto está sobre o chão (indicado pelo teste
@sprite.bottom
, ondebottom
retorna o objeto que está colidindo com@sprite
por baixo, se houver algum). O cálculo da força para cima é dado por15 + 0.7 * @sprite.speed.x.abs
, ou seja, uma constante (15) somada com um valor variável atrelado à velocidade horizontal do objeto (@sprite.speed.x
). O efeito disto é que a carinha poderá pular mais alto quando estiver se movendo mais rápido. - Por fim, também apenas na condição de o objeto estar sobre o chão (
if @sprite.bottom
), aplicamos uma força horizontal contrária à velocidade atual (v.x -= 0.15 * @sprite.speed.x
), para imitar o efeito de atrito. Isso permitirá que o objeto pare alguns momentos depois de soltarmos a tecla de movimento.
Agora, só resta remover o último argumento da chamada ao método move
do objeto (ao não passar o argumento, ele assume o valor padrão false
, indicando que o vetor que está sendo passado é um vetor de forças, e não de velocidades):
...
def update
...
@sprite.move(v, coll_walls, [])
end
...
E é apenas isso! Experimente rodar o jogo agora e veja se você consegue chegar até o final. :)
E abaixo o “game.rb” completo de hoje:
require 'minigl'
include MiniGL
class MyGame < GameWindow
TILES_X = 45
TILES_Y = 10
def initialize
super 800, 600, false
self.caption = 'Meu Primeiro Jogo'
@sprite = GameObject.new(10, 10, 80, 90, :face, Vector.new(-10, -10), 2, 3)
@sprite.max_speed.x = 15
@sprite.max_speed.y = 50
@blinking = false
@walls = []
@goal = GameObject.new(640, 480, 50, 50, :goal, Vector.new(-17, -17))
@map = Map.new(100, 100, TILES_X, TILES_Y)
@walls_matrix = Array.new(TILES_X) { Array.new(TILES_Y) { nil } }
File.open('level.txt') do |f|
f.each_line.with_index do |line, j|
line.each_char.with_index do |char, i|
case char
when '#' then @walls << (@walls_matrix[i][j] = Block.new(i * 100, j * 100, 100, 100))
when '@' then @start_x = @sprite.x = i * 100 + 10; @start_y = @sprite.y = j * 100 + 10
when '!' then @goal.x = i * 100 + 25; @goal.y = j * 100 + 25
end
end
end
end
@finished = false
font = Res.font :fonte, 48
@text_helper = TextHelper.new(font)
font2 = Res.font :fonte, 20
@button = Button.new(325, 330, font2, 'Jogar de novo', :button) do
@finished = false
@sprite.speed.x = @sprite.speed.y = 0
@sprite.x = @start_x
@sprite.y = @start_y
end
end
def needs_cursor?
@finished
end
def update
KB.update
Mouse.update
if @finished
@button.update
else
v = Vector.new(0, 0)
v.x += 1.5 if KB.key_down?(Gosu::KB_RIGHT)
v.x -= 1.5 if KB.key_down?(Gosu::KB_LEFT)
if @sprite.bottom
v.y -= 15 + 0.7 * @sprite.speed.x.abs if KB.key_down?(Gosu::KB_UP)
v.x -= 0.15 * @sprite.speed.x
end
coll_walls = []
c_i = @sprite.x.to_i / 100
c_j = @sprite.y.to_i / 100
((c_i-2)..(c_i+2)).each do |i|
((c_j-2)..(c_j+2)).each do |j|
coll_walls << @walls_matrix[i][j] if i >= 0 && j >= 0 && @walls_matrix[i] && @walls_matrix[i][j]
end
end
@sprite.move(v, coll_walls, [])
end
if @blinking
@sprite.animate_once([4, 5, 4], 7) do
@blinking = false
@sprite.set_animation 0
end
else
@sprite.animate([0, 1, 2, 3], 5)
if KB.key_pressed?(Gosu::KB_SPACE)
@blinking = true
@sprite.set_animation 4
end
end
if @sprite.bounds.intersect?(@goal.bounds)
@finished = true
end
@map.set_camera(@sprite.x - 360, @sprite.y - 260)
end
def draw
clear 0xffabcdef
@goal.draw(@map)
@sprite.draw(@map)
@map.foreach do |i, j, x, y|
draw_quad(x, y, 0xff000000,
x + 100, y, 0xff000000,
x, y + 100, 0xff000000,
x + 100, y + 100, 0xff000000, 0) if @walls_matrix[i][j]
end
if @finished
@text_helper.write_line 'Você venceu!', 400, 276, :center, 0xffff00, 255, :border, 0x000000, 2
@button.draw
end
end
end
MyGame.new.show
Por hoje é só, colegas, até a próxima (que espero que não demore tanto)!