Mostrando postagens com marcador Programação. Mostrar todas as postagens
Mostrando postagens com marcador Programação. Mostrar todas as postagens

sábado, 12 de abril de 2008

Alterando a Fonte

Eu sempre achei a fonte padrão da PAlib limitada por não conter acentuação. Hoje eu resolvi mudar essa situação. Passei a tarde toda e parte da noite preparando uma fonte baseada no padrão ISO-8859-1, que é a fonte latina padrão do Windows em português (BR). Procurei colocar todos os caracteres dessa fonte, mas no final eu deixei a última linha em branco, pois eram sinais tipo um C com acento, um A cou um u em cima, sinais que realmente eu nunca usei.

Como usar a fonte nova:
- Ter a fonte em gif, com a cor que quiser e o fundo em magenta. Eu fiz a letra branca pois fica mais fácil de usar os comandos de cores.
- Converter o gif para tiled background usando o PAgfx.
- Usar o comando PA_InitCustomText para definir a fonte customizada como a default do texto.
- Escrever normalmente.

O único comando para alterar a cor do texto que funciona é o PA_SetTextCol. Os outros comandos deixam as letras pretas.

Arquivo gif com a fonte:


Fonte do programa:
// Includes
#include <pa9.h> // Include for PA_Lib

#include "gfx/all_gfx.h"
#include "gfx/all_gfx.c"
// Function: main()
int main(int argc, char ** argv)
{
  PA_Init(); // Initializes PA_Lib
  PA_InitVBL(); // Initializes a standard VBL

  PA_InitText(0, 0);
  PA_InitCustomText(1,0,fonte3);
  PA_OutputSimpleText(1,2,2,"texto com acentuação");
  PA_OutputSimpleText(1,0,3,"áéíóúàãõâêôüç ÁÉÍÓÚÀÃÕÂÊÔÜÇ");

// Infinite loop to keep the program running
  while (1)
  {
    if (Pad.Newpress.Up) { PA_SetTextCol(1, 0, 0, 31); }
    if (Pad.Newpress.Down) { PA_SetTextCol(1, 0, 31, 0); }
    if (Pad.Newpress.Left) { PA_SetTextCol(1, 31, 0, 0); }
    if (Pad.Newpress.Right) { PA_SetTextCol(1, 31, 31, 31); }
    PA_WaitForVBL();
  }

  return 0;
} // End of main()


Alguns avisos:
- Eu realmente desisti do jogo de tanque. Eu não estava contente com o jogo e estava apenas me estressando. Eu gosto de programar jogos de raciocínio. Jogos de ação não são minha praia.
- Eu estou começando a projetar um jogo que vai misturar adventure point&click (tipo os da Lucas Arts) com aqueles adventures puro texto (tipo os da Infocom). Por isso que precisava de uma fonte acentuada.
- Quem tentar instalar a PAlib 080203 (beta) no DevKitPro vai ter problemas na compilação, é necessário imcluir uma variável de ambiente chamada papath apontando para onde está a PAlib (usando / ao invés de \). Como fazer isso:
1) Clique com o botão direito em "Meu Computador" e clique em propriedades.
2) Vá na aba "Avançado" e clique no botão "Variáveis de Ambiente".
3) Em "Variáveis do Sistema", clique no botão "Nova".
4) Em "Nome da Variável", escreva papath
5) Em "Valor da Variável", escreva c:/devkitpro/palib
6) Vá clicando em "OK" até fechar todas as janelas.
No passo 5, é importante colocar o caminho para onde está a palib. Se for diferente do que eu postei, tem que ser o caminho do seu HD até chegar na palib.

quarta-feira, 13 de fevereiro de 2008

Novidades na PAlib

Não testei nada, só li o que estava no site e estou passando aqui:
- Antigamente, os arquivos de som só podiam ser RAW (um Wave sem cabeçalho) e MOD (do Amiga). Agora, além desses formatos, tem bibliotecas para MP3, s3m, it e outros formatos similares ao MOD.
- Funciona no Windows Vista
- Biblioteca para comunicação direta entre dois DSs.
- EFSlib - para tornar os programas que acessam a FAT do flashcard compatíveis com a DLDI. A PAFS só funcionava em flashcards de slot2.
- A partir de agora, é necessário alterar o Makefile para compilar com a biblioteca ARM7 apropriada. Agora tem diversas bibliotecas conflitantes, como a do Wifi e a de comunicação direta entre dois DSs (só pode usar uma delas num programa). Mod e MP3, também.
- Novos exemplos, abrangendo as novas bibliotecas.
- E mais correções de bugs.

Problemas na instalação da PAlib


Eu andei testando a instalação da PAlib com o novo DevKitPro e não funcionou. Na hora de compilar o programa só dava erro

A solução eu descobri no fórum da PAlib. Não pode mais usar o instalador da PAlib. Tem que baixar a PAlib em ZIP e mandar extrair na pasta onde está o DevKitPro.

Detalhe importante: Não pode extrair a pasta libnds, senão sobrescreve a biblioteca do DevKitPro e dá o erro.

Eu estava olhando (agora) o site da PAlib e vi que saiu uma nova versão em 03/02/2008. Vou olhar o que tem de novidades e depois posto aqui. É até capaz de já ter corrigido o problema acima.

terça-feira, 29 de maio de 2007

Vetores

Vetores são como variáveis, mas podem conter diversos valores.

Digamos, por exemplo, que eu queira trabalhar com moedas. Cada tipo de moeda tem um valor (em centavos): moeda1=1, moeda2=5, moeda3=10, moeda4=25, moeda5=50 e moeda6=100. Usando as variáveis dessa forma, se eu quisesse usar um for para mostrar o valor de todas as moedas, eu não conseguiria.

O vetor vem ao meu socorro. Por exemplo:
s16 moeda[6]={1,5,10,25,50,100};
for (i=0; i<6; i++) {
  PA_OutputText(0,0,i,"moeda[%d]=%d",i,moeda[i]);
}

O código acima apresentará uma saída asim:
moeda[0]=1
moeda[1]=5
moeda[2]=10
moeda[3]=25
moeda[4]=50
moeda[5]=100

Nós fizemos um vetor de 6 posições (de 0 a 5) contendo os valores das moedas do Brasil.

Vamos aplicar isso ao nosso jogo. O cenário tem áreas de areia, de terra, de grama e de água. O tanque não deve andar sobre a água. Mas o tipo de solo também influencia na velocidade do tanque. Então faremos o tanque andar na metade da velocidade na areia (1 pixel a cada 2 frames), em velocidade normal na grama (1 pixel por frame) e em velocidade dobrada na terra (2 pixels por frame). Usando a lógica de bit shifting que já estudamos, teríamos uma tabela contendo velocidade=0 se o terreno for água, 128 se for areia, 256 se for grama e 512 se for terra.

Eu dividi o background em quadrados de 16x16 pixels e coloquei num vetor. Como a imagem tem 256x192 pixels, isso divindo por 16 cada valor me gerou um vetor de 192 posições (16x12=192). O vetor é representado de forma linear, mas a tela tem linhas e colunas. Então é necessária uma fórmula para acessarmos todas as posições. Essa fórmula é: linha*largura+coluna. Neste caso é linha*16+coluna. A posição de linha e coluna é calculada relativamente ao centro do tanque para facilitar a programação.

Como existe a possibilidade de ter áreas onde o tanque não se move, ele jamais deve entrar com o centro nessas áreas. Quando ele entrar em uma área de velocidade 0, a posição do tanque deve ser restaurada para posição antiga, anterior à entrada nessa área. Se o tanque entrasse numa área de velocidade 0, ele jamais sairia dela, pois a sua velocidade seria 0.

Outra coisa que corrigi é que quando o tanque atingia uma borda da tela e o controle estivesse sendo pressionado na diagonal, o tanque continuava se movendo em apenas um dos eixos ao invés de parar. Agora ele pára.

Vamos ao código-fonte. Analisem o fonte e encontrem o que foi falado acima. Se houver alguma dúvida, façam a pergunta nos comentários.


// Includes
#include <PA9.h> // Include for PA_Lib
#include "gfx/all_gfx.c"
#include "gfx/all_gfx.h"
// Variaveis Globais
s16 angulo_body=0;
s16 angulo_turret=0;
s32 x=111<<8;
s32 y=(79+192+48)<<8;
s16 angulo_tiro=0;
s32 x_tiro=-8<<8;
s32 y_tiro=-8<<8;
s16 vel_tiro=4;
s8 atirando=0;
s16 vel_terra[192]={0,128,128,256,256,256,256,256,256,256,256,256,256,128,128,0,
  0,128,128,256,256,512,512,512,512,512,512,256,256,128,128,0,
  0,128,128,256,256,512,512,512,512,512,512,256,256,128,128,0,
  0,128,128,256,256,512,512,512,512,512,512,256,256,128,128,0,
  0,128,128,256,256,512,512,512,512,512,512,256,256,128,128,0,
  0,128,128,256,256,256,256,256,256,256,256,256,256,128,128,0,
  0,128,128,256,256,256,256,256,256,256,256,256,256,128,128,0,
  0,128,128,256,256,256,256,256,256,256,256,256,256,128,128,0,
  0,128,128,256,256,256,256,256,256,256,256,256,256,128,128,0,
  0,128,128,128,128,128,128,128,128,128,128,128,128,128,128,0,
  0,128,128,128,128,128,128,128,128,128,128,128,128,128,128,0,
  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};

void giraturret() {
  if (Stylus.Held) {
    angulo_turret=PA_GetAngle((x>>8)+16,(y>>8)+16-240,Stylus.X,Stylus.Y);
    PA_DualSetRotsetNoZoom(0,angulo_turret);
  }
}

void movetanque() {
  s16 centro_x=((x>>8)+15)>>4;
  s16 centro_y=((y>>8)-240+15)>>4;
  s16 tipoterra=0;
  s16 velocidade=0;
  s32 xvelho=x;
  s32 yvelho=y;
  if (centro_x<0||centro_x>15||centro_y<0||centro_y>11) {
    tipoterra=0;
    velocidade=0;
  } else {
    tipoterra=centro_y*16+centro_x;
    velocidade=vel_terra[tipoterra];
  }
  if ((Pad.Held.Left && Pad.Held.A) || (Pad.Held.Right && Pad.Held.Y) || (Pad.Held.Up && Pad.Held.B) || (Pad.Held.Down && Pad.Held.X) || (Pad.Held.X && Pad.Held.B) || (Pad.Held.Y && Pad.Held.A)) {
   //o usuário está sacaneando, não executa nada
  } else if (!(Pad.Held.Up || Pad.Held.Down || Pad.Held.Left || Pad.Held.Right || Pad.Held.A || Pad.Held.B || Pad.Held.X || Pad.Held.Y)) {
   //nenhuma direção foi pressionada
  } else {
    if ((Pad.Held.Left || Pad.Held.Y || Pad.Held.Right || Pad.Held.A) && !(Pad.Held.Up || Pad.Held.X || Pad.Held.Down || Pad.Held.B)) {
      angulo_body=256*(Pad.Held.Left || Pad.Held.Y);
      x+=velocidade*(Pad.Held.Right || Pad.Held.A)-velocidade*(Pad.Held.Left || Pad.Held.Y);
    } else if (!(Pad.Held.Left || Pad.Held.Y || Pad.Held.Right || Pad.Held.A) && (Pad.Held.Up || Pad.Held.X || Pad.Held.Down || Pad.Held.B)) {
      angulo_body=128*(Pad.Held.Up || Pad.Held.X)+384*(Pad.Held.Down || Pad.Held.B);
      y+=velocidade*(Pad.Held.Down || Pad.Held.B)-velocidade*(Pad.Held.Up || Pad.Held.X);
    } else {
      angulo_body=256*(Pad.Held.Left || Pad.Held.Y);
      if (angulo_body) {
        angulo_body=192*(Pad.Held.Up || Pad.Held.X)+320*(Pad.Held.Down || Pad.Held.B);
      } else {
        angulo_body=64*(Pad.Held.Up || Pad.Held.X)+448*(Pad.Held.Down || Pad.Held.B);
      }
      x+=velocidade*(Pad.Held.Right || Pad.Held.A)-velocidade*(Pad.Held.Left || Pad.Held.Y);
      y+=velocidade*(Pad.Held.Down || Pad.Held.B)-velocidade*(Pad.Held.Up || Pad.Held.X);
    }
    centro_x=((x>>8)+15)>>4;
    centro_y=((y>>8)-240+15)>>4;
    if (centro_x<0||centro_x>15||centro_y<0||centro_y>11) {
      tipoterra=0;
      velocidade=0;
    } else {
      tipoterra=centro_y*16+centro_x;
      velocidade=vel_terra[tipoterra];
    }
    if (velocidade==0 || x<0 || x>57088 || y<61440 || y>102144) {
      x=xvelho;
      y=yvelho;
    }
//    if (x<0) { x=0; }
//    if (x>57088) { x=57088; }
//    if (y<61440) { y=61440; }
//    if (y>102144) { y=102144; }
    PA_DualSetRotsetNoZoom(1,angulo_body);
    PA_DualSetSpriteXY(0,x>>8,y>>8);
    PA_DualSetSpriteXY(1,x>>8,y>>8);
  }
}

void atira() {
  if (atirando) {
    x_tiro+=PA_Cos(angulo_tiro)*vel_tiro;
    y_tiro-=PA_Sin(angulo_tiro)*vel_tiro;
    PA_DualSetSpriteXY(2,x_tiro>>8,y_tiro>>8);
    if (x_tiro<-8<<8 || x_tiro>260<<8 || y_tiro<-8<<8 || y_tiro>440<<8) {
      atirando=0;
    }
  } else {
    if (Pad.Newpress.L || Pad.Newpress.R) {
      atirando=1;
      angulo_tiro=angulo_turret;
      x_tiro=((x>>8)+12)<<8;
      y_tiro=((y>>8)+12)<<8;
      x_tiro+=PA_Cos(angulo_tiro)*24;
      y_tiro-=PA_Sin(angulo_tiro)*24;
      PA_DualSetRotsetNoZoom(2,angulo_tiro);
      PA_DualSetSpriteXY(2,x_tiro>>8,y_tiro>>8);
    }
  }
}

// Function: main()
int main(int argc, char ** argv)
{
  
  PA_Init(); // Initializes PA_Lib
  PA_InitVBL(); // Initializes a standard VBL
  PA_DualLoadSpritePal( 0, (void*)tank_turret_Pal);
  PA_DualCreateSprite( 0, (void*)tank_turret_Sprite, OBJ_SIZE_32X32, 1, 0, x>>8, y>>8);
  PA_DualLoadSpritePal( 1, (void*)tank_body_Pal);
  PA_DualCreateSprite( 1, (void*)tank_body_Sprite, OBJ_SIZE_32X32, 1, 0, x>>8, y>>8);
  PA_DualLoadSpritePal( 2, (void*)tiro_Pal);
  PA_DualCreateSprite( 2, (void*)tiro_Sprite, OBJ_SIZE_8X8, 1, 0, -8, -8);
  PA_DualSetSpriteRotEnable( 0, 0);
  PA_DualSetSpriteRotEnable( 1, 1);
  PA_DualSetSpriteRotEnable( 2, 2);
  PA_EasyBgLoad(0,3,set_bg_1);
  PA_EasyBgLoad(1,3,set_bg_2);
  PA_InitText(1,0);
  PA_SetTextCol(1,31,31,31);
  // Infinite loop to keep the program running
  while (1)
  {
    giraturret();
    movetanque();
    atira();
    PA_WaitForVBL();
  }
  return 0;
} // End of main()

domingo, 6 de maio de 2007

Ocupando as Duas Telas

Tudo até agora foi programado usando apenas a tela de baixo, a tela 0. Mas este jogo deve ocupar as duas telas do DS. O nosso tanque tem que ficar só na tela de baixo, mas os tiros devem alcançar a tela de cima. Uma maneira de resolver isso seria bastante complexa, colocando os mesmos desenhos dos sprites de baixo nos sprites de cima e ficar controlando para ocultar um sprite em baixo quando ele passa a aparecer em cima.
Outra opção seria usar a própria PAlib para fazer esse controle. É o que nós faremos. Todos os comandos de sprites possuem um irmão "Dual", que não precisa especificar qual a tela ele será colocado, o valor de y é que definirá isso. A altura padrão de tela passa a ser 432 pixels (192 pixels da tela de baixo, mais 192 pixels da tela de cima, mais 48 pixels que não mostram imagem porque são o espaço entre as duas telas). O espaço entre as duas tela pode ser alterado pela função PA_SetScreenSpace mas nós continuaremos a usar os 48 pixels pois isso dá uma continuidade bem legal ao tiro.
Ah, sim. Faltou falar que os comandos para trabalhar com as duas telas como se fossem uma, são os mesmos comandos de gerenciamento de sprites, somente com a palavra "Dual" na frente (logo após o "PA_") e sem o parâmetro do número da tela.
Também iremos colocar os fundos das telas. O fundo set_bg_1.gif vai na tela de baixo e o set_bg_2.gif vai na tela de cima.
No programa, as alterações não são muito radicais:
  • trocar os comandos de sprites para os dual
  • tratar o tanque na tela de baixo, ou seja, y+240
  • permitir que o tiro ande por todos os 432 pixels
  • não desenhar mais o fundo de 16 bits amarelo sujo
  • colocar os dois tiled backgrounds

Fonte do programa:

// Includes
#include <PA9.h> // Include for PA_Lib
#include "gfx/all_gfx.c"
#include "gfx/all_gfx.h"

// Variaveis Globais
s16 angulo_body=0;
s16 angulo_turret=0;
s16 velocidade=1;
s16 x=111;
s16 y=79+192+48;
s16 angulo_tiro=0;
s32 x_tiro=-8<<8;
s32 y_tiro=-8<<8;
s16 vel_tiro=4;
s8 atirando=0;

void giraturret() {
  if (Stylus.Held) {
    angulo_turret=PA_GetAngle(x+16,y+16-240,Stylus.X,Stylus.Y);
    PA_DualSetRotsetNoZoom(0,angulo_turret);
  }
}

void movetanque() {
  if ((Pad.Held.Left && Pad.Held.A) || (Pad.Held.Right && Pad.Held.Y) || (Pad.Held.Up && Pad.Held.B) || (Pad.Held.Down && Pad.Held.X) || (Pad.Held.X && Pad.Held.B) || (Pad.Held.Y && Pad.Held.A)) {
   //o usuário está sacaneando, não executa nada
  } else if (!(Pad.Held.Up || Pad.Held.Down || Pad.Held.Left || Pad.Held.Right || Pad.Held.A || Pad.Held.B || Pad.Held.X || Pad.Held.Y)) {
   //nenhuma direção foi pressionada
  } else {
    if ((Pad.Held.Left || Pad.Held.Y || Pad.Held.Right || Pad.Held.A) && !(Pad.Held.Up || Pad.Held.X || Pad.Held.Down || Pad.Held.B)) {
      angulo_body=256*(Pad.Held.Left || Pad.Held.Y);
      x+=velocidade*(Pad.Held.Right || Pad.Held.A)-velocidade*(Pad.Held.Left || Pad.Held.Y);
    } else if (!(Pad.Held.Left || Pad.Held.Y || Pad.Held.Right || Pad.Held.A) && (Pad.Held.Up || Pad.Held.X || Pad.Held.Down || Pad.Held.B)) {
      angulo_body=128*(Pad.Held.Up || Pad.Held.X)+384*(Pad.Held.Down || Pad.Held.B);
      y+=velocidade*(Pad.Held.Down || Pad.Held.B)-velocidade*(Pad.Held.Up || Pad.Held.X);
    } else {
      angulo_body=256*(Pad.Held.Left || Pad.Held.Y);
      x+=velocidade*(Pad.Held.Right || Pad.Held.A)-velocidade*(Pad.Held.Left || Pad.Held.Y);
      if (angulo_body) {
        angulo_body=192*(Pad.Held.Up || Pad.Held.X)+320*(Pad.Held.Down || Pad.Held.B);
      } else {
        angulo_body=64*(Pad.Held.Up || Pad.Held.X)+448*(Pad.Held.Down || Pad.Held.B);
      }
      y+=velocidade*(Pad.Held.Down || Pad.Held.B)-velocidade*(Pad.Held.Up || Pad.Held.X);
    }
    if (x<0) { x=0; }
    if (x>223) { x=223; }
    if (y<240) { y=240; }
    if (y>399) { y=399; }
    PA_DualSetRotsetNoZoom(1,angulo_body);
    PA_DualSetSpriteXY(0,x,y);
    PA_DualSetSpriteXY(1,x,y);
  }
}

void atira() {
  if (atirando) {
    x_tiro+=PA_Cos(angulo_tiro)*vel_tiro;
    y_tiro-=PA_Sin(angulo_tiro)*vel_tiro;
    PA_DualSetSpriteXY(2,x_tiro>>8,y_tiro>>8);
    if (x_tiro<-8<<8 || x_tiro>260<<8 || y_tiro<-8<<8 || y_tiro>440<<8) {
      atirando=0;
    }
  } else {
    if (Pad.Newpress.L || Pad.Newpress.R) {
      atirando=1;
      angulo_tiro=angulo_turret;
      x_tiro=(x+12)<<8;
      y_tiro=(y+12)<<8;
      x_tiro+=PA_Cos(angulo_tiro)*24;
      y_tiro-=PA_Sin(angulo_tiro)*24;
      PA_DualSetRotsetNoZoom(2,angulo_tiro);
      PA_DualSetSpriteXY(2,x_tiro>>8,y_tiro>>8);
    }
  }
}

// Function: main()
int main(int argc, char ** argv)
{
  PA_Init(); // Initializes PA_Lib
  PA_InitVBL(); // Initializes a standard VBL
  PA_DualLoadSpritePal(0, (void*)tank_turret_Pal);
  PA_DualCreateSprite(0, (void*)tank_turret_Sprite, OBJ_SIZE_32X32, 1, 0, x, y);
  PA_DualLoadSpritePal(1, (void*)tank_body_Pal);
  PA_DualCreateSprite(1, (void*)tank_body_Sprite, OBJ_SIZE_32X32, 1, 0, x, y);
  PA_DualLoadSpritePal(2, (void*)tiro_Pal);
  PA_DualCreateSprite(2, (void*)tiro_Sprite, OBJ_SIZE_8X8, 1, 0, -8, -8);
  PA_DualSetSpriteRotEnable(0, 0);
  PA_DualSetSpriteRotEnable(1, 1);
  PA_DualSetSpriteRotEnable(2, 2);
  PA_EasyBgLoad(0,3,set_bg_1);
  PA_EasyBgLoad(1,3,set_bg_2);
  // Infinite loop to keep the program running
  while (1)
  {
    giraturret();
    movetanque();
    atira();
    PA_WaitForVBL();
  }
  return 0;
} // End of main()

Imagens para fazer os fundos:

quarta-feira, 2 de maio de 2007

Variáveis Globais (Continuando a Atirar)


Variáveis globais são aquelas que valem para todo o programa e não apenas para uma função. Como o C trabalha? Variáveis declaradas dentro de uma função são apenas daquela função. Variáveis declaradas fora de funções valem para todo o programa. Não é interessante usar só variáveis globais? Não. Variáveis globais consomem mais memória que variáveis locais, pois continuam ocupando o seu espaço na memória depois que a função termina de executar. Além disso, se eu mudar o valor de uma variável em uma função e esta variável estiver sendo usada em outra função, o valor na outra função também será alterado. Olhem o exemplo:

s16 i=0;
void func2() {
  i++;
}
void func1 () {
  for (i=0; i<40; i++) {
    func2();
    PA_OutputText(0,i>>1,'i=%d',i);
  }
}

No exemplo acima, vai imprimir os números de 0 a 39 pulando de dois em dois, apesar do for em func1 mandar i++. Isso complica muito a interpretação e pode mascarar erros. Então vamos usar variáveis globais para aquilo que envolve todo o programa, como a posição do tanque, o ângulo do canhão, a posição do tiro, etc.

Para organizar melhor a função main, vamos usar funções para as diversas ações do jogo. Desta forma a função main contém o esqueleto principal, e o detalhamento de cada ação fica em uma função diferente. Olhem o fonte do programa como ficou:

// Includes
#include <PA9.h> // Include for PA_Lib
#include "gfx/all_gfx.c"
#include "gfx/all_gfx.h"

// Variaveis Globais
s16 angulo_body=0;
s16 angulo_turret=0;
s16 velocidade=1;
s16 x=111;
s16 y=79;
s16 angulo_tiro=0;
s32 x_tiro=-8<<8;
s32 y_tiro=-8<<8;
s16 vel_tiro=4;
s8 atirando=0;

void giraturret() {
  if (Stylus.Held) {
    angulo_turret=PA_GetAngle(x+16,y+16,Stylus.X,Stylus.Y);
    PA_SetRotsetNoZoom(0,0,angulo_turret);
  }
}

void movetanque() {
  if ((Pad.Held.Left && Pad.Held.A) || (Pad.Held.Right && Pad.Held.Y) || (Pad.Held.Up && Pad.Held.B) || (Pad.Held.Down && Pad.Held.X) || (Pad.Held.X && Pad.Held.B) || (Pad.Held.Y && Pad.Held.A)) {
   //o usuário está sacaneando, não executa nada
  } else if (!(Pad.Held.Up || Pad.Held.Down || Pad.Held.Left || Pad.Held.Right || Pad.Held.A || Pad.Held.B || Pad.Held.X || Pad.Held.Y)) {
   //nenhuma direção foi pressionada
  } else {
    if ((Pad.Held.Left || Pad.Held.Y || Pad.Held.Right || Pad.Held.A) && !(Pad.Held.Up || Pad.Held.X || Pad.Held.Down || Pad.Held.B)) {
      angulo_body=256*(Pad.Held.Left || Pad.Held.Y);
      x+=velocidade*(Pad.Held.Right || Pad.Held.A)-velocidade*(Pad.Held.Left || Pad.Held.Y);
    } else if (!(Pad.Held.Left || Pad.Held.Y || Pad.Held.Right || Pad.Held.A) && (Pad.Held.Up || Pad.Held.X || Pad.Held.Down || Pad.Held.B)) {
      angulo_body=128*(Pad.Held.Up || Pad.Held.X)+384*(Pad.Held.Down || Pad.Held.B);
      y+=velocidade*(Pad.Held.Down || Pad.Held.B)-velocidade*(Pad.Held.Up || Pad.Held.X);
    } else {
      angulo_body=256*(Pad.Held.Left || Pad.Held.Y);
      x+=velocidade*(Pad.Held.Right || Pad.Held.A)-velocidade*(Pad.Held.Left || Pad.Held.Y);
      if (angulo_body) {
        angulo_body=192*(Pad.Held.Up || Pad.Held.X)+320*(Pad.Held.Down || Pad.Held.B);
      } else {
        angulo_body=64*(Pad.Held.Up || Pad.Held.X)+448*(Pad.Held.Down || Pad.Held.B);
      }
      y+=velocidade*(Pad.Held.Down || Pad.Held.B)-velocidade*(Pad.Held.Up || Pad.Held.X);
    }
    if (x<0) { x=0; }
    if (x>223) { x=223; }
    if (y<0) { y=0; }
    if (y>159) { y=159; }
    PA_SetRotsetNoZoom(0,1,angulo_body);
    PA_SetSpriteXY(0,0,x,y);
    PA_SetSpriteXY(0,1,x,y);
  }
}

void atira() {
  if (atirando) {
    x_tiro+=PA_Cos(angulo_tiro)*vel_tiro;
    y_tiro-=PA_Sin(angulo_tiro)*vel_tiro;
    PA_SetSpriteXY(0,2,x_tiro>>8,y_tiro>>8);
    if (x_tiro<-8<<8 || x_tiro>260<<8 || y_tiro<-8<<8 || y_tiro>200<<8) {
      atirando=0;
    }
  } else {
    if (Pad.Newpress.L || Pad.Newpress.R) {
      atirando=1;
      angulo_tiro=angulo_turret;
      x_tiro=(x+12)<<8;
      y_tiro=(y+12)<<8;
      x_tiro+=PA_Cos(angulo_tiro)*24;
      y_tiro-=PA_Sin(angulo_tiro)*24;
      PA_SetRotsetNoZoom(0,2,angulo_tiro);
      PA_SetSpriteXY(0,2,x_tiro>>8,y_tiro>>8);
    }
  }
}

// Function: main()
int main(int argc, char ** argv)
{
  
  PA_Init(); // Initializes PA_Lib
  PA_InitVBL(); // Initializes a standard VBL
  PA_Init16bitBg(0,0);
  PA_Draw16bitRect(0,0,0,255,191,32768+10*1024+31*32+25);
  PA_LoadSpritePal(0, 0, (void*)tank_turret_Pal);
  PA_CreateSprite(0, 0, (void*)tank_turret_Sprite, OBJ_SIZE_32X32, 1, 0, x, y);
  PA_LoadSpritePal(0, 1, (void*)tank_body_Pal);
  PA_CreateSprite(0, 1, (void*)tank_body_Sprite, OBJ_SIZE_32X32, 1, 0, x, y);
  PA_LoadSpritePal(0, 2, (void*)tiro_Pal);
  PA_CreateSprite(0, 2, (void*)tiro_Sprite, OBJ_SIZE_8X8, 1, 0, -8, -8);
  PA_SetSpriteRotEnable(0, 0, 0);
  PA_SetSpriteRotEnable(0, 1, 1);
  PA_SetSpriteRotEnable(0, 2, 2);
  
  // Infinite loop to keep the program running
  while (1)
  {
    giraturret();
    movetanque();
    atira();
    PA_WaitForVBL();
  }
  
  return 0;
} // End of main()


O que o programa faz:
Bom, quase todo o programa já foi explicado nos últimos artigos. Somente a rotina de tiro foi alterada, para o tiro sair do canhão ao invés do centro da tela, e permitir que o canhão se mova enquanto o tiro também se move. A melhor maneira de fazer isso é substituir o loop dentro da rotina de tiro (que iria paralizar o tanque enquanto o tiro se movesse) por uma única alteração de posição. O loop passa a ser o principal do programa, o loop infinto da função main.
Na função atira, o programa verifica se foi disparado um tiro. Se existe algum tiro disparado, atualiza a posição dele. Se passar do limite da tela, encerra o disparo, permitindo que um novo disparo seja feito. Se o flag de tiro não está ativo (flag é uma variável que representa se algo aconteceu ou não), o programa verifica se foi pressionado o botão L ou o R. Se foi, inicia o tiro a 24 pixels de raio do centro do canhão aplicados seno e cosseno do ângulo do canhão. Isso vai assegurar que nenhum tiro saia de cima do tanque ou do meio do canhão. Outra coisa é que o programa fixa o ângulo do tiro igual ao do canhão no momento do disparo. Se o canhão trocar de ângulo depois de disparar, ele não influenciará o tiro.
A função main pode ser reduzida mais ainda, criando uma função "inicia", onde são colocadas todos aqueles comandos anteriores ao loop infinito.

quarta-feira, 25 de abril de 2007

Atirando


Antes de incluir os tiros no jogo do tanque, vamos testar como atirar naquele programa de teste dos ângulos. Então iremos atirar em uma tela preta, a partir do centro da tela, conforme a stylus é usada.

Para processar o movimento, necessitamos usar seno e cosseno. Seno é a projeção do ponto da circunferência em determinado ângulo sobre o eixo Y. Cosseno é a projeção desse mesmo ponto sobre o eixo X. Seno e Cosseno são expressos em valores entre -1 e 1, o que é um problema no DS porque os processadores dele não processam ponto flutuante, tornando muito lento o uso de ponto flutuante. Mas é possível processar ponto fixo, ou ponto não-flutuante. É só fazer "bit shifting", que é o processo de determinar que uma quantidade x de bits vai ser considerada à direita da vírgula (aqui não podemos falar em ponto decimal pois estaremos trabalhando em binário). Os sinais para bit shifting são >> e <<. Por exemplo: 256>>8 retorna 1 em inteiro pois a gente moveu 8 bits para depois da vírgula. 256 em binário é 100000000, cortando os últimos 8 bits, fica 1. 256>>4 fica 16, pois eu estou cortando os últimos quatro bits, sobrando 10000, que é a representação binária de 16.

A PAlib tem funções de Seno e Cosseno para aqueles ângulos de 0 a 511, e o resultado é apresentado com bit shipfting para mais de 8 bits, ou seja, vai retornar de -256 a 256. Aí X e Y também têm que ser usados com bit shifting << para o cálculo e bit shifting >> para a apresentação. Pode-se dizer que <<8 é o mesmo que multiplicar por 256 e >>8 é o mesmo que dividir por 256, mas com um custo computacional muito menor.

Vamos ao programa, então:
// Includes
#include <PA9.h> // Include for PA_Lib
#include "gfx/all_gfx.c"
#include "gfx/all_gfx.h"

void atira(angulo) {
  s32 x=127<<8;
  s32 y=95<<8;
  while ((x>-8<<8&&x<262<<8)&&(y>-8<<8&&y<200<<8)) {
    x+=PA_Cos(angulo);
    y-=PA_Sin(angulo);
    PA_SetSpriteXY(0,0,x>>8,y>>8);
    PA_OutputText(1,0,5,"x=%d, x>>8=%d ",x,x>>8);
    PA_OutputText(1,0,6,"y=%d, y>>8=%d ",y,y>>8);
    PA_WaitForVBL();
  }
}

// Function: main()
int main(int argc, char ** argv)
{
  s16 angulo=0;
  PA_Init(); // Initializes PA_Lib
  PA_InitVBL(); // Initializes a standard VBL
  PA_InitText(1,0);
  PA_SetTextCol(1,31,31,31);
  PA_LoadSpritePal(0, 0, (void*)tiro_Pal);
  PA_CreateSprite(0, 0, (void*)tiro_Sprite, OBJ_SIZE_8X8, 1, 0, -8, -8);
  PA_SetSpriteRotEnable(0, 0, 0);
  // Infinite loop to keep the program running
  while (1)
  {
    if (Stylus.Held) {
      angulo=PA_GetAngle(127,95,Stylus.X,Stylus.Y);
      PA_SetRotsetNoZoom(0,0,angulo);
      PA_OutputText(1,0,4,"Angulo a partir do centro: %d ",angulo);
      atira(angulo);
    }
    PA_WaitForVBL();
  }
  return 0;
} // End of main()


Como o programa funciona:
  • primeiro é criada uma função que vai movimentar o tiro. Ela funciona da seguinte forma:
    • recebe o parâmetro angulo.
    • posiciona x e y no centro da tela.
    • enquanto x ou y não passar para fora da tela faz o seguinte:
      • soma o cosseno do angulo a x.
      • diminui o seno do angulo de y. O eixo Y da circunferência cresce para cima, mas o eixo Y da tela do DS cresce para baixo, por isso diminuir o seno ao invés de aumentar.
      • mostra o sprite do tiro na posição x,y (cortando 8 bits).
      • escreve os valor de x e y e os mesmos valores cortando os 8 bits.
      • aguarda o próximo frame
  • depois o procedimento é igual ao nosso penúltimo programa, mas acrescentando o sprite do tiro, que é rotacionado conforme a stylus é usada. Esta parte não precisa comentar por já ter sido comentado nos dois últimos programas. A exceção é a chamada da função, que é a linha:
    atira(angulo);
    ou seja, chama a função atira passando o valor da variável angulo como parâmetro. Lembrem que na definição da função existe a leitura do parâmetro angulo. Neste caso foi usado angulo para a variável e angulo para o parâmetro da função, mas isso não é obrigatório nem necessário porque aqui estamos passando o valor, não interessando o nome da variável.
Imagem do tiro:

segunda-feira, 16 de abril de 2007

Movendo o Tanque


O programa usa os dois sprites abaixo e rotaciona conforme são pressionados os direcionais ou a stylus. Como o programa poderá ser usado por destros e canhotos, eu programei os botões A, B, X e Y para agirem também como se fossem um direcional.




O que o programa faz:
  • Inicia as variáveis.
  • Inicia a PA-Lib, criando um fundo liso com uma cor "amarelo sujo esverdeado"
  • Inicializa os sprites e as rotações
  • Começa o loop infinito
  • Se for usada a stylus, verifica o ângulo entre a stylus e o centro do tanque. Rotaciona o sprite do canhão.
  • Se for feita sacanagem (tipo pressionar esquerda e A), não processa movimento.
  • Se não for pressionado nenhum botão de movimento, também não processa movimento.
  • Se forem pressionados somente botões de movimento horizontal, calcula o movimento horizontal e rotaciona o sprite do tanque.
  • Se forem pressionados somente botões de movimento vertical, processa o movimento vertical e rotaciona o sprite do tanque.
  • Senão (se forem pressionados botões verticais e horizontais), processa os movimentos horizontal e vertical e rotaciona o sprite do tanque.
  • Não deixa o tanque sair da tela.
  • Posiciona os sprites na nova posição calculada.
  • faz os encerramentos de praxe, aguardando o ciclo de frame e voltando ao início do loop infinito.
