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 4: Rugosidade
Sumário
- Experimente a textura do ruído.
- Mova os vértices.
- Preservamos o nivelamento das células.
- Subdividir as bordas das células.
Enquanto nossa grade era um padrão estrito de favos de mel. Nesta parte, adicionaremos solavancos para tornar o mapa mais natural.
Não há mais hexágonos.O barulho
Para adicionar solavancos, precisamos de randomização, mas não de aleatoriedade verdadeira. Queremos que tudo seja consistente ao alterar o mapa. Caso contrário, quando você fizer alguma alteração, os objetos irão pular. Ou seja, precisamos de alguma forma de ruído pseudo-aleatório reproduzível.
Um bom candidato é o barulho de Perlin. É reproduzível em qualquer lugar. Ao combinar várias frequências, também cria ruído, que pode variar bastante em grandes distâncias, mas permanece quase o mesmo em pequenas distâncias. Graças a isso, distorções relativamente suaves podem ser criadas. Os pontos adjacentes um ao outro geralmente permanecem próximos e não estão espalhados em direções opostas.
Podemos gerar ruído Perlin programaticamente. No tutorial
Noise , explico como fazer isso. Mas também podemos obter amostras de uma textura de ruído pré-gerada. A vantagem de usar a textura é que ela é mais simples e muito mais rápida que o cálculo do ruído multi-frequência da Perlin. Sua desvantagem é que a textura ocupa mais memória e cobre apenas uma pequena área de ruído. Portanto, deve ser perfeitamente conectado e grande o suficiente para que a repetição não seja impressionante.
Textura de ruído
Usaremos a textura, portanto o tutorial
Noise é opcional. Então, precisamos de uma textura. Aqui está:
Conecte perfeitamente a textura de ruído perlin.A textura mostrada acima contém o ruído de múltiplas frequências perfeitamente integrado da Perlin. Esta é uma imagem em escala de cinza. Seu valor médio é 0,5 e os valores extremos tendem a 0 e 1.
Mas espere, só há um valor para cada ponto. Se precisamos de distorção 3D, precisamos de pelo menos três amostras pseudo-aleatórias! Portanto, precisamos de mais duas texturas com ruído diferente.
Podemos criá-los ou armazenar diferentes valores de ruído em cada um dos canais de cores. Isso nos permitirá armazenar até quatro padrões de ruído em uma textura. Aqui está essa textura.
Quatro em um.Como criar essa textura?Eu usei o
NumberFlow . Este é o editor de textura processual que criei para o Unity.
Faça o download dessa textura e importe-a para o seu projeto do Unity. Como vamos amostrar a textura através do código, ela deve ser legível. Alterne o
Tipo de textura para
Avançado e ative a opção
Leitura / Gravação ativada . Isso salvará os dados de textura na memória e pode ser acessado a partir do código C #. Defina
Format como
Automatic Truecolor , caso contrário nada funcionará. Não queremos que a compressão de textura destrua nosso padrão de ruído.
Você pode desativar o
recurso Gerar mapas Mip , porque não precisamos deles. Ative também a opção
Bypass sRGB Sampling . Não precisaremos disso, mas será assim. Este parâmetro indica que a textura não contém dados de cores no espaço gama.
Textura de ruído importada.
Quando a amostragem sRGB é importante?Se quiséssemos usar uma textura em um shader, isso faria diferença. Ao usar o modo de renderização Linear, a amostragem da textura converte automaticamente os dados de cores da gama em um espaço de cores linear. No caso de nossa textura de ruído, isso levará a resultados incorretos, portanto não precisamos disso.
Por que minhas configurações de importação de textura parecem diferentes?Eles foram alterados após a criação deste tutorial. Você precisa usar as configurações de textura 2D padrão, sRGB (Textura de cor) deve estar desativado e Compactação deve estar definido como Nenhum .
Amostragem de ruído
Vamos adicionar a funcionalidade de amostragem de ruído ao
HexMetrics
para que você possa usá-lo em qualquer lugar. Isso significa que o
HexMetrics
deve conter uma referência à textura do ruído.
public static Texture2D noiseSource;
Como este não é um componente, não podemos atribuir uma textura através do editor. Portanto, como intermediário, usamos o
HexGrid
. Como o
HexGrid
atuará primeiro, não haverá problema se passarmos a textura no início de seu método
Awake
.
public Texture2D noiseSource; void Awake () { HexMetrics.noiseSource = noiseSource; … }
No entanto, essa abordagem não sobreviverá à recompilação no modo de reprodução. Variáveis estáticas não são serializadas pelo mecanismo do Unity. Para resolver esse problema, reatribua também a textura no método de evento
OnEnable
. Este método será chamado após a recompilação.
void OnEnable () { HexMetrics.noiseSource = noiseSource; }
Atribua uma textura de ruído.Agora que o
HexMetrics
tem acesso à textura, vamos adicionar um método conveniente de amostragem de ruído. Este método assume uma posição no mundo e cria um vetor 4D contendo quatro amostras de ruído.
public static Vector4 SampleNoise (Vector3 position) { }
As amostras foram criadas por amostragem da textura usando filtragem bilinear, na qual as coordenadas do mundo X e Z foram usadas como coordenadas UV. Como nossa fonte de ruído é bidimensional, ignoramos a terceira coordenada do mundo. Se a fonte de ruído fosse tridimensional, também usaríamos a coordenada Y.
Como resultado, obtemos uma cor que pode ser convertida em um vetor 4D. Essa redução pode ser indireta, ou seja, podemos retornar a cor diretamente, sem incluir explicitamente
(Vector4)
.
public static Vector4 SampleNoise (Vector3 position) { return noiseSource.GetPixelBilinear(position.x, position.z); }
unitypackageMovimento de vértice
Distorceremos nossa grade suave de favos de mel, movendo individualmente cada um dos vértices. Para fazer isso, vamos adicionar o método
Perturb
ao
Perturb
. É preciso um ponto imóvel e retorna o ponto movido. Para fazer isso, ele usa um ponto sem deslocamento ao amostrar ruído.
Vector3 Perturb (Vector3 position) { Vector4 sample = HexMetrics.SampleNoise(position); }
Vamos apenas adicionar as amostras de ruído X, Y e Z diretamente às coordenadas dos pontos correspondentes e usá-las como resultado.
Vector3 Perturb (Vector3 position) { Vector4 sample = HexMetrics.SampleNoise(position); position.x += sample.x; position.y += sample.y; position.z += sample.z; return position; }
Como
HexMesh
rapidamente o
HexMesh
para mover todos os vértices? Alterando cada vértice ao adicionar vértices à lista nos
AddQuad
e
AddQuad
. Vamos fazer isso.
void AddTriangle (Vector3 v1, Vector3 v2, Vector3 v3) { int vertexIndex = vertices.Count; vertices.Add(Perturb(v1)); vertices.Add(Perturb(v2)); vertices.Add(Perturb(v3)); … } void AddQuad (Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4) { int vertexIndex = vertices.Count; vertices.Add(Perturb(v1)); vertices.Add(Perturb(v2)); vertices.Add(Perturb(v3)); vertices.Add(Perturb(v4)); … }
Os quadrângulos permanecerão planos depois de mover seus vértices?Provavelmente não. Eles consistem em dois triângulos que não ficam mais no mesmo plano. No entanto, como esses triângulos têm dois vértices comuns, as normais desses vértices serão suavizadas. Isso significa que não teremos transições nítidas entre dois triângulos. Se a distorção não for muito grande, ainda perceberemos os quadrângulos como planos.
Os vértices são movidos ou não.Embora as alterações não sejam muito visíveis, apenas os rótulos das células desapareceram. Isso aconteceu porque adicionamos amostras de ruído aos pontos, e elas sempre são positivas. Portanto, como resultado, todos os triângulos se elevaram acima de suas marcas, fechando-os. Devemos centralizar as mudanças para que elas ocorram nas duas direções. Altere o intervalo da amostra de ruído de 0-1 para -1.
Vector3 Perturb (Vector3 position) { Vector4 sample = HexMetrics.SampleNoise(position); position.x += sample.x * 2f - 1f; position.y += sample.y * 2f - 1f; position.z += sample.z * 2f - 1f; return position; }
Deslocamento centralizado.A magnitude (força) do deslocamento
Agora é óbvio que distorcemos a grade, mas o efeito é quase imperceptível. A alteração em cada dimensão não é superior a 1 unidade. Ou seja, o deslocamento máximo teórico é de √3 ≈ 1,73 unidades, o que acontecerá muito raramente, se é que ocorrerá. Como o raio externo das células é de 10 unidades, os deslocamentos são relativamente pequenos.
A solução é adicionar um parâmetro de
HexMetrics
ao
HexMetrics
para que você possa dimensionar os movimentos. Vamos tentar usar a força 5. Nesse caso, o deslocamento máximo teórico será de √75 ≈ 8,66 unidades, o que é muito mais perceptível.
public const float cellPerturbStrength = 5f;
Aplicamos força multiplicando-a por amostras em
HexMesh.Perturb
.
Vector3 Perturb (Vector3 position) { Vector4 sample = HexMetrics.SampleNoise(position); position.x += (sample.x * 2f - 1f) * HexMetrics.cellPerturbStrength; position.y += (sample.y * 2f - 1f) * HexMetrics.cellPerturbStrength; position.z += (sample.z * 2f - 1f) * HexMetrics.cellPerturbStrength; return position; }
Força aumentada.Escala de ruído
Embora a grade pareça boa antes da mudança, tudo pode dar errado depois que as bordas aparecerem. Seus picos podem ser distorcidos em direções imprevisivelmente diferentes, criando o caos. Ao usar o ruído Perlin, isso não deve acontecer.
O problema surge porque usamos diretamente as coordenadas do mundo para provar o ruído. Por esse motivo, a textura fica oculta em cada unidade e as células são muito maiores que esse valor. De fato, a textura é amostrada em pontos arbitrários, destruindo sua integridade existente.
Linhas de 10 a 10 células de sobreposição de grade.Teremos que escalar a amostragem de ruído para que a textura cubra uma área muito maior. Vamos adicionar essa escala ao
HexMetrics
e atribuir um valor de 0,003 e, em seguida, dimensionar as coordenadas das amostras por esse fator.
public const float noiseScale = 0.003f; public static Vector4 SampleNoise (Vector3 position) { return noiseSource.GetPixelBilinear( position.x * noiseScale, position.z * noiseScale ); }
De repente, acontece que nossa textura cobre 333 & frac13; unidades quadradas e sua integridade local se torna aparente.
Ruído em escala.Além disso, uma nova escala aumenta a distância entre as articulações do ruído. De fato, como as células têm um diâmetro interno de 10√3 unidades, nunca serão exatamente lado a lado na dimensão X. No entanto, devido à integridade local do ruído, em uma escala maior, ainda seremos capazes de reconhecer padrões de repetição, aproximadamente a cada 20 células, mesmo se os detalhes não corresponderem. Mas eles serão óbvios apenas no mapa sem outras características.
unitypackageAlinhando centros celulares
Mover todos os vértices dá ao mapa uma aparência mais natural, mas há vários problemas. Como as células agora estão irregulares, seus rótulos se cruzam com a malha. E nas articulações das bordas dos penhascos surgem fendas. Vamos deixar as rachaduras para mais tarde, mas agora vamos nos concentrar nas superfícies das células.
O mapa ficou menos rigoroso, mas surgiram mais problemas.A maneira mais fácil de resolver o problema de interseção é tornar os centros das células achatados. Vamos apenas não alterar a coordenada Y no
HexMesh.Perturb
.
Vector3 Perturb (Vector3 position) { Vector4 sample = HexMetrics.SampleNoise(position); position.x += (sample.x * 2f - 1f) * HexMetrics.cellPerturbStrength;
Células alinhadas.Com essa mudança, todas as posições verticais permanecerão inalteradas, tanto no centro das células quanto nas etapas das bordas. Deve-se notar que isso reduz o deslocamento máximo para √50 ≈ 7,07 apenas no plano XZ.
Essa é uma boa mudança, porque simplifica a identificação de células individuais e não permite que as bordas fiquem caóticas demais. Mas ainda seria bom adicionar um pouco de movimento vertical.
Mover altura da célula
Em vez de aplicar o movimento vertical a cada vértice, podemos aplicá-lo a uma célula. Nesse caso, cada célula permanecerá plana, mas a variabilidade ainda permanecerá entre as células. Também seria lógico usar uma escala diferente para mover a altura; adicione-a ao
HexMetrics
. Uma força de 1,5 unidades cria uma pequena variação, aproximadamente igual à altura de um passo da borda.
public const float elevationPerturbStrength = 1.5f;
Altere a propriedade
HexCell.Elevation
para
HexCell.Elevation
essa movimentação à posição vertical da célula.
public int Elevation { get { return elevation; } set { elevation = value; Vector3 position = transform.localPosition; position.y = value * HexMetrics.elevationStep; position.y += (HexMetrics.SampleNoise(position).y * 2f - 1f) * HexMetrics.elevationPerturbStrength; transform.localPosition = position; Vector3 uiPosition = uiRect.localPosition; uiPosition.z = -position.y; uiRect.localPosition = uiPosition; } }
Para que a movimentação seja aplicada imediatamente, precisamos definir explicitamente a altura de cada célula em
HexGrid.CreateCell
. Caso contrário, a grade será inicialmente plana. Vamos fazer isso no final, depois de criar a interface do usuário.
void CreateCell (int x, int z, int i) { … cell.Elevation = 0; }
Alturas deslocadas com rachaduras.Usando as mesmas alturas
Muitas rachaduras apareceram na malha, porque quando triangulamos a malha, não usamos a mesma altura de célula. Vamos adicionar uma propriedade ao
HexCell
para obter sua posição, para que você possa usá-la em qualquer lugar.
public Vector3 Position { get { return transform.localPosition; } }
Agora podemos usar essa propriedade em
HexMesh.Triangulate
para determinar o centro da célula.
void Triangulate (HexDirection direction, HexCell cell) { Vector3 center = cell.Position; … }
E podemos usá-lo em
TriangulateConnection
ao definir as posições verticais das células vizinhas.
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.Position.y; … HexCell nextNeighbor = cell.GetNeighbor(direction.Next()); if (direction <= HexDirection.E && nextNeighbor != null) { Vector3 v5 = v2 + HexMetrics.GetBridge(direction.Next()); v5.y = nextNeighbor.Position.y; … } }
Uso consistente da altura da célula.unitypackageCell Edge Unit
Embora as células tenham uma bela variação, elas ainda parecem hexágonos óbvios. Isso por si só não é um problema, mas podemos melhorar sua aparência.
Células hexagonais claramente visíveis.Se tivéssemos mais vértices, haveria maior variabilidade local. Então, vamos dividir cada extremidade da célula em duas partes, adicionando a parte superior da borda no meio entre cada par de cantos. Isso significa que
HexMesh.Triangulate
deve adicionar não um, mas dois triângulos.
void Triangulate (HexDirection direction, HexCell cell) { Vector3 center = cell.Position; Vector3 v1 = center + HexMetrics.GetFirstSolidCorner(direction); Vector3 v2 = center + HexMetrics.GetSecondSolidCorner(direction); Vector3 e1 = Vector3.Lerp(v1, v2, 0.5f); AddTriangle(center, v1, e1); AddTriangleColor(cell.color); AddTriangle(center, e1, v2); AddTriangleColor(cell.color); if (direction <= HexDirection.SE) { TriangulateConnection(direction, cell, v1, v2); } }
Doze lados em vez de seis.A duplicação de vértices e triângulos adiciona mais variabilidade às bordas da célula. Vamos torná-los ainda mais irregulares, triplicando o número de vértices.
Vector3 e1 = Vector3.Lerp(v1, v2, 1f / 3f); Vector3 e2 = Vector3.Lerp(v1, v2, 2f / 3f); AddTriangle(center, v1, e1); AddTriangleColor(cell.color); AddTriangle(center, e1, e2); AddTriangleColor(cell.color); AddTriangle(center, e2, v2); AddTriangleColor(cell.color);
18 lados.Divisão de costela
Obviamente, também precisamos subdividir as juntas das arestas. Portanto, passaremos as novas arestas de vértice para
TriangulateConnection
.
if (direction <= HexDirection.SE) { TriangulateConnection(direction, cell, v1, e1, e2, v2); }
Adicione os parâmetros apropriados ao
TriangulateConnection
para que ele possa trabalhar com vértices adicionais.
void TriangulateConnection ( HexDirection direction, HexCell cell, Vector3 v1, Vector3 e1, Vector3 e2, Vector3 v2 ) { … }
Também precisamos calcular as arestas adicionais das arestas das células vizinhas. Podemos calculá-los depois de conectar a ponte ao outro lado.
Vector3 bridge = HexMetrics.GetBridge(direction); Vector3 v3 = v1 + bridge; Vector3 v4 = v2 + bridge; v3.y = v4.y = neighbor.Position.y; Vector3 e3 = Vector3.Lerp(v3, v4, 1f / 3f); Vector3 e4 = Vector3.Lerp(v3, v4, 2f / 3f);
Em seguida, precisamos alterar a triangulação da costela. Até ignorarmos as pistas com as bordas, basta adicionar três em vez de um quad.
if (cell.GetEdgeType(direction) == HexEdgeType.Slope) { TriangulateEdgeTerraces(v1, v2, cell, v3, v4, neighbor); } else { AddQuad(v1, e1, v3, e3); AddQuadColor(cell.color, neighbor.color); AddQuad(e1, e2, e3, e4); AddQuadColor(cell.color, neighbor.color); AddQuad(e2, v2, e4, v4); AddQuadColor(cell.color, neighbor.color); }
Conexões subdivididas.A união das arestas das arestas
Como para descrever as arestas, precisamos agora de quatro vértices, seria lógico combiná-las em um conjunto. Isso é mais conveniente do que trabalhar com quatro vértices independentes. Crie uma estrutura
EdgeVertices
simples para isso. Ele deve conter quatro vértices no sentido horário ao longo da borda da célula.
using UnityEngine; public struct EdgeVertices { public Vector3 v1, v2, v3, v4; }
Eles não deveriam ser serializáveis?Usaremos essa estrutura apenas para triangulação. Nesse estágio, não precisamos armazenar os vértices das arestas, portanto, não é necessário que sejam serializáveis.
Adicione um método construtor conveniente a ele, que lidará com o cálculo dos pontos intermediários da aresta.
public EdgeVertices (Vector3 corner1, Vector3 corner2) { v1 = corner1; v2 = Vector3.Lerp(corner1, corner2, 1f / 3f); v3 = Vector3.Lerp(corner1, corner2, 2f / 3f); v4 = corner2; }
Agora podemos adicionar um método de triangulação separado ao
HexMesh
para criar um leque de triângulos entre o centro da célula e uma de suas bordas.
void TriangulateEdgeFan (Vector3 center, EdgeVertices edge, Color color) { AddTriangle(center, edge.v1, edge.v2); AddTriangleColor(color); AddTriangle(center, edge.v2, edge.v3); AddTriangleColor(color); AddTriangle(center, edge.v3, edge.v4); AddTriangleColor(color); }
E um método para triangular uma faixa de quadrângulos entre duas arestas.
void TriangulateEdgeStrip ( EdgeVertices e1, Color c1, EdgeVertices e2, Color c2 ) { AddQuad(e1.v1, e1.v2, e2.v1, e2.v2); AddQuadColor(c1, c2); AddQuad(e1.v2, e1.v3, e2.v2, e2.v3); AddQuadColor(c1, c2); AddQuad(e1.v3, e1.v4, e2.v3, e2.v4); AddQuadColor(c1, c2); }
Isso nos permitirá simplificar o método
Triangulate
.
void Triangulate (HexDirection direction, HexCell cell) { Vector3 center = cell.Position; EdgeVertices e = new EdgeVertices( center + HexMetrics.GetFirstSolidCorner(direction), center + HexMetrics.GetSecondSolidCorner(direction) ); TriangulateEdgeFan(center, e, cell.color); if (direction <= HexDirection.SE) { TriangulateConnection(direction, cell, e); } }
Vamos passar para
TriangulateConnection
. Agora podemos usar
TriangulateEdgeStrip
, mas outras substituições precisam ser feitas. Onde costumávamos usar a
v1
, precisamos usar
e1.v1
. Da mesma forma,
v2
se torna
e1.v4
,
v3
se torna
e2.v1
e
v4
se torna
e2.v4
.
void TriangulateConnection ( HexDirection direction, HexCell cell, EdgeVertices e1 ) { HexCell neighbor = cell.GetNeighbor(direction); if (neighbor == null) { return; } Vector3 bridge = HexMetrics.GetBridge(direction); bridge.y = neighbor.Position.y - cell.Position.y; EdgeVertices e2 = new EdgeVertices( e1.v1 + bridge, e1.v4 + bridge ); if (cell.GetEdgeType(direction) == HexEdgeType.Slope) { TriangulateEdgeTerraces(e1.v1, e1.v4, cell, e2.v1, e2.v4, neighbor); } else { TriangulateEdgeStrip(e1, cell.color, e2, neighbor.color); } HexCell nextNeighbor = cell.GetNeighbor(direction.Next()); if (direction <= HexDirection.E && nextNeighbor != null) { Vector3 v5 = e1.v4 + HexMetrics.GetBridge(direction.Next()); v5.y = nextNeighbor.Position.y; if (cell.Elevation <= neighbor.Elevation) { if (cell.Elevation <= nextNeighbor.Elevation) { TriangulateCorner( e1.v4, cell, e2.v4, neighbor, v5, nextNeighbor ); } else { TriangulateCorner( v5, nextNeighbor, e1.v4, cell, e2.v4, neighbor ); } } else if (neighbor.Elevation <= nextNeighbor.Elevation) { TriangulateCorner( e2.v4, neighbor, v5, nextNeighbor, e1.v4, cell ); } else { TriangulateCorner( v5, nextNeighbor, e1.v4, cell, e2.v4, neighbor ); } }
Divisão Ledge
Precisamos dividir as bordas. Portanto, passamos as arestas para
TriangulateEdgeTerraces
.
if (cell.GetEdgeType(direction) == HexEdgeType.Slope) { TriangulateEdgeTerraces(e1, cell, e2, neighbor); }
Agora precisamos modificar
TriangulateEdgeTerraces
para que ele interpole entre arestas e não entre pares de vértices. Vamos supor que o
EdgeVertices
tenha um método estático conveniente para fazer isso. Isso nos permitirá simplificar o
TriangulateEdgeTerraces
vez de complicá-lo.
void TriangulateEdgeTerraces ( EdgeVertices begin, HexCell beginCell, EdgeVertices end, HexCell endCell ) { EdgeVertices e2 = EdgeVertices.TerraceLerp(begin, end, 1); Color c2 = HexMetrics.TerraceLerp(beginCell.color, endCell.color, 1); TriangulateEdgeStrip(begin, beginCell.color, e2, c2); for (int i = 2; i < HexMetrics.terraceSteps; i++) { EdgeVertices e1 = e2; Color c1 = c2; e2 = EdgeVertices.TerraceLerp(begin, end, i); c2 = HexMetrics.TerraceLerp(beginCell.color, endCell.color, i); TriangulateEdgeStrip(e1, c1, e2, c2); } TriangulateEdgeStrip(e2, c2, end, endCell.color); }
O método
EdgeVertices.TerraceLerp
simplesmente interpola as bordas entre os quatro pares de vértices de duas arestas.
public static EdgeVertices TerraceLerp ( EdgeVertices a, EdgeVertices b, int step) { EdgeVertices result; result.v1 = HexMetrics.TerraceLerp(a.v1, b.v1, step); result.v2 = HexMetrics.TerraceLerp(a.v2, b.v2, step); result.v3 = HexMetrics.TerraceLerp(a.v3, b.v3, step); result.v4 = HexMetrics.TerraceLerp(a.v4, b.v4, step); return result; }
Bordas subdivididas.unitypackageReconecte penhascos e saliências
Até agora, ignoramos as rachaduras na junção de falésias e saliências. É hora de resolver este problema. Vejamos primeiro os casos de penhasco-declive-declive (OSS) e declive-precipício-declive (SOS).
Buracos de malha.O problema surge porque os topos das fronteiras se moveram. Isso significa que agora eles não estão exatamente do lado do penhasco, o que leva a uma rachadura. Às vezes, esses buracos são invisíveis e às vezes impressionantes.
A solução é não mover o topo da borda. Isso significa que precisamos controlar se o ponto será movido. A maneira mais fácil seria criar uma alternativa
AddTriangle
que não mova os vértices.
void AddTriangleUnperturbed (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); }
Altere o
TriangulateBoundaryTriangle
para que ele use esse método. Isso significa que ele terá que mover explicitamente todos os vértices, exceto os limites.
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); AddTriangleUnperturbed(Perturb(begin), Perturb(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); AddTriangleUnperturbed(Perturb(v1), Perturb(v2), boundary); AddTriangleColor(c1, c2, boundaryColor); } AddTriangleUnperturbed(Perturb(v2), Perturb(left), boundary); AddTriangleColor(c2, leftCell.color, boundaryColor); }
Vale a pena observar o seguinte: como não usamos a
v2
para obter outro ponto, podemos movê-lo imediatamente. Essa é uma otimização simples e reduz a quantidade de código, então vamos apresentá-la.
void TriangulateBoundaryTriangle ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 boundary, Color boundaryColor ) { Vector3 v2 = Perturb(HexMetrics.TerraceLerp(begin, left, 1)); Color c2 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, 1); AddTriangleUnperturbed(Perturb(begin), v2, boundary); AddTriangleColor(beginCell.color, c2, boundaryColor); for (int i = 2; i < HexMetrics.terraceSteps; i++) { Vector3 v1 = v2; Color c1 = c2; v2 = Perturb(HexMetrics.TerraceLerp(begin, left, i)); c2 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, i); AddTriangleUnperturbed(v1, v2, boundary); AddTriangleColor(c1, c2, boundaryColor); } AddTriangleUnperturbed(v2, Perturb(left), boundary); AddTriangleColor(c2, leftCell.color, boundaryColor); }
Fronteiras imóveis.Parece melhor, mas ainda não terminamos. Dentro do método
TriangulateCornerTerracesCliff
, o ponto de limite é interpolado entre os pontos esquerdo e direito. No entanto, esses pontos ainda não foram movidos. Para que o ponto de contorno corresponda ao penhasco resultante, precisamos interpolar entre os pontos movidos.
Vector3 boundary = Vector3.Lerp(Perturb(begin), Perturb(right), b);
O mesmo vale para o método
TriangulateCornerCliffTerraces
.
Vector3 boundary = Vector3.Lerp(Perturb(begin), Perturb(left), b);
Os buracos se foram.Falésias e declives duplos
Em todos os casos problemáticos restantes, dois penhascos e uma inclinação estão presentes.
Buraco grande por causa de um único triângulo.Esse problema é resolvido movendo manualmente um único triângulo no bloco
else
no final do
TriangulateCornerTerracesCliff
.
else { AddTriangleUnperturbed(Perturb(left), Perturb(right), boundary); AddTriangleColor(leftCell.color, rightCell.color, boundaryColor); }
O mesmo vale para
TriangulateCornerCliffTerraces
.
else { AddTriangleUnperturbed(Perturb(left), Perturb(right), boundary); AddTriangleColor(leftCell.color, rightCell.color, boundaryColor); }
Livre-se das últimas rachaduras.unitypackageConclusão
Agora temos uma malha distorcida completamente correta. Sua aparência depende do ruído específico, de sua escala e das forças de distorção. No nosso caso, a distorção pode parecer muito forte. Embora essa desigualdade pareça bonita, não queremos que as células se desviem muito da grade uniforme. No final, ainda a usamos para definir a célula a ser redimensionada. E se o tamanho das células variar muito, será mais difícil colocar o conteúdo nelas.Malhas não distorcidas e distorcidas.Parece que a força 5 para distorcer as células é muito grande.A distorção das células é de 0 a 5.Vamos reduzi-lo para 4 para aumentar a conveniência da grade, sem torná-la muito correta. Isso garante que o deslocamento máximo de XZ seja √32 ,65,6 unidades. public const float cellPerturbStrength = 4f;
Força da distorção celular 4.Outro valor que pode ser alterado é o coeficiente de integridade. Se aumentarmos, os centros planos das células se tornarão maiores, ou seja, haverá mais espaço para o conteúdo futuro. Obviamente, ao fazê-lo, eles se tornarão mais hexagonais.Coeficiente de integridade de 0,75 a 0,95.Um ligeiro aumento do coeficiente de integridade para 0,8 simplificará levemente nossa vida no futuro. public const float solidFactor = 0.8f;
Coeficiente de integridade 0.8.Finalmente, você pode perceber que as diferenças entre os níveis de elevação são muito nítidas. Isso é conveniente quando você precisa garantir que a malha seja gerada corretamente, mas já concluímos isso. Vamos reduzi-lo para 1 unidade por etapa, ou seja, para 3. public const float elevationStep = 3f;
O tom é reduzido para 3.Também podemos alterar a força da distorção do tom. Mas agora ele tem um valor de 1,5, que é igual a meio passo de altura, o que nos convém.Pequenos degraus de altura permitem um uso mais lógico de todos os sete níveis de altura. Isso aumenta a variabilidade do mapa.Usamos sete níveis de altura.unitypackageParte 5: cartões maiores
- Dividimos a grade em fragmentos.
- Nós controlamos a câmera.
- Colorir as cores e alturas separadamente.
- Use o pincel aumentado das células.
Até agora, temos trabalhado com um mapa muito pequeno. É hora de aumentá-lo.É hora de aumentar o zoom.Fragmentos de malha
Não podemos tornar a grade muito grande, porque nos deparamos com os limites do que pode caber em uma malha. Como resolver este problema? Use várias malhas. Para fazer isso, precisamos dividir nossa grade em vários fragmentos. Usamos fragmentos retangulares de tamanho constante.Dividindo a grade em 3 por 3 segmentos,vamos usar 5 por 5 blocos, ou seja, 25 células por fragmento. Defina-os HexMetrics
. public const int chunkSizeX = 5, chunkSizeZ = 5;
Que tamanho de fragmento pode ser considerado adequado?. , . . , (frustum culling), . .
Agora não podemos usar nenhum tamanho para a malha; ela deve ser um múltiplo do tamanho do fragmento. Portanto, vamos alterá-lo HexGrid
para que ele defina seu tamanho não em células separadas, mas em fragmentos. Defina o tamanho padrão como 4 por 3 fragmentos, ou seja, apenas 12 fragmentos ou 300 células. Portanto, temos um cartão de teste conveniente. public int chunkCountX = 4, chunkCountZ = 3;
Ainda usamos width
e height
, mas agora eles devem se tornar privados. E renomeie-os para cellCountX
e cellCountZ
. Use o editor para renomear todas as ocorrências dessas variáveis de uma só vez. Agora ficará claro quando estivermos lidando com o número de fragmentos ou células.
Especifique o tamanho em fragmentos.Altere Awake
para que, se necessário, o número de células seja calculado a partir do número de fragmentos. Destacamos a criação de células em um método separado, para não entupir Awake
. void Awake () { HexMetrics.noiseSource = noiseSource; gridCanvas = GetComponentInChildren<Canvas>(); hexMesh = GetComponentInChildren<HexMesh>(); cellCountX = chunkCountX * HexMetrics.chunkSizeX; cellCountZ = chunkCountZ * HexMetrics.chunkSizeZ; CreateCells(); } void CreateCells () { cells = new HexCell[cellCountZ * cellCountX]; for (int z = 0, i = 0; z < cellCountZ; z++) { for (int x = 0; x < cellCountX; x++) { CreateCell(x, z, i++); } } }
Pré-fabricado de fragmentos
Para descrever os fragmentos de malha, precisamos de um novo tipo de componente. using UnityEngine; using UnityEngine.UI; public class HexGridChunk : MonoBehaviour { }
Em seguida, criaremos um fragmento pré-fabricado. Faremos isso duplicando o objeto Hex Grid e renomeando-o como Chunk Hex Grid . Remova seu componente HexGrid
e adicione um componente HexGridChunk
. Em seguida, transforme-o em uma pré-fabricada e remova o objeto da cena.Um fragmento pré-fabricado com sua própria tela e malha.Como ele criará instâncias desses fragmentos HexGrid
, daremos a ele um link para a pré-fabricada do fragmento. public HexGridChunk chunkPrefab;
Agora com fragmentos.Criar instâncias de fragmentos é como criar instâncias de células. Nós os rastrearemos com a ajuda de uma matriz e usaremos um loop duplo para preenchê-la. HexGridChunk[] chunks; void Awake () { … CreateChunks(); CreateCells(); } void CreateChunks () { chunks = new HexGridChunk[chunkCountX * chunkCountZ]; for (int z = 0, i = 0; z < chunkCountZ; z++) { for (int x = 0; x < chunkCountX; x++) { HexGridChunk chunk = chunks[i++] = Instantiate(chunkPrefab); chunk.transform.SetParent(transform); } } }
A inicialização de um fragmento é semelhante à forma como inicializamos uma grade de hexágonos. Ela coloca tudo Awake
e executa a triangulação Start
. Requer uma referência à sua tela e malha, bem como uma matriz para as células. No entanto, o fragmento não criará essas células. A grade continuará fazendo isso. public class HexGridChunk : MonoBehaviour { HexCell[] cells; HexMesh hexMesh; Canvas gridCanvas; void Awake () { gridCanvas = GetComponentInChildren<Canvas>(); hexMesh = GetComponentInChildren<HexMesh>(); cells = new HexCell[HexMetrics.chunkSizeX * HexMetrics.chunkSizeZ]; } void Start () { hexMesh.Triangulate(cells); } }
Atribuindo células a fragmentos
HexGrid
ainda cria todas as células. Isso é normal, mas agora precisamos adicionar cada célula a um fragmento adequado e não defini-las usando nossa própria malha e tela. void CreateCell (int x, int z, int i) { … HexCell cell = cells[i] = Instantiate<HexCell>(cellPrefab);
Podemos encontrar o fragmento correto usando a divisão inteira x
e z
pelo tamanho do fragmento. void AddCellToChunk (int x, int z, HexCell cell) { int chunkX = x / HexMetrics.chunkSizeX; int chunkZ = z / HexMetrics.chunkSizeZ; HexGridChunk chunk = chunks[chunkX + chunkZ * chunkCountX]; }
Usando resultados intermediários, também podemos determinar o índice local da célula nesse fragmento. Depois disso, você pode adicionar uma célula ao fragmento. void AddCellToChunk (int x, int z, HexCell cell) { int chunkX = x / HexMetrics.chunkSizeX; int chunkZ = z / HexMetrics.chunkSizeZ; HexGridChunk chunk = chunks[chunkX + chunkZ * chunkCountX]; int localX = x - chunkX * HexMetrics.chunkSizeX; int localZ = z - chunkZ * HexMetrics.chunkSizeZ; chunk.AddCell(localX + localZ * HexMetrics.chunkSizeX, cell); }
Em seguida, HexGridChunk.AddCell
coloca a célula em sua própria matriz e define os elementos pai para a célula e sua interface do usuário. public void AddCell (int index, HexCell cell) { cells[index] = cell; cell.transform.SetParent(transform, false); cell.uiRect.SetParent(gridCanvas.transform, false); }
Varrer
Nesse ponto, ele HexGrid
pode se livrar da tela de seus filhos e da malha hexagonal, além do código.
Desde que nos livramos Refresh
, não devemos HexMapEditor
mais usá-lo. void EditCell (HexCell cell) { cell.color = activeColor; cell.Elevation = activeElevation;
A grade limpa de hexágonos.Depois de iniciar o modo Play, o cartão ainda parece o mesmo. Mas a hierarquia dos objetos será diferente. A Grade hexadecimal agora cria objetos filho de fragmento que contêm células, bem como suas malhas e telas.Fragmentos filho no modo Play.Talvez tenhamos alguns problemas com os rótulos das células. Inicialmente, definimos a largura do rótulo como 5. Isso foi suficiente para exibir os dois caracteres que eram suficientes para nós em um pequeno mapa. Mas agora podemos ter coordenadas como -10, nas quais existem três caracteres. Eles não cabem e serão aparados. Para corrigir isso, aumente a largura do rótulo da célula para 10 ou até mais.Rótulos de células estendidos.Agora podemos criar mapas muito maiores! Como geramos toda a grade na inicialização, pode demorar muito tempo para criar mapas grandes. Mas após a conclusão, teremos um enorme espaço para experimentação.Corrigir edição de células
A edição parece não funcionar no estágio atual, porque não atualizamos mais a grade. Precisamos atualizar fragmentos individuais, então adicione um método Refresh
para HexGridChunk
. public void Refresh () { hexMesh.Triangulate(cells); }
Quando devemos chamar esse método? Sempre atualizamos toda a grade porque tínhamos apenas uma malha. Mas agora temos muitos fragmentos. Em vez de atualizar todos eles a cada vez, será muito mais eficiente atualizar os fragmentos alterados. Caso contrário, a troca de cartões grandes se tornará uma operação muito lenta.Mas como sabemos qual fragmento atualizar? A maneira mais fácil é fazer com que cada célula saiba a que fragmento pertence. A célula poderá atualizar seu fragmento ao alterar essa célula. Então, vamos dar um HexCell
link para seu fragmento. public HexGridChunk chunk;
HexGridChunk
pode se adicionar à célula ao adicionar. public void AddCell (int index, HexCell cell) { cells[index] = cell; cell.chunk = this; cell.transform.SetParent(transform, false); cell.uiRect.SetParent(gridCanvas.transform, false); }
Ao combinar-los, adicionar HexCell
e método Refresh
. Cada vez que uma célula é atualizada, ela simplesmente atualiza seu fragmento. void Refresh () { chunk.Refresh(); }
Não precisamos torná-lo HexCell.Refresh
comum, porque a própria célula sabe melhor quando foi alterada. Por exemplo, depois que sua altura foi alterada. public int Elevation { get { return elevation; } set { … Refresh(); } }
De fato, precisamos atualizá-lo somente quando sua altura mudar para um valor diferente. Ela nem precisa recalcular nada se atribuirmos a ela a mesma altura de antes. Portanto, podemos sair do início do levantador. public int Elevation { get { return elevation; } set { if (elevation == value) { return; } … } }
No entanto, também ignoraremos os cálculos pela primeira vez quando a altura estiver definida como 0, porque esse é o valor padrão da altura da malha. Para evitar isso, criaremos o valor inicial como nunca usamos. int elevation = int.MinValue;
O que é int.MinValue?, integer. C# integer —
32- , 2 32 integer, , . .
— −2 31 = −2 147 483 648. !
2 31 − 1 = 2 147 483 647. 2 31 - .
Para reconhecer a alteração de cor da célula, também precisamos transformá-la em uma propriedade. Renomeie-o para Color
maiúsculas e, em seguida, transforme-o em uma propriedade com uma variável privada color
. O valor padrão da cor será preto transparente, o que é adequado para nós. public Color Color { get { return color; } set { if (color == value) { return; } color = value; Refresh(); } } Color color;
Agora, quando iniciamos o modo Play, obtemos exceções de referência nula. Isso acontece porque definimos a cor e a altura com seus valores padrão antes de atribuir uma célula ao seu fragmento. É normal que não atualizemos os fragmentos nesse estágio, porque os triangulamos após a conclusão de toda a inicialização. Em outras palavras, atualizamos um fragmento apenas se ele estiver atribuído. void Refresh () { if (chunk) { chunk.Refresh(); } }
Finalmente podemos mudar as células novamente! No entanto, surge um problema. Ao desenhar ao longo das bordas dos fragmentos, as costuras aparecem.Erros nos limites dos fragmentos.Isso é lógico, porque quando uma única célula muda, todas as conexões com seus vizinhos também mudam. E esses vizinhos podem estar em outros fragmentos. A solução mais simples é atualizar todas as células vizinhas, se forem diferentes. void Refresh () { if (chunk) { chunk.Refresh(); for (int i = 0; i < neighbors.Length; i++) { HexCell neighbor = neighbors[i]; if (neighbor != null && neighbor.chunk != chunk) { neighbor.chunk.Refresh(); } } } }
Embora isso funcione, pode ser que atualizemos um fragmento várias vezes. E quando começamos a colorir várias células ao mesmo tempo, tudo fica pior.Mas não somos obrigados a triangular imediatamente após a atualização do fragmento. Em vez disso, simplesmente escrevemos que é necessária uma atualização e triangulamos após a conclusão da alteração.Como ele HexGridChunk
não faz mais nada, podemos usar o estado ativado para sinalizar a necessidade de atualizações. Ao atualizá-lo, incluímos o componente. Ligá-lo várias vezes não muda nada. O componente é atualizado posteriormente. Vamos triangular neste ponto e desativar o componente novamente.Em LateUpdate
vez disso, usamosUpdate
para garantir que a triangulação ocorra após a conclusão da alteração no quadro atual. public void Refresh () {
Qual é a diferença entre Update e LateUpdate?Update
- . LateUpdate
. , .
Como nosso componente é ativado por padrão, não precisamos mais triangular explicitamente Start
. Portanto, esse método pode ser removido.
Fragmentos de 20 por 20 contendo 10.000 células.Listas generalizadas
Embora tenhamos alterado significativamente a maneira como a grade é triangulada, HexMesh
ela permanece a mesma. Tudo o que ele precisa para trabalhar é uma matriz de células. Ele não se importa se há uma malha de hexágonos, ou vários deles. Mas ainda não consideramos o uso de várias malhas. Talvez algo possa ser melhorado aqui?As HexMesh
listas usadas são essencialmente buffers temporários. Eles são usados apenas para triangulação. E os fragmentos são triangulados um de cada vez. Portanto, de fato, precisamos apenas de um conjunto de listas, e não de um conjunto para cada objeto de malha hexagonal. Isso pode ser alcançado tornando as listas estáticas. static List<Vector3> vertices = new List<Vector3>(); static List<Color> colors = new List<Color>(); static List<int> triangles = new List<int>(); void Awake () { GetComponent<MeshFilter>().mesh = hexMesh = new Mesh(); meshCollider = gameObject.AddComponent<MeshCollider>(); hexMesh.name = "Hex Mesh";
As listas estáticas são realmente tão importantes?. , , .
, . 20 20 100.
unitypackageControle da câmera
A câmera grande é maravilhosa, mas é inútil se não conseguimos vê-la. Para inspecionar o mapa inteiro, precisamos mover a câmera. O zoom também é útil. Portanto, vamos criar uma câmera para executar essas ações.Crie um objeto fictício e chame-o de câmera de mapa hexadecimal . Solte seu componente de transformação para que ele se mova para a origem sem alterar sua rotação e escala. Adicione um filho a ele chamado Swivel e adicione um filho a ele Stick . Faça da câmera principal um filho do Stick e redefina seu componente de transformação.A hierarquia da câmera.O objetivo da dobradiça da câmera (giratória) é controlar o ângulo em que a câmera olha para o mapa. Vamos dar uma volta (45, 0, 0). A alça (Stick) controla a distância em que as câmeras estão localizadas. Vamos definir uma posição para ela (0, 0, -45).Agora precisamos de um componente para controlar esse sistema. Atribua este componente à raiz da hierarquia da câmera. Vamos dar-lhe um link para a dobradiça e segurar a obtenção-los em Awake
. using UnityEngine; public class HexMapCamera : MonoBehaviour { Transform swivel, stick; void Awake () { swivel = transform.GetChild(0); stick = swivel.GetChild(0); } }
Câmera de mapa do hexágono.Zoom
A primeira função que criaremos é o zoom (zoom). Podemos controlar o nível atual de zoom usando a variável float. Um valor de 0 significa que estamos completamente distantes, e um valor de 1 significa que estamos completamente próximos. Vamos começar com o zoom máximo. float zoom = 1f;
O zoom geralmente é realizado com a roda do mouse ou o controle analógico. Podemos implementá-lo usando o eixo de entrada padrão Mouse ScrollWheel . Adicione um método Update
que verifique a presença de um delta de entrada e, se houver, chamará o método de alteração de zoom. void Update () { float zoomDelta = Input.GetAxis("Mouse ScrollWheel"); if (zoomDelta != 0f) { AdjustZoom(zoomDelta); } } void AdjustZoom (float delta) { }
Para alterar o nível de zoom, basta adicionar um delta e limitar o valor (grampo) para permanecer no intervalo de 0 a 1. void AdjustZoom (float delta) { zoom = Mathf.Clamp01(zoom + delta); }
Ao ampliar e reduzir, a distância da câmera deve mudar de acordo. Isso pode ser feito alterando a posição da alça em Z. Adicione duas variáveis de flutuação comuns para ajustar a posição da alça no zoom mínimo e máximo. Como estamos desenvolvendo um mapa relativamente pequeno, defina os valores para -250 e -45. public float stickMinZoom, stickMaxZoom;
Após alterar o zoom, realizamos a interpolação linear entre esses dois valores com base no novo valor de zoom. Em seguida, atualize a posição da alça. void AdjustZoom (float delta) { zoom = Mathf.Clamp01(zoom + delta); float distance = Mathf.Lerp(stickMinZoom, stickMaxZoom, zoom); stick.localPosition = new Vector3(0f, 0f, distance); }
Valores mínimos e máximos da vara.Agora o zoom funciona, mas até agora não é muito útil. Normalmente, quando o zoom está mais distante, a câmera entra na vista superior. Podemos perceber isso girando a dobradiça. Portanto, adicionamos as variáveis min e max para a dobradiça. Vamos definir os valores 90 e 45. public float swivelMinZoom, swivelMaxZoom;
Assim como na posição da alça, interpolamos para encontrar um ângulo de zoom adequado. Em seguida, ajustamos a rotação da dobradiça. void AdjustZoom (float delta) { zoom = Mathf.Clamp01(zoom + delta); float distance = Mathf.Lerp(stickMinZoom, stickMaxZoom, zoom); stick.localPosition = new Vector3(0f, 0f, distance); float angle = Mathf.Lerp(swivelMinZoom, swivelMaxZoom, zoom); swivel.localRotation = Quaternion.Euler(angle, 0f, 0f); }
O valor mínimo e máximo de giro.A taxa de alteração do zoom pode ser ajustada alterando a sensibilidade dos parâmetros de entrada da roda do mouse. Eles podem ser encontrados em Editar / Configurações do projeto / Entrada . Por exemplo, alterando-os de 0,1 para 0,025, obtemos uma alteração mais lenta e suave no zoom.Opções de entrada da roda do mouse.Movendo
Agora vamos passar a mover a câmera. O movimento na direção de X e Z devemos implementar em Update
, como no caso do zoom. Podemos usar eixos de entrada horizontais e verticais para isso . Isso nos permitirá mover a câmera com as setas e as teclas WASD. void Update () { float zoomDelta = Input.GetAxis("Mouse ScrollWheel"); if (zoomDelta != 0f) { AdjustZoom(zoomDelta); } float xDelta = Input.GetAxis("Horizontal"); float zDelta = Input.GetAxis("Vertical"); if (xDelta != 0f || zDelta != 0f) { AdjustPosition(xDelta, zDelta); } } void AdjustPosition (float xDelta, float zDelta) { }
A abordagem mais simples é obter a posição atual do sistema da câmera, adicionar deltas X e Z a ele e atribuir o resultado à posição do sistema. void AdjustPosition (float xDelta, float zDelta) { Vector3 position = transform.localPosition; position += new Vector3(xDelta, 0f, zDelta); transform.localPosition = position; }
Devido a isso, a câmera se moverá enquanto segura as setas ou WASD, mas não a uma velocidade constante. Isso dependerá da taxa de quadros. Para determinar a distância que você precisa mover, usamos o delta do tempo, bem como a velocidade necessária. Portanto, adicionamos uma variável comum moveSpeed
e a definimos como 100, e a multiplicamos pelo delta do tempo para obter o delta da posição. public float moveSpeed; void AdjustPosition (float xDelta, float zDelta) { float distance = moveSpeed * Time.deltaTime; Vector3 position = transform.localPosition; position += new Vector3(xDelta, 0f, zDelta) * distance; transform.localPosition = position; }
Velocidade de movimento.Agora podemos nos mover a uma velocidade constante ao longo dos eixos X ou Z. Mas, ao se mover nos dois eixos ao mesmo tempo (na diagonal), o movimento será mais rápido. Para corrigir isso, precisamos normalizar o vetor delta. Isso permitirá que você o use como destino. void AdjustPosition (float xDelta, float zDelta) { Vector3 direction = new Vector3(xDelta, 0f, zDelta).normalized; float distance = moveSpeed * Time.deltaTime; Vector3 position = transform.localPosition; position += direction * distance; transform.localPosition = position; }
O movimento diagonal agora está implementado corretamente, mas de repente acontece que a câmera continua se movendo por um longo tempo, mesmo depois de liberar todas as teclas. Isso acontece porque os eixos de entrada não saltam instantaneamente para os valores limite imediatamente após pressionar as teclas. Eles precisam de algum tempo para isso. O mesmo vale para a liberação de chaves. Leva tempo para retornar aos valores do eixo zero. No entanto, como normalizamos os valores de entrada, a velocidade máxima é mantida constantemente.Podemos ajustar os parâmetros de entrada para eliminar os atrasos, mas eles proporcionam uma sensação de suavidade que vale a pena salvar. Podemos aplicar o valor mais extremo dos eixos como coeficiente de movimento de amortecimento. Vector3 direction = new Vector3(xDelta, 0f, zDelta).normalized; float damping = Mathf.Max(Mathf.Abs(xDelta), Mathf.Abs(zDelta)); float distance = moveSpeed * damping * Time.deltaTime;
Movimento com atenuação.Agora, o movimento funciona bem, pelo menos com um aumento no zoom. Mas, à distância, acaba sendo muito lento. Com zoom reduzido, precisamos acelerar. Isso pode ser feito substituindo uma variável moveSpeed
por duas para o zoom mínimo e máximo e, em seguida, interpolando. Atribua a eles valores de 400 e 100.
A velocidade do movimento varia com o nível de zoom.Agora podemos nos mover rapidamente pelo mapa! De fato, podemos ir muito além do mapa, mas isso é indesejável. A câmera deve permanecer dentro do mapa. Para garantir isso, precisamos conhecer os limites do mapa, para que seja necessário um link para a grade. Adicione e conecte-o. public HexGrid grid;
Precisa solicitar o tamanho da grade.Depois de passar para uma nova posição, a limitaremos usando o novo método. void AdjustPosition (float xDelta, float zDelta) { … transform.localPosition = ClampPosition(position); } Vector3 ClampPosition (Vector3 position) { return position; }
A posição X tem um valor mínimo de 0 e o máximo é determinado pelo tamanho do mapa. Vector3 ClampPosition (Vector3 position) { float xMax = grid.chunkCountX * HexMetrics.chunkSizeX * (2f * HexMetrics.innerRadius); position.x = Mathf.Clamp(position.x, 0f, xMax); return position; }
O mesmo se aplica à posição Z. Vector3 ClampPosition (Vector3 position) { float xMax = grid.chunkCountX * HexMetrics.chunkSizeX * (2f * HexMetrics.innerRadius); position.x = Mathf.Clamp(position.x, 0f, xMax); float zMax = grid.chunkCountZ * HexMetrics.chunkSizeZ * (1.5f * HexMetrics.outerRadius); position.z = Mathf.Clamp(position.z, 0f, zMax); return position; }
De fato, isso é um pouco impreciso. O ponto de partida está no centro da célula, não no lado esquerdo. Portanto, queremos que a câmera pare no centro das células mais à direita. Para fazer isso, subtraia metade da célula do máximo de X. float xMax = (grid.chunkCountX * HexMetrics.chunkSizeX - 0.5f) * (2f * HexMetrics.innerRadius); position.x = Mathf.Clamp(position.x, 0f, xMax);
Pelo mesmo motivo, precisamos reduzir o Z máximo. Como as métricas são um pouco diferentes, precisamos subtrair a célula completa. float zMax = (grid.chunkCountZ * HexMetrics.chunkSizeZ - 1) * (1.5f * HexMetrics.outerRadius); position.z = Mathf.Clamp(position.z, 0f, zMax);
Com o movimento que terminamos, apenas um pequeno detalhe permanece. Às vezes, a interface do usuário reage às teclas de seta, e isso leva ao fato de que, quando você move a câmera, o controle deslizante se move. Isso acontece quando a interface do usuário se considera ativa, depois que você clica nela e o cursor continua acima dela.Você pode impedir que a interface do usuário escute a entrada do teclado. Isso pode ser feito instruindo o objeto EventSystem a não executar Enviar Eventos de Navegação .Não há mais eventos de navegação.Turn
Quer ver o que está por trás do penhasco? Seria conveniente poder girar a câmera! Vamos adicionar esse recurso.O nível de zoom não é importante para a rotação, apenas a velocidade é suficiente. Adicione uma variável comum rotationSpeed
e defina-a em 180 graus. Verifique o delta de rotação Update
amostrando o eixo de rotação e alterando a rotação, se necessário. public float rotationSpeed; void Update () { float zoomDelta = Input.GetAxis("Mouse ScrollWheel"); if (zoomDelta != 0f) { AdjustZoom(zoomDelta); } float rotationDelta = Input.GetAxis("Rotation"); if (rotationDelta != 0f) { AdjustRotation(rotationDelta); } float xDelta = Input.GetAxis("Horizontal"); float zDelta = Input.GetAxis("Vertical"); if (xDelta != 0f || zDelta != 0f) { AdjustPosition(xDelta, zDelta); } } void AdjustRotation (float delta) { }
Velocidade de giro.De fato, o eixo de rotação não é por padrão. Teremos que criar nós mesmos. Vá para os parâmetros de entrada e duplique a entrada superior, na vertical . Altere o nome da duplicata para Rotação e altere as teclas para QE e uma vírgula (,) com um ponto (.).Gire o eixo de entrada.Baixei o unitypackage, por que não tenho essa entrada?. Unity. , . , , .
O ângulo de rotação que rastrearemos e alteraremos AdjustRotation
. Depois disso, rodaremos todo o sistema da câmera. float rotationAngle; void AdjustRotation (float delta) { rotationAngle += delta * rotationSpeed * Time.deltaTime; transform.localRotation = Quaternion.Euler(0f, rotationAngle, 0f); }
Como o círculo completo é de 360 graus, rolamos o ângulo de rotação para que fique no intervalo de 0 a 360. void AdjustRotation (float delta) { rotationAngle += delta * rotationSpeed * Time.deltaTime; if (rotationAngle < 0f) { rotationAngle += 360f; } else if (rotationAngle >= 360f) { rotationAngle -= 360f; } transform.localRotation = Quaternion.Euler(0f, rotationAngle, 0f); }
Ligue em ação.Agora a rotação está funcionando. Se você verificar, poderá ver que o movimento é absoluto. Portanto, depois de girar 180 graus, o movimento será o oposto do esperado. Seria muito mais conveniente para o usuário que o movimento seja realizado em relação ao ângulo de visão da câmera. Podemos fazer isso multiplicando a rotação atual pela direção do movimento. void AdjustPosition (float xDelta, float zDelta) { Vector3 direction = transform.localRotation * new Vector3(xDelta, 0f, zDelta).normalized; … }
Deslocamento relativo.unitypackageEdição Avançada
Agora que temos um mapa maior, você pode melhorar as ferramentas de edição de mapas. A alteração de uma célula por vez é muito longa, por isso seria bom criar um pincel maior. Também será conveniente se você optar por pintar ou alterar a altura, deixando o resto inalterado.Cor e altura opcionais
Podemos tornar as cores opcionais adicionando uma opção de seleção vazia ao grupo de alternância. Duplique um dos comutadores de cores e substitua seu rótulo por --- ou algo semelhante para indicar que não é uma cor. Em seguida, altere o argumento do evento On Value Changed para -1.Índice de cores inválido.Obviamente, esse índice não é válido para uma matriz de cores. Podemos usá-lo para determinar se a cor deve ser aplicada às células. bool applyColor; public void SelectColor (int index) { applyColor = index >= 0; if (applyColor) { activeColor = colors[index]; } } void EditCell (HexCell cell) { if (applyColor) { cell.Color = activeColor; } cell.Elevation = activeElevation; }
A altura é controlada por um controle deslizante, portanto, não podemos adicionar uma opção a ele. Em vez disso, podemos usar uma opção separada para ativar ou desativar a edição de altura. Por padrão, ele será ativado. bool applyElevation = true; void EditCell (HexCell cell) { if (applyColor) { cell.Color = activeColor; } if (applyElevation) { cell.Elevation = activeElevation; } }
Adicione um novo interruptor de altura à interface do usuário. Também colocarei tudo em um novo painel e tornarei o controle deslizante de altura horizontal para que a interface do usuário fique mais bonita.Cor e altura opcionais.Para ativar a altura, precisamos de um novo método, o qual iremos conectar com a interface do usuário. public void SetApplyElevation (bool toggle) { applyElevation = toggle; }
Ao conectá-lo à chave de altura, verifique se o método bool dinâmico é usado na parte superior da lista de métodos. As versões corretas não exibem uma marca de seleção no inspetor.Nós transmitimos o status do interruptor de altura.Agora podemos escolher apenas colorir com flores ou apenas altura. Ou ambos, como sempre. Podemos até optar por não mudar um ou outro, mas até agora não é particularmente útil para nós.Alterne entre cor e altura.Por que a altura é desativada ao escolher uma cor?, toggle group. , , toggle group.
Tamanho do pincel
Para oferecer suporte ao tamanho redimensionável do pincel, adicione uma variável inteira brushSize
e um método para defini-lo por meio da interface do usuário. Usaremos o controle deslizante; portanto, novamente teremos que converter o valor de float para int. int brushSize; public void SetBrushSize (float size) { brushSize = (int)size; }
Controle deslizante de tamanho de pincel.Você pode criar um novo controle deslizante duplicando o controle deslizante de altura. Altere seu valor máximo para 4 e anexe-o ao método correspondente. Eu também adicionei uma tag para ele.Configurações do controle deslizante de tamanho do pincel.Agora que podemos editar várias células ao mesmo tempo, precisamos usar o método EditCells
. Este método exigirá EditCell
todas as células envolvidas. A célula inicialmente selecionada será considerada o centro do pincel. void HandleInput () { Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(inputRay, out hit)) { EditCells(hexGrid.GetCell(hit.point)); } } void EditCells (HexCell center) { } void EditCell (HexCell cell) { … }
O tamanho do pincel determina o raio da edição. Com um raio de 0, essa será apenas uma célula central. Com um raio de 1, este será o centro e seus vizinhos. Em um raio de 2, os vizinhos do centro e seus vizinhos imediatos são ativados. E assim por diante
Até o raio 3.Para editar as células, você precisa contorná-las em um loop. Primeiro, precisamos das coordenadas X e Z do centro. void EditCells (HexCell center) { int centerX = center.coordinates.X; int centerZ = center.coordinates.Z; }
Nós encontramos a coordenada Z mínima subtraindo o raio. Então, definimos a linha zero. Começando nesta linha, percorremos até cobrir a linha no centro. void EditCells (HexCell center) { int centerX = center.coordinates.X; int centerZ = center.coordinates.Z; for (int r = 0, z = centerZ - brushSize; z <= centerZ; z++, r++) { } }
A primeira célula na linha inferior tem a mesma coordenada X que a célula central. Essa coordenada diminui com o aumento do número da linha.A última célula sempre tem uma coordenada X igual à coordenada central mais o raio.Agora podemos fazer um loop em torno de cada linha e obter células por suas coordenadas. for (int r = 0, z = centerZ - brushSize; z <= centerZ; z++, r++) { for (int x = centerX - r; x <= centerX + brushSize; x++) { EditCell(hexGrid.GetCell(new HexCoordinates(x, z))); } }
Ainda não temos um método HexGrid.GetCell
com um parâmetro de coordenada, então crie-o. Converta nas coordenadas dos deslocamentos e obtenha a célula. public HexCell GetCell (HexCoordinates coordinates) { int z = coordinates.Z; int x = coordinates.X + z / 2; return cells[x + z * cellCountX]; }
A parte inferior do pincel, tamanho 2.Cobrimos o restante do pincel, realizando um ciclo de cima para baixo até o centro. Nesse caso, a lógica é espelhada e a linha central precisa ser excluída. void EditCells (HexCell center) { int centerX = center.coordinates.X; int centerZ = center.coordinates.Z; for (int r = 0, z = centerZ - brushSize; z <= centerZ; z++, r++) { for (int x = centerX - r; x <= centerX + brushSize; x++) { EditCell(hexGrid.GetCell(new HexCoordinates(x, z))); } } for (int r = 0, z = centerZ + brushSize; z > centerZ; z--, r++) { for (int x = centerX - brushSize; x <= centerX + r; x++) { EditCell(hexGrid.GetCell(new HexCoordinates(x, z))); } } }
O pincel inteiro, tamanho 2.Isso funciona, a menos que o pincel ultrapasse as bordas da grade. Quando isso acontece, obtemos uma exceção de índice fora da faixa. Para evitar isso, verifique os limites HexGrid.GetCell
e retorne null
quando uma célula inexistente for solicitada. public HexCell GetCell (HexCoordinates coordinates) { int z = coordinates.Z; if (z < 0 || z >= cellCountZ) { return null; } int x = coordinates.X + z / 2; if (x < 0 || x >= cellCountX) { return null; } return cells[x + z * cellCountX]; }
Para evitar a exceção de referência nula, ele HexMapEditor
deve verificar antes de editar se a célula realmente existe. void EditCell (HexCell cell) { if (cell) { if (applyColor) { cell.Color = activeColor; } if (applyElevation) { cell.Elevation = activeElevation; } } }
Usando vários tamanhos de pincel.Alternar visibilidade do rótulo da célula
Na maioria das vezes, não precisamos ver os rótulos das células. Então, vamos torná-los opcionais. Como cada fragmento controla sua própria tela, adicione um método ShowUI
a HexGridChunk
. Quando a interface do usuário deve estar visível, ativamos a tela. Caso contrário, desative-o. public void ShowUI (bool visible) { gridCanvas.gameObject.SetActive(visible); }
Vamos ocultar a interface do usuário por padrão. void Awake () { gridCanvas = GetComponentInChildren<Canvas>(); hexMesh = GetComponentInChildren<HexMesh>(); cells = new HexCell[HexMetrics.chunkSizeX * HexMetrics.chunkSizeZ]; ShowUI(false); }
Como a visibilidade da interface do usuário é alterada para todo o mapa, adicionamos o método ShowUI
a HexGrid
. Apenas passa a solicitação para seus fragmentos. public void ShowUI (bool visible) { for (int i = 0; i < chunks.Length; i++) { chunks[i].ShowUI(visible); } }
HexMapEditor
obtém o mesmo método, passando a solicitação para a grade. public void ShowUI (bool visible) { hexGrid.ShowUI(visible); }
Por fim, podemos adicionar um switch à interface do usuário e conectá-lo.Mudar a visibilidade da etiqueta.unitypackageParte 6: rios
- Adicionando rios às células.
- Arraste e solte o suporte para desenhar rios.
- Criando leitos fluviais.
- Usando várias malhas por fragmento.
- Crie um pool de listas compartilhadas.
- Triangulação e animação de água corrente.
Na parte anterior, falamos sobre o suporte a mapas grandes. Agora podemos passar para elementos de alívio maiores. Desta vez, falaremos sobre os rios.Rios fluem das montanhas.Células do rio
Existem três maneiras de adicionar rios a uma grade de hexágonos. A primeira maneira é deixá-los fluir de célula para célula. É assim que é implementado no Endless Legend. A segunda maneira é permitir que elas fluam entre as células, de ponta a ponta. Por isso, é implementado na Civilização 5. A terceira maneira não é criar estruturas especiais de rios, mas usar células de água para sugeri-las. Portanto, os rios são implementados no Age of Wonders 3.No nosso caso, as margens das células já estão ocupadas por encostas e falésias. Isso deixa pouco espaço para rios. Portanto, vamos fazê-los fluir de célula para célula. Isso significa que em cada célula não haverá rio, ou um rio fluirá ao longo dela, ou haverá um começo ou fim no rio. Nas células ao longo das quais o rio flui, ele pode fluir reto, dar uma volta um passo ou dois passos.Cinco configurações possíveis do rio.Não apoiaremos a ramificação ou fusão de rios. Isso complicará ainda mais as coisas, principalmente o fluxo de água. Além disso, não ficaremos intrigados com grandes volumes de água. Vamos considerá-los em outro tutorial.Acompanhamento fluvial
A célula ao longo da qual o rio flui pode ser considerada simultaneamente como tendo um rio de entrada e saída. Se ele contém o começo de um rio, então ele possui apenas um rio de saída. E se ele contém o final do rio, então ele tem apenas um rio que chega. Podemos armazenar essas informações HexCell
usando dois valores booleanos. bool hasIncomingRiver, hasOutgoingRiver;
Mas isso não é suficiente. Também precisamos saber a direção desses rios. No caso de um rio de saída, indica para onde está se movendo. No caso de um rio que entra, indica de onde veio. bool hasIncomingRiver, hasOutgoingRiver; HexDirection incomingRiver, outgoingRiver;
Precisamos dessas informações ao triangular células, portanto, adicionaremos propriedades para ter acesso a elas. Não apoiaremos a atribuição direta deles. Para fazer isso, adicionaremos ainda um método separado. public bool HasIncomingRiver { get { return hasIncomingRiver; } } public bool HasOutgoingRiver { get { return hasOutgoingRiver; } } public HexDirection IncomingRiver { get { return incomingRiver; } } public HexDirection OutgoingRiver { get { return outgoingRiver; } }
Uma questão importante é se existe um rio na célula, independentemente dos detalhes. Portanto, vamos adicionar uma propriedade para isso também. public bool HasRiver { get { return hasIncomingRiver || hasOutgoingRiver; } }
Outra questão lógica: é o começo ou o fim do rio na célula. Se o estado do rio de entrada e saída for diferente, esse é apenas o caso. Portanto, tornaremos essa outra propriedade. public bool HasRiverBeginOrEnd { get { return hasIncomingRiver != hasOutgoingRiver; } }
E, finalmente, será útil saber se o rio flui através de uma determinada cordilheira, se é de entrada ou de saída. public bool HasRiverThroughEdge (HexDirection direction) { return hasIncomingRiver && incomingRiver == direction || hasOutgoingRiver && outgoingRiver == direction; }
Remoção do rio
Antes de começarmos a adicionar um rio a uma célula, vamos primeiro implementar o suporte à remoção de rios. Para começar, escreveremos um método para remover apenas a parte que sai do rio.Se não houver rio de saída na célula, nada precisará ser feito. Caso contrário, desligue-o e execute a atualização. public void RemoveOutgoingRiver () { if (!hasOutgoingRiver) { return; } hasOutgoingRiver = false; Refresh(); }
Mas isso não é tudo. O rio que sai deve seguir em algum lugar. Portanto, deve haver um vizinho com o rio que chega. Precisamos nos livrar dela também. public void RemoveOutgoingRiver () { if (!hasOutgoingRiver) { return; } hasOutgoingRiver = false; Refresh(); HexCell neighbor = GetNeighbor(outgoingRiver); neighbor.hasIncomingRiver = false; neighbor.Refresh(); }
Um rio não pode fluir para fora de um mapa?, . , .
A remoção de um rio de uma célula altera apenas a aparência dessa célula. Diferente da edição de altura ou cor, ela não afeta os vizinhos. Portanto, precisamos atualizar apenas a própria célula, mas não seus vizinhos. public void RemoveOutgoingRiver () { if (!hasOutgoingRiver) { return; } hasOutgoingRiver = false; RefreshSelfOnly(); HexCell neighbor = GetNeighbor(outgoingRiver); neighbor.hasIncomingRiver = false; neighbor.RefreshSelfOnly(); }
Esse método RefreshSelfOnly
simplesmente atualiza o fragmento ao qual a célula pertence. Como não mudamos o rio durante a inicialização da rede, não precisamos nos preocupar se um fragmento já foi atribuído. void RefreshSelfOnly () { chunk.Refresh(); }
A remoção dos rios que chegam funciona da mesma maneira. public void RemoveIncomingRiver () { if (!hasIncomingRiver) { return; } hasIncomingRiver = false; RefreshSelfOnly(); HexCell neighbor = GetNeighbor(incomingRiver); neighbor.hasOutgoingRiver = false; neighbor.RefreshSelfOnly(); }
E a remoção de todo o rio significa simplesmente a remoção de ambas as partes que entram e saem do rio. public void RemoveRiver () { RemoveOutgoingRiver(); RemoveIncomingRiver(); }
Adicionando rios
Para apoiar a criação de rios, precisamos de um método para especificar o rio de saída da célula. Ele deve redefinir todos os rios de saída anteriores e definir o rio de entrada correspondente.Para começar, não precisamos fazer nada se o rio já existir. public void SetOutgoingRiver (HexDirection direction) { if (hasOutgoingRiver && outgoingRiver == direction) { return; } }
Em seguida, precisamos garantir que haja um vizinho na direção certa. Além disso, os rios não podem fluir. Portanto, devemos concluir a operação se o vizinho for maior. HexCell neighbor = GetNeighbor(direction); if (!neighbor || elevation < neighbor.elevation) { return; }
Em seguida, precisamos limpar o rio de saída anterior. E também precisamos remover o rio que entra, se estiver sobreposto a um novo rio que sai. RemoveOutgoingRiver(); if (hasIncomingRiver && incomingRiver == direction) { RemoveIncomingRiver(); }
Agora podemos avançar para a criação do rio de saída. hasOutgoingRiver = true; outgoingRiver = direction; RefreshSelfOnly();
E não se esqueça de definir o rio de entrada para outra célula após remover o rio de entrada atual, se ele existir. neighbor.RemoveIncomingRiver(); neighbor.hasIncomingRiver = true; neighbor.incomingRiver = direction.Opposite(); neighbor.RefreshSelfOnly();
Livrar-se dos rios que correm
Agora que tornamos possível adicionar apenas os rios certos, outras ações ainda podem criar as erradas. Quando mudamos a altura da célula, devemos novamente garantir que os rios só possam fluir para baixo. Todos os rios irregulares devem ser removidos. public int Elevation { get { return elevation; } set { … if ( hasOutgoingRiver && elevation < GetNeighbor(outgoingRiver).elevation ) { RemoveOutgoingRiver(); } if ( hasIncomingRiver && elevation > GetNeighbor(incomingRiver).elevation ) { RemoveIncomingRiver(); } Refresh(); } }
unitypackageMudar rios
Para dar suporte à edição do rio, precisamos adicionar uma opção de rio à interface do usuário. De fato. precisamos de suporte para três modos de edição. Precisamos ignorar os rios, adicioná-los ou excluí-los. Podemos usar uma enumeração auxiliar simples de comutadores para rastrear o status. Como o usaremos apenas dentro do editor, podemos defini-lo dentro da classe HexMapEditor
, junto com o campo do modo rio. enum OptionalToggle { Ignore, Yes, No } OptionalToggle riverMode;
E precisamos de um método para alterar o regime fluvial por meio da interface do usuário. public void SetRiverMode (int mode) { riverMode = (OptionalToggle)mode; }
Para controlar o regime do rio, adicione três switches à interface do usuário e conecte-os ao novo grupo de alternância, como fizemos com as cores. Eu configurei os switches para que seus rótulos fiquem sob as caixas de seleção. Devido a isso, eles permanecerão finos o suficiente para caber nas três opções em uma linha.Rios da interface do usuárioPor que não usar uma lista suspensa?, . dropdown list Unity Play. , .
Arrastar e soltar reconhecimento
Para criar um rio, precisamos de uma célula e uma direção. No momento, HexMapEditor
não nos fornece essas informações. Portanto, precisamos adicionar suporte a arrastar e soltar de uma célula para outra.Precisamos saber se esse arrasto será correto e também determinar sua direção. E para reconhecer o arrastar e soltar, precisamos lembrar a célula anterior. bool isDrag; HexDirection dragDirection; HexCell previousCell;
Inicialmente, quando o arrasto não é realizado, a célula anterior não é. Ou seja, quando não há entrada ou não interagimos com o cartão, você precisa atribuir um valor a ele null
. void Update () { if ( Input.GetMouseButton(0) && !EventSystem.current.IsPointerOverGameObject() ) { HandleInput(); } else { previousCell = null; } } void HandleInput () { Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(inputRay, out hit)) { EditCells(hexGrid.GetCell(hit.point)); } else { previousCell = null; } }
A célula atual é a que encontramos cruzando a viga com a malha. Após editar as células, ela é atualizada e se torna a célula anterior para uma nova atualização. void HandleInput () { Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(inputRay, out hit)) { HexCell currentCell = hexGrid.GetCell(hit.point); EditCells(currentCell); previousCell = currentCell; } else { previousCell = null; } }
Após determinar a célula atual, podemos compará-la com a célula anterior, se houver. Se obtivermos duas células diferentes, poderemos arrastar e soltar corretamente e precisamos verificar isso. Caso contrário, isso definitivamente não é um arrastar e soltar. if (Physics.Raycast(inputRay, out hit)) { HexCell currentCell = hexGrid.GetCell(hit.point); if (previousCell && previousCell != currentCell) { ValidateDrag(currentCell); } else { isDrag = false; } EditCells(currentCell); previousCell = currentCell; isDrag = true; }
Como verificamos o arrastar e soltar? Verificando se a célula atual é vizinha da anterior. Verificamos isso contornando seus vizinhos em um ciclo. Se encontrarmos uma correspondência, também reconhecemos imediatamente a direção do arrasto. void ValidateDrag (HexCell currentCell) { for ( dragDirection = HexDirection.NE; dragDirection <= HexDirection.NW; dragDirection++ ) { if (previousCell.GetNeighbor(dragDirection) == currentCell) { isDrag = true; return; } } isDrag = false; }
Vamos criar arrastões bruscos?, . «» , .
, . .
Alterar células
Agora que podemos reconhecer o arrastar e soltar, podemos definir os rios que saem. Também podemos remover rios; por isso, o suporte de arrastar e soltar não é necessário. void EditCell (HexCell cell) { if (cell) { if (applyColor) { cell.Color = activeColor; } if (applyElevation) { cell.Elevation = activeElevation; } if (riverMode == OptionalToggle.No) { cell.RemoveRiver(); } else if (isDrag && riverMode == OptionalToggle.Yes) { previousCell.SetOutgoingRiver(dragDirection); } } }
Este código irá desenhar o rio da célula anterior para a corrente. Mas ele ignora o tamanho do pincel. Isso é bastante lógico, mas vamos desenhar os rios para todas as células fechadas pelo mato. Isso pode ser feito executando operações na célula editada. No nosso caso, precisamos garantir que outra célula realmente exista. else if (isDrag && riverMode == OptionalToggle.Yes) { HexCell otherCell = cell.GetNeighbor(dragDirection.Opposite()); if (otherCell) { otherCell.SetOutgoingRiver(dragDirection); } }
Agora podemos editar os rios, mas ainda não os vemos. Podemos verificar se isso funciona examinando as células modificadas no inspetor de depuração.Uma célula com um rio no inspetor de depuração.O que é um inspetor de depuração?. . , .
unitypackageLeitos fluviais entre células
Ao triangular um rio, precisamos considerar duas partes: a localização do leito do rio e a água que flui através dele. Primeiro, criaremos um canal e deixaremos a água para mais tarde.A parte mais simples do rio é onde ele flui em junções entre as células. Enquanto triangulamos esta área com uma faixa de três quadrantes. Podemos adicionar um leito de rio abaixando o quad do meio e adicionando duas paredes de canal.Adicionando um rio a uma tira de costela.Para isso, no caso do rio, serão necessários dois quadriláteros adicionais e será criado um canal com duas paredes verticais. Uma abordagem alternativa é usar quatro quad. Então abaixamos o pico do meio para criar uma cama com paredes inclinadas.Sempre quatro quad.O uso constante do mesmo número de quadrângulos é conveniente, então vamos escolher esta opção.Adicionando Tops de Borda
A transição de três para quatro por aresta requer a criação de um vértice adicional da aresta. Reescrevemos EdgeVertices
primeiro renomeando v4
para v5
e, em seguida, renomeando v3
para v4
. As ações nesta ordem garantem que todo o código continue referenciando os vértices corretos. Use a opção renomear ou refatorar do seu editor para fazer as alterações serem aplicadas em todos os lugares. Caso contrário, você terá que inspecionar manualmente todo o código e fazer alterações. public Vector3 v1, v2, v4, v5;
Depois de renomear tudo, adicione um novo v3
. public Vector3 v1, v2, v3, v4, v5;
Adicione um novo vértice ao construtor. Está localizado no meio, entre os picos dos cantos. Além disso, agora outros vértices devem estar em ½ e ¾, e não em & frac13; e & frac23;. public EdgeVertices (Vector3 corner1, Vector3 corner2) { v1 = corner1; v2 = Vector3.Lerp(corner1, corner2, 0.25f); v3 = Vector3.Lerp(corner1, corner2, 0.5f); v4 = Vector3.Lerp(corner1, corner2, 0.75f); v5 = corner2; }
Adicione v3
e entre TerraceLerp
. public static EdgeVertices TerraceLerp ( EdgeVertices a, EdgeVertices b, int step) { EdgeVertices result; result.v1 = HexMetrics.TerraceLerp(a.v1, b.v1, step); result.v2 = HexMetrics.TerraceLerp(a.v2, b.v2, step); result.v3 = HexMetrics.TerraceLerp(a.v3, b.v3, step); result.v4 = HexMetrics.TerraceLerp(a.v4, b.v4, step); result.v5 = HexMetrics.TerraceLerp(a.v5, b.v5, step); return result; }
Agora devo HexMesh
incluir um vértice adicional nos ventiladores dos triângulos da costela. void TriangulateEdgeFan (Vector3 center, EdgeVertices edge, Color color) { AddTriangle(center, edge.v1, edge.v2); AddTriangleColor(color); AddTriangle(center, edge.v2, edge.v3); AddTriangleColor(color); AddTriangle(center, edge.v3, edge.v4); AddTriangleColor(color); AddTriangle(center, edge.v4, edge.v5); AddTriangleColor(color); }
E também em suas faixas de quadriláteros. void TriangulateEdgeStrip ( EdgeVertices e1, Color c1, EdgeVertices e2, Color c2 ) { AddQuad(e1.v1, e1.v2, e2.v1, e2.v2); AddQuadColor(c1, c2); AddQuad(e1.v2, e1.v3, e2.v2, e2.v3); AddQuadColor(c1, c2); AddQuad(e1.v3, e1.v4, e2.v3, e2.v4); AddQuadColor(c1, c2); AddQuad(e1.v4, e1.v5, e2.v4, e2.v5); AddQuadColor(c1, c2); }
Comparação de quatro e cinco vértices por aresta.A altura do leito do rio
Criamos o canal abaixando a parte superior inferior da costela. Determina a posição vertical do leito do rio. Embora a posição vertical exata de cada célula esteja distorcida, devemos manter a mesma altura do leito do rio em células com a mesma altura. Graças a essa água, ela não precisa fluir a montante. Além disso, o leito deve ser baixo o suficiente para permanecer abaixo, mesmo no caso das células verticais mais desviadas, deixando ao mesmo tempo espaço suficiente para a água.Vamos definir esse deslocamento HexMetrics
e expressá-lo como altura. Compensações de um nível serão suficientes. public const float streamBedElevationOffset = -1f;
Podemos usar essa métrica para adicionar propriedades HexCell
para obter a posição vertical do leito do rio da célula. public float StreamBedY { get { return (elevation + HexMetrics.streamBedElevationOffset) * HexMetrics.elevationStep; } }
Criando um canal
Quando HexMesh
uma das seis partes triangulares de uma célula é triangulada, podemos determinar se um rio flui ao longo de sua borda. Nesse caso, podemos diminuir o pico médio da costela até a altura do leito do rio. void Triangulate (HexDirection direction, HexCell cell) { Vector3 center = cell.Position; EdgeVertices e = new EdgeVertices( center + HexMetrics.GetFirstSolidCorner(direction), center + HexMetrics.GetSecondSolidCorner(direction) ); if (cell.HasRiverThroughEdge(direction)) { e.v3.y = cell.StreamBedY; } TriangulateEdgeFan(center, e, cell.Color); if (direction <= HexDirection.SE) { TriangulateConnection(direction, cell, e); } }
Mude o vértice do meio da nervura.Podemos ver como os primeiros sinais do rio aparecem, mas surgem buracos no relevo. Para fechá-las, precisamos alterar outra extremidade e triangular a conexão. void TriangulateConnection ( HexDirection direction, HexCell cell, EdgeVertices e1 ) { HexCell neighbor = cell.GetNeighbor(direction); if (neighbor == null) { return; } Vector3 bridge = HexMetrics.GetBridge(direction); bridge.y = neighbor.Position.y - cell.Position.y; EdgeVertices e2 = new EdgeVertices( e1.v1 + bridge, e1.v5 + bridge ); if (cell.HasRiverThroughEdge(direction)) { e2.v3.y = neighbor.StreamBedY; } … }
Canais completos das articulações das costelas.unitypackageLeitos fluviais passando através de uma cela
Agora temos os leitos corretos entre as células. Mas quando o rio flui através da célula, os canais sempre terminam em seu centro. Para resolver este problema terá que funcionar. Vamos começar com o caso em que um rio flui diretamente através de uma célula, de uma extremidade à outra.Se não houver rio, cada parte da célula pode ser um simples leque de triângulos. Mas quando o rio flui diretamente, é necessário inserir um canal. De fato, precisamos esticar o vértice central em uma linha, transformando os dois triângulos do meio em quadrângulos. Então o leque de triângulos se transforma em um trapézio.Nós inserimos o canal no triângulo.Esses canais serão muito mais longos do que aqueles que passam pela conexão de células. Isso se torna aparente quando as posições dos vértices são distorcidas. Portanto, vamos dividir o trapézio em dois segmentos, inserindo outro conjunto de arestas de vértices no meio entre o centro e a aresta.Triangulação do canal.Como a triangulação com um rio será muito diferente da triangulação sem rio, vamos criar um método separado para isso. Se temos um rio, usamos esse método, caso contrário, deixaremos um leque de triângulos. void Triangulate (HexDirection direction, HexCell cell) { Vector3 center = cell.Position; EdgeVertices e = new EdgeVertices( center + HexMetrics.GetFirstSolidCorner(direction), center + HexMetrics.GetSecondSolidCorner(direction) ); if (cell.HasRiver) { if (cell.HasRiverThroughEdge(direction)) { e.v3.y = cell.StreamBedY; TriangulateWithRiver(direction, cell, center, e); } } else { TriangulateEdgeFan(center, e, cell.Color); } if (direction <= HexDirection.SE) { TriangulateConnection(direction, cell, e); } } void TriangulateWithRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { }
Buracos em que deveria haver rios.Para ver melhor o que acontece, desative temporariamente a distorção da célula. public const float cellPerturbStrength = 0f;
Picos sem distorção.Triangulação diretamente através da célula
Para criar um canal diretamente através de parte da célula, precisamos esticar o centro em uma linha. Esta linha deve ter a mesma largura que o canal. Podemos encontrar o vértice esquerdo movendo ¼ da distância do centro para o primeiro canto da parte anterior. void TriangulateWithRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { Vector3 centerL = center + HexMetrics.GetFirstSolidCorner(direction.Previous()) * 0.25f; }
Da mesma forma para o vértice certo. Nesse caso, precisamos do segundo canto da próxima parte. Vector3 centerL = center + HexMetrics.GetFirstSolidCorner(direction.Previous()) * 0.25f; Vector3 centerR = center + HexMetrics.GetSecondSolidCorner(direction.Next()) * 0.25f;
A linha do meio pode ser encontrada criando arestas de vértice entre o centro e a aresta. EdgeVertices m = new EdgeVertices( Vector3.Lerp(centerL, e.v1, 0.5f), Vector3.Lerp(centerR, e.v5, 0.5f) );
Em seguida, altere o vértice do meio da costela do meio, bem como o centro, porque eles se tornarão os pontos mais baixos do canal. m.v3.y = center.y = e.v3.y;
Agora podemos usar TriangulateEdgeStrip
para preencher o espaço entre a linha do meio e a linha da aresta. TriangulateEdgeStrip(m, cell.Color, e, cell.Color);
Canais comprimidos.Infelizmente, os canais parecem compactados. Isso acontece porque os vértices médios da costela estão muito próximos um do outro. Por que isso aconteceu?Se assumirmos que o comprimento da aresta externa é 1, o comprimento da linha central será ½. Como a borda do meio está localizada no meio entre eles, seu comprimento deve ser igual a ¾.A largura do canal é ½ e deve permanecer constante. Como o comprimento da aresta do meio é ¾, apenas ¼ permanece, de acordo com o & frac18; nos dois lados do canal.Comprimentos relativos.Como o comprimento da aresta do meio é ¾, o & frac18; torna-se relativo ao comprimento da costela média igual a & frac16;. Isso significa que seu segundo e quarto vértices devem ser interpolados com sextos, não com quartos.Podemos fornecer suporte para essa interpolação alternativa adicionando a EdgeVertices
outro construtor. Em vez de interpolações fixas para v2
e v4
vamos usar um parâmetro. public EdgeVertices (Vector3 corner1, Vector3 corner2, float outerStep) { v1 = corner1; v2 = Vector3.Lerp(corner1, corner2, outerStep); v3 = Vector3.Lerp(corner1, corner2, 0.5f); v4 = Vector3.Lerp(corner1, corner2, 1f - outerStep); v5 = corner2; }
Agora podemos usá-lo com o & frac16; c HexMesh.TriangulateWithRiver
. EdgeVertices m = new EdgeVertices( Vector3.Lerp(centerL, e.v1, 0.5f), Vector3.Lerp(centerR, e.v5, 0.5f), 1f / 6f );
Canais diretos.Tendo tornado o canal reto, podemos ir para a segunda parte do trapézio. Nesse caso, não podemos usar a faixa de costela, portanto, temos que fazê-lo manualmente. Vamos primeiro criar triângulos nas laterais. AddTriangle(centerL, m.v1, m.v2); AddTriangleColor(cell.Color); AddTriangle(centerR, m.v4, m.v5); AddTriangleColor(cell.Color);
Triângulos laterais.Parece bom, então vamos preencher o espaço restante com dois quadrângulos, criando a última parte do canal. AddTriangle(centerL, m.v1, m.v2); AddTriangleColor(cell.Color); AddQuad(centerL, center, m.v2, m.v3); AddQuadColor(cell.Color); AddQuad(center, centerR, m.v3, m.v4); AddQuadColor(cell.Color); AddTriangle(centerR, m.v4, m.v5); AddTriangleColor(cell.Color);
De fato, não temos uma alternativa que AddQuadColor
exija apenas um parâmetro. Enquanto não precisávamos disso. Então, vamos criá-lo. void AddQuadColor (Color color) { colors.Add(color); colors.Add(color); colors.Add(color); colors.Add(color); }
Canais diretos concluídos.Triangulação inicial e final
A triangulação de uma parte que tem apenas o começo ou o fim de um rio é bem diferente e, portanto, requer seu próprio método. Portanto, verificaremos isso Triangulate
e chamaremos o método apropriado. if (cell.HasRiver) { if (cell.HasRiverThroughEdge(direction)) { e.v3.y = cell.StreamBedY; if (cell.HasRiverBeginOrEnd) { TriangulateWithRiverBeginOrEnd(direction, cell, center, e); } else { TriangulateWithRiver(direction, cell, center, e); } } }
Nesse caso, queremos concluir o canal no centro, mas ainda usamos duas etapas para isso. Portanto, criaremos novamente a borda do meio entre o centro ou a borda. Como queremos concluir o canal, estamos muito felizes por ele ser compactado. void TriangulateWithRiverBeginOrEnd ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { EdgeVertices m = new EdgeVertices( Vector3.Lerp(center, e.v1, 0.5f), Vector3.Lerp(center, e.v5, 0.5f) ); }
Para que o canal não se torne raso muito rapidamente, atribuiremos a altura do leito do rio ao pico do meio. Mas o centro não precisa ser mudado. m.v3.y = e.v3.y;
Podemos triangular com uma tira de costela e um ventilador. TriangulateEdgeStrip(m, cell.Color, e, cell.Color); TriangulateEdgeFan(center, m, cell.Color);
Pontos inicial e final.Voltas em uma etapa
Em seguida, considere as curvas fechadas em zigue-zague entre as células adjacentes. Nós vamos lidar com eles também TriangulateWithRiver
. Portanto, precisamos determinar com que tipo de rio estamos trabalhando.Rio em zigue-zague.Se a célula tem um rio que flui na direção oposta, bem como na direção com a qual estamos trabalhando, então este deve ser um rio reto. Nesse caso, podemos salvar a linha central que já calculamos. Caso contrário, ele retornará a um ponto, dobrando a linha central. Vector3 centerL, centerR; if (cell.HasRiverThroughEdge(direction.Opposite())) { centerL = center + HexMetrics.GetFirstSolidCorner(direction.Previous()) * 0.25f; centerR = center + HexMetrics.GetSecondSolidCorner(direction.Next()) * 0.25f; } else { centerL = centerR = center; }
Ziguezagues enrolados.Podemos reconhecer curvas fechadas, verificando se a célula tem um rio passando pela parte seguinte ou anterior da célula. Se houver, precisamos alinhar a linha central com a aresta entre esta e a parte vizinha. Podemos fazer isso colocando o lado correspondente da linha no meio entre o centro e o ângulo comum. O outro lado da linha se torna o centro. if (cell.HasRiverThroughEdge(direction.Opposite())) { centerL = center + HexMetrics.GetFirstSolidCorner(direction.Previous()) * 0.25f; centerR = center + HexMetrics.GetSecondSolidCorner(direction.Next()) * 0.25f; } else if (cell.HasRiverThroughEdge(direction.Next())) { centerL = center; centerR = Vector3.Lerp(center, e.v5, 0.5f); } else if (cell.HasRiverThroughEdge(direction.Previous())) { centerL = Vector3.Lerp(center, e.v1, 0.5f); centerR = center; } else { centerL = centerR = center; }
Tendo decidido onde estão os pontos esquerdo e direito, podemos determinar o centro resultante calculando a média deles. if (cell.HasRiverThroughEdge(direction.Opposite())) { … } center = Vector3.Lerp(centerL, centerR, 0.5f);
Costela central deslocada.Embora o canal tenha a mesma largura nos dois lados, ele parece bastante compactado. Isso é causado ao girar a linha central 60 °. Você pode suavizar esse efeito aumentando ligeiramente a largura da linha central. Em vez de interpolar com ½, usamos & frac23;. else if (cell.HasRiverThroughEdge(direction.Next())) { centerL = center; centerR = Vector3.Lerp(center, e.v5, 2f / 3f); } else if (cell.HasRiverThroughEdge(direction.Previous())) { centerL = Vector3.Lerp(center, e.v1, 2f / 3f); centerR = center; }
Ziguezague sem compressão.Curvas de dois estágios
Os casos restantes são entre ziguezagues e rios retos. São curvas de dois estágios que criam rios suavemente curvos.O rio sinuoso.Para distinguir entre duas orientações possíveis, precisamos usar direction.Next().Next()
. Mas vamos torná-lo mais conveniente adicionando HexDirection
os métodos de extensão Next2
e Previous2
. public static HexDirection Previous2 (this HexDirection direction) { direction -= 2; return direction >= HexDirection.NE ? direction : (direction + 6); } public static HexDirection Next2 (this HexDirection direction) { direction += 2; return direction <= HexDirection.NW ? direction : (direction - 6); }
Voltar para HexMesh.TriangulateWithRiver
. Agora podemos reconhecer a direção do nosso rio sinuoso com direction.Next2()
. if (cell.HasRiverThroughEdge(direction.Opposite())) { centerL = center + HexMetrics.GetFirstSolidCorner(direction.Previous()) * 0.25f; centerR = center + HexMetrics.GetSecondSolidCorner(direction.Next()) * 0.25f; } else if (cell.HasRiverThroughEdge(direction.Next())) { centerL = center; centerR = Vector3.Lerp(center, e.v5, 2f / 3f); } else if (cell.HasRiverThroughEdge(direction.Previous())) { centerL = Vector3.Lerp(center, e.v1, 2f / 3f); centerR = center; } else if (cell.HasRiverThroughEdge(direction.Next2())) { centerL = centerR = center; } else { centerL = centerR = center; }
Nestes dois últimos casos, precisamos mudar a linha central para a parte da célula que está localizada no interior da curva. Se tivéssemos um vetor no meio de uma aresta sólida, poderíamos usá-lo para posicionar o ponto final. Vamos imaginar que temos um método para isso. else if (cell.HasRiverThroughEdge(direction.Next2())) { centerL = center; centerR = center + HexMetrics.GetSolidEdgeMiddle(direction.Next()) * 0.5f; } else { centerL = center + HexMetrics.GetSolidEdgeMiddle(direction.Previous()) * 0.5f; centerR = center; }
Claro, agora precisamos adicionar esse método ao HexMetrics
. Ele só precisa calcular em média dois vetores de ângulos adjacentes e aplicar o coeficiente de integridade. public static Vector3 GetSolidEdgeMiddle (HexDirection direction) { return (corners[(int)direction] + corners[(int)direction + 1]) * (0.5f * solidFactor); }
Curvas ligeiramente compactadas.Nossas linhas centrais agora estão giradas corretamente 30 °. Mas eles não são longos o suficiente, e é por isso que os canais são um pouco compactados. Isso acontece porque o ponto médio da costela está mais próximo do centro do que o ângulo da costela. Sua distância é igual ao raio interno, não ao externo. Ou seja, estamos trabalhando na escala errada.Já estamos convertendo de raio externo para interno em HexMetrics
. Precisamos executar a operação reversa. Então, vamos disponibilizar os dois fatores de conversão HexMetrics
. public const float outerToInner = 0.866025404f; public const float innerToOuter = 1f / outerToInner; public const float outerRadius = 10f; public const float innerRadius = outerRadius * outerToInner;
Agora podemos avançar para a escala certa HexMesh.TriangulateWithRiver
. Os canais ainda permanecerão um pouco apertados devido à sua vez, mas isso é muito menos pronunciado do que no caso dos ziguezagues. Portanto, não precisamos compensar isso. else if (cell.HasRiverThroughEdge(direction.Next2())) { centerL = center; centerR = center + HexMetrics.GetSolidEdgeMiddle(direction.Next()) * (0.5f * HexMetrics.innerToOuter); } else { centerL = center + HexMetrics.GetSolidEdgeMiddle(direction.Previous()) * (0.5f * HexMetrics.innerToOuter); centerR = center; }
Curvas suaves.unitypackageTriangulação nas proximidades de rios
Nossos rios estão prontos. Mas ainda não triangulamos outras partes das células que contêm os rios. Agora vamos fechar esses buracos.Buracos perto dos canais.Se a célula tiver um rio, mas não fluir na direção atual, Triangulate
chamaremos um novo método em. if (cell.HasRiver) { if (cell.HasRiverThroughEdge(direction)) { e.v3.y = cell.StreamBedY; if (cell.HasRiverBeginOrEnd) { TriangulateWithRiverBeginOrEnd(direction, cell, center, e); } else { TriangulateWithRiver(direction, cell, center, e); } } else { TriangulateAdjacentToRiver(direction, cell, center, e); } } else { TriangulateEdgeFan(center, e, cell.Color); }
Neste método, preenchemos o triângulo celular com uma faixa e um ventilador. Apenas um ventilador não será suficiente para nós, porque os picos devem corresponder à borda do meio das partes que contêm o rio. void TriangulateAdjacentToRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { EdgeVertices m = new EdgeVertices( Vector3.Lerp(center, e.v1, 0.5f), Vector3.Lerp(center, e.v5, 0.5f) ); TriangulateEdgeStrip(m, cell.Color, e, cell.Color); TriangulateEdgeFan(center, m, cell.Color); }
Sobreposição em curvas e rios retos.Corresponder ao canal
É claro que precisamos fazer o centro que usamos corresponder à parte central usada pelas partes do rio. Com os ziguezagues, tudo está em ordem, e curvas e rios retos exigem atenção. Portanto, precisamos determinar o tipo de rio e sua orientação relativa.Vamos começar verificando se estamos dentro da curva. Nesse caso, as direções anterior e seguinte contêm o rio. Nesse caso, precisamos mover o centro para a borda. if (cell.HasRiverThroughEdge(direction.Next())) { if (cell.HasRiverThroughEdge(direction.Previous())) { center += HexMetrics.GetSolidEdgeMiddle(direction) * (HexMetrics.innerToOuter * 0.5f); } } EdgeVertices m = new EdgeVertices( Vector3.Lerp(center, e.v1, 0.5f), Vector3.Lerp(center, e.v5, 0.5f) );
Corrigido um caso em que o rio flui de ambos os lados.Se temos um rio em uma direção diferente, mas não na anterior, então verificamos se é reto. Nesse caso, mova o centro para o primeiro canto. if (cell.HasRiverThroughEdge(direction.Next())) { if (cell.HasRiverThroughEdge(direction.Previous())) { center += HexMetrics.GetSolidEdgeMiddle(direction) * (HexMetrics.innerToOuter * 0.5f); } else if ( cell.HasRiverThroughEdge(direction.Previous2()) ) { center += HexMetrics.GetFirstSolidCorner(direction) * 0.25f; } }
Corrigida meia sobreposição com um rio reto.Então resolvemos o problema com metade das partes adjacentes aos rios retos. O último caso - temos um rio na direção anterior e é reto. Nesse caso, você precisa mover o centro para a próxima esquina. if (cell.HasRiverThroughEdge(direction.Next())) { if (cell.HasRiverThroughEdge(direction.Previous())) { center += HexMetrics.GetSolidEdgeMiddle(direction) * (HexMetrics.innerToOuter * 0.5f); } else if ( cell.HasRiverThroughEdge(direction.Previous2()) ) { center += HexMetrics.GetFirstSolidCorner(direction) * 0.25f; } } else if ( cell.HasRiverThroughEdge(direction.Previous()) && cell.HasRiverThroughEdge(direction.Next2()) ) { center += HexMetrics.GetSecondSolidCorner(direction) * 0.25f; }
Não há mais sobreposições.unitypackageGeneralização HexMesh
Concluímos a triangulação de canais. Agora podemos enchê-los com água. Como a água é diferente da terra, precisaremos usar uma malha diferente com diferentes dados de vértices e materiais diferentes. Seria bastante conveniente se pudéssemos usar HexMesh
sushi e água. Então, vamos generalizar HexMesh
, transformando-o em uma classe que lida com essas malhas, independentemente do que é usado. Vamos passar a tarefa de triangular suas células HexGridChunk
.Movendo o método Perturb
Como o método é Perturb
bastante generalizado e será usado em lugares diferentes, vamos para HexMetrics
. Primeiro, renomeie-o para HexMetrics.Perturb
. Este é um nome de método incorreto, mas refatora todo o código para seu uso adequado. Se o seu editor de código tiver uma funcionalidade especial para mover métodos, use-o.Movendo o método para dentro HexMetrics
, torne-o geral e estático e corrija seu nome. public static Vector3 Perturb (Vector3 position) { Vector4 sample = SampleNoise(position); position.x += (sample.x * 2f - 1f) * cellPerturbStrength; position.z += (sample.z * 2f - 1f) * cellPerturbStrength; return position; }
Métodos de triangulação em movimento
HexGridChunk
Na mudança de variáveis hexMesh
na variável compartilhada terrain
. public HexMesh terrain;
A seguir, refatoramos todos os métodos Add…
de HexMesh
c terrain.Add…
. Em seguida, mova todos os métodos Triangulate…
para HexGridChunk
. Você pode então corrigir os nomes de métodos Add…
em HexMesh
e torná-los comum. Como resultado, todos os métodos complexos de triangulação serão encontrados HexGridChunk
, e métodos simples para adicionar dados à malha permanecerão HexMesh
.Ainda não terminamos. Agora, ele HexGridChunk.LateUpdate
deve chamar seu próprio método Triangulate
. Além disso, não deve mais passar células como argumento. Portanto, ele Triangulate
pode perder seu parâmetro. E ele deve delegar a limpeza e aplicação dos dados da malha HexMesh
. void LateUpdate () { Triangulate();
Adicione os métodos necessários Clear
e Apply
no HexMesh
. public void Clear () { hexMesh.Clear(); vertices.Clear(); colors.Clear(); triangles.Clear(); } public void Apply () { hexMesh.SetVertices(vertices); hexMesh.SetColors(colors); hexMesh.SetTriangles(triangles, 0); hexMesh.RecalculateNormals(); meshCollider.sharedMesh = hexMesh; }
E as SetVertices, SetColors e SetTriangles?Mesh
. . , .
SetTriangles
integer, . , .
Por fim, anexe manualmente o filho da malha ao prefab do fragmento. Não podemos mais fazer isso automaticamente, porque adicionaremos um segundo filho à malha em breve. Renomeie-o para Terrain para indicar sua finalidade.Atribua um alívio.Renomear um filho pré-fabricado não funciona?. , . , Apply , . .
Criando pools de listas
Embora tenhamos movido bastante código, nosso mapa ainda deve funcionar da mesma maneira que antes. Adicionar outra malha ao fragmento não mudará isso. Mas se fizermos isso com o presente HexMesh
, poderão surgir erros.O problema é que assumimos que trabalharíamos apenas com uma malha por vez. Isso nos permitiu usar listas estáticas para armazenar dados de malha temporários. Mas, depois de adicionar água, trabalharemos simultaneamente com duas malhas, para que não possamos mais usar listas estáticas.No entanto, não retornaremos aos conjuntos de listas para cada instância HexMesh
. Em vez disso, usamos um pool de listas estático. Por padrão, esse pool não existe, então vamos começar criando uma classe comum de pool de listas. public static class ListPool<T> { }
Como o ListPool <T> funciona?, List<int>
. <T>
ListPool
, , . , T
( template).
Para armazenar uma coleção de listas em um pool, podemos usar a pilha. Normalmente, não uso listas porque o Unity não as serializa, mas, neste caso, não importa. using System.Collections.Generic; public static class ListPool<T> { static Stack<List<T>> stack = new Stack<List<T>>(); }
O que significa stack <list <t>>?. , . .
Adicione um método estático comum para obter a lista do pool. Se a pilha não estiver vazia, extrairemos a lista superior e retornaremos esta. Caso contrário, criaremos uma nova lista no local. public static List<T> Get () { if (stack.Count > 0) { return stack.Pop(); } return new List<T>(); }
Para reutilizar listas, você precisa adicioná-las ao pool depois de terminar de trabalhar com elas. ListPool
irá limpar a lista e colocá-la na pilha. public static void Add (List<T> list) { list.Clear(); stack.Push(list); }
Agora podemos usar as piscinas HexMesh
. Substitua listas estáticas por links privados não estáticos. Vamos marcá-los NonSerialized
para que o Unity não os preserve durante a recompilação. Ou escreva System.NonSerialized
ou adicione using System;
no início do script. [NonSerialized] List<Vector3> vertices; [NonSerialized] List<Color> colors; [NonSerialized] List<int> triangles;
Como a malha é limpa logo antes de adicionar novos dados, é aqui que você precisa obter listas dos pools. public void Clear () { hexMesh.Clear(); vertices = ListPool<Vector3>.Get(); colors = ListPool<Color>.Get(); triangles = ListPool<int>.Get(); }
Depois de aplicar essas malhas, não precisamos mais delas, então aqui podemos adicioná-las às piscinas. public void Apply () { hexMesh.SetVertices(vertices); ListPool<Vector3>.Add(vertices); hexMesh.SetColors(colors); ListPool<Color>.Add(colors); hexMesh.SetTriangles(triangles, 0); ListPool<int>.Add(triangles); hexMesh.RecalculateNormals(); meshCollider.sharedMesh = hexMesh; }
Por isso, implementamos o uso múltiplo de listas, independentemente de quantas malhas preenchermos ao mesmo tempo.Colisor opcional
Embora nosso terreno precise de um colisor, ele não é realmente necessário para os rios. Os raios simplesmente passarão pela água e se cruzarão com o canal abaixo. Vamos fazer para que possamos configurar a presença de um colisor para HexMesh
. Percebemos isso adicionando um campo comum bool useCollider
. Para terrenos, ligamos. public bool useCollider;
Usando um colisor de malha.Precisamos criar e atribuir o colisor somente quando ele estiver ativado. void Awake () { GetComponent<MeshFilter>().mesh = hexMesh = new Mesh(); if (useCollider) { meshCollider = gameObject.AddComponent<MeshCollider>(); } hexMesh.name = "Hex Mesh"; } public void Apply () { … if (useCollider) { meshCollider.sharedMesh = hexMesh; } … }
Cores opcionais
As cores dos vértices também podem ser opcionais. Precisamos deles para demonstrar vários tipos de relevo, mas a água não muda de cor. Podemos torná-los opcionais, assim como tornamos o colisor opcional. public bool useCollider, useColors; public void Clear () { hexMesh.Clear(); vertices = ListPool<Vector3>.Get(); if (useColors) { colors = ListPool<Color>.Get(); } triangles = ListPool<int>.Get(); } public void Apply () { hexMesh.SetVertices(vertices); ListPool<Vector3>.Add(vertices); if (useColors) { hexMesh.SetColors(colors); ListPool<Color>.Add(colors); } … }
Obviamente, o terreno deve usar as cores dos vértices, então ligue-os.Uso de cores.UV opcional
Enquanto fazemos isso, também podemos adicionar suporte para coordenadas UV opcionais. Embora o alívio não os use, precisaremos deles para obter água. public bool useCollider, useColors, useUVCoordinates; [NonSerialized] List<Vector2> uvs; public void Clear () { hexMesh.Clear(); vertices = ListPool<Vector3>.Get(); if (useColors) { colors = ListPool<Color>.Get(); } if (useUVCoordinates) { uvs = ListPool<Vector2>.Get(); } triangles = ListPool<int>.Get(); } public void Apply () { hexMesh.SetVertices(vertices); ListPool<Vector3>.Add(vertices); if (useColors) { hexMesh.SetColors(colors); ListPool<Color>.Add(colors); } if (useUVCoordinates) { hexMesh.SetUVs(0, uvs); ListPool<Vector2>.Add(uvs); } … }
Nós não usamos coordenadas UV.Para usar esta função, crie métodos para adicionar coordenadas UV a triângulos e quadrângulos. public void AddTriangleUV (Vector2 uv1, Vector2 uv2, Vector3 uv3) { uvs.Add(uv1); uvs.Add(uv2); uvs.Add(uv3); } public void AddQuadUV (Vector2 uv1, Vector2 uv2, Vector3 uv3, Vector3 uv4) { uvs.Add(uv1); uvs.Add(uv2); uvs.Add(uv3); uvs.Add(uv4); }
Vamos adicionar um método adicional AddQuadUV
para adicionar convenientemente uma área UV retangular. Este é o caso padrão quando o quadrilátero e sua textura são os mesmos, vamos usá-lo para a água do rio. public void AddQuadUV (float uMin, float uMax, float vMin, float vMax) { uvs.Add(new Vector2(uMin, vMin)); uvs.Add(new Vector2(uMax, vMin)); uvs.Add(new Vector2(uMin, vMax)); uvs.Add(new Vector2(uMax, vMax)); }
unitypackageRios atuais
Finalmente, é hora de criar água! Faremos isso com um quad, que indicará a superfície da água. E como trabalhamos com rios, a água deve fluir. Para fazer isso, usamos coordenadas UV indicando a orientação do rio. Para visualizar isso, precisamos de um novo shader. Portanto, crie um novo shader padrão e chame-o de River . Altere-o para que as coordenadas UV sejam gravadas nos canais verde e vermelho do albedo. Shader "Custom/River" { … 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; o.Albedo.rg = IN.uv_MainTex; } ENDCG } FallBack "Diffuse" }
Adicione ao HexGridChunk
campo geral HexMesh rivers
. Nós limpamos e aplicamos da mesma maneira que no caso de alívio. public HexMesh terrain, rivers; public void Triangulate () { terrain.Clear(); rivers.Clear(); for (int i = 0; i < cells.Length; i++) { Triangulate(cells[i]); } terrain.Apply(); rivers.Apply(); }
Teremos chamadas adicionais, mesmo que não tenhamos rios?Unity , . , - .
Altere a pré-fabricada (por meio da instância), duplicando seu objeto de terreno, renomeando-o para Rivers e conectando-o.Fragmento pré-fabricado com rios.Crie o material River usando nosso novo shader e faça com que o objeto Rivers o use . Também configuramos o componente de malha hexagonal do objeto para que ele use coordenadas UV, mas não use cores de vértice ou colisor.Subobjeto Rivers.Água triangulada
Antes de podermos triangular a água, precisamos determinar o nível de sua superfície. Vamos fazer uma mudança de altura HexMetrics
, como fizemos com o leito do rio. Como a distorção vertical da célula é igual a metade da mudança de altura, vamos usá-la para mudar a superfície do rio. Portanto, garantimos que a água nunca estará acima da topografia da célula. public const float riverSurfaceElevationOffset = -0.5f;
Por que não diminuir um pouco?, . , .
Adicione uma HexCell
propriedade para obter a posição vertical da superfície do rio. public float RiverSurfaceY { get { return (elevation + HexMetrics.riverSurfaceElevationOffset) * HexMetrics.elevationStep; } }
Agora podemos trabalhar HexGridChunk
! Como criaremos muitos quadrângulos de rios, vamos adicionar um método separado para isso. Vamos dar quatro vértices e uma altura como parâmetros. Isso nos permitirá definir convenientemente a posição vertical de todos os quatro vértices simultaneamente antes de adicionar quad. void TriangulateRiverQuad ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, float y ) { v1.y = v2.y = v3.y = v4.y = y; rivers.AddQuad(v1, v2, v3, v4); }
Vamos adicionar aqui as coordenadas UV do quadrilátero. Basta girar da esquerda para a direita e de baixo para cima. rivers.AddQuad(v1, v2, v3, v4); rivers.AddQuadUV(0f, 1f, 0f, 1f);
TriangulateWithRiver
- Este é o primeiro método ao qual adicionaremos os quadrângulos dos rios. O primeiro quad está entre o centro e o meio. O segundo é entre o meio e a costela. Nós apenas usamos os vértices que já temos. Como esses picos serão subestimados, a água ficará parcialmente sob as paredes inclinadas do canal. Portanto, não precisamos nos preocupar com a posição exata da beira da água. void TriangulateWithRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … TriangulateRiverQuad(centerL, centerR, m.v2, m.v4, cell.RiverSurfaceY); TriangulateRiverQuad(m.v2, m.v4, e.v2, e.v4, cell.RiverSurfaceY); }
Os primeiros sinais de água.Por que a largura da água muda?, , — . . .
Movendo-se com o fluxo
Atualmente, as coordenadas UV não são consistentes com a direção do rio. Precisamos manter a consistência aqui. Suponha que a coordenada U seja 0 no lado esquerdo do rio e 1 no direito, ao olhar rio abaixo. E a coordenada V deve variar de 0 a 1 na direção do rio.Usando esta especificação, os UVs estarão corretos quando o rio de saída for triangulado, mas eles ficarão incorretos e precisarão ser revertidos quando o rio de entrada for triangulado. Para tornar mais fácil, adicione um TriangulateRiverQuad
parâmetro bool reversed
. Use-o para virar UV, se necessário. void TriangulateRiverQuad ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, float y, bool reversed ) { v1.y = v2.y = v3.y = v4.y = y; rivers.AddQuad(v1, v2, v3, v4); if (reversed) { rivers.AddQuadUV(1f, 0f, 1f, 0f); } else { rivers.AddQuadUV(0f, 1f, 0f, 1f); } }
Como TriangulateWithRiver
sabemos que precisamos virar a direção, quando se lida com o rio entrada. bool reversed = cell.IncomingRiver == direction; TriangulateRiverQuad( centerL, centerR, m.v2, m.v4, cell.RiverSurfaceY, reversed ); TriangulateRiverQuad( m.v2, m.v4, e.v2, e.v4, cell.RiverSurfaceY, reversed );
A direção acordada dos rios.O começo e o fim do rio
No interior, TriangulateWithRiverBeginOrEnd
precisamos apenas verificar se temos um rio de entrada para determinar a direção do fluxo. Então podemos inserir outro rio quad entre o meio e a costela. void TriangulateWithRiverBeginOrEnd ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … bool reversed = cell.HasIncomingRiver; TriangulateRiverQuad( m.v2, m.v4, e.v2, e.v4, cell.RiverSurfaceY, reversed ); }
A parte entre o centro e o meio é um triângulo, por isso não podemos usá-lo TriangulateRiverQuad
. A única diferença significativa aqui é que o pico central está no meio do rio. Portanto, sua coordenada U é sempre igual a ½. center.y = m.v2.y = m.v4.y = cell.RiverSurfaceY; rivers.AddTriangle(center, m.v2, m.v4); if (reversed) { rivers.AddTriangleUV( new Vector2(0.5f, 1f), new Vector2(1f, 0f), new Vector2(0f, 0f) ); } else { rivers.AddTriangleUV( new Vector2(0.5f, 0f), new Vector2(0f, 1f), new Vector2(1f, 1f) ); }
Água no começo e no fim.Faltam porções de água nas extremidades?, quad , . . .
, . , . .
Fluxo entre células
Ao adicionar água entre as células, devemos ter cuidado com a diferença de altura. Para que a água possa fluir ladeiras e falésias, ela TriangulateRiverQuad
deve suportar dois parâmetros de altura. Então, vamos adicionar um segundo. void TriangulateRiverQuad ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, float y1, float y2, bool reversed ) { v1.y = v2.y = y1; v3.y = v4.y = y2; rivers.AddQuad(v1, v2, v3, v4); if (reversed) { rivers.AddQuadUV(1f, 0f, 1f, 0f); } else { rivers.AddQuadUV(0f, 1f, 0f, 1f); } }
Além disso, por conveniência, vamos adicionar uma opção que receberá uma altura. Apenas chamará outro método. void TriangulateRiverQuad ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, float y, bool reversed ) { TriangulateRiverQuad(v1, v2, v3, v4, y, y, reversed); }
Agora podemos adicionar quad river e in TriangulateConnection
. Estando entre as células, não podemos descobrir imediatamente com que tipo de rio estamos lidando. Para determinar se um giro é necessário, precisamos verificar se temos um rio recebido e se ele está se movendo em nossa direção. if (cell.HasRiverThroughEdge(direction)) { e2.v3.y = neighbor.StreamBedY; TriangulateRiverQuad( e1.v2, e1.v4, e2.v2, e2.v4, cell.RiverSurfaceY, neighbor.RiverSurfaceY, cell.HasIncomingRiver && cell.IncomingRiver == direction ); }
O rio concluído.Alongamento de coordenadas V
Até agora, em cada segmento do rio, temos coordenadas V indo de 0 a 1. Ou seja, existem apenas quatro delas na célula. Cinco se também adicionarmos conexões entre células. Tudo o que usamos para texturizar o rio, ele deve ser repetido tantas vezes.Podemos reduzir o número de repetições esticando as coordenadas V para que elas passem de 0 a 1 por toda a célula mais uma conexão. Isso pode ser feito aumentando a coordenada V em cada segmento em 0,2. Se colocarmos 0,4 no centro, então no meio ele se tornará 0,6 e na borda chegará a 0,8. Em seguida, na conexão da célula, o valor será 1.Se o rio fluir na direção oposta, ainda podemos colocar 0,4 no centro, mas no meio ele se tornará 0,2 e na margem - 0. Se continuarmos até a célula se unir, o resultado será -0,2. Isso é normal porque é semelhante a 0,8 para uma textura com modo de filtragem repetida, assim como 0 é equivalente a 1.Mudança de coordenadas V.Para criar suporte para isso, precisamos adicionar TriangulateRiverQuad
mais um parâmetro. void TriangulateRiverQuad ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, float y, float v, bool reversed ) { TriangulateRiverQuad(v1, v2, v3, v4, y, y, v, reversed); } void TriangulateRiverQuad ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, float y1, float y2, float v, bool reversed ) { … }
Quando a direção não é invertida, simplesmente usamos a coordenada transmitida na parte inferior do quadrilátero e adicionamos 0,2 na parte superior. else { rivers.AddQuadUV(0f, 1f, v, v + 0.2f); }
Podemos trabalhar com uma direção invertida subtraindo a coordenada de 0,8 e 0,6. if (reversed) { rivers.AddQuadUV(1f, 0f, 0.8f - v, 0.6f - v); }
Agora devemos transmitir as coordenadas corretas, como se estivéssemos lidando com um rio que sai. Vamos começar com TriangulateWithRiver
. TriangulateRiverQuad( centerL, centerR, m.v2, m.v4, cell.RiverSurfaceY, 0.4f, reversed ); TriangulateRiverQuad( m.v2, m.v4, e.v2, e.v4, cell.RiverSurfaceY, 0.6f, reversed );
Em seguida, TriangulateConnection
alterar o seguinte. TriangulateRiverQuad( e1.v2, e1.v4, e2.v2, e2.v4, cell.RiverSurfaceY, neighbor.RiverSurfaceY, 0.8f, cell.HasIncomingRiver && cell.IncomingRiver == direction );
Finalmente TriangulateWithRiverBeginOrEnd
. TriangulateRiverQuad( m.v2, m.v4, e.v2, e.v4, cell.RiverSurfaceY, 0.6f, reversed ); center.y = m.v2.y = m.v4.y = cell.RiverSurfaceY; rivers.AddTriangle(center, m.v2, m.v4); if (reversed) { rivers.AddTriangleUV( new Vector2(0.5f, 0.4f), new Vector2(1f, 0.2f), new Vector2(0f, 0.2f) ); } else { rivers.AddTriangleUV( new Vector2(0.5f, 0.4f), new Vector2(0f, 0.6f), new Vector2(1f, 0.6f) ); }
Coordenadas esticadas em V.Para exibir corretamente o dobramento das coordenadas em V, verifique se elas permanecem positivas no sombreador de rio. if (IN.uv_MainTex.y < 0) { IN.uv_MainTex.y += 1; } o.Albedo.rg = IN.uv_MainTex;
Coordenadas recolhidas V.unitypackageAnimação do rio
Tendo terminado com as coordenadas UV, podemos passar a animar os rios. O shader do rio fará isso para que não tenhamos que atualizar constantemente a malha.Não criaremos um shader de rio complexo neste tutorial, mas o faremos mais tarde. Por enquanto, criaremos um efeito simples que permita entender como a animação funciona.A animação é criada alterando as coordenadas V com base no tempo do jogo. O Unity permite que você obtenha seu valor usando uma variável _Time
. Seu componente Y contém o tempo inalterado que usamos. Outros componentes contêm diferentes escalas de tempo.Vamos nos livrar da dobra ao longo de V, porque não precisamos mais dela. Em vez disso, subtraímos o tempo atual da coordenada V. Isso muda a coordenada para baixo, o que cria a ilusão da corrente que flui a jusante do rio. // if (IN.uv_MainTex.y < 0) { // IN.uv_MainTex.y += 1; // } IN.uv_MainTex.y -= _Time.y; o.Albedo.rg = IN.uv_MainTex;
Em um segundo, a coordenada V em todos os pontos se tornará menor que zero, portanto não veremos mais a diferença. Novamente, isso é normal ao usar a filtragem no modo de repetição de textura. Mas, para ver o que acontece, podemos pegar a parte fracionária da coordenada V. IN.uv_MainTex.y -= _Time.y; IN.uv_MainTex.y = frac(IN.uv_MainTex.y); o.Albedo.rg = IN.uv_MainTex;
Coordenadas em V animadas.Uso de ruído
Agora nosso rio está animado, mas na direção e velocidade há transições nítidas. Nosso padrão UV os torna bastante óbvios, mas será mais difícil reconhecer se você usar um padrão mais parecido com a água. Então, em vez de exibir UV cru, vamos provar a textura. Podemos usar nossa textura de ruído existente. Nós o amostramos e multiplicamos a cor do material pelo primeiro canal de ruído. void surf (Input IN, inout SurfaceOutputStandard o) { float2 uv = IN.uv_MainTex; uv.y -= _Time.y; float4 noise = tex2D(_MainTex, uv); fixed4 c = _Color * noise.r; o.Albedo = c.rgb; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; }
Atribua a textura do ruído ao material do rio e verifique se ele é branco.Usando textura de ruído.Como as coordenadas V são muito esticadas, a textura do ruído também se estende ao longo do rio. Infelizmente, o curso não é muito bonito. Vamos tentar esticá-lo de outra maneira - reduzir bastante a escala das coordenadas de U. Um décimo sexto será suficiente. Isso significa que apenas amostraremos uma faixa estreita de textura de ruído. float2 uv = IN.uv_MainTex; uv.x *= 0.0625; uv.y -= _Time.y;
Alongando a coordenada U.Vamos também diminuir para um quarto por segundo, para que a conclusão do ciclo de textura leve quatro segundos. uv.y -= _Time.y * 0.25;
O barulho atual.Mistura de ruído
Tudo já parece muito melhor, mas o padrão sempre permanece o mesmo. A água não se comporta assim.Como usamos apenas uma pequena faixa de ruído, podemos variar o padrão deslocando essa faixa ao longo da textura. Isso é feito adicionando tempo à coordenada U. Devemos fazê-lo lentamente, caso contrário o rio parecerá fluir de lado. Vamos tentar o coeficiente de 0,005. Isso significa que são necessários 200 segundos para concluir o padrão. uv.x = uv.x * 0.0625 + _Time.y * 0.005;
Barulho em movimento.Infelizmente, isso não parece muito bonito. A água ainda parece estática e a mudança é claramente perceptível, embora seja muito lenta. Podemos ocultar a mudança combinando duas amostras de ruído e deslocando-as em direções opostas. E se usarmos valores ligeiramente diferentes para mover a segunda amostra, criaremos uma animação leve da mudança.Assim, como resultado, nunca sobrepomos o mesmo padrão de ruído, usamos um canal diferente para a segunda amostra. float2 uv = IN.uv_MainTex; uv.x = uv.x * 0.0625 + _Time.y * 0.005; uv.y -= _Time.y * 0.25; float4 noise = tex2D(_MainTex, uv); float2 uv2 = IN.uv_MainTex; uv2.x = uv2.x * 0.0625 - _Time.y * 0.0052; uv2.y -= _Time.y * 0.23; float4 noise2 = tex2D(_MainTex, uv2); fixed4 c = _Color * (noise.r * noise2.a);
Uma combinação de dois padrões de ruído variáveis.Água translúcida
Nosso padrão parece bastante dinâmico. O próximo passo é torná-lo translúcido.Primeiro, verifique se a água não projeta sombras. Você pode desativá-los através do componente renderizador do objeto Rivers na pré-fabricada.A projeção de sombra está desativada.Agora mude o sombreador para o modo transparente. Para indicar isso, use tags shader. Em seguida, adicione a #pragma surface
palavra - chave à linha alpha
. Enquanto estamos aqui, você pode remover a palavra-chave fullforwardshadows
, porque ainda não projetamos sombras. Tags { "RenderType"="Transparent" "Queue"="Transparent" } LOD 200 CGPROGRAM #pragma surface surf Standard alpha // fullforwardshadows #pragma target 3.0
Agora vamos mudar a maneira como definimos a cor do rio. Em vez de multiplicar o ruído por cor, adicionaremos ruído a ele. Em seguida, usamos a função saturate
para limitar o resultado para que não exceda 1. fixed4 c = saturate(_Color + noise.r * noise2.a);
Isso nos permitirá usar a cor do material como cor de base. O ruído aumentará seu brilho e opacidade. Vamos tentar usar uma cor azul com uma opacidade bastante baixa. Como resultado, obtemos água translúcida azul com salpicos brancos.Água translúcida colorida.unitypackageConclusão
Agora que tudo parece estar funcionando, é hora de distorcer os picos novamente. Além de deformar as bordas das células, isso tornará nossos rios irregulares. public const float cellPerturbStrength = 4f;
Picos distorcidos e distorcidos.Vamos examinar o terreno em busca de problemas que surgiram devido à distorção. Parece que eles são! Vamos conferir as cachoeiras altas.Água truncada por falésias.A água que cai de uma cachoeira alta desaparece atrás de um penhasco. Quando isso acontece, é muito perceptível, por isso precisamos fazer algo a respeito.Muito menos óbvio é que as cachoeiras podem estar inclinadas, em vez de descer diretamente. Embora a água na realidade não flua dessa maneira, ela não é particularmente perceptível. Nosso cérebro irá interpretá-lo de tal maneira que parece normal para nós. Então apenas ignore.A maneira mais fácil de evitar a perda de água é aprofundando os leitos dos rios. Então, criaremos mais espaço entre a superfície da água e o leito do rio. Isso também tornará as paredes do canal mais verticais, para não ir muito fundo. Vamos perguntarHexMetrics.streamBedElevationOffset
valor -1,75. Isso resolverá a maior parte dos problemas e a cama não ficará muito profunda. Parte da água ainda será cortada, mas não as cachoeiras inteiras. public const float streamBedElevationOffset = -1.75f;
Canais detalhados.unitypackageParte 7: estradas
- Adicione suporte rodoviário.
- Triangular a estrada.
- Combinamos estradas e rios.
- Melhorando a aparência das estradas.
Os primeiros sinais da civilização.Células com estradas
Como rios, as estradas passam de célula em célula, através do meio das bordas da célula. A grande diferença é que não há água fluindo nas estradas, portanto elas são bidirecionais. Além disso, são necessárias interseções para uma rede viária funcional, portanto, precisamos oferecer suporte a mais de duas estradas por célula.Se você permitir que as estradas sigam nas seis direções, a célula poderá conter de zero a seis estradas. Isso é um total de quatorze configurações possíveis de estradas. Isso é muito mais do que cinco configurações possíveis de rios. Para lidar com isso, precisamos usar uma abordagem mais geral que possa lidar com todas as configurações.14 possíveis configurações de estrada.Rastreamento rodoviário
A maneira mais simples de rastrear estradas em uma célula é usar uma matriz de valores booleanos. Adicione o campo privado da matriz HexCell
e torne-o serializável para que você possa vê-lo no inspetor. Defina o tamanho da matriz através da prefab da célula para suportar seis estradas. [SerializeField] bool[] roads;
Cela pré-fabricada com seis estradas.Adicione um método para verificar se a célula possui um caminho em uma determinada direção. public bool HasRoadThroughEdge (HexDirection direction) { return roads[(int)direction]; }
Também será conveniente saber se há pelo menos uma estrada na célula, portanto, adicionaremos uma propriedade para isso. Basta percorrer a matriz no loop e retornar true
assim que encontrarmos o caminho. Se não houver estradas, volte false
. public bool HasRoads { get { for (int i = 0; i < roads.Length; i++) { if (roads[i]) { return true; } } return false; } }
Remoção de estradas
Como nos rios, adicionaremos um método para remover todas as estradas da célula. Isso pode ser feito com um loop que desconecta todas as estradas que foram ativadas anteriormente. public void RemoveRoads () { for (int i = 0; i < neighbors.Length; i++) { if (roads[i]) { roads[i] = false; } } }
Obviamente, também precisamos desativar as células caras correspondentes nos vizinhos. if (roads[i]) { roads[i] = false; neighbors[i].roads[(int)((HexDirection)i).Opposite()] = false; }
Depois disso, precisamos atualizar cada uma das células. Como as estradas são locais para as células, precisamos atualizar apenas as próprias células sem seus vizinhos. if (roads[i]) { roads[i] = false; neighbors[i].roads[(int)((HexDirection)i).Opposite()] = false; neighbors[i].RefreshSelfOnly(); RefreshSelfOnly(); }
Adicionando estradas
Adicionar estradas é semelhante a remover estradas. A única diferença é que atribuímos um valor a booleano true
, não false
. Podemos criar um método privado que pode executar as duas operações. Então será possível usá-lo para adicionar e remover a estrada. public void AddRoad (HexDirection direction) { if (!roads[(int)direction]) { SetRoad((int)direction, true); } } public void RemoveRoads () { for (int i = 0; i < neighbors.Length; i++) { if (roads[i]) { SetRoad(i, false); } } } void SetRoad (int index, bool state) { roads[index] = state; neighbors[index].roads[(int)((HexDirection)index).Opposite()] = state; neighbors[index].RefreshSelfOnly(); RefreshSelfOnly(); }
Não podemos ter um rio e uma estrada seguindo na mesma direção ao mesmo tempo. Portanto, antes de adicionar a estrada, verificaremos se há um local para ela. public void AddRoad (HexDirection direction) { if (!roads[(int)direction] && !HasRiverThroughEdge(direction)) { SetRoad((int)direction, true); } }
Além disso, as estradas não podem ser combinadas com falésias porque são muito afiadas. Ou talvez valha a pena abrir o caminho através de um penhasco baixo, mas não através de um alto? Para determinar isso, precisamos criar um método que nos diga a diferença de altura em uma determinada direção. public int GetElevationDifference (HexDirection direction) { int difference = elevation - GetNeighbor(direction).elevation; return difference >= 0 ? difference : -difference; }
Agora podemos fazer com que as estradas aumentem a uma diferença de altura suficientemente pequena. Vou me limitar apenas a declives, ou seja, no máximo 1 unidade. public void AddRoad (HexDirection direction) { if ( !roads[(int)direction] && !HasRiverThroughEdge(direction) && GetElevationDifference(direction) <= 1 ) { SetRoad((int)direction, true); } }
Remoção das estradas erradas
Tornamos as estradas adicionadas apenas quando permitidas. Agora precisamos garantir que eles sejam removidos se, posteriormente, se tornarem incorretos, por exemplo, ao adicionar um rio. Podemos proibir a colocação de rios no topo das estradas, mas os rios não são interrompidos pelas estradas. Deixe-os lavar a estrada fora do caminho.Será o suficiente para pedirmos a estrada false
, independentemente de ser a estrada. Não vai ser sempre atualizado ambas as células, de modo que não temos mais necessidade de chamar explicitamente RefreshSelfOnly
no SetOutgoingRiver
. public void SetOutgoingRiver (HexDirection direction) { if (hasOutgoingRiver && outgoingRiver == direction) { return; } HexCell neighbor = GetNeighbor(direction); if (!neighbor || elevation < neighbor.elevation) { return; } RemoveOutgoingRiver(); if (hasIncomingRiver && incomingRiver == direction) { RemoveIncomingRiver(); } hasOutgoingRiver = true; outgoingRiver = direction;
Outra operação que pode dar errado na estrada é uma mudança de altura. Nesse caso, teremos que procurar estradas em todas as direções. Se a diferença de altura for muito grande, a estrada existente precisará ser excluída. public int Elevation { get { return elevation; } set { … for (int i = 0; i < roads.Length; i++) { if (roads[i] && GetElevationDifference((HexDirection)i) > 1) { SetRoad(i, false); } } Refresh(); } }
unitypackageEdição de estradas
A edição de estradas funciona como a edição de rios. Portanto HexMapEditor
, é necessária mais uma opção, além de um método para definir seu estado. OptionalToggle riverMode, roadMode; public void SetRiverMode (int mode) { riverMode = (OptionalToggle)mode; } public void SetRoadMode (int mode) { roadMode = (OptionalToggle)mode; }
O método EditCell
agora deve apoiar a remoção com a adição de estradas. Isso significa que, ao arrastar e soltar, ele pode executar uma das duas ações possíveis. Estamos reestruturando um pouco o código para que, ao executar o arrastar e soltar correto, os estados de ambos os comutadores sejam verificados. void EditCell (HexCell cell) { if (cell) { if (applyColor) { cell.Color = activeColor; } if (applyElevation) { cell.Elevation = activeElevation; } if (riverMode == OptionalToggle.No) { cell.RemoveRiver(); } if (roadMode == OptionalToggle.No) { cell.RemoveRoads(); } if (isDrag) { HexCell otherCell = cell.GetNeighbor(dragDirection.Opposite()); if (otherCell) { if (riverMode == OptionalToggle.Yes) { otherCell.SetOutgoingRiver(dragDirection); } if (roadMode == OptionalToggle.Yes) { otherCell.AddRoad(dragDirection); } } } } }
Podemos adicionar rapidamente uma barra de estrada à interface do usuário copiando a barra do rio e alterando o método chamado pelos comutadores.Como resultado, obtemos uma interface do usuário bastante alta. Para consertar isso, mudei o layout do painel colorido para ajustar os painéis rodoviários e fluviais mais compactos.UI com estradas.Como agora eu uso duas linhas de três opções para cores, há espaço para outra cor. Então eu adicionei um item para laranja.Cinco cores: amarelo, verde, azul, laranja e branco.Agora podemos editar as estradas, mas até agora elas não são visíveis. Você pode usar o inspetor para garantir que tudo esteja funcionando.Cela com estradas no inspetor.unitypackageTriangulação da estrada
Para exibir estradas, você precisa triangulá-las. Isso é semelhante à criação de uma malha para rios, apenas o leito do rio não aparecerá no relevo.Primeiro, crie um novo shader padrão que usará novamente as coordenadas UV para pintar a superfície da estrada. Shader "Custom/Road" { 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; }; half _Glossiness; half _Metallic; fixed4 _Color; void surf (Input IN, inout SurfaceOutputStandard o) { fixed4 c = fixed4(IN.uv_MainTex, 1, 1); o.Albedo = c.rgb; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; } ENDCG } FallBack "Diffuse" }
Crie um material de estrada usando esse shader.Estrada material.Defina a pré-fabricação do fragmento para que ele receba outra malha filho de hexágonos para as estradas. Essa malha não deve projetar sombras e deve usar apenas coordenadas UV. A maneira mais rápida de fazer isso é através de uma instância pré-fabricada - duplique o objeto Rivers e substitua seu material.Estradas de objeto filho.Depois disso, adicione ao HexGridChunk
campo geral HexMesh roads
e inclua-o Triangulate
. Conecte-o no inspetor ao objeto Estradas . public HexMesh terrain, rivers, roads; public void Triangulate () { terrain.Clear(); rivers.Clear(); roads.Clear(); for (int i = 0; i < cells.Length; i++) { Triangulate(cells[i]); } terrain.Apply(); rivers.Apply(); roads.Apply(); }
O objeto Estradas está conectado.Estradas entre celas
Vamos primeiro olhar para os segmentos da estrada entre as células. Como rios, as estradas são fechadas por dois quadriciclos médios. Cobrimos completamente esses quadrângulos de conexão com os quadrângulos da estrada para que as posições dos mesmos seis picos possam ser usadas. Adicione isso ao HexGridChunk
método TriangulateRoadSegment
. void TriangulateRoadSegment ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, Vector3 v5, Vector3 v6 ) { roads.AddQuad(v1, v2, v4, v5); roads.AddQuad(v2, v3, v5, v6); }
Como não precisamos mais nos preocupar com o fluxo de água, a coordenada V não é necessária; portanto, atribuímos o valor 0. em qualquer lugar. Podemos usar a coordenada U para indicar se estamos no meio da estrada ou ao lado. Seja igual a 1 no meio e igual a 0 em ambos os lados. void TriangulateRoadSegment ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, Vector3 v5, Vector3 v6 ) { roads.AddQuad(v1, v2, v4, v5); roads.AddQuad(v2, v3, v5, v6); roads.AddQuadUV(0f, 1f, 0f, 0f); roads.AddQuadUV(1f, 0f, 0f, 0f); }
Um segmento da estrada entre células.Seria lógico chamar esse método TriangulateEdgeStrip
, mas apenas se realmente houver uma estrada. Adicione um parâmetro booleano ao método para passar essas informações. void TriangulateEdgeStrip ( EdgeVertices e1, Color c1, EdgeVertices e2, Color c2, bool hasRoad ) { … }
Obviamente, agora receberemos erros do compilador, porque até agora essas informações ainda não foram transmitidas. Como o último argumento em todos os casos, a chamada TriangulateEdgeStrip
pode ser adicionada false
. No entanto, também podemos declarar que o valor padrão deste parâmetro é igual false
. Devido a isso, o parâmetro se tornará opcional e os erros de compilação desaparecerão. void TriangulateEdgeStrip ( EdgeVertices e1, Color c1, EdgeVertices e2, Color c2, bool hasRoad = false ) { … }
Como funcionam os parâmetros opcionais?, . ,
int MyMethod (int x = 1, int y = 2) { return x + y; }
int MyMethod (int x, int y) { return x + y; } int MyMethod (int x) { return MyMethod(x, 2); } int MyMethod () { return MyMethod(1, 2}; }
. . . .
Para triangular a estrada, basta ligar TriangulateRoadSegment
com os seis picos do meio, se necessário. void TriangulateEdgeStrip ( EdgeVertices e1, Color c1, EdgeVertices e2, Color c2, bool hasRoad = false ) { terrain.AddQuad(e1.v1, e1.v2, e2.v1, e2.v2); terrain.AddQuadColor(c1, c2); terrain.AddQuad(e1.v2, e1.v3, e2.v2, e2.v3); terrain.AddQuadColor(c1, c2); terrain.AddQuad(e1.v3, e1.v4, e2.v3, e2.v4); terrain.AddQuadColor(c1, c2); terrain.AddQuad(e1.v4, e1.v5, e2.v4, e2.v5); terrain.AddQuadColor(c1, c2); if (hasRoad) { TriangulateRoadSegment(e1.v2, e1.v3, e1.v4, e2.v2, e2.v3, e2.v4); } }
É assim que lidamos com conexões de célula plana. Para apoiar as estradas nas bordas, também precisamos dizer TriangulateEdgeTerraces
onde a estrada deve ser adicionada. Ele pode simplesmente transmitir essas informações TriangulateEdgeStrip
. void TriangulateEdgeTerraces ( EdgeVertices begin, HexCell beginCell, EdgeVertices end, HexCell endCell, bool hasRoad ) { EdgeVertices e2 = EdgeVertices.TerraceLerp(begin, end, 1); Color c2 = HexMetrics.TerraceLerp(beginCell.Color, endCell.Color, 1); TriangulateEdgeStrip(begin, beginCell.Color, e2, c2, hasRoad); for (int i = 2; i < HexMetrics.terraceSteps; i++) { EdgeVertices e1 = e2; Color c1 = c2; e2 = EdgeVertices.TerraceLerp(begin, end, i); c2 = HexMetrics.TerraceLerp(beginCell.Color, endCell.Color, i); TriangulateEdgeStrip(e1, c1, e2, c2, hasRoad); } TriangulateEdgeStrip(e2, c2, end, endCell.Color, hasRoad); }
TriangulateEdgeTerraces
chamado dentro TriangulateConnection
. É aqui que podemos determinar se há realmente uma estrada na direção atual, durante a triangulação da nervura e a triangulação das bordas. if (cell.GetEdgeType(direction) == HexEdgeType.Slope) { TriangulateEdgeTerraces( e1, cell, e2, neighbor, cell.HasRoadThroughEdge(direction) ); } else { TriangulateEdgeStrip( e1, cell.Color, e2, neighbor.Color, cell.HasRoadThroughEdge(direction) ); }
Segmentos de estrada entre células.Célula sobre renderização
Ao desenhar estradas, você verá que os segmentos de estrada aparecem entre as células. O meio desses segmentos será roxo com uma transição para azul nas bordas.No entanto, quando você move a câmera, os segmentos podem tremer e às vezes desaparecer completamente. Isso ocorre porque os triângulos das estradas se sobrepõem exatamente aos triângulos do terreno. Triângulos para renderização são selecionados aleatoriamente. Esse problema pode ser corrigido em dois estágios.Em primeiro lugar, queremos traçar as estradas depois que o alívio for traçado. Isso pode ser conseguido renderizando-os após renderizar a geometria usual, ou seja, colocando-os em uma fila de renderização posterior. Tags { "RenderType"="Opaque" "Queue" = "Geometry+1" }
Em segundo lugar, precisamos garantir que as estradas sejam traçadas sobre triângulos do terreno na mesma posição. Isso pode ser feito adicionando o deslocamento do teste de profundidade. Isso permitirá que a GPU assuma que os triângulos estão mais próximos da câmera do que realmente são. Tags { "RenderType"="Opaque" "Queue" = "Geometry+1" } LOD 200 Offset -1, -1
Estradas através das celas
Ao triangular rios, tivemos que lidar com não mais do que duas direções de rio por célula. Poderíamos identificar cinco opções possíveis e triangulá-las de maneira diferente para criar os rios com aparência correta. No entanto, no caso de estradas, existem catorze opções possíveis. Não usaremos abordagens separadas para cada uma dessas opções. Em vez disso, processaremos cada uma das seis direções da célula da mesma maneira, independentemente da configuração específica da estrada.Quando uma estrada passa ao longo de uma parte da célula, vamos traçá-la diretamente para o centro da célula, sem sair da zona de triângulos. Traçaremos um segmento da estrada da borda até a metade na direção do centro. Então usamos dois triângulos para fechar o resto no centro.Triangulação de uma parte da estrada.Para triangular esse esquema, precisamos conhecer o centro da célula, os vértices médios esquerdo e direito e os vértices da aresta. Adicione um método TriangulateRoad
com os parâmetros apropriados. void TriangulateRoad ( Vector3 center, Vector3 mL, Vector3 mR, EdgeVertices e ) { }
Para construir um segmento de estrada, precisamos de um pico adicional. Está localizado entre os picos do meio esquerdo e direito. void TriangulateRoad ( Vector3 center, Vector3 mL, Vector3 mR, EdgeVertices e ) { Vector3 mC = Vector3.Lerp(mL, mR, 0.5f); TriangulateRoadSegment(mL, mC, mR, e.v2, e.v3, e.v4); }
Agora também podemos adicionar os dois triângulos restantes. TriangulateRoadSegment(mL, mC, mR, e.v2, e.v3, e.v4); roads.AddTriangle(center, mL, mC); roads.AddTriangle(center, mC, mR);
Também precisamos adicionar as coordenadas UV dos triângulos. Dois de seus picos estão no meio da estrada, e o resto está à beira. roads.AddTriangle(center, mL, mC); roads.AddTriangle(center, mC, mR); roads.AddTriangleUV( new Vector2(1f, 0f), new Vector2(0f, 0f), new Vector2(1f, 0f) ); roads.AddTriangleUV( new Vector2(1f, 0f), new Vector2(1f, 0f), new Vector2(0f, 0f) );
Por enquanto, vamos nos limitar a células nas quais não há rios. Nesses casos, ele Triangulate
simplesmente cria um leque de triângulos. Mova esse código para um método separado. Em seguida, adicionamos uma chamada TriangulateRoad
quando a estrada é realmente. Os vértices médios esquerdo e direito podem ser encontrados por interpolação entre o centro e os dois vértices de canto. void Triangulate (HexDirection direction, HexCell cell) { … if (cell.HasRiver) { … } else { TriangulateWithoutRiver(direction, cell, center, e); } … } void TriangulateWithoutRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { TriangulateEdgeFan(center, e, cell.Color); if (cell.HasRoadThroughEdge(direction)) { TriangulateRoad( center, Vector3.Lerp(center, e.v1, 0.5f), Vector3.Lerp(center, e.v5, 0.5f), e ); } }
Estradas passando pelas celas.Costelas de estrada
Agora podemos ver as estradas, mas mais perto do centro das celas elas se estreitam. Como não verificamos qual das catorze opções estamos lidando, não podemos mudar o centro da estrada para criar formas mais bonitas. Em vez disso, podemos adicionar arestas adicionais em outras partes da célula.Quando as estradas passam pela célula, mas não na direção atual, adicionaremos um triângulo às bordas da estrada. Este triângulo é definido pelos vértices central, esquerdo e direito. Nesse caso, apenas o pico central fica no meio da estrada. Os outros dois estão na costela dela. void TriangulateRoadEdge (Vector3 center, Vector3 mL, Vector3 mR) { roads.AddTriangle(center, mL, mR); roads.AddTriangleUV( new Vector2(1f, 0f), new Vector2(0f, 0f), new Vector2(0f, 0f) ); }
Parte da beira da estrada.Quando precisamos triangular uma estrada cheia ou apenas uma margem, precisamos deixá-la como está TriangulateRoad
. Para fazer isso, esse método deve saber se a estrada passa pela direção da borda da célula atual. Portanto, adicionamos um parâmetro para isso. void TriangulateRoad ( Vector3 center, Vector3 mL, Vector3 mR, EdgeVertices e, bool hasRoadThroughCellEdge ) { if (hasRoadThroughCellEdge) { Vector3 mC = Vector3.Lerp(mL, mR, 0.5f); TriangulateRoadSegment(mL, mC, mR, e.v2, e.v3, e.v4); roads.AddTriangle(center, mL, mC); roads.AddTriangle(center, mC, mR); roads.AddTriangleUV( new Vector2(1f, 0f), new Vector2(0f, 0f), new Vector2(1f, 0f) ); roads.AddTriangleUV( new Vector2(1f, 0f), new Vector2(1f, 0f), new Vector2(0f, 0f) ); } else { TriangulateRoadEdge(center, mL, mR); } }
Agora TriangulateWithoutRiver
ele terá que ligar TriangulateRoad
quando alguma estrada passar pela célula. E ele terá que transmitir informações sobre se a estrada passa pela borda atual. void TriangulateWithoutRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { TriangulateEdgeFan(center, e, cell.Color); if (cell.HasRoads) { TriangulateRoad( center, Vector3.Lerp(center, e.v1, 0.5f), Vector3.Lerp(center, e.v5, 0.5f), e, cell.HasRoadThroughEdge(direction) ); } }
Estradas com nervuras concluídas.Suavização de estradas
As estradas estão agora completas. Infelizmente, essa abordagem cria protuberâncias no centro das células. Colocar os picos esquerdo e direito no meio, entre o centro e as esquinas, nos convém quando há uma estrada adjacente a eles. Mas se não for, então há uma protuberância. Para evitar isso, nesses casos, podemos colocar os vértices mais próximos do centro. Mais especificamente, interpolando com ¼, não com ½.Vamos criar um método separado para descobrir quais interpoladores usar. Como existem dois deles, podemos colocar o resultado Vector2
. Seu componente X será o interpolador do ponto esquerdo e o componente Y será o interpolador do ponto direito. Vector2 GetRoadInterpolators (HexDirection direction, HexCell cell) { Vector2 interpolators; return interpolators; }
Se houver uma estrada na direção atual, podemos colocar os pontos no meio. Vector2 GetRoadInterpolators (HexDirection direction, HexCell cell) { Vector2 interpolators; if (cell.HasRoadThroughEdge(direction)) { interpolators.x = interpolators.y = 0.5f; } return interpolators; }
Caso contrário, as opções podem ser diferentes. Para o ponto esquerdo, podemos usar ½ se houver uma estrada na direção anterior. Caso contrário, devemos usar ¼. O mesmo se aplica ao ponto certo, mas levando em consideração a seguinte direção. Vector2 GetRoadInterpolators (HexDirection direction, HexCell cell) { Vector2 interpolators; if (cell.HasRoadThroughEdge(direction)) { interpolators.x = interpolators.y = 0.5f; } else { interpolators.x = cell.HasRoadThroughEdge(direction.Previous()) ? 0.5f : 0.25f; interpolators.y = cell.HasRoadThroughEdge(direction.Next()) ? 0.5f : 0.25f; } return interpolators; }
Agora você pode usar esse novo método para determinar quais interpoladores são usados. Graças a isso, as estradas serão suavizadas. void TriangulateWithoutRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { TriangulateEdgeFan(center, e, cell.Color); if (cell.HasRoads) { Vector2 interpolators = GetRoadInterpolators(direction, cell); TriangulateRoad( center, Vector3.Lerp(center, e.v1, interpolators.x), Vector3.Lerp(center, e.v5, interpolators.y), e, cell.HasRoadThroughEdge(direction) ); } }
Estradas lisas.unitypackageA combinação de rios e estradas
No estágio atual, temos estradas funcionais, mas apenas se não houver rios. Se houver um rio na cela, as estradas não serão trianguladas.Não há estradas perto dos rios.Vamos criar um método TriangulateRoadAdjacentToRiver
para lidar com essa situação. Definimos os parâmetros usuais. Vamos chamá-lo no início do método TriangulateAdjacentToRiver
. void TriangulateAdjacentToRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { if (cell.HasRoads) { TriangulateRoadAdjacentToRiver(direction, cell, center, e); } … } void TriangulateRoadAdjacentToRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { }
Para começar, faremos o mesmo que em estradas sem rios. Vamos verificar se a estrada passa pela margem atual, obter interpoladores, criar picos médios e ligar TriangulateRoad
. Mas como os rios aparecerão no caminho, precisamos afastar as estradas deles. Como resultado, o centro da estrada estará em uma posição diferente. Usamos uma variável para armazenar esta nova posição roadCenter
. Inicialmente, será igual ao centro da célula. void TriangulateRoadAdjacentToRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { bool hasRoadThroughEdge = cell.HasRoadThroughEdge(direction); Vector2 interpolators = GetRoadInterpolators(direction, cell); Vector3 roadCenter = center; Vector3 mL = Vector3.Lerp(roadCenter, e.v1, interpolators.x); Vector3 mR = Vector3.Lerp(roadCenter, e.v5, interpolators.y); TriangulateRoad(roadCenter, mL, mR, e, hasRoadThroughEdge); }
Então, criaremos estradas parciais em celas com rios. As direções pelas quais os rios passam cortam as brechas nas estradas.Estradas com espaços.Início ou fim do rio
Vamos primeiro olhar para as células que contêm o início ou o fim de um rio. Para que as estradas não se sobreponham à água, vamos mover o centro da estrada do rio. Para obter a direção do rio de entrada ou saída, adicione a HexCell
propriedade public HexDirection RiverBeginOrEndDirection { get { return hasIncomingRiver ? incomingRiver : outgoingRiver; } }
Agora podemos usar essa propriedade HexGridChunk.TriangulateRoadAdjacentToRiver
para mover o centro da estrada na direção oposta. Basta mover um terço para a costela do meio nessa direção. bool hasRoadThroughEdge = cell.HasRoadThroughEdge(direction); Vector2 interpolators = GetRoadInterpolators(direction, cell); Vector3 roadCenter = center; if (cell.HasRiverBeginOrEnd) { roadCenter += HexMetrics.GetSolidEdgeMiddle( cell.RiverBeginOrEndDirection.Opposite() ) * (1f / 3f); } Vector3 mL = Vector3.Lerp(roadCenter, e.v1, interpolators.x); Vector3 mR = Vector3.Lerp(roadCenter, e.v5, interpolators.y); TriangulateRoad(roadCenter, mL, mR, e, hasRoadThroughEdge);
Estradas modificadas.Em seguida, precisamos fechar as lacunas. Faremos isso adicionando triângulos adicionais às margens da estrada quando estivermos perto do rio. Se houver um rio na direção anterior, adicionamos um triângulo entre o centro da estrada, o centro da célula e o ponto médio esquerdo. E se o rio estiver na próxima direção, adicionamos um triângulo entre o centro da estrada, o ponto médio direito e o centro da célula.Vamos fazer isso independentemente da configuração do rio, portanto, coloque esse código no final do método. Vector3 mL = Vector3.Lerp(roadCenter, e.v1, interpolators.x); Vector3 mR = Vector3.Lerp(roadCenter, e.v5, interpolators.y); TriangulateRoad(roadCenter, mL, mR, e, hasRoadThroughEdge); if (cell.HasRiverThroughEdge(direction.Previous())) { TriangulateRoadEdge(roadCenter, center, mL); } if (cell.HasRiverThroughEdge(direction.Next())) { TriangulateRoadEdge(roadCenter, mR, center); }
Você não pode usar a instrução else?. , .
Estradas prontas.Rios retos
Células com rios retos são particularmente difíceis porque basicamente dividem o centro da célula em duas. Já adicionamos triângulos extras para preencher as lacunas entre os rios, mas também precisamos desconectar as estradas em lados opostos do rio.Estradas sobrepostas a um rio reto.Se a célula não tiver o início ou o fim do rio, podemos verificar se os rios que entram e saem seguem direções opostas. Se sim, então temos um rio direto. if (cell.HasRiverBeginOrEnd) { roadCenter += HexMetrics.GetSolidEdgeMiddle( cell.RiverBeginOrEndDirection.Opposite() ) * (1f / 3f); } else if (cell.IncomingRiver == cell.OutgoingRiver.Opposite()) { }
Para determinar onde o rio é relativo à direção atual, precisamos verificar as direções vizinhas. O rio é esquerdo ou direito. Como fazemos isso no final do método, armazenamos essas solicitações em cache em variáveis booleanas. Isso também simplificará a leitura do nosso código. bool hasRoadThroughEdge = cell.HasRoadThroughEdge(direction); bool previousHasRiver = cell.HasRiverThroughEdge(direction.Previous()); bool nextHasRiver = cell.HasRiverThroughEdge(direction.Next()); Vector2 interpolators = GetRoadInterpolators(direction, cell); Vector3 roadCenter = center; if (cell.HasRiverBeginOrEnd) { roadCenter += HexMetrics.GetSolidEdgeMiddle( cell.RiverBeginOrEndDirection.Opposite() ) * (1f / 3f); } else if (cell.IncomingRiver == cell.OutgoingRiver.Opposite()) { if (previousHasRiver) { } else { } } Vector3 mL = Vector3.Lerp(roadCenter, e.v1, interpolators.x); Vector3 mR = Vector3.Lerp(roadCenter, e.v5, interpolators.y); TriangulateRoad(roadCenter, mL, mR, e, hasRoadThroughEdge); if (previousHasRiver) { TriangulateRoadEdge(roadCenter, center, mL); } if (nextHasRiver) { TriangulateRoadEdge(roadCenter, mR, center); }
Precisamos mudar o centro da estrada para um vetor angular apontando na direção oposta ao rio. Se o rio passa pela direção anterior, esse é o segundo ângulo sólido. Caso contrário, este é o primeiro ângulo sólido. else if (cell.IncomingRiver == cell.OutgoingRiver.Opposite()) { Vector3 corner; if (previousHasRiver) { corner = HexMetrics.GetSecondSolidCorner(direction); } else { corner = HexMetrics.GetFirstSolidCorner(direction); } }
Para mover a estrada de forma que fique adjacente ao rio, precisamos mover o centro da estrada pela metade da distância até esse canto. Então também temos que mover o centro da célula um quarto da distância nessa direção. else if (cell.IncomingRiver == cell.OutgoingRiver.Opposite()) { Vector3 corner; if (previousHasRiver) { corner = HexMetrics.GetSecondSolidCorner(direction); } else { corner = HexMetrics.GetFirstSolidCorner(direction); } roadCenter += corner * 0.5f; center += corner * 0.25f; }
Estradas divididas.Nós compartilhamos uma rede de estradas dentro desta célula. Isso é normal quando as estradas estão nos dois lados do rio. Mas se de um lado não houver estrada, teremos um pequeno pedaço de estrada isolada. Isso é ilógico, então vamos nos livrar dessas partes.Verifique se há uma estrada na direção atual. Caso contrário, verifique a outra direção do mesmo lado do rio quanto à presença da estrada. Se não houver uma estrada que passe por lá ou por lá, sairemos do método antes de triangular. if (previousHasRiver) { if ( !hasRoadThroughEdge && !cell.HasRoadThroughEdge(direction.Next()) ) { return; } corner = HexMetrics.GetSecondSolidCorner(direction); } else { if ( !hasRoadThroughEdge && !cell.HasRoadThroughEdge(direction.Previous()) ) { return; } corner = HexMetrics.GetFirstSolidCorner(direction); }
Estradas truncadas.Rios em zigue-zague
O próximo tipo de rio é em zigue-zague. Como esses rios não compartilham a rede de estradas, precisamos apenas mover o centro da estrada.Ziguezagues passando pelas estradas.A maneira mais fácil de verificar se há ziguezagues é comparando as direções dos rios que entram e saem. Se eles são adjacentes, então temos um zigue-zague. Isso leva a duas opções possíveis, dependendo da direção do fluxo. if (cell.HasRiverBeginOrEnd) { … } else if (cell.IncomingRiver == cell.OutgoingRiver.Opposite()) { … } else if (cell.IncomingRiver == cell.OutgoingRiver.Previous()) { } else if (cell.IncomingRiver == cell.OutgoingRiver.Next()) { }
Podemos mover o centro da estrada usando um dos cantos da direção do rio que chega. O ângulo que você seleciona depende da direção do fluxo. Mova o centro da estrada desse ângulo com um fator de 0,2. else if (cell.IncomingRiver == cell.OutgoingRiver.Previous()) { roadCenter -= HexMetrics.GetSecondCorner(cell.IncomingRiver) * 0.2f; } else if (cell.IncomingRiver == cell.OutgoingRiver.Next()) { roadCenter -= HexMetrics.GetFirstCorner(cell.IncomingRiver) * 0.2f; }
A estrada se afastou dos ziguezagues.Dentro dos rios tortos
A última configuração do rio é uma curva suave. Como no rio direto, este também pode separar estradas. Mas, neste caso, as partes serão diferentes. Primeiro, precisamos trabalhar com o interior da curva.Um rio curvo com estradas pavimentadas.Quando temos um rio nos dois lados da direção atual, estamos dentro da curva. else if (cell.IncomingRiver == cell.OutgoingRiver.Next()) { … } else if (previousHasRiver && nextHasRiver) { }
Precisamos mover o centro da estrada em direção à borda atual da célula, encurtando um pouco a estrada. Um coeficiente de 0,7 serve. O centro da célula também deve mudar com um coeficiente de 0,5. else if (previousHasRiver && nextHasRiver) { Vector3 offset = HexMetrics.GetSolidEdgeMiddle(direction) * HexMetrics.innerToOuter; roadCenter += offset * 0.7f; center += offset * 0.5f; }
Estradas encurtadas.Como no caso de rios retos, precisaremos cortar as partes isoladas das estradas. Nesse caso, basta verificar apenas a direção atual. else if (previousHasRiver && nextHasRiver) { if (!hasRoadThroughEdge) { return; } Vector3 offset = HexMetrics.GetSolidEdgeMiddle(direction) * HexMetrics.innerToOuter; roadCenter += offset * 0.7f; center += offset * 0.5f; }
Cortar estradas.Fora dos rios tortos
Depois de verificar todos os casos anteriores, a única opção restante era a parte externa do rio curvo. Lá fora, existem três partes da célula. Precisamos encontrar a direção do meio. Após recebê-lo, podemos mover o centro da estrada em direção a esta costela por um fator de 0,25. else if (previousHasRiver && nextHasRiver) { … } else { HexDirection middle; if (previousHasRiver) { middle = direction.Next(); } else if (nextHasRiver) { middle = direction.Previous(); } else { middle = direction; } roadCenter += HexMetrics.GetSolidEdgeMiddle(middle) * 0.25f; }
Mudou o lado de fora da estrada.Como último passo, precisamos truncar as estradas deste lado do rio. A maneira mais fácil é verificar as três direções da estrada em relação ao meio. Se não houver estradas, paramos de trabalhar. else { HexDirection middle; if (previousHasRiver) { middle = direction.Next(); } else if (nextHasRiver) { middle = direction.Previous(); } else { middle = direction; } if ( !cell.HasRoadThroughEdge(middle) && !cell.HasRoadThroughEdge(middle.Previous()) && !cell.HasRoadThroughEdge(middle.Next()) ) { return; } roadCenter += HexMetrics.GetSolidEdgeMiddle(middle) * 0.25f; }
Estradas antes e depois do recorte.Depois de processar todas as opções de rios, nossos rios e estradas podem coexistir. Os rios ignoram as estradas e as estradas se adaptam aos rios.A combinação de rios e estradas.unitypackageA aparência das estradas
Até aquele momento, usamos as coordenadas UV como cores da estrada. Como apenas a coordenada U mudou, exibimos a transição entre o meio e a beira da estrada.Exibição de coordenadas UV.Agora que as estradas estão exatamente trianguladas corretamente, podemos alterar o sombreador para tornar algo mais parecido com estradas. Como no caso dos rios, será uma visualização simples, sem frescuras.Começaremos usando cores sólidas para estradas. Basta usar a cor do material. Eu fiz vermelho. void surf (Input IN, inout SurfaceOutputStandard o) { fixed4 c = _Color; o.Albedo = c.rgb; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; }
Estradas vermelhas.E já parece muito melhor! Mas vamos continuar e misturar a estrada com o terreno, usando a coordenada U como fator de mistura. void surf (Input IN, inout SurfaceOutputStandard o) { fixed4 c = _Color; float blend = IN.uv_MainTex.x; o.Albedo = c.rgb; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = blend; }
Parece que isso não mudou nada. Isso aconteceu porque o nosso shader é opaco. Agora ele precisa de mistura alfa. Em particular, precisamos de um shader para uma superfície de decalque correspondente. Podemos obter o sombreador necessário adicionando uma #pragma surface
linha à diretiva decal:blend
. #pragma surface surf Standard fullforwardshadows decal:blend
A mistura de estradas.Por isso, criamos uma mistura linear suave do meio ao extremo que não parece muito bonita. Para fazer com que pareça uma estrada, precisamos de uma área sólida, seguida de uma rápida transição para uma área opaca. Você pode usar a função para isso smoothstep
. Ele converte uma progressão linear de 0 para 1 em uma curva em forma de S.Progressão linear e passo suave.A função smoothstep
possui um parâmetro mínimo e máximo para ajustar a curva em um intervalo arbitrário. Os valores de entrada fora do intervalo são limitados para manter a curva plana. Vamos usar 0,4 no início da curva e 0,7 no final. Isso significa que a coordenada U de 0 a 0,4 será completamente transparente. E as coordenadas U de 0,7 a 1 serão completamente opacas. A transição ocorre entre 0,4 e 0,7. float blend = IN.uv_MainTex.x; blend = smoothstep(0.4, 0.7, blend);
Transição rápida entre áreas opacas e transparentes.Estrada com barulho
Como a malha da estrada será distorcida, as estradas têm larguras variadas. Portanto, a largura da transição nas bordas também será variável. Às vezes é embaçada, às vezes dura. Essa variabilidade nos convém, se percebermos as estradas como arenosas ou terrosas.Vamos dar o próximo passo e adicionar ruído às margens da estrada. Isso os tornará mais irregulares e menos poligonais. Podemos fazer isso amostrando a textura do ruído. Para amostragem, você pode usar as coordenadas do mundo XZ, exatamente como fizemos ao distorcer os vértices das células.Para obter acesso à posição do mundo no shader de superfície, adicione à estrutura de entrada float3 worldPos
. struct Input { float2 uv_MainTex; float3 worldPos; };
Agora podemos usar essa posição surf
para provar a textura principal. Diminua o zoom também, caso contrário a textura será repetida com muita frequência. float4 noise = tex2D(_MainTex, IN.worldPos.xz * 0.025); fixed4 c = _Color; float blend = IN.uv_MainTex.x;
Distorcemos a transição multiplicando a coordenada U por noise.x
. Mas como os valores de ruído são em média 0,5, a maioria das estradas desaparece. Para evitar isso, adicione 0,5 ao ruído antes da multiplicação. float blend = IN.uv_MainTex.x; blend *= noise.x + 0.5; blend = smoothstep(0.4, 0.7, blend);
Bordas distorcidas da estrada.Para finalizar, também distorceremos a cor das estradas. Isso dará às estradas uma sensação de sujeira correspondente a bordas difusas.Multiplique a cor por outro canal de ruído, diga por noise.y
. Portanto, obtemos uma média da metade do valor da cor. Como isso é demais, reduziremos um pouco a escala de ruído e adicionaremos uma constante para que a soma chegue a 1. fixed4 c = _Color * (noise.y * 0.75 + 0.25);
Estradas irregulares.unitypackage