Game em Canvas no Android!

Introdução

Sempre tive interesse em aprender como fazer jogos para dispositivos móveis, mas não sabia por onde começar. Depois de algumas pesquisas na internet encontrei esse tutorial. Nesse artigo o autor ensina a fazer um jogo de ping-pong no Canvas. Depois de seguir os passos escritos no texto, consegui fazer o jogo rodar. Desde então fiquei trabalhando em maneiras de melhorar o código de modo que consigamos criar outros tipos de jogos a partir dele. Além de deixa-lo mais legível e de fácil entendimento.

Agora que já consegui melhorar um pouco a estrutura desse jogo, vou compartilhar o resultado com vocês neste Post.

Obs: Se seu interesse é apenas saber um pouco mais sobre Canvas recomendo que veja esse video.

O Game

Vamos construir o mesmo jogo deste post.
Se trata de um jogo de ping pong, onde existe uma barra que impede a bolinha de passar para o fim da tela. A barra é controlada pelo usuário com o dedo.
Somente algumas novas funcionalidades foram acrescentadas, se a bolinha passar da barra, o celular irá vibrar e o usuário perderá uma vida.
No final se ele perder três vidas aparecerá uma tela de Game Over.

Segue abaixo uma tela do jogo.

Tela do Jogo

A Estrutura do projeto

As principais estruturas necessárias para a criação desse jogo são:

– A Activity, representa a tela do jogo. Apenas conterá um componente, o GameView.

GameView, se trata de um componente gráfico que herda funcionalidades da classe SurfaceView. Essa classe por sua vez é uma View, muito utilizada para execução de streams de vídeo e desenhos em canvas. Veja a documentação oficial aqui.
O GameView vai interagir com o componente GameThread.

GameThread, a lógica do jogo irá acontecer aqui. Se trata de uma classe que herda funcionalidades da classe Thread. Sua ação é rodar um loop infinito onde cada iteração do loop acarretará em mudanças do jogo. Por exemplo, a cada iteração do loop a bola movimenta uma posição.

– Outra estrutura importante é o Sprite, aqui chamamos de Sprite os principais atores do jogo que irão se relacionar entre si. Nesse jogo específico podemos destacar apenas dois: a barra e a bola. Eles que irão movimentar pela tela, se colidirem, mudarem de direção dentre outras coisas.

Vamos a prática…

Agora que sabemos um pouco sobre os principais componentes que o nosso projeto deve ter. Vamos saber como cria-los.
Primeiro, Crie um projeto no Eclipse e o chame de AndroidGame. Quando criamos um projeto Android no eclipse, dependendo da configuração de criação a ferramenta já inicia o projeto com uma Activity.
Iremos aproveita-la, como vimos anteriormente a Activity do Game terá apenas um componente gráfico, o GameView que por sua vez também será criado por nós.
Após colocarmos o GameView na nossa Activity, iniciamos a Thread do jogo através do próprio GameView.

Acompanhe o código logo abaixo:


public class MainActivity extends Activity {

	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);

		GameView mGameView = new GameView(this);
		setContentView(mGameView);
		mGameView.mThread.doStart();
	}

}

Agora que já utilizamos o GameView vamos saber como cria-lo:


public class GameView extends SurfaceView implements Callback {

	private static final int GAME_OVER = 1;

	Context mContext;    
	GameThread mThread;  

	public GameView(Context context) {
		super(context);
		this.mContext = context;

		getHolder().addCallback(this);

		mThread = new GameThread(getHolder(), mContext, new Handler() {
			@Override
			public void handleMessage(Message m) {
				if(m.what == GAME_OVER) {
					Intent intent = new Intent(mContext, GameOverScreen.class);
					mContext.startActivity(intent);
					((Activity)mContext).finish();
				}

			}
		});

		setFocusable(true);
	}

