BlogBlogs.Com.Br

quarta-feira, 25 de junho de 2008

ActionScript 3.0 Game Development - Tiles

Ainda sobre desenvolvimentos de jogos usando AS 3.0 este post é um exemplo de como carregar um cenário com tiles. Esta técnica é muito usada em games plataforma ou RPGs com de visão isométrica, onde vários quadrados (tiles) se unem e formam uma imagem.

O lugar e qual peça deve ser encaixada para formar o cenário é representado por uma matriz de coordenadas. Pensando dessa forma uma fase de Super Mario Bros nada mais é que um enorme vetor multidimensional :P

Antes de codificar no flash é preciso um arquivo png com os tiles que formarão seu cenário. No Google imagens é possível encontrar esses arquivos rapidamente. Para este exemplo vou usar os tiles de um dos games de Zelda (Zelda tileSet).
Note as dimensões do bloco de tiles: 256 x 2272 pixels. Esse bloco é formado por tiles de 32 x 32 pixels. Sendo assim podemos mapear esta figura como uma matriz de 7 colunas por 70 linhas onde cada elemento é um tile (tendo a primeira coluna como 0 ).

O trabalho seria muito grande para montar um cenário escolhendo os tiles e anotando sua respectiva posição para gerar a matriz de referencia da cena. Por esta razão vamos usar um editor de mapas que alem de facilitar as coisas evita alguns erros.

