Atirador de zumbis simples no Unity

Olá pessoal! Em breve, as aulas começarão no primeiro grupo do curso Unity Games Developer . Antecipando o início do curso, foi realizada uma lição aberta sobre a criação de um atirador de zumbis no Unity. O webinar foi organizado por Nikolai Zapolnov , desenvolvedor sênior de jogos da Rovio Entertainment Corporation. Ele também escreveu um artigo detalhado, que chamamos a sua atenção.



Neste artigo, gostaria de mostrar como é fácil criar jogos no Unity. Se você possui conhecimentos básicos de programação, pode começar rapidamente a trabalhar com esse mecanismo e fazer seu primeiro jogo.



Isenção de responsabilidade nº 1: este artigo é para iniciantes. Se você comeu um cachorro no Unity, então pode parecer chato para você.

Isenção de responsabilidade nº 2: para ler este artigo, você precisa de pelo menos conhecimentos básicos de programação. No mínimo, as palavras "classe" e "método" não devem assustá-lo.

Cuidado, tráfego sob o corte!

Introdução à Unidade


Se você já conhece o editor do Unity, pode pular a introdução e ir direto para a seção "Criando um mundo de jogo".

A unidade estrutural básica no Unity é a "cena". Uma cena é geralmente um nível do jogo, embora em alguns casos possa haver vários níveis ao mesmo tempo em uma cena ou, inversamente, um nível grande pode ser dividido em várias cenas carregadas dinamicamente. As cenas são preenchidas com objetos do jogo e, por sua vez, são preenchidas com componentes. São os componentes que implementam várias funções do jogo: objetos de desenho, animação, física etc. Este modelo permite montar a funcionalidade a partir de blocos simples, como um brinquedo do construtor Lego.

Você pode escrever componentes você mesmo, usando a linguagem de programação C # para isso. É assim que a lógica do jogo é escrita. Abaixo, veremos como isso é feito, mas, por enquanto, vamos dar uma olhada no próprio mecanismo.

Ao iniciar o mecanismo e criar um novo projeto, você verá uma janela à sua frente, onde poderá selecionar quatro elementos principais:



No canto superior esquerdo da captura de tela está a janela "Hierarquia". Aqui podemos ver a hierarquia dos objetos do jogo na cena aberta atual. O Unity criou dois objetos de jogo para nós: uma câmera (“Câmera principal”) através da qual o jogador verá nosso mundo de jogo e uma “Luz direcional” que iluminará nossa cena. Sem ele, veríamos apenas um quadrado preto.

No centro está a janela de edição de cena ("Cena"). Aqui vemos nosso nível e podemos editá-lo visualmente - mova e gire objetos com o mouse e veja o que acontece. Nas proximidades, você pode ver a guia "Jogo", que está atualmente inativa; se você mudar para ele, poderá ver como o jogo se parece com a câmera. E se você iniciar o jogo (usando o botão com o ícone de reprodução na barra de ferramentas), o Unity mudará para essa guia, onde jogaremos o jogo iniciado.

Na parte superior direita está a janela "Inspetor". Nesta janela, o Unity mostra os parâmetros do objeto selecionado e podemos editá-los. Em particular, podemos ver que a câmera selecionada possui dois componentes - "Transform", que define a posição da câmera no mundo do jogo e, de fato, "Camera", que implementa a funcionalidade da câmera.

A propósito, o componente Transform está de uma forma ou de outra em todos os objetos do jogo no Unity.

E, finalmente, na parte inferior, há a guia "Projeto", onde podemos ver todos os ativos chamados que estão em nosso projeto. Ativos são arquivos de dados, como texturas, sprites, modelos 3D, animações, sons e músicas, arquivos de configuração. Ou seja, quaisquer dados que possamos usar para criar níveis ou a interface do usuário. O Unity entende um grande número de formatos padrão (por exemplo, png e jpg para fotos ou fbx para modelos 3d), para que não haja problemas ao carregar dados em um projeto. E se você, como eu, não sabe desenhar, os ativos podem ser baixados na Unity Asset Store, que contém uma enorme coleção de todos os tipos de recursos: gratuitos e vendidos por dinheiro.

À direita da guia "Projeto", a guia "Console" inativa é visível. O Unity grava avisos e mensagens de erro no console, portanto, verifique periodicamente. Especialmente se algo não funcionar - o mais provável é que o console indique a causa do problema. Além disso, o console pode exibir mensagens do código do jogo, para depuração.

Crie um mundo de jogo


Como sou programador e desenho pior do que a pata de galinha, para os gráficos, peguei alguns ativos gratuitos na Unity Asset Store. Você pode encontrar links para eles no final deste artigo.

A partir desses ativos, reuni um nível simples com o qual trabalharemos:



Não é mágica, eu apenas arrastei os objetos de que gostei da janela Projeto e usando o mouse os organizei da maneira que gosto:



A propósito, o Unity permite adicionar objetos padrão à cena com um clique, como um cubo, esfera ou plano. Para fazer isso, clique com o botão direito do mouse na janela Hierarquia e selecione, por exemplo, 3D Object⇨Plane. Portanto, o asfalto no meu nível é apenas montado a partir de um conjunto de aviões no qual eu "puxei" uma textura de um conjunto de ativos.

NB: Se você está se perguntando por que usei muitos planos, e não um com valores em grande escala, a resposta é bastante simples: um plano com uma escala maior terá uma textura muito aumentada, que parecerá antinatural em relação a outros objetos na cena (isso pode ser corrigido com os parâmetros material, mas estamos tentando fazer tudo da maneira mais simples possível, certo?)

