Header Ads

Game design - Pulo e queda

Postado: na M.Cassab
Estado: falta muito para as 17h30 ?
Escutando: Whitesnake - Still Of The Night

Atribuição: Uso Não-Comercial


Creative Commons License
O conteúdo desta página foi baseado nos tutoriais contidos no site Tile based games, autorizado sob a licença Creative Commons e não pode ser utilizado para fins comerciais.

A tradução e adaptação dos textos, códigos ActionScripts e todas as outras obras originais criadas por Tonypa foram autorizada é são de propriedade dele.

Attribution: Noncommercial


Creative Commons License
The content on this page was based tutorials in the site Tile based games, licensed under a Creative Commons License and you may not use this work for commercial purposes.

The translation and adaptation of text, ActionScripts codes and all other original works created by Tonypa were authorized is are his property.


Vamos deixar o exemplo do Zelda um pouco de lado e vamos falar um pouco sobre jogos do tipo plataforma (também conhecidos como slide scroll ou visão lateral), jogos em que o personagem se movimenta na horizontal, permitindo pular, escalar e, principalmente, cair. Jogos como Mário, Sonic, Megaman e Metroid são baseados nesse tipo.

Normalmente, esse é o tipo de jogo tende a ser mais divertido por permitir que o jogador explore mais movimentos, possibilitando interagir melhor com o cenário e seus elementos.

E um dos principais comandos utilizados é o pulo.

Neste capítulo, vamos ver como fazer nosso personagem saltar no cenário, permitindo alcançar planos mais altos.

Como exemplo de estudo, vamos propor uma cena do jogo Megaman, em que o personagem (PC) está em um dos castelos do Dr. Wily.

No final deste capítulo, o resultado do seu trabalho deverá ser parecido com esse exemplo do Tony (aperte a tecla da barra de espaço para pular):


Esse SFW foi criado pelo Tony para o site Tile based games e está sendo utilizado sob sua autorização.


1º passo: A teoria

Infelizmente, para falar de salto, teremos que entender um pouco de física antes.

Na física clássica, salto é um impulso que realizado por um corpo em que se aplica uma força na superfície... tá não vou me aprofundar, mesmo porque, não vamos precisar calcular newtons por segundo para achar a força do impulso.

Mas essa informação é importante. Se para dar um salto eu preciso aplicar uma força, então definir um valor para a força do salto.

Além disso, nosso corpo, além da força cinética, também sofre uma atração pela força da gravidade.

Se você não dormiu durante a leitura, deve ter percebido que precisará declarar duas informações para realizar o salto: impulso e gravidade.

Fazer esse tipo de abstração antes de começar a desenvolver é muito importante para definir as ações e os parâmetros que serão necessários.

Vai por mim, se deixar para fazer isso na hora, terá problemas e vai ter que apelar para a boa e velha gambiarra. Logo, não tenha preguiça e faça o planejamento direitinho antes de enfiar a mão no mouse.

2º passo: A Lógica

Ao dar um impulso, nosso corpo é projetado para cima. No Flash, sabemos pode ser feito alterando o eixo Y de um objeto. Logo, bastaria calcular o impulso e deslocar a posição Y do PC.

Porém, após o salto, vamos precisar criar uma função que continue calculando a posição Y, simulando os efeitos da gravidade. Se não fizer isso, seu PC ficará preso no teto ou parecerá e desaparecerá.

Alias, esse é o grande erro da maioria das pessoas que tentam fazer jogos de luta pela primeira vez, esquecer de aplicar as leis da física, utilizando impulso, força cinética e gravidade.

Sendo assim, a primeira coisa que fará é adicionar algumas propriedades no objeto do PC (char).

char={xtile:2, ytile:1, speed:4, jumpstart:-18, gravity:2, jump:false};

No código acima, além de passar a posição inicial do PC e sua velocidade, também estamos declarando a força impulso (jumpstart), a força da gravidade (gravity) e um parâmetro indicando que ele não está pulando (jump), para não começar o jogo ricocheteando pelo cenário.

Depois veremos algumas possibilidades interessantes para utilizarmos com esses parâmetros.