Atualmente estou usando o Mappy que bem simples e gratuito ( http://www.tilemap.co.uk/ ).

1- No editor de mapas, click em “new map” e escolha 32 x 32 pixels para o tamanho de cada tile e 3 x 3 para o tamanho do mapa em si .
Após gerar seu mapa click em File>Import e escolha o tileset do zelda e crie seu mapa.
Escolha “Custom>Export Flash ActionScript”, para o valor de ajuste coloque “-1” pois o Mappy cria sempre um tile vazio no tileset.
O arquivo ActionScript gerado é um vetor multidimensional com os números dos tiles usados no mapa.

2- No flash crie um novo arquivo para ActionScript 3.0. E vamos aos códigos.

var loader:Loader;
var screenBitmapData:BitmapData;
var tilesBitmapData:BitmapData;
var screenBitmap:Bitmap;
var mapRows:int=3;
var mapCols:int=3;
var tileSize:int=32;
var mapWidth:int=mapRows*tileSize;
var mapHeight:int=mapCols*tileSize;

loader = responsável por carregar o tileset do zelda.
tilesBitmapData = armazena o tileset após ser carregado.
screenBitmapData = armazena os tiles montados.
screenBitmap = armazena os tiles montados para a exibição na tela.
mapRows e mapCols = tamanho a matriz .
mapWidth e mapHeight = tamanho do mapa.

var mapArray:Array = new Array(mapRows);
for (var i = 0; i < mapRows; i++) {
mapArray[i] = new Array(mapRows);
}

mapArray = ([
[416,417,418],
[424,425,426],
[432,433,434]
] );

Declaração de um array de duas dimensões (matriz 3 x 3). E atribuição da matriz gerada pelo Mappy ao vetor multidimensional.

tiles();
function tiles() {
loader = new Loader();
loader.contentLoaderInfo.addEventListener
(Event.COMPLETE,loadComplete);
loader.load(new URLRequest("zelda_tile.png"));
}

Instanciamos o objeto da classe Loader, e adicionamos um Listener que chamará a função loadComplete quando zelda_tile.png for carregado. O arquivo zelda_tile.png deve estar mesma pasta que o arquivo swf.

function loadComplete(e:Event):void {
tilesBitmapData=Bitmap(loader.content).bitmapData;
copyTiles();
}

Como não podemos manipular pixels pelo objeto loader , copiamos o tileset para um objeto da classe BitmapData. (Para passar o arquivo png para o objeto BitmapData temos que chamar loader.content como Bitmap e usar a propriedade bitmapData)

function copyTiles():void {

screenBitmapData=new BitmapData
(mapWidth,mapHeight,false,0x000000);
for (i=0; i < mapRows; i++) {

for (var j=0; j< mapCols; j++) {
var tileNum:int=int(mapArray[i][j]);
var destY:int=i*tileSize;
var destX:int=j*tileSize;
var sourceX:int=(tileNum % 8)*tileSize;
var sourceY:int=(int(tileNum/8))*tileSize;

screenBitmapData.copyPixels
(tilesBitmapData,newRectangle
(sourceX,sourceY,tileSize,tileSize),
new Point(destX,destY));
}

}
screenBitmap=new Bitmap(screenBitmapData);
addChild(screenBitmap);
screenBitmap.x=32;
screenBitmap.y=32;
}

O objeto screenBitmapData é instanciado nas dimensões de 96 x 96 pixels, sem transparência, e com fundo preto.

O loop for é usado para varrer o vetor e encontrar o número de cada tile e partir desde a referência de como montar o mapa.

destY e destX = guardam o ponto X e Y de onde um tile deve ser colocado. Por exemplo: O tile [0] [0] terá sua extremidade esquerda colocada nos pontos 0, 0.

sourceX e sourceY= guardam o ponto X e Y de um tile a ser usado. Exemplo : O elemento [0][0] é o tile número 416. Para X temos (416 mod 8) que é zero multiplicado pelo tamanho do tile. Desta forma encontramos em que coluna está o tile 416. ( O resto da divisão de 416 por 8 é zero) . Em Y temos 1664 (a parte inteira da divisão de 416 por 8 multiplicado pelo tamanho do tile).
Assim o elemento [0][0] é o tile 416 e esta no pixel 0 em X e o pixel 1664 em Y.

Os tiles são copiados um a um para o BitmapData passando para método copyPixels a origem e o destino dos pixels.

No final apenas instaciamos o Bitmap com o BitmapData já pronto e adicionamos no Display list
O resultado dessa experiencia está abaixo:

segunda-feira, 16 de junho de 2008

ActionScript 3.0 Game Development

Neste post algumas experiencias simples para o desenvolvimento de games com AS 3. Abra um novo arquivo no flash para AS 3.0 , insira duas camadas , chame uma de actions (todos os códigos nesta camada) e a bloqueie.

Segue o código:

var ball:Ball = new Ball();
ball.x = stage.stageWidth/2;
ball.y = stage.stageHeight/2;
addChild(ball)

var wall:Wall = new Wall();
wall.x=50;
wall.y=100;
addChild(wall);

A primeira coisa a fazer é criar um Movie Clip que será manipulado por ações realizadas no teclado. Em um game, este seria o personagem controlado pelo jogador. Ao criar este Movie Clip, marque a opção “export for actionScript” desta forma podemos chamar o mc direto da biblioteca. Neste caso o objeto é ball da classe Ball. Após a declaração, atribuímos valor para a posição x e y e adicionamos no stage.

No meu caso também criei um outro Movie Clip (wall) para testar algumas colisões.

var forca:Number = new Number(0.1)
var velocidadeX:Number = new Number(0);
var velocidadeY:Number = new Number(0);
var atrito:Number = new Number(0.92);
var gravidade:Number = new Number (0.2);
var empuxo:Number = new Number(0.5);
var vento:Number = new Number (0.2);

Agora temos algumas declarações que serão necessárias para simular as forças q atuam sobre o personagem. Resumidamente força (ganho de desolamento), VelocidadeX (velocidade no eixo X), VelocidadeY (velocidade no eixo Y) , atrito (força que vai se opor ao movimento), gravidade (força que vai tender o deslocamento para baixo), empuxo (força resistiva que vai tornar o movimento para cima mais pesado ou mais leve) e vento (força que inicia o movimento para um lado).

stage.addEventListener(Event.ENTER_FRAME, starts);

Ao adicionar o event listener enter frame no stage, a função starts será chamada sempre que se passar pelo frame. No código em questão criamos uma especie de loop.
Todo o restante do código se resume a função starts que podemos dividir em quatro blocos:

function starts (event:Event):void{

//-----------Mudança de Escala pela altura------
ball.scaleX=((ball.y*0.5)/stage.stageWidth)+0.5;
ball.scaleY=((ball.y*0.5)/stage.stageWidth)+0.5;

// ----------------- Colisão----------------------
if(ball.hitTestObject(wall)){
velocidadeX=0;
velocidadeX += 5;
}
if(ball.x>530){
velocidadeX=0;
velocidadeX -= 5;
}
if(ball.x<0){
velocidadex=0;
velocidadeX += 5;
}
if(ball.y>335){
velocidadeY=0;
velocidadeY -= 9;
}
if(ball.y<0){
velocidadey=0;
velocidadey+=1;
}

// -----------------Rotina do Teclado----------------
stage.addEventListener(KeyboardEvent.KEY_DOWN,
keyHandler);
function keyHandler (e:KeyboardEvent):void{

// 37 = LEFT
if (e.keyCode==37){
velocidadeX -= forca;
}

//38 = UP
if (e.keyCode==38){
velocidadeY -= forca*empuxo;
}

//39 = RIGHT
if (e.keyCode==39){
velocidadeX += forca;
}

//40 = DOWN
if (e.keyCode==40){
velocidadeY += forca*empuxo;
}

}
//------Ação de forças-----------

velocidadeX += vento;
velocidadeX *= atrito;
velocidadeY += gravidade;

ball.x += velocidadeX;
ball.y += velocidadeY;
}

Mudança de Escala pela altura: Nesta parte o tamanho da bola é alterado conforme a altura pelo método scaleX. A equação para alterar as dimensões da bola foram obtidas a partir de uma regra de três simples (no pixel 400 o tamanho de ball é 1, no pixel 0 o tamanho é 0.5 )

Colisão: A colisão no flash pode ser feita de várias formas, sendo que existem classes prontas com efeitos de colisão bem realistas. Implementei dois tipos de colisão. A primeira usando hitTestObject que verifica a colisão entre dois objetos e retorna True ou false. Pela posição do meu objeto (wall) quando há uma colisão a velocidade no eixo X da bola passa a ser zero e em seqüência recebe um valor contrário ao movimento.
O segundo tipo de colisão ocorre nas bordas do stage onde não é possível usar o hitTestObject (a não ser que haja um mc para a borda).

Rotina do Teclado: Um listener é adicionado para o caso de uma tecla for pressionada. Todo o teclado é mapeado numericamente , e os códigos correspondentes para as direcionais são checados na função do listener. No help do flash estão os códigos das demais teclas.

Quando a tecla LEFT é pressionada por exemplo a velocidade no eixo X recebe o valor dado para força. Fácil notar que quanto maior a força, maior será o deslocamento para cada vez q se pressiona o botão. No caso da tecla UP , vemos que a velocidade no eixo Y recebe o valor da força multiplicado pelo empuxo, o que torna o movimento de subida mais pesado (lembre-se multiplicar por 0.5 é o mesmo que dividir por 2).

Ação das forças: Neste bloco a velocidade do eixo X e Y recebe a ação do vento (que vai somar valores a X constantemente), a ação do atrito (que vai diminuir o movimento , semelhante ao empuxo) e a gravidade ( que inicia o movimento no eixo Y para baixo) e após isso modifica a posição de ball.

Veja meu experimento funcionando. Note a ação do vento e gravidade sobre a bola. E ainda a colisão entre objetos e nas margens do stage (obs: ocorrem alguns bugs devido a maneira como o arquivo foi upado. ^^)

Exemplo

sábado, 14 de junho de 2008

Preloader Com ActionScript 3

Este é um simples preloader para carregar um dado conteúdo. Utilizei um movie clip com uma simples animação e uma máscara que vai revelando o mc conforme o arquivo é carregado. Com essa idéia (máscara + movie clip) é possivel criar preloaders interessantes.

Veja um exemplo : Preloader AS 3

1-Crie um novo documento e adicione duas camadas (actions e objetos). Adicione dois frames.
2-Crie um movie clip para ser sua barra animada e marque a opção export for actionScript como na figura abaixo:
3-Crie um retangulo para a máscara a ser aplicada sobre o movie clip. Faça da mesma forma que no passo anterior, selecione a opção export for actionScript e preencha o campo Class com "Mascara".
4-No primeiro frame adicione o código.

A idéia é a seguinte: Como no primeiro frame temos um stop, a execução do filme ficará limitada ao frame 1, só sendo liberada quando o carregamento for completo. Note que após o carregamento podemos mandar executar tanto o frame seguinte ou uma outra cena.
Segue o código:
stop();
var bar :BarraListras = new BarraListras();
bar.x = ((stage.stageWidth/2) - bar.width/2);
bar.y = ((stage.stageHeight/2) - bar.height/2);
stage.addChild(bar);
Declaração da barra animada e seu posicionamento no meio do stage.
var maskX :Number = new Number(bar.x);
var maskY :Number = new Number(bar.y-4);
var mascara : Mascara = new Mascara();
mascara.x = maskX;
mascara.y = maskY;
addChild(mascara);
bar.mask = mascara;
Posicionamento da mascara pelo valor de mask. Declaração da mascara e sua aplicação da sobre a barra.
this.loaderInfo.addEventListener(ProgressEvent.PROGRESS,
checkingProgress);
loaderInfo.addEventListener(Event.COMPLETE,progressComplete);
Listeners que verificam o carregamento e quando o evento está completo.
function checkingProgress(event:ProgressEvent):void{
var procentLoaded:Number = event.bytesLoaded/event.bytesTotal*100;
mascara.scaleX = procentLoaded/100;
}
function progressComplete (e:Event):void{
loaderInfo.removeEventListener(ProgressEvent.PROGRESS,
checkingProgress);
stage.removeChild(bar);
gotoAndPlay(2);
}
Funções dos listeners. Na função progressComplete podemos substiuir o método gotoAndPlay(2) por this.nextScene( ) , se for o caso de um preloader entre duas cenas.

No segundo frame adcione um stop() e o conteúdo que deseja carregar(no meu caso coloquei uma imagem). Para testar exporte o swf, na barra de menus view>simulate download.