Continuando a série de desenvolvimento de jogos para Android, vamos completar nosso jogo e deixa-lo jogável. Vamos criar as telas iniciais e de game over, criar a interação e computar scores e levels.
Veja a primeira parte aqui.
Vamos criar a tela inicial (que é a da foto). Criaremos uma TitleActivity e apenas setamos o contentView dela para um new TitleGameView(this) que iremos criar em seguida:
package com.tutoriandroid.games.smash; |
import android.app.Activity; |
import android.os.Bundle; |
public class TitleActivity extends Activity { |
/** Called when the activity is first created. */ |
@Override |
public void onCreate(Bundle savedInstanceState) { |
super.onCreate(savedInstanceState); |
setContentView(new TitleGameView(this)); |
} |
} |
Temos que adicionar uma referência a ele no AndroidManifest, como toda Activity, e seta-la como Activity inicial da aplicação:
<?xml version="1.0" encoding="utf-8"?> |
<manifest xmlns:android="http://schemas.android.com/apk/res/android" |
package="com.tutoriandroid.games.smash" |
android:versionCode="1" |
android:versionName="1.0" > |
<uses-sdk android:minSdkVersion="7" /> |
<application |
android:icon="@drawable/ic_launcher" |
android:label="@string/app_name" > |
<activity |
android:name=".TitleActivity" |
android:label="@string/app_name" > |
<intent-filter> |
<action android:name="android.intent.action.MAIN" /> |
<category android:name="android.intent.category.LAUNCHER" /> |
</intent-filter> |
</activity> |
<activity android:name=".MainGameActivity"/> |
</application> |
</manifest> |
E vamos criar o TitleGameView, que ira executar tudo da tela inicial:
package com.tutoriandroid.games.smash;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.view.MotionEvent;
import com.gdacarv.engine.androidgame.GameView;
import com.gdacarv.engine.androidgame.Sprite;
public class TitleGameView extends GameView {
private Paint paintText;
private Context context;
public TitleGameView(Context context) {
super(context);
this.context = context;
}
@Override
public void TouchEvents(MotionEvent event) {
if((event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN){
Context ctx = getContext();
((Activity) ctx).finish();
Intent intent = new Intent(ctx, MainGameActivity.class); //Inicia o jogo
ctx.startActivity(intent);
}
}
@Override
protected void onLoad() {
Resources res = getResources();
Sprite title;
mSprites.add(title = new Sprite(BitmapFactory.decodeResource(res, R.drawable.title)));
title.x = getWidth()/2 - title.width/2;
title.y = 20;
paintText = new Paint();
paintText.setColor(Color.WHITE);
paintText.setTextSize(25);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawText(context.getString(R.string.iniciar_jogo), 50, getHeight()*0.6f, paintText);
}
}
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.view.MotionEvent;
import com.gdacarv.engine.androidgame.GameView;
import com.gdacarv.engine.androidgame.Sprite;
public class TitleGameView extends GameView {
private Paint paintText;
private Context context;
public TitleGameView(Context context) {
super(context);
this.context = context;
}
@Override
public void TouchEvents(MotionEvent event) {
if((event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN){
Context ctx = getContext();
((Activity) ctx).finish();
Intent intent = new Intent(ctx, MainGameActivity.class); //Inicia o jogo
ctx.startActivity(intent);
}
}
@Override
protected void onLoad() {
Resources res = getResources();
Sprite title;
mSprites.add(title = new Sprite(BitmapFactory.decodeResource(res, R.drawable.title)));
title.x = getWidth()/2 - title.width/2;
title.y = 20;
paintText = new Paint();
paintText.setColor(Color.WHITE);
paintText.setTextSize(25);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawText(context.getString(R.string.iniciar_jogo), 50, getHeight()*0.6f, paintText);
}
}
O que fazemos aqui é apenas desenhar uma sprite e uma mensagem na tela, então temos que adiciona-los aos recursos. Adicione a seguinte imagem em res/drawable-hdpi/:
title.png |
E em res/values/strings.xml adicione:
<string name="iniciar_jogo">Toque na tela para iniciar o jogo</string>
Pronto. Agora nosso jogo tem uma tela inicial bem simples. Vamos adicionar a tela de Game Over. O processo é análogo: crie uma GameOverActivity setando o contentView para um new GameOverView(this), adicione a referência ao AndroidManifest(sem a parte de configurar como Activity principal):
<activity android:name=".GameOverActivity"/>
E crie o GameOverView:
package com.tutoriandroid.games.smash;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.view.MotionEvent;
import com.gdacarv.engine.androidgame.GameView;
import com.gdacarv.engine.androidgame.Sprite;
public class GameOverView extends GameView {
private Paint paintText;
private Context context;
private int score;
public GameOverView(Context context) {
super(context);
this.context = context;
}
@Override
public void TouchEvents(MotionEvent event) {
if((event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN){
Context ctx = getContext();
((Activity) ctx).finish();
Intent intent = new Intent(ctx, MainGameActivity.class); //Inicia novamente o jogo
ctx.startActivity(intent);
}
}
@Override
protected void onLoad() {
Resources res = getResources();
Sprite gameover;
mSprites.add(gameover = new Sprite(BitmapFactory.decodeResource(res, R.drawable.gameover)));
gameover.x = getWidth()/2 - gameover.width/2;
gameover.y = (int) (getHeight()*0.2f);
paintText = new Paint();
paintText.setColor(Color.WHITE);
paintText.setTextSize(25);
score = ((Activity) context).getIntent().getIntExtra("SCORE", 0); //Carrega o score passado pela Intent
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawText(context.getString(R.string.score) + " " + score, 50, getHeight()*0.6f, paintText);
canvas.drawText(context.getString(R.string.iniciar_jogo), 50, getHeight()*0.8f, paintText);
}
}
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.view.MotionEvent;
import com.gdacarv.engine.androidgame.GameView;
import com.gdacarv.engine.androidgame.Sprite;
public class GameOverView extends GameView {
private Paint paintText;
private Context context;
private int score;
public GameOverView(Context context) {
super(context);
this.context = context;
}
@Override
public void TouchEvents(MotionEvent event) {
if((event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN){
Context ctx = getContext();
((Activity) ctx).finish();
Intent intent = new Intent(ctx, MainGameActivity.class); //Inicia novamente o jogo
ctx.startActivity(intent);
}
}
@Override
protected void onLoad() {
Resources res = getResources();
Sprite gameover;
mSprites.add(gameover = new Sprite(BitmapFactory.decodeResource(res, R.drawable.gameover)));
gameover.x = getWidth()/2 - gameover.width/2;
gameover.y = (int) (getHeight()*0.2f);
paintText = new Paint();
paintText.setColor(Color.WHITE);
paintText.setTextSize(25);
score = ((Activity) context).getIntent().getIntExtra("SCORE", 0); //Carrega o score passado pela Intent
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawText(context.getString(R.string.score) + " " + score, 50, getHeight()*0.6f, paintText);
canvas.drawText(context.getString(R.string.iniciar_jogo), 50, getHeight()*0.8f, paintText);
}
}
Novamente essa tela apenas exibe uma imagem, o score (que é passado do MainGame pela intent) e uma mensagem. Adicione a seguinte imagem em res/drawable-hdpi/:
gameover.png |
E a seguinte string em res/values/strings.xml:
<string name="score">Score:</string>
Vamos então modificar nosso MainGameView para responder ao toque, passar de nível e dar Game Over. Primeiro vamos modificar e acrescentar variáveis:
public class MainGameView extends GameView { |
protected int level = 1, score = 0; // Mudar level inicial para 1 |
protected ArrayList<Pink> pinks; |
protected ArrayList<Gold> golds; |
protected Background background; |
private long startTime; // Tempo de inicio do jogo, usado no score |
private Context context; // Salvar referência do contexto |
protected Sprite nextLevelSprite; // Sprite que mostra imagem NextLevel |
private Paint paintText; |
float scoreX, scoreY; // Posição da mensagem do score |
private int alivePinks; // Quantidade de pinks vivos |
public MainGameView(Context context) { |
super(context); |
this.context = context; |
} |
Feito isso podemos criar os eventos de toque:
@Override
public void TouchEvents(MotionEvent event) {
if((event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN){
if(alivePinks > 0){ // Se tiver pinks vivos, verifica se clicou em pink ou gold
float x = event.getX(), y = event.getY();
for(Gold gold : golds) // Para cada gold...
if(x > gold.x && y > gold.y && x < gold.x + gold.width && y < gold.y + gold.height){ // Verifica de clicou nele...
((Activity) context).finish(); // Se clicou termina a activity...
Intent intent = new Intent(context, GameOverActivity.class); // Chama o GameOverActivity...
intent.putExtra("SCORE", score); // Passa o score como parâmetro
context.startActivity(intent);
}
for(Pink pink : pinks) // Para cada pink...
if(!pink.isDead() && x > pink.x && y > pink.y && x < pink.x + pink.width && y < pink.y + pink.height){ // Se o pink estiver vivo e tocar nele...
pink.kill(); // Mata o pink...
int add;
score += add = (int) Math.max(100 - level*3 - (System.currentTimeMillis() - startTime)/500, 1); // Adiciona o score dependendo de qual rapido matou o pink...
alivePinks--; // Subtrai um do contador de pinks vivos...
if(alivePinks == 0) // Se não tem mais nenhum vivo...
nextLevelSprite.visible = true; // Exibe imagem de proximo nível
Log.d("Score", "Valeu: " + add);
}
}else{ // Sem pink vivo, a mensagens de proximo level já está exibida e tocar significa passar de level
newStage();
}
}
}
public void TouchEvents(MotionEvent event) {
if((event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN){
if(alivePinks > 0){ // Se tiver pinks vivos, verifica se clicou em pink ou gold
float x = event.getX(), y = event.getY();
for(Gold gold : golds) // Para cada gold...
if(x > gold.x && y > gold.y && x < gold.x + gold.width && y < gold.y + gold.height){ // Verifica de clicou nele...
((Activity) context).finish(); // Se clicou termina a activity...
Intent intent = new Intent(context, GameOverActivity.class); // Chama o GameOverActivity...
intent.putExtra("SCORE", score); // Passa o score como parâmetro
context.startActivity(intent);
}
for(Pink pink : pinks) // Para cada pink...
if(!pink.isDead() && x > pink.x && y > pink.y && x < pink.x + pink.width && y < pink.y + pink.height){ // Se o pink estiver vivo e tocar nele...
pink.kill(); // Mata o pink...
int add;
score += add = (int) Math.max(100 - level*3 - (System.currentTimeMillis() - startTime)/500, 1); // Adiciona o score dependendo de qual rapido matou o pink...
alivePinks--; // Subtrai um do contador de pinks vivos...
if(alivePinks == 0) // Se não tem mais nenhum vivo...
nextLevelSprite.visible = true; // Exibe imagem de proximo nível
Log.d("Score", "Valeu: " + add);
}
}else{ // Sem pink vivo, a mensagens de proximo level já está exibida e tocar significa passar de level
newStage();
}
}
}
Perceba que precisamos usar a função pink.isDead(), que não fizemos anteriormente. Então vamos adiciona-la agora na classe Pink:
public boolean isDead() { |
return dying || dead; |
} |
Ela apenas retorna de o Pink esta morto ou morrendo. Outra mudança é a criação do método newStage() que cria novos Pinks e um novo Gold para o jogo. Com isso, vamos alterar o onLoad para que ele não crie os Pinks (deixe isso para o newStage) e carregue a imagem do próximo level e o texto que mostra o score:
@Override
protected void onLoad() {
pinks = new ArrayList<Pink>();
golds = new ArrayList<Gold>();
Random random = new Random();
Resources res = getResources();
Bitmap bitmapGold = BitmapFactory.decodeResource(res, R.drawable.gold_head);
int limitGoldX = getWidth()-bitmapGold.getWidth()/8,
limitGoldY = getHeight()-bitmapGold.getHeight();
for(int i = 0; i < 10; i++)
golds.add(new Gold(random.nextInt(limitGoldX), random.nextInt(limitGoldY), random, bitmapGold, 1, 8));
background = new Background(random, getWidth(), getHeight(), BitmapFactory.decodeResource(res, R.drawable.background));
mSprites.add(background);
mSprites.addAll(golds);
paintText = new Paint();
paintText.setColor(Color.WHITE);
paintText.setTextSize(25);
scoreX = getWidth()*0.05f;
scoreY = getHeight()*0.95f;
mSprites.add(nextLevelSprite = new Sprite(BitmapFactory.decodeResource(res, R.drawable.nextlevel)));
nextLevelSprite.x = getWidth()/2 - nextLevelSprite.width/2;
nextLevelSprite.y = (int) (getHeight()*0.2f);
nextLevelSprite.visible = false;
newStage();
}
protected void onLoad() {
pinks = new ArrayList<Pink>();
golds = new ArrayList<Gold>();
Random random = new Random();
Resources res = getResources();
Bitmap bitmapGold = BitmapFactory.decodeResource(res, R.drawable.gold_head);
int limitGoldX = getWidth()-bitmapGold.getWidth()/8,
limitGoldY = getHeight()-bitmapGold.getHeight();
for(int i = 0; i < 10; i++)
golds.add(new Gold(random.nextInt(limitGoldX), random.nextInt(limitGoldY), random, bitmapGold, 1, 8));
background = new Background(random, getWidth(), getHeight(), BitmapFactory.decodeResource(res, R.drawable.background));
mSprites.add(background);
mSprites.addAll(golds);
paintText = new Paint();
paintText.setColor(Color.WHITE);
paintText.setTextSize(25);
scoreX = getWidth()*0.05f;
scoreY = getHeight()*0.95f;
mSprites.add(nextLevelSprite = new Sprite(BitmapFactory.decodeResource(res, R.drawable.nextlevel)));
nextLevelSprite.x = getWidth()/2 - nextLevelSprite.width/2;
nextLevelSprite.y = (int) (getHeight()*0.2f);
nextLevelSprite.visible = false;
newStage();
}
Adicione a seguinte imagem em res/drawable-hdpi/:
nextlevel.png |
Precisamos também alterar o update do MainGameView para que os Pinks mortos sejam limpados de nossas listas, e adicionar uma verificações para evitar bugs como clicar exatamente no momento de troca de direção:
@Override
public void update() {
super.update();
Pink pink;
for(int i = 0; i < pinks.size(); i++){
pink = pinks.get(i);
if(pink.dead){ // Se ta morto, retira das listas
mSprites.remove(pink);
pinks.remove(pink);
i--;
}else if(!pink.isDead()){
if(pink.x < 0 && pink.direction >= 5 && pink.direction <= 7)
pink.changeDirection((byte) (4 - pink.direction % 4));
else if(pink.x > getWidth()-pink.width && pink.direction >= 1 && pink.direction <= 3)
pink.changeDirection((byte) (8 - pink.direction));
else if(pink.y < 0 && pink.direction >= 3 && pink.direction <= 5)
pink.changeDirection((byte) ((12 - pink.direction) % 8));
else if(pink.y > getHeight()-pink.height && (pink.direction >= 7 || pink.direction <= 1))
pink.changeDirection((byte) (5 - (pink.direction+1) % 8));
}
}
for(Gold gold : golds)
if(gold.x < 0 || gold.x > getWidth()-gold.width)
gold.speedX *= -1;
else if(gold.y < 0 || gold.y > getHeight()-gold.height)
gold.speedY *= -1;
}
public void update() {
super.update();
Pink pink;
for(int i = 0; i < pinks.size(); i++){
pink = pinks.get(i);
if(pink.dead){ // Se ta morto, retira das listas
mSprites.remove(pink);
pinks.remove(pink);
i--;
}else if(!pink.isDead()){
if(pink.x < 0 && pink.direction >= 5 && pink.direction <= 7)
pink.changeDirection((byte) (4 - pink.direction % 4));
else if(pink.x > getWidth()-pink.width && pink.direction >= 1 && pink.direction <= 3)
pink.changeDirection((byte) (8 - pink.direction));
else if(pink.y < 0 && pink.direction >= 3 && pink.direction <= 5)
pink.changeDirection((byte) ((12 - pink.direction) % 8));
else if(pink.y > getHeight()-pink.height && (pink.direction >= 7 || pink.direction <= 1))
pink.changeDirection((byte) (5 - (pink.direction+1) % 8));
}
}
for(Gold gold : golds)
if(gold.x < 0 || gold.x > getWidth()-gold.width)
gold.speedX *= -1;
else if(gold.y < 0 || gold.y > getHeight()-gold.height)
gold.speedY *= -1;
}
Precisamos também modificar o onDraw para mostrar o score e a mensagem de próximo level quando preciso:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawText(context.getString(R.string.score) + " " + score, scoreX, scoreY, paintText);
if(nextLevelSprite.visible)
canvas.drawText(context.getString(R.string.nextlevel_msg), 50, getHeight()*0.7f, paintText);
}
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawText(context.getString(R.string.score) + " " + score, scoreX, scoreY, paintText);
if(nextLevelSprite.visible)
canvas.drawText(context.getString(R.string.nextlevel_msg), 50, getHeight()*0.7f, paintText);
}
Claro, precisamos adicionar uma string em res/values/strings.xml:
<string name="nextlevel_msg">Toque na tela para continuar o jogo</string>
private void newStage() {
level++; //Passa de level
nextLevelSprite.visible = false; //Deixa a mensagem e imagem de proximo level invisiveis
alivePinks = 5+level; //Define a quantidade de Pinks de acordo com o level
startTime = System.currentTimeMillis(); // Guarda o tempo do inicio do level
Random random = new Random();
Resources res = getResources();
Bitmap bitmapPink = BitmapFactory.decodeResource(res, R.drawable.pink);
Bitmap bitmapGold = BitmapFactory.decodeResource(res, R.drawable.gold_head);
Pink pink;
Gold gold;
int limitPinkX = getWidth()-bitmapPink.getWidth()/6,
limitPinkY = getHeight()-bitmapPink.getHeight()/5,
limitGoldX = getWidth()-bitmapGold.getWidth()/8,
limitGoldY = getHeight()-bitmapGold.getHeight();
for(int i = 0; i < alivePinks; i++){ // Cria Pinks
pinks.add(pink = new Pink(random.nextInt(limitPinkX), random.nextInt(limitPinkY), random, bitmapPink, 5, 6));
pink.speed = level; // Velocidade de acordo com o level
}
golds.add(gold = new Gold(random.nextInt(limitGoldX), random.nextInt(limitGoldY), random, bitmapGold, 1, 8)); // Cria mais um Gold
gold.speedX *= 1+level/3; // Gold criado pode ser mais rapido que o normal
gold.speedY *= 1+level/3;
mSprites.addAll(1, pinks); //Adiciona os Sprites criado se preocupado com a ordem da lista pois influencia na ordem de desenho
mSprites.add(mSprites.size()-1, gold);
}
level++; //Passa de level
nextLevelSprite.visible = false; //Deixa a mensagem e imagem de proximo level invisiveis
alivePinks = 5+level; //Define a quantidade de Pinks de acordo com o level
startTime = System.currentTimeMillis(); // Guarda o tempo do inicio do level
Random random = new Random();
Resources res = getResources();
Bitmap bitmapPink = BitmapFactory.decodeResource(res, R.drawable.pink);
Bitmap bitmapGold = BitmapFactory.decodeResource(res, R.drawable.gold_head);
Pink pink;
Gold gold;
int limitPinkX = getWidth()-bitmapPink.getWidth()/6,
limitPinkY = getHeight()-bitmapPink.getHeight()/5,
limitGoldX = getWidth()-bitmapGold.getWidth()/8,
limitGoldY = getHeight()-bitmapGold.getHeight();
for(int i = 0; i < alivePinks; i++){ // Cria Pinks
pinks.add(pink = new Pink(random.nextInt(limitPinkX), random.nextInt(limitPinkY), random, bitmapPink, 5, 6));
pink.speed = level; // Velocidade de acordo com o level
}
golds.add(gold = new Gold(random.nextInt(limitGoldX), random.nextInt(limitGoldY), random, bitmapGold, 1, 8)); // Cria mais um Gold
gold.speedX *= 1+level/3; // Gold criado pode ser mais rapido que o normal
gold.speedY *= 1+level/3;
mSprites.addAll(1, pinks); //Adiciona os Sprites criado se preocupado com a ordem da lista pois influencia na ordem de desenho
mSprites.add(mSprites.size()-1, gold);
}
Pronto. Agora nosso jogo já tem tela inicial, game over, levels, score e funciona bem. No próximo tutorial faremos o jogo salvar o high score, adicionaremos algumas animações e efeitos sonoros.
Desenvolvendo jogo para Android - Parte 3 - Sons e High Score >>
Desenvolvendo jogo para Android - Parte 3 - Sons e High Score >>
Até mais.
5 comentários:
Tutorial é ótimo! só senti falta de mais detalhamento sobre algumas funções e onde certas coisas acontece. se possível poderia disponibilizar uma versão comentada.
Fiquei com dúvida sobre como posso alterar as imagens pq usar certa função e assim por diante!
E você pode indicar algum livro? e quais são suas fontes de estudos?
Muito obrigado pela tutorial.
@Diego: Obrigado! Para alterar as imagens você tem que alterar o resource passado pro bitmap em:
Bitmap bitmapPink = BitmapFactory.decodeResource(res, R.drawable.pink);
Bitmap bitmapGold = BitmapFactory.decodeResource(res, R.drawable.gold_head);
Esse trecho do código esta na parte um do tutorial.
Sobre o livro, eu acho esses bons: http://www.livroandroid.com.br/
Tenho os dois e são bons. O problema com livros é que a tecnologia Android evolui muito rápido então provavelmente você nunca terá um livro que cubra as ultimas versões por mais de 3 meses.
Fora isso, muito Google, Stackoverflow e a documentação oficial em http://d.android.com
Gustavo muito bom saber q alguem programa p/ android. Acho muito fácil trabalhar com unity 3d e ate da a cara de um joguim só p/ teste.
Agora oque eu não conseguir foi os scripts de movimentos touch, de um personagem ex.: p/ frente ,p/ os lados e etc. VC poderia me ajudar?
E um conselho acho, q vc se daria bem com o unity, a licença Android está grátis agora.
Aguardo resposta vlw.abraço.
Parabéns, o melhor tutorial que ja vi na net.
pratico e funcional, excelente.
Olá,
Excelente tutorial! Parabéns!
Estava usando seu material para estudos e terminei esta parte, mas trava quando tento jogá-lo (encerra a aplicação). Pode me ajudar por favor? Obrigado!
Postar um comentário