A grande maioria dos jogos de plataforma inicia as fases com o PC no alto e caindo até o chão. Não é pau e nem burrice, é proposital.

A primeira vez que um jogador inicia uma fase, ele sempre se distrai com os gráficos. É normal, pois ele está curioso para ver o que tem ali. Para evitar que ele se perca ou tome um tiro de "alegre", utilizamos o recurso do deslocamento para focar a atenção do jogador em um objeto principal (no caso: o PC).

Se você desejar o mesmo, você terá que declarar a posição inicial do PC (que já foi definido pelos parâmetros xtile e ytile) e indicar na função construtora (buildmap) até o ponto em que o PC será deslocado (o chão):

char.y = ((char.ytile+1)*game.tileH)-char.height;

3º passo: Detectando o pulo

Agora, vamos incluir uma condição na função de entrada de comando (detectKeys) para verificar se o jogador apertou o botão de pular, no nosso caso, a barra de espaço:

if (Key.isDown(Key.SPACE) and !ob.jump) {
     ob.jump = true;
     ob.jumpspeed = ob.jumpstart;
}


A condição verifica se o botão foi pressionado (Key.SPACE) e se a condição de pulo é diferente de verdadeiro (jump:false), ou seja, o PC só vai pular se ele não estiver pulando.

Se for o caso,, mudará a condição de pulo para verdadeiro e declarará que a força cinética (jumpspeed) é igual ao impulso (jumpstart).

Quase no final da função, após verificar os outros botões, você incluirá uma verificação que indicará o que pulará:

if (ob.jump) {
     keyPressed=_root.jump(ob);
}


Na verdade, o parâmetro jump é uma função. Se for verdadeiro (true) será declarada o resultado da função, indicando um objeto como alvo (ob).

Para terminar, vamos adicionar o comando para indicar qual frame do movieClip deverá aparecer na hora do pulo:

if (!keyPressed) {
ob.clip.char.gotoAndStop(1);
} else {
ob.clip.char.play();
}


Agora vamos a função jump, que deverá ficar após a função de entrada de comando e será responsável por mover o movieClip relacionado ao objeto indicado no argumento ob, verificando a força cinética (jumpspeed) e a ação da gravidade (gravity):