	@Override
	public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
		Log.d(this.getClass().toString(), "in SurfaceChanged()");
	}

	@Override
	public void surfaceCreated(SurfaceHolder holder) {
		Log.d(this.getClass().toString(), "in SurfaceCreated()");
		mThread.running = true;
		mThread.start();
	}

	@Override
	public boolean onTouchEvent(MotionEvent event) {
		try {
			mThread.onTouch(event);
		} catch(Exception e) {
			e.printStackTrace();
		}

		return true;
	}

	@Override
	public void surfaceDestroyed(SurfaceHolder holder) {
		Log.d(this.getClass().toString(), "in SurfaceDestroyed()");
		boolean retry = true;
		mThread.running = false;
		while (retry) {
			try {
				mThread.join();
				retry = false;
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}

Veja que essa classe possui um objeto do tipo GameThread, sua principal função é oferecer um objeto canvas para que a Thread consiga desenhar o jogo.

Podemos destacar também a presença de um objeto Handler nessa classe que possibilita a interceptação de eventos realizados na Thread do jogo pela nossa Activity. É através dessa estrutura que conseguimos chamar a tela de Game Over por exemplo. Saiba mais sobre a classe Handler neste post.

Repare também que é através da classe GameView que interceptamos os eventos de touch screen, fundamentais para a iteração do usuário com a barra. Os eventos touch screen, são capturados através do método onTouchEvent(MotionEvent event) como pode ser visto ele apenas chama um outro método da classe GameThread pois é la que esse evento será útil.

Agora chegamos a classe GameThread, onde a lógica do Game esta presente.


public class GameThread extends Thread {

	private static final int GAME_OVER = 1;
	public static final int VELOCIDADE_GAME = 10;
	
	private Barra barra;
	private Bola bola;
	private SurfaceHolder mHolder = null;  
	boolean running = false;               

	private int screenCanvasWidth;  
	private int screenCanvasHeight;
	private Canvas c = null;
	private Paint paint = new Paint();
	private Rect rect = new Rect(0, 0, screenCanvasWidth, screenCanvasHeight);
	
	private int quantidadeDeVidas = 3;
	private Context context;
	private Handler handler;

	public GameThread(SurfaceHolder surfaceholder, Context context, Handler handler) {
		this.context = context;
		this.handler = handler;
		paint.setColor(Color.WHITE);
		Velocidade velocidadeAdequadaParaTela = getVelocidadeAdequada();
		bola = new Bola(context, velocidadeAdequadaParaTela);


		barra = new Barra(context, velocidadeAdequadaParaTela);
		barra.setX(100);

		this.mHolder = surfaceholder; 
	}
	
	private Velocidade getVelocidadeAdequada() {
		
		switch (context.getResources().getDisplayMetrics().densityDpi) {
		
		case DisplayMetrics.DENSITY_LOW:
			return Velocidade.TELA_PEQUENA;
		    
		case DisplayMetrics.DENSITY_MEDIUM:
			return Velocidade.TELA_MEDIA;
			
		case DisplayMetrics.DENSITY_HIGH:
			return Velocidade.TELA_GRANDE;
			
		}
		return Velocidade.TELA_EXTRA_GRANDE;
	}

	public void doStart() {
		synchronized(mHolder) {

		}
	}

	public void onTouch(MotionEvent event) {
		int posicaoX = (int) (event.getX() - barra.getWidth() /2);
		barra.setX(posicaoX);
		barra.draw(c);
	}

	@Override
	public void run() {
		while(running) {
			try {
				initializeCanvas();
				update(); 
				draw(c);  
				pausar();
			} finally {
				if(c != null) {
					mHolder.unlockCanvasAndPost(c);
				}
			}

		}
	}
	
	private void initializeCanvas() {
		c = mHolder.lockCanvas(); 
	
		if(screenCanvasWidth == 0) { 
			screenCanvasWidth  = c.getWidth();
			screenCanvasHeight = c.getHeight();
			
			int posicaoY = screenCanvasHeight - 100; 
			barra.setY(posicaoY);
			barra.draw(c);
		}
	}
	
	private void update() {

		bola.movimentarBola();

		bola.mudarDePosicaoSeBaterNaLateral(screenCanvasWidth);
		bola.mudarDePosicaoSeBaterNoTeto(screenCanvasHeight);


		if(bola.getAreaPosicaoY() >= barra.getY()) { 

			if(bola.isHouveColisao(barra)) { 
				bola.mudarDirecaoY();
			} else if(isBolaPassouDaPosicaoDeSerAtingida(bola, barra)) {
				bola.irParaPosicaoEDirecaoInicial();
				vibrar();
				quantidadeDeVidas --;
				chamarTelaGameOverCasoVidasTenhamAcabado();
			}

		}
	}
	
	private boolean isBolaPassouDaPosicaoDeSerAtingida(Bola bola, Barra barra) {
		if(bola.getAreaPosicaoY() >= barra.getAreaPosicaoY()) {
			return true;
		} else {
			return false;
		}
	}
	
	private void vibrar() {
		Vibrator v = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
		v.vibrate(300);
	}
	
	private void chamarTelaGameOverCasoVidasTenhamAcabado() {
		if(quantidadeDeVidas == 0) {
			running = false;
			handler.sendEmptyMessage(GAME_OVER);
		}
	}
	
	private void draw(Canvas c) {
		c.drawColor(Color.BLACK); // Desenhando novamente a tela para que não fique o rastro da bola. 
		c.drawRect(rect, paint);
		bola.draw(c);
		barra.draw(c);
		atualizarVidasGraficamente(c);
	}
	
	private void atualizarVidasGraficamente(Canvas canvas) {
		Bitmap heart = BitmapFactory.decodeResource(context.getResources(), R.drawable.heart);
		for(int i = 1; i <= quantidadeDeVidas; i ++) {
			int width = 10 *  i;
			canvas.drawBitmap(heart,width,10, paint);	
		}
	}
	
	private void pausar() {
		try {
			Thread.sleep(VELOCIDADE_GAME);
		} catch (InterruptedException e) {
			e.printStackTrace();
		} 
	}	
}

A classe GameThread é o coração do nosso Game. A lógica do game será reunida aqui além de se encontrar nessa estrutura os principais métodos de desenho em Canvas.
O loop do jogo, que falamos anteriormente, pode ser encontrado no método run() é la onde tudo acontece.
Podemos ver que a cada interação do loop os métodos initializeCanvas(), update(), draw() e pausar() são executados.
Segue uma breve explicação sobre cada um deles:

initializeCanvas: Esse método recupera a instância de um objeto Canvas através do GameView. É esse objeto Canvas que será utilizado para o desenho do nosso Game.

update: Nesse método, as operações lógicas de cada componente do jogo serão executadas. Por exemplo, é aqui que é verificado se houve alguma colisão, se acabou as vidas do jogador, aqui também é executado a lógica da movimentação da bolinha dentre outras operações do jogo. Repare que esse método executa principalmente ações envolvendo a Bola e a Barra esses elementos são o que definimos como Sprites, principais atores do nosso jogo, serão explicados com mais detalhes logo mais a frente.
No final, depois de todas as verificações e atualizações, o jogo deverá se redesenhar para refletir a lógica que acabou de se executada, é ai que entra o próximo método do loop.

draw: Método executado após o update, é nesse momento que os elementos serão desenhados na tela. Ou seja, se a bolinha estiver movimentando ela será desenhada na nova posição que recebeu no método update.

pausar: Esse método da uma parada no jogo. A intenção é evitar que loop execute em um ritmo tão grande que o celular não consiga acompanhar.

Outro método a ser ressaltado na classe GameThread é o onTouch(MotionEvent event). Como sabemos a bola será movimentada automaticamente, porém a barra que tentara intercepta-la será movimentada pelo jogador através do seu dedo. Esse processo será feito nesse método, basicamente o sistema pega o ponto da tela onde o usuário apertou com o dedo e desenha a barra la.

Agora que vimos a classe GameThread, onde a lógica do game será executada. Vamos agora ver os atores do nosso jogo, a classe Bola e a classe Barra que são muito utilizadas na classe GameThread.

Começando pela classe Bola:

public class Bola extends Sprite {

	public static final int POSICAO_INICIAL_X = 100;
	public static final int POSICAO_INICIAL_Y = 100;

	private int direcaoX;
	private int direcaoY;
	
	public Bola(Context context, Velocidade velocidade) {
		super(context, velocidade);
		direcaoX = getMovimentacaoLateralXDireita();
		direcaoY = getMovimentacaoVerticalYSobe();
		irParaPosicaoEDirecaoInicial();
	}
	
	@Override
	protected int getDrawableId() {
		return R.drawable.ball;
	}

	public void irParaPosicaoEDirecaoInicial() {
		setX(POSICAO_INICIAL_X);
		setY(POSICAO_INICIAL_Y);

		direcaoX = getMovimentacaoLateralXDireita();
		direcaoY = getMovimentacaoVerticalYSobe();
	}

	public void movimentarBola() {
		andarX();
		andarY();
	}

	public void andarX() {
		setX(getX() + direcaoX); // Andara pixels na lateral até bater na parede e ir para o outro lado.
	}

	public void andarY() {
		setY(getY() - direcaoY); // Vai abaixando pixels até chegar no topo onde começara a subir novamente.
	}

	public void mudarDirecaoX() {
		if(direcaoX == getMovimentacaoLateralXDireita()) {
			direcaoX = getMovimentacaoLateralXEsquerda();
		} else {
			
			direcaoX = getMovimentacaoLateralXDireita();
		}
	}

	public void mudarDirecaoY() {
		
		if(direcaoY == getMovimentacaoVerticalYDesce()) {
			direcaoY = getMovimentacaoVerticalYSobe();
		} else {
			direcaoY = getMovimentacaoVerticalYDesce();
		}
	}

	public void mudarDePosicaoSeBaterNaLateral(int screenCanvasWidth) {
		if(isBateuLateralTela(screenCanvasWidth))
			mudarDirecaoX();
	}

	public void mudarDePosicaoSeBaterNoTeto(int screenCanvasHeight) {
		if(isBateuTetoDaTela(screenCanvasHeight))
			mudarDirecaoY();
	}

	public boolean isBateuLateralTela(int screenCanvasWidth) {
		if(getX() < 0 || getAreaPosicaoX() > screenCanvasWidth) {  // Se a posicao da bola for maior que a parede quer dizer que encostou na parede.
			return true;
		} else {
			return false;
		}
	}

	public boolean isBateuTetoDaTela(int screenCanvasHeight) {
		if(getY() < 0 || getAreaPosicaoY() > screenCanvasHeight) { // Se a posicao da bola for maior que o chao quer dizer que encostou no chão.
			return true;
		} else {
			return false;
		}
	}
}

A classe Bola possui uma série de métodos interessantes:

irParaPosicaoEDirecaoInicial
movimentarBola
mudarDirecaoX
mudarDirecaoY
mudarDePosicaoSeBaterNaLateral
mudarDePosicaoSeBaterNoTeto

Esses métodos são necessários pois como sabemos, a Bola se movimentará pela tela de forma automática. A estratégia tomada para conseguir esse comportamento é simples, esse elemento terá um atributo x e y que representam a sua posição na horizontal e na lateral. Quanto mais incrementarmos o valor de Y mais a bola vai irá para baixo, quanto mais incrementarmos o valor de X mais a bola irá para a direita. Sabendo dessas métricas, o que temos que fazer é incrementarmos ou decrementarmos o valor de X e Y em cada iteração do game para que a bola troque de posição X e Y dando a impressão que esta se movimentando.

Também temos os métodos, que mudarão de direção caso a bola bata nas laterais ou no “teto” da tela .

O método irParaPosicaoEDirecaoInicial é executado sempre que a barra não interceptar a bola a tempo, de modo que o usuário irá perder uma vida. Caso ainda tenha outras vidas o jogo é reiniciado através desse método.

Outro ponto que deve ser ressaltado nessa classe é a necessidade do Enum Velocidade. Sabemos que para a bola se movimentar, ela deve incrementar ou decrementar os valores de X e Y para que o canvas o escreva em uma nova posição na tela. Essa incrementação de valores deve variar de acordo com o tamanho da tela. Por xemplo, em uma tela grande se a bola se movimentar 5 pixels por vez a movimentação será muito devagar e o jogo ficará muito lento. Em contrapartida se a tela for pequena essa velocidade poderá ser interessante. Por isso criamos o Enum Velocidade que contém a taxa de incrementação ideal para cada tela. Então o processo fica simples, na classe GameThread achamos qual é o tamanho da tela, e com base nele criamos o enum correto.

Podemos ver o Enum velocidade logo abaixo:


public enum Velocidade {
	
   TELA_PEQUENA (6,-6,4,-4),// Andar pouco de cada vez pq a tela é pequena
   TELA_MEDIA (12,-12,8,-8),
   TELA_GRANDE (18,-18,12,-12),
   TELA_EXTRA_GRANDE (24,-24,16,-16); // Andar muito para compensar o tamanho da tela
   
   private final int movimentoLateralXDireita;
   private final int movimentoLateralXEsquerda;
   
   private final int movimentoVerticalYSobe;
   private final int movimentoVerticalYDesce;
  
   
   private Velocidade(int movimentoLateralXDireita,	int movimentoLateralXEsquerda, int movimentoVerticalYSobe, int movimentoVerticalYDesce) {
		this.movimentoLateralXDireita = movimentoLateralXDireita;
		this.movimentoLateralXEsquerda = movimentoLateralXEsquerda;
		this.movimentoVerticalYSobe = movimentoVerticalYSobe;
		this.movimentoVerticalYDesce = movimentoVerticalYDesce;
	}

    
    public int movimentoLateralXDireita() {
        return this.movimentoLateralXDireita;
    }
    
    
    public int movimentoLateralXEsquerda() {
        return this.movimentoLateralXEsquerda;
    }
    
  
    public int movimentoVerticalYSobe() {
        return this.movimentoVerticalYSobe;
    }
    
  
    public int movimentoVerticalYDesce() {
        return this.movimentoVerticalYDesce;
    }

}

Agora vimos os detalhes da classe Bola vejamos a classe Barra:


public class Barra extends Sprite {

	public Barra(Context context, Velocidade velocidade) {
		super(context, velocidade);
	}

	@Override
	protected int getDrawableId() {
		return R.drawable.bar;
	}
}

Veja que a classe Barra é muito simples, por ser movimentada pelo usuário não devemos fazer nenhuma lógica de movimentação nem de colisão com as laterais ou teto da tela.
Apenas devemos implementar o método getDrawableId(); que representa a imagem da barra que deve ser desenhada. Este método é uma exigência da classe mãe Sprite que também é herdada pela classe Bola.
A classe abstrata Sprite possui uma série de métodos que são úteis para os dois atores do nosso jogo, tais como isHouveColisao, getAreaX, getAreaY, e os atributos x, y, Bitmap e Velocidade.

Podemos analisa-la logo abaixo:


public abstract class Sprite {

	public static final int POSICAO_INICIAL_X = 100;
	public static final int POSICAO_INICIAL_Y = 100;
	
	private Velocidade velocidade;

	private int direcaoX;
	private int direcaoY;

	private int x;
	private int y;
	
	private Context context;
	private Bitmap texture; // A bitmap texture

	public Sprite(Context context, Velocidade velocidade) {
		this.context = context;
		this.velocidade = velocidade;
		Bitmap texture = getBitmapFromResource(getDrawableId());
		this.texture = texture;
		direcaoX = getMovimentacaoLateralXDireita();
		direcaoY = getMovimentacaoVerticalYSobe(); 
		
	}
	
	protected Bitmap getBitmapFromResource(int resId) {
	 return	BitmapFactory.decodeResource(context.getResources(), resId);
	}
	
	protected abstract int getDrawableId();
	
	
	public void setTexture(Bitmap texture) {
		this.texture = texture;
	}
	
	public void setTexture(int resID) {
		this.texture = getBitmapFromResource(resID);
	}
	
	protected int getMovimentacaoLateralXDireita() {
		return velocidade.movimentoLateralXDireita(); 
	}
	
	protected int getMovimentacaoLateralXEsquerda() {
		return velocidade.movimentoLateralXEsquerda();
	}
	
	protected int getMovimentacaoVerticalYSobe() {
		return velocidade.movimentoVerticalYSobe();
	}
	
	protected int getMovimentacaoVerticalYDesce() {
		return velocidade.movimentoVerticalYDesce(); 
	}

	public int getWidth()  {
		return texture.getWidth(); 
	}
	
	public int getHeight() { 
		return texture.getHeight(); 
	}

	public void draw(Canvas c) {
		c.drawBitmap(texture, x, y, null);
	}
	
	public int getAreaPosicaoX() {
		int areaX = x + getWidth();
		return areaX;
	}
	
	public int getAreaPosicaoY() {
		int areaY = y + getHeight();
		return areaY;
	}
	
	public boolean isHouveColisao(Sprite sprite) {
		// Se a barra estiver mais a esquerda do que a bola
		//(Se eu medir a área da barra como um todo terei a falsa impressão que estara mais 
		//a direida do que a posicao da bola)

		if(isHouveColisaoPosicaoX(sprite) && isHouveColisaoPosicaoY(sprite)){
			return true;
		} else {
			return false;
		}

	}
	
	private boolean isHouveColisaoPosicaoX(Sprite sprite) {
		if(sprite.getX() < this.getAreaPosicaoX()  &&  sprite.getAreaPosicaoX() > this.getX()){
			return true;
		} else {
			return false;
		}
	}
	
    private boolean isHouveColisaoPosicaoY(Sprite sprite) {
    	if(sprite.getY() < this.getAreaPosicaoY()  &&  sprite.getAreaPosicaoY() > this.getY()){
			return true;
		} else {
			return false;
		}
	}
	
	
	public void irParaPosicaoEDirecaoInicial() {
		x = POSICAO_INICIAL_X;
		y = POSICAO_INICIAL_Y;

		direcaoX = getMovimentacaoLateralXDireita();
		direcaoY = getMovimentacaoVerticalYSobe();
	}
	
	public int getDirecaoY() {
		return direcaoY;
	}
	
	public int getDirecaoX() {
		return direcaoX;
	}

	public int getX() {
		return x;
	}

	public void setX(int x) {
		this.x = x;
	}
	
	public int getY() {
		return y;
	}
	
	public void setY(int y) {
		this.y = y;
	}
	
}

Atente para o método de colisão, é uma lógica muito interessante que verifica se os sprites invadiram o espaço um do outro.

O único ponto que nos resta mostrar é a tela de GameOver.
Nessa Activity pegamos uma fonte externa e a utilizamos para exibir os dizeres Game Over. Ela faz uso de um layout xml bem simples que possui apenas um componente TextView.

Segue o código do layout logo abaixo:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/black"
    android:orientation="vertical" >

    <TextView
        android:id="@+id/text_view_game_over"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginTop="50dip"
        android:gravity="center"
        android:text="@string/game_over"
        android:textColor="#ef0000"
        android:textSize="70dip" />

</LinearLayout>

Agora segue o código da Activity:

public class GameOverScreen extends Activity {
	
	private static final String FACE_YOUR_FEARS_FONT = "fonts/Face Your Fears.ttf";
	
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.game_over);
		setTitle("");
        
        TextView txtGhost = (TextView) findViewById(R.id.text_view_game_over);
        Typeface tf = Typeface.createFromAsset(getAssets(), FACE_YOUR_FEARS_FONT);
        txtGhost.setTypeface(tf);
	}
}

