As vezes quero descansar um pouco de todos desses frameworks, engines e bibliotecas prontas. Para falar verdade, não é descansar mas pegar mais pesado – sentar e escrever um jogo utilizando apenas as ferramentas comuns. Qual exatamente? Escolher é fácil porque no coração de qualquer desenvolvedor vivem os três jogos, criados no século passado: Tetris, Digger e Xonix.
Falando disso, este artigo vale a pena de ler mesmo no caso se você não planeja dar mais uma nova vida nos jogos de nossa infância mais demonstrar funcionamento com paletas, fontes, objetos gráficos, arquitetura de pardrão dos aplicativos e hierarquia.
Ferramentas e lógica
Para trabalhar vamos utilizar o Android Studio recomendado por Android. Tudo que a gente precisa é fácil de encontrar pelo link.
Criamos um projeto novo e vamos adicionar todas classes manualmente. Todo jogo vai contar cinco classes, quatros de que serão descritas em manifest e quinta vai ser o parente das todas. Então, todas as classes serão herdados de uma, assim, temos menos código. Iniciar aplicativo vai o SplashActivity que vai causar MainMenuActivity depois de um tempo. Em seguida, pode acessar configurações de SettingsActivity ou executar a parte principal do jogo localizada em GameActivity. Todas as atividades (exceto a última) implementarão sua interface padrão nas todas as formas chamando a descrição dos elementos em arquivos de layout XML.
A área principal do jogo será desenhada no sucessor de classe SurfaceView. A renderização será realizada em paralelo ao fluxo principal de UI. Agora considere a classe que não foi incluída no manifest mas isso vai poupa para nós muito código e tempo.
A classe BaseActivity
Para garantir que a tela do aparelho não vai desligar enquanto o aplicativo estiver executado, tem que criar uma classe BaseActivity e herdar o aplicativo da atividade dela:
public class BaseActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // O título foi excluído requestWindowFeature(Window.FEATURE_NO_TITLE); // Removido a tela desligar e definir o modo de tela cheia getWindow().addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON | WindowManager.LayoutParams.FLAG_FULLSCREEN); // Defina a orientação do retrato setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); // Definir animação quando a atividade começar overridePendingTransition(android.R.anim.slide_in_left, android.R.anim.slide_out_right); } }
Essa classe fornece para nós uma atividade sem título com orientação vertical de tela, não encerrando devido a economia de energia e animação da aparência de cada nova atividade.
Tela de Boas-vindas
O Google não recomenda usar tela de apresentação em aplicativos. Mas não recomendar não é significa proibir. Eu realmente quero mostrar como realizar isso, vamos implementar apenas uma classe SplashActivity:
public class SplashActivity extends BaseActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_splash); new Handler().postDelayed(new Runnable() { @Override public void run() { startActivity(new Intent(SplashActivity.this, MainMenuActivity.class)); finish(); } }, 2000); } }
Aqui eu acabei de apresentar o nosso marcador setContentView(R.layout.activity_splash). Após de 2000ms vamos iniciar MainMenuActivity e quando precisa se desligar usando o método finish(). Tem que anotar que a tela de Boas-vindas é bem útil quando você quer preparar seu aplicativo para uso, por exemplo, copiar uma coisa para base de dados ou baixar rapidinho alguma coisa da Web.
Bom, agora o usuário tem que receber uma imagem bonita e não importa se seja simples. A maioria dos usuários gostam de expectativa, eles vão ter uma expressão que o aplicativo é uma coisa séria pois leva um tempinho antes de iniciar. Isso é utilizado por alguns desenvolvedores: eles apresentam uma barra de progresso (ProgressBar) antes de comesar mas não acontece nada útil.
Para criar alguma coisa bacana e bem original, podemos usar fontes TTF ou OTF. Podemos aplicar pelo dois jeitos diferentes:
- Dinamicamente. Quando aplicativo já está rodando, instalar uma fonte desejada pelo método setTypeface()1. Dinamicamente. Quando aplicativo já está rodando, instalar uma fonte desejada pelo método setTypeface()
- Estaticamente. Definir fonte como uma componente de padrão da tela no arquivo markup.
No nosso caso ambos dos métodos serem implementados. Copia arquivo de fonte Dots.ttf para diretoria de configuração. O layout das duas primeiras telas é implementado automaticamente. Para realizar isso tem que criar uma classe MyTextView como herdeiro de TextView e define estilo em arquivo attrs.xml
O método principal de classe init():
private void init(AttributeSet attrs) { if (attrs != null) { TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.MyTextView); String fontName = a.getString(R.styleable.MyTextView_fontName); if (fontName != null) { Typeface myTypeface = Typeface.createFromAsset(getContext().getAssets(), fontName); setTypeface(myTypeface); } a.recycle(); } }
Em activity_splash.xml vamos atribuir a nossa classe:
<com.rusdelphi.xonix.MyTextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:layout_centerVertical="true" android:text="@string/app_name" android:textSize="40sp" android:textStyle="bold" customfont:fontName="Dots.ttf" />
Nós aplicamos fonte dinamicamente enquanto desenhamos texto no campo de jogo.
No menu principal implementamos três botões: o jogo, configurações e outros aplicativos. Vamos começar com o último… Para dar ao usuário a oportunidade de aproveitar outros aplicativos do desenvolvedor é basta escrever uma linha (uma linha de pesquisa com o nome do desenvolvedor):
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("market://search?q=pub:Cool author")));
Ou seja, executamos o programa através de certas intenções.
O aplicativo deve procegir URI com linha. O sistema determina propriamente qual aplicativo vai procegir essa item pelo descrição em manifest, e se tem vários vai oferecer escolher.
Antes de publicar seu aplicativo no market: não esquece usa uma boa ferramenta para limpar seu projeto. Ela está uncluida ao Android Studio e chama-se Analyze – Inspect Code. Como o resultado, vamos ter uma lista de observações no projeto: de recursos não usados até agumentos desnecessários. Leva em conta essas observações, otimiza seu projeto!
Configurações (SettingsActivity) usam o mecanismo de padrão SharedPreferences. Todas as configurações são realizadas em uma classe de preferência separada. Quando atividade começa (onStart()) com configurações, carregamos as configurações necessárias para controles. Se mudamos as configurações elas serão salvas quando terminamos atividade (onStop()). Nas configurações indicamos a velocidade do jogo e o número de vidas no início do processo de jogo.
Atividade de jogo (GameActivity) é herdeiro de BaseActivity. A classe herdou todas as propriedades úteis descritas acima, somente para que podemos realizar o próprio jogo. Durante a criação desta atividade, em vez da indicação comum de um arquivo XML com elementos de layout, vamos apontar na tela a nossa própria classe DrawView, que é o sucessor do SurfaceView:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); self = this; ctx = this; mDrawView = new DrawView(this); setContentView(mDrawView); DisplayMetrics dimension = new DisplayMetrics(); getWindowManager().getDefaultDisplay().getMetrics(dimension); int width = dimension.widthPixels; int height = dimension.heightPixels; if (width < height) mSidePopup = (int) (width * 0.8); else mSidePopup = (int) (height * 0.8); }
Aqui nós conseguimos os parâmetros da tela (largura e altura) que usaremos ao criar uma janela pop-up (PopupWindow) com uma mensagem sobre o final do jogo.
Classe SurfaceView
O SurfaceView é um invólucro em torno da classe SurfaceHolder que por sua vez serve como um invólucro para a classe Surface usada para atualizar uma imagem de fundo. Uma característica da classe SurfaceView é que ela fornece uma área separada para desenhar, as ações de que devem ser retiradas para um segmento separado. Assim, o aplicativo não precisa esperar até que o sistema esteja pronto para desenhar toda a hierarquia dos elementos de visualização. O segmento auxiliar pode usar paleta (Canvas) do nosso SurfaceView para desenhar na velocidade desejada.
Toda a implementação é reduzida a dois pontos principais:
- Criar uma classe herdada do SurfaceView e implementar a interface SurfaceHolder.Callback1. Criar uma classe herdada do SurfaceView e implementar a interface SurfaceHolder.Callback
- Criar um segmento que gerencie a renderização
Classe Canvas
A classe Canvas fornece métodos para desenhar que exibem os primitivos gráficos na imagem de raster original. No mesmo tempo, primeiramente você tem que preparar um pincel (classe Paint) que permite que você especifica como os primitivos gráficos devem ser exibidos na imagem de raster (cor, traço, estilo, alisamento de fontes e etc). Além disso, você precisa especificar Bitmap – a superfície na qual você desenha. O Android suporta translucidez, preenchimentos gradientes, retângulos arredondados e anti-aliasing. Devido a restrições de recursos, a gráfica vetorial ainda não está suportada, em vez disso, desenho de raster tradicional é usado.
A classe Canvas pode ser chamada de um invólucro em torno de uma imagem de raster, que usaremos como uma paleta para nossos experimentos de arte. Isso fornece um conjunto de métodos da forma draw* para criar imagens de qualquer objetos.
Classe DrawView
Em classe DrawView vamos capturar os eventos de cliques na tela (onTouchEvent) e iniciar o trabalho DrawThread que implementa toda a lógica do jogo e trabalha com paletas. O monstro no jogo se move mas o jogador precisa ser controlado também. Por simples cálculos definimos quatro gestos:
@Override public boolean onTouchEvent(MotionEvent touchevent) { switch (touchevent.getAction()) { // Determine as coordenadas do primeiro toque case MotionEvent.ACTION_DOWN: { x1 = touchevent.getX(); y1 = touchevent.getY(); break; } case MotionEvent.ACTION_UP: { x2 = touchevent.getX(); y2 = touchevent.getY(); float dx = x2 - x1; float dy = y2 - y1; if (Math.abs(dx) > Math.abs(dy)) { if (dx > 0) DrawThread.playerDirection = "right"; if (dx < 0) DrawThread.playerDirection = "left"; } else { if (dy > 0) DrawThread.playerDirection = "down"; if (dy < 0) DrawThread.playerDirection = "up"; } break; } } return true; }
No método surfaceCreated() obtemos elementos para paleta e começamos desenhar, e no surfaceDestroyed() é pelo contrário, terminamos:
@Override public void surfaceCreated(SurfaceHolder holder) { Preference prefs = new Preference(getContext()); // Configurações recebidas int indent = Tools.dpToPx(5); // Recuo entre elementos int side = (getWidth() / 40) - indent; // O tamanho do quadrado int startY = (getHeight() - (side + indent) * 20) / 2; int startX = 5; int i, j; for (i = 0; i < 40; i++) for (j = 0; j < 20; j++) { int x1 = startX + side * i + indent * i; int y1 = startY + side * j + indent * j; int x2 = startX + side * i + side + indent * i; int y2 = startY + side * j + side + indent * j; if (i == 0 || i == 39 || j == 0 || j == 19) matrixField[i][j] = new QuadrateItem(x1, y1, x2, y2, Color.BLUE); else matrixField[i][j] = new QuadrateItem(x1, y1, x2, y2, Color.TRANSPARENT); } Activity activity = (Activity) getContext(); drawThread = new DrawThread(getHolder(), getResources(), matrixField,prefs.getData(Preference.GAME_SPEED),prefs.getData(Preference.NUMBER_OF_LIFES),activity); drawThread.setRunning(true); drawThread.start(); } @Override public void surfaceDestroyed(SurfaceHolder holder) { boolean retry = true; drawThread.setRunning(false); while (retry) { try { drawThread.join(); retry = false; } catch (InterruptedException e) { } } }
Nossa área de jogo consiste em um título em cima e uma matriz de quadrados 40 x 20 onde vai rolar toda drama de jogo. Todo desenho é feito na paleta, isto é classe Canvas. Em seguida temos isso:
canvas = surfaceHolder.lockCanvas(null);
Desenhamos:
canvas.drawColor(Color.BLACK);
Ou:
public void drawRect(Canvas canvas, QuadrateItem item) { Rect myRect = new Rect(); myRect.set(item.x1, item.y1, item.x2, item.y2); Paint itemPaint = new Paint(); itemPaint.setColor(item.color); itemPaint.setStyle(Paint.Style.FILL); canvas.drawRect(myRect, itemPaint); }
E devolvemos isso atualizado:
surfaceHolder.unlockCanvasAndPost(canvas);
A lógica do jogo, eu acho, que é desnecessário de explicar. Se número de vidas é menor do que um, vamos apresentar uma mensagem Game Over (final do jogo).
Conclusão
Hoje em dia existem muitas ferramentas para desenvolvimento de jogos para o Android, inclusive de editores on-line. O seu objetivo é transformar o código de JavaScript em aplicativo com componente de WebView, o quê é um navegador comum. Deve notar que o próprio JS no sistema de Android foi cortado que não permite de execução dele além do sandbox do navegador, e portanto não tem acesso às muitas das funções do próprio aparelho. Não estamos falando sobre erros do navegador que é atualizado apenas nas versões mais recentes do OS. Em geral, sobre tais ofícios em uma sociedade decente é melhor ficar em silêncio. Trabalhar com frameworks impõe erros nos erros do desenvolvedor na própria plataforma que, no final das contas, leva a uma série de problemas, muitas vezes até mesmo sem solução. Portanto, se você quiser desenvolver um bom jogo, você tem que ser mais familiar com plataforma e usar as ferramentas padrão (mais do que uma declaração controversa mas parece brutal).