Código Fonte (desculpem mas não consegui fazer mais otimizado que isso):

// Includes
#include <PA9.h> // Include for PA_Lib
#include "gfx/all_gfx.c"
#include "gfx/all_gfx.h"

// Function: main()
int main(int argc, char ** argv)
{
  s16 angulo_body=0;
  s16 angulo_turret=0;
  s16 velocidade=1;
  s16 x=111;
  s16 y=79;

  PA_Init(); // Initializes PA_Lib
  PA_InitVBL(); // Initializes a standard VBL
  PA_Init16bitBg(0,0);
  PA_Draw16bitRect(0,0,0,255,191,32768+10*1024+31*32+25);
  PA_LoadSpritePal(0, 0, (void*)tank_turret_Pal);
  PA_CreateSprite(0, 0, (void*)tank_turret_Sprite, OBJ_SIZE_32X32, 1, 0, x, y);
  PA_LoadSpritePal(0, 1, (void*)tank_body_Pal);
  PA_CreateSprite(0, 1, (void*)tank_body_Sprite, OBJ_SIZE_32X32, 1, 0, x, y);
  PA_SetSpriteRotEnable(0, 0, 0);
  PA_SetSpriteRotEnable(0, 1, 1);
  // Infinite loop to keep the program running
  while (1)
  {
    if (Stylus.Held) {
      angulo_turret=PA_GetAngle(x+16,y+16,Stylus.X,Stylus.Y);
      PA_SetRotsetNoZoom(0,0,angulo_turret);
    }
    if ((Pad.Held.Left&&Pad.Held.A) || (Pad.Held.Right&&Pad.Held.Y) || (Pad.Held.Up&&Pad.Held.B) || (Pad.Held.Down&&Pad.Held.X) || (Pad.Held.X&&Pad.Held.B) || (Pad.Held.Y&&Pad.Held.A)) {
      //o usuário está sacaneando, não executa nada
    } else if (!(Pad.Held.Up || Pad.Held.Down || Pad.Held.Left || Pad.Held.Right || Pad.Held.A || Pad.Held.B || Pad.Held.X || Pad.Held.Y)) {
      //nenhuma direção foi pressionada
    } else {
      if ((Pad.Held.Left || Pad.Held.Y || Pad.Held.Right || Pad.Held.A) && !(Pad.Held.Up || Pad.Held.X || Pad.Held.Down || Pad.Held.B)) {
        angulo_body=256*(Pad.Held.Left || Pad.Held.Y);
        x+=velocidade*(Pad.Held.Right || Pad.Held.A) -velocidade*(Pad.Held.Left || Pad.Held.Y);
      } else if (!(Pad.Held.Left || Pad.Held.Y || Pad.Held.Right || Pad.Held.A) && (Pad.Held.Up || Pad.Held.X || Pad.Held.Down || Pad.Held.B)) {
        angulo_body=128*(Pad.Held.Up || Pad.Held.X) +384*(Pad.Held.Down || Pad.Held.B);
        y+=velocidade*(Pad.Held.Down || Pad.Held.B) -velocidade*(Pad.Held.Up || Pad.Held.X);
      } else {
        angulo_body=256*(Pad.Held.Left || Pad.Held.Y);
        x+=velocidade*(Pad.Held.Right || Pad.Held.A) -velocidade*(Pad.Held.Left || Pad.Held.Y);
        if (angulo_body) {
          angulo_body=192*(Pad.Held.Up || Pad.Held.X) +320*(Pad.Held.Down || Pad.Held.B);
        } else {
          angulo_body=64*(Pad.Held.Up || Pad.Held.X) +448*(Pad.Held.Down || Pad.Held.B);
        }
        y+=velocidade*(Pad.Held.Down || Pad.Held.B) -velocidade*(Pad.Held.Up || Pad.Held.X);
      }
      if (x<0) { x=0; }
      if (x>223) { x=223; }
      if (y<0) { y=0; }
      if (y>159) { y=159; }
      PA_SetRotsetNoZoom(0,1,angulo_body);
      PA_SetSpriteXY(0,0,x,y);
      PA_SetSpriteXY(0,1,x,y);
    }
    PA_WaitForVBL();
  }
  return 0;
} // End of main()

segunda-feira, 9 de abril de 2007

Começando um Jogo

O Leonardo me enfogueirou para fazermos um jogo, tanto por e-mail quanto na comunidade de homebrews de DS. A idéia é fazer um jogo de tanque, tipo o Combat do Atari com mais gráfico e cenário maior, mas a idéia é que dê para jogar por wifi.

Então surgiu a idéia de ir desenvolvendo esse jogo e ir explicando a programação dele aqui. Nada melhor para aprender do que a prática.

Movimento básico do jogo: o direcional controla o tanque nas 8 direções. A stylus controla o canhão nos 360 graus.

Primeiro passo: como fazer o jogo saber qual o ângulo em que a stylus está apontando?
A PALib trata ângulos com valores entre 0 e 511 ao invés de 0 a 359. 0=0 graus, 128=90 graus, 256=180 graus e 384=270 graus. Tanto para rotacionar sprites quanto para detectar um ângulo, a informação é sempre em 9 bits (valores de 0 a 511).

Vamos fazer um programa para testar se isso é possível, medindo o ângulo do toque da stylus em relação ao centro da tela.

// Includes
#include <PA9.h> // Include for PA_Lib
// Function: main()
int main(int argc, char ** argv)
{
  s16 angulo=0;
  PA_Init(); // Initializes PA_Lib
  PA_InitVBL(); // Initializes a standard VBL
  PA_InitText(1,0);
  PA_SetTextCol(1,31,31,31);
  // Infinite loop to keep the program running
  while (1)
  {
    if (Stylus.Held) {
      angulo=PA_GetAngle(127,95,Stylus.X,Stylus.Y);
      PA_OutputText(1,0,4,"Angulo a partir do centro: %d   ",angulo);
    }
    PA_WaitForVBL();
  }
  return 0;
} // End of main()


O que o programa faz:
  • Inicializa a variável angulo em 0
  • Inicializa a PALib e define o texto na tela 1 na cor branca
  • começa o loop infinito
  • verifica se a stylus está apertada
  • calcula o angulo entre o centro da tela (coordenada 127,95) e a posição da stylus
  • escreve esse angulo
  • termina o loop, voltando para o início dele (é infinito).

quinta-feira, 29 de março de 2007

Colocando a Mão nos Fundos


Vamos usar essas imagens que foram baixadas e convertidas. A intenção é fazer um programa onde os fundos corram da direita para a esquerda, o fundo mais da frente mais rápido e o mais de trás mais lento, para dar a sensação de profundidade. O sprite do avião deverá se mover conforme pressionamos o direcional.

Copie o conteúdo de PATemplate para uma nova pasta. Entre em source e crie uma pasta chamada "gfx". Copie os arquivos gerados pelo PAGfx (incluindo o PAGfx.ini) para a pasta "gfx" criada. Não é necessário copiar a pasta bin e seu conteúdo. Dois clicks em "Template.pnproj". Abra a pasta source e edite o arquivo main.c. Coloque o seguinte conteúdo nele:

// Includes
#include <pa9.h> // Include for PA_Lib

#include "gfx/all_gfx.c"
#include "gfx/all_gfx.h"

// Function: main()
int main(int argc, char ** argv)
{
  //variaveis
  s32 x=95;
  s32 y=79;
  s32 scrl=0;

  //programa
  PA_Init(); // Initializes PA_Lib
  PA_InitVBL(); // Initializes a standard VBL

  PA_EasyBgLoad(1, 0, fundo3);
  PA_EasyBgLoad(1, 1, fundo4);
  PA_EasyBgLoad(1, 2, fundo5);

  PA_InitParallaxX(1, 512, 256, 128, 0);

  PA_LoadSpritePal(1, 0, (void*)aviao_Pal);
  PA_CreateSprite(1, 0, (void*)aviao_Sprite, OBJ_SIZE_64X32, 1, 0, x, y);

  // Infinite loop to keep the program running
  while (1)
  {
    scrl ++;
    PA_ParallaxScrollX(1, scrl);

    x += (Pad.Held.Right - Pad.Held.Left) * 2;
    y += (Pad.Held.Down - Pad.Held.Up) * 2;
    if (y<0) y=0;
    if (y>159) y=159;
    if (x<0) x=0;
    if (x>191) x=191;

    PA_SetSpriteXY(1,0,x,y);

    PA_WaitForVBL();
  }

  return 0;
} // End of main()