A tela deve ficar assim:

Tela de Game Over

Você pode saber mais como utilizer fontes externas em projetos Android através deste link.
Nesse link aqui você irá conseguir pegar os ícones que utilizamos durante todo o projeto, além de ver como outro desenvolvedor implementou o mesmo projeto.

Conclusão

Mesmo no canvas, sem nenhum framework especifico para games conseguimos criar um projeto bem bacana. Fica como desafio pra você através desse projeto criar outros jogos ainda mais interessantes.

Anúncios

,

  1. #1 by Everton on Setembro 23, 2014 - 4:02 pm

    Oi Leonardo, parabéns pela iniciativa, projeto muito esclarecedor; mas talvez como eu seja novo em programação android, provavelmente tenha-me passado algo despercebido, pois dá erro ao carregar o programa no aparelho android, talvez esteja faltando algum tipo de permissão? Se vc souber , ou alguém mais ficaria grato.

  2. #2 by Márcio Tamashiro on Setembro 29, 2013 - 10:00 pm

    Leonardo, primeiramente gostaria de parabenizar pelo conteúdo excelente do blog. Comecei a desenvolver para Android recentemente e ainda não havia encontrado um material prático e direto como o que vem disponibilizando no seu blog. Testei o código deste post sobre game canvas no Android, mas não consegui baixar os ícones. O endereço em que se baseou encontra-se indisponível. Há como disponibilizar os arquivos das figuras? Grato pela atenção. Espero que continue com o blog e seu excelente conteúdo.

    • #3 by Leonardo Casasanta on Setembro 30, 2013 - 3:53 am

      Ola Márcio,

      Obrigado pelas palavras. Fico muito feliz com o seu feedback.

      Veja se os links abaixo atendem a suas necessidades:

      Abraço.

Deixe uma Resposta

Preencha os seus detalhes abaixo ou clique num ícone para iniciar sessão:

Logótipo da WordPress.com

Está a comentar usando a sua conta WordPress.com Terminar Sessão / Alterar )

Imagem do Twitter

Está a comentar usando a sua conta Twitter Terminar Sessão / Alterar )

Facebook photo

Está a comentar usando a sua conta Facebook Terminar Sessão / Alterar )

Google+ photo

Está a comentar usando a sua conta Google+ Terminar Sessão / Alterar )

Connecting to %s

%d bloggers like this: