ZIM - Lasers

sábado, 28 de abril de 2012

Depois do último post com os detalhes sobre o jogo ZIM, algumas pessoas me pediram pra comentar um pouco sobre como foi feita os lasers das sentinelas.
No início, pensei que seria mais simples se objetos "refletores" tivessem um script pra detectar que foram atingidos pelo laser e emitir um laser na direção/ângulo apropriada. Apesar de funcionar, achei que uma solução mais limpa seria o emissor script emissor do laser, se responsabilizar pelas reflexões.

Para explicar o código dos lasers preciso explicar que neste projeto tenho enumerações globais (Enums) para as tags e layers configurados na Unity3D. Por que ?
Primeiramente, facilita para as pessoas que pegam o projeto, saberem que tags e layers precisam definir na Unity. Segundo, pois algumas vezes, precisamos dizer pra um script que objetos com a tag X deve ser afetados mas com a tag Y ou no layer Z, devem ser ignorados. Permitir que um script afete apenas um grupo de tags ou layers direto pelo editor, da uma flexibilidade imensa pro projeto. Também diminui a quantidade de strings fixas no código, que é um anti-padrão que gera bastante rertabalho caso uma tag seja renomeada mais a frente.

Então, nos códigos abaixo, quando vir "GameTags" e "GameLayers" saiba que se trata destas enumerações globais contendo todas as tags e layers do projeto. Um efeito colateral é ter que converter string para enumeração e vice-versa.

O laser é basicamente um LineRenderer então, nada mais justo que tornar o LineNereder um componente necessário pro script
 [RequireComponent (typeof(LineRenderer))]  

Abaixo 4 variáveis importantes que definem respectivamente:

  • quanto dano o lase causa por segundo
  • objeto atualmente sob a mira do laser
  • tags que recebem dano em contato com o laser
  • tags de objetos que refletem o laser
    public float damagePerSecond = 30; 
private GameObject currentTarget;
public List AffectedTags = new List(); public List ReflectiveTags = new List();

Perceba que tenho listas de tags como parâmetros públicos, assim posso definir diretamente no editor, que objetos refletem o laser ou recebem dano desta sentinela.



O LineRenderer permite ter um número variável de vértices. Uma linha reta sem refletir em nada, teria 2 vértices. A cada reflexão do laser, teremos 1 vértice a mais. Então precisei de uma variável para controlar a quantidade de vértices do LineRenderer
   private int vertexCount = 1;  


A cada frame, defino o LineRenderer como tendo apenas 1 vértice posicionado na origem do gameObject e em seguida chama o método ApplyLaserReflection()
   void FixedUpdate()  
   {  
     // Reseta o line renderer e faz o primeiro ponto iniciar na posicao deste transform  
     vertexCount=1;  
     lineRenderer.SetVertexCount(vertexCount);  
     lineRenderer.SetPosition(0,transform.position);  
     // Emite o laser e calcula as possíveis reflexoes  
      ApplyLaserReflection(transform.position,transform.forward);  
   }  

Este é o méodo principal responsável por emitir o laser.

Observe que este método checa se a tag objeto na mira está entre as tags que devem receber dano (configurada pelo editor) ou está entre as tags que refletem o laser. A cada reflexão do laser um novo vértice é adicionado ao LineRenderer e há uma nova recursão do método. Para evitar loops infinitos caso um objeto esteja 100% alinhado com o laser, impus um limite de 10 reflexões. Caso o objeto atingido não seja um objeto reflexivo, o laser para na posição deste objeto.

 void ApplyLaserReflection(Vector3 position, Vector3 direction)  
   {      
     RaycastHit hitInfo;  
     vertexCount++;  
     lineRenderer.SetVertexCount(vertexCount);  
     if (Physics.Raycast(position, direction, out hitInfo, Mathf.Infinity) && vertexCount < 10)  
     {  
       // Adiciona um novo vertice ao lineRederer  
       lineRenderer.SetPosition(vertexCount-1, hitInfo.point);  
       // Obtem a tag do objeto que atingimos  
       GameTags targetTag = (GameTags)Enum.Parse(typeof(GameTags), hitInfo.transform.tag);  
       // Atingimos alguma coisa que reflete o laser ?  
       if ( ReflectiveTags.Contains(targetTag))  
       {  
         Vector3 newDirection = Vector3.Reflect(direction, hitInfo.transform.forward.normalized);  
         //testamos por mais reflexoes...  
         ApplyLaserReflection(hitInfo.point, newDirection);  
       }  
       else   
       {  
         //Atingimos algo que o laser pode causar dano ?  
         if (AffectedTags.Contains(targetTag))  
           currentTarget = hitInfo.transform.gameObject;  
         else  
           currentTarget = null;  
       }  
     }else  
     {  
       lineRenderer.SetPosition(vertexCount-1 ,position + (direction * 100));  
     }      
   }  

Como este método atualiza a variável global currentTarget o método Update() aplica o dano enviando uma mensagem com BroadcastMessage();

Bem simples e eficaz!


Read more ...

ZIM - Prototipando a idéia

sexta-feira, 27 de abril de 2012

Faz pouco mais de 20 dias que comecei um projeto pessoal chamado "Zim".
Como de praxe, fiquei obcecado pelo projeto que acabou tomando (com muito prazer) todo o meu tempo livre, e tem sido um ótimo exercício de programação e game design.