Como o programa funciona:
  • Inicializa as variáveis x, y (que serão a posição do canto superior esquerdo do sprite) e a variável scrl (que será o valor auxiliar do scroll da tela).
  • Carrega, na tela 0, no background mais da frente (zero), o arquivo gerado a partir da imagem "fundo3".
  • Carrega, na tela 0, no background nivel 1, o arquivo gerado de "fundo4".
  • Carrega, na tela 0, no BG2, o "fundo5".
  • Define o Parallax dos fundos. Parallax é um recurso da PAlib para a movimentação de fundos. No caso, vamos setar apenas o ParallaxX (somente horizontal). Os parâmetros de PA_InitParallaxX são: tela, velocidade_BG0, velocidade_BG1, velocidade_BG2 e velocidade_BG3. As velocidades seguem a seguinte lógica: 256 é um pixel a cada frame de animação. Valores maiores aumentam a velocidades (mais pixels por frame), valores menores diminuem a velocidade (demora mais frames para mover um pixel). Por exemplo: velocidade=512 irá mover dois pixels por frame, 1024 irá mover 4 pixels; 128 irá mover um pixel a cada 2 frames, 64 moverá um pixel a cada 4 frames, 96 moverá um pixel a cada 3 frames. 0 (zero) significa que esse fundo não será afetado pelo parallax.
  • Carrega a paleta de cores do sprite na tela 1, na paleta 0 da memória de vídeo, usando a paleta gerada pela imagem do avião.
  • Cria o sprite na tela 1, identificado como 0, usando a imagem do avião, de tamanho 64x32 (OBJ_SIZE_64X32), em modo 256 cores (1), usando a paleta 0, posicionado em x,y.
  • Começa o loop infinito do jogo.
  • Aumenta scrl (soma 1 a scrl).
  • Move o parallax da tela 1 conforme o valor de scrl.
  • Se for pressionado para a direita, x é acrescido de 2. Se for pressionado para a esquerda, x é decrescido de 2. Se nenhum dos dois for pressionado, x se mantém igual (lembram o que eu escrevi no segundo artigo?)
  • Se for pressionado para baixo, y é acrescido de 2. Se for pressionado para cima, y é decrescido de 2. Se nenhum dos dois for pressionado, y permanece igual.
  • Verifica que o sprite não passe dos limites da tela (x não pode ser menor que 0 nem maior que 191, ou seja, 255-64; y não pode ser menor que 0 e nem maior que 159, ou seja, 191-32). Se o sprite passar do limite da tela, ele deve ser resetado para o limite da tela. Reparem também que nesta seqüência de IFs eu não usei as chaves; se o if executar um comando apenas, elas não são necessárias.
  • Posiciona, na tela 1, o sprite 0 na posição x,y
  • Aguarda o processamento do frame.
  • Encerra o loop, como ele é infinito, volta para o início do loop.

sexta-feira, 23 de março de 2007

Começando a Explorar a PAlib

O que seria de um video-game se não fossem as imagens. Hoje iremos começar a trabalhar com elas.

Um jogo 2D normalmente usa dois tipos de imagens: fundos e objetos. O DS tem 4 níveis de fundos (backgrounds) e 4 níveis de objetos (sprites)

O tipo de imagem mais comum para o fundo é o tiled background, a analogia da vida real são os azulejos: eu forro uma parede com azulejos que podem ser iguais ou diferentes. Para ilustrar, olhem este link. Ele é composto apenas de pequenas imagens que postas uma do lado da outra, repetidas ou não, aparecem como um fundo completo e coeso. As imagens estão neste link. Para termos uma melhor analogia, devemos comparar essa cena com aqueles azulejados portugueses, onde cada parte de uma imagem é pintada num azulejo, e todos dispostos juntos formam a imagem completa, como no caso desta imagem. Qual a utilidade de fazer a imagem com tiles (ajulejos)? Economia de memória de vídeo. Se houver dois ou mais tiles iguais, só é necessário armazenar um na memória, os outros são cópias.

Os sprites normalmente são desenhados também em forma de tiles, mas de uma forma um pouco diferente. Na memória de vídeo, eles são alocados em outra área e passam sobre o fundo. Então se eu tenho um sprite e um tiled background, o sprite sempre aparecerá sobre o tiled background. O Nintendo DS pode armazenar até 128 sprites de até 64x64 pixels com 256 cores na memória de vídeo. A ordem de sobreposição dos sprites depende primeiramente do nível do sprite e depois do número do sprite. O nível 0 é o mais da frente e o 3 é o mais ao fundo. Dois sprites no mesmo nível, o de menor número aparecerá na frente.

A PAlib tem um utilitário muito bom para transformar imagens de PC em tiled backgrounds ou sprites. Ele fica na pasta PAlib\Tools\PAGfx dentro da pasta do devkitpro. É necessário ter o .NET Framework instalado em seu computador.

Primeiro, baixe as quatro imagens a seguir para o seu computador:




Lembre-se não renomear as imagens, elas devem ser fundo5.gif, fundo4.gif, fundo3.gif e aviao.gif, respectivamente.

Agora, vamos rodar o "PAGC Frontend.exe". Clique na aba "Sprites" e clique no botão "Add File(s)". Selecione o aviao.gif e confirme. O programa irá listar o avião.gif. O ColorMode deve ficar em "256 colors" e a Palette "aviao". Agora clique na aba "Backgrounds". Clique novamente em "Add File(s)". Selecione os três arquivos fundo3.gif, fundo4.gif e fundo5.gif e confirme. Eles serão listados também. O BgMode dos três deve ser TileBg e a Palette de cada um deve ser o nome do arquivo sem o gif. Caso eu tivesse dois arquivos com a mesma palete de cores, poderia alterar o nome da paleta de um deles para o mesmo nome da do outro, economizando memória, mas este não é o caso aqui. Em Global Options, verifique que a Transparent Color seja Magenta. Clique em "Save Ini" e pode sair do programa.

Depois do ini pronto, rode o "PAGfx.exe". Ele irá criar diversos arquivos. Nós usaremos esses arquivos para fazer um fundo que se move em três níveis de velocidade. O fundo3 se moverá mais rápido e o 5 mais lento, dando efeito de profundidade. Tente procurar imagens de fundos que possam ser usados com essa finalidade, só lembrando que o tiled background deve ter tamanho de 256x256 ou 512x512 pixels. Pode-se recortar um pedaço de uma imagem e fazer. Eu cortei e espelhei, para dar continuidade quando fizesse o deslocamento, por isso as sombras das árvores ficaram todas voltadas para o centro da imagem. Depois cortei os três níveis e corrigi os espaços das montanhas no céu e preenchi tudo o que eu quisesse transparente com a cor magenta (magenta acho que é a cor menos usada, a não ser que eu estivesse fazendo um jogo da Barbie). O PAGfx permite apenas que magenta, verde, preto ou branco seja a cor transparente.

sexta-feira, 16 de março de 2007

Começando a interagir com o DS


Chegou a hora de fazer os programas interagirem com o DS, obedecerem comandos. A primeira coisa é entender os comandos que a PAlib tem para fazer isso.

O objeto Pad é o encarregado de ler os comandos dos controles do DS. O formato é Pad.Ação.Botão
  • Ação pode ser Held ou Newpress
  • Botão pode ser Up, Down, Left, Right, A, B, X, Y, L, R, Start ou Select
