Partes 1-3: malha, cores e altura das célulasPartes 4-7: solavancos, rios e estradasPeças 8-11: água, formas terrestres e muralhasPeças 12-15: salvar e carregar, texturas, distânciasPartes 16-19: encontrando o caminho, esquadrões de jogadores, animaçõesPartes 20-23: Nevoeiro da Guerra, Pesquisa de Mapas, Geração de ProcedimentosPartes 24-27: ciclo da água, erosão, biomas, mapa cilíndricoParte 16: encontrando o caminho
- Realçar células
- Selecione um destino de pesquisa
- Encontre o caminho mais curto
- Crie uma fila prioritária
Depois de calcular as distâncias entre as células, passamos a encontrar os caminhos entre elas.
A partir desta parte, os tutoriais de mapa hexagonal serão criados no Unity 5.6.0. Deve-se notar que na versão 5.6 existe um erro que destrói matrizes de texturas em montagens para várias plataformas. Você pode contornar isso incluindo
É legível no inspetor de matriz de textura.
Planejando uma viagemCélulas destacadas
Para pesquisar o caminho entre duas células, primeiro precisamos selecionar essas células. É mais do que apenas escolher uma célula e monitorar a pesquisa no mapa. Por exemplo, primeiro selecionaremos a célula inicial e depois a final. Nesse caso, seria conveniente que eles fossem destacados. Portanto, vamos adicionar essa funcionalidade. Até criarmos uma maneira sofisticada ou eficiente de destacar, apenas criamos algo para nos ajudar no desenvolvimento.
Textura de contorno
Uma maneira simples de selecionar células é adicionar um caminho a elas. A maneira mais fácil de fazer isso é com uma textura contendo um contorno hexagonal.
Aqui você pode baixar essa textura. É transparente, exceto pelo contorno branco do hexágono. Depois de branco, no futuro poderemos colorir conforme necessário.
Contorno de célula em fundo pretoImporte a textura e defina seu
Tipo de textura como
Sprite . O
Modo Sprite dela será definido como
Único com as configurações padrão. Como essa é uma textura excepcionalmente branca, não precisamos converter para
sRGB . O canal alfa indica transparência, portanto, ative
Alpha é Transparency . Também defino a textura do
Modo de filtro como
Trilinear , porque, caso contrário, as transições mip para os caminhos podem se tornar muito visíveis.
Opções de importação de texturaUm sprite por célula
A maneira mais rápida é adicionar um possível contorno às células, adicionando cada sprite. Crie um novo objeto de jogo, adicione o componente Imagem (
Componente / UI / Imagem ) e atribua a ele nosso sprite de estrutura de tópicos. Em seguida, insira a instância pré-fabricada do
Hex Cell Label na cena, torne o objeto sprite filho dela, aplique as alterações à pré-fabricada e depois se livre da pré-fabricada.
Elemento de seleção filho pré-fabricadoAgora cada célula tem um sprite, mas será muito grande. Para fazer com que os contornos correspondam aos centros das células, altere a
Largura e a
Altura do componente de transformação do sprite para 17.
Sprites de seleção parcialmente ocultos por um relevoDesenho em cima de tudo
Como o contorno é sobreposto à área das bordas das células, geralmente aparece sob a geometria do relevo. Por esse motivo, parte do circuito desaparece. Isso pode ser evitado aumentando ligeiramente os sprites na vertical, mas não no caso de quebras. Em vez disso, podemos fazer o seguinte: sempre desenhe sprites em cima de tudo o mais. Para fazer isso, crie seu próprio shader de sprite. Será suficiente copiarmos o shader de sprite padrão do Unity e fazermos algumas alterações.
Shader "Custom/Highlight" { Properties { [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {} _Color ("Tint", Color) = (1,1,1,1) [MaterialToggle] PixelSnap ("Pixel snap", Float) = 0 [HideInInspector] _RendererColor ("RendererColor", Color) = (1,1,1,1) [HideInInspector] _Flip ("Flip", Vector) = (1,1,1,1) [PerRendererData] _AlphaTex ("External Alpha", 2D) = "white" {} [PerRendererData] _EnableExternalAlpha ("Enable External Alpha", Float) = 0 } SubShader { Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "PreviewType"="Plane" "CanUseSpriteAtlas"="True" } Cull Off ZWrite Off Blend One OneMinusSrcAlpha Pass { CGPROGRAM
A primeira mudança é que ignoramos o buffer de profundidade, tornando o teste Z sempre bem-sucedido.
ZWrite Off ZTest Always
A segunda mudança é que estamos renderizando após o restante da geometria transparente. O suficiente para adicionar 10 à fila de transparência.
"Queue"="Transparent+10"
Crie um novo material que esse shader usará. Podemos ignorar todas as suas propriedades, aderindo aos valores padrão. Em seguida, faça a pré-fabricada de sprite usar esse material.
Usamos nosso próprio material de spriteAgora os contornos da seleção estão sempre visíveis. Mesmo se a célula estiver oculta sob um relevo mais alto, seu contorno ainda será desenhado em cima de tudo o resto. Pode não parecer bonito, mas as células selecionadas sempre estarão visíveis, o que é útil para nós.
Ignore o buffer de profundidadeControle de seleção
Não queremos que todas as células sejam destacadas ao mesmo tempo. De fato, inicialmente todos eles devem ser desmarcados. Podemos implementar isso desabilitando o componente Image do objeto pré-fabricado
Highlight .
Componente de imagem desativadaPara habilitar a seleção de células, adicione o método
EnableHighlight
ao
EnableHighlight
. Ele deve pegar o único filho do
uiRect
e incluir o componente Image. Também criaremos o método
DisableHighlight
.
public void DisableHighlight () { Image highlight = uiRect.GetChild(0).GetComponent<Image>(); highlight.enabled = false; } public void EnableHighlight () { Image highlight = uiRect.GetChild(0).GetComponent<Image>(); highlight.enabled = true; }
Finalmente, podemos especificar a cor para que, quando ativada, dê um tom à luz de fundo.
public void EnableHighlight (Color color) { Image highlight = uiRect.GetChild(0).GetComponent<Image>(); highlight.color = color; highlight.enabled = true; }
unitypackageEncontrando o caminho
Agora que podemos selecionar as células, precisamos seguir em frente e selecionar duas células e, em seguida, encontrar o caminho entre elas. Primeiro, precisamos selecionar as células, depois restringir a pesquisa a um caminho entre elas e, finalmente, mostrar esse caminho.
Início da pesquisa
Precisamos selecionar duas células diferentes, os pontos inicial e final da pesquisa. Suponha que, para selecionar a célula de pesquisa inicial, mantenha pressionada a tecla Shift esquerda enquanto clica no mouse. Nesse caso, a célula é destacada em azul. Precisamos salvar o link para esta célula para futuras pesquisas. Além disso, ao escolher uma nova célula inicial, a seleção da antiga deve ser desativada. Portanto, adicionamos o campo
searchFromCell
ao
searchFromCell
.
HexCell previousCell, searchFromCell;
Dentro do
HandleInput
podemos usar
Input.GetKey(KeyCode.LeftShift)
para testar a tecla Shift pressionada.
if (editMode) { EditCells(currentCell); } else if (Input.GetKey(KeyCode.LeftShift)) { if (searchFromCell) { searchFromCell.DisableHighlight(); } searchFromCell = currentCell; searchFromCell.EnableHighlight(Color.blue); } else { hexGrid.FindDistancesTo(currentCell); }
Onde procurarPonto final de pesquisa
Em vez de procurar todas as distâncias de uma célula, agora estamos procurando um caminho entre duas células específicas. Portanto, renomeie
HexGrid.FindDistancesTo
para
HexGrid.FindPath
e atribua o segundo parâmetro
HexCell
, além de alterar o método
Search
.
public void FindPath (HexCell fromCell, HexCell toCell) { StopAllCoroutines(); StartCoroutine(Search(fromCell, toCell)); } IEnumerator Search (HexCell fromCell, HexCell toCell) { for (int i = 0; i < cells.Length; i++) { cells[i].Distance = int.MaxValue; } WaitForSeconds delay = new WaitForSeconds(1 / 60f); List<HexCell> frontier = new List<HexCell>(); fromCell.Distance = 0; frontier.Add(fromCell); … }
Agora
HexMapEditor.HandleInput
deve chamar o método modificado, usando
searchFromCell
e
currentCell
como argumentos. Além disso, só podemos pesquisar quando sabemos em qual célula pesquisar. E não precisamos nos preocupar em procurar se os pontos inicial e final coincidem.
if (editMode) { EditCells(currentCell); } else if (Input.GetKey(KeyCode.LeftShift)) { … } else if (searchFromCell && searchFromCell != currentCell) { hexGrid.FindPath(searchFromCell, currentCell); }
Voltando à pesquisa, primeiro precisamos nos livrar de todas as seleções anteriores. Portanto, faça o
HexGrid.Search
desativar a seleção ao redefinir distâncias. Como isso também desliga a iluminação da célula inicial, ligue-a novamente. Nesta fase, também podemos destacar o ponto final. Vamos fazê-la vermelha.
IEnumerator Search (HexCell fromCell, HexCell toCell) { for (int i = 0; i < cells.Length; i++) { cells[i].Distance = int.MaxValue; cells[i].DisableHighlight(); } fromCell.EnableHighlight(Color.blue); toCell.EnableHighlight(Color.red); … }
Pontos finais de um caminho potencialLimitar pesquisa
Nesse ponto, nosso algoritmo de pesquisa ainda calcula as distâncias para todas as células acessíveis a partir da célula inicial. Mas não precisamos mais disso. Podemos parar assim que encontrarmos a distância final para a célula final. Ou seja, quando a célula atual é finita, podemos sair do loop do algoritmo.
while (frontier.Count > 0) { yield return delay; HexCell current = frontier[0]; frontier.RemoveAt(0); if (current == toCell) { break; } for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { … } }
Pare no ponto finalO que acontece se o ponto final não puder ser alcançado?Em seguida, o algoritmo continuará funcionando até encontrar todas as células acessíveis. Sem a possibilidade de uma saída prematura, ele funcionará como o antigo método FindDistancesTo
.
Exibição de caminho
Podemos encontrar a distância entre o começo e o fim do caminho, mas ainda não sabemos qual será o caminho real. Para encontrá-lo, você precisa acompanhar como cada célula é alcançada. Mas como fazer isso?
Ao adicionar uma célula à borda, fazemos isso porque é uma vizinha da célula atual. A única exceção é a célula inicial. Todas as outras células foram alcançadas através da célula atual. Se mantivermos o controle de qual célula cada célula foi alcançada, obtemos uma rede de células como resultado. Mais precisamente, uma rede em forma de árvore, cuja raiz é o ponto de partida. Podemos usá-lo para construir o caminho depois de atingir o ponto final.
Rede em árvore que descreve os caminhos para o centroPodemos salvar essas informações adicionando um link para outra célula no
HexCell
. Como não precisamos serializar esses dados, usamos a propriedade padrão para isso.
public HexCell PathFrom { get; set; }
No
HexGrid.Search
defina o valor
PathFrom
do vizinho para a célula atual ao adicioná-lo à borda. Além disso, precisamos alterar esse link quando encontrarmos um caminho mais curto para o vizinho.
if (neighbor.Distance == int.MaxValue) { neighbor.Distance = distance; neighbor.PathFrom = current; frontier.Add(neighbor); } else if (distance < neighbor.Distance) { neighbor.Distance = distance; neighbor.PathFrom = current; }
Tendo atingido o ponto final, podemos visualizar o caminho seguindo esses links de volta à célula inicial e selecioná-los.
if (current == toCell) { current = current.PathFrom; while (current != fromCell) { current.EnableHighlight(Color.white); current = current.PathFrom; } break; }
Caminho encontradoVale a pena considerar que muitas vezes existem vários caminhos mais curtos. O encontrado depende da ordem de processamento das células. Alguns caminhos podem parecer bons, outros podem ser ruins, mas nunca há um caminho mais curto. Voltaremos a isso mais tarde.
Alterar o início da pesquisa
Após selecionar o ponto inicial, a alteração do ponto final acionará uma nova pesquisa. O mesmo deve acontecer ao escolher uma nova célula inicial. Para tornar isso possível, o
HexMapEditor
também deve se lembrar do ponto final.
HexCell previousCell, searchFromCell, searchToCell;
Usando esse campo, também podemos iniciar uma nova pesquisa ao escolher um novo começo.
else if (Input.GetKey(KeyCode.LeftShift)) { if (searchFromCell) { searchFromCell.DisableHighlight(); } searchFromCell = currentCell; searchFromCell.EnableHighlight(Color.blue); if (searchToCell) { hexGrid.FindPath(searchFromCell, searchToCell); } } else if (searchFromCell && searchFromCell != currentCell) { searchToCell = currentCell; hexGrid.FindPath(searchFromCell, searchToCell); }
Além disso, precisamos evitar pontos iguais de início e de término.
if (editMode) { EditCells(currentCell); } else if ( Input.GetKey(KeyCode.LeftShift) && searchToCell != currentCell ) { … }
unitypackagePesquisa mais inteligente
Embora nosso algoritmo encontre o caminho mais curto, ele passa muito tempo explorando pontos que obviamente não farão parte desse caminho. Pelo menos é óbvio para nós. O algoritmo não pode olhar para baixo no mapa; não pode ver que uma pesquisa em algumas direções não faz sentido. Ele prefere seguir as estradas, apesar de estarem na direção oposta ao ponto final. É possível tornar a pesquisa mais inteligente?
No momento, ao escolher a célula a ser processada a seguir, consideramos apenas a distância da célula ao início. Se queremos ser mais inteligentes, também devemos considerar a distância até o ponto final. Infelizmente, ainda não o conhecemos. Mas podemos criar uma estimativa da distância restante. Adicionar esta estimativa à distância da célula nos dá uma compreensão do comprimento total do caminho que passa por essa célula. Em seguida, podemos usá-lo para priorizar pesquisas em células.
Heurística da Pesquisa
Quando usamos estimativas ou suposições em vez de dados exatamente conhecidos, isso é chamado usando heurísticas de pesquisa. Essa heurística representa o melhor palpite da distância restante. Devemos determinar esse valor para cada célula que estamos procurando, portanto, adicionaremos uma propriedade inteira
HexCell
. Não precisamos serializá-lo, portanto outra propriedade padrão será suficiente.
public int SearchHeuristic { get; set; }
Como fazemos uma suposição sobre a distância restante? No caso mais ideal, teremos uma estrada que leva diretamente ao ponto final. Nesse caso, a distância é igual à distância inalterada entre as coordenadas dessa célula e a célula final. Vamos tirar proveito disso em nossa heurística.
Como a heurística não depende de um caminho percorrido anteriormente, é constante no processo de busca. Portanto, precisamos calculá-lo apenas uma vez quando o
HexGrid.Search
adicionar uma célula à borda.
if (neighbor.Distance == int.MaxValue) { neighbor.Distance = distance; neighbor.PathFrom = current; neighbor.SearchHeuristic = neighbor.coordinates.DistanceTo(toCell.coordinates); frontier.Add(neighbor); }
Prioridade de pesquisa
A partir de agora, determinaremos a prioridade da pesquisa com base na distância da célula mais suas heurísticas. Vamos adicionar uma propriedade para esse valor no
HexCell
.
public int SearchPriority { get { return distance + SearchHeuristic; } }
Para que isso funcione,
HexGrid.Search
para que ele use essa propriedade para classificar a borda.
frontier.Sort( (x, y) => x.SearchPriority.CompareTo(y.SearchPriority) );
Pesquisa sem heurística e com heurísticaHeurística válida
Graças às novas prioridades de pesquisa, visitaremos menos células como resultado. No entanto, em um mapa uniforme, o algoritmo ainda processa células que estão na direção errada. Isso ocorre porque, por padrão, os custos para cada etapa da movimentação são 5 e a heurística por etapa adiciona apenas 1. Ou seja, a influência da heurística não é muito forte.
Se os custos de movimentação de todas as cartas forem os mesmos, podemos usar os mesmos custos ao determinar a heurística. No nosso caso, essa será a heurística atual multiplicada por 5. Isso reduzirá significativamente o número de células processadas.
Usando heurística × 5No entanto, se houver estradas no mapa, podemos superestimar a distância restante. Como resultado, o algoritmo pode cometer erros e criar um caminho que na verdade não é o mais curto.
Heurísticas superestimadas e válidasPara garantir que o caminho mais curto seja encontrado, precisamos garantir que nunca superestimamos a distância restante. Essa abordagem é chamada heurística válida. Como o custo mínimo de movimentação é 1, não temos escolha a não ser usar os mesmos custos na determinação da heurística.
A rigor, é bastante normal usar custos ainda mais baixos, mas isso só tornará a heurística mais fraca. A heurística mínima possível é zero, o que nos dá apenas o algoritmo de Dijkstra. Para heurísticas diferentes de zero, o algoritmo é chamado A
* (pronunciado "Uma estrela").
Por que é chamado A *?A idéia de adicionar heurísticas ao algoritmo de Dijkstra foi proposta pela primeira vez por Niels Nilsson. Ele nomeou sua versão A1. Mais tarde, Bertram Rafael apresentou a melhor versão que ele chamou de A2. Então, Peter Hart provou que, com uma boa heurística A2, é ideal, ou seja, não pode haver uma versão melhor. Isso o forçou a chamar o algoritmo A * para mostrar que não poderia ser melhorado, ou seja, A3 ou A4 não apareceria. Então, sim, o algoritmo A * é o melhor que podemos obter, mas é tão bom quanto sua heurística.
unitypackageFila prioritária
Embora A
* seja um bom algoritmo, nossa implementação não é tão eficaz, porque para armazenar a borda, usamos uma lista que precisa ser classificada em cada iteração. Como mencionado na parte anterior, precisamos de uma fila de prioridade, mas sua implementação padrão não existe. Portanto, vamos criar você mesmo.
Nossa vez deve apoiar a operação de configuração e exclusão da fila com base na prioridade. Ele também deve suportar a alteração da prioridade de uma célula que já está na fila. Idealmente, nós o implementamos, minimizando a busca por classificação e memória alocada. Além disso, deve permanecer simples.
Crie sua própria fila
Crie uma nova classe
HexCellPriorityQueue
com os métodos comuns necessários. Usamos uma lista simples para rastrear o conteúdo de uma fila. Além disso, adicionaremos o método
Clear
para limpar a fila para que possa ser usado repetidamente.
using System.Collections.Generic; public class HexCellPriorityQueue { List<HexCell> list = new List<HexCell>(); public void Enqueue (HexCell cell) { } public HexCell Dequeue () { return null; } public void Change (HexCell cell) { } public void Clear () { list.Clear(); } }
Armazenamos as prioridades das células nas próprias células. Ou seja, antes de adicionar uma célula à fila, sua prioridade deve ser definida. Mas, no caso de uma mudança de prioridade, provavelmente será útil saber qual era a antiga prioridade. Então, vamos adicionar isso a
Change
como um parâmetro.
public void Change (HexCell cell, int oldPriority) { }
Também é útil saber quantas células estão na fila, então vamos adicionar a propriedade
Count
para isso. Basta usar o campo para o qual executaremos o incremento e decremento correspondente.
int count = 0; public int Count { get { return count; } } public void Enqueue (HexCell cell) { count += 1; } public HexCell Dequeue () { count -= 1; return null; } … public void Clear () { list.Clear(); count = 0; }
Adicionar à fila
Quando uma célula é adicionada à fila, primeiro vamos usar sua prioridade como um índice, tratando a lista como uma matriz simples.
public void Enqueue (HexCell cell) { count += 1; int priority = cell.SearchPriority; list[priority] = cell; }
No entanto, isso só funciona se a lista for longa o suficiente, caso contrário, iremos além das fronteiras. Você pode evitar isso adicionando itens vazios à lista até atingir o comprimento necessário. Esses elementos vazios não fazem referência à célula, para que você possa criá-los adicionando
null
à lista.
int priority = cell.SearchPriority; while (priority >= list.Count) { list.Add(null); } list[priority] = cell;
Lista com furosMas é assim que armazenamos apenas uma célula por prioridade e, provavelmente, haverá várias. Para rastrear todas as células com a mesma prioridade, precisamos usar outra lista. Embora possamos usar uma lista real para todas as prioridades, também podemos adicionar uma propriedade ao
HexCell
para
HexCell
-las. Isso nos permite criar uma cadeia de células chamada lista vinculada.
public HexCell NextWithSamePriority { get; set; }
Para criar uma cadeia, deixe
HexCellPriorityQueue.Enqueue
forçar a célula recém-adicionada a se referir ao valor atual com a mesma prioridade, antes de excluí-lo.
cell.NextWithSamePriority = list[priority]; list[priority] = cell;
Lista de listas vinculadasRemover da fila
Para obter uma célula de uma fila prioritária, precisamos acessar a lista vinculada no menor índice não vazio. Portanto, percorreremos a lista em um loop até encontrá-la. Se não encontrarmos, a fila está vazia e retornamos
null
.
Da cadeia encontrada, podemos retornar qualquer célula, porque todas elas têm a mesma prioridade. A maneira mais fácil é devolver a célula desde o início da cadeia.
public HexCell Dequeue () { count -= 1; for (int i = 0; i < list.Count; i++) { HexCell cell = list[i]; if (cell != null) { return cell; } } return null; }
Para manter o link para a cadeia restante, use a próxima célula com a mesma prioridade que o novo início. Se houvesse apenas uma célula nesse nível de prioridade, o elemento se
null
e será ignorado no futuro.
if (cell != null) { list[i] = cell.NextWithSamePriority; return cell; }
Rastreamento mínimo
Essa abordagem funciona, mas percorre a lista sempre que uma célula é recebida. Não podemos evitar encontrar o menor índice não vazio, mas não somos obrigados a começar do zero todas as vezes. Em vez disso, podemos rastrear a prioridade mínima e iniciar a pesquisa com ela. Inicialmente, o mínimo é essencialmente igual ao infinito.
int minimum = int.MaxValue; … public void Clear () { list.Clear(); count = 0; minimum = int.MaxValue; }
Ao adicionar uma célula à fila, alteramos o mínimo conforme necessário.
public void Enqueue (HexCell cell) { count += 1; int priority = cell.SearchPriority; if (priority < minimum) { minimum = priority; } … }
E ao sair da fila, usamos pelo menos a lista para iterações e não começamos do zero.
public HexCell Dequeue () { count -= 1; for (; minimum < list.Count; minimum++) { HexCell cell = list[minimum]; if (cell != null) { list[minimum] = cell.NextWithSamePriority; return cell; } } return null; }
Isso reduz significativamente a quantidade de tempo necessária para ignorar o loop da lista de prioridades.
Alterar prioridades
Ao alterar a prioridade de uma célula, ela deve ser removida da lista vinculada da qual faz parte. Para fazer isso, precisamos seguir a cadeia até encontrá-la.
Vamos começar declarando que o cabeçalho da lista de prioridades antiga será a célula atual e também rastrearemos a próxima célula. Podemos pegar a próxima célula imediatamente, porque sabemos que há pelo menos uma célula nesse índice.
public void Change (HexCell cell, int oldPriority) { HexCell current = list[oldPriority]; HexCell next = current.NextWithSamePriority; }
Se a célula atual é uma célula alterada, essa é a célula principal e podemos cortá-la como se a tivéssemos retirado da fila.
HexCell current = list[oldPriority]; HexCell next = current.NextWithSamePriority; if (current == cell) { list[oldPriority] = next; }
Se não for esse o caso, precisamos seguir a cadeia até estarmos na célula em frente à célula alterada. Ele contém um link para a célula que foi modificada.
if (current == cell) { list[oldPriority] = next; } else { while (next != cell) { current = next; next = current.NextWithSamePriority; } }
Nesse ponto, podemos remover a célula alterada da lista vinculada, ignorando-a.
while (next != cell) { current = next; next = current.NextWithSamePriority; } current.NextWithSamePriority = cell.NextWithSamePriority;
Após excluir uma célula, é necessário adicioná-la novamente para que ela apareça na lista de sua nova prioridade.
public void Change (HexCell cell, int oldPriority) { … Enqueue(cell); }
O método
Enqueue
incrementa o contador, mas, na realidade, não estamos adicionando uma nova célula. Portanto, para compensar isso, teremos que diminuir o contador.
Enqueue(cell); count -= 1;
Uso da fila
Agora podemos tirar proveito da nossa fila de prioridades no
HexGrid
. Isso pode ser feito com uma única instância, reutilizável para todas as operações de pesquisa.
HexCellPriorityQueue searchFrontier; … IEnumerator Search (HexCell fromCell, HexCell toCell) { if (searchFrontier == null) { searchFrontier = new HexCellPriorityQueue(); } else { searchFrontier.Clear(); } … }
Antes de iniciar o loop, o método Search
deve primeiro ser adicionado à fila fromCell
e cada iteração começa com a saída da célula da fila. Isso substituirá o código de borda antigo. WaitForSeconds delay = new WaitForSeconds(1 / 60f);
Altere o código para adicionar e alterar o vizinho. Antes da mudança, lembraremos da antiga prioridade. if (neighbor.Distance == int.MaxValue) { neighbor.Distance = distance; neighbor.PathFrom = current; neighbor.SearchHeuristic = neighbor.coordinates.DistanceTo(toCell.coordinates);
Além disso, não precisamos mais classificar a borda.
Pesquisa usando uma fila de prioridadeComo mencionado anteriormente, o caminho mais curto encontrado depende da ordem de processamento das células. Nossa vez cria uma ordem diferente da ordem da lista classificada, para que possamos obter outras maneiras. Como adicionamos e removemos do cabeçalho da lista vinculada para cada prioridade, elas são mais como pilhas do que filas. As células adicionadas por último são processadas primeiro. Um efeito colateral dessa abordagem é que o algoritmo é propenso a ziguezague. Portanto, a probabilidade de caminhos em zigue-zague também aumenta. Felizmente, esses caminhos geralmente parecem melhores, então esse efeito colateral é a nosso favor.Lista e fila classificadas com prioridade paraunitypackageParte 17: movimento limitado
- Encontramos formas de movimento passo a passo.
- Exiba imediatamente o caminho.
- Criamos uma pesquisa mais eficaz.
- Nós visualizamos apenas o caminho.
Nesta parte, dividiremos o movimento em movimentos e aceleraremos a busca o máximo possível.Viaje de vários movimentosMovimento passo a passo
Jogos de estratégia que usam redes hexagonais são quase sempre baseados em turnos. As unidades que se deslocam no mapa têm uma velocidade limitada, o que limita a distância percorrida em um turno.Velocidade
Para fornecer suporte a movimentos limitados, adicionamos HexGrid.FindPath
e HexGrid.Search
inserimos o parâmetro inteiro speed
. Determina a amplitude de movimento de um movimento. public void FindPath (HexCell fromCell, HexCell toCell, int speed) { StopAllCoroutines(); StartCoroutine(Search(fromCell, toCell, speed)); } IEnumerator Search (HexCell fromCell, HexCell toCell, int speed) { … }
Diferentes tipos de unidades no jogo usam velocidades diferentes. A cavalaria é rápida, a infantaria é lenta e assim por diante. Ainda não temos unidades, portanto, por enquanto, usaremos uma velocidade constante. Vamos pegar um valor de 24. Esse é um valor razoavelmente grande, não divisível por 5 (o custo padrão da mudança). Adicionar um argumento para FindPath
a HexMapEditor.HandleInput
uma velocidade constante. if (editMode) { EditCells(currentCell); } else if ( Input.GetKey(KeyCode.LeftShift) && searchToCell != currentCell ) { if (searchFromCell) { searchFromCell.DisableHighlight(); } searchFromCell = currentCell; searchFromCell.EnableHighlight(Color.blue); if (searchToCell) { hexGrid.FindPath(searchFromCell, searchToCell, 24); } } else if (searchFromCell && searchFromCell != currentCell) { searchToCell = currentCell; hexGrid.FindPath(searchFromCell, searchToCell, 24); }
Movimentos
Além de rastrear o custo total da movimentação ao longo do caminho, agora também precisamos saber quantos movimentos serão necessários para movê-lo. Mas não precisamos armazenar essas informações em cada célula. Pode ser obtido dividindo a distância percorrida pela velocidade. Como esses números são inteiros, usaremos a divisão inteira. Ou seja, as distâncias totais não superiores a 24 correspondem ao curso 0. Isso significa que todo o caminho pode ser concluído no curso atual. Se o ponto final estiver a uma distância de 30, esse deverá ser o turno 1. Para chegar ao ponto final, a unidade terá que gastar todo o seu movimento no turno atual e em parte do próximo turno.Vamos determinar o curso da célula atual e todos os seus vizinhos dentroHexGrid.Search
. O curso da célula atual pode ser calculado apenas uma vez, logo antes de dar a volta no ciclo vizinho. A jogada do vizinho pode ser determinada assim que encontrarmos a distância para ele. int currentTurn = current.Distance / speed; for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { … int distance = current.Distance; if (current.HasRoadThroughEdge(d)) { distance += 1; } else if (current.Walled != neighbor.Walled) { continue; } else { distance += edgeType == HexEdgeType.Flat ? 5 : 10; distance += neighbor.UrbanLevel + neighbor.FarmLevel + neighbor.PlantLevel; } int turn = distance / speed; … }
Movimento perdido
Se o movimento do vizinho for maior que o movimento atual, cruzamos o limite do movimento. Se o movimento necessário para alcançar um vizinho foi 1, tudo está bem. Mas se a mudança para a próxima célula for mais cara, tudo ficará mais complicado.Suponha que movamos ao longo de um mapa homogêneo, ou seja, para entrar em cada célula, você precisa de 5 unidades de movimento. Nossa velocidade é 24. Após quatro etapas, gastamos 20 unidades de nosso estoque de movimento e restam 4. Na próxima etapa, são necessárias 5 unidades novamente, ou seja, uma a mais do que as disponíveis. O que precisamos fazer nesta fase?Existem duas abordagens para essa situação. A primeira é permitir que a unidade entre na quinta célula no turno atual, mesmo que não tenhamos movimento suficiente. O segundo é proibir o movimento durante o movimento atual, ou seja, os pontos de movimento restantes não podem ser usados e eles serão perdidos.A escolha da opção depende do jogo. Em geral, a primeira abordagem é mais apropriada para jogos nos quais as unidades podem se mover apenas alguns passos por turno, por exemplo, para jogos da série Civilization. Isso garante que as unidades sempre possam mover pelo menos uma célula por turno. Se as unidades puderem mover muitas células por turno, como no Age of Wonders ou no Battle for Wesnoth, a segunda opção será melhor.Como usamos a velocidade 24, vamos escolher a segunda abordagem. Para que ele comece a funcionar, precisamos isolar os custos de entrar na próxima célula antes de adicioná-la à distância atual.
Se, como resultado, cruzarmos a borda do movimento, primeiro usaremos todos os pontos de movimento do movimento atual. Podemos fazer isso simplesmente multiplicando o movimento pela velocidade. Depois disso, adicionamos o custo da mudança. int distance = current.Distance + moveCost; int turn = distance / speed; if (turn > currentTurn) { distance = turn * speed + moveCost; }
Como resultado, concluiremos o primeiro movimento na quarta célula com 4 pontos de movimento não utilizados. Esses pontos perdidos são adicionados aos custos da quinta célula, de modo que sua distância se torna 29, e não 25. Como resultado, as distâncias são maiores do que antes. Por exemplo, a décima célula tinha uma distância de 50. Mas agora, para entrar nela, precisamos cruzar as fronteiras de dois movimentos, perdendo 8 pontos de movimento, ou seja, a distância até agora se torna 58.Mais do que o esperadoComo os pontos de movimento não utilizados são adicionados às distâncias das células, eles são levados em consideração na determinação do caminho mais curto. A maneira mais eficaz é desperdiçar o mínimo de pontos possível. Portanto, em velocidades diferentes, podemos obter caminhos diferentes.Mostrando movimentos em vez de distâncias
Quando jogamos o jogo, não estamos muito interessados nos valores de distância usados para encontrar o caminho mais curto. Estamos interessados no número de movimentos necessários para alcançar o ponto final. Portanto, em vez de distâncias, vamos exibir os movimentos.Primeiro, livre-se da UpdateDistanceLabel
ligação dele HexCell
. public int Distance { get { return distance; } set { distance = value;
Em vez disso, adicionaremos ao HexCell
método geral SetLabel
que recebe uma string arbitrária. public void SetLabel (string text) { UnityEngine.UI.Text label = uiRect.GetComponent<Text>(); label.text = text; }
Usamos esse novo método na HexGrid.Search
limpeza de células. Para ocultar células, basta atribuí-las null
. for (int i = 0; i < cells.Length; i++) { cells[i].Distance = int.MaxValue; cells[i].SetLabel(null); cells[i].DisableHighlight(); }
Em seguida, atribuímos à marca do vizinho o valor de sua jogada. Depois disso, poderemos ver quantos movimentos adicionais serão necessários para percorrer todo o caminho. if (neighbor.Distance == int.MaxValue) { neighbor.Distance = distance; neighbor.SetLabel(turn.ToString()); neighbor.PathFrom = current; neighbor.SearchHeuristic = neighbor.coordinates.DistanceTo(toCell.coordinates); searchFrontier.Enqueue(neighbor); } else if (distance < neighbor.Distance) { int oldPriority = neighbor.SearchPriority; neighbor.Distance = distance; neighbor.SetLabel(turn.ToString()); neighbor.PathFrom = current; searchFrontier.Change(neighbor, oldPriority); }
O número de movimentos necessários para percorrer o caminho dounitypackageCaminhos instantâneos
Além disso, quando jogamos o jogo, não nos importamos como o algoritmo de pesquisa de caminho encontra o caminho. Queremos ver o caminho solicitado imediatamente. No momento, podemos ter certeza de que o algoritmo funciona, então vamos nos livrar da visualização de pesquisa.Sem corutin
Para uma passagem lenta pelo algoritmo, usamos corutin. Mais disto que precisamos, então se livrar da chamada StartCoroutine
e StopAllCoroutines
no HexGrid
. Em vez disso, simplesmente o invocamos Search
como um método regular. public void Load (BinaryReader reader, int header) {
Como não a usamos mais Search
como corotina, ela não precisa de rendimento, portanto, nos livraremos desse operador. Isso significa que também removeremos a declaração WaitForSeconds
e alteraremos o tipo de retorno do método para void
. void Search (HexCell fromCell, HexCell toCell, int speed) { …
Resultados instantâneosDefinição do tempo de pesquisa
Agora podemos encontrar os caminhos instantaneamente, mas com que rapidez eles são calculados? Caminhos curtos aparecem quase imediatamente, mas caminhos longos em mapas grandes podem parecer um pouco lentos.Vamos medir quanto tempo leva para encontrar e exibir o caminho. Podemos usar um criador de perfil para determinar o tempo de pesquisa, mas isso é um pouco demais e cria custos adicionais. Vamos usar em vez disso Stopwatch
, que está no espaço para nome System.Diagnostics
. Como o usamos apenas temporariamente, não adicionarei a construção using
ao início do script.Logo antes da pesquisa, crie um novo cronômetro e inicie-o. Após a conclusão da pesquisa, pare o cronômetro e exiba o tempo decorrido no console. public void FindPath (HexCell fromCell, HexCell toCell, int speed) { System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch(); sw.Start(); Search(fromCell, toCell, speed); sw.Stop(); Debug.Log(sw.ElapsedMilliseconds); }
Vamos escolher o pior caso para o nosso algoritmo - uma pesquisa do canto inferior esquerdo ao canto superior direito de um mapa grande. O pior é um mapa uniforme, porque o algoritmo terá que processar todas as 4.800 células do mapa.Pesquise no pior dos casos O tempogasto na pesquisa pode ser diferente, porque o editor do Unity não é o único processo em execução na sua máquina. Portanto, teste-o várias vezes para entender a duração média. No meu caso, a pesquisa leva cerca de 45 milissegundos. Isso não é muito e corresponde a 22,22 caminhos por segundo; denote isso como 22 pps (caminhos por segundo). Isso significa que a taxa de quadros do jogo também diminuirá em um máximo de 22 qps nesse quadro quando esse caminho for calculado. E isso sem levar em consideração todos os outros trabalhos, por exemplo, renderizar o próprio quadro. Ou seja, temos uma diminuição bastante grande na taxa de quadros, ela cairá para 20 fps.Ao executar esse teste de desempenho, é necessário considerar que o desempenho do editor do Unity não será tão alto quanto o desempenho do aplicativo finalizado. Se eu realizar o mesmo teste com a montagem, em média, serão necessários apenas 15 ms. São 66 pps, o que é muito melhor. No entanto, essa ainda é uma grande parte dos recursos alocados por quadro, portanto a taxa de quadros ficará menor que 60 fps.Onde posso ver o log de depuração do assembly?Unity , . . , , Unity
Log Files .
Pesquise apenas se necessário.
Podemos fazer uma otimização simples - realizar uma pesquisa somente quando necessário. Enquanto iniciamos uma nova pesquisa em cada quadro em que o botão do mouse é pressionado. Portanto, a taxa de quadros será constantemente subestimada ao arrastar e soltar. Podemos evitar isso iniciando uma nova pesquisa HexMapEditor.HandleInput
apenas quando estamos realmente lidando com um novo ponto de extremidade. Caso contrário, o caminho visível atual ainda é válido. if (editMode) { EditCells(currentCell); } else if ( Input.GetKey(KeyCode.LeftShift) && searchToCell != currentCell ) { if (searchFromCell != currentCell) { if (searchFromCell) { searchFromCell.DisableHighlight(); } searchFromCell = currentCell; searchFromCell.EnableHighlight(Color.blue); if (searchToCell) { hexGrid.FindPath(searchFromCell, searchToCell, 24); } } } else if (searchFromCell && searchFromCell != currentCell) { if (searchToCell != currentCell) { searchToCell = currentCell; hexGrid.FindPath(searchFromCell, searchToCell, 24); } }
Mostrar rótulos apenas para o caminho
Exibir marcas de viagem é uma operação bastante cara, principalmente porque usamos uma abordagem não otimizada. Executar esta operação para todas as células definitivamente atrasará a execução. Então, vamos pular a marcação HexGrid.Search
. if (neighbor.Distance == int.MaxValue) { neighbor.Distance = distance;
Precisamos ver essas informações apenas para o caminho encontrado. Portanto, depois de atingir o ponto final, calcularemos o curso e definiremos os rótulos apenas das células que estão a caminho. if (current == toCell) { current = current.PathFrom; while (current != fromCell) { int turn = current.Distance / speed; current.SetLabel(turn.ToString()); current.EnableHighlight(Color.white); current = current.PathFrom; } break; }
Exibindo rótulos apenas para células de caminhoAgora, incluímos apenas rótulos de células entre o início e o fim. Mas o ponto final é a coisa mais importante, também devemos definir um rótulo para ele. Você pode fazer isso iniciando o ciclo do caminho a partir da célula de destino e não da célula na frente dela. Nesse caso, a iluminação do ponto final do vermelho mudará para branco, então removeremos a luz de fundo do ciclo. fromCell.EnableHighlight(Color.blue);
As informações de progresso são mais importantes para o terminal.Depoisdessas alterações, o pior caso é reduzido para 23 milissegundos no editor e para 6 milissegundos na montagem finalizada. Estes são 43 pps e 166 pps - muito melhor.unitypackageA pesquisa mais inteligente
Na parte anterior, tornamos o procedimento de pesquisa mais inteligente, implementando o algoritmo A * . No entanto, na realidade, ainda não estamos realizando a pesquisa da maneira mais ideal. Em cada iteração, calculamos as distâncias da célula atual para todos os seus vizinhos. Isso é verdade para células que ainda não fazem parte da borda de pesquisa. Mas as células que já foram removidas da borda não precisam mais ser consideradas, porque já encontramos o caminho mais curto para essas células. A implementação correta de A * ignora essas células, para que possamos fazer o mesmo.Fase de Pesquisa Celular
Como sabemos se uma célula já saiu da fronteira? Enquanto não podemos determinar isso. Portanto, você precisa acompanhar em que fase da pesquisa a célula está. Ela ainda não estava na fronteira, ou está agora ou está no exterior. Podemos acompanhar isso adicionando a uma HexCell
propriedade inteira simples. public int SearchPhase { get; set; }
Por exemplo, 0 significa que as células ainda não atingiram, 1 - que a célula está na borda agora e 2 - que já foi removida da borda.Atingindo a fronteira
Em HexGrid.Search
podemos redefinir todas as células para 0 e sempre usar 1 para a borda. Ou podemos aumentar o número de bordas a cada nova pesquisa. Graças a isso, não teremos que lidar com o despejo de células se aumentarmos o número de fronteiras a cada duas vezes. int searchFrontierPhase; … void Search (HexCell fromCell, HexCell toCell, int speed) { searchFrontierPhase += 2; … }
Agora precisamos definir a fase da pesquisa de células ao adicioná-las à borda. O processo começa com uma célula inicial, que é adicionada à borda. fromCell.SearchPhase = searchFrontierPhase; fromCell.Distance = 0; searchFrontier.Enqueue(fromCell);
E também toda vez que adicionamos um vizinho à fronteira. if (neighbor.Distance == int.MaxValue) { neighbor.SearchPhase = searchFrontierPhase; neighbor.Distance = distance; neighbor.PathFrom = current; neighbor.SearchHeuristic = neighbor.coordinates.DistanceTo(toCell.coordinates); searchFrontier.Enqueue(neighbor); }
Verificação de fronteira
Até agora, para verificar se a célula ainda não foi adicionada à borda, usamos uma distância igual a int.MaxValue
. Agora podemos comparar a fase da pesquisa de células com a borda atual.
Isso significa que não precisamos mais redefinir as distâncias das células antes de pesquisar, ou seja, teremos que fazer menos trabalho, o que é bom. for (int i = 0; i < cells.Length; i++) {
Saindo da fronteira
Quando uma célula é removida do limite, denotamos isso por um aumento em sua fase de pesquisa. Isso a coloca além da fronteira atual e antes da próxima. while (searchFrontier.Count > 0) { HexCell current = searchFrontier.Dequeue(); current.SearchPhase += 1; … }
Agora podemos pular células removidas da borda, evitando cálculos inúteis e comparações de distâncias. for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = current.GetNeighbor(d); if ( neighbor == null || neighbor.SearchPhase > searchFrontierPhase ) { continue; } … }
Neste ponto, nosso algoritmo ainda produz os mesmos resultados, mas com mais eficiência. Na minha máquina, a pesquisa de pior caso leva 20 ms no editor e 5 ms na montagem.Também podemos calcular quantas vezes a célula foi processada pelo algoritmo, aumentando o contador ao calcular a distância até a célula. Anteriormente, nosso algoritmo, na pior das hipóteses, calculava 28.239 distâncias. No algoritmo A * pronto, calculamos suas 14.120 distâncias. A quantidade diminuiu 50%. O grau de impacto desses indicadores na produtividade depende dos custos no cálculo do custo da mudança. No nosso caso, não há muito trabalho aqui, portanto, a melhoria na montagem não é muito grande, mas é muito perceptível no editor.unitypackageAbrindo caminho
Ao iniciar uma nova pesquisa, primeiro precisamos limpar a visualização do caminho anterior. Enquanto fazemos isso, desative a seleção e remova os rótulos de cada célula da grade. Esta é uma abordagem muito difícil. Idealmente, precisamos descartar apenas as células que faziam parte do caminho anterior.Somente Pesquisa
Vamos começar removendo completamente o código de visualização de Search
. Ele só precisa realizar uma pesquisa de caminho e não precisa saber o que faremos com essas informações. void Search (HexCell fromCell, HexCell toCell, int speed) { searchFrontierPhase += 2; if (searchFrontier == null) { searchFrontier = new HexCellPriorityQueue(); } else { searchFrontier.Clear(); }
Para relatar que Search
encontramos uma maneira, retornaremos booleano. bool Search (HexCell fromCell, HexCell toCell, int speed) { searchFrontierPhase += 2; if (searchFrontier == null) { searchFrontier = new HexCellPriorityQueue(); } else { searchFrontier.Clear(); } fromCell.SearchPhase = searchFrontierPhase; fromCell.Distance = 0; searchFrontier.Enqueue(fromCell); while (searchFrontier.Count > 0) { HexCell current = searchFrontier.Dequeue(); current.SearchPhase += 1; if (current == toCell) { return true; } … } return false; }
Lembre-se do caminho
Quando o caminho é encontrado, precisamos nos lembrar dele. Graças a isso, poderemos limpá-lo no futuro. Portanto, rastrearemos os pontos finais e se existe um caminho entre eles. HexCell currentPathFrom, currentPathTo; bool currentPathExists; … public void FindPath (HexCell fromCell, HexCell toCell, int speed) { System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch(); sw.Start(); currentPathFrom = fromCell; currentPathTo = toCell; currentPathExists = Search(fromCell, toCell, speed); sw.Stop(); Debug.Log(sw.ElapsedMilliseconds); }
Exibir o caminho novamente
Podemos usar os dados de pesquisa que gravamos para visualizar o caminho novamente. Vamos criar um novo método para isso ShowPath
. Ele percorrerá o ciclo do final ao início do caminho, destacando as células e atribuindo um valor de traçado aos seus rótulos. Para fazer isso, precisamos saber a velocidade, portanto, torne-o um parâmetro. Se não temos um caminho, o método simplesmente seleciona os pontos finais. void ShowPath (int speed) { if (currentPathExists) { HexCell current = currentPathTo; while (current != currentPathFrom) { int turn = current.Distance / speed; current.SetLabel(turn.ToString()); current.EnableHighlight(Color.white); current = current.PathFrom; } } currentPathFrom.EnableHighlight(Color.blue); currentPathTo.EnableHighlight(Color.red); }
Chame esse método FindPath
após a pesquisa. currentPathExists = Search(fromCell, toCell, speed); ShowPath(speed);
Varrer
Nós vemos o caminho novamente, mas agora ele não está se afastando. Para limpá-lo, crie um método ClearPath
. De fato, é uma cópia ShowPath
, exceto que desativa a seleção e os rótulos, mas não os inclui. Feito isso, ele deve limpar os dados do caminho gravado que não são mais válidos. void ClearPath () { if (currentPathExists) { HexCell current = currentPathTo; while (current != currentPathFrom) { current.SetLabel(null); current.DisableHighlight(); current = current.PathFrom; } current.DisableHighlight(); currentPathExists = false; } currentPathFrom = currentPathTo = null; }
Usando esse método, podemos limpar a visualização do caminho antigo, visitando apenas as células necessárias; o tamanho do mapa não é mais importante. Vamos chamá-lo FindPath
antes de iniciar uma nova pesquisa. sw.Start(); ClearPath(); currentPathFrom = fromCell; currentPathTo = toCell; currentPathExists = Search(fromCell, toCell, speed); if (currentPathExists) { ShowPath(speed); } sw.Stop();
Além disso, limparemos o caminho ao criar um novo mapa. public bool CreateMap (int x, int z) { … ClearPath(); if (chunks != null) { for (int i = 0; i < chunks.Length; i++) { Destroy(chunks[i].gameObject); } } … }
E também antes de carregar outro cartão. public void Load (BinaryReader reader, int header) { ClearPath(); … }
A visualização do caminho é limpa novamente, como antes dessa alteração. Mas agora estamos usando uma abordagem mais eficiente e, no pior caso da pesquisa, o tempo diminuiu para 14 milissegundos. Bastante melhoria séria apenas devido à limpeza mais inteligente. O tempo de montagem diminuiu para 3 ms, ou seja, 333 pps. Graças a isso, a pesquisa de caminhos é exatamente aplicável em tempo real.Agora que fizemos uma pesquisa rápida por caminhos, podemos remover o código de depuração temporário. public void FindPath (HexCell fromCell, HexCell toCell, int speed) {
unitypackageParte 18: unidades
- Colocamos os esquadrões no mapa.
- Salve e carregue esquadrões.
- Nós encontramos caminhos para as tropas.
- Nós movemos as unidades.
Agora que descobrimos como procurar um caminho, vamos colocar os esquadrões no mapa.Chegaram reforçosCriando esquadrões
Até agora, lidamos apenas com células e seus objetos fixos. As unidades diferem deles por serem móveis. Um esquadrão pode significar qualquer coisa de qualquer escala, de uma pessoa ou veículo a um exército inteiro. Neste tutorial, nos restringimos a um tipo simples e generalizado de unidade. Depois disso, passaremos a apoiar combinações de vários tipos de unidades.Esquadrão de pré-fabricados
Para trabalhar com esquadrões, crie um novo tipo de componente HexUnit
. Por enquanto, vamos começar com um vazio MonoBehaviour
e, posteriormente, adicionar funcionalidade a ele. using UnityEngine; public class HexUnit : MonoBehaviour { }
Crie um objeto de jogo vazio com este componente, que deve se tornar uma pré-fabricada. Este será o objeto raiz do esquadrão.Esquadrão de pré-fabricados.Adicione um modelo 3D simbolizando o desapego como um objeto filho. Usei um cubo em escala simples para o qual criei material azul. O objeto raiz determina o nível do solo do desapego; portanto, substituímos o elemento filho.Elemento do cubo filhoAdicione um colisor ao esquadrão para facilitar a seleção no futuro. O colisor do cubo padrão é bastante adequado para nós, basta fazer o colisor caber em uma célula.Criando instâncias de esquadrão
Como ainda não temos jogabilidade, a criação de unidades ocorre no modo de edição. Portanto, isso deve ser resolvido HexMapEditor
. Para fazer isso, ele precisa de uma pré-fabricada, então adicione um campo HexUnit unitPrefab
e conecte-o. public HexUnit unitPrefab;
Conectando a pré-fabricadaAo criar unidades, as colocaremos na célula abaixo do cursor. Existe HandleInput
um código para encontrar essa célula ao editar um terreno. Agora também precisamos dele para os esquadrões, para que movamos o código correspondente para um método separado. HexCell GetCellUnderCursor () { Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(inputRay, out hit)) { return hexGrid.GetCell(hit.point); } return null; }
Agora podemos usar esse método para HandleInput
simplificá-lo. void HandleInput () {
Em seguida, adicione um novo método CreateUnit
que também use GetCellUnderCursor
. Se houver uma célula, criaremos um novo esquadrão. void CreateUnit () { HexCell cell = GetCellUnderCursor(); if (cell) { Instantiate(unitPrefab); } }
Para manter a hierarquia limpa, vamos usar a grade como pai de todos os objetos do jogo nos esquadrões. void CreateUnit () { HexCell cell = GetCellUnderCursor(); if (cell) { HexUnit unit = Instantiate(unitPrefab); unit.transform.SetParent(hexGrid.transform, false); } }
A maneira mais fácil de adicionar HexMapEditor
suporte à criação de unidades é pressionando uma tecla. Altere o método Update
para que ele chame CreateUnit
quando você pressiona a tecla U. Assim como em c HandleInput
, isso deve acontecer se o cursor não estiver sobre o elemento GUI. Primeiro, verificaremos se devemos editar o mapa e, se não, verificaremos se devemos adicionar um esquadrão. Se sim, então ligue CreateUnit
. void Update () {
Instância criada do esquadrãoColocação de tropas
Agora podemos criar unidades, mas elas aparecem na origem do mapa. Precisamos colocá-los no lugar certo. Para isso, é necessário que as tropas estejam cientes de sua posição. Portanto, adicionamos à HexUnit
propriedade Location
denotando a célula que eles ocupam. Ao definir a propriedade, alteraremos a posição do esquadrão para que ele corresponda à posição da célula. public HexCell Location { get { return location; } set { location = value; transform.localPosition = value.Position; } } HexCell location;
Agora devo HexMapEditor.CreateUnit
atribuir a posição da célula de esquadrão sob o cursor. Então as unidades estarão onde deveriam. void CreateUnit () { HexCell cell = GetCellUnderCursor(); if (cell) { HexUnit unit = Instantiate(unitPrefab); unit.transform.SetParent(hexGrid.transform, false); unit.Location = cell; } }
Esquadrões no mapaOrientação da unidade
Até agora, todas as unidades têm a mesma orientação, o que parece pouco natural. Para apimentar as coisas, aumentando a HexUnit
propriedade Orientation
. Este é um valor flutuante que indica a rotação do esquadrão ao longo do eixo Y em graus. Ao configurá-lo, alteraremos a rotação do próprio objeto do jogo. public float Orientation { get { return orientation; } set { orientation = value; transform.localRotation = Quaternion.Euler(0f, value, 0f); } } float orientation;
Em HexMapEditor.CreateUnit
atribuir uma rotação aleatória de 0 a 360 graus. void CreateUnit () { HexCell cell = GetCellUnderCursor(); if (cell) { HexUnit unit = Instantiate(unitPrefab); unit.transform.SetParent(hexGrid.transform, false); unit.Location = cell; unit.Orientation = Random.Range(0f, 360f); } }
Diferentes orientações da unidadeUm esquadrão por célula
As unidades ficam bem se não forem criadas em uma célula. Nesse caso, temos um conjunto de cubos de aparência estranha.Unidades sobrepostasAlguns jogos permitem a colocação de várias unidades em um só lugar, outros não. Como é mais fácil trabalhar com um esquadrão por célula, escolherei essa opção. Isso significa que devemos criar um novo esquadrão apenas quando a célula atual não estiver ocupada. Para que você possa descobrir, adicione à HexCell
propriedade padrão Unit
. public HexUnit Unit { get; set; }
Usamos essa propriedade HexUnit.Location
para informar à célula se a unidade está nela. public HexCell Location { get { return location; } set { location = value; value.Unit = this; transform.localPosition = value.Position; } }
Agora ele HexMapEditor.CreateUnit
pode verificar se a célula atual está livre. void CreateUnit () { HexCell cell = GetCellUnderCursor(); if (cell && !cell.Unit) { HexUnit unit = Instantiate(unitPrefab); unit.Location = cell; unit.Orientation = Random.Range(0f, 360f); } }
Editando células ocupadas
Inicialmente, as unidades são colocadas corretamente, mas tudo pode mudar se suas células forem editadas posteriormente. Se a altura da célula mudar, a unidade que a ocupar ficará pendurada acima dela ou mergulhará nela.Esquadrões pendurados e afogadosA solução é verificar a posição do esquadrão depois de fazer alterações. Para fazer isso, adicione o método a HexUnit
. Até agora, estamos interessados apenas na posição da equipe, então pergunte novamente. public void ValidateLocation () { transform.localPosition = location.Position; }
Devemos coordenar a posição do desapego ao atualizar a célula, o que acontece quando os métodos Refresh
ou RefreshSelfOnly
objetos são HexCell
chamados. Obviamente, isso é necessário apenas quando realmente existe um desapego na célula. void Refresh () { if (chunk) { chunk.Refresh(); … if (Unit) { Unit.ValidateLocation(); } } } void RefreshSelfOnly () { chunk.Refresh(); if (Unit) { Unit.ValidateLocation(); } }
Remoção de esquadrões
Além de criar unidades, seria útil destruí-las. Portanto, nós adicionamos o HexMapEditor
método DestroyUnit
. Ele deve verificar se há um desapego na célula sob o cursor e, se houver, destruir o objeto de jogo do desapego. void DestroyUnit () { HexCell cell = GetCellUnderCursor(); if (cell && cell.Unit) { Destroy(cell.Unit.gameObject); } }
Observe que, para chegar ao esquadrão, passamos pela célula. Para interagir com o esquadrão, basta passar o mouse sobre sua célula. Portanto, para que isso funcione, o esquadrão não precisa ter um colisor. No entanto, adicionar um colisor facilita a seleção porque bloqueia os raios que, de outra forma, colidiriam com a célula atrás do esquadrão.Vamos Update
usar uma combinação de Shift + U esquerdo para destruir o esquadrão . if (Input.GetKeyDown(KeyCode.U)) { if (Input.GetKey(KeyCode.LeftShift)) { DestroyUnit(); } else { CreateUnit(); } return; }
No caso em que criamos e destruímos várias unidades, tenha cuidado e limpe a propriedade ao remover a unidade. Ou seja, limpamos explicitamente o link da célula para o esquadrão. Adicione ao HexUnit
método Die
que lida com isso, bem como a destruição do seu próprio objeto de jogo. public void Die () { location.Unit = null; Destroy(gameObject); }
Vamos chamar esse método de HexMapEditor.DestroyUnit
, e não destruir o esquadrão diretamente. void DestroyUnit () { HexCell cell = GetCellUnderCursor(); if (cell && cell.Unit) {
unitypackageSalvando e carregando esquadrões
Agora que podemos ter unidades no mapa, precisamos incluí-las no processo de salvar e carregar. Podemos abordar essa tarefa de duas maneiras. O primeiro é gravar dados do esquadrão ao gravar uma célula, para que os dados da célula e do esquadrão sejam misturados. A segunda maneira é salvar os dados de célula e esquadrão separadamente. Embora possa parecer que a primeira abordagem seja mais fácil de implementar, a segunda nos fornece dados mais estruturados. Se compartilharmos os dados, será mais fácil trabalhar com eles no futuro.Rastreamento de unidade
Para manter todas as unidades juntas, precisamos rastreá-las. Faremos isso adicionando à HexGrid
lista de unidades. Esta lista deve conter todas as unidades no mapa. List<HexUnit> units = new List<HexUnit>();
Ao criar ou carregar um novo mapa, precisamos nos livrar de todas as unidades no mapa. Para simplificar esse processo, crie um método ClearUnits
que mate todos na lista e apague-o. void ClearUnits () { for (int i = 0; i < units.Count; i++) { units[i].Die(); } units.Clear(); }
Chamamos esse método dentro CreateMap
e dentro Load
. Vamos fazer isso depois de limpar o caminho. public bool CreateMap (int x, int z) { … ClearPath(); ClearUnits(); … } … public void Load (BinaryReader reader, int header) { ClearPath(); ClearUnits(); … }
Adicionando esquadrões à grade
Agora, ao criar novas unidades, precisamos adicioná-las à lista. Vamos definir um método para isso AddUnit
, que também irá lidar com a localização do esquadrão e os parâmetros do seu objeto pai. public void AddUnit (HexUnit unit, HexCell location, float orientation) { units.Add(unit); unit.transform.SetParent(transform, false); unit.Location = location; unit.Orientation = orientation; }
Agora HexMapEditor.CreatUnit
será suficiente chamar AddUnit
com uma nova instância do desapego, sua localização e orientação aleatória. void CreateUnit () { HexCell cell = GetCellUnderCursor(); if (cell && !cell.Unit) {
Removendo esquadrões da grade
Adicione um método para remover o esquadrão e c HexGrid
. Apenas remova o esquadrão da lista e ordene que ele morra. public void RemoveUnit (HexUnit unit) { units.Remove(unit); unit.Die(); }
Chamamos esse método de HexMapEditor.DestroyUnit
, em vez de destruir o esquadrão diretamente. void DestroyUnit () { HexCell cell = GetCellUnderCursor(); if (cell && cell.Unit) {
Salvando unidades
Como vamos manter todas as unidades juntas, precisamos lembrar quais células elas ocupam. A maneira mais confiável é salvar as coordenadas de sua localização. Para tornar isso possível, adicionamos os campos X e Z ao HexCoordinates
método Save
que o escreve. using UnityEngine; using System.IO; [System.Serializable] public struct HexCoordinates { … public void Save (BinaryWriter writer) { writer.Write(x); writer.Write(z); } }
O método Save
para HexUnit
agora pode gravar as coordenadas e a orientação do esquadrão. Esses são todos os dados das unidades que temos no momento. using UnityEngine; using System.IO; public class HexUnit : MonoBehaviour { … public void Save (BinaryWriter writer) { location.coordinates.Save(writer); writer.Write(orientation); } }
Como HexGrid
rastreia unidades, seu método Save
registra os dados das unidades. Primeiro, anote o número total de unidades e, em seguida, contorne todas elas em um loop. public void Save (BinaryWriter writer) { writer.Write(cellCountX); writer.Write(cellCountZ); for (int i = 0; i < cells.Length; i++) { cells[i].Save(writer); } writer.Write(units.Count); for (int i = 0; i < units.Count; i++) { units[i].Save(writer); } }
Como alteramos os dados armazenados, aumentamos o número da versão SaveLoadMenu.Save
para 2. O código de inicialização antigo ainda funcionará, porque simplesmente não lê os dados do esquadrão. No entanto, você precisa aumentar o número da versão para indicar que há informações da unidade no arquivo. void Save (string path) { using ( BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create)) ) { writer.Write(2); hexGrid.Save(writer); } }
Carregando esquadrões
Como HexCoordinates
é uma estrutura, não faz muito sentido adicionar o método usual a ela Load
. Vamos torná-lo um método estático que lê e retorna coordenadas armazenadas. public static HexCoordinates Load (BinaryReader reader) { HexCoordinates c; cx = reader.ReadInt32(); cz = reader.ReadInt32(); return c; }
Como o número de unidades é variável, não temos unidades pré-existentes nas quais os dados podem ser carregados. Podemos criar novas instâncias de unidades antes de carregar seus dados, mas isso exigirá a HexGrid
criação de instâncias de novas unidades no momento da inicialização. Então é melhor deixar isso HexUnit
. Também usamos o método estático HexUnit.Load
. Vamos começar simplesmente lendo esses esquadrões. Para ler o valor de orientações flutuador usar o método BinaryReader.ReadSingle
.Por que solteiro?float
, . , double
, . Unity .
public static void Load (BinaryReader reader) { HexCoordinates coordinates = HexCoordinates.Load(reader); float orientation = reader.ReadSingle(); }
O próximo passo é criar uma instância de um novo esquadrão. No entanto, para isso, precisamos de um link para a pré-fabricada da unidade. Para não complicar ainda, vamos adicionar um HexUnit
método estático para isso . public static HexUnit unitPrefab;
Para definir esse link, vamos usá-lo novamente HexGrid
, como fizemos com a textura do ruído. Quando precisarmos oferecer suporte a muitos tipos de unidades, seguiremos para uma solução melhor. public HexUnit unitPrefab; … void Awake () { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); HexUnit.unitPrefab = unitPrefab; CreateMap(cellCountX, cellCountZ); } … void OnEnable () { if (!HexMetrics.noiseSource) { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); HexUnit.unitPrefab = unitPrefab; } }
Passamos pela pré-fabricação da unidade.Após conectar o campo, não precisamos mais de um link direto para HexMapEditor
. Em vez disso, ele pode usar HexUnit.unitPrefab
.
Agora podemos criar uma instância do novo esquadrão HexUnit.Load
. Em vez de devolvê-lo, podemos usar as coordenadas e a orientação carregadas para adicioná-lo à grade. Para tornar isso possível, adicione um parâmetro HexGrid
. public static void Load (BinaryReader reader, HexGrid grid) { HexCoordinates coordinates = HexCoordinates.Load(reader); float orientation = reader.ReadSingle(); grid.AddUnit( Instantiate(unitPrefab), grid.GetCell(coordinates), orientation ); }
No final, HexGrid.Load
contamos o número de unidades e o usamos para carregar todas as unidades armazenadas, passando-nos como um argumento adicional. public void Load (BinaryReader reader, int header) { … int unitCount = reader.ReadInt32(); for (int i = 0; i < unitCount; i++) { HexUnit.Load(reader, this); } }
Obviamente, isso funcionará apenas para salvar arquivos com versão não inferior a 2, nas versões mais novas não há unidades para carregar. if (header >= 2) { int unitCount = reader.ReadInt32(); for (int i = 0; i < unitCount; i++) { HexUnit.Load(reader, this); } }
Agora, podemos fazer o upload correto dos arquivos da versão 2, SaveLoadMenu.Load
aumentando o número da versão suportada para 2. void Load (string path) { if (!File.Exists(path)) { Debug.LogError("File does not exist " + path); return; } using (BinaryReader reader = new BinaryReader(File.OpenRead(path))) { int header = reader.ReadInt32(); if (header <= 2) { hexGrid.Load(reader, header); HexMapCamera.ValidatePosition(); } else { Debug.LogWarning("Unknown map format " + header); } } }
unitypackageMovimento de Tropa
Os esquadrões são móveis, por isso devemos ser capazes de movê-los pelo mapa. Já temos um código de busca de caminho, mas até agora o testamos apenas para locais arbitrários. Agora precisamos remover a interface do usuário de teste antiga e criar uma nova interface para o gerenciamento de esquadrão.Limpeza do Editor de Mapas
Mover unidades ao longo de caminhos faz parte da jogabilidade, não se aplica ao editor de mapas. Portanto, nos livraremos HexMapEditor
de todo o código associado à localização do caminho.
Depois de remover esse código, não faz mais sentido deixar o editor ativo quando não estamos no modo de edição. Portanto, em vez de um campo de rastreamento de modo, podemos simplesmente ativar ou desativar o componente HexMapEditor
. Além disso, o editor agora não precisa lidar com os rótulos da interface do usuário.
Como, por padrão, não estamos no modo de edição de mapas, no Desperto, desativaremos o editor. void Awake () { terrainMaterial.DisableKeyword("GRID_ON"); SetEditMode(false); }
Use o raycast para procurar a célula atual sob o cursor e necessário ao editar o mapa e gerenciar unidades. Talvez no futuro nos seja útil para outra coisa. Vamos passar a lógica do raycasting de HexGrid
para um novo método GetCell
com um parâmetro de feixe. public HexCell GetCell (Ray ray) { RaycastHit hit; if (Physics.Raycast(ray, out hit)) { return GetCell(hit.point); } return null; }
HexMapEditor.GetCellUniderCursor
pode apenas chamar esse método com o feixe do cursor. HexCell GetCellUnderCursor () { return hexGrid.GetCell(Camera.main.ScreenPointToRay(Input.mousePosition)); }
UI do jogo
Para controlar a interface do usuário do modo de jogo, usaremos um novo componente. Enquanto ele só vai lidar com a seleção e movimentação de unidades. Crie um novo tipo de componente para ele HexGameUI
. Para fazer seu trabalho, basta um link para a rede. using UnityEngine; using UnityEngine.EventSystems; public class HexGameUI : MonoBehaviour { public HexGrid grid; }
Adicione este componente ao novo objeto de jogo na hierarquia da interface do usuário. Ele não precisa ter seu próprio objeto, mas será óbvio para nós que existe uma interface do usuário separada para o jogo.Objeto de interface do jogo do jogoAdicione um HexGameUI
método SetEditMode
, como em HexMapEditor
. A interface do jogo deve ser ativada quando não estamos no modo de edição. Além disso, os rótulos precisam ser incluídos aqui, porque a interface do usuário do jogo funciona com caminhos. public void SetEditMode (bool toggle) { enabled = !toggle; grid.ShowUI(!toggle); }
Adicione o método da interface do jogo à lista de eventos da opção do modo de edição. Isso significa que quando o jogador muda de modo, os dois métodos são chamados.Vários métodos de eventos.Rastrear célula atual
Dependendo da situação, HexGameUI
você precisa saber qual célula está atualmente sob o cursor. Portanto, adicionamos um campo a ele currentCell
. HexCell currentCell;
Crie um método UpdateCurrentCell
que use a HexGrid.GetCell
viga do cursor para atualizar este campo. void UpdateCurrentCell () { currentCell = grid.GetCell(Camera.main.ScreenPointToRay(Input.mousePosition)); }
Ao atualizar a célula atual, podemos precisar descobrir se ela mudou. Forçar a UpdateCurrentCell
retornar essas informações. bool UpdateCurrentCell () { HexCell cell = grid.GetCell(Camera.main.ScreenPointToRay(Input.mousePosition)); if (cell != currentCell) { currentCell = cell; return true; } return false; }
Seleção de Unidade
Antes de mover um esquadrão, ele deve ser selecionado e rastreado. Portanto, adicione um campo selectedUnit
. HexUnit selectedUnit;
Quando tentamos fazer uma seleção, precisamos começar atualizando a célula atual. Se a célula atual for, a unidade que ocupa essa célula se torna a unidade selecionada. Se não houver nenhuma unidade na célula, nenhuma unidade será selecionada. Vamos criar um método para isso DoSelection
. void DoSelection () { UpdateCurrentCell(); if (currentCell) { selectedUnit = currentCell.Unit; } }
Percebemos a escolha das unidades com um simples clique do mouse. Portanto, adicionamos um método Update
que faz uma seleção quando o botão do mouse é ativado.Claro, precisamos executá-lo apenas quando o cursor não está acima do elemento GUI. void Update () { if (!EventSystem.current.IsPointerOverGameObject()) { if (Input.GetMouseButtonDown(0)) { DoSelection(); } } }
Nesta fase, aprendemos como selecionar uma unidade de cada vez com o clique de um mouse. Quando você clica em uma célula vazia, a seleção de qualquer unidade é removida. Mas enquanto não recebemos nenhuma confirmação visual disso.Pesquisa de esquadrão
Quando uma unidade é selecionada, podemos usar sua localização como ponto de partida para encontrar um caminho. Para ativar isso, não será necessário outro clique no botão do mouse. Em vez disso, encontraremos e mostraremos automaticamente o caminho entre a posição do esquadrão e a célula atual. Sempre faremos isso Update
, exceto quando a escolha for feita. Para fazer isso, quando temos um plantel, chamamos o método DoPathfinding
. void Update () { if (!EventSystem.current.IsPointerOverGameObject()) { if (Input.GetMouseButtonDown(0)) { DoSelection(); } else if (selectedUnit) { DoPathfinding(); } } }
DoPathfinding
apenas atualiza a célula atual e chama HexGrid.FindPath
se houver um ponto de extremidade. Novamente, usamos uma velocidade constante de 24. void DoPathfinding () { UpdateCurrentCell(); grid.FindPath(selectedUnit.Location, currentCell, 24); }
Observe que não devemos encontrar um novo caminho toda vez que atualizamos, mas somente quando a célula atual é alterada. void DoPathfinding () { if (UpdateCurrentCell()) { grid.FindPath(selectedUnit.Location, currentCell, 24); } }
Localizando um caminho para um esquadrãoAgora, vemos os caminhos que aparecem quando você move o cursor após selecionar um esquadrão. Graças a isso, é óbvio qual unidade está selecionada. No entanto, os caminhos nem sempre são limpos corretamente. Primeiro, vamos limpar o caminho antigo se o cursor estiver fora do mapa. void DoPathfinding () { if (UpdateCurrentCell()) { if (currentCell) { grid.FindPath(selectedUnit.Location, currentCell, 24); } else { grid.ClearPath(); } } }
Obviamente, isso exige que HexGrid.ClearPath
seja comum, então fazemos essa alteração. public void ClearPath () { … }
Em segundo lugar, limparemos o caminho antigo ao escolher um desapego. void DoSelection () { grid.ClearPath(); UpdateCurrentCell(); if (currentCell) { selectedUnit = currentCell.Unit; } }
Por fim, limparemos o caminho ao alterar o modo de edição. public void SetEditMode (bool toggle) { enabled = !toggle; grid.ShowUI(!toggle); grid.ClearPath(); }
Procure apenas pontos de extremidade válidos
Nem sempre podemos encontrar o caminho, porque às vezes é impossível chegar à célula final. Isso é normal. Mas às vezes a própria célula final é inaceitável. Por exemplo, decidimos que os caminhos não podem incluir células subaquáticas. Mas isso pode depender da unidade. Vamos adicionar a um HexUnit
método que nos diz se uma célula é um ponto de extremidade válido. As células subaquáticas não são. public bool IsValidDestination (HexCell cell) { return !cell.IsUnderwater; }
Além disso, permitimos que apenas uma unidade permanecesse na célula. Portanto, a célula final não será válida se estiver ocupada. public bool IsValidDestination (HexCell cell) { return !cell.IsUnderwater && !cell.Unit; }
Usamos esse método HexGameUI.DoPathfinding
para ignorar pontos de extremidade inválidos. void DoPathfinding () { if (UpdateCurrentCell()) { if (currentCell && selectedUnit.IsValidDestination(currentCell)) { grid.FindPath(selectedUnit.Location, currentCell, 24); } else { grid.ClearPath(); } } }
Mover para o ponto final
Se tivermos um caminho válido, poderemos mover o esquadrão para o ponto final. HexGrid
sabe quando isso pode ser feito. Fazemos passar essas informações em uma nova propriedade somente leitura HasPath
. public bool HasPath { get { return currentPathExists; } }
Para mover um esquadrão, adicione ao HexGameUI
método DoMove
. Este método será chamado quando um comando for emitido e se uma unidade for selecionada. Portanto, ele deve verificar se existe uma maneira e, se houver, alterar a localização do destacamento. Enquanto teletransportamos imediatamente o esquadrão para o ponto final. Em um dos tutoriais a seguir, faremos com que o esquadrão vá até o fim. void DoMove () { if (grid.HasPath) { selectedUnit.Location = currentCell; grid.ClearPath(); } }
Vamos usar o botão 1 do mouse (clique direito) para enviar o comando. Verificaremos isso se um destacamento for selecionado. Se o botão não for pressionado, procuraremos o caminho. void Update () { if (!EventSystem.current.IsPointerOverGameObject()) { if (Input.GetMouseButtonDown(0)) { DoSelection(); } else if (selectedUnit) { if (Input.GetMouseButtonDown(1)) { DoMove(); } else { DoPathfinding(); } } } }
Agora podemos mover unidades! Mas, às vezes, eles se recusam a encontrar um caminho para algumas células. Em particular, naquelas células em que costumava estar o desapego. Isso acontece porque ele HexUnit
não atualiza o local antigo ao definir um novo. Para consertar isso, limparemos o link para o esquadrão em seu antigo local. public HexCell Location { get { return location; } set { if (location) { location.Unit = null; } location = value; value.Unit = this; transform.localPosition = value.Position; } }
Evite esquadrões
Encontrar o caminho agora funciona corretamente e as unidades podem se teletransportar no mapa. Embora eles não possam se mudar para células que já possuem um esquadrão, os destacamentos no caminho são ignorados.As unidades no caminho são ignoradas: asunidades da mesma facção geralmente podem se mover, mas até agora não temos facções. Portanto, vamos considerar todas as unidades desconectadas uma da outra e bloqueando os caminhos. Isso pode ser implementado pulando células ocupadas HexGrid.Search
. if ( neighbor == null || neighbor.SearchPhase > searchFrontierPhase ) { continue; } if (neighbor.IsUnderwater || neighbor.Unit) { continue; }
Evitar destacamentosunitypackageParte 19: Animação em Movimento
- Movemos as unidades entre as células.
- Visualize o caminho percorrido.
- Nós movemos as tropas ao longo das curvas.
- Nós forçamos as tropas a olhar na direção do movimento.
Nesta parte, forçaremos as unidades em vez do teletransporte a se moverem ao longo dos trilhos.Esquadrões a caminhoMovimento ao longo do caminho
Na parte anterior, adicionamos unidades e a capacidade de movê-las. Embora tenhamos usado a busca do caminho para determinar os pontos finais válidos, depois de dar o comando, as tropas simplesmente se teletransportaram para a célula final. Para realmente seguir o caminho encontrado, precisamos rastrear esse caminho e criar um processo de animação que force o esquadrão a se mover de célula em célula. Já que, olhando as animações, é difícil perceber como o esquadrão se moveu, também visualizamos o caminho percorrido com a ajuda de aparelhos. Mas antes de seguirmos em frente, precisamos corrigir o erro.Erro nas voltas
Devido a uma supervisão, calculamos incorretamente o curso em que a célula será atingida. Agora, determinamos o percurso dividindo a distância total pela velocidade do esquadrãot = d / s e descartando o restante. O erro ocorre quando, para entrar na célula, você precisa gastar exatamente todos os pontos de movimento restantes por movimento. Por exemplo, quando cada passo custa 1 e a velocidade é 3, podemos mover três células por turno. No entanto, com os cálculos existentes, só podemos dar dois passos no primeiro passo, porque no terceiro passot = d / s = 3 / 3 = 1 .
Os custos somados da movimentação com movimentos definidos incorretamente, velocidade 3Para o cálculo correto dos movimentos, precisamos mover a borda um passo a partir da célula inicial. Podemos fazer isso reduzindo a distância em 1 antes de calcular o movimento, e o movimento para o terceiro passo serát = 2 / 3 = 0Movimentos corretosPodemos fazer isso alterando a fórmula de cálculo parat = ( d - 1 ) / s .
Faremos essa alteração em HexGrid.Search
. bool Search (HexCell fromCell, HexCell toCell, int speed) { … while (searchFrontier.Count > 0) { … int currentTurn = (current.Distance - 1) / speed; for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { … int distance = current.Distance + moveCost; int turn = (distance - 1) / speed; if (turn > currentTurn) { distance = turn * speed + moveCost; } … } } return false; }
Também mudamos as marcas dos movimentos. void ShowPath (int speed) { if (currentPathExists) { HexCell current = currentPathTo; while (current != currentPathFrom) { int turn = (current.Distance - 1) / speed; … } } … }
Observe que, com essa abordagem, o caminho inicial da célula é -1. Isso é normal, porque não o exibimos e o algoritmo de busca permanece operacional.Ficando maneira
Movendo-se ao longo do caminho é a tarefa do esquadrão. Para ele fazer isso, ele precisa saber o caminho. Como temos essas informações HexGrid
, vamos adicionar um método para obter o caminho atual na forma de uma lista de células. Ele pode retirá-lo do pool de listas e retornar se realmente houver um caminho. public List<HexCell> GetPath () { if (!currentPathExists) { return null; } List<HexCell> path = ListPool<HexCell>.Get(); return path; }
A lista é preenchida seguindo o caminho do link da célula final até a inicial, como é feito ao visualizar o caminho. List<HexCell> path = ListPool<HexCell>.Get(); for (HexCell c = currentPathTo; c != currentPathFrom; c = c.PathFrom) { path.Add(c); } return path;
Nesse caso, precisamos de todo o caminho, que inclui a célula inicial. for (HexCell c = currentPathTo; c != currentPathFrom; c = c.PathFrom) { path.Add(c); } path.Add(currentPathFrom); return path;
Agora temos o caminho na ordem inversa. Podemos trabalhar com ele, mas não será muito intuitivo. Vamos virar a lista para que ela vá do começo ao fim. path.Add(currentPathFrom); path.Reverse(); return path;
Solicitação de movimento
Agora podemos adicionar ao HexUnit
método, ordenando que ele siga o caminho. Inicialmente, simplesmente deixamos que ele se teletransportasse para a célula final. Não retornaremos imediatamente a lista à piscina, porque ela será útil por um tempo. using UnityEngine; using System.Collections.Generic; using System.IO; public class HexUnit : MonoBehaviour { … public void Travel (List<HexCell> path) { Location = path[path.Count - 1]; } … }
Para solicitar movimento, alteramos HexGameUI.DoMove
para que ele chame um novo método com o caminho atual, e não apenas defina a localização da unidade. void DoMove () { if (grid.HasPath) {
Visualização de caminho
Antes de começarmos a animar o esquadrão, vamos verificar se os caminhos estão corretos. Faremos isso ordenando HexUnit
lembrar o caminho ao longo do qual ele deve se mover, para que possa ser visualizado usando dispositivos. List<HexCell> pathToTravel; … public void Travel (List<HexCell> path) { Location = path[path.Count - 1]; pathToTravel = path; }
Adicione um método OnDrawGizmos
para mostrar o último caminho a percorrer (se existir). Se a unidade ainda não se moveu, o caminho deve ser igual null
. Porém, devido à serialização do Unity durante a edição após a recompilação no modo Play, também pode ser uma lista vazia. void OnDrawGizmos () { if (pathToTravel == null || pathToTravel.Count == 0) { return; } }
A maneira mais fácil de mostrar o caminho é desenhar uma esfera de dispositivo para cada célula do caminho. Uma esfera com um raio de 2 unidades é adequada para nós. void OnDrawGizmos () { if (pathToTravel == null || pathToTravel.Count == 0) { return; } for (int i = 0; i < pathToTravel.Count; i++) { Gizmos.DrawSphere(pathToTravel[i].Position, 2f); } }
Como mostraremos os caminhos para o desapego, poderemos ver simultaneamente todos os seus últimos caminhos.Os aparelhos exibem os últimos caminhos percorridos.Paramostrar melhor as conexões das células, desenhamos várias esferas em um loop em uma linha entre as células anteriores e as atuais. Para fazer isso, precisamos iniciar o processo a partir da segunda célula. As esferas podem ser organizadas usando interpolação linear com um incremento de 0,1 unidades, para que possamos obter dez esferas por segmento. for (int i = 1; i < pathToTravel.Count; i++) { Vector3 a = pathToTravel[i - 1].Position; Vector3 b = pathToTravel[i].Position; for (float t = 0f; t < 1f; t += 0.1f) { Gizmos.DrawSphere(Vector3.Lerp(a, b, t), 2f); } }
Maneiras mais óbviasDeslize ao longo do caminho
Você pode usar o mesmo método para mover unidades. Vamos criar uma rotina para isso. Em vez de desenhar um dispositivo, definiremos a posição do esquadrão. Em vez de incrementar, usaremos 0,1 delta de tempo e executaremos o rendimento para cada iteração. Nesse caso, o esquadrão passará de uma célula para a próxima em um segundo. using UnityEngine; using System.Collections; using System.Collections.Generic; using System.IO; public class HexUnit : MonoBehaviour { … IEnumerator TravelPath () { for (int i = 1; i < pathToTravel.Count; i++) { Vector3 a = pathToTravel[i - 1].Position; Vector3 b = pathToTravel[i].Position; for (float t = 0f; t < 1f; t += Time.deltaTime) { transform.localPosition = Vector3.Lerp(a, b, t); yield return null; } } } … }
Vamos começar a rotina no final do método Travel
. Mas primeiro, pararemos todas as corotinas existentes. Portanto, garantimos que duas corotinas não serão iniciadas ao mesmo tempo, caso contrário, isso levaria a resultados muito estranhos. public void Travel (List<HexCell> path) { Location = path[path.Count - 1]; pathToTravel = path; StopAllCoroutines(); StartCoroutine(TravelPath()); }
Mover uma célula por segundo é bem lento. O jogador durante o jogo não vai querer esperar tanto tempo. Você pode fazer com que a velocidade de movimento do esquadrão seja uma opção de configuração, mas, por enquanto, vamos usar uma constante. Eu atribuí a ela um valor de 4 células por segundo; é bem rápido, mas vamos notar o que está acontecendo. const float travelSpeed = 4f; … IEnumerator TravelPath () { for (int i = 1; i < pathToTravel.Count; i++) { Vector3 a = pathToTravel[i - 1].Position; Vector3 b = pathToTravel[i].Position; for (float t = 0f; t < 1f; t += Time.deltaTime * travelSpeed) { transform.localPosition = Vector3.Lerp(a, b, t); yield return null; } } }
Assim como podemos visualizar vários caminhos simultaneamente, também podemos fazer com que várias unidades viajem ao mesmo tempo. Do ponto de vista do estado do jogo, o movimento ainda é o teletransporte, as animações são exclusivamente visuais. As unidades ocupam instantaneamente a célula final. Você pode até encontrar maneiras e iniciar uma nova jogada antes que elas cheguem. Nesse caso, eles são teleportados visualmente para o início de um novo caminho. Isso pode ser evitado através do bloqueio de unidades ou mesmo de toda a interface do usuário enquanto elas estão em movimento, mas uma reação tão rápida é bastante conveniente ao desenvolver e testar movimentos.Unidades móveis.E a diferença de altura?, . , . , . , . , Endless Legend, , . , .
Posição após a compilação
Uma das desvantagens do corutin é que eles não "sobrevivem" quando recompilados no modo Play. Embora o estado do jogo seja sempre verdadeiro, isso pode fazer com que os esquadrões fiquem presos em algum lugar do último caminho, se a recompilação for iniciada enquanto eles ainda estão em movimento. Para mitigar as consequências, vamos garantir que, após a recompilação, as unidades estejam sempre na posição correta. Isso pode ser feito atualizando sua posição em OnEnable
. void OnEnable () { if (location) { transform.localPosition = location.Position; } }
unitypackageMovimento suave
O movimento do centro para o centro da célula parece muito mecanicista e cria mudanças bruscas de direção. Para muitos jogos, isso será normal, mas inaceitável se você precisar de pelo menos um movimento levemente realista. Então, vamos mudar o movimento para torná-lo um pouco mais orgânico.Movendo-se de costela para costela
O esquadrão começa sua jornada a partir do centro da célula. Passa para o meio da borda da célula e entra na próxima célula. Em vez de se mover em direção ao centro, ele pode ir direto para a próxima borda que deve atravessar. De fato, a unidade cortará o caminho quando precisar mudar de direção. Isso é possível para todas as células, exceto os pontos finais do caminho.Três maneiras de se mover de ponta a pontaVamos nos adaptar OnDrawGizmos
para exibir os caminhos gerados dessa maneira. Ele deve interpolar entre as bordas das células, o que pode ser encontrado pela média das posições das células vizinhas. É o suficiente para calcular uma borda por iteração, reutilizando o valor da iteração anterior. Assim, podemos fazer o método funcionar para a célula inicial, mas, em vez da borda, assumimos sua posição. void OnDrawGizmos () { if (pathToTravel == null || pathToTravel.Count == 0) { return; } Vector3 a, b = pathToTravel[0].Position; for (int i = 1; i < pathToTravel.Count; i++) {
Para alcançar o centro da célula final, precisamos usar a posição da célula como o último ponto, não a aresta. Você pode adicionar a verificação desse caso ao loop, mas é um código tão simples que será mais óbvio duplicar o código e alterá-lo levemente. void OnDrawGizmos () { … for (int i = 1; i < pathToTravel.Count; i++) { … } a = b; b = pathToTravel[pathToTravel.Count - 1].Position; for (float t = 0f; t < 1f; t += 0.1f) { Gizmos.DrawSphere(Vector3.Lerp(a, b, t), 2f); } }
Caminhos baseados em nervuras Os caminhos resultantes são menos parecidos com ziguezagues e o ângulo de viragem máximo é reduzido de 120 ° para 90 °. Isso pode ser considerado uma melhoria; portanto, aplicamos as mesmas alterações na corotina TravelPath
para ver como fica a animação. IEnumerator TravelPath () { Vector3 a, b = pathToTravel[0].Position; for (int i = 1; i < pathToTravel.Count; i++) {
Movendo-se com uma velocidade variávelDepois de cortar os ângulos, o comprimento dos segmentos do caminho ficou dependente da mudança de direção. Mas ajustamos a velocidade nas células por segundo. Como resultado, a velocidade do destacamento muda aleatoriamente.Seguindo curvas
Mudanças instantâneas de direção e velocidade ao cruzar os limites das células parecem feias. Melhor usar uma mudança gradual de direção. Podemos acrescentar apoio forçando as tropas a seguirem curvas, em vez de linhas retas. Você pode usar curvas de Bezier para isso. Em particular, podemos fazer curvas quadráticas de Bezier nas quais o centro das células será o ponto de controle intermediário. Nesse caso, as tangentes das curvas adjacentes serão imagens em espelho uma da outra, ou seja, todo o caminho se transformará em uma curva suave contínua.Curvas de ponta a pontaCrie uma classe auxiliar Bezier
com um método para obter pontos em uma curva quadrática de Bezier. Conforme explicado no tutorial Curvas e splines , a fórmula é usada para este( 1 - t ) 2 A + 2 ( 1 - t ) t B + t 2 C onde
Um ,
B e
C são os pontos de controle e t é o interpolador. using UnityEngine; public static class Bezier { public static Vector3 GetPoint (Vector3 a, Vector3 b, Vector3 c, float t) { float r = 1f - t; return r * r * a + 2f * r * t * b + t * t * c; } }
O GetPoint não deve ser limitado a 0-1?0-1, . . , GetPointClamped
, t
. , GetPointUnclamped
.
Para mostrar o caminho da curva OnDrawGizmos
, precisamos rastrear não dois, mas três pontos. Um ponto adicional é o centro da célula com a qual estamos trabalhando na iteração atual, que possui um índice i - 1
, porque o ciclo começa com 1. Após receber todos os pontos, podemos substituí-lo Vector3.Lerp
por Bezier.GetPoint
.Nas células inicial e final, em vez dos pontos final e médio, podemos simplesmente usar o centro da célula. void OnDrawGizmos () { if (pathToTravel == null || pathToTravel.Count == 0) { return; } Vector3 a, b, c = pathToTravel[0].Position; for (int i = 1; i < pathToTravel.Count; i++) { a = c; b = pathToTravel[i - 1].Position; c = (b + pathToTravel[i].Position) * 0.5f; for (float t = 0f; t < 1f; t += Time.deltaTime * travelSpeed) { Gizmos.DrawSphere(Bezier.GetPoint(a, b, c, t), 2f); } } a = c; b = pathToTravel[pathToTravel.Count - 1].Position; c = b; for (float t = 0f; t < 1f; t += 0.1f) { Gizmos.DrawSphere(Bezier.GetPoint(a, b, c, t), 2f); } }
Caminhos criados usando curvas de Bezier Umcaminho curvo parece muito melhor. Aplicamos as mesmas alterações TravelPath
e vemos como as unidades são animadas com essa abordagem. IEnumerator TravelPath () { Vector3 a, b, c = pathToTravel[0].Position; for (int i = 1; i < pathToTravel.Count; i++) { a = c; b = pathToTravel[i - 1].Position; c = (b + pathToTravel[i].Position) * 0.5f; for (float t = 0f; t < 1f; t += Time.deltaTime * travelSpeed) { transform.localPosition = Bezier.GetPoint(a, b, c, t); yield return null; } } a = c; b = pathToTravel[pathToTravel.Count - 1].Position; c = b; for (float t = 0f; t < 1f; t += Time.deltaTime * travelSpeed) { transform.localPosition = Bezier.GetPoint(a, b, c, t); yield return null; } }
Nos movemos pelas curvas e aanimação também se tornou suave, mesmo quando a velocidade do desapego é instável. Como as tangentes da curva dos segmentos adjacentes coincidem, a velocidade é contínua. A mudança na velocidade ocorre gradualmente e acontece quando um desapego passa pela célula, diminuindo a velocidade ao mudar de direção. Se ele seguir em frente, a velocidade permanecerá constante. Além disso, o esquadrão começa e termina sua jornada na velocidade zero. Isso imita o movimento natural, então deixe assim.Rastreamento de tempo
Até esse ponto, começamos a iterar sobre cada um dos segmentos de 0, continuando até chegar a 1. Isso funciona bem ao aumentar por um valor constante, mas nossa iteração depende do delta do tempo. Quando a iteração em um segmento for concluída, é provável que exceda 1 em alguma quantidade, dependendo do tempo delta. Isso é invisível em altas taxas de quadros, mas pode levar a movimentos bruscos em baixas taxas de quadros.Para evitar a perda de tempo, precisamos transferir o tempo restante de um segmento para o próximo. Isso pode ser feito rastreando t
o caminho inteiro, e não apenas em cada segmento. Então, no final de cada segmento, subtrairemos 1 dele. IEnumerator TravelPath () { Vector3 a, b, c = pathToTravel[0].Position; float t = 0f; for (int i = 1; i < pathToTravel.Count; i++) { a = c; b = pathToTravel[i - 1].Position; c = (b + pathToTravel[i].Position) * 0.5f; for (; t < 1f; t += Time.deltaTime * travelSpeed) { transform.localPosition = Bezier.GetPoint(a, b, c, t); yield return null; } t -= 1f; } a = c; b = pathToTravel[pathToTravel.Count - 1].Position; c = b; for (; t < 1f; t += Time.deltaTime * traveSpeed) { transform.localPosition = Bezier.GetPoint(a, b, c, t); yield return null; } }
Se já estivermos fazendo isso, vamos garantir que o delta do tempo seja levado em consideração no início do caminho. Isso significa que começaremos a nos mover imediatamente e não ficaremos ociosos por um quadro. float t = Time.deltaTime * travelSpeed;
Além disso, não terminamos exatamente no momento em que o caminho deve terminar, mas momentos antes. Aqui, a diferença também pode depender da taxa de quadros. Portanto, vamos fazer com que o esquadrão complete o caminho exatamente no ponto final. IEnumerator TravelPath () { … transform.localPosition = location.Position; }
unitypackageAnimação de orientação
As unidades começaram a se mover ao longo de uma curva suave, mas não mudaram a orientação de acordo com a direção do movimento. Como resultado, eles parecem deslizar. Para fazer o movimento parecer um movimento real, precisamos rotacioná-los.Olhando para frente
Como no tutorial Curvas e splines , podemos usar a derivada da curva para determinar a orientação da unidade. A fórmula para a derivada de uma curva de Bezier quadrática:2 ( ( 1 - t ) ( B - A ) + t ( C - B ) ) .
Adicione ao Bezier
método para calculá-lo. public static Vector3 GetDerivative ( Vector3 a, Vector3 b, Vector3 c, float t ) { return 2f * ((1f - t) * (b - a) + t * (c - b)); }
O vetor derivado está localizado em uma linha reta com a direção do movimento. Podemos usar o método Quaternion.LookRotation
para convertê-lo em um turno de esquadrão. Vamos realizá-lo a cada passo HexUnit.TravelPath
. transform.localPosition = Bezier.GetPoint(a, b, c, t); Vector3 d = Bezier.GetDerivative(a, b, c, t); transform.localRotation = Quaternion.LookRotation(d); yield return null; … transform.localPosition = Bezier.GetPoint(a, b, c, t); Vector3 d = Bezier.GetDerivative(a, b, c, t); transform.localRotation = Quaternion.LookRotation(d); yield return null;
Não há erro no início do caminho?, . A e B , . , t=0 , , Quaternion.LookRotation
. , , t=0 . . , t>0 .
, t<1 .
Em contraste com a posição do desapego, a não idealidade de sua orientação no final do caminho não é importante. no entanto, precisamos garantir que sua orientação corresponda à rotação final. Para fazer isso, após a conclusão, equiparamos sua orientação à sua rotação em Y. transform.localPosition = location.Position; orientation = transform.localRotation.eulerAngles.y;
Agora, as unidades estão olhando exatamente na direção do movimento, horizontal e verticalmente. Isso significa que eles se inclinam para frente e para trás, descendo das encostas e subindo nelas. Para garantir que eles sempre permaneçam retos, forçamos o componente Y do vetor de direção a zero antes de usá-lo para determinar a rotação da unidade. Vector3 d = Bezier.GetDerivative(a, b, c, t); dy = 0f; transform.localRotation = Quaternion.LookRotation(d); … Vector3 d = Bezier.GetDerivative(a, b, c, t); dy = 0f; transform.localRotation = Quaternion.LookRotation(d);
Olhando para a frente enquanto se moveNós olhamos para o ponto
Ao longo do caminho, as unidades estão olhando para frente, mas antes de começar a se mover, podem olhar na outra direção. Nesse caso, eles instantaneamente mudam de orientação. Será melhor se eles virarem na direção do caminho antes do início do movimento.Olhar na direção certa pode ser útil em outras situações, então vamos criar um método LookAt
que force o esquadrão a mudar de orientação para observar um determinado ponto. A rotação necessária pode ser definida usando o método Transform.LookAt
, primeiro colocando o ponto na mesma posição vertical que o destacamento. Depois disso, podemos recuperar a orientação do esquadrão. void LookAt (Vector3 point) { point.y = transform.localPosition.y; transform.LookAt(point); orientation = transform.localRotation.eulerAngles.y; }
Para que o desapego realmente gire, transformaremos o método em outro corutin que o girará a uma velocidade constante. A velocidade de rotação também pode ser ajustada, mas usaremos a constante novamente. A rotação deve ser rápida, cerca de 180 ° por segundo. const float rotationSpeed = 180f; … IEnumerator LookAt (Vector3 point) { … }
Não é necessário mexer na aceleração do turno, porque é imperceptível. Basta que interpolemos entre as duas orientações. Infelizmente, isso não é tão simples como no caso de dois números, porque os ângulos são circulares. Por exemplo, uma transição de 350 ° para 10 ° deve resultar em uma rotação de 20 ° no sentido horário, mas uma interpolação simples forçará uma rotação de 340 ° no sentido anti-horário.A maneira mais fácil de criar uma rotação correta é interpolar entre dois quaternions usando interpolação esférica. Isso levará ao turno mais curto. Para fazer isso, obtemos os quaternions do começo e do fim e, em seguida, fazemos uma transição entre eles usando Quaternion.Slerp
. IEnumerator LookAt (Vector3 point) { point.y = transform.localPosition.y; Quaternion fromRotation = transform.localRotation; Quaternion toRotation = Quaternion.LookRotation(point - transform.localPosition); for (float t = Time.deltaTime; t < 1f; t += Time.deltaTime) { transform.localRotation = Quaternion.Slerp(fromRotation, toRotation, t); yield return null; } transform.LookAt(point); orientation = transform.localRotation.eulerAngles.y; }
Isso funcionará, mas a interpolação sempre varia de 0 a 1, independentemente do ângulo de rotação. Para garantir velocidade angular uniforme, precisamos diminuir a interpolação à medida que o ângulo de rotação aumenta. Quaternion fromRotation = transform.localRotation; Quaternion toRotation = Quaternion.LookRotation(point - transform.localPosition); float angle = Quaternion.Angle(fromRotation, toRotation); float speed = rotationSpeed / angle; for ( float t = Time.deltaTime * speed; t < 1f; t += Time.deltaTime * speed ) { transform.localRotation = Quaternion.Slerp(fromRotation, toRotation, t); yield return null; }
Conhecendo o ângulo, podemos pular completamente a curva se ela for zero. float angle = Quaternion.Angle(fromRotation, toRotation); if (angle > 0f) { float speed = rotationSpeed / angle; for ( … ) { … } }
Agora podemos adicionar a rotação da unidade TravelPath
simplesmente executando o rendimento antes de mover a LookAt
posição da segunda célula. O Unity iniciará automaticamente a rotina LookAt
e TravelPath
aguardará sua conclusão. IEnumerator TravelPath () { Vector3 a, b, c = pathToTravel[0].Position; yield return LookAt(pathToTravel[1].Position); float t = Time.deltaTime * travelSpeed; … }
Se você verificar o código, o esquadrão se teletransporta para a célula final, vira para lá e depois se teletransporta de volta para o início do caminho e começa a se mover a partir daí. Isso acontece porque atribuímos um valor a uma propriedade Location
antes do início da corotina TravelPath
. Para se livrar do teletransporte, podemos, no início, TravelPath
retornar a posição do desapego à célula inicial. Vector3 a, b, c = pathToTravel[0].Position; transform.localPosition = c; yield return LookAt(pathToTravel[1].Position);
Gire antes de moverVarrer
Tendo recebido o movimento de que precisamos, podemos nos livrar do método OnDrawGizmos
. Exclua ou comente se precisarmos ver caminhos no futuro.
Como não precisamos mais lembrar para onde estávamos nos movendo, no final, TravelPath
você pode liberar a lista de células. IEnumerator TravelPath () { … ListPool<HexCell>.Add(pathToTravel); pathToTravel = null; }
E as animações de esquadrão real?, . 3D- . . , . Mecanim, TravelPath
.
unitypackage