function jump (ob) {

Depois, vamos atualizar a movimentação realizada pela força cinética (jumpspeed) de acordo com a ação da gravidade (gravity), fazendo com que o personagem perca potência após o impulso:

ob.jumpspeed = ob.jumpspeed+ob.gravity;

Legal, sabemos a velocida, a potência do salto e o tempo de duração no ar. Agora, vamos calcular isso utilizando os Tiles como métrica. Isso facilitará muito para criar uma escala com o cenário, permitindo posicionar as imagens propositalmente ao alcance do jogador ou não:

if (ob.jumpspeed>game.tileH-char.height) {
     ob.jumpspeed = game.tileH-char.height;
}


Só falta verificar se o jogador está pulando em alguma direção:

if (ob.jumpspeed<0) {
     moveChar(ob, 0, -1, -1);
} else if (ob.jumpspeed>0) {
     moveChar(ob, 0, 1, 1);
}


Por último, vamos retornar um valor verdadeiro para confirmar que o PC está saltando.

return (true);

Já que o PC terá a capacidade de pular, ele também deverá ter a capacidade de cair. Logo, vamos criar uma função de queda chamada fall, que verificará se ele está em cima de uma superfície, se não tiver, cairá até tocar uma superfície ou sair do jogo:

Assim como a função anterior, passaremos um argumento indicando que objeto será movimentado (ob):

function fall (ob) {

Claro que a primeira verificação é saber se o PC está pulando ou não.

if (!ob.jump) {

Depois, vamos obter os contornos do movieClip do PC:

getMyCorners (ob.x, ob.y+1, ob);

Como já realizamos a verificação se o Tile atual permite ou não que o PC se movimente, basta apenas resgatar esse valor para saber se ele está em cima de algo ou não:

if (ob.downleft and ob.downright) {
     ob.jumpspeed = 0;
     ob.jump = true;
}


Perceba que agora, o parâmetro que indica se o PC pode se movimentar (walkable=true) perdeu sua função original.

Agora, o valor que indicava a possibilidade de se mover através do Tile passa a indicar um "vão", ou seja, um local sem superfície em que o PC não possui sustentação, como o céu, o segundo plano do cenário entre outras imagens.

Depois de incluir as funções, vamos precisar alterar a função moveChar, responsável por mudar a posição do PC. Logo de cara, vamos passar mais um argumento que retornará o resultado do pulo (jump).

function moveChar(ob, dirx, diry, jump) {

Agora, vamos inserir uma verificação para detectar o pulo, alterando a velocidade de deslocamento do movieClip:

if (Math.abs(jump)==1) {
     speed=ob.jumpspeed*jump;
} else {
     speed=ob.speed;
}


Resumindo, se o PC estiver pulando, a velocidade de deslocamento será igual ao do pulo. Se não, será igual a sua velocidade de caminhada normal.

Depois, vamos resgatar Tile atual e declarar que o PC não está pulando como padrão, para ele não ficar quicando pelo cenário:

ob.y = ob.ytile*game.tileH+ob.height;
ob.jumpspeed = 0;

ob.y = (ob.ytile+1)*game.tileH-ob.height;
ob.jump = false;


Depois, vamos corrigir a sua velocidade na horizontal com o resultado da verificação que criamos agora pouco:

ob.x += speed*dirx;

Por último, executamos a função de queda para verificar se ele está caminhando sobre uma superfície ou não:

fall (ob);

Conclusão

Até agora, para fazer tudo isso, foi preciso:

Fase de Planejamento
- Definir os tipos de cenários
- Definir o comportamento para cada elemento do cenário
- Definir o PC, seu comportamento e movimentação

Fase de Elaboração
- Preparar as imagens do cenário
- Criar um movieClip para agrupar o jogo (empty)
- Criar um movieClip para agrupar as imagens do cenário (tiles)
- Preparar as imagens do PC
- Criar um movieClip para cada movimentação do PC
- Criar um movieClip agrupar os sprite do PC (char)

Fase de Programação
- Declarar as matrizes dos cenários
- Declarar o tamanho de cada tile e a matriz inicial do cenário
- Declarar as classes padrões para os elementos do cenário
- Declarar os elementos de cenário e suas propriedades
- Declarar as propriedades do PC
- Declarar a função construtora
- Declarar a função que troca o cenário
- Declarar a função delimitadora de área de movimentação
- Declarar a função de movimentação
- Declarar a função de pulo
- Declarar a função de queda
- Declarar a função de entrada de comando
- Acionar um evento para iniciar o cenário
- Acionar um evento para verificar queda
- Acionar verificação de entrada de controles

Seu arquivo FLA deverá ter apenas um frame vazio, contendo o seguinte ActionScript:

myMap = [
[1, 1, 1, 1, 1, 1, 1, 1],
[1, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 1, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 1, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 1],
[1, 1, 1, 1, 1, 1, 1, 1]
];

myMap2 = [
[1, 1, 1, 1, 1, 1, 1, 1],
[1, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 1, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 1, 0, 1],
[3, 0, 0, 0, 0, 0, 0, 1],
[1, 1, 1, 1, 1, 1, 1, 1]
];

game = {tileW:30, tileH:30, currentMap:1};

game.Doors = function (newMap, newcharx, newchary) {
     this.newMap=newMap;
     this.newcharx=newcharx;
     this.newchary=newchary;
};

game.Doors.prototype.walkable = true;
game.Doors.prototype.frame = 3;
game.Doors.prototype.door = true;

game.TileClass = function () {};
game.TileClass.prototype.walkable=false;
game.TileClass.prototype.frame=2;

game.Tile0 = function () {};
game.Tile0.prototype.__proto__ = game.TileClass.prototype;
game.Tile0.prototype.walkable=true;
game.Tile0.prototype.frame=1;

game.Tile1 = function () {};
game.Tile1.prototype.__proto__ = game.TileClass.prototype;

game.Tile2 = function () { };
game.Tile2.prototype = new game.Doors(2, 1, 4);

game.Tile3 = function () { };
game.Tile3.prototype = new game.Doors(1, 6, 4);

char = {xtile:2, ytile:1, speed:4, jumpstart:-18, gravity:2, jump:false};

function buildMap(map) {
     _root.attachMovie("empty", "tiles", 1);
     game.clip = _root.tiles;
     var mapWidth = map[0].length;
     var mapHeight = map.length;

     for (var i = 0; i          for (var j = 0; j < mapWidth; ++j) {
               var name = "t_"+i+"_"+j;
               game[name] = new game["Tile"+map[i][j]]();
               game.clip.attachMovie("tile", name, i*100+j*2);
               game.clip[name]._x = (j*game.tileW);
               game.clip[name]._y = (i*game.tileH);
               game.clip[name].gotoAndStop(game[name].frame);
          }
     }

     game.clip.attachMovie("char", "char", 10000);

     char.clip = game.clip.char;
     char.width = char.clip._width/2;
     char.height = char.clip._height/2;
     char.x = (char.xtile*game.tileW)+game.tileW/2;
     char.y = ((char.ytile+1)*game.tileH)-char.height;
     char.clip._x = char.x;
     char.clip._y = char.y;
     char.clip.gotoAndStop(char.frame);
}

function changeMap(ob) {
     var name = "t_"+ob.ytile+"_"+ob.xtile;
     game.currentMap = game[name].newMap;
    
     ob.ytile = game[name].newchary;
     ob.xtile = game[name].newcharx;
     ob.frame = ob.clip._currentframe;
    
     buildMap(_root["myMap"+game.currentMap]);
}
function getMyCorners(x, y, ob) {
     ob.downY = Math.floor((y+ob.height-1)/game.tileH);
     ob.upY = Math.floor((y-ob.height)/game.tileH);
     ob.leftX = Math.floor((x-ob.width)/game.tileW);
     ob.rightX = Math.floor((x+ob.width-1)/game.tileW);

     ob.upleft = game["t_"+ob.upY+"_"+ob.leftX].walkable;
     ob.downleft = game["t_"+ob.downY+"_"+ob.leftX].walkable;
     ob.upright = game["t_"+ob.upY+"_"+ob.rightX].walkable;
     ob.downright = game["t_"+ob.downY+"_"+ob.rightX].walkable;
}

function moveChar(ob, dirx, diry, jump) {
     if (Math.abs(jump) == 1) {
          speed = ob.jumpspeed*jump;
     } else {
          speed = ob.speed;
     }

     getMyCorners(ob.x, ob.y+speed*diry, ob);
    
     if (diry == -1) {
          if (ob.upleft and ob.upright) {
               ob.y += speed*diry;
          } else {
               ob.y = ob.ytile*game.tileH+ob.height;
               ob.jumpspeed = 0;
          }
     }

     if (diry == 1) {
          if (ob.downleft and ob.downright) {
               ob.y += speed*diry;
          } else {
               ob.y = (ob.ytile+1)*game.tileH-ob.height;
               ob.jump = false;
          }
     }

     getMyCorners(ob.x+speed*dirx, ob.y, ob);

     if (dirx == -1) {
          if (ob.downleft and ob.upleft) {
               ob.x += speed*dirx;
               fall(ob);
          } else {
               ob.x = ob.xtile*game.tileW+ob.width;
          }
     }

     if (dirx == 1) {
          if (ob.upright and ob.downright) {
               ob.x += speed*dirx;
               fall(ob);
          } else {
               ob.x = (ob.xtile+1)*game.tileW-ob.width;
          }
     }

     ob.clip._x = ob.x;
     ob.clip._y = ob.y;
     ob.clip.gotoAndStop(dirx+diry*2+3);
     ob.xtile = Math.floor(ob.clip._x/game.tileW);
     ob.ytile = Math.floor(ob.clip._y/game.tileH);

     if (game["t_"+ob.ytile+"_"+ob.xtile].door and ob == _root.char) {
          changeMap(ob);
     }
     return (true);
}

function jump(ob) {
     ob.jumpspeed = ob.jumpspeed+ob.gravity;
    
     if (ob.jumpspeed>game.tileH-char.height) {
          ob.jumpspeed = game.tileH-char.height;
     }
    
     if (ob.jumpspeed<0) {
          moveChar(ob, 0, -1, -1);
     } else if (ob.jumpspeed>0) {
          moveChar(ob, 0, 1, 1);
     }
     return (true);
}

function fall(ob) {
     if (!ob.jump) {
          getMyCorners(ob.x, ob.y+1, ob);
          if (ob.downleft and ob.downright) {
               ob.jumpspeed = 0;
               ob.jump = true;
          }
     }
}

function detectKeys() {
     var ob = _root.char;
     var keyPressed = false;
    
     if (Key.isDown(Key.SPACE) and !ob.jump) {
          ob.jump = true;
          ob.jumpspeed = ob.jumpstart;
     }
    
     if (Key.isDown(Key.RIGHT)) {
          keyPressed = _root.moveChar(ob, 1, 0);
     } else if (Key.isDown(Key.LEFT)) {
          keyPressed = _root.moveChar(ob, -1, 0);
     }

     if (ob.jump) {
          keyPressed = _root.jump(ob);
     }

     if (!keyPressed) {
          ob.clip.char.gotoAndStop(1);
     } else {
          ob.clip.char.play();
     }
}

buildMap(_root["myMap"+game.currentMap]);
fall(_root.char);

_root.onEnterFrame = function(){
     _root.detectKeys();
}


Se estiver tudo certo, o resultado final ao publicar o SWF será esse (aperte a tecla da barra de espaço para pular):


Esse SFW foi criado pelo Tony para o site Tile based games e está sendo utilizado sob sua autorização.


Considerações

Em matéria de interatividade em jogos, um dos aspectos mais importantes é o domínio da gravidade. De longe, é uma das principais artimanhas para tornar um jogo mais real e divertido.

Quando não planejamos ou dominamos adequadamente esse conceito, o jogo fica estranho, com coisas flutuando. É como se tudo deixasse de fazer sentido.

Neste sentido, é muito importante conhecer a teoria básica da física: dois corpos não ocupam o mesmo lugar, tudo que sobe um dia desce, um corpo em movimento tem sua velocidade alterada pelo atrito, todo impacto gera uma reação igual e contrária... e todas aquelas regras chatas.

Não importa se o jogo é simples ou estilizado, a execução correta dessas regras sempre contribuem para a jogabilidade e a interatividade, deixando o jogo mais interessante e divertido.

Tudo isso é ainda mais grave quando falamos de jogos de luta, como Street Fighter e Final Fight. Não há nada mais irritante do que a falta de percepção da queda após o pulo.

O mesmo vale para jogos estilizados como Capcon VS Marvel ou Dragon Ball. Pode não parecer, mas essas regras estão presentes na jogabilidade. O que muda são apenas os valores das propriedades que são exagerados propositalmente.

Logo, como á havia dito, é muito importante planejar tudo antes de começar a desenvolver. Se tiver que arrumar algo "no meio do caminho", terá problemas.

Outro comentário importante é sobre as propriedades do PC, que permite explorar diversas possibilidades:

char={xtile:2, ytile:1, speed:4, jumpstart:-18, gravity:2, jump:false};

Você pode explorar a jogabilidade e adicionar outras propriedades para tornar o jogo mais interessante.

Por exemplo, você poderia adicionar o peso do PC e fazer com que isso influencie ou não na sua velocidade e impulso.

Neste caso, seria mais interessante declarar a gravidade nas propriedades do jogo e não no objeto do PC.

Além disso, também poderia ser acrescentado uma propriedade para definir qual sua velocidade de corrida, de andar devagar ou se arrastando. Não existe um limite para as possibilidades.

Se quiser, volte ao índice para ver a lista completa do conteúdo.

Próximo assunto:
Clouds

Um comentário:

  1. Muito bom Barão, pena q vc parou, e eu to precisando aprender sobre pathfinding, A* (a estrela) etc...

    Espero que vc volte a ter disposição pra continuar com essa aula, abs!

    ResponderExcluir

Tecnologia do Blogger.