Depois de muito tempo sem postar sobre Corona SDK, o nosso framework favorito para desenvolvimento de jogos móveis multi-plataforma, eis que eu volto à ativa no desenvolvimento mobile de games com um clássico dos clássicos: Sunset Riders. Ok, eu já vinha desenvolvendo ele em posts anteriores, mas agora é pra valer. Atendendo a inúmeros pedidos o post de hoje inclui uma série de conceitos importantíssimos: botões, splash screen, tela de seleção de personagens, multimídia, sprites e por aí vai. Como o código é um pouco extenso, me aterei em comentar as etapas mais importantes. Use o link no final do post para baixar o projeto completo e até mesmo o executável para Android caso queira testar em um dispositivo de verdade.

Splash Screen
As splash screens são as clássicas telas de abertura do jogo, onde geralmente temos uma imagem e um menu opcional. No nosso caso, será apenas uma imagem estática do jogo original que achei na Internet, que quando tocada, se fecha, exibindo a tela anterior da pilha de execução. Para esta tela criei um arquivo chamado splashscreen.lua que eu apenas invoco usando require no main.lua.
module(..., package.seeall);
local tela = display.newImageRect( "image/splash-screen.jpg",display.contentWidth,display.contentHeight);
tela:translate(240,160);
function dismiss(event)
tela:removeSelf();
end
tela:addEventListener("tap", dismiss);
Note que a imagem foi colocada dentro de uma pasta image dentro da pasta do projeto. Encontrará todas as imagens utilizadas neste post nos fontes do projeto.

Seleção de Personagens
Embora nos exemplos anteriores tenhamos apenas oferecido o Cormano como única opção de cowboy, sabemos que ele não é unanimidade entre os gamers mundo afora. Eu particularmente gostava de jogar com o Bob e suas espingardas, enquanto meus amigos curtiam o xerife Billy. Como segundo passo, criaremos uma tela de seleção de personagem. Seguindo as boas práticas de programação Lua criaremos um arquivo Lua para cada tela, assim como fizemos com a Splash Screen, então, crie um playerselect.lua e coloque lá o código abaixo:
module(..., package.seeall);
local selected = "steve";
local player;
function getSelected()
return selected;
end
function isVisible()
return player.isVisible;
end
local callbackFunction;
function callback(listener)
callbackFunction = listener;
end
function configuraTela()
player = display.newImageRect( "image/select-"..selected..".png", display.contentWidth, display.contentHeight);
player:translate(240,180);
player:addEventListener("tap", selectPlayer);
end
function selectPlayer( event )
if(selected == "steve") then
if(event.x >= 1 and event.x <= 114) then--steve
player.isVisible = false;
elseif (event.x >= 151 and event.x <= 207) then--billy
selected = "billy";
elseif (event.x >= 271 and event.x <= 327) then--bob
selected = "bob";
elseif (event.x >= 391 and event.x <= 448) then--cormano
selected = "cormano";
end
elseif (selected == "billy") then
if(event.x >= 30 and event.x <= 87) then--steve
selected = "steve";
elseif (event.x >= 119 and event.x <= 234) then--billy
player.isVisible = false;
elseif (event.x >= 271 and event.x <= 327) then--bob
selected = "bob";
elseif (event.x >= 391 and event.x <= 448) then--cormano
selected = "cormano";
end
elseif (selected == "bob") then
if(event.x >= 30 and event.x <= 87) then--steve
selected = "steve";
elseif (event.x >= 150 and event.x <= 207) then--billy
selected = "billy";
elseif (event.x >= 240 and event.x <= 355) then--bob
player.isVisible = false;
elseif (event.x >= 391 and event.x <= 448) then--cormano
selected = "cormano";
end
elseif (selected == "cormano") then
if(event.x >= 30 and event.x <= 87) then--steve
selected = "steve";
elseif (event.x >= 150 and event.x <= 207) then--billy
selected = "billy";
elseif (event.x >= 269 and event.x <= 328) then--bob
selected = "bob";
elseif (event.x >= 358 and event.x <= 476) then--cormano
player.isVisible = false;
end
end
player:removeSelf();
if(player.isVisible == true) then
configuraTela();
else callbackFunction();
end
end
Uma breve explicação quanto a este código pode ser resumida no seguinte: para cada estado possível da tela, e são 4 (um para cada personagem) temos uma imagem diferente com o personagem realçado, ou seja, removemos a imagem anterior e carregamos a nova imagem na tela, dando a impressão de seleção de personagem, assim como no jogo original. Ao ser tocada a primeira vez, a tela realça o personagem e guarda seu nome na variável selected, no segundo toque no mesmo personagem, a tela é dispensada para que exiba o item anterior na pilha gráfica do Corona. As funções deste arquivo servem para expor alguns valores para os demais arquivos do projeto (principalmente o main.lua), como por exemplo, para o main.lua saber quem foi o personagem escolhido.

