terça-feira, 29 de maio de 2012

Como criar um jogo estilo plataforma

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:

John Lenon Design at 1 de junho de 2012 às 01:28 disse...

Muito bom o tutorial!
Mas eu tenho uma dúvida, essa biblioteca quem desenvolveu foi você?
tem documentação ?

Gustavo Carvalho at 1 de junho de 2012 às 09:01 disse...

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

John Lenon Design at 5 de junho de 2012 às 00:25 disse...

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.

Gustavo Carvalho at 5 de junho de 2012 às 08:05 disse...

Obrigado! =]

Se precisar de ajuda só me falar.

Unknown at 6 de agosto de 2012 às 10:57 disse...
Este comentário foi removido pelo autor.
Unknown at 6 de agosto de 2012 às 11:10 disse...

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.

Gustavo Carvalho at 7 de agosto de 2012 às 05:27 disse...

@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 ).

Unknown at 7 de agosto de 2012 às 19:16 disse...

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...

arsolid at 29 de outubro de 2012 às 22:55 disse...

como baixa esse progama de criar jogos para android?

Gustavo Carvalho at 19 de novembro de 2012 às 05:47 disse...

@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

André Kunde at 29 de novembro de 2012 às 16:34 disse...

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?

Gustavo Carvalho at 6 de dezembro de 2012 às 05:32 disse...

@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

Unknown at 14 de fevereiro de 2013 às 12:06 disse...

Como faço para importar esse seu projeto? Fica dando erro para mim?

Marcos at 7 de outubro de 2013 às 01:13 disse...

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.

Kavu at 21 de outubro de 2013 às 09:05 disse...

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)

Kavu at 21 de outubro de 2013 às 09:07 disse...

Acontece quando eu clico em qualquer lugar da tela.

Anônimo at 19 de agosto de 2014 às 11:42 disse...

Cara nenhum dos seus tutoriais funciona, por favor revisa isso antes

Anônimo at 16 de dezembro de 2014 às 18:46 disse...

Parabéns pelos tutoriais, me ajudaram bastante :)

Anônimo at 16 de dezembro de 2014 às 18:49 disse...

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.

Anônimo at 23 de abril de 2015 às 11:41 disse...

O jogo abre e fecha. Tem como resolver esse problema?

Anônimo at 20 de novembro de 2015 às 09:32 disse...

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.

Anônimo at 10 de julho de 2016 às 16:05 disse...
Este comentário foi removido pelo autor.

Postar um comentário

 
© 2011 Tutoriandroid | Recode by Ardhiansyam | Based on Android Developers Blog