Atendendo a pedidos irei explicar aqui a base de como fazer um jogo estilo plataforma. Apesar de parecerem simples são um pouco mais complexo do que se pensa. Neste tutorial irei explicar os conceitos mais básicos, que são a gravidade, o pulo e a movimentação sobre as plataformas.
Claro ainda há muitas outras coisas que compõe esse tipo de jogo, como inimigos, itens, movimentações realistas, colisões com paredes e outros objetos, movimentação da câmera pelo cenário, animação dos personagens, ações, entre muitos outros.
O projeto final você pode baixar aqui.
Para começar vamos criar um projeto, o qual coloquei o nome de "Plataforma Game". Em seguida crie uma pasta no seu projeto, junto com as pastas res, src, etc... E adicione esse aquivo. Esse é o jar da nossa Game Engine. Agora, pelo eclipse, clique com botão direito nesse arquivo recém adicionado (se a pasta libs ainda não tiver aparecido dê um F5 para atualizar), vá em Build Path -> Add to Build Path, similar a figura:
Game Engine adicionado ao projeto Smash |
Pronto, agora nosso projeto já conta com a biblioteca de desenvolvimento de games para Android criada por mim. Vamos começar o desenvolvimento do jogo em si. Primeiro vamos criar o gráfico que representa nosso herói. Geralmente é utilizado uma sequência em Sprite para representar o herói e suas mais variadas animações, mas aqui vamos apenas utilizar uma forma oval azul. Crie uma pasta em res com o nome de drawable e insira o seguinte arquivo XML:
<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval"> <size android:width="30dp" android:height="50dp"/> <solid android:color="#ff0000ff"/> </shape>
Agora na nossa activity principal não precisamos carregar um layout por xml, vamos apenas instanciar o nosso GameView:
public class PlataformaGameActivity extends Activity { /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(new PlataformaGameView(this)); } }
Acusará um erro pois PlataformaGameView não existe, então vamos cria-lo:
public class PlataformaGameView extends GameView { private static final float GRAVITY = 0.4f; private static final float VELOCIDADE_ANDAR = 4; private static final float ALTURA_PULO = 10; protected Sprite mHero; protected float heroVelocidadeVertical = 0; protected boolean[][] esqueletoFase; protected Paint chaoPaint; public PlataformaGameView(Context context) { super(context, true); } }
Ai está nossa classe com as contantes e atributos que iremos utilizar. Note que chamamos o construtor pai passando true como parâmetro pois só assim poderemos utilizar o método TouchEvents que veremos mais a frente. Com a classe criada, vamos definir o onLoad que será executado automaticamente para fazer os carregamentos necessários:
@Override protected void onLoad() { super.onLoad(); mSprites.add(mHero = new Sprite(50, 10, drawableToBitmap(getResources().getDrawable(R.drawable.hero)))); esqueletoFase = new boolean[getWidth()/20 + 1][getHeight()/20 + 1]; chaoPaint = new Paint(); chaoPaint.setARGB(255, 255, 255, 255); //Branco montarFase(); }
O que fazemos aí é carregar o Sprite mHero com o gráfico que definimos anteriormente e adiciona-lo a lista mSprites, pois assim ele será desenhado automaticamente. Utilizamos um método para carregar o bitmap através do drawable que fizemos no xml, e esse método também precisa ser adicionado a classe:
public static Bitmap drawableToBitmap (Drawable drawable) { if (drawable instanceof BitmapDrawable) { return ((BitmapDrawable)drawable).getBitmap(); } Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); drawable.draw(canvas); return bitmap; }
Agora usaremos uma técnica para estabelecer onde há ou não chão no cenário. O que faremos é pegar toda a tela e dividir em uma matriz de 20 pixels cada, e é isso que o atributo esqueletoFase significa. Por exemplo, se esqueletoFase[4][6] for true, significa que do pixel 4*20 = 80x a 100x e 6*20 = 120y a 140y há um chão e o herói deve poder ficar em cima dele. Chamamos o método montarFase() para definir esses espaços de chão:
private void montarFase() { int alturaMax = esqueletoFase[0].length-2; for(int i = 0; i < esqueletoFase.length; i++){ esqueletoFase[i][alturaMax] = true; if(i > 7) esqueletoFase[i][alturaMax-4] = true; if(i > 15) esqueletoFase[i][alturaMax-9] = true; if(i > 20) esqueletoFase[i][alturaMax-14] = true; } }
Nesse caso estou definindo uma plataforma que completa a parte de baixo toda da tela e algumas intermediárias. Agora vamos definir a gravidade e o que acontecerá quando o herói estiver em cima de uma dessas plataformas. Já que isso tem que ser verificado a todo tempo, utilizaremos o método update que é chamado automaticamente a cada frame do jogo:
@Override public void update() { int alturaPlataforma = (mHero.y+mHero.height)/20; if(esqueletoFase[(mHero.x+mHero.width/2)/20][alturaPlataforma]){ mHero.y = alturaPlataforma*20 - mHero.height; if(heroVelocidadeVertical > 0) heroVelocidadeVertical = 0; }else if(heroVelocidadeVertical + GRAVITY < 20) heroVelocidadeVertical += GRAVITY; mHero.y += heroVelocidadeVertical; super.update(); }
O que esse método faz é verificar se no "pé" do herói (no caso a nossa bolinha azul) há uma plataforma, através do esqueletoFase. Se houver ele reposicionará o herói em cima dessa plataforma (isso é necessário pois se o herói caísse a mais de 1 pixel por segundo, ele poderia parar um pouco dentro da plataforma), e setará a velocidade vertical para zero, caso seja maior que zero (estiver caindo). Se não houver uma plataforma nos pés do herói a velocidade vertical será acrescida da gravidade. Após tudo isso é somado a posição y do herói a velocidade vertical. Geralmente nos jogos, toda essa lógica de velocidade é inserida dentro da classe que representa o herói, mas como aqui não possuímos uma classe especifica para isso utilizaremos dessa maneira mesmo.
Para pegar a movimentação do herói usaremos o método TouchEvents:
@Override protected void TouchEvents(int x, int y, int action) { if(y <= getHeight()/2) pular(); else if(x <= getWidth()/2) andar(false); else andar(true); super.TouchEvents(x, y, action); }
Os comandos são simples: se o jogador tocar na metade de cima da tela o herói irá pular, se tocar na parte de baixo ele irá andar para esquerda ou direita a depender da metade vertical que ele toque, como mostra a figura:
Possíveis movimentos |
É necessário definir os métodos andar e pular:
private void andar(boolean praDireita) { mHero.x += praDireita ? VELOCIDADE_ANDAR : -VELOCIDADE_ANDAR; } private void pular() { if(heroVelocidadeVertical == 0) heroVelocidadeVertical = -ALTURA_PULO; }
São métodos simples: andar apenas se movimenta para direita ou esquerda na quantidade de pixels definido pela constante VELOCIDADE_ANDAR e o pular verifica se a velocidade vertical é 0, ou seja, se o herói está no chão e define sua velocidade vertical de acordo com a constante ALTURA_PULO.
Agora falta apenas alterar nosso onDraw para que seja desenhado as plataformas de acordo com esqueletoFase:
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); drawEsqueleto(canvas); } private void drawEsqueleto(Canvas canvas) { int xInicio = -1; for(int h = 0; h < esqueletoFase[0].length; h++){ for(int w = 0; w < esqueletoFase.length; w++) if(esqueletoFase[w][h]){ if(xInicio == -1) xInicio = w*20; }else{ if(xInicio > -1){ canvas.drawRect(xInicio, h*20, w*20, (h+1)*20, chaoPaint); xInicio = -1; } } if(xInicio > -1){ canvas.drawRect(xInicio, h*20, esqueletoFase.length*20, (h+1)*20, chaoPaint); xInicio = -1; } } }
Há várias formas de fazer esse desenho, inclusive num jogo de verdade utilizaria sprites do cenário, mas aqui fiz dessa forma que pega automaticamente nosso esqueletoFase e desenha, onde for true, um quadrado branco. Esse código foi otimizado para plataformas horizontais, não sendo indicado para desenhar paredes ("plataformas verticais").
Nosso exemplo está pronto e ao ser executado terá um resultado satisfatório: o herói começará caindo para ultima plataforma e você poderá move-lo e subir em outras plataformas. Claro que ainda há muita coisa a se fazer, por exemplo: nosso jogo apenas verifica se o herói tocou no chão e o coloca em cima desse chão, então é possível ele passar dentro da plataforma de baixo para cima. Também se o herói tentar sair da tela é produzido um erro.
O código completo e o apk está disponível aqui!
Por enquanto é isso, qualquer coisa entrem em contato. Até mais.
22 comentários:
Muito bom o tutorial!
Mas eu tenho uma dúvida, essa biblioteca quem desenvolveu foi você?
tem documentação ?
Sim, foi eu. Não tem documentação mas fiz uns tutoriais sobre a base dela. Claro que agora está numa versão mas avançada, mas a base é a mesma: http://tutoriandroid.blogspot.com.br/2012/01/game-engine-parte-1-criando-o-loop.html
Beleza, show de bola !
Parabéns, através de seus artigos consegui fazer umas coisinhas legais, tou pegando uma base boá pra desenvolver um jogo.
Tou com uma idéia de um jogo inovador!
Obrigado pelos os tutoriais.
Abraço.
Obrigado! =]
Se precisar de ajuda só me falar.
Parabéns, seus tutoriais são muito bons. Estou com uma dúvida e talvez você possa me ajudar, se não for muito incômodo, como ficaria o código com a implementação da câmera para movimentação horizontal e vertical? Desde já, muito obrigado.
@Alexander Obrigado! Na ultima versão da minha Game Engine ( http://code.google.com/p/gdacarv-android-game-engine/ ) está implementado esse conceito de câmera. O que eu fiz foi criar duas variáveis no GameView chamado CameraX e CameraY, que controlam a posição da câmera, e eu passo-os como parâmetro no onDraw do Sprite, e lá dentro é feito os cálculos necessários ( http://code.google.com/p/gdacarv-android-game-engine/source/browse/trunk/%20gdacarv-android-game-engine/src/com/gdacarv/engine/androidgame/Sprite.java#107 ).
Entendi... Muito obrigado, com sua ajuda consegui resolver meu problema... Sua Game Engine está muito boa. Ah, só lembrando, desconsidera a minha pergunta no facebook, é que eu tinha feito a a mesma pergunta lá. Parabéns e muito obrigado...
como baixa esse progama de criar jogos para android?
@arsolid: http://www.tutoriandroid.com/2012/02/configurando-o-ambiente-de.html
http://www.tutoriandroid.com/2012/02/hello-android-parte-1.html
http://www.tutoriandroid.com/2012/01/game-engine-parte-1-criando-o-loop.html
po cara... bacana esse tuto tambem!
mas fiquei com uma dúvida...
esse esquema de clicar ali, é padrão assim?
ou eu consigo fazer com que as ações sejam executadas quando clicar em um botão específico na tela??
posso mandar ele fazer outras ações também?
@André: Obrigado. =) Essa forma de clicar é especifica para esse jogo. Normalmente num aplicativo comum é utilizado Listeners. Veja esse tutorial que contém um exemplo: http://www.tutoriandroid.com/2012/02/hello-android-parte-1.html
Como faço para importar esse seu projeto? Fica dando erro para mim?
Olá, estou com uma dúvida bem besta, é o seguinte eu desenhei um circulo, e queria que ele se movimentasse, quando eu clicasse sobre ele e movimentasse, da maneira que eu fiz ele só vai para onde eu clico na tela, não consegui pensar em nada.
cara, ta dando esse erro aqui:
10-21 09:57:21.739: E/AndroidRuntime(5337): FATAL EXCEPTION: main
10-21 09:57:21.739: E/AndroidRuntime(5337): java.lang.IllegalArgumentException: pointerIndex out of range
10-21 09:57:21.739: E/AndroidRuntime(5337): at android.view.MotionEvent.nativeGetAxisValue(Native Method)
10-21 09:57:21.739: E/AndroidRuntime(5337): at android.view.MotionEvent.getX(MotionEvent.java:1981)
10-21 09:57:21.739: E/AndroidRuntime(5337): at com.gdacarv.engine.androidgame.GameView$HandlerTouchEvents.handle(GameView.java:128)
10-21 09:57:21.739: E/AndroidRuntime(5337): at com.gdacarv.engine.androidgame.GameView.onTouchEvent(GameView.java:92)
10-21 09:57:21.739: E/AndroidRuntime(5337): at android.view.View.dispatchTouchEvent(View.java:7253)
10-21 09:57:21.739: E/AndroidRuntime(5337): at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2174)
10-21 09:57:21.739: E/AndroidRuntime(5337): at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:1875)
10-21 09:57:21.739: E/AndroidRuntime(5337): at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2174)
10-21 09:57:21.739: E/AndroidRuntime(5337): at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:1875)
10-21 09:57:21.739: E/AndroidRuntime(5337): at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2174)
10-21 09:57:21.739: E/AndroidRuntime(5337): at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:1875)
10-21 09:57:21.739: E/AndroidRuntime(5337): at com.android.internal.policy.impl.PhoneWindow$DecorView.superDispatchTouchEvent(PhoneWindow.java:2215)
10-21 09:57:21.739: E/AndroidRuntime(5337): at com.android.internal.policy.impl.PhoneWindow.superDispatchTouchEvent(PhoneWindow.java:1458)
10-21 09:57:21.739: E/AndroidRuntime(5337): at android.app.Activity.dispatchTouchEvent(Activity.java:2410)
10-21 09:57:21.739: E/AndroidRuntime(5337): at com.android.internal.policy.impl.PhoneWindow$DecorView.dispatchTouchEvent(PhoneWindow.java:2163)
10-21 09:57:21.739: E/AndroidRuntime(5337): at android.view.View.dispatchPointerEvent(View.java:7433)
10-21 09:57:21.739: E/AndroidRuntime(5337): at android.view.ViewRootImpl.deliverPointerEvent(ViewRootImpl.java:3220)
10-21 09:57:21.739: E/AndroidRuntime(5337): at android.view.ViewRootImpl.deliverInputEvent(ViewRootImpl.java:3165)
10-21 09:57:21.739: E/AndroidRuntime(5337): at android.view.ViewRootImpl.doProcessInputEvents(ViewRootImpl.java:4292)
10-21 09:57:21.739: E/AndroidRuntime(5337): at android.view.ViewRootImpl.enqueueInputEvent(ViewRootImpl.java:4271)
10-21 09:57:21.739: E/AndroidRuntime(5337): at android.view.ViewRootImpl$WindowInputEventReceiver.onInputEvent(ViewRootImpl.java:4363)
10-21 09:57:21.739: E/AndroidRuntime(5337): at android.view.InputEventReceiver.dispatchInputEvent(InputEventReceiver.java:179)
10-21 09:57:21.739: E/AndroidRuntime(5337): at android.os.MessageQueue.nativePollOnce(Native Method)
10-21 09:57:21.739: E/AndroidRuntime(5337): at android.os.MessageQueue.next(MessageQueue.java:125)
10-21 09:57:21.739: E/AndroidRuntime(5337): at android.os.Looper.loop(Looper.java:124)
10-21 09:57:21.739: E/AndroidRuntime(5337): at android.app.ActivityThread.main(ActivityThread.java:5227)
10-21 09:57:21.739: E/AndroidRuntime(5337): at java.lang.reflect.Method.invokeNative(Native Method)
10-21 09:57:21.739: E/AndroidRuntime(5337): at java.lang.reflect.Method.invoke(Method.java:511)
10-21 09:57:21.739: E/AndroidRuntime(5337): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:795)
10-21 09:57:21.739: E/AndroidRuntime(5337): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:562)
10-21 09:57:21.739: E/AndroidRuntime(5337): at dalvik.system.NativeStart.main(Native Method)
Acontece quando eu clico em qualquer lugar da tela.
Cara nenhum dos seus tutoriais funciona, por favor revisa isso antes
Parabéns pelos tutoriais, me ajudaram bastante :)
Você poderia desenvolver alguns tutoriais mais avançados, com conceitos de física e movimentação, seria bem interessante já que os tutoriais são todos de alta qualidade.
O jogo abre e fecha. Tem como resolver esse problema?
A maioria dos seu exemplos não funcionam, uma boa parte deles sempre abre e fecha o app. Especifique a versão do android compativel com seu app. Todos ficariam gratos.
Postar um comentário