Orientação à Objetos
Lua não é orientado à objetos. É uma linguagem simples de scripting que é perfeita para criação de lógica de jogos. Entretanto, quando se programa a anos como eu e sabe-se que bons projetos não vão longe sem um mínimo de arquitetura e por isso você já deve ter notado que estou estruturando melhor os arquivos deste projeto em pastas, arquivos, etc. Desta forma, um conceito importantíssimo para criação de bons projetos são os conceitos de orientação à objetos que entre uma de suas premissas está o fato de que não se deve repetir código. Mas se todos personagens pulam, correm e param, como que não repetiremos esse código? A idéia é simples, vamos criar um arquivo chamado character.lua e nele vamos colocar tudo o que é comum aos personagens, como segue:
-- Arquivo contendo toda a lógica comum a todos os personagens
module(..., package.seeall);
local velocidade = 500;
local started = false;
function getVelocidade()
return velocidade;
end
function start()
started = true;
end
--carregando as variáveis e constantes
local relincho = audio.loadSound("audio/relincho.wav");
local trote = audio.loadSound("audio/galope.wav");
local canalTrote;
local cavaloY = 225;
local cavaloX = display.contentWidth / 4;
local escala = 1.2;
local galopandoSheet;
local galopandoSpriteSet;
local galopandoStance;
local jumpingSheet;
local jumpingSpriteSet;
local jumpingStance;
local parado;
function parar()
audio.pause(canalTrote);
parado.x = galopandoStance.x;
parado.y = galopandoStance.y;
parado.isVisible = true;
galopandoStance.isVisible = false;
jumpingStance.isVisible = false;
end
function galopandoConfig(nomeChar,spriteWidth,spriteHeight,spritesNumber)
galopandoSheet = sprite.newSpriteSheet( "image/"..nomeChar..".png", spriteWidth,spriteHeight );
galopandoSpriteSet = sprite.newSpriteSet(galopandoSheet, 1, spritesNumber);
sprite.add( galopandoSpriteSet, nomeChar, 1, spritesNumber, velocidade, 0 );-- roda 5 frames/velocidade
galopandoStance = sprite.newSprite( galopandoSpriteSet );
galopandoStance.x = cavaloX + 40;
galopandoStance.y = cavaloY;
galopandoStance.xScale = escala;
galopandoStance.yScale = escala;
galopandoStance.isVisible = false;
galopandoStance:addEventListener("tap", parar);
end
function jumpingConfig(nomeChar,spriteWidth, spriteHeight, spritesNumber)
jumpingSheet = sprite.newSpriteSheet( "image/"..nomeChar.."-jump.png", spriteWidth, spriteHeight )
jumpingSpriteSet = sprite.newSpriteSet(jumpingSheet, 1, spritesNumber)
sprite.add( jumpingSpriteSet, nomeChar.."-jumping", 1, spritesNumber, 500, 1 ) -- play 3 frames every 500 ms
jumpingStance = sprite.newSprite( jumpingSpriteSet )
jumpingStance.x = cavaloX + 40
jumpingStance.y = cavaloY;
jumpingStance.xScale = escala;
jumpingStance.yScale = escala;
jumpingStance.isVisible = false;
jumpingStance:addEventListener("sprite", fimPulo);
end
function paradoConfig(nomeChar)
parado = display.newImage( "image/"..nomeChar.."-parado.png" )
parado:setReferencePoint( display.CenterLeftReferencePoint )
parado.x = cavaloX;
parado.y = cavaloY;
parado.xScale = escala;
parado.yScale = escala;
parado.isVisible = true;
parado:addEventListener("tap", galopar);
end
function isParado()
return parado.isVisible;
end
function galopar(nomeChar)
if(started == true) then
audio.play(relincho);
canalTrote = audio.play(trote, {loops=-1});
parado.isVisible = false;
jumpingStance.isVisible = false;
galopandoStance.isVisible = true;
galopandoStance:prepare(nomeChar);
galopandoStance:play();
end
end
function pulo(nomeChar)
if (parado.isVisible == false) then
audio.play(relincho);
galopandoStance.isVisible = false;
jumpingStance.x = galopandoStance.x;
jumpingStance.y = galopandoStance.y;
jumpingStance.isVisible = true;
jumpingStance:prepare(nomeChar.."-jumping")
jumpingStance:play()
end
end
function fimPulo(event)
if (event.phase == "end") then
parado.isVisible = false;
galopandoStance.isVisible = true;
jumpingStance:pause();
jumpingStance.isVisible = false;
end
end
function onMove(event)
if (parado.isVisible == false) then
local joyX = event.joyX
local joyY = event.joyY
if(joyX ~= false and joyY ~= false) then
if(joyX > 0) then
if(galopandoStance.x < display.contentWidth - 55) then
galopandoStance.x = galopandoStance.x + 5;
end
else
if(galopandoStance.x > 30) then
galopandoStance.x = galopandoStance.x - 5;
end
end
if(joyY > 0) then
if(galopandoStance.y < 250) then
galopandoStance.y = galopandoStance.y + 5;
end
else
if(galopandoStance.y > 180) then
galopandoStance.y = galopandoStance.y - 5;
end
end
end
end
end
Note que este arquivo carrega os sprites de maneira dinâmica, conforme o nome que for passado por parâmetro. Isso porque cada personagem terá suas próprias sprites, ou seja, só o que muda entre os personagens é seu nome (as spritesheets possuem os nomes dos personagens) e a largura e altura das sprites. O resto é exatamente igual. A função onMove será usada para manipularmos o personagem através do joystick que adicionaremos mais para frente na aplicação. Por ora, deixem-na quieta.Note também a forma como estamos carregando os áudios, de dentro de uma pasta audio, com a API audio (pois a API media foi descontinuada) e que não usamos mais os MIDs que usamos nos exemplos anteriores. A nova APi de áudio usa OpenAL e não suporta mais MID. Uma boa alternativa é WAV e incluí nos fontes do projeto os arquivos de áudio para que possa tê-los em seu projeto também.
A ideia é que todos os personagens vão carregar esta biblioteca em uma variável e chamar suas funções para carregar as sprites e poder correr, pular, etc, como veremos a seguir para o personagem Billy:

-- Arquivo contendo toda a lógica do personagem Billy
module(..., package.seeall);
local char = require("character");
local nomeChar = "billy";
function getVelocidade()
return char.getVelocidade();
end
function start()
char.start();
end
char.galopandoConfig(nomeChar, 96,80,5)
char.jumpingConfig(nomeChar,80,97,3);
char.paradoConfig(nomeChar);
function isParado()
return char.isParado();
end
function galopar()
char.galopar(nomeChar);
end
function parar()
char.parar();
end
function pulo()
char.pulo(nomeChar);
end
function onMove(event)
char.onMove(event);
end
Note a simplicidade de criar um novo personagem usando esta arquitetura de software simplíssima baseada em Orientação à Objetos, ao criarmos o personagen Steve:

-- Arquivo contendo toda a lógica do personagem Steve
module(..., package.seeall);
local char = require("character");
local nomeChar = "steve";
function getVelocidade()
return char.getVelocidade();
end
function start()
char.start();
end
char.galopandoConfig(nomeChar, 96,79,5)
char.jumpingConfig(nomeChar,80,95,3);
char.paradoConfig(nomeChar);
function isParado()
return char.isParado();
end
function galopar()
char.galopar(nomeChar);
end
function parar()
char.parar();
end
function pulo()
char.pulo(nomeChar);
end
function onMove(event)
char.onMove(event);
end
Não postarei o código dos demais personagens pois acredito que sejam capazes de deduzir por vocês mesmos. Note que o que fizemos foi criar uma espécie de herança da orientação à objetos de uma forma tosca mas que funciona.
Agora que temos todos os personagens criados, vamos criar novamente o plano de fundo de nossa aplicação que conta com trilha sonora, trilhos de trem e uma bela paisagem do oeste selvagem!