Para verificar se foi pressionado o botão A, usa-se if (Pad.Newpress.A) { ...
Para verificar se a seta para a esquerda está sendo pressionada, usa-se if (Pad.Held.Left) { ...
E assim por diante.

Vamos analizar, então o programa do cálculo das cores:
  • Fazer um programa que utilize os botões Left, Right, A, B, L e R para alterar os valores de Red, Green e Blue das cores, calculando o valor decimal resultante e mostrar os valores individuais, o resultante e desenhar um quadrado na tela com a cor calculada
  • Fórmula: RGB podem variar cada um entre 0 e 31. O valor resultante é composto de 16 bits. O bit mais significativo sempre é 1, os 5 seguintes são o azul, os 5 seguintes o verde e os últimos 5 são o vermelho. Então a fórmula é 32768+azul*1024+verde*32+vermelho.
Fonte do programa (main.c - acho que não preciso mais ensinar como usar a template da PAlib):

// Includes
#include // Include for PA_Lib

// Function: main()
int main(int argc, char ** argv)
{
  u8 vermelho=0;
  u8 verde=0;
  u8 azul=0;
  u16 soma=0;

  PA_Init(); // Initializes PA_Lib
  PA_InitVBL(); // Initializes a standard VBL

  PA_Init16bitBg(1,0);

  PA_InitText(0,0);
  PA_SetTextCol(0,31,31,31);

  // Infinite loop to keep the program running
  while (1)
  {
    PA_Draw16bitRect(1,0,0,255,191,soma);
    PA_OutputSimpleText(0,0,0,"B / A => Vermelho");
    PA_OutputSimpleText(0,0,1,"Esq / Dir => Verde");
    PA_OutputSimpleText(0,0,2,"L / R => Azul");
    PA_OutputText(0,0,4,"Vermelho: %d ",vermelho);
    PA_OutputText(0,0,5,"Verde : %d ",verde);
    PA_OutputText(0,0,6,"Azul : %d ",azul);
    PA_OutputText(0,0,7,"16 bits : %d ",soma);
    if (Pad.Newpress.B&&vermelho>0) { vermelho--; }
    if (Pad.Newpress.A&&vermelho<31) { vermelho++; }
    if (Pad.Newpress.Left&&verde>0) { verde--; }
    if (Pad.Newpress.Right&&verde<31) { verde++; }
    if (Pad.Newpress.L&&azul>0) { azul--; }
    if (Pad.Newpress.R&&azul<31) { azul++; }
    soma=32768+azul*1024+verde*32+vermelho;
    PA_WaitForVBL();
  }

  return 0;
} // End of main()

domingo, 11 de março de 2007

Mais um controle de fluxo


Bom, além do controle while existe um outro muito usado. É o for. Na verdade, o for pode ser feito num loop while normalmente, mas é mais prático o uso do for nos casos em que ele se aplica. For é usado para a criação de contadores. Lembram de brincar de esconder? Vou contar até 100...
for (variavel=1; variavel<=100; variavel++) { comandos }

Tradução usando um while:
variavel=1;
while (variavel<=100) {
  comandos;
  variavel++;
}

- "Opa, Guilherme. Esse tal de variavel++ a gente também não viu ainda!"
- "espera um pouco que explico tudo."

Como funcionou o while:
  • a variável foi setada com um valor inicial, no caso, 1.
  • enquanto a variável não chegar a um valor, repete o conjunto de comandos. Mas nesse conjunto, no final, tem um comando que incrementa a variável.
Agora podemos ver o que é variável++, é uma abreviação de variavel=variavel+1. Existe outra abreviação possível, que é variavel+=1. Eu poderia colocar qualquer das três expressões, tanto no while quanto no for. Se eu quero decrescer, posso usar variavel--, ou, variavel-=1. Se eu quero usar outras operações ou outros valores, não posso mais usar o primeiro tipo de abreviação, mas ainda posso usar o segundo. Por exemplo, se eu quiser que x seja multiplicado por 2. O normal seria usar x=x*2, mas posso abreviar para x*=2.

Vamos fazer um exemplo para rodar no DS. Novamente copie a PAlibTemplate e renomeie. Abra o projeto e dê dois clicks em main.c. Digite o seguinte, após "PA_InitVBL();":
s8 x=1;
PA_InitText(0,0);
PA_SetTextCol(0,31,31,31);
PA_OutputSimpleText(0,0,0,"while");
PA_OutputSimpleText(0,16,0,"for");
x=1;
while (x<20) {
  PA_OutputText(0,0,x,"%d",x);
  x++;
}
for (x=1; x<20; x++) {
  PA_OutputText(0,16,x,"%d",x);
}

O que o programa faz:
  • Inicializa o texto na tela de baixo com a cor branca
  • Faz o loop while com x começando em 1, verificando que x seja menor que 20
  • imprime na coluna 0, linha x, o resultado de x
  • acresce x
  • encerra o loop while
  • faz um for com x somando de 1 até 19 (último valor menor que 20)
  • imprime na coluna 16, linha x, o valor de x
  • encerra o loop for

Com esse programa pode-se ver que for e while podem fazer a mesma coisa, mas while serve para mais coisas e for é mais fácil para fazer contadores.

Exercício:
Faça um programa que mostre os números pares de 2 a 40 na tela de cima do DS. A resposta (só a parte a ser incluída no main.c) estará nos comentários.

quinta-feira, 8 de março de 2007

Com que cor eu vou?

Conversando com alguns amigos, diversos me falaram que tinham dificuldade de imaginar as cores e me pediram uma tabela de cores do DS. O problema é que são 32768 cores diferentes. Colocar isso na tela do PC fica complicado porque o PC tem 8 bits para cada componente RGB, enquanto o DS tem 5 bits. Então eu resolvi fazer um programa para DS que mostrasse as cores na própria tela do DS.

Eu postei lá no 4shared do Lord Crowley, nas pasta Homebrews, dentro da pasta Appz, dentro da pasta "Visualizador de cores". Tem o código fonte junto com os arquivos para rodar nos diversos flashcards. Dá para tentar também o download direto neste link.

O programa usa as duas telas. Na tela de baixo tem as instruções de uso e os valores de vermelho, verde e azul, bem como o valor do código 16bits. A tela de cima fica toda da cor selecionada na tela de baixo.

Controlando o fluxo do programa

Até agora nós vimos coisas estáticas na tela do DS. Só que um jogo sempre tem ação. A ação vem da repetição de passos, então vamos estudar como passar diversas vezes pelo mesmo ponto.

while
O while é o primeiro controle de fluxo que iremos executar. A sintaxe dele segue o seguinte padrão:

while (expressão_lógica) { comandos }

while significa "enquanto". Ele é um controle de fluxo do tipo loop (loop significa dar uma volta - por isso a manobra "looping" da aviação tem esse nome, porque o avão dá uma volta no ar). A lógica do while é repetir a execução dos comandos que estão entre chaves enquanto a expressão_lógica for verdadeira. Olhem o exemplo:
x=0;
while (x<10) {
    x=x+1;
}
Tradução: x começa com 0. Cada vez que o loop é executado, x é acrescido de 1. Quando x chegar a 10, a expressão_lógica passa a ser falsa e o loop não é mais executado, passando o fluxo para o que vier depois dele.

if
If é um controle de fluxo do tipo decisão. If significa "se". A sua sintaxe é a seguinte:

if (expressão_lógica) { comandos1 } else { comandos2 }

"else { comandos2 }" é opcional, não precisa ser usado sempre que usar um comando if. Else signica "senão". A lógica é a seguinte: se a expressão_lógica for verdadeira, o conjunto de comandos1 é executado, senão o conjunto comandos2 é executado. Por exemplo:
if (x>20) {
    a=0;
    b=15;
} else {
    a=10;
    c=30;
}
Tradução: se x for maior que 20, então a passa a valer 0 e b passa a valer 15, senão a passa a valer 10 e c passa a valer 30.

Vamos a um programa exemplo para o DS:
Antes de "PA_Init();" digite o seguinte:
    u16 x=0;
    u16 y=0;
    s8 passox=1;
    s8 passoy=1;
    u16 xvelho=0;
    u16 yvelho=0;

Depois de "PA_InitVBL();", digite o seguinte:
    PA_InitText(1,0);
    PA_SetTextCol(1,31,31,0);
    PA_InitRand();
    x=PA_RandMinMax(1,30);
    y=PA_RandMinMax(1,22);


Entre as chaves do "while(1)", antes de "PA_WaitForVBL();" , digite o seguinte:
    xvelho=x;
    yvelho=y;
    x=x+passox;
    y=y+passoy;
    if (x==0) { passox=1; }
    if (x==31) { passox=-1; }
    if (y==0) { passoy=1; }
    if (y==23) { passoy=-1; }
    PA_OutputSimpleText(1,x,y,"o");
    PA_OutputSimpleText(1,xvelho,yvelho," ");

O que o programa faz:
primeiro, inicializa as variáveis (u16 significa que vai ser um valor numérico de 16 bits sem sinal, podendo variar de 0 a 65535; s8 significa um valor numérico de 8 bits com sinal, podendo variar de -128 a 127). Depois prepara a tela de cima para receber texto na cor amarela, inicializa o gerador de números aleatórios e define o valor aleatório de x e y dentro da área de texto do DS, sem encostar nas bordas (a posição de um caracter de texto pode variar horizontalmente entre 0 e 31 e verticalmente entre 0 e 23).
Dentro do loop while, que é um loop infinito, pois 1 sempre vai ser verdadeiro (lembram? 1 é verdadeiro, 0 é falso), roda os seguintes comandos:
  • armazena a posição antiga de x e y em xvelho e yvelho
  • soma o passo de x e y a x e y
  • verifica se x ou y chegaram a uma borda e inverte o passo de quem chegou
  • escreve "o" na posição nova de x e y
  • escreve um espaço (tem que digitar um espaço entre " ") na posição antiga de x e y, apagando o "o" que tinha ali.

quarta-feira, 7 de março de 2007

Hardware

Antes de continuarmos a estudar a linguagem C, vamos olhar um pouco o hardware para o qual iremos desenvolver os programas: o Nintendo DS.

Ele é composto por:
  • um ARM 9 de 66 MHz, responsável pelo processamento principal dos jogos;
  • um ARM 7 de 33 MHz, responsável pela renderização de gráficos 3D e outros processamentos secundários;
  • memória RAM de 32 Mbits (4 MBytes).
Possui duas telas de 256x192 pixels com 32.768 cores - RGB de 5 bits por canal, totalizando 15 bits (2 ^15 = 32.768). O RGB é composto da seguinte forma: os 5 bits menos significativos são o vermelho; os 5 bits seguintes são o verde; os outros 5 bits são o azul e o bit mais significativo sempre vale 1 (o bit mais significativo é o mais à esquerda e o menos significativo é o mais à direita). A tela 0 é a de baixo e a 1 é a de cima. A coordenada 0,0 de cada tela fica no canto superior esquerdo. Os pixels são numerados de 0 a 255 horizontalmente e de 0 a 191 verticalmente. As coordenadas sempre são informadas no formato horizontal,vertical. Perceba isso olhando na figura abaixo:
Cada tela tem 4 níveis de background (fundo), onde são colocadas imagens ou traçados desenhos. O background 0 é o que fica por cima e o 3 é o mais de baixo. Acima desses 4 fundos existem 4 níveis de sprites. Sprites são pequenas imagens que podem ser movidas pela tela. O DS permite utilizar até 128 sprites distribuídos nesses 4 fundos. Para diferenciar background de sprite, imagine um jogo como o 1942. Os aviões e os tiros são sprites. A água e as ilhas (cenário) são o background.

Para fazer um teste, vamos fazer um pequeno programa. Os passos iniciais são os mesmos. Copie a pasta PALibTemplate para outra pasta e renomeie. Abra o Template.pnproj. Na área à esquerda, dê dois cliques em "main.c". Digite o seguinte abaixo de "PA_InitVBL();":
PA_Init16bitBg (1,0);
PA_Put16bitPixel (1,0,0,64512);
PA_Put16bitPixel (1,127,95,33760);
PA_Put16bitPixel (1,255,191,32799);
PA_Put16bitPixel (1,20,120,65535);
PA_Put16bitPixel (1,120,111,33791);

O que o programa faz:
  • Inicializa um fundo com todas as cores da tela do DS na tela 1 no background 0.
  • Desenha um ponto na coordenada 0,0 (canto superior esquerdo) na cor azul. Segundo a regra, 1 bit sempre setado, 5 bits para o azul, 5 bits para o verde e 5 bits para o vermelho: 1111110000000000. Isto transformado para decimal é 64512. Pegue uma calculadora científica (pode ser a do windows mesmo, é só ir em exibir e selecionar científica). Selecione para usar valores em binário. Digite 1111110000000000. Selecione para usar valores em decimal. Vai converter para 64512.
  • Desenha um ponto na coordenada 127,95 (centro da tela) na cor verde.
  • Desenha um ponto na coordenada 255,191 (canto inferior direito) na cor vermelha.
  • Desenha um ponto na coordenada 20,120 na cor branca.
  • Desenha um ponto na coordenada 120,111 na cor amarela.
Não se esqueça de pressionar ALT+2 e depois ALT+1 para gerar o programa para o DS.

sexta-feira, 2 de março de 2007

O primeiro programa - Hello World

Antes de começarmos a estudar as estruturas básicas de um programa, vamos fazer um programa simples, que só vai escrever "hello world" no centro da tela do DS.

Passos para começar um programa novo:
  1. copie todo o conteúdo da pasta "PAlibTemplate", que está na pasta onde foi instalado o devkitPro, para outra pasta (não trabalhe direto na pasta "PAlibTemplate" para não perder o template de programa novo). Vamos usar, no exemplo, "C:\ProgDS\helloworld".
  2. abra o arquivo "Template.pnproj" com o "programmers notepad" (é só dar dois clicks no arquivo que abre sozinho).
  3. na área "Projects", procure o arquivo "main.c"
  4. dê dois clicks em "main.c"
  5. na janela que abriu, programe.
Então vamos ao programa:
  • posicione o cursor na linha abaixo de PA_InitVBL(); e acima de while (1). Não posicione na linha que começa com // pois tudo o que vier depois de // será considerado comentário.
  • digite o seguinte:
    PA_InitText(1,2);
    PA_OutputSimpleText(1,9,11,"Hello World !");
  • salve o projeto
  • pressione ALT+2. Este passo irá zerar algum arquivo já compilado. Deve ser feito para evitar erros.
  • pressione ALT+1. Este passo é a compilação propriamente dita. Aqui são gerados os arquivos .nds, .ds.gba e .sc.nds.
  • passe o programa compilado para o flashcard e para o DS, ou rode em um emulador.

O que o programa faz?
  • PA_InitText(1,2); => inicializa o modo texto na tela 1 (a de cima é a 1, a de baixo é a 0), no nível de background 2.
  • PA_OutputSimpleText(1,9,11,"Hello World !"); => vai para a linha 11, coluna 9, da tela 1 e escreve "Hello World !". Estas coordenadas foram calculadas para mostrar o texto aproximadamente no centro da tela. A tela do DS tem 256x192 pixels. Cada caractere da PAlib ocupa um quadrado de 8x8 pixels. Então eu tenho 32 colunas (numeradas de 0 a 31) e 24 linhas (numeradas de 0 a 23). Como "Hello World !" ocupa 1 linha e 13 colunas, temos de sobra 23 linhas e 19 colunas. Dividindo cada valor por dois (para ter aproximadamente o mesmo número de espaços antes e depois do texto), posicionamos o texto na coluna 9 e na linha 11.

quarta-feira, 28 de fevereiro de 2007

Ferramentas


Antes de continuarmos com o treinamento em desenvolvimento de jogos, vamos baixar as ferramentas que irão nos auxiliar.
Como visto, a linguagem que usaremos é o C.
Um ambiente de desenvolvimento bastante completo e gratuito é o DevKitPro. A biblioteca que usaremos para facilitar a vida é a PAlib.


Instalando o DevKitPro
Antes de mais nada, vamos baixar o instalador. Ele se encontra no site http://www.devkitpro.org. Abra a página do DevKitPro, clique, no menu à esquerda em "Downloads" e no link "Windows Installer". Irá abrir a página do Sourceforge, onde estão hospedados os arquivos. A versão mais recente hoje (28/02/2007) é o devkitPro Update 1.4.2. Baixe (apenas 211Kb) e execute.
Ele irá baixar o resto de arquivos necessários (aproximadamente 20MB) e instalar. Siga o processo de instalação até o final.

Instalando a PAlib
Baixe a última versão da PAlib do site http://www.palib.com. Clique no menu à esquerda em "Downloads". A última versão é a 070222. Baixe o Installer (PAlib 070222 Installer), clicando no ícone do "Download now". Execute o programa baixado. Ele irá incluir outras pastas ao DevKitPro (lembre-se de apontar para o local correto onde ele se encontra, na hora de instalar).

Testando tudo
Se você deixou as opções default habilitadas, o devkitpro foi instalado em "C:\devkitpro".
Abra a pasta "C:\devkitPro\PAlibExamples\Demos\PlatformGame\PlatformGame5".
Execute o programa "build". Ele deverá gerar 3 arquivos: "PlatformGame5.ds.gba", "PlatformGame5.nds" e "PlatformGame5.sc.nds". Cada um é para cada modelo de flashcard. O .nds é o programa propriamente dito, o .ds.gba é para os flashcards que só executam arquivos com a extensão e o loader para GBA e o .sc.nds é para os diversos SuperCards. Se tudo correr bem, você deve conseguir rodar este "joguinho" no seu DS:

Este demo também funciona em emuladores como o no$gba (para rodar nele, use o arquivo .ds.gba gerado).

domingo, 25 de fevereiro de 2007

Expressões e Operadores

No artigo anterior nós vimos como linearizar uma fórmula, mas vimos sem comentar um dos comandos básicos de programação, a atribuição; que segue o padrão: variável=expressão.
Uma expressão pode ser composta de variáveis, propriedades, métodos, funções e operadores.
Os operadores aritméticos são:
+ adição
- subtração
* multiplicação
/ divisão
% resto da divisão
Os operadores lógicos são:
== igual
!= diferente
> maior
>= maior ou igual
  • x=5+(a%2)
    x recebe o valor de 5+(resto da divisão de 5 por 2), ou seja, x receberá 5 ou 6 dependendo se a for par ou ímpar.
  • a=Math.sqrt(Math.pow(b,2)+Math.pow(c,2))
    a recebe o valor da raiz quadrada de (b ao quadrado mais c ao quadrado). Alguém reparou o teorema de Pitágoras aqui?
  • y=10*(x==2||x>20)
    quando a gente trabalha com expressões lógicas, o verdadeiro vale 1 e o falso vale 0. Então, neste caso, y vai receber 10 se x for 2 ou se x for maior que 20; senão y recebe 0.
  • b=a-2
    uma simples, b recebe o valor de a menos 2.
  • y=10*x==2||x>20
    truque sujo, só tirei os parênteses. Agora y recebe 1 se x for 0.2 ou se x for maior que 20. O que mudou? Agora estou verificando se 10 multiplicado por x é 2 (ou seja, se x é igual a 0.2) ou se x for maior que 20.
  • a=b>0&&b<=9 a recebe 1 se b valer entre 1 e 9. Se b for menor ou igual a 0 ou se for maior que 9, então a recebe 0. Então eu poderia escrever a mesma coisa da seguinte forma: a=!(b<=0||b>9)
Percebam a diferença entre &&amp;amp; e ||. && vai ser verdadeiro se todos as comparações forem verdadeiras; || vai ser verdadeiro se qualquer uma das comparações for verdadeira. O ! inverte o que é verdadeiro e o que é falso; é literalmente um "não"; tudo que não é verdadeiro, é falso; tudo que não é falso, é verdadeiro.

sábado, 24 de fevereiro de 2007

Primeiro passo rumo a um programa


Eu começo aqui uma série de artigos sobre programação, culminando na programação de jogos para DS. Vou começar com os passos mais básicos, uma vez que as pessoas que mais me pediram que fizesse estes artigos não sabem nada de programação.

Hoje vou ensinar um dos passos mais básicos: a linearização de fórmulas.

Todos que já chegaram no ensino médio, já conhecem a fórmula de Báscara. É uma fórmula que possui diversos sinais especiais e diversos "andares". Um computador não reconhece um fórmula escrita da seguinte forma:
Para começar que o sinal de mais ou menos significa que nó teremos dois resultados, o X' e o X''. Mas vamos aos caracteres que o computador pode reconhecer:
+ é adição
- é subtração
* é multiplicação (o . é o ponto decimal)
/ é divisão
A potenciação depende de cada linguagem. O C usa um método do objeto Math, chamado pow (abreviação de power), na seguinte forma: Math.pow(Base,Expoente). Como o intuito destes artigos é a programação em C, vamos representar como o C representa.
A raiz quadrada é outro método do mesmo objeto Math, chamado sqrt (abreviação de square root), na seguinte forma: Math.sqrt(valor).
Então vamos traduzir a formula de bascara para C:
x1=(-b+Math.sqrt(pow(b,2)-4*a*c))/(2*a)
x2=(-b-Math.sqrt(pow(b,2)-4*a*c))/(2*a)

Como não existe x' e x'' em programação, foi usado x1 e x2.

Quando se estuda matemática se aprende a usar parênteses dentro de colchetes dentro de chaves. Em programação são usados apenas os parênteses, podendo usar quanto níveis forem necessários, sempre lembrando de fechar todos os que forem abertos.