Conceitos básicos do jogo
Zim é um puzzle game jogo em terceira pessoa sendo desenvolvido sobre Uity3D/C# onde o personagem utilizaria algumas poucas habilidades especiais para concluir cada fase.

Decidir quais habilidades o personagem teria, foi um exercício de game design bem interessante, simplesmente por que é difícil descartar boas idéias. Durante esta etapa, me pareceu que muitas habilidades deixaria o jogador confuso. por isso acabei limitando todas as (centenas) de possibilidades a apenas 3:
  1. Um lançador de gel vermelho: O gel repele tudo que toca, logo, pisar no gel, impulsionaria o personagem para cima.
  2. Um lançador de óleo: Enquanto estiver sobre o óleo, o personagem correria mais rápido. Quanto mais longo o caminho com óleo, maior a velocidade que o personagem poderia atingir.
  3.  Uma "arma magnética", que poderia segurar objetos e arremessá-los.

Com as habilidades definidas ficou claro também algumas coisas divertidas que poderiam ser feitas. Com o gel por exemplo poderíamos alcançar plataformas mais altas:


E com o óleo seria possível ganhar velocidade para vencer obstáculos:

E como o personagem estaria constantemente saltando de um lado para o outro, ficou óbvio que precisava de algum desafio enquanto estivesse "solto no ar" e a idéia mais simples foi um jato de vapor que empurrasse o jogador impedindo-o de aterrissar onde planejava

No caso da arma magnética, seria utilizada para mover todo tipo de plataformas e  posicionar objetos em posições específicas como uma bateria num slot ou lançar um objeto contra um inimigo.



Prototipando - Gel, Óleo e Controles
O primeiro teste foi usando o FirstPersonCharacterController da Unity3D mas logo ficou claro que para atingir os efeitos de física do gel e do óleo precisaria escrever um controlador de personagem totalmente baseado em física, onde o personagem fosse um RigidBody.  

Com o controlador do personagem funcionando, restou fazer a lógica do gel e do óleo. Ambos afetam o vetor velocity de todo RigidBody em que tocam, sempre considerando as normais e a direção do movimento. O resultado é impulso e aceleração instantâneo!

O primeiro resultado satisfatório ficou assim:





Neste ponto a mecânica básica estava funcionando mas o óleo não poderia ser aquela "geléia" azul!

Decidi fazê-lo como um decalque, ou seja, um plano com uma imagem alinhado a superfície. Entretanto isso trouxe problemas, já que as vezes o decalque extrapolava as bordas da face onde havia sido aplicado. Algo parecido com a imagem ao lado.


Por simplicidade, decidi não apelar pra uma engine de decalques e contornar o problema alterando os Bounds do decalque no momento em que era aplicado. Apesar de ter restrições a solução funcionou muito bem. Com o problema do decalque solucionado, o óleo estava de cara nova. Nas imagens abaixo, da pra ver o óleo aplicado bem perto das bordas da rampa sem extrapolar os limites.




 Prototipando - Câmera Orbital, Câmera Fixa e desobstrução do campo de visão
Fiz os primeiros testes com perspectiva de primeira pessoa, mas a intenção é criar um game em terceira pessoa, então passo seguinte foi criar uma câmera orbital de terceira pessoa e um sistema para esmaecer objetos que se obstruem a visão do jogador. Além disso criei regiões onde a câmera estaria fixa num ponto específico, dando um ar mais dramático a cena. Note que ainda não tenho um modelo decente para o personagem, por isso nos vídeos a seguir o personagem será representado com uma cápsula, apenas para termos idéia das dimensões do personagem final.




Prototipando - Jato de vapor, e Sentinelas
Quase todo livro de game design que já li,  começou tentando definir o que é "divertitdo" e por que as pessoas jogam. E um dos fatores que sempre está presente nas definições dadas é o desafio.
Por isso a etapa seguinte, foi incluir alguns obstáculos. De início teremos 2 tipos de obstáculos:

  • Jatos de vapor
  • Sentinelas

Os jatos de vapor são emitidos por tubos de metal espalhados pela fase. Conforme havia planejado os jatos de vapor empurram o personagem impedindo e atrapalhando a sua trajetória.

O sistema dos jatos foi feito para ser simples de ser adicionado na fase. A quantidade de jatos,  intervalo entre eles, a duração e força do jato são configurados direto no editor.
 Apesar de serem primariamente um obstáculo do jogo, também se mostraram bons efeitos decorativos para a fase



Enquanto os jatos de vapor são intermitentes e causem pouco dano por si só, os Sentinelas são fachos contínuos de laser letais para o jogador. Os lasers permanecem na mesma posição patrulhando seu derredor mas travará a mira no jogador caso cruze seu facho. Alguns segundos sob a mira dos lasers é suficiente para reduzir seu life a zero.

Apesar do laser só poder mirar em uma direção, certas superfícies podem refleti-lo, criando obstáculos dinâmicos (e mortais mua-ha-ha!!)








Prototipando - Juntando tudo
No final da criação dos lasers, já tinha elementos suficientes para arriscar projetar um level de testes unindo tudo que foi desenvolvido até o momento mais algumas outras features essenciais como checkpoints e pickups. E o resultado é esse:



Conclusão
Muita coisa ainda precisa ser feita, desenvolvida e até mesmo revista, mas estou bastante satisfeito com o resultado. Farei um novo post tão logo tenha boas novidades para mostrar. 

Até!

Read more ...