Em muitos jogos, o movimento de câmera é um recurso essencial. Diversos frameworks o implementam nativamente.
LÖVE ou Love2D é a plataforma de desenvolvimento de jogos em Lua, com suporte a FFI e shaders nativo, porém exige um pouco de verborragia para lidar com outros recursos básicos, como câmera e temporização.
Mas não ficamos órfãos de recursos ao programar em LÖVE. A comunidade tem desenvolvido inúmeras bibliotecas, resolvendo os recursos faltantes.
De todas, um biblioteca da qual não se pode abrir mão é HUMP, que agrega recursos de controle de estado, temporização, gerência de vetores, sinais, câmera e até mesmo adiciona suporte a classes a Lua.
Vamos escrever um exemplo muito trivial de gerência de câmera usando HUMP em MoonScript. Esteja atento, pois será necessário compilar os arquivo
.moon
para .lua
com moonc
a cada alteração.
No diretório onde o código ficará execute (você precisa do git
):
sh$ git clone [email protected]:vrld/hump.git
LÖVE usa um arquivo de configuração inicial, conf.lua
, com informações sobre tamanho e tipo da janela, e quais
recursos ativar ou não.
Vamos criar então um conf.moon
com a versão de LÖVE que estamos usando, a identidade da aplicação – uma string de uso interno – e
as informações da janela – título, tamanho e se será fullscreen ou
não:
love.conf = (t) ->
with t
.version = "0.10.1"
.identity = "cameraexample"
with t.window
.title = "CameraExample"
.width = 600
.height = 600
.fullscreen = false
Nossa aplicação se chama cameraexample
, tem título “CameraExample”, não é fullscreen e tem dimensões 600x600.
Para compilar para Lua:
sh$ moonc conf.moon
Built conf.moon
sh$
Isso gera o arquivo conf.lua
, que é o que LÖVE usará.
LÖVE lê os callbacks do arquivo main.lua
. Criaremos então um arquivo main.moon
para nosso teste. Esse arquivo deve conter a
importação do(s) módulo(s) HUMP, a aplicação em si e os callbacks
que LÖVE espera.
Para a aplicação, vamos criar uma classe CameraExample
. Podemos começar o arquivo então:
local *
Camera = assert require "hump.camera"
app = nil
class CameraExample
new: =>
@camera = Camera 0, 0
@shapes = {
-> love.graphics.circle "fill", 0, 0, 50
}
Importamos hump.camera
– padronizado como Camera
–, reservamos uma variável para a instância de aplicação e criamos a classe, apenas com o
construtor.
No construtor da classe, instanciamos uma câmera olhando para a posição (0, 0), e criamos uma lista de formas (shapes
) contendo uma função
anônima que desenha um círculo de 50 pixels na posição (0, 0).
Nossa classe ainda precisa de um método para chamar as funções que desenham as formas. Acrescente à classe o método:
draw: =>
love.graphics.setColor 0x00, 0x00, 0x00
shape! for shape in *@shapes
Agora precisamos registrar a instanciação da callback no carregamento de LÖVE (love.load
) e o desenho na de desenho (love.draw
).
Abaixo, fora da classe, crie as seguintes funções:
love.load = ->
app = CameraExample!
love.graphics.setBackgroundColor 0x00, 0x50, 0x90
love.draw = ->
app\draw!
Isso já deve funcionar! Compile e rode:
sh$ moonc main.moon
Built main.moon
sh$ love .
O resultado esperado é abrir uma janela azul com um círculo preto no canto. Pode fechá-la, ela ainda não faz mais nada.
Vamos usar a câmera visualizar o “mundo” exibido. Altere o método draw
da classe para usar a câmera:
draw: =>
love.graphics.setColor 0x00, 0x00, 0x00
@camera\draw ->
shape! for shape in *@shapes
Com o contexto gerado pela chamada do método @camera\draw
agora, ao ao rodar a aplicação (não se esqueça de recompilar main.moon
!),
o círculo aparecerá no centro da janela.
Entenda o que aconteceu: agora a câmera está olhando para a posição onde está o círculo, (0, 0) – antes a posição (0, 0) era o canto superior esquerdo.
Há diversos métodos para mover a câmera – veja a documentação. Vamos usar o método que movimenta a câmera de acordo com um delta.
Precisaremos de um método para atualizar o estado da câmera. Tradicionalmente esse método é chamado update
e recebe um delta de tempo.
Vamos criar um delta de movimento a partir do delta de tempo e
adicioná-lo a deltas separados para os eixos x e y de acordo com que
teclas forem pressionadas. Depois passamos os deltas separados para a
função que move a câmera:
update: (dt) =>
ds = dt * 200 -- fator 200 funcionou bem pra mim
dx, dy = 0, 0
with love.keyboard
dx -= ds if .isDown"left"
dx += ds if .isDown"right"
dy -= ds if .isDown"left"
dy += ds if .isDown"left"
@camera\move dx, dy
Agora precisamos avisar LÖVE para chamar esse método a cada vez que o sistema atualizar seu estado. Isso é feito através do callback
love.update = (dt) ->
app\update dt
Então é possível mover a câmera usando as setas do teclado.
Parece estranho… aperta para a esquerda, o círculo vai para a direita, aperta para baixo, vai para cima…
Isso porque o que está sendo movido é a câmera. Para deixar mais claro, basta acrescentar mais formas.
Altere o construtor da classe para receber a lista de formas:
new: (...) =>
@camera = Camera 0, 0
@shapes = {...}
E na instanciação coloque mais formas:
love.load = ->
app = CameraExample (-> love.graphics.circle "fill", 0, 0, 50),
(-> love.graphics.rectangle "fill", 100, 80, 10, 10),
(-> love.graphics.circle "fill", -80, 150, 20)
love.graphics.setBackgroundColor 0x00, 0x50, 0x90
Compile e rode novamente.
Para ficar mais interessante, vamos acrescentar um tweening.
No cabeçalho, logo abaixo da imporação do módulo de câmera acrescente:
Timer = assert require "hump.timer"
Mude agora o construtor da classe para reter a cor dos objetos:
new: (...) =>
@color = 0x00
@camera = Camera 0, 0
@shapes = {...}
Adicione um método start
para iniciar o tweening, que mudará a cor entre branco e preto a cada 2s:
start: =>
guard = ->
color = if @color < 0x80 then 0xff else 0x00
Timer.tween 2, @, {:color}, "in-sine", guard
guard!
Esse método precisa ser chamado no carregamento de LÖVE:
love.load = ->
app = CameraExample (-> love.graphics.circle "fill", 0, 0, 50),
(-> love.graphics.rectangle "fill", 100, 80, 10, 10),
(-> love.graphics.circle "fill", -80, 150, 20)
love.graphics.setBackgroundColor 0x00, 0x50, 0x90
app\start!
O módulo de tweening precisa ser atualizado no callback. Mude o código também:
love.update = (dt) ->
Timer.update dt
app\update dt
Ao compilar e rodar a aplicação você verá os objetos piscando, enquanto a câmera pode ser movida para mudar o ponto de visão.