Cenário, Joystick e Botões
Já havíamos criado o cenário em posts anteriores e mesmo com essa nova abordagem mais arquitetural, o código ainda se parece com o código antigo. As imagens ainda são as mesmas de antes, mas agora temos algumas adicionais que nos levam a estudar duas bibliotecas antes: ui.lua e joystick.lua.
A biblioteca ui.lua lhe permite criar botões de forma muito mais fácil e é largamente utilizado em projetos Lua. O projeto inclui uma versão desta biblioteca, a última na época deste tutorial (1.6) e seu uso neste game é para criar dois botões, o de pular (que dispara a função de salto do personagem) e o de atirar, que ainda não tem utilidade mas que no futuro irá disparar projéteis com as armas dos personagens.
local ui = require("ui");
function jumpButtonEvent(event)
personagem.pulo();
end
function shotButtonEvent(event)
print("shot");
end
function configuraButtons()
jumpButton = ui.newButton{
default = "image/jump-button.png",
over = "image/jump-button-over.png",
onRelease = jumpButtonEvent,
x = display.contentWidth - 50,
y = display.contentHeight - 75
};
shotButton = ui.newButton{
default = "image/shot-button.png",
over = "image/shot-button-over.png",
onRelease = shotButtonEvent,
x = display.contentWidth - 50,
y = display.contentHeight - 150
};
end
Este código deve ir no main.lua, que é um arquivo tão extenso que não incluirei aqui. Apenas pegue os fontes deste projeto e busque este trecho de código para saber onde colocar o seu. Note que as imagens para os botões são duas: a normal e a de botão pressionado. Além disso, os botões disparam uma função quando selecionados, funções estas definidas nas linhas anteriores ao carregamento dos mesmos. Note também que na função de pulo mandamos uma variável personagem pular, sem nem mesmo saber qual personagem que estamos jogando, o que é indiferente para nós. Em orientação à objetos poderíamos dizer que emulamos uma interface.
Outra biblioteca importante que usaremos hoje é a biblioteca joystick.lua (a versão 1 se encontra no projeto), que cria um direcional analógico assim como o visto em controles de Nintendo 64 e Playstation. Esse joystick usa duas imagens existentes em nossa pasta de imagens e dispara o evento onMove presente na biblioteca character.lua. Isso porque independente de personagem, ele se moverá da mesma forma, não é mesmo? Para usar o joystick.lua, use o seguinte código:
local joystickClass = require( "joystick" );
function configuraJoystick()
joystick = joystickClass.newJoystick{
outerImage = "",
outerRadius = 60,
outerAlpha = 0,
innerImage = "image/joystickInner.png",
innerRadius = "",
innerAlpha = 1,
backgroundImage = "image/joystickDial.png",
background_x = 0,
background_y = 0,
backgroundAlpha = 1,
position_x = 0,
position_y = baseline - 100,
ghost = 0,
joystickAlpha = 0.4,
joystickFade = true,
joystickFadeDelay = 2000,
onMove = personagem.onMove
}
end
Olhando este código não é muito difícil de imaginar para que servem as propriedades que configuramos, sendo que as principais delas são as imagens que formam o joystick e a função que vai ser executada toda vez que o joystick for utilizado (neste caso, a onMove do personagem).
Mas voltando a ponto que havíamos parado no último tutorial, o plano de fundo é extremamente simples de implementar novamente, bastando carregar os trilhos duas vezes, adicionar música e os demais elementos, incluindo o próprio personagem. Muita atenção à ordem dos elementos no main.lua pois eles são colocados no display na ordem em que são instanciados. Ou seja, o que você quiser que sua aplicação exiba primeiro deve estar no final do arquivo (como a splashscreen e a selectplayer, por exemplo). Tendo isso em mente, o código para o cenário do jogo com os elementos juntos fica assim:
-- Arquivo contendo toda a lógica da aplicação Sunset Riders Lite
display.setStatusBar(display.HiddenStatusBar);
system.activate( "multitouch" );
local playerSelect = require("playerselect");
local joystickClass = require( "joystick" );
local ui = require("ui");
require "sprite";
Primeiro escondemos a barra de status, carregamos a tela de seleção d epersonagem 9que ainda não é exibida pois exige a chamada de uma função para tanto. Ativamos multitouch em nossa aplicação pois possuímos um joystick e botões, esperando que os usuários jogarão com as duas mãos sobre o dispositivo (impossível de testar no simulador), carregamos as bibliotecas ui e sprite, necessárias mais para frente. Continuando:
--variáveis e constantes
local started = false;
local baseline = 280;
local escalaTrilhos = 1.3;
local trilhosY = baseline - 115;
local textoX = 150;
local textoY = baseline - 175;
local velocidadeFundo = 0.2;--quanto maior mais rápido
local milhas = 0;--número de milhas percorridas pelo cowboy
local percurso = 10;--milhas até a cidade
local victoryMusic = audio.loadSound("audio/victory.wav");
local backMusic = audio.loadSound("audio/sunset-riders-bonus-stage.wav");
local backChannel;
local jumpButton, shotButton;
local tPrevious = system.getTimer()
Algumas variáveis e constantes para definir a linha base de onde o personagem inicia o jogo, a escala dos trilhos, a posição XY dos textos, trilhos, a velocidade do fundo, o número de milhas percorridas pelo cowboy (sim, um dia a fase irá acabar) o tamanho do percurso em milhas, a música de vitória, a música de fundo, o canal da música de fundo (para poder desligá-la depois), os dois botões e o tempo atual do sistema, para sincronizar as atualizações dos trilhos. Mais código:
-- Trilhos
-- O dobro de trilhos para fazer a animação
-- Quando um dos trilhos termina, aparece o outro
function montaTrilhos(x)
local rails = display.newImage( "image/trilhos.jpg" )
rails:setReferencePoint( display.CenterLeftReferencePoint )
rails.y = trilhosY;
rails.x = x;
rails.xScale = escalaTrilhos;
rails.yScale = escalaTrilhos;
return rails;
end
local trilhos = montaTrilhos(0);
local trilhos2 = montaTrilhos(trilhos.contentWidth);
Aqui montamos os trilhos d emaneira bem simples, assim como havíamos construído antes. A diferença é a função que evita repetição de código.
--função para configurar um novo texto a ser exibido
function montaTexto(texto)
local rotulo = display.newImage( "image/"..texto..".png" );
rotulo:setReferencePoint( display.CenterLeftReferencePoint )
rotulo.x = textoX;
rotulo.y = textoY;
rotulo.isVisible = false;
return rotulo;
end
local textoStart = montaTexto("start");
textoStart.isVisible = true;
local textoHurry = montaTexto("hurry");
local textoVictory = montaTexto("victory");
Aqui temos uma função para montar os textos que usaremos na aplicação. Use esta função para exibir quaisquer textos no formato padrão do nosso jogo, basta ter as imagens corretas n pata de imagens do projeto. Agora vamos mover os elementos:
local personagem;
-- movendo os elementos
local function move(event)
if(started == true and personagem.isParado() == false) then
local tDelta = event.time - tPrevious;
tPrevious = event.time;
local xOffset = ( velocidadeFundo * tDelta )
trilhos.x = trilhos.x - xOffset;
trilhos2.x = trilhos2.x - xOffset;
if (trilhos.x + trilhos.contentWidth) < 0 then
milhas = milhas + 1;
trilhos:translate(trilhos.contentWidth * 2, 0)
end
if (trilhos2.x + trilhos2.contentWidth) < 0 then
milhas = milhas + 1;
trilhos2:translate( trilhos2.contentWidth * 2, 0)
end
if(milhas >= percurso) then
started = false;
--jogo terminou
personagem.parar();
milhas = 0;
textoVictory.isVisible = true;
audio.pause();
audio.play(victoryMusic);
end
end
end
--joystick
--botões
Este código é chamado a cada atualização de tela do dispositivo e redesenha o cenário conforme o tempo do sistema. Além disso, conta quantas vezes o cenário já foi desenhado por completo para contar as milhas do projeto e dizer se o cowboy já chegou no final do projeto ou não, neste caso tocando uma música de vitória. No último comentário é onde deve ser colocado o código do joystick, sem o comando require que já foi chamado antes no início do arquivo. Onde diz botões, deve ser colocado o código dos botões sem o comando require. Vamos ao último trecho de código do main.lua:
--função de início
local function start()
if(playerSelect.isVisible() == false) then
personagem = require (playerSelect.getSelected());
configuraJoystick();
configuraButtons();
backChannel = audio.play(backMusic, { loops=-1 });
started = true;
textoStart.isVisible = false;
personagem.start();
end
end
playerSelect.callback(start);
Runtime:addEventListener( "enterFrame", move );
playerSelect.configuraTela();
local splashScreen = require("splashscreen");
Este trecho define a função de início de jogo, define a função de callback da tela de seleção de personagem (uma função de callback é uma função que será executada quando a tela de seleção de personagem for fechada) e o gatilho para o evento enterFrame, que é chamado uma vez para cada refresh da tela (o que depende do framerate do dispositivo, que pode variar de 30 a 60fps). E por fim, configura a tela de seleção de personagem (desenha) e carrega a splash screen (desenha-a também). Note que as primeiras telas do jogo são as últimas a serem desenhadas.
Os arquivos build.settings e config.lua ainda são os mesmos do último projeto de exemplo e não serão repetidos. Pronto, nosso jogo está pronto para ser executado e até mesmo ter uma versão compilada para ser instalado em um dispositivo de verdade. Incluí no final deste post os arquivos do projeto completo e o arquivo APK para instalação no Android.

Conclusões e Futuro
Espero que tenham gostado deste post que ficou muito extenso pois acabei me empolgando no desenvolvimento do game ao longo da semana e me esqueci de ir fazendo este post aos poucos ou até mesmo em pedaços, explicando cada uma das etapas em detalhes. De qualquer forma acredito que servirá para saciar a vontade do pessoal que queria ver coisas como botões, joysticks e a continuidade do jogo, que na verdade meio que começou a ser desenvolvido de verdade agora.
Para o futuro penso em adicionarmos uma barra de progresso enquanto o jogo está sendo inicializado, uma animaçãozinha de abertura, alguns efeitos sonoros adicionais, tiros e alguns obstáculos a serem saltados pelos personagens para dar algum sentido à fase. Então até o próximo post!
Quer saber tudo sobre Corona SDK? Compre já o meu livro!
Sunset Riders Lite 0.1.apk.zip (8,12 mb)
SunsetRiders-4.zip (6,13 mb)