Zumbis em busca de uma maneira


Portanto, temos um nível de jogo, mas nada está acontecendo nele ainda. Em nosso jogo, os zumbis perseguem o jogador e o atacam, e para isso eles devem poder se mover em direção ao jogador e contornar obstáculos.

Para implementar isso, usaremos a ferramenta "Navigation Mesh". Com base nos dados da cena, essa ferramenta calcula as áreas em que você pode se mover e gera um conjunto de dados que podem ser usados ​​para procurar a rota ideal de qualquer ponto do nível para outro durante o jogo. Esses dados são armazenados no ativo e não podem ser alterados no futuro - esse processo é chamado de "cozimento". Se você precisar mudar dinamicamente os obstáculos, poderá usar o componente NavMeshObstacle, mas isso não é necessário para o nosso jogo.

Um ponto importante: para que o Unity saiba quais objetos devem ser incluídos no cálculo, no Inspetor de cada objeto (você pode selecionar tudo de uma vez na janela Hierarquia), clique na seta para baixo ao lado da opção "Estático" e marque "Navegação estática":



Em geral, os pontos restantes também são úteis e ajudam o Unity a otimizar a renderização da cena. Hoje não vamos insistir neles, mas quando você terminar de aprender o básico do mecanismo, eu recomendo que você lide com outros parâmetros também. Às vezes, uma única marca de seleção pode aumentar significativamente a taxa de quadros.

Agora vamos usar o item de menu Janela⇨AI⇨Navegação e, na janela que se abre, selecione a guia “Assar”. Aqui, o Unity nos oferecerá para definir parâmetros como a altura e o raio do personagem, o ângulo máximo de inclinação da terra sobre o qual você ainda pode andar, a altura máxima dos degraus e assim por diante. Ainda não mudaremos nada e apenas pressione o botão "Assar".



A unidade fará os cálculos necessários e nos mostrará o resultado:



Aqui, azul indica a área onde você pode andar. Como você pode ver, o Unity deixou um lado pequeno em torno de obstáculos - a largura desse lado depende do raio do personagem. Assim, se o centro do personagem estiver na zona azul, ele não "cairá" nos obstáculos.

Com uma grade de navegação calculada, podemos usar o componente NavMeshAgent para procurar a rota do movimento e controlar o movimento dos objetos do jogo em nosso nível.

Vamos criar um objeto de jogo “Zumbi”, adicionar um modelo 3d de zumbis a partir de ativos e também o componente NavMeshAgent:



Se você iniciar o jogo agora, nada acontecerá. Precisamos informar ao componente NavMeshAgent para onde ir. Para fazer isso, criaremos nosso primeiro componente em C #.

Na janela do projeto, selecione o diretório raiz (chamado de "Ativos") e, na lista de arquivos, clique com o botão direito do mouse para criar o diretório "Scripts". Armazenaremos todos os nossos scripts nele para que o projeto tenha ordem. Agora, dentro do "Scripts", vamos criar um script "Zombie" e adicioná-lo ao objeto do jogo zombie:



Se você clicar duas vezes no script, ele será aberto no editor. Vamos ver o que o Unity criou para nós.

