De um tradutor: este artigo é o primeiro de uma série detalhada de tutoriais (27 partes) sobre a criação de mapas a partir de hexágonos. É isso que deve acontecer no final dos tutoriais.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 1: malha de hexágonos
Sumário
- Converta quadrados em hexágonos.
- Triangular uma grade de hexágonos.
- Trabalhamos com coordenadas cúbicas.
- Interagimos com células da grade.
- Crie um editor no jogo.
Este tutorial é o início de uma série sobre cartões hexagonais. As redes hexagonais são usadas em muitos jogos, especialmente em estratégias, incluindo Age of Wonders 3, Civilization 5 e Endless Legend. Começaremos com o básico, adicionaremos gradualmente novos recursos e, como resultado, criaremos um alívio complexo baseado em hexágonos.
Este tutorial pressupõe que você já estudou a série
Mesh Basics , que começa com a
Grade de procedimentos . Foi criado no Unity 5.3.1. A série usa várias versões do Unity. A última parte é feita no Unity 2017.3.0p3.
Um mapa simples de hexágonos.Sobre hexágonos
Por que os hexágonos são necessários? Se precisarmos de uma grade, é lógico usar quadrados. Os quadrados são realmente fáceis de desenhar e posicionar, mas também têm uma desvantagem. Olhe para um único quadrado da grade e depois para os vizinhos.
A praça e seus vizinhos.No total, a praça tem oito vizinhos. Quatro deles podem ser alcançados cruzando a borda da praça. Estes são vizinhos horizontais e verticais. Os outros quatro podem ser alcançados atravessando a esquina da praça. Estes são vizinhos diagonais.
Qual é a distância entre os centros das células quadriculares adjacentes? Se o comprimento da aresta for 1, para os vizinhos horizontais e verticais, a resposta será 1. Mas para os vizinhos diagonais, a resposta será √2.
A diferença entre os dois tipos de vizinhos leva a dificuldades. Se usarmos movimentos discretos, como perceber o movimento na diagonal? Devo permitir isso? Como tornar a aparência mais orgânica? Jogos diferentes usam abordagens diferentes com suas vantagens e desvantagens. Uma abordagem não é usar uma grade quadrada, mas usar hexágonos.
Hexagon e seus vizinhos.Ao contrário de um quadrado, um hexágono não tem oito, mas seis vizinhos. Todos esses vizinhos são adjacentes às bordas, não há vizinhos de canto. Ou seja, existe apenas um tipo de vizinhos, o que simplifica muito. Obviamente, é mais difícil construir uma grade de hexágonos do que um quadrado, mas podemos lidar com isso.
Antes de começarmos, precisamos determinar o tamanho dos hexágonos. Deixe o comprimento da borda igual a 10 unidades. Como o hexágono consiste em um círculo de seis triângulos equilaterais, a distância do centro a qualquer ângulo também é 10. Esse valor determina o raio externo da célula hexagonal.
O raio externo e interno do hexágono.Há também um raio interno, que é a distância do centro a cada uma das arestas. Este parâmetro é importante porque a distância entre os centros dos vizinhos é igual a esse valor vezes dois. O raio interno é
f r a c s q r t 3 2 do raio externo, isto é, no nosso caso
5 s q r t 3 . Vamos colocar esses parâmetros em uma classe estática por conveniência.
using UnityEngine; public static class HexMetrics { public const float outerRadius = 10f; public const float innerRadius = outerRadius * 0.866025404f; }
Como derivar o valor do raio interno?Pegue um dos seis triângulos de um hexágono. O raio interno é igual à altura deste triângulo. Essa altura pode ser obtida dividindo o triângulo em dois triângulos regulares e, em seguida, use o teorema de Pitágoras.
Portanto, para o comprimento da costela e raio interno é sqrte2−(e/2)2= sqrt3e2/4=e sqrt3/2 aproximadamente0,886e .
Se já estamos fazendo isso, vamos determinar as posições dos seis cantos em relação ao centro da célula. Note-se que existem duas maneiras de orientar o hexágono: para cima, com um lado afiado ou plano. Vamos colocar a esquina. Vamos começar desse ângulo e adicionar o restante no sentido horário. Coloque-os no plano XZ para que os hexágonos fiquem no chão.
Orientações possíveis. public static Vector3[] corners = { new Vector3(0f, 0f, outerRadius), new Vector3(innerRadius, 0f, 0.5f * outerRadius), new Vector3(innerRadius, 0f, -0.5f * outerRadius), new Vector3(0f, 0f, -outerRadius), new Vector3(-innerRadius, 0f, -0.5f * outerRadius), new Vector3(-innerRadius, 0f, 0.5f * outerRadius) };
unitypackageMalha
Para construir uma grade de hexágonos, precisamos de células da grade. Para esse fim, crie o componente
HexCell
. Por enquanto, deixe em branco porque ainda não estamos usando nenhuma célula.
using UnityEngine; public class HexCell : MonoBehaviour { }
Para começar com o mais simples, crie um objeto de plano padrão, adicione um componente de célula a ele e transforme tudo em uma pré-fabricada.
Usando um avião como uma casa pré-fabricada de uma célula hexagonal.Agora vamos entrar na rede. Vamos criar um componente simples com variáveis comuns de largura, altura e pré-fabricado da célula. Em seguida, adicione um objeto de jogo com este componente à cena.
using UnityEngine; public class HexGrid : MonoBehaviour { public int width = 6; public int height = 6; public HexCell cellPrefab; }
Objeto de malha hexagonal.Vamos começar criando uma grade regular de quadrados, porque já sabemos como fazer isso. Vamos salvar as células em uma matriz para poder acessá-las.
Como os planos, por padrão, têm um tamanho de 10 a 10 unidades, mudaremos cada célula por esse valor.
HexCell[] cells; void Awake () { cells = new HexCell[height * width]; for (int z = 0, i = 0; z < height; z++) { for (int x = 0; x < width; x++) { CreateCell(x, z, i++); } } } void CreateCell (int x, int z, int i) { Vector3 position; position.x = x * 10f; position.y = 0f; position.z = z * 10f; HexCell cell = cells[i] = Instantiate<HexCell>(cellPrefab); cell.transform.SetParent(transform, false); cell.transform.localPosition = position; }
Grade quadrada de aviões.Então, temos uma bela grade sólida de células quadradas. Mas qual célula é onde? Obviamente, isso é fácil de verificar, mas há dificuldades com hexágonos. Seria conveniente se pudéssemos ver simultaneamente as coordenadas de todas as células.
Exibição de coordenadas
Para adicionar telas à cena, selecione
GameObject / UI / Canvas e torne-a filha do nosso objeto de malha. Como essa tela é apenas informativa, removeremos seu componente raycaster. Você também pode excluir o objeto do sistema de eventos, que foi adicionado automaticamente à cena, porque, por enquanto, não precisamos dele.
Defina o
Modo de renderização como
Espaço mundial e gire-o 90 graus ao longo do eixo X para que a tela se sobreponha à grade. Defina o pivô e a posição para zero. Faça um leve deslocamento vertical para que seu conteúdo fique no topo. Largura e altura não são importantes para nós, porque organizamos o conteúdo por conta própria. Podemos definir o valor como 0 para se livrar do retângulo grande na janela da cena.
Como toque final, aumente os
Pixels dinâmicos por unidade para 10. Portanto, garantimos que os objetos de texto utilizem resolução de textura suficiente.
Tela para coordenadas de grade de hexágonos.Para exibir as coordenadas, crie um objeto de texto (
GameObject / UI / Text ) e transforme-o em uma pré-fabricada. Centralize suas âncoras e gire, defina o tamanho de 5 por 15. O texto também deve estar alinhado horizontal e verticalmente no centro. Defina o tamanho da fonte como 4. Por fim, não queremos usar o texto padrão e não usaremos
Rich Text . Além disso, não importa se o
Raycast Target está ativado, porque ainda não é necessário para a nossa tela.
Etiqueta de célula pré-fabricada.Agora precisamos informar a grade sobre tela e pré-fabricada. Adicione ao início do script dela
using UnityEngine.UI;
acessar convenientemente o tipo
UnityEngine.UI.Text
. Um rótulo pré-fabricado precisa de uma variável compartilhada e a tela pode ser encontrada chamando
GetComponentInChildren
.
public Text cellLabelPrefab; Canvas gridCanvas; void Awake () { gridCanvas = GetComponentInChildren<Canvas>(); … }
Tags de conexão pré-fabricada.Após conectar a pré-fabricada do rótulo, podemos criar suas instâncias e exibir as coordenadas da célula. Entre X e Z, insira um caractere de nova linha para que apareça em linhas separadas.
void CreateCell (int x, int z, int i) { … Text label = Instantiate<Text>(cellLabelPrefab); label.rectTransform.SetParent(gridCanvas.transform, false); label.rectTransform.anchoredPosition = new Vector2(position.x, position.z); label.text = x.ToString() + "\n" + z.ToString(); }
Exibição de coordenadas.Posições hexagonais
Agora que podemos reconhecer visualmente cada célula, vamos começar a movê-las. Sabemos que a distância entre as células hexagonais adjacentes na direção X é igual a duas vezes o raio interno. Nós vamos usá-lo. Além disso, a distância para a próxima linha de células deve ser 1,5 vezes maior que o raio externo.
Geometria dos hexágonos vizinhos. position.x = x * (HexMetrics.innerRadius * 2f); position.y = 0f; position.z = z * (HexMetrics.outerRadius * 1.5f);
Aplicamos distâncias entre hexágonos sem compensações.Obviamente, as linhas ordinais dos hexágonos não estão localizadas exatamente uma acima da outra. Cada linha é deslocada ao longo do eixo X pelo valor do raio interno. Este valor pode ser obtido adicionando metade de Z a X e depois multiplicado por duas vezes o raio interno.
position.x = (x + z * 0.5f) * (HexMetrics.innerRadius * 2f);
O posicionamento adequado dos hexágonos cria uma grade em forma de diamante.Embora tenha sido assim que colocamos as células nas posições corretas dos hexágonos, nossa grade agora preenche o losango e não o retângulo. Ficamos muito mais confortáveis trabalhando com grades retangulares, então vamos fazer com que as células voltem a funcionar. Isso pode ser feito recuando parte do deslocamento. Em cada segunda linha, todas as células devem ser recuadas uma etapa adicional. Para fazer isso, precisamos subtrair o resultado da divisão inteira de Z por 2 antes de multiplicar.
position.x = (x + z * 0.5f - z / 2) * (HexMetrics.innerRadius * 2f);
A localização dos hexágonos em uma área retangular.unitypackageRenderização em hexágono
Tendo colocado as células corretamente, podemos continuar exibindo os hexágonos reais. Primeiro, precisamos nos livrar dos planos, para remover todos os componentes, exceto o
HexCell
da
HexCell
de células.
Não há mais aviões.Como nos tutoriais do
Mesh Basics , usamos uma malha para renderizar toda a malha. No entanto, desta vez, não definiremos o número de vértices e triângulos necessários. Em vez disso, usaremos listas.
Crie um novo componente
HexMesh
que cuide da nossa malha. Isso exigirá um filtro e renderizador de malha, uma malha e listas de vértices e triângulos.
using UnityEngine; using System.Collections.Generic; [RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))] public class HexMesh : MonoBehaviour { Mesh hexMesh; List<Vector3> vertices; List<int> triangles; void Awake () { GetComponent<MeshFilter>().mesh = hexMesh = new Mesh(); hexMesh.name = "Hex Mesh"; vertices = new List<Vector3>(); triangles = new List<int>(); } }
Crie um novo objeto filho para esta malha com este componente. Ele receberá automaticamente um renderizador de malha, mas nenhum material será atribuído a ele. Portanto, adicione o material padrão a ele.
Objeto de malha hexagonal.HexGrid
o
HexGrid
poderá recuperar sua malha hexagonal da mesma maneira que ele encontrou a tela.
HexMesh hexMesh; void Awake () { gridCanvas = GetComponentInChildren<Canvas>(); hexMesh = GetComponentInChildren<HexMesh>(); … }
Após a malha acordada, ela deve ordenar que triangule suas células. Precisamos ter certeza de que isso acontecerá após o componente Awake da malha hexadecimal. Como o
Start
é chamado mais tarde, insira o código apropriado lá.
void Start () { hexMesh.Triangulate(cells); }
Esse método
HexMesh.Triangulate
pode ser chamado a qualquer momento, mesmo se as células já tiverem sido trianguladas antes. Portanto, devemos começar limpando os dados antigos. Ao percorrer todas as células, as triangulamos individualmente. Após concluir esta operação, atribuímos os vértices e triângulos gerados à malha e finalizamos recalculando as normais da malha.
public void Triangulate (HexCell[] cells) { hexMesh.Clear(); vertices.Clear(); triangles.Clear(); for (int i = 0; i < cells.Length; i++) { Triangulate(cells[i]); } hexMesh.vertices = vertices.ToArray(); hexMesh.triangles = triangles.ToArray(); hexMesh.RecalculateNormals(); } void Triangulate (HexCell cell) { }
Como os hexágonos são compostos por triângulos, vamos criar um método conveniente para adicionar um triângulo com base nas posições de três vértices. Apenas adicionará vértices em ordem. Ele também adiciona os índices desses vértices para formar um triângulo. O índice do primeiro vértice é igual ao comprimento da lista de vértices antes de adicionar novos vértices a ele. Não se esqueça disso ao adicionar vértices.
void AddTriangle (Vector3 v1, Vector3 v2, Vector3 v3) { int vertexIndex = vertices.Count; vertices.Add(v1); vertices.Add(v2); vertices.Add(v3); triangles.Add(vertexIndex); triangles.Add(vertexIndex + 1); triangles.Add(vertexIndex + 2); }
Agora podemos triangular nossas células. Vamos começar com o primeiro triângulo. Seu primeiro pico está no centro do hexágono. Os dois outros vértices são o primeiro e o segundo ângulos em relação ao centro.
void Triangulate (HexCell cell) { Vector3 center = cell.transform.localPosition; AddTriangle( center, center + HexMetrics.corners[0], center + HexMetrics.corners[1] ); }
O primeiro triângulo de cada célula.Isso funcionou, então vamos dar uma volta em torno dos seis triângulos.
Vector3 center = cell.transform.localPosition; for (int i = 0; i < 6; i++) { AddTriangle( center, center + HexMetrics.corners[i], center + HexMetrics.corners[i + 1] ); }
Os picos podem ser compartilhados?Sim você pode. De fato, podemos fazer ainda melhor e usar apenas quatro em vez de seis triângulos para renderização. Mas, abandonando isso, simplificaremos nosso trabalho, e estará certo, porque nos tutoriais a seguir tudo se torna mais complicado. A otimização de vértices e triângulos nesse estágio nos impedirá no futuro.
Infelizmente, esse processo resultará em um
IndexOutOfRangeException
. Isso ocorre porque o último triângulo está tentando obter o sétimo canto, o que não existe. Claro, ele deve voltar e usar como o último vértice da primeira curva. Ou podemos duplicar o primeiro canto do
HexMetrics.corners
para não ir além dos limites.
public static Vector3[] corners = { new Vector3(0f, 0f, outerRadius), new Vector3(innerRadius, 0f, 0.5f * outerRadius), new Vector3(innerRadius, 0f, -0.5f * outerRadius), new Vector3(0f, 0f, -outerRadius), new Vector3(-innerRadius, 0f, -0.5f * outerRadius), new Vector3(-innerRadius, 0f, 0.5f * outerRadius), new Vector3(0f, 0f, outerRadius) };
Hexágonos completamente.unitypackageCoordenadas hexagonais
Vejamos as coordenadas das células novamente, agora no contexto de uma grade de hexágonos. A coordenada Z parece boa e a coordenada X ziguezagueia. Esse é um efeito colateral do deslocamento da linha para cobrir uma área retangular.
Coordenadas de deslocamento com zero linhas destacadas.Ao trabalhar com hexágonos, essas coordenadas de deslocamento não são fáceis de manusear. Vamos adicionar uma estrutura
HexCoordinates
, que pode ser usada para converter em outro sistema de coordenadas. Vamos torná-lo serializável para que o Unity possa armazená-lo e ele sofrerá uma recompilação no modo Play. Também tornamos essas coordenadas imutáveis usando as propriedades public readonly.
using UnityEngine; [System.Serializable] public struct HexCoordinates { public int X { get; private set; } public int Z { get; private set; } public HexCoordinates (int x, int z) { X = x; Z = z; } }
Adicione um método estático para criar um conjunto de coordenadas a partir de coordenadas de deslocamento comuns. Por enquanto, simplesmente copiaremos essas coordenadas.
public static HexCoordinates FromOffsetCoordinates (int x, int z) { return new HexCoordinates(x, z); } }
Também adicionamos métodos convenientes de conversão de string. O método
ToString
, por padrão, retorna um nome de tipo de estrutura, que não é muito útil para nós. Nós o redefinimos para que ele retorne as coordenadas em uma linha. Também adicionaremos um método para exibir coordenadas em linhas separadas, porque já usamos esse esquema.
public override string ToString () { return "(" + X.ToString() + ", " + Z.ToString() + ")"; } public string ToStringOnSeparateLines () { return X.ToString() + "\n" + Z.ToString(); }
Agora podemos passar muitas coordenadas para o nosso componente
HexCell
.
public class HexCell : MonoBehaviour { public HexCoordinates coordinates; }
Altere
HexGrid.CreateCell
para que ele possa usar as novas coordenadas.
HexCell cell = cells[i] = Instantiate<HexCell>(cellPrefab); cell.transform.SetParent(transform, false); cell.transform.localPosition = position; cell.coordinates = HexCoordinates.FromOffsetCoordinates(x, z); Text label = Instantiate<Text>(cellLabelPrefab); label.rectTransform.SetParent(gridCanvas.transform, false); label.rectTransform.anchoredPosition = new Vector2(position.x, position.z); label.text = cell.coordinates.ToStringOnSeparateLines();
Agora vamos refazer essas coordenadas X para que elas fiquem alinhadas ao longo de um eixo reto. Isso pode ser feito cancelando o deslocamento horizontal. O resultado resultante é geralmente chamado de coordenadas axiais.
public static HexCoordinates FromOffsetCoordinates (int x, int z) { return new HexCoordinates(x - z / 2, z); }
Coordenadas axiais.Esse sistema de coordenadas bidimensional nos permite descrever sequencialmente o movimento do deslocamento em quatro direções. No entanto, as duas direções restantes ainda requerem atenção especial. Isso nos faz perceber que existe uma terceira dimensão. E, de fato, se girássemos horizontalmente a dimensão de X, obteríamos a dimensão ausente de Y.
A medida Y aparece.Como essas medidas de X e Y são cópias espelhadas uma da outra, a adição de suas coordenadas sempre dá o mesmo resultado se Z permanecer constante. De fato, se você somar as três coordenadas, sempre obteremos zero. Se você aumentar uma coordenada, precisará reduzir a outra. E, de fato, isso nos dá seis direções possíveis de movimento. Tais coordenadas são geralmente chamadas de cúbicas, porque são tridimensionais e a topologia se assemelha a um cubo.
Como a soma de todas as coordenadas é zero, sempre podemos obter qualquer uma das coordenadas das outras duas. Como já armazenamos as coordenadas X e Z, não precisamos armazenar as coordenadas Y.
Podemos adicionar uma propriedade que a avalie, se necessário, e usá-la em métodos de string.
public int Y { get { return -X - Z; } } public override string ToString () { return "(" + X.ToString() + ", " + Y.ToString() + ", " + Z.ToString() + ")"; } public string ToStringOnSeparateLines () { return X.ToString() + "\n" + Y.ToString() + "\n" + Z.ToString(); }
Coordenadas cúbicas.Coordenadas do Inspetor
No modo de reprodução, selecione uma das células da grade. Acontece que o inspetor não exibe suas coordenadas, apenas o rótulo do prefixo
HexCell.coordinates
é
HexCell.coordinates
.
O inspetor não exibe as coordenadas.Embora não exista um grande problema com isso, seria ótimo exibir as coordenadas. A unidade não mostra coordenadas porque elas não estão marcadas como campos serializáveis. Para exibi-los, você deve especificar explicitamente os campos serializáveis para X e Z.
[SerializeField] private int x, z; public int X { get { return x; } } public int Z { get { return z; } } public HexCoordinates (int x, int z) { this.x = x; this.z = z; }
As coordenadas X e Z agora são exibidas, mas podem ser alteradas. Não precisamos disso, porque as coordenadas devem ser fixadas. Também não é muito bom que eles sejam exibidos um sob o outro.
Podemos fazer melhor: defina nossa própria gaveta de propriedades para o tipo
HexCoordinates
. Crie um script
HexCoordinatesDrawer
e cole-o na pasta
Editor , porque esse script é apenas para o editor.
A classe deve estender o
UnityEditor.PropertyDrawer
e precisa do atributo
UnityEditor.CustomPropertyDrawer
para associá-lo a um tipo adequado.
using UnityEngine; using UnityEditor; [CustomPropertyDrawer(typeof(HexCoordinates))] public class HexCoordinatesDrawer : PropertyDrawer { }
As gavetas de propriedades exibem seu conteúdo usando o método
OnGUI
. Este método permitiu desenhar dados de propriedades serializáveis e o rótulo do campo ao qual eles pertencem dentro do retângulo da tela.
public override void OnGUI ( Rect position, SerializedProperty property, GUIContent label ) { }
Extraímos os valores de x e z da propriedade e os usamos para criar um novo conjunto de coordenadas. Em seguida, desenhe o rótulo da GUI na posição selecionada usando nosso método
HexCoordinates.ToString
.
public override void OnGUI ( Rect position, SerializedProperty property, GUIContent label ) { HexCoordinates coordinates = new HexCoordinates( property.FindPropertyRelative("x").intValue, property.FindPropertyRelative("z").intValue ); GUI.Label(position, coordinates.ToString()); }
Coordenadas sem um rótulo de prefixo.Isso exibirá as coordenadas, mas agora estamos perdendo o nome do campo. Esses nomes geralmente são renderizados usando o método
EditorGUI.PrefixLabel
. Como bônus, ele retorna um retângulo alinhado que corresponde ao espaço à direita desse rótulo.
position = EditorGUI.PrefixLabel(position, label); GUI.Label(position, coordinates.ToString());
Coordena com um rótulo.unitypackageCélulas de toque
Uma grade de hexágonos não é muito interessante se não podemos interagir com ela. A interação mais simples é tocar a célula, então vamos adicionar suporte a ela. Por enquanto, simplesmente
HexGrid
esse código diretamente no
HexGrid
. Quando começar a funcionar, vamos movê-lo para outro local.
Para tocar em uma célula, você pode emitir raios para a cena a partir da posição do cursor do mouse. Podemos usar a mesma abordagem que no tutorial
Deformação de malha .
void Update () { if (Input.GetMouseButton(0)) { HandleInput(); } } void HandleInput () { Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(inputRay, out hit)) { TouchCell(hit.point); } } void TouchCell (Vector3 position) { position = transform.InverseTransformPoint(position); Debug.Log("touched at " + position); }
Até agora, o código não está fazendo nada. Precisamos adicionar um colisor à grade para que o feixe possa colidir com alguma coisa. Portanto, forneceremos a malha do
HexMesh
.
MeshCollider meshCollider; void Awake () { GetComponent<MeshFilter>().mesh = hexMesh = new Mesh(); meshCollider = gameObject.AddComponent<MeshCollider>(); … }
Depois que a triangulação estiver concluída, atribua uma malha ao colisor.
public void Triangulate (HexCell[] cells) { … meshCollider.sharedMesh = hexMesh; }
Não podemos simplesmente usar o colisor de caixas?Podemos, mas não corresponderá exatamente ao contorno da nossa grade. Sim, e nossa grade não permanecerá plana por muito tempo, mas este é um tópico para futuros tutoriais.
Agora podemos tocar a grade! Mas em qual célula tocamos? Para descobrir, precisamos converter a posição do toque nas coordenadas dos hexágonos. Este é um trabalho para
HexCoordinates
, por isso declararemos que ele possui um método estático
FromPosition
.
public void TouchCell (Vector3 position) { position = transform.InverseTransformPoint(position); HexCoordinates coordinates = HexCoordinates.FromPosition(position); Debug.Log("touched at " + coordinates.ToString()); }
Como esse método determinará qual coordenada pertence à posição? Podemos começar dividindo x pela largura horizontal do hexágono. E como a coordenada Y é uma imagem espelhada da coordenada X, um x negativo nos dá y.
public static HexCoordinates FromPosition (Vector3 position) { float x = position.x / (HexMetrics.innerRadius * 2f); float y = -x; }
Obviamente, isso nos daria as coordenadas corretas se Z fosse zero. Devemos mudar novamente ao mover-se ao longo de Z. A cada duas linhas, devemos mudar para a esquerda por uma unidade.
float offset = position.z / (HexMetrics.outerRadius * 3f); x -= offset; y -= offset;
Como resultado, nossos valores xey são inteiros no centro de cada célula. Portanto, arredondando-os para o número inteiro mais próximo, devemos obter as coordenadas. Também calculamos Z e, portanto, obtemos as coordenadas finais. int iX = Mathf.RoundToInt(x); int iY = Mathf.RoundToInt(y); int iZ = Mathf.RoundToInt(-x -y); return new HexCoordinates(iX, iZ);
Os resultados parecem promissores, mas essas coordenadas estão corretas? Com um estudo cuidadoso, você pode descobrir que algumas vezes obtemos as coordenadas, cuja soma não é igual a zero! Vamos ativar a notificação para garantir que isso realmente esteja acontecendo. if (iX + iY + iZ != 0) { Debug.LogWarning("rounding error!"); } return new HexCoordinates(iX, iZ);
Na verdade, recebemos notificações. Como podemos corrigir esse erro? Ele surge apenas próximo às arestas entre os hexágonos. Ou seja, o arredondamento de coordenadas causa problemas. Qual coordenada é arredondada na direção errada? Quanto mais nos afastamos do centro da célula, mais arredondados chegamos. Portanto, é lógico supor que a coordenada arredondada acima de tudo esteja incorreta.Em seguida, a solução é eliminar a coordenada com o maior delta de arredondamento e recriá-la a partir dos valores dos outros dois. Mas como precisamos apenas de X e Z, não podemos nos incomodar em recriar Y. if (iX + iY + iZ != 0) { float dX = Mathf.Abs(x - iX); float dY = Mathf.Abs(y - iY); float dZ = Mathf.Abs(-x -y - iZ); if (dX > dY && dX > dZ) { iX = -iY - iZ; } else if (dZ > dY) { iZ = -iX - iY; } }
Desenho de hexágonos para colorir
Agora que podemos tocar na célula certa, chegou a hora da interação real. Vamos mudar a cor de cada célula em que entramos. Adicione as HexGrid
cores personalizadas da célula padrão e da célula afetada. public Color defaultColor = Color.white; public Color touchedColor = Color.magenta;
Seleção de cor da célula.Adicione ao HexCell
campo de cores geral. public class HexCell : MonoBehaviour { public HexCoordinates coordinates; public Color color; }
Atribua HexGrid.CreateCell
à cor padrão. void CreateCell (int x, int z, int i) { … cell.coordinates = HexCoordinates.FromOffsetCoordinates(x, z); cell.color = defaultColor; … }
Também precisamos adicionar HexMesh
as informações de cores. List<Color> colors; void Awake () { … vertices = new List<Vector3>(); colors = new List<Color>(); … } public void Triangulate (HexCell[] cells) { hexMesh.Clear(); vertices.Clear(); colors.Clear(); … hexMesh.vertices = vertices.ToArray(); hexMesh.colors = colors.ToArray(); … }
Agora, ao triangular, devemos adicionar dados de cores a cada triângulo. Para esse fim, criaremos um método separado. void Triangulate (HexCell cell) { Vector3 center = cell.transform.localPosition; for (int i = 0; i < 6; i++) { AddTriangle( center, center + HexMetrics.corners[i], center + HexMetrics.corners[i + 1] ); AddTriangleColor(cell.color); } } void AddTriangleColor (Color color) { colors.Add(color); colors.Add(color); colors.Add(color); }
Voltar para HexGrid.TouchCell
. Primeiro, converta as coordenadas da célula no índice correspondente da matriz. Para uma grade quadrada, isso seria apenas X mais Z multiplicado pela largura, mas no nosso caso também teremos que adicionar um deslocamento de metade de Z. Em seguida, pegamos a célula, mudamos de cor e triangulamos a malha novamente.Nós realmente precisamos triangular novamente toda a malha?, . . , , . .
public void TouchCell (Vector3 position) { position = transform.InverseTransformPoint(position); HexCoordinates coordinates = HexCoordinates.FromPosition(position); int index = coordinates.X + coordinates.Z * width + coordinates.Z / 2; HexCell cell = cells[index]; cell.color = touchedColor; hexMesh.Triangulate(cells); }
Embora agora possamos colorir as células, as alterações visuais ainda não são visíveis. Isso ocorre porque o sombreador não usa cores de vértice por padrão. Temos que escrever os nossos. Crie um novo shader padrão ( Ativos / Criar / Shader / Shader de superfície padrão ). Apenas duas alterações precisam ser feitas. Primeiro, adicione dados de cores à sua estrutura de entrada. Em segundo lugar, multiplique albedo por esta cor. Estamos interessados apenas nos canais RGB, porque o material é opaco. Shader "Custom/VertexColors" { Properties { _Color ("Color", Color) = (1,1,1,1) _MainTex ("Albedo (RGB)", 2D) = "white" {} _Glossiness ("Smoothness", Range(0,1)) = 0.5 _Metallic ("Metallic", Range(0,1)) = 0.0 } SubShader { Tags { "RenderType"="Opaque" } LOD 200 CGPROGRAM #pragma surface surf Standard fullforwardshadows #pragma target 3.0 sampler2D _MainTex; struct Input { float2 uv_MainTex; float4 color : COLOR; }; half _Glossiness; half _Metallic; fixed4 _Color; void surf (Input IN, inout SurfaceOutputStandard o) { fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color; o.Albedo = c.rgb * IN.color; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; } ENDCG } FallBack "Diffuse" }
Crie um novo material usando esse shader e faça com que a malha de malha use esse material. Graças a isso, as cores das células aparecerão.Células coloridas.Eu recebo artefatos de sombras estranhas!Unity . , , Z-. .
unitypackageEditor de mapa
Agora que sabemos como mudar as cores, vamos criar um editor simples no jogo. Essa funcionalidade não se aplica aos recursos HexGrid
, portanto, a transformaremos TouchCell
em um método geral com um parâmetro de cor adicional. Exclua também o campo touchedColor
. public void ColorCell (Vector3 position, Color color) { position = transform.InverseTransformPoint(position); HexCoordinates coordinates = HexCoordinates.FromPosition(position); int index = coordinates.X + coordinates.Z * width + coordinates.Z / 2; HexCell cell = cells[index]; cell.color = color; hexMesh.Triangulate(cells); }
Crie um componente HexMapEditor
e mova os métodos Update
e para ele HandleInput
. Adicione um campo comum a ele para se referir à grade de hexágonos, uma matriz de cores e um campo privado para rastrear a cor ativa. Por fim, adicione um método geral para selecionar uma cor e faça com que ele selecione inicialmente a primeira cor. using UnityEngine; public class HexMapEditor : MonoBehaviour { public Color[] colors; public HexGrid hexGrid; private Color activeColor; void Awake () { SelectColor(0); } void Update () { if (Input.GetMouseButton(0)) { HandleInput(); } } void HandleInput () { Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(inputRay, out hit)) { hexGrid.ColorCell(hit.point, activeColor); } } public void SelectColor (int index) { activeColor = colors[index]; } }
Adicione outra tela, desta vez mantendo as configurações padrão. Adicione um componente a ele HexMapEditor
, defina várias cores e conecte-o a uma grade de hexágonos. Desta vez, precisamos de um objeto do sistema de eventos e ele foi criado automaticamente novamente.Editor de mapa hexagonal de quatro cores.Adicione um painel à tela para armazenar seletores de cores ( GameObject / UI / Panel ). Adicione seu grupo de alternância ( Componentes / UI / Grupo de alternância ). Faça o painel pequeno e coloque-o no canto da tela.Painel colorido com grupo de alternância.Agora preencha o painel com opções para cada cor ( GameObject / UI / Toggle ). Desde que não nos preocupemos em criar uma interface do usuário complexa, a configuração manual simples é suficiente.Um interruptor para cada cor.Ligue o primeiro interruptor. Faça também todas as partes dos comutadores do grupo de alternância, para que apenas uma delas possa ser selecionada por vez. Por fim, conecte-os ao método SelectColor
do nosso editor. Isso pode ser feito usando o botão da interface do usuário "+" do evento On Value Changed . Selecione o objeto do editor de mapas e, em seguida, selecione o método desejado na lista suspensa.O primeiro interruptor.Este evento passa um argumento booleano que determina se a opção é ativada toda vez que é alterada. Mas nós não nos importamos. Em vez disso, teremos que passar manualmente um argumento inteiro correspondente ao índice de cores que queremos usar. Portanto, deixe o valor 0 para a primeira opção, defina o valor 1 para a segunda e assim por diante.Quando é chamado o método de evento do switch?. , , .
, , . , SelectColor
. , .
Colorir em várias cores.Embora a interface do usuário funcione, há um detalhe irritante. Para vê-lo, mova o painel para que cubra a grade de hexágonos. Ao escolher uma nova cor, também coloriremos as células na interface do usuário. Ou seja, estamos interagindo simultaneamente com a interface do usuário e com a grade. Este é um comportamento indesejável.Isso pode ser corrigido perguntando ao sistema de eventos se ele determinou a localização do cursor sobre algum objeto. Como ela conhece apenas objetos da interface do usuário, isso nos diz que estamos interagindo com a interface do usuário. Portanto, precisaremos processar a entrada sozinhos se isso não acontecer. using UnityEngine; using UnityEngine.EventSystems; … void Update () { if ( Input.GetMouseButton(0) && !EventSystem.current.IsPointerOverGameObject() ) { HandleInput(); } }
unitypackageParte 2: misturando cores das células
Sumário
- Conecte os vizinhos.
- Interpole as cores entre os triângulos.
- Crie áreas de mesclagem.
- Simplifique a geometria.
Na parte anterior, lançamos as bases da grade e adicionamos a capacidade de editar células. Cada célula tem sua própria cor sólida e as cores nas bordas das células mudam drasticamente. Neste tutorial, criaremos zonas de transição que misturam as cores das células vizinhas.Transições suaves entre células.Células vizinhas
Antes de realizar a suavização entre as cores das células, precisamos descobrir quais células são adjacentes uma à outra. Cada célula tem seis vizinhos que podem ser identificados nas direções dos pontos cardeais. Obteremos as seguintes direções: nordeste, leste, sudeste, sudoeste, oeste e noroeste. Vamos criar uma enumeração para eles e colá-lo em um arquivo de script separado. public enum HexDirection { NE, E, SE, SW, W, NW }
O que é enum?enum
, . . , . , .
enum . , integer . , - , integer.
Seis vizinhos, seis direções.Para armazenar esses vizinhos, adicione à HexCell
matriz. Embora possamos torná-lo geral, o tornaremos privado e forneceremos acesso a métodos usando instruções. Também o tornamos serializável para que os títulos não sejam perdidos na recompilação. [SerializeField] HexCell[] neighbors;
Precisamos armazenar todas as conexões com os vizinhos?, . — , .
Agora a matriz de vizinhos é exibida no inspetor. Como cada célula possui seis vizinhos, para a nossa pré-fabricada Hex Cell , definimos o tamanho da matriz 6.Há espaço para seis vizinhos em nossa casa pré-fabricada.Agora vamos adicionar um método geral para obter um vizinho de célula em uma direção. Como o valor da direção está sempre no intervalo de 0 a 5, não precisamos verificar se o índice está dentro da matriz. public HexCell GetNeighbor (HexDirection direction) { return neighbors[(int)direction]; }
Adicione um método para especificar um vizinho. public void SetNeighbor (HexDirection direction, HexCell cell) { neighbors[(int)direction] = cell; }
As relações dos vizinhos são bidirecionais. Portanto, ao definir um vizinho em uma direção, seria lógico definir imediatamente um vizinho na direção oposta. public void SetNeighbor (HexDirection direction, HexCell cell) { neighbors[(int)direction] = cell; cell.neighbors[(int)direction.Opposite()] = this; }
Vizinhos em direções opostas.Obviamente, isso sugere que podemos solicitar instruções para o vizinho oposto. Podemos implementar isso criando um método de extensão para HexDirection
. Para obter a direção oposta, você precisa adicionar ao 3. original. No entanto, isso só funciona nas três primeiras direções; no restante, você deve subtrair 3. public enum HexDirection { NE, E, SE, SW, W, NW } public static class HexDirectionExtensions { public static HexDirection Opposite (this HexDirection direction) { return (int)direction < 3 ? (direction + 3) : (direction - 3); } }
O que é um método de extensão?— , - . — , , , . this
. , .
? , , , . ? — . , , .
Conexão vizinha
Podemos inicializar o link de vizinhos HexGrid.CreateCell
. Ao percorrer células linha por linha, da esquerda para a direita, sabemos quais células já foram criadas. Estas são as células com as quais podemos nos conectar.O mais simples é o composto E - W. A primeira célula de cada linha não tem um vizinho oriental. Mas todas as outras células têm. E esses vizinhos são criados antes da célula com a qual estamos trabalhando atualmente. Portanto, podemos conectá-los.A conexão de E a W durante a criação de células. void CreateCell (int x, int z, int i) { … cell.color = defaultColor; if (x > 0) { cell.SetNeighbor(HexDirection.W, cells[i - 1]); } Text label = Instantiate<Text>(cellLabelPrefab); … }
Os vizinhos leste e oeste estão conectados.Precisamos criar mais duas conexões bidirecionais. Como essas são as conexões entre as diferentes linhas da grade, só podemos nos comunicar com a linha anterior. Isso significa que devemos pular completamente a primeira linha. if (x > 0) { cell.SetNeighbor(HexDirection.W, cells[i - 1]); } if (z > 0) { }
Como as linhas são em zigue-zague, elas precisam ser processadas de maneira diferente. Vamos primeiro lidar com as linhas pares. Como todas as células nessas linhas têm um vizinho no SE, podemos conectá-las a ele.Conexão de NW a SE para linhas pares. if (z > 0) { if ((z & 1) == 0) { cell.SetNeighbor(HexDirection.SE, cells[i - width]); } }
O que o z & 1 faz?&&
— , &
— . , . 1, 1. , 10101010 & 00001111
00001010
.
. 0 1. 1, 2, 3, 4 1, 10, 11, 100. , 0.
, , . 0, .
Podemos nos conectar com os vizinhos no SW, exceto a primeira célula de cada linha que não a possui.Conexão de NE a SW para linhas pares. if (z > 0) { if ((z & 1) == 0) { cell.SetNeighbor(HexDirection.SE, cells[i - width]); if (x > 0) { cell.SetNeighbor(HexDirection.SW, cells[i - width - 1]); } } }
Linhas estranhas seguem a mesma lógica, mas em uma imagem espelhada. Depois de concluir este processo, todos os vizinhos da nossa rede estão conectados. if (z > 0) { if ((z & 1) == 0) { cell.SetNeighbor(HexDirection.SE, cells[i - width]); if (x > 0) { cell.SetNeighbor(HexDirection.SW, cells[i - width - 1]); } } else { cell.SetNeighbor(HexDirection.SW, cells[i - width]); if (x < width - 1) { cell.SetNeighbor(HexDirection.SE, cells[i - width + 1]); } } }
Todos os vizinhos estão conectados.Obviamente, nem todas as células estão conectadas a exatamente seis vizinhos. As células no limite da grade têm pelo menos dois e não mais que cinco vizinhos. E isso deve ser levado em consideração.Vizinhos para cada célula.unitypackageMistura de cores
A mistura de cores complicará a triangulação de cada célula. Portanto, vamos separar o código da triangulação em uma parte separada. Como agora temos instruções, vamos usá-las em vez de índices numéricos para indicar partes. void Triangulate (HexCell cell) { for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { Triangulate(d, cell); } } void Triangulate (HexDirection direction, HexCell cell) { Vector3 center = cell.transform.localPosition; AddTriangle( center, center + HexMetrics.corners[(int)direction], center + HexMetrics.corners[(int)direction + 1] ); AddTriangleColor(cell.color); }
Agora, quando usamos as direções, seria conveniente obter ângulos com as direções e não realizar a conversão em índices. AddTriangle( center, center + HexMetrics.GetFirstCorner(direction), center + HexMetrics.GetSecondCorner(direction) );
Para fazer isso, você precisa adicionar HexMetrics
dois métodos estáticos. Como um bônus, isso nos permite tornar a matriz de ângulos privada. static Vector3[] corners = { new Vector3(0f, 0f, outerRadius), new Vector3(innerRadius, 0f, 0.5f * outerRadius), new Vector3(innerRadius, 0f, -0.5f * outerRadius), new Vector3(0f, 0f, -outerRadius), new Vector3(-innerRadius, 0f, -0.5f * outerRadius), new Vector3(-innerRadius, 0f, 0.5f * outerRadius), new Vector3(0f, 0f, outerRadius) }; public static Vector3 GetFirstCorner (HexDirection direction) { return corners[(int)direction]; } public static Vector3 GetSecondCorner (HexDirection direction) { return corners[(int)direction + 1]; }
Várias cores em um triângulo
Até agora, o método HexMesh.AddTriangleColor
possui apenas um argumento de cor. Só pode criar um triângulo com cores sólidas. Vamos criar uma alternativa que suporte cores separadas para cada vértice. void AddTriangleColor (Color c1, Color c2, Color c3) { colors.Add(c1); colors.Add(c2); colors.Add(c3); }
Agora podemos começar a misturar cores! Vamos começar simplesmente usando a cor vizinha para os outros dois vértices. void Triangulate (HexDirection direction, HexCell cell) { Vector3 center = cell.transform.localPosition; AddTriangle( center, center + HexMetrics.GetFirstCorner(direction), center + HexMetrics.GetSecondCorner(direction) ); HexCell neighbor = cell.GetNeighbor(direction); AddTriangleColor(cell.color, neighbor.color, neighbor.color); }
Infelizmente, isso leva a NullReferenceException
, porque as células na fronteira não têm seis vizinhos. O que devemos fazer quando há falta de um vizinho? Sejamos pragmáticos e utilizemos a própria célula como substituta. HexCell neighbor = cell.GetNeighbor(direction) ?? cell;
O que o operador faz?null-coalescing operator. , a ?? b
— a != null ? a : b
.
, - Unity . null
. .
Há uma mistura de cores, mas é feita incorretamente.Para onde foram as etiquetas de coordenadas?, UI.
Média de cores
A mistura de cores funciona, mas os resultados estão obviamente errados. A cor nas bordas dos hexágonos deve ser a média de duas células adjacentes. HexCell neighbor = cell.GetNeighbor(direction) ?? cell; Color edgeColor = (cell.color + neighbor.color) * 0.5f; AddTriangleColor(cell.color, edgeColor, edgeColor);
Misturando nas costelas.Embora estejamos mixando nas bordas, ainda temos bordas de cores nítidas. Isso acontece porque cada vértice do hexágono é compartilhado por três hexágonos.Três vizinhos, quatro cores.Isso significa que também precisamos considerar vizinhos nas direções anterior e próxima. Ou seja, temos quatro cores em dois conjuntos de três.Vamos adicionar HexDirectionExtensions
dois métodos de adição para uma transição conveniente para as direções anterior e seguinte. public static HexDirection Previous (this HexDirection direction) { return direction == HexDirection.NE ? HexDirection.NW : (direction - 1); } public static HexDirection Next (this HexDirection direction) { return direction == HexDirection.NW ? HexDirection.NE : (direction + 1); }
Agora podemos obter todos os três vizinhos e fazer mixagem de três vias. HexCell prevNeighbor = cell.GetNeighbor(direction.Previous()) ?? cell; HexCell neighbor = cell.GetNeighbor(direction) ?? cell; HexCell nextNeighbor = cell.GetNeighbor(direction.Next()) ?? cell; AddTriangleColor( cell.color, (cell.color + prevNeighbor.color + neighbor.color) / 3f, (cell.color + neighbor.color + nextNeighbor.color) / 3f );
Misture nos cantos.Portanto, obtemos as transições de cores corretas, com exceção da borda da malha. As células da borda não são consistentes com as cores dos vizinhos ausentes, então aqui ainda vemos bordas nítidas. No entanto, em geral, nossa abordagem atual não fornece bons resultados. Precisamos de uma estratégia melhor.unitypackageÁreas de mistura
Misturar toda a superfície do hexágono leva ao caos turvo. Não podemos ver claramente células individuais. Os resultados podem ser bastante aprimorados misturando apenas ao lado das bordas dos hexágonos. Nesse caso, a região interna dos hexágonos manterá uma cor sólida.Sombreamento contínuo de centavos com áreas de mistura.Qual deve ser o tamanho da região sólida em comparação com a região de mistura? Distribuições diferentes levam a resultados diferentes. Definiremos essa área como uma fração do raio externo. Que seja igual a 75%. Isso nos levará a duas novas métricas, totalizando 100%. public const float solidFactor = 0.75f; public const float blendFactor = 1f - solidFactor;
Criando esse novo fator de preenchimento sólido, podemos escrever métodos para obter os ângulos dos hexágonos internos sólidos. public static Vector3 GetFirstSolidCorner (HexDirection direction) { return corners[(int)direction] * solidFactor; } public static Vector3 GetSecondSolidCorner (HexDirection direction) { return corners[(int)direction + 1] * solidFactor; }
Agora, altere HexMesh.Triangulate
-o para que use esses ângulos sólidos de sombreamento em vez dos ângulos originais. Deixamos as cores iguais por enquanto. AddTriangle( center, center + HexMetrics.GetFirstSolidCorner(direction), center + HexMetrics.GetSecondSolidCorner(direction) );
Hexágonos sólidos sem arestas.Triangulação de áreas de mistura
Precisamos preencher o espaço vazio que criamos reduzindo os triângulos. Em cada direção, esse espaço tem a forma de um trapézio. Para cobri-lo, você pode usar o quadrilátero (quad). Portanto, criaremos métodos para adicionar um quadrilátero e suas cores.Costela trapézio. void AddQuad (Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4) { int vertexIndex = vertices.Count; vertices.Add(v1); vertices.Add(v2); vertices.Add(v3); vertices.Add(v4); triangles.Add(vertexIndex); triangles.Add(vertexIndex + 2); triangles.Add(vertexIndex + 1); triangles.Add(vertexIndex + 1); triangles.Add(vertexIndex + 2); triangles.Add(vertexIndex + 3); } void AddQuadColor (Color c1, Color c2, Color c3, Color c4) { colors.Add(c1); colors.Add(c2); colors.Add(c3); colors.Add(c4); }
Nós o refazemos HexMesh.Triangulate
para que o triângulo receba uma cor e o quadrilátero faça uma mistura entre uma cor sólida e as cores de dois ângulos. void Triangulate (HexDirection direction, HexCell cell) { Vector3 center = cell.transform.localPosition; Vector3 v1 = center + HexMetrics.GetFirstSolidCorner(direction); Vector3 v2 = center + HexMetrics.GetSecondSolidCorner(direction); AddTriangle(center, v1, v2); AddTriangleColor(cell.color); Vector3 v3 = center + HexMetrics.GetFirstCorner(direction); Vector3 v4 = center + HexMetrics.GetSecondCorner(direction); AddQuad(v1, v2, v3, v4); HexCell prevNeighbor = cell.GetNeighbor(direction.Previous()) ?? cell; HexCell neighbor = cell.GetNeighbor(direction) ?? cell; HexCell nextNeighbor = cell.GetNeighbor(direction.Next()) ?? cell; AddQuadColor( cell.color, cell.color, (cell.color + prevNeighbor.color + neighbor.color) / 3f, (cell.color + neighbor.color + nextNeighbor.color) / 3f ); }
Mistura com costelas trapezoidais.Pontes entre costelas
A imagem está melhorando, mas o trabalho ainda não está concluído. A mistura de cores entre dois vizinhos é contaminada pelas células vizinhas. Para evitar isso, precisamos cortar os cantos do trapézio e transformá-lo em um retângulo. Depois disso, ele criará uma ponte entre a célula e seu vizinho, deixando lacunas nas laterais.A ponte entre as costelas.Podemos encontrar novas posições v3
e v4
, começando com v1
e v2
, e depois movendo-nos pela ponte até a borda da célula. Qual será o deslocamento da ponte? Podemos encontrá-lo tomando o ponto médio entre os dois ângulos correspondentes e aplicando o coeficiente de mistura a ele. Isso serve HexMetrics
. public static Vector3 GetBridge (HexDirection direction) { return (corners[(int)direction] + corners[(int)direction + 1]) * 0.5f * blendFactor; }
De volta a HexMesh
, agora será lógico adicionar uma opção AddQuadColor
que requer apenas duas cores. void AddQuadColor (Color c1, Color c2) { colors.Add(c1); colors.Add(c1); colors.Add(c2); colors.Add(c2); }
Altere-o Triangulate
para criar pontes adequadamente misturadas entre vizinhos. Vector3 bridge = HexMetrics.GetBridge(direction); Vector3 v3 = v1 + bridge; Vector3 v4 = v2 + bridge; AddQuad(v1, v2, v3, v4); HexCell prevNeighbor = cell.GetNeighbor(direction.Previous()) ?? cell; HexCell neighbor = cell.GetNeighbor(direction) ?? cell; HexCell nextNeighbor = cell.GetNeighbor(direction.Next()) ?? cell; AddQuadColor(cell.color, (cell.color + neighbor.color) * 0.5f);
Pontes pintadas corretamente com espaços de canto.Preenchendo as lacunas
Agora formamos um espaço triangular na junção de três células. Conseguimos essas lacunas cortando os lados triangulares do trapézio. Vamos recuperar esses triângulos.Primeiro, considere um triângulo conectado a um vizinho anterior. Seu primeiro vértice tem uma cor de célula. A cor do segundo pico será uma mistura de três cores. E o último pico terá a mesma cor do ponto no meio da ponte. Color bridgeColor = (cell.color + neighbor.color) * 0.5f; AddQuadColor(cell.color, bridgeColor); AddTriangle(v1, center + HexMetrics.GetFirstCorner(direction), v3); AddTriangleColor( cell.color, (cell.color + prevNeighbor.color + neighbor.color) / 3f, bridgeColor );
Quase tudo está pronto.Outro triângulo funciona da mesma maneira, exceto que a ponte não toca no terceiro, mas no segundo pico. AddTriangle(v2, v4, center + HexMetrics.GetSecondCorner(direction)); AddTriangleColor( cell.color, bridgeColor, (cell.color + neighbor.color + nextNeighbor.color) / 3f );
Coloração completa.Agora temos belas áreas de mistura que podemos fornecer de qualquer tamanho. As bordas podem ficar embaçadas ou nítidas como você quiser. Mas você pode ver que a mistura perto da borda da malha ainda não foi implementada corretamente. E, novamente, deixaremos para depois, focando em outro tópico por enquanto.Mas as transições entre cores ainda são feias. . .
unitypackageCostela Fusion
Dê uma olhada na topologia da nossa grade. Que formulários são visíveis aqui? Se você não prestar atenção à borda, podemos distinguir três tipos separados de formulários. Existem hexágonos de uma cor, retângulos de duas cores e triângulos de três cores. Todas essas três cores aparecem na junção das três células.Três estruturas visuais.Portanto, a cada dois hexágonos são conectados por uma ponte retangular. E a cada três hexágonos são conectados por um triângulo. No entanto, realizamos triangulações mais complexas. Agora usamos dois quadrângulos em vez de um para conectar um par de hexágonos. E para conectar os três hexágonos, usamos seis triângulos. Isso é muito redundante. Além disso, se quiséssemos conectar-se diretamente a uma forma, não precisaríamos de nenhuma média de cores. Portanto, seríamos capazes de sobreviver com menos complexidade, menos trabalho e menos triângulos.Mais difícil que o necessário.Por que precisamos disso?, . , , . , , . , , .
Ponte direta
Agora, nossas pontes entre as costelas consistem em dois quadrângulos. Para estendê-los ao próximo hexágono, precisamos dobrar o comprimento da ponte. Isso significa que não precisamos mais calcular a média de dois ângulos HexMetrics.GetBridge
. Em vez disso, basta adicioná-los e multiplicar pelo fator de mistura. public static Vector3 GetBridge (HexDirection direction) { return (corners[(int)direction] + corners[(int)direction + 1]) * blendFactor; }
As pontes se estendiam por todo o comprimento e se sobrepunham.As pontes agora criam conexões diretas entre hexágonos. Mas ainda geramos dois quadrângulos por conexão, um em cada direção. Ou seja, apenas um deles deve criar pontes entre duas células.Vamos simplificar nosso código de triangulação primeiro. Excluiremos tudo relacionado aos triângulos de arestas e à mistura de cores. Em seguida, mova o código que adiciona o quadrilátero da ponte ao novo método. Passamos os dois primeiros vértices para esse método, para que não tenhamos que recalculá-los. void Triangulate (HexDirection direction, HexCell cell) { Vector3 center = cell.transform.localPosition; Vector3 v1 = center + HexMetrics.GetFirstSolidCorner(direction); Vector3 v2 = center + HexMetrics.GetSecondSolidCorner(direction); AddTriangle(center, v1, v2); AddTriangleColor(cell.color); TriangulateConnection(direction, cell, v1, v2); } void TriangulateConnection ( HexDirection direction, HexCell cell, Vector3 v1, Vector3 v2 ) { HexCell neighbor = cell.GetNeighbor(direction) ?? cell; Vector3 bridge = HexMetrics.GetBridge(direction); Vector3 v3 = v1 + bridge; Vector3 v4 = v2 + bridge; AddQuad(v1, v2, v3, v4); AddQuadColor(cell.color, neighbor.color); }
Agora podemos facilmente limitar a triangulação de compostos. Para começar, adicionaremos a ponte somente ao trabalhar com a conexão NE. if (direction == HexDirection.NE) { TriangulateConnection(direction, cell, v1, v2); }
As pontes estão apenas na direção do NE.Parece que podemos cobrir todos os compostos triangulando-os apenas nas três primeiras direções: NE, E e SE. if (direction <= HexDirection.SE) { TriangulateConnection(direction, cell, v1, v2); }
Todas as pontes internas e pontes nas fronteiras.Cobrimos todas as conexões entre duas células vizinhas. Mas também temos algumas pontes que levam da célula para lugar nenhum. Vamos nos livrar deles, saindo TriangulateConnection
quando os vizinhos estiverem fora. Ou seja, não precisamos mais substituir os vizinhos ausentes pela própria célula. void TriangulateConnection ( HexDirection direction, HexCell cell, Vector3 v1, Vector3 v2 ) { HexCell neighbor = cell.GetNeighbor(direction); if (neighbor == null) { return; } … }
Somente pontes internas.Articulações triangulares
Agora precisamos fechar as lacunas triangulares novamente. Vamos fazer isso para um triângulo conectado ao próximo vizinho. E isso novamente precisa ser feito apenas quando existe um vizinho. void TriangulateConnection ( HexDirection direction, HexCell cell, Vector3 v1, Vector3 v2 ) { … HexCell nextNeighbor = cell.GetNeighbor(direction.Next()); if (nextNeighbor != null) { AddTriangle(v2, v4, v2); AddTriangleColor(cell.color, neighbor.color, nextNeighbor.color); } }
Qual será a posição do terceiro pico? Eu inseri como um substituto v2
, mas isso está obviamente errado. Como cada extremidade desses triângulos está conectada à ponte, podemos encontrá-la caminhando ao longo da ponte até o próximo vizinho. AddTriangle(v2, v4, v2 + HexMetrics.GetBridge(direction.Next()));
Estamos fazendo a triangulação completa novamente.Nós terminamos? Ainda não, porque agora estamos criando triângulos sobrepostos. Como as três células têm uma conexão triangular comum, precisamos adicioná-las para apenas duas conexões. Portanto, NE e E. farão. if (direction <= HexDirection.E && nextNeighbor != null) { AddTriangle(v2, v4, v2 + HexMetrics.GetBridge(direction.Next())); AddTriangleColor(cell.color, neighbor.color, nextNeighbor.color); }
unitypackageParte 3: alturas
Sumário
- Adicione a altura das células.
- Triangular as encostas.
- Inserir as bordas.
- Combine bordas e falésias.
Nesta parte do tutorial, adicionaremos suporte para diferentes níveis de altura e criaremos transições especiais entre eles.Alturas e bordas.Altura da célula
Dividimos nosso mapa em células separadas, cobrindo uma área plana. Agora daremos a cada célula seu próprio nível de altura. Usaremos níveis de elevação discretos para armazená-los como um campo inteiro em HexCell
. public int elevation;
Qual o tamanho de cada nível subseqüente de altura? Podemos usar qualquer valor, então vamos defini-lo como outra constante HexMetrics
. Usaremos uma etapa de cinco unidades para que as transições sejam claramente visíveis. Em um jogo real, eu usaria um passo menor. public const float elevationStep = 5f;
Alterar células
Até agora, só podíamos mudar a cor da célula, mas agora podemos mudar sua altura. Portanto, o método HexGrid.ColorCell
não é suficiente para nós. Além disso, no futuro, podemos adicionar outras opções de edição de células, por isso precisamos de uma nova abordagem.Mudar o nome ColorCell
em GetCell
e fazer de modo a que, ao contrário devolver células de cores de refercia de uma célula de uma posição predeterminada. Como esse método não altera mais nada, precisamos triangular imediatamente as células. public HexCell GetCell (Vector3 position) { position = transform.InverseTransformPoint(position); HexCoordinates coordinates = HexCoordinates.FromPosition(position); int index = coordinates.X + coordinates.Z * width + coordinates.Z / 2; return cells[index]; }
Agora o editor irá lidar com a alteração da célula. Após a conclusão do trabalho, a grade precisa ser triangulada novamente. Para fazer isso, adicione um método geral HexGrid.Refresh
. public void Refresh () { hexMesh.Triangulate(cells); }
Mude HexMapEditor
para que ele possa trabalhar com novos métodos. Vamos dar a ele um novo método EditCell
, que lidará com todas as alterações na célula, após o que atualizará a grade. void HandleInput () { Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(inputRay, out hit)) { EditCell(hexGrid.GetCell(hit.point)); } } void EditCell (HexCell cell) { cell.color = activeColor; hexGrid.Refresh(); }
Podemos alterar as alturas simplesmente atribuindo à célula desejada o nível de altura desejado. int activeElevation; void EditCell (HexCell cell) { cell.color = activeColor; cell.elevation = activeElevation; hexGrid.Refresh(); }
Assim como as cores, precisamos de um método para definir o nível de altura ativo, que associaremos à interface do usuário. Para selecionar valores do intervalo de altura, usamos o controle deslizante. Como os controles deslizantes funcionam com float, nosso método requer um parâmetro do tipo float. Vamos apenas convertê-lo para inteiro. public void SetElevation (float elevation) { activeElevation = (int)elevation; }
Adicione um controle deslizante ( GameObject / Create / Slider ) à tela e coloque-o sob a barra de cores. Tornamos vertical, de baixo para cima, para que corresponda visualmente aos níveis de altura. Nós o limitamos a números inteiros e criamos um intervalo adequado, por exemplo, de 0 a 6. Em seguida, anexamos o evento On Value Changed ao método de SetElevation
objeto do Hex Map Editor . O método deve ser selecionado na lista dinâmica para que seja chamado com o valor do controle deslizante.Controle deslizante de altura.Visualização de altura
Ao alterar uma célula, agora definimos cor e altura. Embora no inspetor possamos ver que a altura realmente muda, o processo de triangulação ainda a ignora.Basta mudar a posição local vertical da célula ao alterar a altura. Por conveniência, vamos tornar o método HexCell.elevation
privado e adicionar uma propriedade geral HexCell.Elevation
. public int Elevation { get { return elevation; } set { elevation = value; } } int elevation;
Agora podemos mudar a posição vertical da célula ao editar a altura. set { elevation = value; Vector3 position = transform.localPosition; position.y = value * HexMetrics.elevationStep; transform.localPosition = position; }
Claro, isso requer pequenas alterações para HexMapEditor.EditCell
. void EditCell (HexCell cell) { cell.color = activeColor; cell.Elevation = activeElevation; hexGrid.Refresh(); }
Células com diferentes alturas.O colisor de malha muda para se ajustar à nova altura?Unity mesh collider null. , , null . . ( ) .
A altura das células agora é visível, mas há dois problemas. Primeiro de tudo. os rótulos das células desaparecem sob as células levantadas. Em segundo lugar, as conexões entre células ignoram a altura. Vamos consertar.Alterar a posição dos rótulos das células
Atualmente, os rótulos de interface do usuário para células são criados e colocados apenas uma vez, após o que os esquecemos. Para atualizar suas posições verticais, precisamos rastreá-las. Vamos dar a todos um HexCell
link para RectTransform
seus rótulos de interface do usuário para que você possa atualizá-lo mais tarde. public RectTransform uiRect;
Atribua-os no final HexGrid.CreateCell
. void CreateCell (int x, int z, int i) { … cell.uiRect = label.rectTransform; }
Agora podemos expandir a propriedade HexCell.Elevation
para que ela também mude a posição da interface do usuário da célula. Como a tela da grade hexagonal é girada, os rótulos precisam ser movidos na direção negativa ao longo do eixo Z, e não no lado positivo do eixo Y. set { elevation = value; Vector3 position = transform.localPosition; position.y = value * HexMetrics.elevationStep; transform.localPosition = position; Vector3 uiPosition = uiRect.localPosition; uiPosition.z = elevation * -HexMetrics.elevationStep; uiRect.localPosition = uiPosition; }
Tags com altura.Criação de encostas
Agora precisamos converter as conexões de célula plana em declives. Isso é feito em HexMesh.TriangulateConnection
. No caso de conexões de borda, precisamos redefinir a altura da outra extremidade da ponte. Vector3 bridge = HexMetrics.GetBridge(direction); Vector3 v3 = v1 + bridge; Vector3 v4 = v2 + bridge; v3.y = v4.y = neighbor.Elevation * HexMetrics.elevationStep;
No caso de juntas de canto, precisamos fazer o mesmo com a ponte para o próximo vizinho. if (direction <= HexDirection.E && nextNeighbor != null) { Vector3 v5 = v2 + HexMetrics.GetBridge(direction.Next()); v5.y = nextNeighbor.Elevation * HexMetrics.elevationStep; AddTriangle(v2, v4, v5); AddTriangleColor(cell.color, neighbor.color, nextNeighbor.color); }
Conexão levando em consideração a altura.Agora temos suporte para células em diferentes alturas, com as articulações inclinadas corretas entre elas. Mas não vamos parar por aí. Tornaremos essas encostas mais interessantes.unitypackageArticulações com nervuras
Encostas retas não parecem muito atraentes. Podemos dividi-los em várias etapas adicionando etapas. Essa abordagem é usada no jogo Endless Legend.Por exemplo, podemos inserir duas bordas em cada inclinação. Como resultado, uma grande inclinação se transforma em três pequenas, entre as quais existem duas áreas planas. Para triangular esse esquema, teremos que separar cada conexão em cinco estágios.Duas bordas na encosta.Podemos definir o número de etapas da inclinação HexMetrics
e calcular o número de etapas com base nisso. public const int terracesPerSlope = 2; public const int terraceSteps = terracesPerSlope * 2 + 1;
Idealmente, poderíamos simplesmente interpolar cada passo ao longo da encosta. Mas isso não é inteiramente trivial, porque a coordenada Y deve mudar apenas em estágios ímpares. Caso contrário, não teremos bordas planas. Vamos adicionar um método de interpolação especial para isso HexMetrics
. public static Vector3 TerraceLerp (Vector3 a, Vector3 b, int step) { return a; }
A interpolação horizontal é simples se soubermos o tamanho da etapa de interpolação. public const float horizontalTerraceStepSize = 1f / terraceSteps; public static Vector3 TerraceLerp (Vector3 a, Vector3 b, int step) { float h = step * HexMetrics.horizontalTerraceStepSize; ax += (bx - ax) * h; az += (bz - az) * h; return a; }
Como funciona a interpolação entre dois valores?a e b t . t 0, a . 1, b . t - 0 1, a e b . : (1−t)a+tb .
, (1−t)a+tb=a−ta+tb=a+t(b−a) . a b (b−a) . , .
Para alterar Y apenas em estágios ímpares, podemos usar ( s t e p + 1 ) / 2 .
Se usarmos a divisão inteira, ela transformará as séries 1, 2, 3, 4 em 1, 1, 2, 2. public const float verticalTerraceStepSize = 1f / (terracesPerSlope + 1); public static Vector3 TerraceLerp (Vector3 a, Vector3 b, int step) { float h = step * HexMetrics.horizontalTerraceStepSize; ax += (bx - ax) * h; az += (bz - az) * h; float v = ((step + 1) / 2) * HexMetrics.verticalTerraceStepSize; ay += (by - ay) * v; return a; }
Vamos adicionar um método para interpolar bordas de cores também. Apenas interpole-os como se as conexões fossem planas. public static Color TerraceLerp (Color a, Color b, int step) { float h = step * HexMetrics.horizontalTerraceStepSize; return Color.Lerp(a, b, h); }
Triangulação
À medida que a triangulação da conexão de borda se torna mais complicada, removemos o código correspondente HexMesh.TriangulateConnection
e o colocamos em um método separado. Nos comentários, salvarei o código-fonte para consultá-lo no futuro. void TriangulateConnection ( HexDirection direction, HexCell cell, Vector3 v1, Vector3 v2 ) { … Vector3 bridge = HexMetrics.GetBridge(direction); Vector3 v3 = v1 + bridge; Vector3 v4 = v2 + bridge; v3.y = v4.y = neighbor.Elevation * HexMetrics.elevationStep; TriangulateEdgeTerraces(v1, v2, cell, v3, v4, neighbor);
Vamos começar desde o primeiro passo do processo. Usaremos nossos métodos especiais de interpolação para criar o primeiro quad. Nesse caso, uma inclinação curta deve ser criada, mais íngreme que a original. void TriangulateEdgeTerraces ( Vector3 beginLeft, Vector3 beginRight, HexCell beginCell, Vector3 endLeft, Vector3 endRight, HexCell endCell ) { Vector3 v3 = HexMetrics.TerraceLerp(beginLeft, endLeft, 1); Vector3 v4 = HexMetrics.TerraceLerp(beginRight, endRight, 1); Color c2 = HexMetrics.TerraceLerp(beginCell.color, endCell.color, 1); AddQuad(beginLeft, beginRight, v3, v4); AddQuadColor(beginCell.color, c2); }
O primeiro passo na criação de uma borda.Agora iremos imediatamente para a última etapa, pulando tudo no meio. Isso completará a conexão das arestas, embora até agora com uma forma irregular. AddQuad(beginLeft, beginRight, v3, v4); AddQuadColor(beginCell.color, c2); AddQuad(v3, v4, endLeft, endRight); AddQuadColor(c2, endCell.color);
O último passo na criação de uma borda.Etapas intermediárias podem ser adicionadas através do loop. Em cada estágio, os dois últimos vértices anteriores se tornam o novo primeiro. O mesmo vale para a cor. Depois de calcular os novos vetores e cores, outro quad é adicionado. AddQuad(beginLeft, beginRight, v3, v4); AddQuadColor(beginCell.color, c2); for (int i = 2; i < HexMetrics.terraceSteps; i++) { Vector3 v1 = v3; Vector3 v2 = v4; Color c1 = c2; v3 = HexMetrics.TerraceLerp(beginLeft, endLeft, i); v4 = HexMetrics.TerraceLerp(beginRight, endRight, i); c2 = HexMetrics.TerraceLerp(beginCell.color, endCell.color, i); AddQuad(v1, v2, v3, v4); AddQuadColor(c1, c2); } AddQuad(v3, v4, endLeft, endRight); AddQuadColor(c2, endCell.color);
Todas as etapas intermediárias.Agora todas as juntas de aresta têm duas bordas, ou qualquer outro número que você especificar HexMetrics.terracesPerSlope
. Obviamente, até criarmos bordas para juntas de canto, deixaremos isso para mais tarde.Todas as juntas de arestas têm bordas.unitypackageTipos de conexão
Converter todas as juntas de arestas em bordas não é uma boa idéia. Eles só ficam bem quando a diferença de altura é apenas um nível. Mas com uma diferença maior, bordas estreitas são criadas com grandes lacunas entre elas, e isso não parece muito bonito. Além disso, não precisamos criar bordas para todas as juntas.Vamos formalizar isso e definir três tipos de arestas: um plano, uma inclinação e um penhasco. Vamos criar uma enumeração para isso. public enum HexEdgeType { Flat, Slope, Cliff }
Como determinar com que tipo de conexão estamos lidando? Para fazer isso, podemos adicionar um HexMetrics
método que usa dois níveis de altura. public static HexEdgeType GetEdgeType (int elevation1, int elevation2) { }
Se as alturas forem as mesmas, teremos uma nervura plana. public static HexEdgeType GetEdgeType (int elevation1, int elevation2) { if (elevation1 == elevation2) { return HexEdgeType.Flat; } }
Se a diferença nos níveis for igual a um passo, então esta é uma inclinação. Não importa se sobe ou desce. Em todos os outros casos, temos uma pausa. public static HexEdgeType GetEdgeType (int elevation1, int elevation2) { if (elevation1 == elevation2) { return HexEdgeType.Flat; } int delta = elevation2 - elevation1; if (delta == 1 || delta == -1) { return HexEdgeType.Slope; } return HexEdgeType.Cliff; }
Vamos também adicionar um método conveniente HexCell.GetEdgeType
para obter o tipo de borda da célula em uma determinada direção. public HexEdgeType GetEdgeType (HexDirection direction) { return HexMetrics.GetEdgeType( elevation, neighbors[(int)direction].elevation ); }
Não precisamos verificar se existe um vizinho nessa direção?, , . , NullReferenceException
. , , - . , . .
, , , . - , NullReferenceException
.
Crie saliências apenas para inclinações
Agora que podemos determinar o tipo de conexão, podemos decidir se deseja inserir bordas. Mude HexMesh.TriangulateConnection
para que ele crie bordas apenas para as encostas. if (cell.GetEdgeType(direction) == HexEdgeType.Slope) { TriangulateEdgeTerraces(v1, v2, cell, v3, v4, neighbor); }
Nesse ponto, podemos descomentar o código comentado anteriormente para que ele possa manipular planos e recortes. if (cell.GetEdgeType(direction) == HexEdgeType.Slope) { TriangulateEdgeTerraces(v1, v2, cell, v3, v4, neighbor); } else { AddQuad(v1, v2, v3, v4); AddQuadColor(cell.color, neighbor.color); }
As etapas são criadas apenas nas encostas.unitypackageBordas com bordas
As juntas de canto são mais complexas do que as juntas de borda, porque estão envolvidas não em duas, mas em três células. Cada canto é conectado a três arestas, que podem ser planos, declives ou falésias. Portanto, existem muitas configurações possíveis. Assim como nas articulações das costelas, é melhor adicionar HexMesh
triangulação ao novo método.Nosso novo método exigirá os vértices de um triângulo angular e células conectadas. Por conveniência, vamos organizar as conexões para saber qual célula tem a menor altura. Depois disso, podemos começar a trabalhar a partir do canto inferior esquerdo e direito.Junta de canto. void TriangulateCorner ( Vector3 bottom, HexCell bottomCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { AddTriangle(bottom, left, right); AddTriangleColor(bottomCell.color, leftCell.color, rightCell.color); }
Agora devo TriangulateConnection
determinar qual das células é a mais baixa. Primeiro, verificamos se a célula triangulada está abaixo de seus vizinhos ou se está no mesmo nível com o menor. Nesse caso, podemos usá-lo como a célula mais baixa. void TriangulateConnection ( HexDirection direction, HexCell cell, Vector3 v1, Vector3 v2 ) { … HexCell nextNeighbor = cell.GetNeighbor(direction.Next()); if (direction <= HexDirection.E && nextNeighbor != null) { Vector3 v5 = v2 + HexMetrics.GetBridge(direction.Next()); v5.y = nextNeighbor.Elevation * HexMetrics.elevationStep; if (cell.Elevation <= neighbor.Elevation) { if (cell.Elevation <= nextNeighbor.Elevation) { TriangulateCorner(v2, cell, v4, neighbor, v5, nextNeighbor); } } } }
Se a verificação mais profunda falhar, isso significa que o próximo vizinho é a célula mais baixa. Para uma orientação adequada, precisamos rotacionar o triângulo no sentido anti-horário. if (cell.Elevation <= neighbor.Elevation) { if (cell.Elevation <= nextNeighbor.Elevation) { TriangulateCorner(v2, cell, v4, neighbor, v5, nextNeighbor); } else { TriangulateCorner(v5, nextNeighbor, v2, cell, v4, neighbor); } }
Se o primeiro teste falhar, você precisará comparar duas células vizinhas. Se o vizinho da nervura for o mais baixo, você precisará girar no sentido horário, caso contrário - no sentido anti-horário. if (cell.Elevation <= neighbor.Elevation) { if (cell.Elevation <= nextNeighbor.Elevation) { TriangulateCorner(v2, cell, v4, neighbor, v5, nextNeighbor); } else { TriangulateCorner(v5, nextNeighbor, v2, cell, v4, neighbor); } } else if (neighbor.Elevation <= nextNeighbor.Elevation) { TriangulateCorner(v4, neighbor, v5, nextNeighbor, v2, cell); } else { TriangulateCorner(v5, nextNeighbor, v2, cell, v4, neighbor); }
Gire no sentido anti-horário, sem volta, rotação no sentido horário.Triangulação da inclinação
Para saber como triangular um ângulo, precisamos entender com que tipos de arestas estamos lidando. Para simplificar esta tarefa, vamos adicionar HexCell
outro método conveniente para reconhecer a inclinação entre duas células. public HexEdgeType GetEdgeType (HexCell otherCell) { return HexMetrics.GetEdgeType( elevation, otherCell.elevation ); }
Usamos esse novo método HexMesh.TriangulateCorner
para determinar os tipos de arestas esquerda e direita. void TriangulateCorner ( Vector3 bottom, HexCell bottomCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { HexEdgeType leftEdgeType = bottomCell.GetEdgeType(leftCell); HexEdgeType rightEdgeType = bottomCell.GetEdgeType(rightCell); AddTriangle(bottom, left, right); AddTriangleColor(bottomCell.color, leftCell.color, rightCell.color); }
Se as duas nervuras forem inclinadas, teremos bordas à esquerda e à direita. Além disso, como a célula inferior é a mais baixa, sabemos que essas encostas se elevam. Além disso, as células esquerda e direita têm a mesma altura, ou seja, a conexão da borda superior é plana. Podemos designar esse caso como "plano de declive-declive" ou MTP.Duas inclinações e um plano, BSCVamos verificar se estamos nessa situação e, se estiver, chamaremos um novo método TriangulateCornerTerraces
. Depois disso, retornaremos do método Insira essa verificação antes do código de triangulação antigo, para que substitua o triângulo original. void TriangulateCorner ( Vector3 bottom, HexCell bottomCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { HexEdgeType leftEdgeType = bottomCell.GetEdgeType(leftCell); HexEdgeType rightEdgeType = bottomCell.GetEdgeType(rightCell); if (leftEdgeType == HexEdgeType.Slope) { if (rightEdgeType == HexEdgeType.Slope) { TriangulateCornerTerraces( bottom, bottomCell, left, leftCell, right, rightCell ); return; } } AddTriangle(bottom, left, right); AddTriangleColor(bottomCell.color, leftCell.color, rightCell.color); } void TriangulateCornerTerraces ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { }
Como não estamos fazendo nada lá dentro TriangulateCornerTerraces
, algumas junções de esquina com duas pistas se tornarão vazias. Se a conexão fica vazia ou não, depende de qual das células é mais baixa.Há um vazio.Para preencher o vazio, precisamos conectar a borda esquerda e direita através de um espaço. A abordagem aqui é a mesma que para unir arestas, mas dentro de um triângulo de três cores em vez de um quadrilátero de duas cores. Vamos começar de novo com o primeiro estágio, que agora é um triângulo. void TriangulateCornerTerraces ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { Vector3 v3 = HexMetrics.TerraceLerp(begin, left, 1); Vector3 v4 = HexMetrics.TerraceLerp(begin, right, 1); Color c3 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, 1); Color c4 = HexMetrics.TerraceLerp(beginCell.color, rightCell.color, 1); AddTriangle(begin, v3, v4); AddTriangleColor(beginCell.color, c3, c4); }
A primeira etapa do triângulo.E novamente vamos direto para a última etapa. Este é o quadrilátero que forma um trapézio. A única diferença das conexões de borda aqui é que não estamos lidando com duas, mas com quatro cores. AddTriangle(begin, v3, v4); AddTriangleColor(beginCell.color, c3, c4); AddQuad(v3, v4, left, right); AddQuadColor(c3, c4, leftCell.color, rightCell.color);
A última etapa do quadrilátero.Todos os estágios entre eles também são quadrângulos. AddTriangle(begin, v3, v4); AddTriangleColor(beginCell.color, c3, c4); for (int i = 2; i < HexMetrics.terraceSteps; i++) { Vector3 v1 = v3; Vector3 v2 = v4; Color c1 = c3; Color c2 = c4; v3 = HexMetrics.TerraceLerp(begin, left, i); v4 = HexMetrics.TerraceLerp(begin, right, i); c3 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, i); c4 = HexMetrics.TerraceLerp(beginCell.color, rightCell.color, i); AddQuad(v1, v2, v3, v4); AddQuadColor(c1, c2, c3, c4); } AddQuad(v3, v4, left, right); AddQuadColor(c3, c4, leftCell.color, rightCell.color);
Todas as etapas.Variações de duas inclinações
O caso com duas inclinações tem duas variações com orientações diferentes, dependendo de qual das células é a parte inferior. Podemos encontrá-los verificando combinações esquerda-direita para plano de inclinação e plano-inclinação.ATP e MSS.Se a borda direita for plana, devemos começar a criar bordas à esquerda, e não na parte inferior. Se a borda esquerda for plana, será necessário começar pela direita. if (leftEdgeType == HexEdgeType.Slope) { if (rightEdgeType == HexEdgeType.Slope) { TriangulateCornerTerraces( bottom, bottomCell, left, leftCell, right, rightCell ); return; } if (rightEdgeType == HexEdgeType.Flat) { TriangulateCornerTerraces( left, leftCell, right, rightCell, bottom, bottomCell ); return; } } if (rightEdgeType == HexEdgeType.Slope) { if (leftEdgeType == HexEdgeType.Flat) { TriangulateCornerTerraces( right, rightCell, bottom, bottomCell, left, leftCell ); return; } }
Devido a isso, as bordas percorrerão as células sem interrupção até chegarem ao penhasco ou ao final do mapa.Bordas sólidas.unitypackageMesclagem de encostas e falésias
Que tal conectar a encosta e o penhasco? Se sabemos que a borda esquerda é uma inclinação e a borda direita é um penhasco, qual será a borda superior? Não pode ser plano, mas pode ser uma encosta ou um penhasco.SOS e COO.Vamos adicionar um novo método para lidar com todos os casos de encostas. void TriangulateCornerTerracesCliff ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { }
Deve ser chamada como a última opção TriangulateCorner
quando a borda esquerda é uma inclinação. if (leftEdgeType == HexEdgeType.Slope) { if (rightEdgeType == HexEdgeType.Slope) { TriangulateCornerTerraces( bottom, bottomCell, left, leftCell, right, rightCell ); return; } if (rightEdgeType == HexEdgeType.Flat) { TriangulateCornerTerraces( left, leftCell, right, rightCell, bottom, bottomCell ); return; } TriangulateCornerTerracesCliff( bottom, bottomCell, left, leftCell, right, rightCell ); return; } if (rightEdgeType == HexEdgeType.Slope) { if (leftEdgeType == HexEdgeType.Flat) { TriangulateCornerTerraces( right, rightCell, bottom, bottomCell, left, leftCell ); return; } }
Como triangulamos isso? Esta tarefa pode ser dividida em duas partes: inferior e superior.Parte inferior
A parte inferior tem bordas à esquerda e um penhasco à direita. Precisamos de alguma forma combiná-los. A maneira mais fácil de fazer isso é apertando as bordas para que elas se encontrem no canto direito. Isso aumentará as bordas.Compressão de bordas.Mas, na verdade, não queremos que eles se encontrem no canto direito, porque isso interferirá nas bordas que possam existir acima. Além disso, podemos lidar com um penhasco muito alto, devido ao qual temos triângulos muito finos e finos. Em vez disso, vamos compactá-los para um ponto limite que se encontra ao longo de um penhasco.Compressão na fronteira.Vamos posicionar o ponto de limite um nível acima da célula inferior. Você pode encontrá-lo por interpolação com base na diferença de altura. void TriangulateCornerTerracesCliff ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { float b = 1f / (rightCell.Elevation - beginCell.Elevation); Vector3 boundary = Vector3.Lerp(begin, right, b); Color boundaryColor = Color.Lerp(beginCell.color, rightCell.color, b); }
Para garantir que obtivemos corretamente, cobrimos toda a parte inferior com um triângulo. float b = 1f / (rightCell.Elevation - beginCell.Elevation); Vector3 boundary = Vector3.Lerp(begin, right, b); Color boundaryColor = Color.Lerp(beginCell.color, rightCell.color, b); AddTriangle(begin, left, boundary); AddTriangleColor(beginCell.color, leftCell.color, boundaryColor);
Triângulo inferior.Tendo colocado a borda no lugar certo, podemos proceder à triangulação das bordas. Vamos começar de novo apenas a partir do primeiro estágio. float b = 1f / (rightCell.Elevation - beginCell.Elevation); Vector3 boundary = Vector3.Lerp(begin, right, b); Color boundaryColor = Color.Lerp(beginCell.color, rightCell.color, b); Vector3 v2 = HexMetrics.TerraceLerp(begin, left, 1); Color c2 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, 1); AddTriangle(begin, v2, boundary); AddTriangleColor(beginCell.color, c2, boundaryColor);
O primeiro estágio da compactação.Desta vez, a última etapa também será um triângulo. AddTriangle(begin, v2, boundary); AddTriangleColor(beginCell.color, c2, boundaryColor); AddTriangle(v2, left, boundary); AddTriangleColor(c2, leftCell.color, boundaryColor);
O último estágio da compactação.E todos os passos intermediários também são triângulos. AddTriangle(begin, v2, boundary); AddTriangleColor(beginCell.color, c2, boundaryColor); for (int i = 2; i < HexMetrics.terraceSteps; i++) { Vector3 v1 = v2; Color c1 = c2; v2 = HexMetrics.TerraceLerp(begin, left, i); c2 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, i); AddTriangle(v1, v2, boundary); AddTriangleColor(c1, c2, boundaryColor); } AddTriangle(v2, left, boundary); AddTriangleColor(c2, leftCell.color, boundaryColor);
Bordas compactadas.Não podemos manter o nível da borda?, , , . . , . .
Conclusão de canto
Depois de terminar o fundo, você pode ir para o topo. Se a aresta superior for uma inclinação, então novamente precisaremos conectar as bordas e o penhasco. Então, vamos mover esse código para um método separado. void TriangulateCornerTerracesCliff ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { float b = 1f / (rightCell.Elevation - beginCell.Elevation); Vector3 boundary = Vector3.Lerp(begin, right, b); Color boundaryColor = Color.Lerp(beginCell.color, rightCell.color, b); TriangulateBoundaryTriangle( begin, beginCell, left, leftCell, boundary, boundaryColor ); } void TriangulateBoundaryTriangle ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 boundary, Color boundaryColor ) { Vector3 v2 = HexMetrics.TerraceLerp(begin, left, 1); Color c2 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, 1); AddTriangle(begin, v2, boundary); AddTriangleColor(beginCell.color, c2, boundaryColor); for (int i = 2; i < HexMetrics.terraceSteps; i++) { Vector3 v1 = v2; Color c1 = c2; v2 = HexMetrics.TerraceLerp(begin, left, i); c2 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, i); AddTriangle(v1, v2, boundary); AddTriangleColor(c1, c2, boundaryColor); } AddTriangle(v2, left, boundary); AddTriangleColor(c2, leftCell.color, boundaryColor); }
Agora, completar o topo será fácil. Se tivermos uma inclinação, adicione o triângulo girado da borda. Caso contrário, um simples triângulo é suficiente. void TriangulateCornerTerracesCliff ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { float b = 1f / (rightCell.Elevation - beginCell.Elevation); Vector3 boundary = Vector3.Lerp(begin, right, b); Color boundaryColor = Color.Lerp(beginCell.color, rightCell.color, b); TriangulateBoundaryTriangle( begin, beginCell, left, leftCell, boundary, boundaryColor ); if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) { TriangulateBoundaryTriangle( left, leftCell, right, rightCell, boundary, boundaryColor ); } else { AddTriangle(left, right, boundary); AddTriangleColor(leftCell.color, rightCell.color, boundaryColor); } }
Triangulação completa de ambas as partes.Casos espelhados
Examinamos os casos de "declive-penhasco". Há também dois casos de espelho, cada um com um penhasco à esquerda.OSS e CCA.Usaremos a abordagem anterior, com pequenas diferenças devido a uma mudança na orientação. Nós copiamos TriangulateCornerTerracesCliff
e alteramos de acordo. void TriangulateCornerCliffTerraces ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { float b = 1f / (leftCell.Elevation - beginCell.Elevation); Vector3 boundary = Vector3.Lerp(begin, left, b); Color boundaryColor = Color.Lerp(beginCell.color, leftCell.color, b); TriangulateBoundaryTriangle( right, rightCell, begin, beginCell, boundary, boundaryColor ); if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) { TriangulateBoundaryTriangle( left, leftCell, right, rightCell, boundary, boundaryColor ); } else { AddTriangle(left, right, boundary); AddTriangleColor(leftCell.color, rightCell.color, boundaryColor); } }
Adicione esses casos a TriangulateCorner
. if (leftEdgeType == HexEdgeType.Slope) { … } if (rightEdgeType == HexEdgeType.Slope) { if (leftEdgeType == HexEdgeType.Flat) { TriangulateCornerTerraces( right, rightCell, bottom, bottomCell, left, leftCell ); return; } TriangulateCornerCliffTerraces( bottom, bottomCell, left, leftCell, right, rightCell ); return; }
OSS triangulado e CCA.Falésias duplas
Os únicos casos não-planares restantes são as células inferiores com falésias de ambos os lados. Nesse caso, a costela superior pode ser qualquer - plana, encosta ou falésia. Estamos interessados apenas no caso “penhasco-penhasco”, porque ele só terá bordas.De fato, existem duas versões diferentes da “encosta do penhasco”, dependendo de qual lado é mais alto. São imagens de espelho um do outro. Vamos designá-los como OOSP e OOSL.OOSP e OOSL.Podemos cobrir ambos os casos TriangulateCorner
chamando métodos TriangulateCornerCliffTerraces
e TriangulateCornerTerracesCliff
com diferentes rotações de células. if (leftEdgeType == HexEdgeType.Slope) { … } if (rightEdgeType == HexEdgeType.Slope) { … } if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) { if (leftCell.Elevation < rightCell.Elevation) { TriangulateCornerCliffTerraces( right, rightCell, bottom, bottomCell, left, leftCell ); } else { TriangulateCornerTerracesCliff( left, leftCell, right, rightCell, bottom, bottomCell ); } return; }
No entanto, isso cria uma triangulação estranha. Isso ocorre porque agora estamos triangulando de cima para baixo. Por esse motivo, nossa borda é interpolada como negativa, o que está incorreto. A solução aqui é sempre ter interpoladores positivos. void TriangulateCornerTerracesCliff ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { float b = 1f / (rightCell.Elevation - beginCell.Elevation); if (b < 0) { b = -b; } … } void TriangulateCornerCliffTerraces ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { float b = 1f / (leftCell.Elevation - beginCell.Elevation); if (b < 0) { b = -b; } … }
OOSP triangulado e OOSL.Varrer
Examinamos todos os casos que requerem tratamento especial para garantir a triangulação correta das bordas.Triangulação completa com bordas.Podemos limpar um pouco nos TriangulateCorner
livrando dos operadores return
e usando blocos else
. void TriangulateCorner ( Vector3 bottom, HexCell bottomCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { HexEdgeType leftEdgeType = bottomCell.GetEdgeType(leftCell); HexEdgeType rightEdgeType = bottomCell.GetEdgeType(rightCell); if (leftEdgeType == HexEdgeType.Slope) { if (rightEdgeType == HexEdgeType.Slope) { TriangulateCornerTerraces( bottom, bottomCell, left, leftCell, right, rightCell ); } else if (rightEdgeType == HexEdgeType.Flat) { TriangulateCornerTerraces( left, leftCell, right, rightCell, bottom, bottomCell ); } else { TriangulateCornerTerracesCliff( bottom, bottomCell, left, leftCell, right, rightCell ); } } else if (rightEdgeType == HexEdgeType.Slope) { if (leftEdgeType == HexEdgeType.Flat) { TriangulateCornerTerraces( right, rightCell, bottom, bottomCell, left, leftCell ); } else { TriangulateCornerCliffTerraces( bottom, bottomCell, left, leftCell, right, rightCell ); } } else if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) { if (leftCell.Elevation < rightCell.Elevation) { TriangulateCornerCliffTerraces( right, rightCell, bottom, bottomCell, left, leftCell ); } else { TriangulateCornerTerracesCliff( left, leftCell, right, rightCell, bottom, bottomCell ); } } else { AddTriangle(bottom, left, right); AddTriangleColor(bottomCell.color, leftCell.color, rightCell.color); } }
O último bloco else
cobre todos os casos restantes que ainda não foram cobertos. Esses casos são RFP (avião-avião-avião), OOP, LLC e LLC. Todos eles são cobertos por um triângulo..unitypackage