using System.Collections; using System.Collections.Generic; using UnityEngine; public class Zombie : MonoBehaviour { // Start is called before the first frame update void Start() { } // Update is called once per frame void Update() { } } 

Este é um componente padrão em branco. Como podemos ver, o Unity conectou as bibliotecas System.Collections e System.Collections.Generic a nós (agora elas não são necessárias, mas muitas vezes são necessárias no código dos jogos do Unity, portanto estão incluídas no modelo padrão), bem como na biblioteca UnityEngine, que contém todos os API do mecanismo principal.

Além disso, o Unity criou a classe Zombie para nós (o nome corresponde ao nome do arquivo; isso é importante: se eles não corresponderem, o Unity não poderá combinar o script com o componente na cena). A classe é herdada do MonoBehaviour - essa é a classe base dos componentes criados pelo usuário.

Dentro da classe, o Unity criou dois métodos para nós: Iniciar e Atualizar. O mecanismo chamará esses métodos: Iniciar - imediatamente após o carregamento da cena e Atualizar - todos os quadros. De fato, existem muitas dessas funções chamadas pelo mecanismo, mas a maioria delas não será necessária hoje. A lista completa, bem como a sequência da chamada, sempre pode ser encontrada na documentação: https://docs.unity3d.com/Manual/ExecutionOrder.html

Vamos fazer os zumbis se moverem no mapa!

Primeiro, precisamos conectar a biblioteca UnityEngine.AI. Ele contém a classe NavMeshAgent e outras classes relacionadas à grade de navegação. Para fazer isso, adicione a diretiva UnityEngine.AI usando no início do arquivo.

Em seguida, precisamos acessar o componente NavMeshAgent. Para fazer isso, podemos usar o método GetComponent padrão. Ele permite que você obtenha um link para qualquer componente no mesmo objeto de jogo no qual o componente do qual chamamos esse método está localizado (no nosso caso, é o objeto de jogo “Zumbi”). Criaremos o campo NavMeshAgent navMeshAgent na classe, no método Start, obteremos um link para o NavMeshAgent e solicitaremos que ele vá para o ponto (0, 0, 0). Deveríamos obter este script:

 using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; public class Zombie : MonoBehaviour { NavMeshAgent navMeshAgent; // Start is called before the first frame update void Start() { navMeshAgent = GetComponent<NavMeshAgent>(); navMeshAgent.SetDestination(Vector3.zero); } // Update is called once per frame void Update() { } } 

Começando o jogo, veremos como o zumbi se move para o centro do mapa:



Zumbis perseguindo uma vítima


Ótimo Mas nossos zumbis estão entediados e solitários, vamos adicionar a vítima de um jogador ao jogo para ele.

Por analogia com os zumbis, criaremos um objeto de jogo "Player" (desta vez, selecionaremos um modelo 3d de um policial), também adicionaremos o componente NavMeshAgent e o script Player recém-criado. Ainda não tocaremos no conteúdo do script Player, mas precisaremos fazer alterações no script Zombie. Além disso, recomendo definir o valor da propriedade Priority do jogador como 10 no componente NavMeshAgent (ou qualquer outro valor menor que o padrão 50, ou seja, dando ao jogador uma prioridade mais alta). Nesse caso, se o jogador e os zumbis se encontrarem no mapa, os zumbis não serão capazes de mover o jogador, enquanto o jogador será capaz de empurrar os zumbis para fora.

Para perseguir um jogador, um zumbi precisa saber sua posição. E para isso, precisamos obter um link para ele em nossa classe Zombie usando o método padrão FindObjectOfType. Tendo lembrado o link, podemos recorrer ao componente de transformação do jogador e pedir a ele o valor da posição. E para que o zumbi persiga o jogador sempre, e não apenas no início do jogo, definiremos uma meta para o NavMeshAgent no método Update. Você obtém o seguinte script:

 using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; public class Zombie : MonoBehaviour { NavMeshAgent navMeshAgent; Player player; // Start is called before the first frame update void Start() { navMeshAgent = GetComponent<NavMeshAgent>(); player = FindObjectOfType<Player>(); } // Update is called once per frame void Update() { navMeshAgent.SetDestination(player.transform.position); } } 

Execute o jogo e verifique se o zumbi encontrou sua vítima:



Escape Escape


Nosso jogador está de pé como um ídolo. Isso claramente não o ajudará a sobreviver em um mundo tão agressivo, então você precisa ensiná-lo a se movimentar pelo mapa.

Para fazer isso, precisamos obter informações sobre as teclas pressionadas no Unity. O método GetKey da classe Input padrão fornece apenas essas informações!

NB Em geral, essa maneira de obter informações não é totalmente canônica. É melhor usar o Input.GetAxis e a ligação por meio de Project Settings ProjectInput Manager. Melhor ainda, novo sistema de entrada . Mas este artigo acabou sendo muito longo e, portanto, vamos fazê-lo da maneira mais simples.

Abra o script do Player e altere-o da seguinte maneira:

 using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; public class Player : MonoBehaviour { NavMeshAgent navMeshAgent; public float moveSpeed; // Start is called before the first frame update void Start() { navMeshAgent = GetComponent<NavMeshAgent>(); } // Update is called once per frame void Update() { Vector3 dir = Vector3.zero; if (Input.GetKey(KeyCode.LeftArrow)) dir.z = -1.0f; if (Input.GetKey(KeyCode.RightArrow)) dir.z = 1.0f; if (Input.GetKey(KeyCode.UpArrow)) dir.x = -1.0f; if (Input.GetKey(KeyCode.DownArrow)) dir.x = 1.0f; navMeshAgent.velocity = dir.normalized * moveSpeed; } } 

Como no caso de zumbis, no método Start, obtemos um link para o componente NavMeshAgent do jogador e o armazenamos no campo da classe. Mas agora também adicionamos o campo moveSpeed.
Como esse campo é público, seu valor pode ser editado diretamente no Inspetor na Unity! Se você tem um designer de jogos em sua equipe, ele ficará muito feliz por não precisar entrar no código para editar os parâmetros do jogador.

Defina 10 como velocidade:



No método Update, usaremos Input.GetKey para verificar se alguma das setas do teclado está pressionada e formar um vetor de direção para o player. Observe que usamos as coordenadas X e Z. Isso se deve ao fato de que no Unity o eixo Y olha para o céu e a Terra está localizada no plano XZ.

Depois de formarmos o vetor de direção da direção do movimento dir, normalizamos-na (caso contrário, se o jogador quiser se mover ao longo da diagonal, o vetor será um pouco mais longo que um único e esse movimento será mais rápido do que se mover diretamente) e multiplicará pela velocidade do movimento. O resultado é passado para navMeshAgent.velocity e o agente fará o resto.

Ao iniciar o jogo, podemos finalmente tentar escapar dos zumbis para um lugar seguro:



Para fazer a câmera se mover com o player, vamos escrever outro script simples. Vamos chamá-lo de "PlayerCamera":

 using System.Collections; using System.Collections.Generic; using UnityEngine; public class PlayerCamera : MonoBehaviour { Player player; Vector3 offset; // Start is called before the first frame update void Start() { player = FindObjectOfType<Player>(); offset = transform.position - player.transform.position; } // Update is called once per frame void LateUpdate() { transform.position = player.transform.position + offset; } } 

O significado desse script deve ser amplamente entendido. A partir dos recursos - aqui, em vez do Update, usamos o LateUpdate. Esse método é semelhante ao Update, mas é sempre chamado estritamente após a atualização ter sido concluída para todos os scripts em cena. Nesse caso, usamos o LateUpdate, porque é importante para nós que o NavMeshAgent calcule a nova posição do player antes de mover a câmera. Caso contrário, pode ocorrer um efeito desagradável de “empurrão”.

Se você agora anexar esse componente ao objeto de jogo "Câmera principal" e iniciar o jogo, o personagem do jogador estará sempre em destaque!

Momento de animação


Por um momento, nos afastamos dos problemas de sobrevivência nas condições de um apocalipse zumbi e pensamos sobre a arte eterna. Nossos personagens agora parecem estátuas animadas, acionadas por uma força desconhecida (possivelmente ímãs sob o asfalto). E eu gostaria que eles parecessem pessoas reais e vivas (e não muito) - eles moveram os braços e as pernas. O componente Animator e uma ferramenta chamada Animator Controller nos ajudarão com isso.

O Animator Controller é uma máquina de estados finitos (máquina de estados), onde definimos certos estados (o personagem está em pé, o personagem está ativado, o personagem está morrendo etc.), anexamos animações a eles e definimos as regras para a transição de um estado para outro. O Unity mudará automaticamente de uma animação para outra assim que a regra correspondente funcionar.

Vamos criar um Animator Controller para zumbis. Para fazer isso, crie o diretório Animations no projeto (lembre-se da ordem no projeto) e nele - usando o botão direito - Animator Controller. E vamos chamá-lo de "zumbi". Clique duas vezes - e o editor aparecerá diante de nós:



Até o momento, não há estados aqui, mas existem dois pontos de entrada ("Entrada" e "Qualquer estado") e um ponto de saída ("Saída"). Arraste algumas animações dos ativos:



Como você pode ver, assim que arrastamos a primeira animação, o Unity a vinculou automaticamente ao ponto de entrada de entrada. Essa é a chamada animação padrão. Será jogado imediatamente após o início do nível.

Para mudar para um estado diferente (e reproduzir outra animação), precisamos criar regras de transição. E para isso, em primeiro lugar, precisaremos adicionar um parâmetro que definiremos no código para gerenciar animações.

Existem dois botões no canto superior esquerdo da janela do editor: "Camadas" e "Parâmetros". Por padrão, a guia "Camadas" está selecionada, mas precisamos mudar para "Parâmetros". Agora podemos adicionar um novo parâmetro do tipo float usando o botão "+". Vamos chamá-lo de "velocidade":



Agora precisamos dizer ao Unity que a animação “Z_run” deve ser executada quando a velocidade for maior que 0 e “Z_idle_A” quando a velocidade for zero. Para fazer isso, precisamos criar duas transições: uma de “Z_idle_A” para “Z_run” e a outra na direção oposta.

Vamos começar com a transição da ociosidade para a execução. Clique com o botão direito do mouse no retângulo “Z_idle_A” e selecione “Make Transition”. Uma seta aparecerá, clicando sobre a qual você pode configurar seus parâmetros. Primeiro, você precisa desmarcar a opção "Tem hora de saída". Se isso não for feito, a animação mudará não de acordo com a nossa condição, mas quando a anterior terminar de jogar. Como não precisamos disso, desmarcamos. Em segundo lugar, na parte inferior, na lista de condições ("Condições"), você precisa clicar em "+" e o Unity adicionará uma condição a nós. Os valores padrão neste caso são exatamente o que precisamos: o parâmetro "speed" deve ser maior que zero para alternar entre ocioso e executado.



Por analogia, criamos uma transição na direção oposta, mas, como condição, agora especificamos "velocidade" menor que 0,0001. Não há verificações de igualdade para parâmetros do tipo float, eles podem ser comparados apenas para mais / menos:



Agora você precisa vincular o controlador ao objeto do jogo. Selecionaremos o modelo 3d do zumbi na cena (esse é um filho do objeto "Zumbi") e arrastaremos o controlador com o mouse para o campo correspondente no componente Animator:



Resta apenas escrever um script que controle o parâmetro speed!

Crie o script MovementAnimator com o seguinte conteúdo:

 using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; public class MovementAnimator : MonoBehaviour { NavMeshAgent navMeshAgent; Animator animator; // Start is called before the first frame update void Start() { navMeshAgent = GetComponent<NavMeshAgent>(); animator = GetComponentInChildren<Animator>(); } // Update is called once per frame void Update() { animator.SetFloat("speed", navMeshAgent.velocity.magnitude); } } 

Aqui nós, como em outros scripts, no método Start, obtemos acesso ao NavMeshAgent. Também temos acesso ao componente Animator, mas como anexaremos o componente “MovementAnimator” ao objeto de jogo “Zombie” e o Animator estiver no objeto filho, em vez de GetComponent, precisamos usar o método padrão GetComponentInChildren.

No método Update, solicitamos ao NavMeshAgent seu vetor de velocidade, calculamos seu comprimento e passamos ao animador como parâmetro de velocidade. Sem mágica, tudo na ciência!

Agora adicione o componente MovementAnimator ao objeto do jogo Zombie e, se o jogo iniciar, vemos que os zumbis agora estão animados:



Observe que, uma vez que colocamos o código de controle do animador em um componente separado do MovementAnimation, ele pode ser facilmente adicionado ao player. Nem precisamos criar um controlador a partir do zero - você pode copiar um controlador zumbi (isso pode ser feito selecionando o arquivo "Zumbi" e pressionando Ctrl + D) e substituir as animações nos retângulos de estado por "m_idle_" e "m_run". Tudo o resto é como um zumbi. Vou deixar isso para você como um exercício (bem, ou faça o download do código no final do artigo).

Uma pequena adição que é útil fazer é adicionar as seguintes linhas à classe Zombie:

No método Iniciar:

 navMeshAgent.updateRotation = false; 

No método Update:

 transform.rotation = Quaternion.LookRotation(navMeshAgent.velocity.normalized); 

A primeira linha diz ao NavMeshAgent que ele não deve controlar a rotação do personagem, nós faremos isso sozinhos. A segunda linha define a vez do personagem na mesma direção em que seu movimento é direcionado. O NavMeshAgent, por padrão, interpola o ângulo de rotação do personagem e isso não parece muito bom (o zumbi gira mais devagar do que muda a direção do movimento). A adição dessas linhas remove esse efeito.

NB Utilizamos o quaternion para especificar a rotação. Nos gráficos tridimensionais, as principais maneiras de especificar a rotação de um objeto são ângulos de Euler, matrizes de rotação e quaternions. Os dois primeiros nem sempre são fáceis de usar e também estão sujeitos a um efeito desagradável como o "Gimbal Lock". Os quaterniões são privados dessa desvantagem e agora são usados ​​quase universalmente. O Unity fornece ferramentas convenientes para trabalhar com quaternions (assim como com matrizes e ângulos de Euler), o que permite que você não entre nos detalhes do dispositivo desse aparato matemático.

Eu vejo o objetivo


Ótimo, agora podemos escapar dos zumbis. Mas isso não basta, mais cedo ou mais tarde um segundo zumbi aparecerá, depois um terceiro, quinto, décimo ... mas você não pode simplesmente fugir da multidão. Para sobreviver, você tem que matar. Além disso, o jogador já tem uma arma na mão.

Para que o jogador possa atirar, você precisa dar a ele a oportunidade de escolher um alvo. Para fazer isso, coloque o cursor controlado pelo mouse no chão.

Na tela, o cursor do mouse se move no espaço bidimensional - a superfície do monitor. Ao mesmo tempo, nossa cena do jogo é tridimensional. O observador vê a cena através dos olhos, onde todos os raios de luz convergem em um ponto. Combinando todos esses raios, obtemos uma pirâmide de visibilidade:



O olho do observador vê apenas o que cai nesta pirâmide. Além disso, o mecanismo trunca especificamente essa pirâmide de dois lados: primeiro, do lado do observador, há uma tela de monitor, o chamado “plano próximo” (na figura é pintado em amarelo). O monitor não pode exibir objetos fisicamente mais perto do que a tela, então o mecanismo os interrompe. Em segundo lugar, como o computador possui uma quantidade finita de recursos, o mecanismo não pode estender os raios até o infinito (por exemplo, um determinado intervalo de valores possíveis deve ser definido para o buffer de profundidade; além disso, quanto maior for, menor a precisão), então a pirâmide é cortada atrás do chamado "Avião distante".

Como o cursor do mouse se move ao longo do plano próximo, podemos liberar o raio do ponto em que ele está localizado profundamente na cena. O primeiro objeto com o qual ele se cruza será o objeto que o cursor do mouse aponta para o ponto de vista do observador.



Para construir um raio e encontrar sua interseção com objetos na cena, você pode usar o método Raycast padrão da classe Physics. Mas se usarmos esse método, ele encontrará a interseção com todos os objetos da cena - terra, paredes, zumbis ... Mas queremos que o cursor se mova apenas no chão, por isso precisamos explicar de alguma forma à Unity que a busca pela interseção deve ser limitada apenas um determinado conjunto de objetos (no nosso caso, apenas os planos da terra).

Se você selecionar qualquer objeto de jogo na cena, na parte superior do inspetor poderá ver a lista suspensa "Camada". Por padrão, haverá um valor "Padrão". Ao abrir a lista suspensa, você pode encontrar o item "Adicionar camada ...", que abrirá a janela do editor de camadas. No editor, você precisa adicionar uma nova camada (vamos chamá-la de "Ground"):



Agora você pode selecionar todos os planos de solo na cena e usar esta lista suspensa para atribuir a eles a camada de solo. Isso nos permitirá indicar no script para o método Physics.Raycast que é necessário verificar a interseção da viga apenas com esses objetos.

Agora vamos arrastar o sprite do cursor dos recursos para a cena (eu uso Spags Assets⇨Textures⇨Demo⇨white_hip⇨white_hip_14):



Adicionei uma rotação de 90 graus ao redor do eixo X ao cursor para que ele ficasse horizontalmente no chão, defina a escala para 0,25 para que não seja tão grande e defina a coordenada Y para 0,01. Este último é importante para que não haja efeito chamado “combate ao Z”. A placa de vídeo usa cálculos de ponto flutuante para determinar quais objetos estão mais próximos da câmera. Se você definir o cursor como 0 (ou seja, o mesmo do plano de terra), devido a erros nesses cálculos, para alguns pixels, a placa de vídeo decidirá que o cursor está mais próximo e, para outros, que a Terra. Além disso, em quadros diferentes, os conjuntos de pixels serão diferentes, o que criará um efeito desagradável de partes brilhantes do cursor no chão e "tremeluzir" quando ele se mover. O valor de 0,01 é grande o suficiente para compensar os erros no cálculo da placa de vídeo, mas não tão grande que o olho percebeu que o cursor estava suspenso no ar.

Agora renomeie o objeto do jogo para Cursor e crie um script com o mesmo nome e o seguinte conteúdo:

 using System.Collections; using System.Collections.Generic; using UnityEngine; public class Cursor : MonoBehaviour { SpriteRenderer spriteRenderer; int layerMask; // Start is called before the first frame update void Start() { spriteRenderer = GetComponent<SpriteRenderer>(); layerMask = LayerMask.GetMask("Ground"); } // Update is called once per frame void Update() { Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (!Physics.Raycast(ray, out hit, 1000, layerMask)) spriteRenderer.enabled = false; else { transform.position = new Vector3(hit.point.x, transform.position.y, hit.point.z); spriteRenderer.enabled = true; } } } 

Como o cursor é um sprite (desenho bidimensional), o Unity usa o componente SpriteRenderer para renderizá-lo. Obtemos um link para esse componente no método Start para poder ativá-lo / desativá-lo conforme necessário.

Também no método Start, convertemos o nome da camada "Ground" que criamos anteriormente em uma máscara de bit. O Unity usa operações bit a bit para filtrar objetos ao procurar interseções, e o método LayerMask.GetMask retorna a máscara de bit correspondente à camada especificada.

No método Update, acessamos a câmera principal da cena usando Camera.main e solicitamos que recalcule as coordenadas bidimensionais do mouse (obtidas usando Input.mousePosition) em um raio tridimensional. Em seguida, passamos esse raio para o método Physics.Raycast e verificamos se ele se cruza com algum objeto na cena. Um valor de 1000 é a distância máxima. Em matemática, os raios são infinitos, mas os recursos de computação e a memória de um computador não são. Portanto, a Unidade nos pede para determinar uma distância máxima razoável.

Se não houver interseção, desligamos o SpriteRenderer e a imagem do cursor desaparece da tela. Se a interseção foi encontrada, movemos o cursor para o ponto de interseção.Observe que não alteramos a coordenada Y, porque o ponto de interseção do raio com o solo terá Y igual a zero e, atribuindo-o ao nosso cursor, obtemos novamente o efeito Z-fighting, do qual tentamos nos livrar acima. Portanto, tomamos apenas as coordenadas X e Z do ponto de interseção e Y permanece o mesmo.

Adicione o componente Cursor ao objeto de jogo Cursor.

Agora, vamos finalizar o script Player: primeiro, adicione o campo do cursor Cursor. Em seguida, no método Iniciar, adicione as seguintes linhas:

 cursor = FindObjectOfType<Cursor>(); navMeshAgent.updateRotation = false; 

E, finalmente, para que o player sempre gire em direção ao cursor, no método Update, adicione:

 Vector3 forward = cursor.transform.position - transform.position; transform.rotation = Quaternion.LookRotation(new Vector3(forward.x, 0, forward.z)); 

Aqui também não levamos em consideração a coordenada Y.

Atire para sobreviver


O simples fato de virar para o cursor não nos protegerá de zumbis, mas apenas aliviará o personagem do jogador do efeito de surpresa - agora você não pode se esconder atrás dele. Para que ele possa realmente sobreviver nas duras realidades do nosso jogo, você precisa ensiná-lo a atirar. E que tipo de cena é essa se não estiver visível? Todo mundo sabe que qualquer atirador respeitável sempre dispara em balas.

Crie um objeto de jogo Shot e adicione o componente LineRenderer padrão. Usando o campo "Largura" no editor, forneça uma largura pequena, por exemplo, 0,04. Como podemos ver, o Unity o pinta com uma cor púrpura brilhante - dessa maneira objetos sem material são realçados.

Os materiais são um elemento importante de qualquer mecanismo tridimensional. O uso de materiais descreve a aparência do objeto. Todos os parâmetros de iluminação, texturas, shaders - tudo isso é descrito pelo material.

Vamos criar o diretório Materials no projeto e dentro dele o material, vamos chamá-lo de Yellow. Como um sombreador, selecione Apagado / Cor. Esse shader padrão não inclui iluminação, portanto nossa bala ficará visível mesmo no escuro. Selecione a cor amarela:



agora que o material foi criado, você pode atribuí-lo ao LineRenderer:



Criar um script Shot:

 using System.Collections; using System.Collections.Generic; using UnityEngine; public class Shot : MonoBehaviour { LineRenderer lineRenderer; bool visible; // Start is called before the first frame update void Start() { lineRenderer = GetComponent<LineRenderer>(); } // Update is called once per frame void FixedUpdate() { if (visible) visible = false; else gameObject.SetActive(false); } public void Show(Vector3 from, Vector3 to) { lineRenderer.SetPositions(new Vector3[]{ from, to }); visible = true; gameObject.SetActive(true); } } 

Esse script, como você provavelmente já adivinhou, precisa ser adicionado ao objeto do jogo Shot.

Aqui, usei um pequeno truque para exibir uma foto na tela para exatamente um quadro com o mínimo de código. Primeiro, eu uso o FixedUpdate em vez do Update. O método FixedUpdate é chamado na frequência especificada (por padrão - 60 quadros por segundo), mesmo que a taxa de quadros real seja instável. Em segundo lugar, defino a variável visível, que defini como verdadeira quando mostro a foto na tela. No próximo FixedUpdate, redefino-o para false e somente no próximo quadro desativo o objeto do jogo da foto. Essencialmente, eu uso uma variável booleana como um contador de 1 a 0.

O método gameObject.SetActive ativa ou desativa todo o objeto do jogo no qual nosso componente está localizado. Os objetos do jogo desativados não são desenhados na tela e seus componentes não chamam os métodos Update, FixedUpdate etc. O uso desse método permite tornar a foto invisível quando o jogador não está fotografando.

Há também um método Show público no script, que usaremos no script Player para realmente exibir o marcador quando disparado.

Mas primeiro você precisa ser capaz de obter as coordenadas do cano da arma para que o tiro saia do buraco correto. Para fazer isso, localize o objeto Bip001⇨Bip001 Pelvis⇨Bip001 SpBip001R Clavícula⇨Bip001R UpperArmArBip001R AntebraçoipBip001R Hand⇨R_hand_container⇨w_handgun no modelo 3d do jogador e adicione o objeto filho GunBarrel. Coloque-o de forma que fique ao lado do cano da arma:



Agora, no script Player, adicione os campos:

 Shot shot; public Transform gunBarrel; 


Adicione ao método Start do script Player:

 shot = FindObjectOfType<Shot>(); 

E no método Update:

 if (Input.GetMouseButtonDown(0)) { var from = gunBarrel.position; var target = cursor.transform.position; var to = new Vector3(target.x, from.y, target.z); shot.Show(from, to); } 

Como você pode imaginar, o campo público adicionado gunBarrel, como o moveSpeed ​​anterior, estará disponível no Inspetor. Vamos atribuir a ele o objeto de jogo real que criamos:



se agora iniciarmos o jogo, podemos finalmente atirar nos zumbis!



Algo está errado aqui! Parece que os tiros não matam zumbis, mas simplesmente voam através dele!

Bem, é claro, se você olhar para o nosso código de tiro, não rastrearemos de forma alguma se nosso tiro atingiu o inimigo ou não. Basta desenhar uma linha no cursor.

Isso é muito fácil de corrigir. No código para processar cliques do mouse na classe Player, após a linha var para = ... e antes da linha shot.Show (...), adicione as seguintes linhas:

 var direction = (to - from).normalized; RaycastHit hit; if (Physics.Raycast(from, to - from, out hit, 100)) to = new Vector3(hit.point.x, from.y, hit.point.z); else to = from + direction * 100; 

Aqui, usamos o familiar Physics.Raycast para deixar o feixe sair do cano de uma arma e determinar se ele se cruza com qualquer objeto do jogo.

Aqui, no entanto, há uma ressalva: a bala ainda voará pelos zumbis. O fato é que o autor do ativo adicionou um colisor aos objetos do nível (edifícios, caixas, etc.). E o autor do ativo com os personagens não. Vamos consertar esse mal entendido irritante.

Um colisor é um componente com o qual o mecanismo de física determina colisões entre objetos. Geralmente, formas geométricas simples são usadas como colisores - cubos, esferas, etc. Embora essa abordagem forneça colisões menos precisas, as fórmulas de interseção entre esses objetos são bastante simples e não requerem grandes recursos computacionais. Obviamente, se você precisar de precisão máxima, sempre poderá sacrificar o desempenho e usar o MeshCollider. Mas como não precisamos de alta precisão, usaremos o componente CapsuleCollider:



agora a bala não voará pelos zumbis. No entanto, os zumbis ainda são imortais.

Zumbis - Morte de zumbis!


Vamos primeiro adicionar uma animação da morte ao zombie Animation Controller. Para fazer isso, arraste a animação AssetPacks⇨ToonyTinyPeople⇨TT_demo⇨animation⇨zombie⇨Z_death_A para ele. Para ativá-lo, crie um novo parâmetro que tenha morrido com o tipo de gatilho. Ao contrário de outros parâmetros (bool, float, etc.), os gatilhos não lembram seu estado e são mais como uma chamada de função: eles ativaram um gatilho - a transição funcionou e o gatilho foi redefinido. E como um zumbi pode morrer em qualquer estado - e se ele ficar parado e estiver em execução, adicionaremos a transição do estado Qualquer Estado:



Adicione os seguintes campos ao script Zombie:

 CapsuleCollider capsuleCollider; Animator animator; MovementAnimator movementAnimator; bool dead; 

No método Iniciar da classe Zombie, insira:

 capsuleCollider = GetComponent<CapsuleCollider>(); animator = GetComponentInChildren<Animator>(); movementAnimator = GetComponent<MovementAnimator>(); 

No início do método Update, você precisa adicionar uma verificação:

 if (dead) return; 

E, finalmente, adicione o método público Kill à classe Zombie:

 public void Kill() { if (!dead) { dead = true; Destroy(capsuleCollider); Destroy(movementAnimator); Destroy(navMeshAgent); animator.SetTrigger("died"); } } 

A atribuição de novos campos, eu acho, é bastante óbvia. Quanto ao método Kill - nele (se não estamos mortos), definimos a bandeira da morte do zumbi e removemos os componentes CapsuleCollider, MovementAnimator e NavMeshAgent do nosso objeto de jogo, após o qual ativamos a reprodução da animação da morte do controlador de animação.

Por que remover componentes? Para que assim que um zumbi morra, ele pare de se mover pelo mapa e não seja mais um obstáculo para as balas. Para sempre, você ainda precisa se livrar do corpo de alguma maneira bonita após a execução da animação da morte. Caso contrário, os zumbis mortos continuarão a corroer os recursos e, quando houver muitos cadáveres, o jogo diminuirá notavelmente. A maneira mais fácil é adicionar a chamada Destruir (gameObject, 3) aqui. Isso fará com que o Unity exclua esse objeto de jogo 3 segundos após esta chamada.

Para que tudo isso finalmente funcionasse, o último toque permaneceu. Na classe Player, no método Update, onde chamamos Physics.Raycast, no ramo do caso em que uma interseção foi encontrada, adicionamos uma verificação:

 if (hit.transform != null) { var zombie = hit.transform.GetComponent<Zombie>(); if (zombie != null) zombie.Kill(); } 

Physics.Raycast chama as informações de interseção na variável de ocorrência. Em particular, no campo de transformação, haverá um link para o componente Transformar do objeto de jogo com o qual o raio se cruzou. Se este objeto de jogo tiver um componente Zumbi, ele será um zumbi e nós o mataremos. Elementar!

Bem, para que a morte do inimigo pareça espetacular, adicionamos um sistema de partículas simples aos zumbis.

Os sistemas de partículas permitem controlar um grande número de objetos pequenos (geralmente sprites) de acordo com algum tipo de lei física ou fórmula matemática. Por exemplo, você pode fazê-los voar separados ou voar direto para baixo a uma certa velocidade. Com a ajuda de sistemas de partículas em jogos, todos os tipos de efeitos são feitos: fogo, fumaça, faíscas, chuva, neve, sujeira sob as rodas, etc. Usaremos um sistema de partículas para que, no momento da morte, o sangue espirre de um zumbi.

Adicione um sistema de partículas ao objeto do jogo Zombie (clique com o botão direito do mouse e selecione Efeitos⇨Sistema de partículas):

Sugiro as seguintes opções:
Transformação:

  • Posição: Y 0.5
  • Rotação: X -90

Sistema de partículas
  • Duração: 0.2
  • Loop: false
  • Vida útil do início: 0,8
  • Tamanho inicial: 0.5
  • Cor inicial: verde
  • Modificador de gravidade: 1
  • Reproduzir acordado: falso
  • Emissão:
  • Taxa ao longo do tempo: 100
  • Forma:
  • Raio: 0,25

Deve ser algo como isto:



Resta ativá-lo no método Kill da classe Zombie:

 GetComponentInChildren<ParticleSystem>().Play(); 

E agora uma questão completamente diferente!



Zumbis atacam no rebanho


De fato, lutar contra um único zumbi é chato. Você o matou e é isso. Onde está o drama? Onde está o medo de morrer jovem? Para criar uma verdadeira atmosfera de apocalipse e desesperança, deve haver muitos zumbis.

Felizmente, isso é bem simples. Como você deve ter adivinhado, precisamos de outro script. Chame-o de EnemySpawner e preencha-o com o seguinte conteúdo:

 using System.Collections; using System.Collections.Generic; using UnityEngine; public class EnemySpawner : MonoBehaviour { public float Period; public GameObject Enemy; float TimeUntilNextSpawn; // Start is called before the first frame update void Start() { TimeUntilNextSpawn = Random.Range(0, Period); } // Update is called once per frame void Update() { TimeUntilNextSpawn -= Time.deltaTime; if (TimeUntilNextSpawn <= 0.0f) { TimeUntilNextSpawn = Period; Instantiate(Enemy, transform.position, transform.rotation); } } } 

Usando o campo público Período, o designer do jogo pode definir no Inspetor quantas vezes um novo inimigo precisa ser criado. No campo Inimigo, indicamos qual inimigo criar (até agora temos apenas um inimigo, mas no futuro podemos adicionar mais). Bem, então tudo é simples - usando TimeUntilNextSpawn, contamos quanto tempo resta até a próxima aparição do inimigo e, assim que chega a hora, adicionamos um novo zumbi à cena usando o método Instantiate padrão. Sim, no método Start, atribuímos um valor aleatório ao campo TimeUntilNextSpawn, para que, se tivermos vários criadores com o mesmo atraso no nível, eles não adicionarão zumbis ao mesmo tempo.

Uma pergunta permanece: como perguntar ao inimigo no campo Inimigo? Para fazer isso, usaremos uma ferramenta do Unity, como "Prefabs". De fato, uma pré-fabricada é uma parte da cena salva em um arquivo separado. Em seguida, podemos inserir esse arquivo em outras cenas (ou na mesma) e não precisamos coletá-lo novamente em pedaços novamente. Por exemplo, coletamos, dos objetos de paredes, piso, teto, janelas e portas, uma bela casa e a salvamos como uma casa pré-fabricada. Agora você pode inserir esta casa em outros cartões com um movimento do pulso. Ao mesmo tempo, se você editar o arquivo pré-fabricado (por exemplo, adicionar uma porta traseira à casa), o objeto será alterado em todas as cenas. Às vezes é muito conveniente. Também podemos usar prefabs como modelos para o Instantiate - e usaremos essa oportunidade agora.

Para criar uma pré-fabricada, basta arrastar o objeto do jogo da janela da hierarquia para a janela do projeto, o Unity fará o resto. Vamos criar uma casa pré-fabricada a partir de zumbis e, em seguida, adicionar um criador inimigo à cena:



adicionei mais três criadores no projeto para uma mudança (então, no final, tenho quatro deles). E então, o que aconteceu:



Aqui! Já parece um apocalipse zumbi!

Conclusão


Claro, isso está longe de ser um jogo completo. Não consideramos muitos problemas, como criar uma interface de usuário, sons, vidas e morte de um jogador - tudo isso é deixado de fora do escopo deste artigo. Mas parece-me que este artigo será uma introdução digna ao Unity para aqueles que não estão familiarizados com essa ferramenta. Ou talvez alguém experiente consiga extrair algum truque disso?

Em geral, amigos, espero que tenham gostado do meu artigo. Escreva suas perguntas nos comentários, tentarei responder. O código-fonte do projeto pode ser baixado no github: https://github.com/zapolnov/otus_zombies . Você precisará do Unity 2019.3.0f3 ou superior, ele pode ser baixado completamente gratuitamente e sem SMS no site oficial: https://store.unity.com/download .

Links para ativos usados ​​no artigo:

Source: https://habr.com/ru/post/pt485210/


All Articles