Karten aus Sechsecken in der Einheit: Wasser, Reliefobjekte und Festungsmauern

Published on October 12, 2018

Karten aus Sechsecken in der Einheit: Wasser, Reliefobjekte und Festungsmauern

Ursprünglicher Autor: Jasper Flick
  • Übersetzung
Teile 1-3: Maschen, Farben und Höhen der Zellen

Teile 4-7: Unebenheiten, Flüsse und Straßen

Teile 8-11: Wasser, Reliefgegenstände und Mauern der

Teile 12-15: Konservieren und Laden, Texturen, Entfernungen

Teile 16-19: Suche nach dem Pfad, Spielertrupps, Animationen

Teile 20-23: Nebel des Krieges, Kartenerforschung, Verfahrensgenerierung

Teile 24-27: Wasserkreislauf, Erosion, Biomes, zylindrische Karte

Teil 8: Wasser


  • Füge Wasser zu den Zellen hinzu.
  • Triangulieren Sie die Wasseroberfläche.
  • Erstellen Sie eine Brandung mit Schaum.
  • Kombinieren Sie Wasser und Flüsse.

Wir haben bereits Unterstützung für die Flüsse hinzugefügt, und in diesem Teil werden wir die Zellen vollständig in Wasser tauchen.


Wasser kommt.

Wasserstand


Die einfachste Möglichkeit, die Wasserunterstützung zu implementieren, besteht darin, sie auf das gleiche Niveau einzustellen. Alle Zellen unterhalb dieses Niveaus werden in Wasser getaucht. Eine flexiblere Möglichkeit besteht darin, Wasser in verschiedenen Höhen abzustützen. Lassen Sie uns also den Wasserstand veränderbar machen. Dazu müssen Sie HexCellIhren Wasserstand verfolgen.

	public int WaterLevel {
		get {
			return waterLevel;
		}
		set {
			if (waterLevel == value) {
				return;
			}
			waterLevel = value;
			Refresh();
		}
	}
	int waterLevel;

Auf Wunsch können Sie sicherstellen, dass bestimmte Merkmale des Reliefs unter Wasser nicht vorhanden sind. Aber im Moment werde ich das nicht tun. Dinge wie Unterwasserstraßen passen zu mir. Sie können als Gebiete betrachtet werden, die kürzlich überflutet wurden.

Überflutung von Zellen


Jetzt, da wir Wasserstände haben, ist die wichtigste Frage, ob sich die Zellen unter Wasser befinden. Die Zelle befindet sich unter Wasser, wenn ihr Wasserstand höher als ihre Höhe ist. Um diese Informationen zu erhalten, fügen wir eine Eigenschaft hinzu.

	public bool IsUnderwater {
		get {
			return waterLevel > elevation;
		}
	}

Dies bedeutet, dass sich die Zelle über dem Wasser erhebt, wenn der Wasserstand und die Höhe gleich sind. Das heißt, die reale Wasseroberfläche befindet sich unterhalb dieser Höhe. Wie bei Flussoberflächen addieren wir den gleichen Versatz - HexMetrics.riverSurfaceElevationOffset. Ändern Sie den Namen in allgemeiner.

//	public const float riverSurfaceElevationOffset = -0.5f;
	public const float waterElevationOffset = -0.5f;

Ändern Sie es HexCell.RiverSurfaceYso, dass es den neuen Namen verwendet. Fügen Sie dann der Wasseroberfläche der überfluteten Zelle eine ähnliche Eigenschaft hinzu.

	public float RiverSurfaceY {
		get {
			return
				(elevation + HexMetrics.waterElevationOffset) *
				HexMetrics.elevationStep;
		}
	}
	public float WaterSurfaceY {
		get {
			return
				(waterLevel + HexMetrics.waterElevationOffset) *
				HexMetrics.elevationStep;
		}
	}

Wasseraufbereitung


Das Ändern des Wasserstands ähnelt dem Ändern der Höhe. Daher HexMapEditormuss der aktive Wasserstand überwacht werden und ob er auf Zellen angewendet werden soll.

	int activeElevation;
	int activeWaterLevel;
	…
	bool applyElevation = true;
	bool applyWaterLevel = true;
	

Fügen Sie Methoden hinzu, um diese Parameter mit der Benutzeroberfläche zu verbinden.

	public void SetApplyWaterLevel (bool toggle) {
		applyWaterLevel = toggle;
	}
	public void SetWaterLevel (float level) {
		activeWaterLevel = (int)level;
	}

Und fügen Sie den Wasserstand in EditCell.

	void EditCell (HexCell cell) {
		if (cell) {
			if (applyColor) {
				cell.Color = activeColor;
			}
			if (applyElevation) {
				cell.Elevation = activeElevation;
			}
			if (applyWaterLevel) {
				cell.WaterLevel = activeWaterLevel;
			}
			…
		}
	}

Um der Benutzeroberfläche einen Wasserstand hinzuzufügen, duplizieren Sie die Beschriftung und den Höhenregler und ändern Sie sie dann. Vergessen Sie nicht, ihre Ereignisse an die entsprechenden Methoden anzuhängen.


Wasserstandsregler.

Einheitspaket

Wassertriangulation


Um Wasser zu triangulieren, brauchen wir ein neues Netz mit neuem Material. Erstellen Sie zuerst den Water- Shader, indem Sie den River- Shader duplizieren . Ändern Sie es, um die Farbeigenschaft zu verwenden.

Shader "Custom/Water" {
	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"="Transparent" "Queue"="Transparent" }
		LOD 200
		CGPROGRAM
		#pragma surface surf Standard alpha
		#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 = _Color;
			o.Albedo = c.rgb;
			o.Metallic = _Metallic;
			o.Smoothness = _Glossiness;
			o.Alpha = c.a;
		}
		ENDCG
	}
	FallBack "Diffuse"
}

Erstellen Sie mit diesem Shader ein neues Material, indem Sie das Material Wasser duplizieren und durch einen Shader ersetzen. Lassen wir die Noise-Textur, weil wir sie später verwenden.


Materielles Wasser.

Fügen Sie dem Fertighaus ein neues untergeordnetes Element hinzu, indem Sie das untergeordnete Element " Rivers" duplizieren . Es werden keine UV-Koordinaten benötigt und es muss Wasser verwendet werden . Erstellen Sie dazu wie gewohnt eine Instanz des Fertighauses, ändern Sie sie und wenden Sie die Änderungen dann auf das Fertighaus an. Beseitigen Sie danach die Instanz.



Kind Wasserobjekt.

Als nächstes geben Sie das HexGridChunkWasser in das Stützgitter.

	public HexMesh terrain, rivers, roads, water;
	public void Triangulate () {
		terrain.Clear();
		rivers.Clear();
		roads.Clear();
		water.Clear();
		for (int i = 0; i < cells.Length; i++) {
			Triangulate(cells[i]);
		}
		terrain.Apply();
		rivers.Apply();
		roads.Apply();
		water.Apply();
	}

Und verbinden Sie es mit dem untergeordneten Objekt des Fertighauses.


Wasserobjekt ist angeschlossen.

Wasser Sechsecke


Da Wasser die zweite Schicht bildet, geben wir für jede Richtung eine eigene Triangulationsmethode an. Wir brauchen es nur zu nennen, wenn die Zelle in Wasser eingetaucht ist.

	void Triangulate (HexDirection direction, HexCell cell) {
		…
		if (cell.IsUnderwater) {
			TriangulateWater(direction, cell, center);
		}
	}
	void TriangulateWater (
		HexDirection direction, HexCell cell, Vector3 center
	) {
	}

Wie bei Flüssen variiert die Höhe der Wasseroberfläche in Zellen mit gleichem Wasserstand nicht sehr. Daher scheinen wir keine komplexen Kanten zu benötigen. Genug wird ein einfaches Dreieck sein.

	void TriangulateWater (
		HexDirection direction, HexCell cell, Vector3 center
	) {
		center.y = cell.WaterSurfaceY;
		Vector3 c1 = center + HexMetrics.GetFirstSolidCorner(direction);
		Vector3 c2 = center + HexMetrics.GetSecondSolidCorner(direction);
		water.AddTriangle(center, c1, c2);
	}


Sechsecke Wasser.

Wasseranschlüsse


Wir können benachbarte Zellen mit einem Viereck mit Wasser verbinden.

		water.AddTriangle(center, c1, c2);
		if (direction <= HexDirection.SE) {
			HexCell neighbor = cell.GetNeighbor(direction);
			if (neighbor == null || !neighbor.IsUnderwater) {
				return;
			}
			Vector3 bridge = HexMetrics.GetBridge(direction);
			Vector3 e1 = c1 + bridge;
			Vector3 e2 = c2 + bridge;
			water.AddQuad(c1, c2, e1, e2);
		}


Die Ränder des Wassers verbinden.

Und füllen Sie die Ecken mit einem Dreieck.

		if (direction <= HexDirection.SE) {
			…
			water.AddQuad(c1, c2, e1, e2);
			if (direction <= HexDirection.E) {
				HexCell nextNeighbor = cell.GetNeighbor(direction.Next());
				if (nextNeighbor == null || !nextNeighbor.IsUnderwater) {
					return;
				}
				water.AddTriangle(
					c2, e2, c2 + HexMetrics.GetBridge(direction.Next())
				);
			}
		}


Anschlusswinkel von Wasser.

Jetzt haben wir Wasserzellen angeschlossen, wenn sie in der Nähe sind. Sie lassen eine Lücke zwischen sich und trockenen Zellen mit größerer Höhe, aber das werden wir für später belassen.

Abgestimmte Wasserstände


Wir gingen davon aus, dass die benachbarten Unterseebootzellen den gleichen Wasserstand haben. Wenn ja, dann sieht alles gut aus, aber wenn diese Annahme verletzt wird, dann treten Fehler auf.


Inkonsistente Wasserstände.

Wir können dafür sorgen, dass das Wasser auf dem gleichen Niveau bleibt. Wenn sich beispielsweise der Wasserstand einer überfluteten Zelle ändert, können wir die Änderungen auf benachbarte Zellen verteilen, um die Pegel synchron zu halten. Dieser Prozess muss jedoch fortgesetzt werden, bis er auf Zellen trifft, die nicht in Wasser eingetaucht sind. Diese Zellen legen die Grenzen des Wasserkörpers fest.

Die Gefahr dieses Ansatzes besteht darin, dass er schnell außer Kontrolle geraten kann. Wenn die Bearbeitung nicht erfolgreich ist, kann Wasser die gesamte Karte bedecken. Dann müssen alle Fragmente gleichzeitig trianguliert werden, was zu einem enormen Verspätungssprung führt.

Also lass es uns noch nicht tun. Diese Funktion kann in einem komplexeren Editor hinzugefügt werden. Solange die Konsistenz des Wasserstandes besteht, verlassen wir das Gewissen des Benutzers.

Einheitspaket

Wasser beleben


Anstelle einer einheitlichen Farbe werden wir etwas schaffen, das einer Welle ähnelt. Wie in anderen Shadern werden wir uns immer noch nicht um schöne Grafiken bemühen, wir müssen nur die Wellen bezeichnen.


Perfekt flaches Wasser.

Machen wir dasselbe wie mit den Flüssen. Lassen Sie uns das Rauschen mit der Position der Welt abtasten und der einheitlichen Farbe hinzufügen. Fügen Sie der V-Koordinate Zeit hinzu, um die Oberfläche zu animieren.

		struct Input {
			float2 uv_MainTex;
			float3 worldPos;
		};
		…
		void surf (Input IN, inout SurfaceOutputStandard o) {
			float2 uv = IN.worldPos.xz;
			uv.y += _Time.y;
			float4 noise = tex2D(_MainTex, uv * 0.025);
			float waves = noise.z;
			fixed4 c = saturate(_Color + waves);
			o.Albedo = c.rgb;
			o.Metallic = _Metallic;
			o.Smoothness = _Glossiness;
			o.Alpha = c.a;
		}


Wasserlaufzeit × 10.

Zwei Richtungen


Bisher ist dies überhaupt nicht wie Wellen. Lassen Sie uns das Bild komplizieren, indem Sie ein zweites Rauschmuster
und diesmal die U-Koordinate hinzufügen. Verwenden Sie einen anderen Rauschkanal, um zwei verschiedene Muster als Ergebnis zu erhalten. Die fertigen Wellen werden aus diesen beiden Proben zusammengesetzt.

			float2 uv1 = IN.worldPos.xz;
			uv1.y += _Time.y;
			float4 noise1 = tex2D(_MainTex, uv1 * 0.025);
			float2 uv2 = IN.worldPos.xz;
			uv2.x += _Time.y;
			float4 noise2 = tex2D(_MainTex, uv2 * 0.025);
			float waves = noise1.z + noise2.x;

Wenn wir beide Samples summieren, erhalten wir Ergebnisse im Bereich von 0 bis 2, daher müssen wir sie auf 0 bis 1 zurückskalieren. Anstatt die Wellen einfach in zwei Hälften zu teilen, können wir die Funktion verwenden smoothstep, um ein interessanteres Ergebnis zu erzielen. Wir werden ¾ - 2 auf 0–1 setzen, damit es keine sichtbaren Wellen auf der Wasseroberfläche gibt.

			float waves = noise1.z + noise2.x;
			waves = smoothstep(0.75, 2, waves);


Zwei Richtungen, Zeit × 10.

Wellen des Mischens


Es ist immer noch auffällig, dass wir zwei sich bewegende Geräuschmuster haben, die sich tatsächlich nicht ändern. Es wäre plausibel, wenn die Muster geändert würden. Dies können wir erreichen, indem wir zwischen verschiedenen Kanälen von Rauschabtastwerten interpolieren. Dies kann jedoch nicht auf die gleiche Weise geschehen, da sich sonst die gesamte Wasseroberfläche gleichzeitig ändert, und dies ist sehr auffällig. Stattdessen werden wir eine Welle des Mischens erzeugen.

Wir werden eine Mischwelle mit einer Sinuswelle erzeugen, die sich diagonal über die Wasseroberfläche bewegt. Dazu addieren wir die Koordinaten der Welt X und Z und verwenden die Summe als Eingabedaten für die Funktion sin. Verkleinern, um ausreichend große Bänder zu erhalten. Und natürlich den gleichen Wert hinzufügen, um sie zu animieren.

			float blendWave =
				sin((IN.worldPos.x + IN.worldPos.z) * 0.1 + _Time.y);

Die Sinuskurven liegen zwischen -1 und 1, und wir benötigen ein Intervall von 0-1. Sie können es erhalten, indem Sie die Welle quadrieren. Um das isolierte Ergebnis anzuzeigen, verwenden Sie es anstelle der geänderten Farbe als Ausgabewert.

			sin((IN.worldPos.x + IN.worldPos.z) * 0.1 + _Time.y);
			blendWave *= blendWave;
			float waves = noise1.z + noise2.x;
			waves = smoothstep(0.75, 2, waves);
			fixed4 c = blendWave; //saturate(_Color + waves);


Wellen der Verwirrung.

Fügen Sie beiden Samples etwas Rauschen hinzu, damit Mischwellen weniger auffallen.

			float blendWave = sin(
				(IN.worldPos.x + IN.worldPos.z) * 0.1 +
				(noise1.y + noise2.z) + _Time.y
			);
			blendWave *= blendWave;


Verzerrte Mischwellen.

Verwenden Sie zum Schluss eine Mischwelle, um zwischen den beiden Kanälen beider Rauschproben zu interpolieren. Nehmen Sie für maximale Variabilität vier verschiedene Kanäle.

			float waves =
				lerp(noise1.z, noise1.w, blendWave) +
				lerp(noise2.x, noise2.y, blendWave);
			waves = smoothstep(0.75, 2, waves);
			fixed4 c = saturate(_Color + waves);


Wellenmischung, Zeit × 2.

Einheitspaket

Küstenlinie


Am Ende hatten wir offenes Wasser, aber jetzt müssen wir die Lücke im Wasser entlang der Küste füllen. Da wir die Landkonturen einhalten müssen, erfordert das Wasser der Küste einen anderen Ansatz. Lassen Sie uns TriangulateWaterin zwei Methoden einteilen - eine für offenes Wasser, die zweite für die Küste. Um zu verstehen, wann wir mit der Küste arbeiten, müssen wir uns die nächste Zelle ansehen. Das heißt, in werden TriangulateWaterwir einen Nachbarn empfangen. Wenn es einen Nachbarn gibt und er nicht unter Wasser ist, dann haben wir es mit der Küste zu tun.

	void TriangulateWater (
		HexDirection direction, HexCell cell, Vector3 center
	) {
		center.y = cell.WaterSurfaceY;
		HexCell neighbor = cell.GetNeighbor(direction);
		if (neighbor != null && !neighbor.IsUnderwater) {
			TriangulateWaterShore(direction, cell, neighbor, center);
		}
		else {
			TriangulateOpenWater(direction, cell, neighbor, center);
		}
	}
	void TriangulateOpenWater (
		HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center
	) {
		Vector3 c1 = center + HexMetrics.GetFirstSolidCorner(direction);
		Vector3 c2 = center + HexMetrics.GetSecondSolidCorner(direction);
		water.AddTriangle(center, c1, c2);
		if (direction <= HexDirection.SE && neighbor != null) {
//			HexCell neighbor = cell.GetNeighbor(direction);
//			if (neighbor == null || !neighbor.IsUnderwater) {
//				return;
//			}
			Vector3 bridge = HexMetrics.GetBridge(direction);
			…
		}
	}
	void TriangulateWaterShore (
		HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center
	) {
	}


Es gibt keine Triangulation entlang der Küste.

Da die Küste verzerrt ist, müssen wir die Dreiecke des Wassers entlang der Küste verzerren. Deshalb brauchen wir Eckpunkte von Kanten und einen Fächer von Dreiecken.

	void TriangulateWaterShore (
		HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center
	) {
		EdgeVertices e1 = new EdgeVertices(
			center + HexMetrics.GetFirstSolidCorner(direction),
			center + HexMetrics.GetSecondSolidCorner(direction)
		);
		water.AddTriangle(center, e1.v1, e1.v2);
		water.AddTriangle(center, e1.v2, e1.v3);
		water.AddTriangle(center, e1.v3, e1.v4);
		water.AddTriangle(center, e1.v4, e1.v5);
	}


Fächerdreiecke entlang der Küste.

Als nächstes kommt ein Streifen Rippen, wie im üblichen Relief. Wir sind jedoch nicht verpflichtet, uns nur auf bestimmte Richtungen zu beschränken, da wir TriangulateWaterShoreerst anrufen, wenn wir uns mit der Küste treffen, für die der Streifen immer benötigt wird.

		water.AddTriangle(center, e1.v4, e1.v5);
		Vector3 bridge = HexMetrics.GetBridge(direction);
		EdgeVertices e2 = new EdgeVertices(
			e1.v1 + bridge,
			e1.v5 + bridge
		);
		water.AddQuad(e1.v1, e1.v2, e2.v1, e2.v2);
		water.AddQuad(e1.v2, e1.v3, e2.v2, e2.v3);
		water.AddQuad(e1.v3, e1.v4, e2.v3, e2.v4);
		water.AddQuad(e1.v4, e1.v5, e2.v4, e2.v5);


Streifen von Rippen entlang der Küste.

Ebenso müssen wir jedes Mal ein eckiges Dreieck hinzufügen.

		water.AddQuad(e1.v4, e1.v5, e2.v4, e2.v5);
		HexCell nextNeighbor = cell.GetNeighbor(direction.Next());
		if (nextNeighbor != null) {
			water.AddTriangle(
				e1.v5, e2.v5, e1.v5 + HexMetrics.GetBridge(direction.Next())
			);
		}


Die Winkel der Rippen entlang der Küste.

Jetzt haben wir bereit Wasserküste. Ein Teil davon befindet sich immer unter dem Reliefnetz, daher gibt es keine Löcher.

UV-Küste


Wir können alles so lassen, wie es ist, aber es wäre interessant, wenn das Küstenwasser einen eigenen Zeitplan hätte. Zum Beispiel die Wirkung von Schaum, der bei Annäherung an die Küste größer wird. Um es zu implementieren, muss der Shader wissen, wie nahe das Fragment an der Küste liegt. Wir können diese Informationen über UV-Koordinaten übertragen.

Offenes Wasser hat keine UV-Koordinaten und benötigt keinen Schaum. Es wird nur für küstennahe Gewässer benötigt. Daher sind die Anforderungen für beide Wassertypen sehr unterschiedlich. Es ist logisch, für jeden Typ ein eigenes Netz zu erstellen. Daher fügen wir HexGridChunkein weiteres Mesh-Objekt hinzu , um es zu unterstützen.

	public HexMesh terrain, rivers, roads, water, waterShore;
	public void Triangulate () {
		terrain.Clear();
		rivers.Clear();
		roads.Clear();
		water.Clear();
		waterShore.Clear();
		for (int i = 0; i < cells.Length; i++) {
			Triangulate(cells[i]);
		}
		terrain.Apply();
		rivers.Apply();
		roads.Apply();
		water.Apply();
		waterShore.Apply();
	}

Dieses neue Netz wird verwendet TriangulateWaterShore.

	void TriangulateWaterShore (
		HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center
	) {
		…
		waterShore.AddQuad(e1.v1, e1.v2, e2.v1, e2.v2);
		waterShore.AddQuad(e1.v2, e1.v3, e2.v2, e2.v3);
		waterShore.AddQuad(e1.v3, e1.v4, e2.v3, e2.v4);
		waterShore.AddQuad(e1.v4, e1.v5, e2.v4, e2.v5);
		HexCell nextNeighbor = cell.GetNeighbor(direction.Next());
		if (nextNeighbor != null) {
			waterShore.AddTriangle(
				e1.v5, e2.v5, e1.v5 + HexMetrics.GetBridge(direction.Next())
			);
		}
	}

Duplizieren Sie das Wasserobjekt, verbinden Sie es mit dem Fertighaus und konfigurieren Sie es so, dass es die UV-Koordinaten verwendet. Wir werden auch einen Shader und ein Material für Küstenwasser erstellen, wobei der vorhandene Shader und das vorhandene Wassermaterial dupliziert werden.


Wasserufergegenstand und -material mit UV.

Ändern Sie den Water Shore- Shader so, dass anstelle von Wasser die UV-Koordinaten angezeigt werden.

			fixed4 c = fixed4(IN.uv_MainTex, 1, 1);

Da die Koordinaten noch nicht angegeben sind, wird eine Volltonfarbe angezeigt. Dies macht es leicht zu erkennen, dass die Küste tatsächlich ein separates Netz mit Material verwendet.


Separates Netz für die Küste.

Lassen Sie uns Informationen über die Küste in die Koordinate V eintragen. Auf der Wasserseite weisen wir ihr den Wert 0 zu, auf der Landseite den Wert 1. Da wir nichts mehr senden müssen, sind alle U-Koordinaten einfach 0.

		waterShore.AddQuad(e1.v1, e1.v2, e2.v1, e2.v2);
		waterShore.AddQuad(e1.v2, e1.v3, e2.v2, e2.v3);
		waterShore.AddQuad(e1.v3, e1.v4, e2.v3, e2.v4);
		waterShore.AddQuad(e1.v4, e1.v5, e2.v4, e2.v5);
		waterShore.AddQuadUV(0f, 0f, 0f, 1f);
		waterShore.AddQuadUV(0f, 0f, 0f, 1f);
		waterShore.AddQuadUV(0f, 0f, 0f, 1f);
		waterShore.AddQuadUV(0f, 0f, 0f, 1f);
		HexCell nextNeighbor = cell.GetNeighbor(direction.Next());
		if (nextNeighbor != null) {
			waterShore.AddTriangle(
				e1.v5, e2.v5, e1.v5 + HexMetrics.GetBridge(direction.Next())
			);
			waterShore.AddTriangleUV(
				new Vector2(0f, 0f),
				new Vector2(0f, 1f),
				new Vector2(0f, 0f)
			);
		}


Übergänge zu den Küsten, falsch.

Der obige Code funktioniert für Kanten, ist jedoch in einigen Winkeln fehlerhaft. Befindet sich der nächste Nachbar unter Wasser, ist dieser Ansatz korrekt. Befindet sich der nächste Nachbar jedoch nicht unter Wasser, befindet sich der dritte Scheitelpunkt des Dreiecks unter trockenem Land.

			waterShore.AddTriangleUV(
				new Vector2(0f, 0f),
				new Vector2(0f, 1f),
				new Vector2(0f, nextNeighbor.IsUnderwater ? 0f : 1f)
			);


Übergänge zu den Küsten, richtig.

Schaum an der Küste


Nachdem die Übergänge zur Küste korrekt implementiert wurden, können Sie sie verwenden, um einen Schaumeffekt zu erzeugen. Der einfachste Weg, einer einheitlichen Farbe den Wert der Küste zu verleihen.

		void surf (Input IN, inout SurfaceOutputStandard o) {
			float shore = IN.uv_MainTex.y;
			float foam = shore;
			fixed4 c = saturate(_Color + foam);
			o.Albedo = c.rgb;
			o.Metallic = _Metallic;
			o.Smoothness = _Glossiness;
			o.Alpha = c.a;
		}


Linearer Schaum.

Um den Schaum interessanter zu machen, multiplizieren Sie ihn mit dem Quadrat der Sinuswelle.

			float foam = sin(shore * 10);
			foam *= foam * shore;


Verblassender quadratischer Sinusschaum.

Lassen Sie uns die Schaumfront vergrößern, wenn wir uns dem Ufer nähern. Dies kann erreicht werden, indem die Quadratwurzel gezogen wird, bevor der Wert der Küste verwendet wird.

			float shore = IN.uv_MainTex.y;
			shore = sqrt(shore);


In Ufernähe wird der Schaum dicker.

Fügen Sie Verzerrungen hinzu, damit es natürlicher aussieht. Machen wir es so, dass die Verzerrung schwächer wird, wenn wir uns dem Ufer nähern. So wird es besser an die Küste passen.

			float2 noiseUV = IN.worldPos.xz;
			float4 noise = tex2D(_MainTex, noiseUV * 0.015);
			float distortion = noise.x * (1 - shore);
			float foam = sin((shore + distortion) * 10);
			foam *= foam * shore;


Verzerrter Schaum.

Und das alles ist natürlich animiert: sowohl eine Sinuswelle als auch eine Verzerrung.

			float2 noiseUV = IN.worldPos.xz + _Time.y * 0.25;
			float4 noise = tex2D(_MainTex, noiseUV * 0.015);
			float distortion = noise.x * (1 - shore);
			float foam = sin((shore + distortion) * 10 - _Time.y);
			foam *= foam * shore;


Animierter Schaum.

Zusätzlich zum ankommenden Schaum gibt es einen Rückzug. Fügen wir für die Simulation eine zweite Sinuskurve hinzu, die sich in die entgegengesetzte Richtung bewegt. Machen Sie es schwächer und fügen Sie eine Zeitverschiebung hinzu. Der fertige Schaum wird das Maximum dieser beiden Sinuskurven sein.

			float distortion1 = noise.x * (1 - shore);
			float foam1 = sin((shore + distortion1) * 10 - _Time.y);
			foam1 *= foam1;
			float distortion2 = noise.y * (1 - shore);
			float foam2 = sin((shore + distortion2) * 10 + _Time.y + 2);
			foam2 *= foam2 * 0.7;
			float foam = max(foam1, foam2) * shore;


Eingehender und zurückgehender Schaum.

Wellen und Schaum mischen


Es gibt einen scharfen Übergang zwischen offenen Gewässern und Küstengewässern, da offene Wasserwellen nicht in den Küstengewässern enthalten sind. Um dies zu beheben, müssen wir diese Wellen in den Water Shore- Shader aufnehmen .

Anstatt den Code der Wellen zu kopieren, fügen wir ihn in die Include- Datei Water.cginc ein . Tatsächlich fügen wir Code für Schaum und für Wellen ein, jeweils als separate Funktion.

Wie funktionieren Shader-Include-Dateien?
Создание собственных include-файлов шейдеров рассматривается в туториале Rendering 5, Multiple Lights.

float Foam (float shore, float2 worldXZ, sampler2D noiseTex) {
//	float shore = IN.uv_MainTex.y;
	shore = sqrt(shore);
	float2 noiseUV = worldXZ + _Time.y * 0.25;
	float4 noise = tex2D(noiseTex, noiseUV * 0.015);
	float distortion1 = noise.x * (1 - shore);
	float foam1 = sin((shore + distortion1) * 10 - _Time.y);
	foam1 *= foam1;
	float distortion2 = noise.y * (1 - shore);
	float foam2 = sin((shore + distortion2) * 10 + _Time.y + 2);
	foam2 *= foam2 * 0.7;
	return max(foam1, foam2) * shore;
}
float Waves (float2 worldXZ, sampler2D noiseTex) {
	float2 uv1 = worldXZ;
	uv1.y += _Time.y;
	float4 noise1 = tex2D(noiseTex, uv1 * 0.025);
	float2 uv2 = worldXZ;
	uv2.x += _Time.y;
	float4 noise2 = tex2D(noiseTex, uv2 * 0.025);
	float blendWave = sin(
		(worldXZ.x + worldXZ.y) * 0.1 +
		(noise1.y + noise2.z) + _Time.y
	);
	blendWave *= blendWave;
	float waves =
		lerp(noise1.z, noise1.w, blendWave) +
		lerp(noise2.x, noise2.y, blendWave);
	return smoothstep(0.75, 2, waves);
}

Ändern Sie den Water- Shader , um die neue Include-Datei zu verwenden.

		#include "Water.cginc"
		sampler2D _MainTex;
		…
		void surf (Input IN, inout SurfaceOutputStandard o) {
			float waves = Waves(IN.worldPos.xz, _MainTex);
			fixed4 c = saturate(_Color + waves);
			o.Albedo = c.rgb;
			o.Metallic = _Metallic;
			o.Smoothness = _Glossiness;
			o.Alpha = c.a;
		}

Der Water Shore Shader berechnet Werte für Schaum und Wellen. Dann dämpfen wir die Wellen, während wir uns dem Ufer nähern. Das Endergebnis wird ein Maximum an Schaum und Wellen sein.

		#include "Water.cginc"
		sampler2D _MainTex;
		…
		void surf (Input IN, inout SurfaceOutputStandard o) {
			float shore = IN.uv_MainTex.y;
			float foam = Foam(shore, IN.worldPos.xz, _MainTex);
			float waves = Waves(IN.worldPos.xz, _MainTex);
			waves *= 1 - shore;
			fixed4 c = saturate(_Color + max(foam, waves));
			o.Albedo = c.rgb;
			o.Metallic = _Metallic;
			o.Smoothness = _Glossiness;
			o.Alpha = c.a;
		}


Schaum und Wellen mischen.

Einheitspaket

Wieder über Küstenwasser


Ein Teil des Küstennetzes ist unter dem Netz des Reliefs verborgen. Das ist normal, aber nur ein kleiner Teil ist verborgen. Leider verbergen steile Klippen den größten Teil des Küstenwassers und schäumen daher.


Fast verstecktes Küstenwasser.

Wir können damit umgehen, indem wir die Küste vergrößern. Dies kann erreicht werden, indem der Radius der Sechsecke des Wassers verringert wird. Hierfür benötigen wir neben dem Integritätskoeffizienten HexMetricsein Wasserverhältnis sowie Methoden zur Ermittlung von Wasserwinkeln.

Der Integritätskoeffizient beträgt 0,8. Um die Größe der Wasserverbindungen zu verdoppeln, müssen wir dem Wasserkoeffizienten einen Wert von 0,6 zuweisen.

	public const float waterFactor = 0.6f;
	public static Vector3 GetFirstWaterCorner (HexDirection direction) {
		return corners[(int)direction] * waterFactor;
	}
	public static Vector3 GetSecondWaterCorner (HexDirection direction) {
		return corners[(int)direction + 1] * waterFactor;
	}

Mit diesen neuen Methoden ermitteln HexGridChunkwir die Winkel des Wassers.

	void TriangulateOpenWater (
		HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center
	) {
		Vector3 c1 = center + HexMetrics.GetFirstWaterCorner(direction);
		Vector3 c2 = center + HexMetrics.GetSecondWaterCorner(direction);
		…
	}
	void TriangulateWaterShore (
		HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center
	) {
		EdgeVertices e1 = new EdgeVertices(
			center + HexMetrics.GetFirstWaterCorner(direction),
			center + HexMetrics.GetSecondWaterCorner(direction)
		);
		…
	}


Mit Wasserwinkeln.

Der Abstand zwischen den Sechsecken des Wassers hat sich in der Tat verdoppelt. Jetzt HexMetricssollte auch eine Methode zur Schaffung von Brücken im Wasser haben.

	public const float waterBlendFactor = 1f - waterFactor;
	public static Vector3 GetWaterBridge (HexDirection direction) {
		return (corners[(int)direction] + corners[(int)direction + 1]) *
			waterBlendFactor;
	}

Ändern wir es HexGridChunkso, dass es die neue Methode verwendet.

	void TriangulateOpenWater (
		HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center
	) {
		…
		if (direction <= HexDirection.SE && neighbor != null) {
			Vector3 bridge = HexMetrics.GetWaterBridge(direction);
			…
			if (direction <= HexDirection.E) {
				…
				water.AddTriangle(
					c2, e2, c2 + HexMetrics.GetWaterBridge(direction.Next())
				);
			}
		}
	}
	void TriangulateWaterShore (
		HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center
	) {
		…
		Vector3 bridge = HexMetrics.GetWaterBridge(direction);
		…
		HexCell nextNeighbor = cell.GetNeighbor(direction.Next());
		if (nextNeighbor != null) {
			waterShore.AddTriangle(
				e1.v5, e2.v5, e1.v5 +
					HexMetrics.GetWaterBridge(direction.Next())
			);
			…
		}
	}


Lange Brücken im Wasser.

Zwischen den Rippen von Wasser und Land


Das gibt uns zwar mehr Platz für den Schaum, verbirgt aber jetzt noch mehr unter dem Relief. Im Idealfall können wir den Wasserrand von der Wasserseite und den Landrand von der Landseite verwenden.

Wir können keine einfache Brücke benutzen, um den gegenüberliegenden Rand des Landes zu finden, wenn wir von den Ecken des Wassers ausgehen. Stattdessen können wir vom Zentrum des Nachbarn aus in die entgegengesetzte Richtung gehen. Ändern Sie TriangulateWaterShore, um diesen neuen Ansatz zu verwenden.

//		Vector3 bridge = HexMetrics.GetWaterBridge(direction);
		Vector3 center2 = neighbor.Position;
		center2.y = center.y;
		EdgeVertices e2 = new EdgeVertices(
			center2 + HexMetrics.GetSecondSolidCorner(direction.Opposite()),
			center2 + HexMetrics.GetFirstSolidCorner(direction.Opposite())
		);
		…
		HexCell nextNeighbor = cell.GetNeighbor(direction.Next());
		if (nextNeighbor != null) {
			Vector3 center3 = nextNeighbor.Position;
			center3.y = center.y;
			waterShore.AddTriangle(
				e1.v5, e2.v5, center3 +
					HexMetrics.GetFirstSolidCorner(direction.Previous())
			);
			…
		}


Falsche Ecken der Kanten.

Es hat funktioniert, aber jetzt müssen wir wieder zwei Fälle für Eckdreiecke betrachten.

		HexCell nextNeighbor = cell.GetNeighbor(direction.Next());
		if (nextNeighbor != null) {
//			Vector3 center3 = nextNeighbor.Position;
//			center3.y = center.y;
			Vector3 v3 = nextNeighbor.Position + (nextNeighbor.IsUnderwater ?
				HexMetrics.GetFirstWaterCorner(direction.Previous()) :
				HexMetrics.GetFirstSolidCorner(direction.Previous()));
			v3.y = center.y;
			waterShore.AddTriangle(e1.v5, e2.v5, v3);
			waterShore.AddTriangleUV(
				new Vector2(0f, 0f),
				new Vector2(0f, 1f),
				new Vector2(0f, nextNeighbor.IsUnderwater ? 0f : 1f)
			);
		}


Die richtigen Winkel der Kanten.

Es hat gut funktioniert, aber jetzt, da der größte Teil des Schaums sichtbar ist, ist er ziemlich ausgeprägt. Um dies zu kompensieren, werden wir den Effekt etwas schwächer machen und den Maßstab der Küste im Shader verringern.

	shore = sqrt(shore) * 0.9;


Fertigschaum.

Einheitspaket

U-Boot-Flüsse


Zumindest dort, wo Flüsse nicht hineinfließen, haben wir Wasser. Da sich Wasser und Flüsse noch nicht bemerken, fließen Flüsse durch und unter das Wasser.


Flüsse fließen in Wasser.

Die Reihenfolge, in der durchscheinende Objekte gerendert werden, hängt von ihrem Abstand zur Kamera ab. Die nächstgelegenen Objekte werden zuletzt gerendert, sodass sie sich oben befinden. Wenn Sie die Kamera bewegen, treten manchmal Flüsse und manchmal Wasser übereinander auf. Beginnen wir damit, die Renderreihenfolge konstant zu halten. Flüsse müssen über dem Wasser gezogen werden, damit die Wasserfälle richtig angezeigt werden. Dies können wir erreichen, indem wir die Warteschlange des River Shader ändern .

		Tags { "RenderType"="Transparent" "Queue"="Transparent+1" }


Zeichne den Fluss zuletzt.

Unterwasserflüsse verstecken


Obwohl das Flussbett unter Wasser sein kann und tatsächlich Wasser durch es fließen kann, sollten wir dieses Wasser nicht sehen. Und noch mehr sollte es nicht auf der realen Wasseroberfläche gerendert werden. Wir können das Wasser von U-Boot-Flüssen loswerden, indem wir Flusssegmente nur hinzufügen, wenn sich die aktuelle Zelle nicht unter Wasser befindet.

	void TriangulateWithRiverBeginOrEnd (
		HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e
	) {
		…
		if (!cell.IsUnderwater) {
			bool reversed = cell.HasIncomingRiver;
			…
		}
	}
	void TriangulateWithRiver (
		HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e
	) {
		…
		if (!cell.IsUnderwater) {
			bool reversed = cell.IncomingRiver == direction;
			…
		}
	}

Zu TriangulateConnectionBeginn werden wir einen Abschnitt des Flusses hinzufügen, wenn weder die Strömung noch die benachbarte Zelle unter Wasser sind.

		if (cell.HasRiverThroughEdge(direction)) {
			e2.v3.y = neighbor.StreamBedY;
			if (!cell.IsUnderwater && !neighbor.IsUnderwater) {
				TriangulateRiverQuad(
					e1.v2, e1.v4, e2.v2, e2.v4,
					cell.RiverSurfaceY, neighbor.RiverSurfaceY, 0.8f,
					cell.HasIncomingRiver && cell.IncomingRiver == direction
				);
			}
		}


Keine Unterwasserflüsse mehr.

Wasserfälle


Es gibt keine Unterwasserflüsse mehr, aber jetzt haben wir Löcher in den Teilen der Flüsse, in denen sie auf die Wasseroberfläche treffen. Flüsse, die mit Wasser gespült werden, verursachen kleine Löcher oder Überlappungen. Am auffälligsten sind jedoch die fehlenden Wasserfälle für Flüsse aus größerer Höhe. Lass es uns zuerst tun.

Das Flusssegment mit einem Wasserfall führte durch die Wasseroberfläche. Infolgedessen war es teilweise über und teilweise unter Wasser. Wir müssen einen Teil über dem Wasserspiegel halten und alles andere wegwerfen. Wir müssen hart dafür arbeiten, also werden wir eine separate Methode erstellen.

Die neue Methode erfordert vier Gipfel, zwei Ebenen von Flüssen und Wasserstand. Wir werden es so aufstellen, dass wir in die Richtung der Strömung schauen, den Wasserfall hinunter. Daher befinden sich die ersten beiden Eckpunkte sowie die linke und die rechte Seite oben, gefolgt von den unteren.

	void TriangulateWaterfallInWater (
		Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4,
		float y1, float y2, float waterY
	) {
		v1.y = v2.y = y1;
		v3.y = v4.y = y2;
		rivers.AddQuad(v1, v2, v3, v4);
		rivers.AddQuadUV(0f, 1f, 0.8f, 1f);
	}

Nennen wir diese Methode TriangulateConnection, wenn ein Nachbar unter Wasser ist und wir einen Wasserfall erstellen.

			if (!cell.IsUnderwater) {
				if (!neighbor.IsUnderwater) {
					TriangulateRiverQuad(
						e1.v2, e1.v4, e2.v2, e2.v4,
						cell.RiverSurfaceY, neighbor.RiverSurfaceY, 0.8f,
						cell.HasIncomingRiver && cell.IncomingRiver == direction
					);
				}
				else if (cell.Elevation > neighbor.WaterLevel) {
					TriangulateWaterfallInWater(
						e1.v2, e1.v4, e2.v2, e2.v4,
						cell.RiverSurfaceY, neighbor.RiverSurfaceY,
						neighbor.WaterSurfaceY
					);
				}
			}

Wir müssen auch Wasserfälle in die entgegengesetzte Richtung verarbeiten, wenn sich die aktuelle Zelle unter Wasser befindet und die nächste Zelle nicht.

			if (!cell.IsUnderwater) {
				…
			}
			else if (
				!neighbor.IsUnderwater &&
				neighbor.Elevation > cell.WaterLevel
			) {
				TriangulateWaterfallInWater(
					e2.v4, e2.v2, e1.v4, e1.v2,
					neighbor.RiverSurfaceY, cell.RiverSurfaceY,
					cell.WaterSurfaceY
				);
			}

Also bekommen wir wieder das Quad des Quellflusses. Als nächstes müssen wir ändern, TriangulateWaterfallInWaterso dass es die unteren Spitzen auf den Wasserspiegel anhebt. Leider reicht es nicht aus, nur die Y-Koordinaten zu ändern. Dadurch kann der Wasserfall von der Klippe wegbewegt werden, wodurch sich Löcher bilden können. Stattdessen müssen Sie die unteren Eckpunkte mithilfe der Interpolation nach oben verschieben.


Wir interpolieren.

Um die unteren Gipfel nach oben zu bewegen, teilen Sie deren Abstand unter der Wasseroberfläche durch die Höhe des Wasserfalls. Dies gibt uns den Wert des Interpolators.

		v1.y = v2.y = y1;
		v3.y = v4.y = y2;
		float t = (waterY - y2) / (y1 - y2);
		v3 = Vector3.Lerp(v3, v1, t);
		v4 = Vector3.Lerp(v4, v2, t);
		rivers.AddQuad(v1, v2, v3, v4);
		rivers.AddQuadUV(0f, 1f, 0.8f, 1f);

Als Ergebnis erhalten wir einen verkürzten Wasserfall mit der gleichen Ausrichtung. Da sich jedoch die Positionen der unteren Scheitelpunkte geändert haben, werden sie nicht wie die ursprünglichen Scheitelpunkte verzerrt. Dies bedeutet, dass das Endergebnis immer noch nicht mit dem ursprünglichen Wasserfall übereinstimmt. Um dieses Problem zu lösen, müssen wir die Eckpunkte vor der Interpolation manuell verzerren und dann ein unverzerrtes Quad hinzufügen.

		v1.y = v2.y = y1;
		v3.y = v4.y = y2;
		v1 = HexMetrics.Perturb(v1);
		v2 = HexMetrics.Perturb(v2);
		v3 = HexMetrics.Perturb(v3);
		v4 = HexMetrics.Perturb(v4);
		float t = (waterY - y2) / (y1 - y2);
		v3 = Vector3.Lerp(v3, v1, t);
		v4 = Vector3.Lerp(v4, v2, t);
		rivers.AddQuadUnperturbed(v1, v2, v3, v4);
		rivers.AddQuadUV(0f, 1f, 0.8f, 1f);

Da wir bereits eine Methode zum Hinzufügen unverzerrter Dreiecke haben, müssen wir sie nicht unbedingt für Quad-S erstellen. Daher fügen wir die erforderliche Methode hinzu HexMesh.AddQuadUnperturbed.

	public void AddQuadUnperturbed (
		Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4
	) {
		int vertexIndex = vertices.Count;
		vertices.Add(v1);
		vertices.Add(v2);
		vertices.Add(v3);
		vertices.Add(v4);
		triangles.Add(vertexIndex);
		triangles.Add(vertexIndex + 2);
		triangles.Add(vertexIndex + 1);
		triangles.Add(vertexIndex + 1);
		triangles.Add(vertexIndex + 2);
		triangles.Add(vertexIndex + 3);
	}


Wasserfälle enden an der Wasseroberfläche.

Einheitspaket

Mündung


Wenn Flüsse auf der gleichen Höhe wie die Wasseroberfläche fließen, berührt die Masche des Flusses die Masche der Küste. Wenn es ein Fluss wäre, der ins Meer oder in den Ozean fließt, dann gäbe es eine Strömung des Flusses mit einer Brandung. Daher werden wir diese Bereiche Münder nennen.


Der Fluss trifft auf die Küste, ohne die Gipfel zu verzerren.

Jetzt haben wir zwei Probleme mit dem Mund. Erstens verbinden sich Quad-Flüsse mit der zweiten und vierten Spitze der Rippen und passieren die dritte. Da die Küste des Wassers den dritten Peak nicht nutzt, kann es zu einem Loch oder einer Überlappung kommen. Wir können dieses Problem lösen, indem wir die Mundgeometrie ändern.

Das zweite Problem ist, dass es einen scharfen Übergang zwischen Schaum und Flussmaterial gibt. Um das Problem zu lösen, benötigen wir ein anderes Material, das die Auswirkungen des Flusses und des Wassers mischt.

Dies bedeutet, dass die Münder einen speziellen Ansatz erfordern. Lassen Sie uns daher eine separate Methode für sie erstellen. Es sollte aufgerufen werden, TriangulateWaterShorewenn sich ein Fluss in die aktuelle Richtung bewegt.

	void TriangulateWaterShore (
		HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center
	) {
		…
		if (cell.HasRiverThroughEdge(direction)) {
			TriangulateEstuary(e1, e2);
		}
		else {
			waterShore.AddQuad(e1.v1, e1.v2, e2.v1, e2.v2);
			waterShore.AddQuad(e1.v2, e1.v3, e2.v2, e2.v3);
			waterShore.AddQuad(e1.v3, e1.v4, e2.v3, e2.v4);
			waterShore.AddQuad(e1.v4, e1.v5, e2.v4, e2.v5);
			waterShore.AddQuadUV(0f, 0f, 0f, 1f);
			waterShore.AddQuadUV(0f, 0f, 0f, 1f);
			waterShore.AddQuadUV(0f, 0f, 0f, 1f);
			waterShore.AddQuadUV(0f, 0f, 0f, 1f);
		}
		…
	}
	void TriangulateEstuary (EdgeVertices e1, EdgeVertices e2) {
	}

Eine Region, in der beide Effekte gemischt werden, muss nicht das gesamte Band ausfüllen. Die Form eines Trapezes reicht uns. Daher können wir zwei Küstendreiecke an den Seiten verwenden.

	void TriangulateEstuary (EdgeVertices e1, EdgeVertices e2) {
		waterShore.AddTriangle(e2.v1, e1.v2, e1.v1);
		waterShore.AddTriangle(e2.v5, e1.v5, e1.v4);
		waterShore.AddTriangleUV(
			new Vector2(0f, 1f), new Vector2(0f, 0f), new Vector2(0f, 0f)
		);
		waterShore.AddTriangleUV(
			new Vector2(0f, 1f), new Vector2(0f, 0f), new Vector2(0f, 0f)
		);
	}


Trapezloch für den Mischbereich.

UV2-Koordinaten


Um den Effekt des Flusses zu erzeugen, benötigen wir UV-Koordinaten. Aber um einen Schaumeffekt zu erzeugen, brauchen wir auch UV-Koordinaten. Das heißt, wenn wir sie mischen, benötigen wir zwei Sätze von UV-Koordinaten. Glücklicherweise können Unity-Engine-Maschen bis zu vier UV-Sätze unterstützen. Wir müssen nur HexMeshden zweiten Satz unterstützen.

	public bool useCollider, useColors, useUVCoordinates, useUV2Coordinates;
	[NonSerialized] List<Vector2> uvs, uv2s;
	public void Clear () {
		…
		if (useUVCoordinates) {
			uvs = ListPool<Vector2>.Get();
		}
		if (useUV2Coordinates) {
			uv2s = ListPool<Vector2>.Get();
		}
		triangles = ListPool<int>.Get();
	}
	public void Apply () {
		…
		if (useUVCoordinates) {
			hexMesh.SetUVs(0, uvs);
			ListPool<Vector2>.Add(uvs);
		}
		if (useUV2Coordinates) {
			hexMesh.SetUVs(1, uv2s);
			ListPool<Vector2>.Add(uv2s);
		}
		…
	}

Um einen zweiten UV-Satz hinzuzufügen, duplizieren wir die Methoden zum Arbeiten mit UV und ändern die Art und Weise, die wir benötigen.

	public void AddTriangleUV2 (Vector2 uv1, Vector2 uv2, Vector3 uv3) {
		uv2s.Add(uv1);
		uv2s.Add(uv2);
		uv2s.Add(uv3);
	}
	public void AddQuadUV2 (Vector2 uv1, Vector2 uv2, Vector3 uv3, Vector3 uv4) {
		uv2s.Add(uv1);
		uv2s.Add(uv2);
		uv2s.Add(uv3);
		uv2s.Add(uv4);
	}
	public void AddQuadUV2 (float uMin, float uMax, float vMin, float vMax) {
		uv2s.Add(new Vector2(uMin, vMin));
		uv2s.Add(new Vector2(uMax, vMin));
		uv2s.Add(new Vector2(uMin, vMax));
		uv2s.Add(new Vector2(uMax, vMax));
	}

River Shader-Funktion


Da wir den River-Effekt in zwei Shadern verwenden werden, verschieben Sie den Code aus dem River- Shader in die neue Funktion der Include-Datei Water .

float River (float2 riverUV, sampler2D noiseTex) {
	float2 uv = riverUV;
	uv.x = uv.x * 0.0625 + _Time.y * 0.005;
	uv.y -= _Time.y * 0.25;
	float4 noise = tex2D(noiseTex, uv);
	float2 uv2 = riverUV;
	uv2.x = uv2.x * 0.0625 - _Time.y * 0.0052;
	uv2.y -= _Time.y * 0.23;
	float4 noise2 = tex2D(noiseTex, uv2);
	return noise.x * noise2.w;
}

Ändern Sie den River Shader , um diese neue Funktion zu verwenden.

		#include "Water.cginc"
		sampler2D _MainTex;
		…
		void surf (Input IN, inout SurfaceOutputStandard o) {
			float river = River(IN.uv_MainTex, _MainTex);
			fixed4 c = saturate(_Color + river);
			…
		}

Objekte Mund


Zur HexGridChunkUnterstützung des Mundobjektnetzes hinzufügen.

	public HexMesh terrain, rivers, roads, water, waterShore, estuaries;
	public void Triangulate () {
		terrain.Clear();
		rivers.Clear();
		roads.Clear();
		water.Clear();
		waterShore.Clear();
		estuaries.Clear();
		for (int i = 0; i < cells.Length; i++) {
			Triangulate(cells[i]);
		}
		terrain.Apply();
		rivers.Apply();
		roads.Apply();
		water.Apply();
		waterShore.Apply();
		estuaries.Apply();
	}

Erstellen Sie einen Shader-, Material- und Objektmund, duplizieren Sie die Küste und ändern Sie sie. Verbinden Sie es mit dem Fragment und verwenden Sie die UV- und UV2-Koordinaten.


Estuarties Objekt.

Mündungs-Triangulation


Wir können das Problem eines Lochs oder einer Überlagerung lösen, indem wir ein Dreieck zwischen dem Ende des Flusses und dem mittleren Rand des Wassers platzieren. Da unser Mund-Shader ein Duplikat des Küsten-Shaders ist, stellen wir die UV-Koordinaten ein, die dem Schaum-Effekt entsprechen.

	void TriangulateEstuary (EdgeVertices e1, EdgeVertices e2) {
		…
		estuaries.AddTriangle(e1.v3, e2.v2, e2.v4);
		estuaries.AddTriangleUV(
			new Vector2(0f, 0f), new Vector2(0f, 1f), new Vector2(0f, 1f)
		);
	}


Mittleres Dreieck.

Wir können das gesamte Trapez füllen, indem wir auf beiden Seiten des mittleren Dreiecks ein Viereck einfügen.

		estuaries.AddQuad(e1.v2, e1.v3, e2.v1, e2.v2);
		estuaries.AddTriangle(e1.v3, e2.v2, e2.v4);
		estuaries.AddQuad(e1.v3, e1.v4, e2.v4, e2.v5);
		estuaries.AddQuadUV(0f, 0f, 0f, 1f);
		estuaries.AddTriangleUV(
			new Vector2(0f, 0f), new Vector2(0f, 1f), new Vector2(0f, 1f)
		);
		estuaries.AddQuadUV(0f, 0f, 0f, 1f);


Bereit Trapez.

Drehen wir die Ausrichtung des Vierecks nach links, so dass es eine verkürzte diagonale Verbindung hat und als Ergebnis eine symmetrische Geometrie erhalten.

		estuaries.AddQuad(e2.v1, e1.v2, e2.v2, e1.v3);
		estuaries.AddTriangle(e1.v3, e2.v2, e2.v4);
		estuaries.AddQuad(e1.v3, e1.v4, e2.v4, e2.v5);
		estuaries.AddQuadUV(
			new Vector2(0f, 1f), new Vector2(0f, 0f),
			new Vector2(0f, 1f), new Vector2(0f, 0f)
		);
//		estuaries.AddQuadUV(0f, 0f, 0f, 1f);


Gedrehte vierfache, symmetrische Geometrie

Fluss fließen


Um die Wirkung des Flusses zu unterstützen, müssen wir UV2-Koordinaten hinzufügen. Der Boden des mittleren Dreiecks befindet sich in der Mitte des Flusses, daher sollte seine U-Koordinate gleich 0,5 sein. Wenn der Fluss in Richtung Wasser fließt, erhält der linke Punkt die U-Koordinate gleich 1 und der rechte die U-Koordinate mit dem Wert 0. Die Y-Koordinaten seien 0 und 1, entsprechend der Flussrichtung.

		estuaries.AddTriangleUV2(
			new Vector2(0.5f, 1f), new Vector2(1f, 0f), new Vector2(0f, 0f)
		);

Die Vierecke auf beiden Seiten des Dreiecks müssen mit dieser Ausrichtung übereinstimmen. Wir behalten die gleichen U-Koordinaten für Punkte bei, die die Breite des Flusses überschreiten.

		estuaries.AddQuadUV2(
			new Vector2(1f, 0f), new Vector2(1f, 1f),
			new Vector2(1f, 0f), new Vector2(0.5f, 1f)
		);
		estuaries.AddTriangleUV2(
			new Vector2(0.5f, 1f), new Vector2(1f, 0f), new Vector2(0f, 0f)
		);
		estuaries.AddQuadUV2(
			new Vector2(0.5f, 1f), new Vector2(0f, 1f),
			new Vector2(0f, 0f), new Vector2(0f, 0f)
		);


UV2-Trapez.

Um sicherzustellen, dass die UV2-Koordinaten korrekt eingestellt sind, rendern sie mit dem Shader " Mündung" . Wir können auf diese Koordinaten zugreifen, indem wir sie zur Eingabestruktur hinzufügen float2 uv2_MainTex.

		struct Input {
			float2 uv_MainTex;
			float2 uv2_MainTex;
			float3 worldPos;
		};
		…
		void surf (Input IN, inout SurfaceOutputStandard o) {
			float shore = IN.uv_MainTex.y;
			float foam = Foam(shore, IN.worldPos.xz, _MainTex);
			float waves = Waves(IN.worldPos.xz, _MainTex);
			waves *= 1 - shore;
			fixed4 c = fixed4(IN.uv2_MainTex, 1, 1);
			…
		}


UV2-Koordinaten.

Alles sieht gut aus, Sie können einen Shader verwenden, um einen Flusseffekt zu erzeugen.

		void surf (Input IN, inout SurfaceOutputStandard o) {
			…
			float river = River(IN.uv2_MainTex, _MainTex);
			fixed4 c = saturate(_Color + river);
			…
		}


Verwenden Sie UV2, um einen Flusseffekt zu erzielen.

Wir haben Flüsse so angelegt, dass sich beim Triangulieren der Verbindungen zwischen den Zellen die V-Koordinaten des Flusses von 0,8 auf 1 ändern. Daher sollten wir hier auch dieses Intervall und nicht Werte von 0 auf 1 verwenden. Die Küstenverbindung ist jedoch 50% größer als normale Zellenverbindungen . Für eine optimale Anpassung an den Flussverlauf müssen wir daher die Werte von 0,8 auf 1,1 ändern.

		estuaries.AddQuadUV2(
			new Vector2(1f, 0.8f), new Vector2(1f, 1.1f),
			new Vector2(1f, 0.8f), new Vector2(0.5f, 1.1f)
		);
		estuaries.AddTriangleUV2(
			new Vector2(0.5f, 1.1f),
			new Vector2(1f, 0.8f),
			new Vector2(0f, 0.8f)
		);
		estuaries.AddQuadUV2(
			new Vector2(0.5f, 1.1f), new Vector2(0f, 1.1f),
			new Vector2(0f, 0.8f), new Vector2(0f, 0.8f)
		);



Synchronisierter Fluss und Mündung.

Flusskontrolle


Während sich die Strömung des Flusses in einer geraden Linie bewegt. Aber wenn Wasser in ein größeres Gebiet fließt, dehnt es sich aus. Die Strömung wird gebogen. Wir können dies simulieren, indem wir die UV2-Koordinaten falten.

Anstatt die oberen Koordinaten U über die Flussbreite hinaus konstant zu halten, verschieben wir sie um 0,5. Der Punkt ganz links ist auf 1,5 eingestellt, der Punkt ganz rechts ist -0,5.

Gleichzeitig erweitern wir den Fluss, indem wir die Koordinaten U des linken und rechten unteren Punkts verschieben. Ändern Sie die linke von 1 auf 0,7 und die rechte von 0 auf 0,3.

		estuaries.AddQuadUV2(
			new Vector2(1.5f, 0.8f), new Vector2(0.7f, 1.1f),
			new Vector2(1f, 0.8f), new Vector2(0.5f, 1.1f)
		);
		…
		estuaries.AddQuadUV2(
			new Vector2(0.5f, 1.1f), new Vector2(0.3f, 1.1f),
			new Vector2(0f, 0.8f), new Vector2(-0.5f, 0.8f)
		);



Die Ausdehnung des Flusses.

Ändern Sie die V-Koordinaten derselben vier Punkte, um den Krümmungseffekt zu vervollständigen. Wenn das Wasser vom Ende des Flusses wegfließt, erhöhen wir die V-Koordinaten der oberen Punkte auf 1. Um eine bessere Kurve zu erzielen, erhöhen wir die V-Koordinaten der beiden unteren Punkte auf 1,15.

		estuaries.AddQuadUV2(
			new Vector2(1.5f, 1f), new Vector2(0.7f, 1.15f),
			new Vector2(1f, 0.8f), new Vector2(0.5f, 1.1f)
		);
		estuaries.AddTriangleUV2(
			new Vector2(0.5f, 1.1f),
			new Vector2(1f, 0.8f),
			new Vector2(0f, 0.8f)
		);
		estuaries.AddQuadUV2(
			new Vector2(0.5f, 1.1f), new Vector2(0.3f, 1.15f),
			new Vector2(0f, 0.8f), new Vector2(-0.5f, 1f)
		);



Gebogener Flussfluss.

Mischen von Fluss und Küste


Wir haben nur noch die Auswirkungen der Küste und des Flusses zu mischen. Dazu verwenden wir die lineare Interpolation, wobei der Wert der Küste als Interpolator verwendet wird.

			float shoreWater = max(foam, waves);
			float river = River(IN.uv2_MainTex, _MainTex);
			float water = lerp(shoreWater, river, IN.uv_MainTex.x);
			fixed4 c = saturate(_Color + water);

Obwohl dies funktionieren sollte, wird möglicherweise ein Kompilierungsfehler angezeigt. Der Compiler beschwert sich über das Überschreiben _MainTex_ST. Der Grund ist ein Fehler im Unity Surface Shader-Compiler, der durch die Verwendung von uv_MainTexund gleichzeitig verursacht wurde uv2_MainTex. Wir müssen einen Workaround finden.

Anstatt zu verwenden, müssen uv2_MainTexwir die sekundären UV-Koordinaten manuell übertragen. Um dies zu tun, umbenennen uv2_MainTexin riverUV. Dann fügen wir dem Shader eine Vertex-Funktion hinzu, die ihm Koordinaten zuweist.

		#pragma surface surf Standard alpha vertex:vert
		…
		struct Input {
			float2 uv_MainTex;
			float2 riverUV;
			float3 worldPos;
		};
		…
		void vert (inout appdata_full v, out Input o) {
			UNITY_INITIALIZE_OUTPUT(Input, o);
			o.riverUV = v.texcoord1.xy;
		}
		void surf (Input IN, inout SurfaceOutputStandard o) {
			…
			float river = River(IN.riverUV, _MainTex);
			…
		}


Interpolation basierend auf dem Wert der Küste.

Die Interpolation funktioniert mit Ausnahme des linken und rechten Scheitelpunkts oben. An diesen Punkten sollte der Fluss verschwinden. Daher können wir den Wert der Küste nicht nutzen. Wir müssen einen anderen Wert verwenden, der in diesen beiden Eckpunkten 0 ist. Glücklicherweise haben wir immer noch die U-Koordinate des ersten Satzes von UVs, sodass wir diesen Wert dort speichern können.

		estuaries.AddQuadUV(
			new Vector2(0f, 1f), new Vector2(0f, 0f),
			new Vector2(1f, 1f), new Vector2(0f, 0f)
		);
		estuaries.AddTriangleUV(
			new Vector2(0f, 0f), new Vector2(1f, 1f), new Vector2(1f, 1f)
		);
		estuaries.AddQuadUV(
			new Vector2(0f, 0f), new Vector2(0f, 0f),
			new Vector2(1f, 1f), new Vector2(0f, 1f)
		);
//		estuaries.AddQuadUV(0f, 0f, 0f, 1f);


Richtiges Mischen.

Jetzt haben die Mündungen eine gute Mischung zwischen dem sich ausdehnenden Fluss, dem Küstenwasser und dem Schaum. Obwohl es keine exakte Übereinstimmung mit Wasserfällen schafft, sieht dieser Effekt bei Wasserfällen gut aus.


Mund in Aktion

Einheitspaket

Flüsse fließen aus Stauseen


Wir haben bereits Flüsse, die in Stauseen fließen, aber es gibt keine Unterstützung für Flüsse, die in eine andere Richtung fließen. Es gibt Seen, aus denen Flüsse fließen, also müssen wir sie auch hinzufügen.

Wenn ein Fluss aus einem Stausee fließt, fließt er tatsächlich in eine größere Höhe. Dies ist derzeit nicht möglich. Wir müssen eine Ausnahme machen und diese Situation zulassen, wenn der Wasserstand der Höhe des Zielpunkts entspricht. Fügen wir der HexCellprivaten Methode hinzu und prüfen anhand unseres neuen Kriteriums, ob der Nachbar der richtige Zielpunkt für den abfließenden Fluss ist.

	bool IsValidRiverDestination (HexCell neighbor) {
		return neighbor && (
			elevation >= neighbor.elevation || waterLevel == neighbor.elevation
		);
	}

Wir werden unsere neue Methode anwenden, um festzustellen, ob es möglich ist, einen abgehenden Fluss zu erzeugen.

	public void SetOutgoingRiver (HexDirection direction) {
		if (hasOutgoingRiver && outgoingRiver == direction) {
			return;
		}
		HexCell neighbor = GetNeighbor(direction);
//		if (!neighbor || elevation < neighbor.elevation) {
		if (!IsValidRiverDestination(neighbor)) {
			return;
		}
		RemoveOutgoingRiver();
		…
	}

Auch dort müssen Sie den Fluss überprüfen, wenn sich die Höhe der Zelle oder der Wasserstand ändert. Erstellen Sie eine private Methode, die diese Aufgabe übernimmt.

	void ValidateRivers () {
		if (
			hasOutgoingRiver &&
			!IsValidRiverDestination(GetNeighbor(outgoingRiver))
		) {
			RemoveOutgoingRiver();
		}
		if (
			hasIncomingRiver &&
			!GetNeighbor(incomingRiver).IsValidRiverDestination(this)
		) {
			RemoveIncomingRiver();
		}
	}

Wir verwenden diese neue Methode in den Eigenschaften Elevationund WaterLevel.

	public int Elevation {
		…
		set {
			…
//			if (
//				hasOutgoingRiver &&
//				elevation < GetNeighbor(outgoingRiver).elevation
//			) {
//				RemoveOutgoingRiver();
//			}
//			if (
//				hasIncomingRiver &&
//				elevation > GetNeighbor(incomingRiver).elevation
//			) {
//				RemoveIncomingRiver();
//			}
			ValidateRivers();
			…
		}
	}
	public int WaterLevel {
		…
		set {
			if (waterLevel == value) {
				return;
			}
			waterLevel = value;
			ValidateRivers();
			Refresh();
		}
	}


Ausgehend und betretend die Seen des Flusses.

Flow entfalten


Wir HexGridChunk.TriangulateEstuarygingen davon aus, dass Flüsse nur in Stauseen fließen können. Daher bewegt sich der Fluss eines Flusses immer in eine Richtung. Wir müssen den Fluss umkehren, wenn wir es mit einem Fluss zu tun haben, der aus einem Stausee fließt. Dazu muss man TriangulateEstuarydie Strömungsrichtung kennen. Daher geben wir ihm einen booleschen Parameter, der festlegt, ob es sich um einen eingehenden Fluss handelt.

	void TriangulateEstuary (
		EdgeVertices e1, EdgeVertices e2, bool incomingRiver
	) {
	…
}

Wir werden diese Information übermitteln, wenn wir diese Methode von aufrufen TriangulateWaterShore.

		if (cell.HasRiverThroughEdge(direction)) {
			TriangulateEstuary(e1, e2, cell.IncomingRiver == direction);
		}

Jetzt müssen wir den Fluss des Flusses drehen und die Koordinaten von UV2 ändern. Die Koordinaten von U für abgehende Flüsse müssen gespiegelt werden: -0,5 wird zu 1,5, 0 wird zu 1, 1 wird zu 0 und 1,5 wird zu -0,5.

Bei V-Koordinaten sind die Dinge etwas komplizierter. Wenn Sie sich ansehen, wie wir mit invertierten Verbindungen von Flüssen gearbeitet haben, sollte 0.8 0 und 1 −0.2 sein. Dies bedeutet, dass 1.1 zu -0.3 und 1.15 zu -0.35 wird.

Da die Koordinaten von UV2 in jedem Fall sehr unterschiedlich sind, schreiben wir einen separaten Code für sie.

	void TriangulateEstuary (
		EdgeVertices e1, EdgeVertices e2, bool incomingRiver
	) {
		…
		if (incomingRiver) {
			estuaries.AddQuadUV2(
				new Vector2(1.5f, 1f), new Vector2(0.7f, 1.15f),
				new Vector2(1f, 0.8f), new Vector2(0.5f, 1.1f)
			);
			estuaries.AddTriangleUV2(
				new Vector2(0.5f, 1.1f),
				new Vector2(1f, 0.8f),
				new Vector2(0f, 0.8f)
			);
			estuaries.AddQuadUV2(
				new Vector2(0.5f, 1.1f), new Vector2(0.3f, 1.15f),
				new Vector2(0f, 0.8f), new Vector2(-0.5f, 1f)
			);
		}
		else {
			estuaries.AddQuadUV2(
				new Vector2(-0.5f, -0.2f), new Vector2(0.3f, -0.35f),
				new Vector2(0f, 0f), new Vector2(0.5f, -0.3f)
			);
			estuaries.AddTriangleUV2(
				new Vector2(0.5f, -0.3f),
				new Vector2(0f, 0f),
				new Vector2(1f, 0f)
			);
			estuaries.AddQuadUV2(
				new Vector2(0.5f, -0.3f), new Vector2(0.7f, -0.35f),
				new Vector2(1f, 0f), new Vector2(1.5f, -0.2f)
			);
		}
	}


Der richtige Fluss der Flüsse.

Einheitspaket

Teil 9: Reliefobjekte


  • Fügen Sie dem Relief Objekte hinzu.
  • Erstellen Sie Unterstützung für Dichtestufen von Objekten.
  • Wir benutzen verschiedene Objekte im Level.
  • Wir mischen drei verschiedene Arten von Objekten.

In diesem Teil werden wir über das Hinzufügen von Objekten zum Relief sprechen. Wir werden Objekte wie Gebäude und Bäume erstellen.


Konflikt zwischen Wäldern, Ackerland und Urbanisierung.

Unterstützung für Objekte hinzufügen


Obwohl die Form des Reliefs Variationen aufweist, passiert nichts darauf. Es ist ein lebloses Land. Um ihm Leben einzuhauchen, müssen Sie solche Objekte hinzufügen. wie Bäume und Häuser. Diese Objekte sind nicht Teil des Reliefnetzes, sondern separate Objekte. Dies hindert uns jedoch nicht daran, sie beim Triangulieren des Geländes hinzuzufügen.

HexGridChunkEs ist mir egal, wie das Netz funktioniert. Er befiehlt einfach einem seiner Kinder, HexMeshein Dreieck oder ein Viereck hinzuzufügen. Ebenso kann es ein untergeordnetes Element haben, das Objekte darauf platziert.

Objekt-Manager


Lassen Sie uns eine Komponente erstellen HexFeatureManager, die Objekte innerhalb eines Fragments behandelt. Wir verwenden das gleiche Schema , das und in HexMesh- ihm Methoden geben Clear, Applyund AddFeature. Da das Objekt irgendwo platziert werden muss, AddFeatureerhält die Methode den Positionsparameter.

Wir werden mit der Implementierungsbeschaffung beginnen, die bisher nichts bewirken wird.

using UnityEngine;
public class HexFeatureManager : MonoBehaviour {
	public void Clear () {}
	public void Apply () {}
	public void AddFeature (Vector3 position) {}
}

Jetzt können wir einen Link zu einer solchen Komponente in hinzufügen HexGridChunk. Dann können Sie es sowie alle untergeordneten Elemente in den Triangulationsprozess einbeziehenHexMesh .

	public HexFeatureManager features;
	public void Triangulate () {
		terrain.Clear();
		rivers.Clear();
		roads.Clear();
		water.Clear();
		waterShore.Clear();
		estuaries.Clear();
		features.Clear();
		for (int i = 0; i < cells.Length; i++) {
			Triangulate(cells[i]);
		}
		terrain.Apply();
		rivers.Apply();
		roads.Apply();
		water.Apply();
		waterShore.Apply();
		estuaries.Apply();
		features.Apply();
	}

Beginnen wir mit dem Platzieren eines Objekts in der Mitte jeder Zelle.

	void Triangulate (HexCell cell) {
		for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) {
			Triangulate(d, cell);
		}
		features.AddFeature(cell.Position);
	}

Jetzt brauchen wir einen echten Objektmanager. Fügen Sie dem Hex Grid Chunk- Fertighaus ein weiteres Kind hinzu und geben Sie ihm eine Komponente HexFeatureManager. Dann können Sie ein Fragment daran anschließen.




Objektmanager zu Fragment-Fertighaus hinzugefügt.

Fertige Objekte


Welches Geländeobjekt werden wir erstellen? Für den ersten Test ist der Würfel durchaus geeignet. Erstellen Sie beispielsweise einen ausreichend großen Würfel mit einer Skala (3, 3, 3) und verwandeln Sie ihn in einen Fertigbau. Erstellen Sie auch ein Material dafür. Ich habe das Standardmaterial mit roter Farbe verwendet. Entfernen Sie es aus dem Collider, da wir es nicht brauchen.


Fertighauswürfel.

Objektmanager benötigen einen Link zu diesem Fertighaus. Fügen Sie diesen hinzu HexFeatureManagerund verbinden Sie sie anschließend. Da das Platzieren eines Objekts den Zugriff auf die Transformationskomponente erfordert, verwenden wir es als Referenztyp.

	public Transform featurePrefab;


Objektmanager mit Fertigteil.

Instanzen von Objekten erstellen


Die Struktur ist fertig und wir können beginnen, Reliefobjekte hinzuzufügen! Erstellen Sie einfach eine Instanz des Fertighauses HexFeatureManager.AddFeatureund legen Sie dessen Position fest.

	public void AddFeature (Vector3 position) {
		Transform instance = Instantiate(featurePrefab);
		instance.localPosition = position;
	}


Instanzen von Reliefobjekten.

Ab diesem Zeitpunkt wird das Relief mit Würfeln gefüllt. Zumindest die oberen Hälften der Würfel, da der lokale Ursprungspunkt für das Würfelnetz in Unity in der Mitte des Würfels liegt und der untere Teil unter der Oberfläche des Reliefs liegt. Um die Würfel auf der Topographie zu platzieren, müssen wir sie auf die Hälfte ihrer Höhe bewegen.

	public void AddFeature (Vector3 position) {
		Transform instance = Instantiate(featurePrefab);
		position.y += instance.localScale.y * 0.5f;
		instance.localPosition = position;
	}


Würfel auf der Oberfläche des Reliefs.

Was ist, wenn wir ein anderes Netz verwenden?
Такой подход применим только для стандартного куба. Если мы используем собственные меши, то лучше будет создавать их так, чтобы их локальная точка начала координат находилась внизу. Тогда не придётся вообще изменять их позицию.

Natürlich sind unsere Zellen verzerrt, also müssen wir die Position von Objekten verzerren. So werden wir die perfekte Wiederholbarkeit des Gitters los.

		instance.localPosition = HexMetrics.Perturb(position);


Verzerrte Positionen von Objekten.

Zerstörung von Reliefobjekten


Mit jeder Aktualisierung des Fragments erstellen wir neue Reliefobjekte. Dies bedeutet, dass wir immer mehr Objekte an denselben Positionen erstellen. Um Duplikate zu vermeiden, müssen wir beim Aufräumen eines Fragments alte Objekte entfernen.

Der schnellste Weg, dies zu tun, besteht darin, ein Containerspielobjekt zu erstellen und alle Reliefobjekte in seine untergeordneten Objekte umzuwandeln. Wenn Clearwir dann anrufen, werden wir diesen Container zerstören und einen neuen erstellen. Der Container selbst wird ein Kind seines Managers sein.

	Transform container;
	public void Clear () {
		if (container) {
			Destroy(container.gameObject);
		}
		container = new GameObject("Features Container").transform;
		container.SetParent(transform, false);
	}
	…
	public void AddFeature (Vector3 position) {
		Transform instance = Instantiate(featurePrefab);
		position.y += instance.localScale.y * 0.5f;
		instance.localPosition = HexMetrics.Perturb(position);
		instance.SetParent(container, false);
	}

Wahrscheinlich jedes Mal ineffizient, um Reliefobjekte zu erschaffen und zu zerstören.
Да, кажется, что это так. Но пока нас это волновать не должно. Сначала нам нужно правильно разместить объекты. Разобравшись с этим, мы увидим, что такие действия являются узким местом, поэтому подумаем об эффективности. Именно тогда мы можем прийти и к использованию метода HexFeatureManager.Apply. Но оставим это для будущего туториала. К счастью, всё не так плохо, потому что мы разделили рельеф на фрагменты.

Einheitspaket

Platzierung von Reliefobjekten


Im Moment platzieren wir Objekte in der Mitte jeder Zelle. Für leere Zellen sieht das normal aus, aber auf Zellen mit Flüssen und Straßen sowie mit Wasser überflutet scheint es seltsam.


Objekte befinden sich überall.

Überprüfen Sie daher vor dem Platzieren des Objekts, HexGridChunk.Triangulateob die Zelle leer ist.

		if (!cell.IsUnderwater && !cell.HasRiver && !cell.HasRoads) {
			features.AddFeature(cell.Position);
		}


Begrenzte Unterkunft.

Ein Objekt pro Richtung


Nur ein Objekt pro Zelle ist nicht zu viel. Es gibt viel Platz für jede Menge Gegenstände. Daher fügen wir ein zusätzliches Objekt in die Mitte jedes der sechs Dreiecke der Zelle ein, dh eines pro Richtung.

Wir werden dies in einer anderen Methode tun, Triangulatewenn wir wissen, dass sich kein Fluss in der Zelle befindet. Wir müssen noch prüfen, ob wir unter Wasser sind und ob es eine Straße in der Zelle gibt. In diesem Fall interessieren uns jedoch nur die Straßen, die in die aktuelle Richtung führen.

	void Triangulate (HexDirection direction, HexCell cell) {
		…
		if (cell.HasRiver) {
			…
		}
		else {
			TriangulateWithoutRiver(direction, cell, center, e);
			if (!cell.IsUnderwater && !cell.HasRoadThroughEdge(direction)) {
				features.AddFeature((center + e.v1 + e.v5) * (1f / 3f));
			}
		}
		…
	}


Viele Einrichtungen, aber nicht in der Nähe der Flüsse.

Dies schafft viel mehr Objekte! Sie erscheinen neben den Straßen, aber sie meiden immer noch die Flüsse. Um Objekte entlang der Flüsse zu platzieren, können wir sie auch hinzufügen TriangulateAdjacentToRiver. Aber auch hier nur, wenn das Dreieck nicht unter Wasser ist und keine Straße auf ihm ist.

	void TriangulateAdjacentToRiver (
		HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e
	) {
		…
		if (!cell.IsUnderwater && !cell.HasRoadThroughEdge(direction)) {
			features.AddFeature((center + e.v1 + e.v5) * (1f / 3f));
		}
	}


Gegenstände tauchten neben den Flüssen auf.

Können so viele Objekte gerendert werden?
Большое количество объектов создаёт множество вызовов отрисовки, но здесь помогает dynamic batching движка Unity. Так как объекты малы, их меши должны иметь всего несколько вершин. Это позволяет объединить многие из них в один batch. Но если это окажется «узким местом», то придётся поработать с ними в будущем. Также можно использовать instancing, который при работе со множеством мелких мешей сравним с dynamic batching.

Einheitspaket

Vielzahl von Objekten


Alle unsere Reliefobjekte haben die gleiche Ausrichtung, was völlig unnatürlich aussieht. Lassen Sie uns jedem von ihnen eine zufällige Wendung geben.

	public void AddFeature (Vector3 position) {
		Transform instance = Instantiate(featurePrefab);
		position.y += instance.localScale.y * 0.5f;
		instance.localPosition = HexMetrics.Perturb(position);
		instance.localRotation = Quaternion.Euler(0f, 360f * Random.value, 0f);
		instance.SetParent(container, false);
	}


Zufällige Runden.

Das Ergebnis wird also viel vielfältiger. Leider erhalten Objekte bei jeder Aktualisierung eines Fragments eine neue zufällige Rotation. Das Bearbeiten von Zellen sollte die Objekte in der Nachbarschaft nicht verändern, daher benötigen wir einen anderen Ansatz.

Wir haben eine Geräuschstruktur, die immer gleich ist. Diese Textur enthält jedoch Perlins Gradientenrauschen und ist lokal abgestimmt. Das ist es, was wir brauchen, wenn wir die Positionen der Eckpunkte in den Zellen verzerren. Die Kurven müssen aber nicht koordiniert werden. Alle Windungen sollten gleich wahrscheinlich und gemischt sein. Aus diesem Grund benötigen wir eine Textur mit Zufallswerten, die keine Gradienten aufweisen und ohne bilineare Filterung abgetastet werden können. Im Wesentlichen handelt es sich hierbei um ein Hash-Gitter, das die Grundlage für Gradientenrauschen bildet.

Erstellen einer Hash-Tabelle


Wir können eine Hash-Tabelle aus einem Array von Float-Werten erstellen und diese einmal mit zufälligen Werten füllen. Aus diesem Grund brauchen wir überhaupt keine Textur. Fügen wir es hinzu HexMetrics. Eine Größe von 256 mal 256 reicht für eine ausreichende Variabilität aus.

	public const int hashGridSize = 256;
	static float[] hashGrid;
	public static void InitializeHashGrid () {
		hashGrid = new float[hashGridSize * hashGridSize];
		for (int i = 0; i < hashGrid.Length; i++) {
			hashGrid[i] = Random.value;
		}
	}

Zufällige Werte werden durch eine mathematische Formel generiert, die immer die gleichen Ergebnisse liefert. Die resultierende Sequenz hängt von der Anzahl der Startwerte ab, die standardmäßig dem aktuellen Zeitwert entspricht. Deshalb erhalten wir in jeder Spielsitzung unterschiedliche Ergebnisse.

Um die Neuerstellung immer identischer Objekte sicherzustellen, müssen wir der Initialisierungsmethode den Seed-Parameter hinzufügen.

	public static void InitializeHashGrid (int seed) {
		hashGrid = new float[hashGridSize * hashGridSize];
		Random.InitState(seed);
		for (int i = 0; i < hashGrid.Length; i++) {
			hashGrid[i] = Random.value;
		}
	}

Nachdem wir den Zufallszahlenstrom initialisiert haben, erhalten wir immer die gleiche Reihenfolge. Daher sind scheinbar zufällige Ereignisse, die nach der Kartengenerierung auftreten, auch immer dieselben. Wir können dies vermeiden, indem wir den Zustand des Zufallszahlengenerators beibehalten, bevor er initialisiert wird. Nach Abschluss der Arbeiten können wir ihn nach dem alten Zustand fragen.

		Random.State currentState = Random.state;
		Random.InitState(seed);
		for (int i = 0; i < hashGrid.Length; i++) {
			hashGrid[i] = Random.value;
		}
		Random.state = currentState;

Die Hash-Tabelle wird HexGridgleichzeitig mit der Zuweisung einer Rauschtextur initialisiert . Das heißt, in den Methoden HexGrid.Startund HexGrid.Awake. Lassen Sie es uns so machen, dass Werte nicht öfter als nötig generiert werden.

	public int seed;
	void Awake () {
		HexMetrics.noiseSource = noiseSource;
		HexMetrics.InitializeHashGrid(seed);
		…
	}
	void OnEnable () {
		if (!HexMetrics.noiseSource) {
			HexMetrics.noiseSource = noiseSource;
			HexMetrics.InitializeHashGrid(seed);
		}
	}

Die gemeinsame Startvariable ermöglicht es uns, den Startwert für die Karte zu wählen. Jeder Wert wird ausreichen. Ich habe 1234 gewählt.


Auswahl des Samens.

Hash-Tabelle verwenden


Fügen Sie zur Verwendung der Hash-Tabelle die HexMetricsStichprobenmethode hinzu. Wie es SampleNoiseverwendet es die xz-Position, um den Wert zu erhalten. Der Hash-Index wird gefunden, indem die Koordinaten auf ganzzahlige Werte begrenzt werden und dann der Rest der ganzzahligen Division durch die Größe der Tabelle erhalten wird.

	public static float SampleHashGrid (Vector3 position) {
		int x = (int)position.x % hashGridSize;
		int z = (int)position.z % hashGridSize;
		return hashGrid[x + z * hashGridSize];
	}

Was macht %?
Это оператор модуля, он вычисляет остаток от деления, в нашем случае — целочисленного деления. Например, ряд −4, −3, −2, −1, 0, 1, 2, 3, 4 modulo 3 превращается в −1, 0, −2, −1, 0, 1, 2, 0, 1.

Dies funktioniert für positive Koordinaten, aber nicht für negative, da für solche Zahlen der Rest negativ ist. Wir können dies beheben, indem wir negative Ergebnisse durch Hinzufügen einer Tabellengröße korrigieren.

		int x = (int)position.x % hashGridSize;
		if (x < 0) {
			x += hashGridSize;
		}
		int z = (int)position.z % hashGridSize;
		if (z < 0) {
			z += hashGridSize;
		}

Jetzt schaffen wir für jede quadratische Einheit unseren eigenen Wert. In der Realität brauchen wir jedoch keine solche Dichtetabelle. Objekte sind weiter voneinander entfernt. Wir können die Tabelle dehnen, indem wir die Skalierung der Position reduzieren, bevor wir den Index berechnen. Wir benötigen nur einen eindeutigen Wert für ein 4 x 4-Quadrat.

	public const float hashGridScale = 0.25f;
	public static float SampleHashGrid (Vector3 position) {
		int x = (int)(position.x * hashGridScale) % hashGridSize;
		if (x < 0) {
			x += hashGridSize;
		}
		int z = (int)(position.z * hashGridScale) % hashGridSize;
		if (z < 0) {
			z += hashGridSize;
		}
		return hashGrid[x + z * hashGridSize];
	}

Kehren wir zu HexFeatureManager.AddFeatureunserer neuen Hash-Tabelle zurück, um den Wert zu ermitteln. Nachdem wir es angewendet haben, um die Drehung festzulegen, bleiben die Objekte beim Bearbeiten des Reliefs fixiert.

	public void AddFeature (Vector3 position) {
		float hash = HexMetrics.SampleHashGrid(position);
		Transform instance = Instantiate(featurePrefab);
		position.y += instance.localScale.y * 0.5f;
		instance.localPosition = HexMetrics.Perturb(position);
		instance.localRotation = Quaternion.Euler(0f, 360f * hash, 0f);
		instance.SetParent(container, false);
	}

Unterbringungsschwelle


Obwohl Objekte eine andere Drehung haben, ist das Muster in ihrer Platzierung immer noch erkennbar. In jeder Zelle befinden sich sieben Objekte. Wir können diesem Schema Chaos hinzufügen, indem wir einige der Objekte willkürlich weglassen. Wie entscheiden wir, ob wir ein Objekt hinzufügen oder nicht? Natürlich testen wir einen anderen Zufallswert!

Das heißt, jetzt brauchen wir statt eines einzelnen Hash-Werts zwei. Ihre Unterstützung kann durch die Verwendung einer Hash-Tabelle anstelle einer floatVariablen als Array-Typ hinzugefügt werden Vector2. Vektoroperationen sind jedoch für Hash-Werte nicht sinnvoll. Erstellen wir daher eine spezielle Struktur für diesen Zweck. Sie wird nur zwei Float-Werte benötigen. Fügen wir eine statische Methode hinzu, um ein Paar zufälliger Werte zu erstellen.

using UnityEngine;
public struct HexHash {
	public float a, b;
	public static HexHash Create () {
		HexHash hash;
		hash.a = Random.value;
		hash.b = Random.value;
		return hash;
	}
}

Sollte es nicht serialisiert werden?
Мы храним эти структуры только в таблице хешей, которая статична, поэтому при рекомпиляции не будет сериализоваться движком Unity. Следовательно, она не обязана быть сериализуемой.

Ändern Sie es HexMetricsso, dass es die neue Struktur verwendet.

	static HexHash[] hashGrid;
	public static void InitializeHashGrid (int seed) {
		hashGrid = new HexHash[hashGridSize * hashGridSize];
		Random.State currentState = Random.state;
		Random.InitState(seed);
		for (int i = 0; i < hashGrid.Length; i++) {
			hashGrid[i] = HexHash.Create();
		}
		Random.state = currentState;
	}
	public static HexHash SampleHashGrid (Vector3 position) {
		…
	}

Hat HexFeatureManager.AddFeaturenun Zugriff auf zwei Hashwerte. Lassen Sie uns zunächst entscheiden, ob ein Objekt hinzugefügt oder übersprungen werden soll. Wenn der Wert gleich oder größer als 0,5 ist, überspringen wir ihn. In diesem Fall werden wir ungefähr die Hälfte der Objekte los. Der zweite Wert wird normalerweise zur Bestimmung der Kurve verwendet.

	public void AddFeature (Vector3 position) {
		HexHash hash = HexMetrics.SampleHashGrid(position);
		if (hash.a >= 0.5f) {
			return;
		}
		Transform instance = Instantiate(featurePrefab);
		position.y += instance.localScale.y * 0.5f;
		instance.localPosition = HexMetrics.Perturb(position);
		instance.localRotation = Quaternion.Euler(0f, 360f * hash.b, 0f);
		instance.SetParent(container, false);
	}


Die Dichte der Objekte wird um 50% reduziert.

Einheitspaket

Objekte zeichnen


Anstatt Objekte überall zu platzieren, lassen Sie sie sich bearbeiten. Wir werden jedoch keine separaten Objekte zeichnen, sondern jeder Zelle die Ebene der Objekte hinzufügen. Diese Ebene steuert die Wahrscheinlichkeit von Objekten in der Zelle. Standardmäßig ist der Wert Null, dh die Objekte fehlen.

Da rote Würfel auf unserem Relief nicht wie natürliche Objekte aussehen, nennen wir sie Gebäude. Sie werden die Urbanisierung repräsentieren. Lassen Sie uns den HexCellGrad der Verstädterung erhöhen.

	public int UrbanLevel {
		get {
			return urbanLevel;
		}
		set {
			if (urbanLevel != value) {
				urbanLevel = value;
				RefreshSelfOnly();
			}
		}
	}
	int urbanLevel;

Wir können es so machen, dass der Urbanisierungsgrad für die Unterwasserzelle Null ist, aber dies ist nicht notwendig, wir überspringen bereits die Erstellung von Unterwasserobjekten. Und vielleicht werden wir irgendwann Wasserobjekte der Urbanisierung wie Docks und Unterwasserstrukturen hinzufügen.

Dichteregler


Um den Verstädterungsgrad zu ändern, werden wir HexMapEditoreinen weiteren Schieberegler zur Unterstützung hinzufügen .

	int activeUrbanLevel;
	…
	bool applyUrbanLevel;
	…
	public void SetApplyUrbanLevel (bool toggle) {
		applyUrbanLevel = toggle;
	}
	public void SetUrbanLevel (float level) {
		activeUrbanLevel = (int)level;
	}
	void EditCell (HexCell cell) {
		if (cell) {
			…
			if (applyWaterLevel) {
				cell.WaterLevel = activeWaterLevel;
			}
			if (applyUrbanLevel) {
				cell.UrbanLevel = activeUrbanLevel;
			}
			if (riverMode == OptionalToggle.No) {
				cell.RemoveRiver();
			}
			…
		}
	}

Fügen Sie der Benutzeroberfläche einen weiteren Schieberegler hinzu und verbinden Sie ihn mit den entsprechenden Methoden. Ich werde ein neues Bedienfeld auf der rechten Seite des Bildschirms platzieren, um ein Überlaufen des linken Bedienfelds zu vermeiden.

Wie viele Level werden wir brauchen? Schauen wir uns vier an, die Null, Niedrig, Mittel und Hohe Dichte bezeichnen.



Schieberegler für die Urbanisierung.

Schwellenwertänderung


Jetzt, da wir eine Urbanisierungsebene haben, müssen wir diese verwenden, um zu bestimmen, ob Objekte platziert werden sollen. Dazu müssen wir den Urbanisierungsgrad als zusätzlichen Parameter in hinzufügen HexFeatureManager.AddFeature. Lassen Sie uns noch einen Schritt weitergehen und die Zelle selbst passieren. In Zukunft werden wir uns wohler fühlen.

Am schnellsten können Sie den Urbanisierungsgrad ausnutzen, indem Sie ihn mit 0,25 multiplizieren und den Wert als neuen Schwellenwert für das Überspringen von Objekten verwenden. Aus diesem Grund steigt die Wahrscheinlichkeit eines Objekts mit jedem Level um 25%.

	public void AddFeature (HexCell cell, Vector3 position) {
		HexHash hash = HexMetrics.SampleHashGrid(position);
		if (hash.a >= cell.UrbanLevel * 0.25f) {
			return;
		}
		…
	}

Übergeben Sie die Zellen an, damit es funktioniert HexGridChunk.

	void Triangulate (HexCell cell) {
		…
		if (!cell.IsUnderwater && !cell.HasRiver && !cell.HasRoads) {
			features.AddFeature(cell,  cell.Position);
		}
	}
	void Triangulate (HexDirection direction, HexCell cell) {
		…
			if (!cell.IsUnderwater && !cell.HasRoadThroughEdge(direction)) {
				features.AddFeature(cell, (center + e.v1 + e.v5) * (1f / 3f));
			}
		…
	}
	…
	void TriangulateAdjacentToRiver (
		HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e
	) {
		…
		if (!cell.IsUnderwater && !cell.HasRoadThroughEdge(direction)) {
			features.AddFeature(cell, (center + e.v1 + e.v5) * (1f / 3f));
		}
	}


Zeichnungsdichte der Urbanisierung.

Einheitspaket

Mehrere Fertighäuser mit Geländeobjekten


Die Unterschiede in der Wahrscheinlichkeit des Auftretens von Objekten reichen nicht aus, um eine klare Trennung zwischen niedrigem und hohem Verstädterungsgrad zu erreichen. In einigen Zellen wird es einfach mehr oder weniger als die erwartete Anzahl von Gebäuden geben. Wir können den Unterschied deutlicher machen, indem wir für jedes Level ein eigenes Fertighaus verwenden.

Lassen Sie uns das Feld featurePrefabin loswerden HexFeatureManagerund es durch ein Array für die Fertighäuser der Urbanisierung ersetzen. Um das entsprechende Fertighaus zu erhalten, subtrahieren wir eins vom Urbanisierungsgrad und verwenden den Wert als Index.

<del>//	public Transform featurePrefab;</del>
	public Transform[] urbanPrefabs;
	public void AddFeature (HexCell cell, Vector3 position) {
		…
		Transform instance = Instantiate(urbanPrefabs[cell.UrbanLevel - 1]);
		…
	}

Wir erstellen zwei Duplikate des Fertigteils des Objekts, benennen sie um und ändern sie so, dass sie drei verschiedene Urbanisierungsebenen kennzeichnen. Stufe 1 ist eine niedrige Dichte, daher verwenden wir einen Würfel mit einer Kantenlänge, die eine Hütte bezeichnet. Ich werde das Fertigteil-Level 2 auf (1,5, 2, 1,5) skalieren, damit es wie ein zweistöckiges Gebäude aussieht. Für hohe Gebäude der Stufe 3 habe ich die Skala (2, 5, 2) verwendet.



Verwendung verschiedener Fertighäuser für jede Urbanisierungsebene.

Fertiggerichte mischen


Wir sind nicht verpflichtet, uns auf die strikte Aufteilung der Gebäudetypen zu beschränken. Sie können sie ein wenig verwechseln, wie es in der realen Welt passiert. Anstelle eines Schwellenwerts pro Ebene verwenden wir drei, einen für jeden Gebäudetyp.

Auf Stufe 1 verwenden wir in 40% der Fälle Fesseln. Es wird hier überhaupt keine anderen Gebäude geben. Für das Level verwenden wir drei Werte (0.4, 0, 0).

Auf Stufe 2 ersetzen wir die Hütten durch mehr Gebäude und erhöhen die Wahrscheinlichkeit für zusätzliche Hütten um 20%. Hohe Gebäude reichen nicht aus. Das heißt, wir verwenden die drei Schwellenwerte (0,2, 0,4, 0).

Auf Stufe 3 ersetzen wir mittelgroße Gebäude durch hohe, ersetzen wieder Hütten und addieren eine weitere Wahrscheinlichkeit von 20% für Hütten. Die Schwellenwerte sind gleich (0,2, 0,2, 0,4).

Das heißt, die Idee ist, dass wir mit zunehmender Urbanisierung vorhandene Gebäude aufrüsten und neue an leeren Orten hinzufügen. Um ein bestehendes Gebäude zu löschen, müssen wir dieselben Hash-Wert-Intervalle verwenden. Wenn es sich bei den Hashes zwischen 0 und 0,4 auf Ebene 1 um Hütten handelte, werden auf Ebene 3 durch denselben Abstand hohe Gebäude erstellt. Auf Stufe 3 sollten hohe Gebäude mit Hashwerten im Bereich von 0 bis 0,4, zweistöckige Gebäude im Bereich von 0,4 bis 0,6 und Hütten im Bereich von 0,6 bis 0,8 erstellt werden. Wenn Sie sie vom höchsten zum niedrigsten Wert prüfen, können Sie dies mit Hilfe von dreifachen Schwellenwerten (0,4, 0,6, 0,8) tun. Die Schwellenwerte von Stufe 2 werden dann (0, 0,4, 0,6) und die Schwellenwerte von Stufe 1 werden (0, 0, 0,4).

Speichern wir diese Schwellenwerte inHexMetricsals Sammlung von Arrays mit einer Methode, mit der Sie Schwellenwerte für eine bestimmte Ebene abrufen können. Da wir uns nur für Ebenen mit Objekten interessieren, ignorieren wir Ebene 0.

	static float[][] featureThresholds = {
		new float[] {0.0f, 0.0f, 0.4f},
		new float[] {0.0f, 0.4f, 0.6f},
		new float[] {0.4f, 0.6f, 0.8f}
	};
	public static float[] GetFeatureThresholds (int level) {
		return featureThresholds[level];
	}

HexFeatureManagerFügen Sie als Nächstes die Methode hinzu, die den Level- und den Hash-Wert verwendet, um die Fertigteile auszuwählen. Wenn der Pegel größer als Null ist, erhalten wir die Schwellenwerte unter Verwendung des um eins verringerten Pegels. Dann durchlaufen wir die Schwellenwerte, bis einer den Wert des Hash überschreitet. Dies bedeutet, dass wir das Fertighaus gefunden haben. Wenn nicht gefunden, wird null zurückgegeben.

	Transform PickPrefab (int level, float hash) {
		if (level > 0) {
			float[] thresholds = HexMetrics.GetFeatureThresholds(level - 1);
			for (int i = 0; i < thresholds.Length; i++) {
				if (hash < thresholds[i]) {
					return urbanPrefabs[i];
				}
			}
		}
		return null;
	}

Bei diesem Ansatz muss die Reihenfolge der Referenzen auf Fertighäuser geändert werden, damit sie von einer hohen zu einer niedrigen Dichte wechseln.


Invertierte Fertigteilreihenfolge.

Verwenden wir unsere neue Methode AddFeature, um das Fertigteil auszuwählen. Wenn wir es nicht erhalten, überspringen Sie das Objekt. Andernfalls erstellen Sie eine Instanz davon und fahren Sie wie zuvor fort.

	public void AddFeature (HexCell cell, Vector3 position) {
		HexHash hash = HexMetrics.SampleHashGrid(position);
//		if (hash.a >= cell.UrbanLevel * 0.25f) {
//			return;
//		}
//		Transform instance = Instantiate(urbanPrefabs[cell.UrbanLevel - 1]);
		Transform prefab = PickPrefab(cell.UrbanLevel, hash.a);
		if (!prefab) {
			return;
		}
		Transform instance = Instantiate(prefab);
		position.y += instance.localScale.y * 0.5f;
		instance.localPosition = HexMetrics.Perturb(position);
		instance.localRotation = Quaternion.Euler(0f, 360f * hash.b, 0f);
		instance.SetParent(container, false);
	}


Fertiggerichte mischen.

Level-Variationen


Jetzt haben wir gut gemischte Gebäude, aber bisher gibt es nur drei davon. Wir können die Variabilität weiter erhöhen, indem wir eine Sammlung von Fertigteilen an jede Urbanisierungsdichte binden. Danach ist es möglich, eine davon zufällig auszuwählen. Dies erfordert einen neuen Zufallswert. Fügen Sie also den dritten hinzu HexHash.

	public float a, b, c;
	public static HexHash Create () {
		HexHash hash;
		hash.a = Random.value;
		hash.b = Random.value;
		hash.c = Random.value;
		return hash;
	}

Verwandeln wir uns HexFeatureManager.urbanPrefabsin ein Array von Arrays und fügen der Methode einen PickPrefabParameter hinzu choice. Verwenden Sie diese Option, um den Index des integrierten Arrays auszuwählen. Multiplizieren Sie ihn mit der Länge dieses Arrays und konvertieren Sie ihn in eine Ganzzahl.

	public Transform[][] urbanPrefabs;
	…
	Transform PickPrefab (int level, float hash, float choice) {
		if (level > 0) {
			float[] thresholds = HexMetrics.GetFeatureThresholds(level - 1);
			for (int i = 0; i < thresholds.Length; i++) {
				if (hash < thresholds[i]) {
					return urbanPrefabs[i][(int)(choice * urbanPrefabs[i].Length)];
				}
			}
		}
		return null;
	}

Begründen wir unsere Wahl mit dem Wert des zweiten Hash (B). Dann biegt man von B nach C ab.

	public void AddFeature (HexCell cell, Vector3 position) {
		HexHash hash = HexMetrics.SampleHashGrid(position);
		Transform prefab = PickPrefab(cell.UrbanLevel, hash.a, hash.b);
		if (!prefab) {
			return;
		}
		Transform instance = Instantiate(prefab);
		position.y += instance.localScale.y * 0.5f;
		instance.localPosition = HexMetrics.Perturb(position);
		instance.localRotation = Quaternion.Euler(0f, 360f * hash.c, 0f);
		instance.SetParent(container, false);
	}

Bevor wir fortfahren, müssen wir überlegen, was der Random.valueWert von 1 bewirken kann. Aus diesem Grund kann der Array-Index darüber hinausgehen. Um dies zu verhindern, ändern wir die Skala der Hash-Werte geringfügig. Wir skalieren sie einfach alle, um uns keine Sorgen um das, was wir verwenden, zu machen.

	public static HexHash Create () {
		HexHash hash;
		hash.a = Random.value * 0.999f;
		hash.b = Random.value * 0.999f;
		hash.c = Random.value * 0.999f;
		return hash;
	}

Leider werden die Arrays nicht im Inspector angezeigt. Daher können wir sie nicht konfigurieren. Um diese Einschränkung zu umgehen, erstellen Sie eine serialisierbare Struktur, in der wir das eingebettete Array einkapseln. Geben Sie eine Methode an, die von Choice in einen Array-Index konvertiert und das Prefab zurückgibt.

using UnityEngine;
[System.Serializable]
public struct HexFeatureCollection {
	public Transform[] prefabs;
	public Transform Pick (float choice) {
		return prefabs[(int)(choice * prefabs.Length)];
	}
}

Wir verwenden HexFeatureManageranstelle der eingebauten Arrays ein Array solcher Sammlungen.

//	public Transform[][] urbanPrefabs;
	public HexFeatureCollection[] urbanCollections;
	…
	Transform PickPrefab (int level, float hash, float choice) {
		if (level > 0) {
			float[] thresholds = HexMetrics.GetFeatureThresholds(level - 1);
			for (int i = 0; i < thresholds.Length; i++) {
				if (hash < thresholds[i]) {
					return urbanCollections[i].Pick(choice);
				}
			}
		}
		return null;
	}

Jetzt können wir jeder Dichtestufe mehrere Gebäude zuordnen. Da sie unabhängig sind, müssen wir nicht den gleichen Betrag pro Level verwenden. Ich habe nur zwei Optionen pro Ebene verwendet und zu jeder eine längere niedrigere Version hinzugefügt. Ich habe für sie die Skalen (3.5, 3, 2), (2.75, 1.5, 1.5) und (1.75, 1, 1) gewählt.



Zwei Gebäudetypen pro Dichteebene.

Einheitspaket

Mehrere Arten von Objekten


Mit dem bestehenden Schema können wir recht anständige städtische Strukturen schaffen. Das Relief kann aber nicht nur Gebäude enthalten. Wie wäre es mit Farmen oder Pflanzen? Lassen Sie uns die HexCellEbenen und für sie hinzufügen . Sie schließen sich nicht gegenseitig aus und können gemischt werden.

	public int FarmLevel {
		get {
			return farmLevel;
		}
		set {
			if (farmLevel != value) {
				farmLevel = value;
				RefreshSelfOnly();
			}
		}
	}
	public int PlantLevel {
		get {
			return plantLevel;
		}
		set {
			if (plantLevel != value) {
				plantLevel = value;
				RefreshSelfOnly();
			}
		}
	}
	int urbanLevel, farmLevel, plantLevel;

Dies erfordert natürlich die Unterstützung von HexMapEditorzwei zusätzlichen Schiebereglern.

	int activeUrbanLevel, activeFarmLevel, activePlantLevel;
	bool applyUrbanLevel, applyFarmLevel, applyPlantLevel;
	…
	public void SetApplyFarmLevel (bool toggle) {
		applyFarmLevel = toggle;
	}
	public void SetFarmLevel (float level) {
		activeFarmLevel = (int)level;
	}
	public void SetApplyPlantLevel (bool toggle) {
		applyPlantLevel = toggle;
	}
	public void SetPlantLevel (float level) {
		activePlantLevel = (int)level;
	}
	…
	void EditCell (HexCell cell) {
		if (cell) {
			…
			if (applyUrbanLevel) {
				cell.UrbanLevel = activeUrbanLevel;
			}
			if (applyFarmLevel) {
				cell.FarmLevel = activeFarmLevel;
			}
			if (applyPlantLevel) {
				cell.PlantLevel = activePlantLevel;
			}
			…
		}
	}

Fügen Sie sie der Benutzeroberfläche hinzu.


Drei Schieberegler.

Es werden auch zusätzliche Sammlungen benötigt HexFeatureManager.

	public HexFeatureCollection[]
		urbanCollections, farmCollections, plantCollections;


Drei Sammlungen von Reliefobjekten.

Ich habe sowohl Fertighäuser für die Dichte als auch für Farmen und Pflanzen sowie für Sammlungen von Gebäuden erstellt. Für alle habe ich Würfel verwendet. Betriebe haben ein hellgrünes Material, Pflanzen - dunkelgrün.

Ich habe Binderwürfel mit einer Höhe von 0,1 Einheiten hergestellt, um quadratische Zuteilungen von landwirtschaftlichem Land zu kennzeichnen. Ich wählte (2,5, 0,1, 2,5) und (3,5, 0,1, 2) als Skalen mit hoher Dichte. Im Durchschnitt haben Standorte eine Fläche von 1,75 und eine Größe von 2,5 mal 1,25. Die geringe Dichte erhielt eine Fläche von 1 und eine Größe von 1,5 mal 0,75.

Vorgefertigte Pflanzen bezeichnen hohe Bäume und große Sträucher. Fertighäuser mit hoher Dichte sind die größten (1,25, 4,5, 1,25) und (1,5, 3, 1,5). Die Durchschnittsskalen sind (0,75, 3, 0,75) und (1, 1,5, 1). Die kleinsten Pflanzen haben Dimensionen (0,5, 1,5, 0,5) und (0,75, 1, 0,75).

Auswahl der Reliefobjekte


Jeder Objekttyp muss einen eigenen Hash-Wert erhalten, damit sie unterschiedliche Erstellungsmuster haben und gemischt werden können. Fügen Sie HexHashzwei zusätzliche Werte hinzu.

	public float a, b, c, d, e;
	public static HexHash Create () {
		HexHash hash;
		hash.a = Random.value * 0.999f;
		hash.b = Random.value * 0.999f;
		hash.c = Random.value * 0.999f;
		hash.d = Random.value * 0.999f;
		hash.e = Random.value * 0.999f;
		return hash;
	}

Jetzt müssen Sie HexFeatureManager.PickPrefabmit verschiedenen Sammlungen arbeiten. Fügen Sie einen Parameter hinzu, um den Prozess zu vereinfachen. Wir werden auch den von der Version des ausgewählten Prefab verwendeten Hash in D und den Hash in E ändern.

	Transform PickPrefab (
		HexFeatureCollection[] collection,
		int level, float hash, float choice
	) {
		if (level > 0) {
			float[] thresholds = HexMetrics.GetFeatureThresholds(level - 1);
			for (int i = 0; i < thresholds.Length; i++) {
				if (hash < thresholds[i]) {
					return collection[i].Pick(choice);
				}
			}
		}
		return null;
	}
	public void AddFeature (HexCell cell, Vector3 position) {
		HexHash hash = HexMetrics.SampleHashGrid(position);
		Transform prefab = PickPrefab(
			urbanCollections, cell.UrbanLevel, hash.a, hash.d
		);
		…
		instance.localRotation = Quaternion.Euler(0f, 360f * hash.e, 0f);
		instance.SetParent(container, false);
	}

Derzeit AddFeaturewählt Fertighaus Urbanisierung. Das ist normal, wir brauchen mehr Optionen. Fügen Sie daher ein weiteres Fertighaus aus der Farm hinzu. Als Hash-Wert verwenden wir B. Die Wahl der Option ist wieder D.

		Transform prefab = PickPrefab(
			urbanCollections, cell.UrbanLevel, hash.a, hash.d
		);
		Transform otherPrefab = PickPrefab(
			farmCollections, cell.FarmLevel, hash.b, hash.d
		);
		if (!prefab) {
			return;
		}

Welche Kopie des Fertigteils erstellen wir als Ergebnis? Wenn sich herausstellt, dass einer von ihnen null ist, liegt die Wahl auf der Hand. Wenn jedoch beide existieren, müssen wir eine Entscheidung treffen. Fügen wir einfach den Prefab mit dem niedrigsten Hashwert hinzu.

		Transform otherPrefab = PickPrefab(
			farmCollections, cell.FarmLevel, hash.b, hash.d
		);
		if (prefab) {
			if (otherPrefab && hash.b < hash.a) {
				prefab = otherPrefab;
			}
		}
		else if (otherPrefab) {
			prefab = otherPrefab;
		}
		else {
			return;
		}


Stadt und Land mischen.
Machen Sie dasselbe mit den Pflanzen, indem Sie den Wert des Hashs C verwenden.

		if (prefab) {
			if (otherPrefab && hash.b < hash.a) {
				prefab = otherPrefab;
			}
		}
		else if (otherPrefab) {
			prefab = otherPrefab;
		}
		otherPrefab = PickPrefab(
			plantCollections, cell.PlantLevel, hash.c, hash.d
		);
		if (prefab) {
			if (otherPrefab && hash.c < hash.a) {
				prefab = otherPrefab;
			}
		}
		else if (otherPrefab) {
			prefab = otherPrefab;
		}
		else {
			return;
		}

Wir können den Code jedoch nicht einfach kopieren. Wenn wir ein ländliches Objekt anstelle eines städtischen Objekts auswählen, müssen wir den Pflanzen-Hash mit dem Farm-Hash vergleichen, nicht mit dem städtischen. Daher müssen wir den Hash verfolgen, den wir ausgewählt und damit verglichen haben.

		float usedHash = hash.a;
		if (prefab) {
			if (otherPrefab && hash.b < hash.a) {
				prefab = otherPrefab;
				usedHash = hash.b;
			}
		}
		else if (otherPrefab) {
			prefab = otherPrefab;
			usedHash = hash.b;
		}
		otherPrefab = PickPrefab(
			plantCollections, cell.PlantLevel, hash.c, hash.d
		);
		if (prefab) {
			if (otherPrefab && hash.c < usedHash) {
				prefab = otherPrefab;
			}
		}
		else if (otherPrefab) {
			prefab = otherPrefab;
		}
		else {
			return;
		}


Eine Mischung aus städtischen, ländlichen und pflanzlichen Objekten.

Einheitspaket

Teil 10: Wände


  • Wir schließen die Zelle ein.
  • Wir bauen die Wände entlang der Ränder der Zellen.
  • Erlaubt durch Flüsse und Straßen zu fahren.
  • Vermeiden Sie Wasser und verbinden Sie sich mit Klippen.

In diesem Abschnitt fügen wir Wände zwischen die Zellen ein.


Nichts ist einladender als die hohe Mauer.

Bearbeiten der Wände


Um die Wände zu stützen, müssen wir wissen, wo wir sie platzieren müssen. Wir werden sie zwischen Zellen entlang der sie verbindenden Kanten platzieren. Da sich die vorhandenen Objekte im zentralen Teil der Zellen befinden, brauchen wir uns keine Sorgen zu machen, dass die Wände durch sie hindurchgehen.


Die Wände befinden sich an den Rändern.

Die Wände sind Reliefobjekte, obwohl sie groß sind. Wie andere Objekte werden sie nicht direkt bearbeitet. Stattdessen werden wir die Zellen ändern. Wir werden keine getrennten Segmente der Wände haben, aber wir werden uns bemühen, die Zellen als Ganzes einzuschließen.

Ummauertes Grundstück


Fügen Sie der HexCellEigenschaft hinzu, um umzäunte Zellen zu unterstützen Walled. Dies ist ein einfacher Schalter. Da sich die Wände zwischen den Zellen befinden, müssen sowohl die bearbeiteten Zellen als auch ihre Nachbarn aktualisiert werden.

	public bool Walled {
		get {
			return walled;
		}
		set {
			if (walled != value) {
				walled = value;
				Refresh();
			}
		}
	}
	bool walled;

Editor wechseln


Um den Status des "Fencing" von Zellen zu ändern, müssen wir HexMapEditorden Switch unterstützen. Aus diesem Grund werden wir ein weiteres Feld OptionalToggleund eine Methode zum Festlegen hinzufügen .

	OptionalToggle riverMode, roadMode, walledMode;
	…
	public void SetWalledMode (int mode) {
		walledMode = (OptionalToggle)mode;
	}

Im Gegensatz zu Flüssen und Straßen verlaufen Mauern nicht von Zelle zu Zelle, sondern liegen zwischen ihnen. Daher müssen wir nicht über das Ziehen nachdenken. Wenn der Wandschalter aktiv ist, legen wir einfach den Status des "Fencing" der aktuellen Zelle basierend auf dem Status dieses Schalters fest.

	void EditCell (HexCell cell) {
		if (cell) {
			…
			if (roadMode == OptionalToggle.No) {
				cell.RemoveRoads();
			}
			if (walledMode != OptionalToggle.Ignore) {
				cell.Walled = walledMode == OptionalToggle.Yes;
			}
			if (isDrag) {
				…
			}
		}
	}

Wir duplizieren eines der vorherigen Elemente der UI-Schalter und ändern sie so, dass sie den Status des "Fencing" steuern. Ich werde sie zusammen mit anderen Objekten in das UI-Panel einfügen.


Schalter "Zaun".

Einheitspaket

Wände bauen


Da die Wände den Konturen der Zellen folgen, sollten sie keine dauerhafte Form haben. Daher können wir für sie nicht einfach ein Fertighaus verwenden, wie es bei anderen Reliefobjekten der Fall war. Stattdessen müssen wir ein Netz bauen, wie wir es mit dem Relief getan haben. Dies bedeutet, dass unser vorgefertigtes Fragment ein weiteres Kind benötigt HexMesh. Duplizieren Sie eines der verbleibenden untergeordneten Netze, und werfen Sie mit den neuen Walls- Objekten Schatten. Sie benötigen nur Scheitelpunkte und Dreiecke, daher sollten alle Optionen HexMeshdeaktiviert sein.



Vorgefertigte Wände für Kinder.

Es wird logisch sein, dass die Wände ein städtisches Objekt sind, deshalb habe ich für sie das rote Baumaterial verwendet.

Wandverwaltung


Da die Wände Reliefobjekte sind, müssen sie sich damit auseinandersetzen HexFeatureManager. Daher geben wir dem Manager der Feature-Objekte einen Verweis auf das Walls- Objekt und rufen die Methoden Clearund auf Apply.

	public HexMesh walls;
	…
	public void Clear () {
		…
		walls.Clear();
	}
	public void Apply () {
		walls.Apply();
	}


Mit dem Verwalter der Reliefobjekte verbundene Wände.

Sollten Wände nicht ein Kind von Features sein?
Мы можем упорядочить объекты и таким образом, но это необязательно. Так как в окне иерархии отображаются только непосредственные дочерние элементы корневых объектов префабов, я предпочитаю оставить Walls непосредственным дочерним элементом Hex Grid Chunk.

Jetzt müssen wir dem Manager eine Methode hinzufügen, um ihm Wände hinzuzufügen. Da sich die Wände entlang der Kanten zwischen den Zellen befinden, muss er die entsprechenden Kanten der Kanten und Zellen kennen. HexGridChunkIch werde es TriangulateConnectionzum Zeitpunkt der Triangulation der Zelle und eines ihrer Nachbarn durchrufen. Unter diesem Gesichtspunkt befindet sich die aktuelle Zelle auf der nahen Seite der Wand und die andere auf der anderen Seite.

	public void AddWall (
		EdgeVertices near, HexCell nearCell,
		EdgeVertices far, HexCell farCell
	) {
	}

Rufen Sie diese neue Methode HexGridChunk.TriangulateConnectionnach Abschluss aller anderen Verbindungsarbeiten und unmittelbar vor dem Übergang zum Eckdreieck auf. Wir stellen dem Verwalter der Reliefobjekte die Möglichkeit zur Verfügung, selbst zu entscheiden, wo sich die Wand tatsächlich befinden soll.

	void TriangulateConnection (
		HexDirection direction, HexCell cell, EdgeVertices e1
	) {
		…
		if (cell.GetEdgeType(direction) == HexEdgeType.Slope) {
			…
		}
		else {
			…
		}
		features.AddWall(e1, cell, e2, neighbor);
		HexCell nextNeighbor = cell.GetNeighbor(direction.Next());
		if (direction <= HexDirection.E && nextNeighbor != null) {
			…
		}
	}

Baue ein Wandsegment


Die gesamte Wand windet sich durch mehrere Zellränder. Jede Kante enthält nur ein Wandelement. Aus Sicht der nahen Zelle beginnt das Segment auf der linken Seite der Kante und endet auf der rechten Seite. Fügen wir einer HexFeatureManagerseparaten Methode vier Eckpunkte in den Ecken der Kanten hinzu.

	void AddWallSegment (
		Vector3 nearLeft, Vector3 farLeft, Vector3 nearRight, Vector3 farRight
	) {
	}


Nahe und ferne Seite.

AddWallkann diese Methode mit den ersten und letzten Eckpunkten der Kanten aufrufen. Wände sollten jedoch nur hinzugefügt werden, wenn eine Verbindung zwischen einer umzäunten Zelle und einer nicht umzäunten Zelle besteht. Es spielt keine Rolle, welche der Zellen sich innerhalb und welche außerhalb befindet, nur der Unterschied in ihren Zuständen wird berücksichtigt.

	public void AddWall (
		EdgeVertices near, HexCell nearCell,
		EdgeVertices far, HexCell farCell
	) {
		if (nearCell.Walled != farCell.Walled) {
			AddWallSegment(near.v1, far.v1, near.v5, far.v5);
		}
	}

Das einfachste Mauersegment ist ein Quad, das in der Mitte des Randes steht. Wir werden seine unteren Spitzen finden, die zur Mitte der nächsten zu den fernen Spitzen interpolieren.

	void AddWallSegment (
		Vector3 nearLeft, Vector3 farLeft, Vector3 nearRight, Vector3 farRight
	) {
		Vector3 left = Vector3.Lerp(nearLeft, farLeft, 0.5f);
		Vector3 right = Vector3.Lerp(nearRight, farRight, 0.5f);
	}

Wie hoch sollte die Wand sein? Lassen Sie uns seine Höhe einstellen HexMetrics. Ich habe sie auf die Größe einer Ebene der Zellenhöhe gebracht.

	public const float wallHeight = 3f;

HexFeatureManager.AddWallSegmentMit dieser Höhe können Sie den dritten und vierten Eckpunkt des Quadrates positionieren und dem Netz hinzufügen walls.

		Vector3 left = Vector3.Lerp(nearLeft, farLeft, 0.5f);
		Vector3 right = Vector3.Lerp(nearRight, farRight, 0.5f);
		Vector3 v1, v2, v3, v4;
		v1 = v3 = left;
		v2 = v4 = right;
		v3.y = v4.y = left.y + HexMetrics.wallHeight;
		walls.AddQuad(v1, v2, v3, v4);

Jetzt können wir die Wände bearbeiten und sie werden als Quad-Bänder angezeigt. Wir werden jedoch keine durchgehende Wand sehen. Jedes Quad ist nur von einer Seite sichtbar. Sein Gesicht ist auf die Zelle gerichtet, aus der es hinzugefügt wurde.


Einseitige Quadwände.

Wir können dieses Problem schnell lösen, indem wir ein zweites Quad hinzufügen, das in die andere Richtung zeigt.

		walls.AddQuad(v1, v2, v3, v4);
		walls.AddQuad(v2, v1, v4, v3);


Doppelseitige Wände.

Jetzt sind alle Wände vollständig sichtbar, aber in den Ecken der Zellen, wo es drei Zellen gibt, sind alle gleich, bis es Löcher gibt. Wir werden sie später ausfüllen.

Dicke Wände


Obwohl die Wände bereits auf beiden Seiten sichtbar sind, haben sie keine Dicke. Tatsächlich sind die Wände dünn wie Papier und in einem bestimmten Winkel fast unsichtbar. Lassen Sie uns sie fest machen, indem wir Stärke addieren. Lassen Sie uns ihre Dicke einstellen HexMetrics. Ich habe den Wert von 0,75 Einheiten gewählt, er erschien mir passend.

	public const float wallThickness = 0.75f;

Um zwei Wände dick zu machen, müssen Sie die beiden Quad-Seiten voneinander trennen. Sie müssen sich in entgegengesetzte Richtungen bewegen. Eine Seite sollte sich zur nahen Kante bewegen, die andere - zur fernen. Der Verschiebungsvektor dafür ist gleich far - near, aber um den oberen Teil der Wand flach zu halten, müssen wir der Komponente Y den Wert 0 zuweisen.

Da dies sowohl für die linke als auch für die rechte Seite des Wandsegments erforderlich ist, fügen wir die HexMetricsMethode zur Berechnung dieses Verschiebungsvektors hinzu.

	public static Vector3 WallThicknessOffset (Vector3 near, Vector3 far) {
		Vector3 offset;
		offset.x = far.x - near.x;
		offset.y = 0f;
		offset.z = far.z - near.z;
		return offset;
	}

Damit die Wand in der Mitte der Kante bleibt, muss der aktuelle Bewegungsabstand entlang dieses Vektors für jede Seite gleich der Hälfte der Dicke sein. Um sicherzustellen, dass wir uns wirklich auf die gewünschte Distanz bewegt haben, normalisieren wir den Verschiebungsvektor, bevor wir ihn skalieren.

		return offset.normalized * (wallThickness * 0.5f);

Wir verwenden diese Methode HexFeatureManager.AddWallSegment, um die Position des Quad zu ändern. Da der Verschiebungsvektor von der nächsten zur fernen Zelle geht, subtrahieren wir ihn vom nahen Quad und addieren ihn zur fernen.

		Vector3 left = Vector3.Lerp(nearLeft, farLeft, 0.5f);
		Vector3 right = Vector3.Lerp(nearRight, farRight, 0.5f);
		Vector3 leftThicknessOffset =
			HexMetrics.WallThicknessOffset(nearLeft, farLeft);
		Vector3 rightThicknessOffset =
			HexMetrics.WallThicknessOffset(nearRight, farRight);
		Vector3 v1, v2, v3, v4;
		v1 = v3 = left - leftThicknessOffset;
		v2 = v4 = right - rightThicknessOffset;
		v3.y = v4.y = left.y + HexMetrics.wallHeight;
		walls.AddQuad(v1, v2, v3, v4);
		v1 = v3 = left + leftThicknessOffset;
		v2 = v4 = right + rightThicknessOffset;
		v3.y = v4.y = left.y + HexMetrics.wallHeight;
		walls.AddQuad(v2, v1, v4, v3);


Wände mit Versatz.

Quads werden jetzt verschoben, obwohl dies nicht ganz auffällt.

Ist die Wandstärke gleich?
Она была бы одинакова, если бы все векторы смещения «близко-далеко» смотрели бы в одном направлении. Так как стены искривляются вокруг ячеек, этого естественно не происходит. Векторы направлены противоположно или друг на друга. В результате основание сегмента стены становится трапецоидом, а не прямоугольником. Поэтому стены будут тоньше, чем настроенная нами толщина. Кроме того, из-за искажения ячеек угол между векторами варьируется, что приводит к неравномерной толщине. Мы решим эту проблему в будущем.

Wandoberseiten


Um die Wandstärke von oben sichtbar zu machen, müssen wir oben an der Wand ein Viereck anbringen. Der einfachste Weg, dies zu tun, besteht darin, die beiden oberen Eckpunkte des ersten Quadrates zu speichern und sie mit den beiden oberen Eckpunkten des zweiten Quadrates zu verbinden.

		Vector3 v1, v2, v3, v4;
		v1 = v3 = left - leftThicknessOffset;
		v2 = v4 = right - rightThicknessOffset;
		v3.y = v4.y = left.y + HexMetrics.wallHeight;
		walls.AddQuad(v1, v2, v3, v4);
		Vector3 t1 = v3, t2 = v4;
		v1 = v3 = left + leftThicknessOffset;
		v2 = v4 = right + rightThicknessOffset;
		v3.y = v4.y = left.y + HexMetrics.wallHeight;
		walls.AddQuad(v2, v1, v4, v3);
		walls.AddQuad(t1, t2, v3, v4);


Wände mit Tops.

Kurvenfahrt


Wir haben immer noch Löcher in den Ecken der Zellen. Um sie zu füllen, müssen wir dem dreieckigen Bereich zwischen den Zellen ein Segment hinzufügen. Jede Ecke verbindet drei Zellen. Jede Zelle kann eine Wand haben oder nicht. Das heißt, acht Konfigurationen sind möglich.


Konfigurationswinkel.

Wir platzieren Wände nur zwischen Zellen mit unterschiedlichen „umzäunten“ Bedingungen. Dies reduziert die Anzahl der Konfigurationen auf sechs. In jeder von ihnen befindet sich eine der Zellen innerhalb der gekrümmten Wände. Nehmen wir diese Zelle als Bezugspunkt, um den die Wand gebogen ist. Aus der Sicht dieser Zelle beginnt die Wand mit einer Kante, die der linken Zelle gemeinsam ist, und endet mit einer Kante, die der rechten Zelle gemeinsam ist.


Rollen von Zellen.

Das heißt, wir müssen eine Methode erstellen, AddWallSegmentderen Parameter die drei Eckpunkte des Winkels sind. Obwohl wir Code schreiben können, um dieses Segment zu triangulieren, ist dies tatsächlich ein Sonderfall der Methode AddWallSegment. Der Drehpunkt spielt die Rolle beider nahen Eckpunkte.

	void AddWallSegment (
		Vector3 pivot, HexCell pivotCell,
		Vector3 left, HexCell leftCell,
		Vector3 right, HexCell rightCell
	) {
		AddWallSegment(pivot, left, pivot, right);
	}

Erstellen Sie als Nächstes eine Variante der Methode AddWallfür die drei Eckpunkte des Winkels und ihre Zellen. Die Aufgabe dieser Methode ist es, den Winkel zu bestimmen, der der Referenzpunkt ist, falls vorhanden. Daher muss er alle acht möglichen Konfigurationen berücksichtigen und AddWallSegmentsechs davon fordern .

	public void AddWall (
		Vector3 c1, HexCell cell1,
		Vector3 c2, HexCell cell2,
		Vector3 c3, HexCell cell3
	) {
		if (cell1.Walled) {
			if (cell2.Walled) {
				if (!cell3.Walled) {
					AddWallSegment(c3, cell3, c1, cell1, c2, cell2);
				}
			}
			else if (cell3.Walled) {
				AddWallSegment(c2, cell2, c3, cell3, c1, cell1);
			}
			else {
				AddWallSegment(c1, cell1, c2, cell2, c3, cell3);
			}
		}
		else if (cell2.Walled) {
			if (cell3.Walled) {
				AddWallSegment(c1, cell1, c2, cell2, c3, cell3);
			}
			else {
				AddWallSegment(c2, cell2, c3, cell3, c1, cell1);
			}
		}
		else if (cell3.Walled) {
			AddWallSegment(c3, cell3, c1, cell1, c2, cell2);
		}
	}

Rufen Sie diese Methode am Ende auf, um Ecksegmente hinzuzufügen HexGridChunk.TriangulateCorner.

	void TriangulateCorner (
		Vector3 bottom, HexCell bottomCell,
		Vector3 left, HexCell leftCell,
		Vector3 right, HexCell rightCell
	) {
		…
		features.AddWall(bottom, bottomCell, left, leftCell, right, rightCell);
	}


Die Wände sind mit Ecken versehen, aber es gibt noch Löcher.

Schließe das Loch


Es gibt noch Löcher in den Wänden, weil die Höhe der Wandsegmente nicht konstant ist. Während die Segmente entlang der Kanten eine konstante Höhe haben, liegen die Eckensegmente zwischen zwei verschiedenen Kanten. Da jede Kante eine eigene Höhe haben kann, werden an den Ecken Löcher angezeigt.

Um dies zu beheben, ändern Sie AddWallSegmentes so, dass die Y-Koordinaten des linken und rechten oberen Scheitelpunkts separat gespeichert werden.

		float leftTop = left.y + HexMetrics.wallHeight;
		float rightTop = right.y + HexMetrics.wallHeight;
		Vector3 v1, v2, v3, v4;
		v1 = v3 = left - leftThicknessOffset;
		v2 = v4 = right - rightThicknessOffset;
		v3.y = leftTop;
		v4.y = rightTop;
		walls.AddQuad(v1, v2, v3, v4);
		Vector3 t1 = v3, t2 = v4;
		v1 = v3 = left + leftThicknessOffset;
		v2 = v4 = right + rightThicknessOffset;
		v3.y = leftTop;
		v4.y = rightTop;
		walls.AddQuad(v2, v1, v4, v3);


Geschlossene Wände.

Die Wände sind jetzt geschlossen, aber Sie sehen wahrscheinlich immer noch Löcher in den Schatten der Wand. Dies wird durch die Einstellung " Normal Bias" der gerichteten Schatten verursacht. Wenn es größer als Null ist, bewegen sich die Dreiecke der Schatten werfenden Objekte entlang der Oberflächennormalen. Dies vermeidet eine Selbstbeschattung, erzeugt jedoch gleichzeitig Löcher in Fällen, in denen die Dreiecke in verschiedene Richtungen schauen. Gleichzeitig können Löcher in den Schatten dünner Geometrie erzeugt werden, beispielsweise in unseren Wänden.

Sie können diese Schattenartefakte beseitigen, indem Sie die normale Verzerrung auf Null reduzieren. Oder ändern Sie den Wandmodus des Mesh-Renderers " Schattenwurf" in " Beidseitig" . Dadurch wird das Schattenwurfobjekt veranlasst, beide Seiten jeder zu rendernden Dreieckwand zu rendern, wodurch alle Löcher geschlossen werden.


Es gibt keine Schattenlöcher mehr.

Einheitspaket

Wände auf Felsvorsprüngen


Solange unsere Wände ziemlich gerade sind. Für flaches Gelände ist dies überhaupt nicht schlecht, aber es sieht seltsam aus, wenn die Wände mit den Vorsprüngen zusammenfallen. Dies ist der Fall, wenn zwischen den Zellen auf den gegenüberliegenden Seiten der Wand ein Höhenunterschied besteht.


Gerade Wände auf Vorsprüngen.

Kante folgen


Anstatt ein Segment für die gesamte Kante zu erstellen, erstellen wir eines für jeden Teil des Kantenstreifens. Wir können dies tun, indem wir viermal AddWallSegmentin der AddWallRippenversion aufrufen .

	public void AddWall (
		EdgeVertices near, HexCell nearCell,
		EdgeVertices far, HexCell farCell
	) {
		if (nearCell.Walled != farCell.Walled) {
			AddWallSegment(near.v1, far.v1, near.v2, far.v2);
			AddWallSegment(near.v2, far.v2, near.v3, far.v3);
			AddWallSegment(near.v3, far.v3, near.v4, far.v4);
			AddWallSegment(near.v4, far.v4, near.v5, far.v5);
		}
	}


Geschwungene Wände.

Die Wände replizieren jetzt die Formen verzerrter Kanten. In Kombination mit den Leisten sieht es viel besser aus. Außerdem werden auf flachem Gelände interessantere Wände geschaffen.

Wände auf den Boden legen


Wenn Sie sich die Wände auf den Felsvorsprüngen ansehen, können Sie ein Problem feststellen. Die Wände hängen über dem Boden! Dies gilt für geneigte flache Kanten, ist aber normalerweise nicht so auffällig.


Wände hängen in der Luft.

Um das Problem zu lösen, müssen wir die Wände senken. Am einfachsten ist es, die gesamte Wand so abzusenken, dass ihre Oberseite flach bleibt. Gleichzeitig taucht ein Teil der Wand auf der Oberseite leicht in das Relief ein, aber das passt zu uns.

Um die Wand zu senken, müssen wir bestimmen, welche der Seiten niedriger ist - nah oder fern. Wir können einfach die Höhe der untersten Seite verwenden, müssen aber nicht so tief runter. Sie können die Y-Koordinate von niedrig nach hoch mit einem Versatz von knapp 0,5 interpolieren. Da die Wände nur gelegentlich höher werden als die untere Stufe der Stufe, können wir die vertikale Stufe der Stufe als Versatz verwenden. Unterschiedliche Wandstärken der Leistenanordnung können einen unterschiedlichen Versatz erfordern.


Abgesenkte Wand

Fügen wir der HexMetricsMethode hinzu WallLerp, die diese Interpolation ausführt, und mitteln wir die X- und Z-Koordinaten der nahen und fernen Scheitelpunkte. Es basiert auf Methode TerraceLerp.

	public const float wallElevationOffset = verticalTerraceStepSize;
	…
	public static Vector3 WallLerp (Vector3 near, Vector3 far) {
		near.x += (far.x - near.x) * 0.5f;
		near.z += (far.z - near.z) * 0.5f;
		float v =
			near.y < far.y ? wallElevationOffset : (1f - wallElevationOffset);
		near.y += (far.y - near.y) * v;
		return near;
	}

HexFeatureManagerVerwenden wir diese Methode, um die linken und rechten Eckpunkte zu definieren.

	void AddWallSegment (
		Vector3 nearLeft, Vector3 farLeft, Vector3 nearRight, Vector3 farRight
	) {
		Vector3 left = HexMetrics.WallLerp(nearLeft, farLeft);
		Vector3 right = HexMetrics.WallLerp(nearRight, farRight);
		…
	}


Wände stehen auf dem Boden.

Wandverzerrung ändern


Jetzt passen unsere Wände gut zu Höhenunterschieden. Sie entsprechen jedoch immer noch nicht vollständig den verzerrten Kanten, obwohl sie sich in ihrer Nähe befinden. Es geschah, weil wir zuerst die Wandseiten definieren und sie dann verzerren. Da diese Scheitelpunkte irgendwo zwischen den Scheitelpunkten der nahen und der fernen Kante liegen, ist ihre Verzerrung geringfügig unterschiedlich.

Die Tatsache, dass die Wände den Kanten nicht genau folgen, ist kein Problem. Die Verzerrung der Spitzen der Wand ändert jedoch ansonsten die relativ gleichmäßige Dicke. Wenn wir Wände auf der Basis verzerrter Eckpunkte platzieren und dann unverzerrte Quadrate hinzufügen, sollte ihre Dicke nicht stark variieren.

	void AddWallSegment (
		Vector3 nearLeft, Vector3 farLeft, Vector3 nearRight, Vector3 farRight
	) {
		nearLeft = HexMetrics.Perturb(nearLeft);
		farLeft = HexMetrics.Perturb(farLeft);
		nearRight = HexMetrics.Perturb(nearRight);
		farRight = HexMetrics.Perturb(farRight);
		…
		walls.AddQuadUnperturbed(v1, v2, v3, v4);
		…
		walls.AddQuadUnperturbed(v2, v1, v4, v3);
		walls.AddQuadUnperturbed(t1, t2, v3, v4);
	}


Unverdorbene Mauerspitzen.

Dank dieser Vorgehensweise folgen die Wände den Rippen nicht mehr so ​​genau wie früher. Im Gegenzug werden sie jedoch weniger gebrochen und weisen eine konstantere Dicke auf.


Konstantere Wandstärke.

Einheitspaket

Löcher in den Wänden


Wir haben vorerst die Möglichkeit ignoriert, dass ein Fluss oder eine Straße eine Mauer überqueren kann. In diesem Fall müssen wir ein Loch in die Mauer bohren, durch das ein Fluss oder eine Straße führen kann.

Dazu fügen wir AddWallzwei Boolesche Parameter hinzu, die angeben, ob ein Fluss oder eine Straße durch eine Kante verläuft. Obwohl wir sie unterschiedlich behandeln können, löschen wir in beiden Fällen einfach die beiden mittleren Segmente.

	public void AddWall (
		EdgeVertices near, HexCell nearCell,
		EdgeVertices far, HexCell farCell,
		bool hasRiver, bool hasRoad
	) {
		if (nearCell.Walled != farCell.Walled) {
			AddWallSegment(near.v1, far.v1, near.v2, far.v2);
			if (hasRiver || hasRoad) {
				// Leave a gap.
			}
			else {
				AddWallSegment(near.v2, far.v2, near.v3, far.v3);
				AddWallSegment(near.v3, far.v3, near.v4, far.v4);
			}
			AddWallSegment(near.v4, far.v4, near.v5, far.v5);
		}
	}

Jetzt HexGridChunk.TriangulateConnectionmüssen die notwendigen Daten bereitgestellt werden. Da er bereits die gleichen Informationen benötigte, zwischenspeichern wir sie in Booleschen Variablen und schreiben die Aufrufe der entsprechenden Methoden nur einmal.

	void TriangulateConnection (
		HexDirection direction, HexCell cell, EdgeVertices e1
	) {
		…
		bool hasRiver = cell.HasRiverThroughEdge(direction);
		bool hasRoad = cell.HasRoadThroughEdge(direction);
		if (hasRiver) {
			…
		}
		if (cell.GetEdgeType(direction) == HexEdgeType.Slope) {
			TriangulateEdgeTerraces(e1, cell, e2, neighbor, hasRoad);
		}
		else {
			TriangulateEdgeStrip(e1, cell.Color, e2, neighbor.Color, hasRoad);
		}
		features.AddWall(e1, cell, e2, neighbor, hasRiver, hasRoad);
		…
	}


Löcher in den Wänden für die Passage von Flüssen und Straßen.

Wir decken die Wände ab


Diese neuen Öffnungen vervollständigen die Wände. Wir müssen diese Endpunkte mit Quad schließen, damit es unmöglich ist, durch die Seiten der Wände zu schauen. Legen Sie dazu in der HexFeatureManagerMethode an AddWallCap. Es funktioniert so AddWallSegment, aber es benötigt nur ein Paar nah-fern Spitzen. Fügen Sie ein Quad hinzu, das von der nahen zur entfernten Seite der Wand verläuft.

	void AddWallCap (Vector3 near, Vector3 far) {
		near = HexMetrics.Perturb(near);
		far = HexMetrics.Perturb(far);
		Vector3 center = HexMetrics.WallLerp(near, far);
		Vector3 thickness = HexMetrics.WallThicknessOffset(near, far);
		Vector3 v1, v2, v3, v4;
		v1 = v3 = center - thickness;
		v2 = v4 = center + thickness;
		v3.y = v4.y = center.y + HexMetrics.wallHeight;
		walls.AddQuadUnperturbed(v1, v2, v3, v4);
	}

Wenn sich AddWallherausstellt, dass wir ein Loch benötigen, fügen wir einen Deckel zwischen das zweite und vierte Paar der Scheitelpunktkanten ein. Für das vierte Scheitelpunktpaar müssen Sie die Ausrichtung ändern, andernfalls schaut die Quad-Fläche nach innen.

	public void AddWall (
		EdgeVertices near, HexCell nearCell,
		EdgeVertices far, HexCell farCell,
		bool hasRiver, bool hasRoad
	) {
		if (nearCell.Walled != farCell.Walled) {
			AddWallSegment(near.v1, far.v1, near.v2, far.v2);
			if (hasRiver || hasRoad) {
				AddWallCap(near.v2, far.v2);
				AddWallCap(far.v4, near.v4);
			}
			…
		}
	}


Geschlossene Löcher in den Wänden.

Was ist mit den Löchern an den Rändern der Karte?
Можно предпринять меры, чтобы закрыть стены и там. Лично я избегаю размещения стен на краях карты. Обычно нам не нужно, чтобы игровой процесс проходил слишком близко к краю.

Einheitspaket

Vermeiden Sie Klippen und Wasser


Schauen wir uns zum Schluss die Kanten an, die Schnitte oder Wasser enthalten. Da die Klippen im Wesentlichen große Mauern sind, wäre es unlogisch, eine zusätzliche Mauer darauf zu platzieren. Außerdem wird es schlecht aussehen. Unterwasserwände sind ebenso unlogisch wie die Beschränkung auf die Küstenwände.


Wände an den Klippen und im Wasser.

Mit zusätzlichen Überprüfungen können wir Wände von diesen unerwünschten Kanten entfernen AddWall. Die Wand kann nicht unter Wasser sein, und die gemeinsame Kante mit ihr kann kein Abgrund sein.

	public void AddWall (
		EdgeVertices near, HexCell nearCell,
		EdgeVertices far, HexCell farCell,
		bool hasRiver, bool hasRoad
	) {
		if (
			nearCell.Walled != farCell.Walled &&
			!nearCell.IsUnderwater && !farCell.IsUnderwater &&
			nearCell.GetEdgeType(farCell) != HexEdgeType.Cliff
		) {
			…
		}
	}


Störende Wände entlang der Kanten wurden entfernt, die Ecken blieben jedoch erhalten.

Entfernen Sie die Ecken der Wände


Es ist etwas aufwendiger, unnötige Eckensegmente zu entfernen. Der einfachste Fall ist, wenn sich die Stützzelle unter Wasser befindet. Dies stellt sicher, dass keine Wandsegmente zum Verbinden vorhanden sind.

	void AddWallSegment (
		Vector3 pivot, HexCell pivotCell,
		Vector3 left, HexCell leftCell,
		Vector3 right, HexCell rightCell
	) {
		if (pivotCell.IsUnderwater) {
			return;
		}
		AddWallSegment(pivot, left, pivot, right);
	}


Es gibt keine Unterwasserunterstützungszellen mehr.

Nun müssen wir uns die beiden anderen Zellen ansehen. Befindet sich einer von ihnen unter Wasser oder ist er über eine Klippe mit der Stützzelle verbunden, befindet sich entlang dieser Rippe keine Wand. Wenn dies für mindestens eine Seite zutrifft, sollte sich in dieser Ecke kein Wandsegment befinden.

Stellen Sie separat fest, ob es eine linke oder eine rechte Wand gibt. Wir platzieren die Ergebnisse in booleschen Variablen, um die Arbeit mit ihnen zu erleichtern.

		if (pivotCell.IsUnderwater) {
			return;
		}
		bool hasLeftWall = !leftCell.IsUnderwater &&
			pivotCell.GetEdgeType(leftCell) != HexEdgeType.Cliff;
		bool hasRighWall = !rightCell.IsUnderwater &&
			pivotCell.GetEdgeType(rightCell) != HexEdgeType.Cliff;
		if (hasLeftWall && hasRighWall) {
			AddWallSegment(pivot, left, pivot, right);
		}


Alle störenden Ecken entfernt.

Schließen Sie die Ecken


Befindet sich weder am linken noch am rechten Rand eine Wand, ist die Arbeit beendet. Befindet sich die Wand jedoch nur in einer Richtung, bedeutet dies, dass sich ein weiteres Loch in der Wand befindet. Daher müssen Sie es schließen.

		if (hasLeftWall) {
			if (hasRighWall) {
				AddWallSegment(pivot, left, pivot, right);
			}
			else {
				AddWallCap(pivot, left);
			}
		}
		else if (hasRighWall) {
			AddWallCap(right, pivot);
		}


Mauern schließen.

Die Verbindung der Mauern mit Klippen


In einer Situation sehen die Wände unvollkommen aus. Wenn die Wand den Boden der Klippe erreicht, endet sie. Da die Klippen jedoch nicht vollständig vertikal sind, entsteht ein schmales Loch zwischen der Wand und dem Rand der Klippe. An der Spitze der Klippe tritt dieses Problem nicht auf.


Löcher zwischen den Wänden und den Rändern der Klippen.

Es wäre viel besser, wenn die Mauer bis an den Rand der Klippe reichte. Dazu fügen wir ein weiteres Mauersegment zwischen dem aktuellen Ende der Mauer und der Ecke oben auf der Klippe hinzu. Da der größte Teil dieses Abschnitts in der Klippe verborgen ist, können wir die Wandstärke in der Klippe auf Null reduzieren. Es genügt uns also, einen Keil zu schaffen: zwei Quadrate, die zu einem Punkt führen, und ein Dreieck darüber. Lassen Sie uns zu diesem Zweck eine Methode erstellen AddWallWedge. Dies kann durch Kopieren AddWallCapund Hinzufügen eines Keilpunkts erfolgen.

	void AddWallWedge (Vector3 near, Vector3 far, Vector3 point) {
		near = HexMetrics.Perturb(near);
		far = HexMetrics.Perturb(far);
		point = HexMetrics.Perturb(point);
		Vector3 center = HexMetrics.WallLerp(near, far);
		Vector3 thickness = HexMetrics.WallThicknessOffset(near, far);
		Vector3 v1, v2, v3, v4;
		Vector3 pointTop = point;
		point.y = center.y;
		v1 = v3 = center - thickness;
		v2 = v4 = center + thickness;
		v3.y = v4.y = pointTop.y = center.y + HexMetrics.wallHeight;
//		walls.AddQuadUnperturbed(v1, v2, v3, v4);
		walls.AddQuadUnperturbed(v1, point, v3, pointTop);
		walls.AddQuadUnperturbed(point, v2, pointTop, v4);
		walls.AddTriangleUnperturbed(pointTop, v3, v4);
	}

In AddWallSegmentden Ecken nennen wir diese Methode, wenn die Wand nur in eine Richtung verläuft und sich diese Wand auf einer niedrigeren Höhe als die andere Seite befindet. Unter diesen Bedingungen treffen wir auf den Rand einer Klippe.

		if (hasLeftWall) {
			if (hasRighWall) {
				AddWallSegment(pivot, left, pivot, right);
			}
			else if (leftCell.Elevation < rightCell.Elevation) {
				AddWallWedge(pivot, left, right);
			}
			else {
				AddWallCap(pivot, left);
			}
		}
		else if (hasRighWall) {
			if (rightCell.Elevation < leftCell.Elevation) {
				AddWallWedge(right, pivot, left);
			}
			else {
				AddWallCap(right, pivot);
			}
		}


Keilförmige Wände, die mit Klippen verbunden sind.

Einheitspaket

Teil 11: Neue Geländeobjekte


  • Fügen Sie an den Wänden des Turms.
  • Wir verbinden Straßen durch Brücken.
  • Unterstützung für große Spezialobjekte hinzufügen.


Gefüllt mit Objekten der Landschaft.

Türme an den Wänden


Im vorherigen Abschnitt haben wir Unterstützung für die Wände hinzugefügt. Dies sind einfache gerade Wandsegmente ohne sichtbare Unterschiede. Jetzt werden wir die Wände interessanter machen, indem wir ihnen Türme hinzufügen.

Um dem Relief zu entsprechen, sollten Wandsegmente prozedural erstellt werden. Für Türme ist dies nicht erforderlich, wir können das übliche Fertigteil verwenden. Da das Tower-Modell aus mehreren Objekten besteht, werden sie zu untergeordneten Elementen des Stammobjekts. Platzieren Sie sie so, dass der lokale Ursprungspunkt der Wurzel an der Basis des Turms liegt. Dank dessen können wir die Türme platzieren, ohne auf ihre Höhe achten zu müssen.

Wir können einen einfachen Turm aus zwei Würfeln mit rotem Material herstellen. Die Basis des Turms hat eine Größe von 2 bis 2 Einheiten und eine Höhe von 4 Einheiten, das heißt, es ist dicker und höher als die Wand. Über diesem Würfel platzieren wir einen Einheitswertwürfel, der die Spitze des Turms kennzeichnet. Wie alle anderen Fertighäuser erfordern diese Würfel keine Collider.




Fertigbau-Turm.

Fügen Sie einen Link zu diesem Fertighaus hinzu HexFeatureManagerund verbinden Sie es.

	public Transform wallTower;


Link zum Fertigbau des Turms.

Baue Türme


Beginnen wir mit der Platzierung der Türme in der Mitte jedes Mauersegments. Dazu erstellen wir am Ende der Methode einen Turm AddWallSegment. Ihre Position ist die Mitte des linken und rechten Punktes des Segments.

	void AddWallSegment (
		Vector3 nearLeft, Vector3 farLeft, Vector3 nearRight, Vector3 farRight
	) {
		…
		Transform towerInstance = Instantiate(wallTower);
		towerInstance.transform.localPosition = (left + right) * 0.5f;
		towerInstance.SetParent(container, false);
	}


Ein Turm für jedes Mauersegment.

Wir haben viele Türme entlang der Mauer, aber ihre Ausrichtung ändert sich nicht. Wir müssen ihre Rotation so ändern, dass sie sich an der Wand ausrichten. Da wir rechte und linke Punkte der Wand haben, wissen wir, welche Richtung richtig ist. Mit diesem Wissen können wir die Ausrichtung des Mauersegments und damit des Turms bestimmen.

Anstatt die Drehung unabhängig zu berechnen, weisen wir einfach die Transform.rightVektoreigenschaft zu. Der Unity-Code ändert die Drehung des Objekts so, dass seine lokale Richtung rechts dem übertragenen Vektor entspricht.

		Transform towerInstance = Instantiate(wallTower);
		towerInstance.transform.localPosition = (left + right) * 0.5f;
		Vector3 rightDirection = right - left;
		rightDirection.y = 0f;
		towerInstance.transform.right = rightDirection;
		towerInstance.SetParent(container, false);


Die Türme sind mit einer Mauer gesäumt.

Wie funktioniert die Zuweisung von Transform.right?
В нём используется метод Quaternion.FromToRotation для вычисления поворота. Вот код свойства.

public Vector3 right {
	get {
		return rotation * Vector3.right;
	}
	set {
		rotation = Quaternion.FromToRotation(Vector3.right, value);
	}
}

Reduzieren Sie die Anzahl der Türme


Ein Turm pro Mauersegment ist zu viel. Lassen Sie uns das Hinzufügen eines Turms optional machen, indem Sie dem AddWallSegmentBooleschen Wert einen Parameter hinzufügen . Geben Sie einen Standardwert ein false. In diesem Fall verschwinden alle Türme.

	void AddWallSegment (
		Vector3 nearLeft, Vector3 farLeft, Vector3 nearRight, Vector3 farRight,
		bool addTower = false
	) {
		…
		if (addTower) {
			Transform towerInstance = Instantiate(wallTower);
			towerInstance.transform.localPosition = (left + right) * 0.5f;
			Vector3 rightDirection = right - left;
			rightDirection.y = 0f;
			towerInstance.transform.right = rightDirection;
			towerInstance.SetParent(container, false);
		}
	}

Platzieren wir die Türme nur in den Ecken der Zellen. Infolgedessen erhalten wir weniger Türme mit ziemlich konstanten Abständen zwischen ihnen.

	void AddWallSegment (
		Vector3 pivot, HexCell pivotCell,
		Vector3 left, HexCell leftCell,
		Vector3 right, HexCell rightCell
	) {
		…
				AddWallSegment(pivot, left, pivot, right, true);
		…
	}


Die Türme sind nur in den Ecken.

Es sieht gut genug aus, aber wir brauchen möglicherweise eine weniger regelmäßige Platzierung der Türme. Wie bei anderen Reliefobjekten können wir anhand einer Hash-Tabelle entscheiden, ob ein Turm in eine Ecke gestellt werden soll. Dazu verwenden wir den Mittelpunkt des Winkels, um die Tabelle abzutasten, und vergleichen dann einen der Hash-Werte mit dem Schwellenwert der Türme.

				HexHash hash = HexMetrics.SampleHashGrid(
					(pivot + left + right) * (1f / 3f)
				);
				bool hasTower = hash.e < HexMetrics.wallTowerThreshold;
				AddWallSegment(pivot, left, pivot, right, hasTower);

Der Schwellenwert bezieht sich auf HexMetrics. Mit einem Wert von 0,5 werden Türme in der Hälfte der Fälle erstellt. Wir können jedoch Wände mit vielen Türmen erstellen oder ohne diese.

	public const float wallTowerThreshold = 0.5f;


Zufällige Türme.

Türme von Hängen entfernen


Jetzt platzieren wir den Turm, unabhängig von der Form des Reliefs. An den Hängen des Turms sehen sie jedoch unlogisch aus. Hier verlaufen die Wände schräg und können die Turmspitze durchschneiden.


Türme auf den Pisten.

Um Steigungen zu vermeiden, prüfen wir, ob sich die rechten und linken Winkelzellen auf derselben Höhe befinden. Nur in diesem Fall ist es möglich, den Turm zu platzieren.

				bool hasTower = false;
				if (leftCell.Elevation == rightCell.Elevation) {
					HexHash hash = HexMetrics.SampleHashGrid(
						(pivot + left + right) * (1f / 3f)
					);
					hasTower = hash.e < HexMetrics.wallTowerThreshold;
				}
				AddWallSegment(pivot, left, pivot, right, hasTower);


Mehr an den Wänden der Hänge der Türme.

Wir setzen Mauern und Türme auf den Boden


Obwohl wir die Wände an den Hängen meiden, kann das Relief auf beiden Seiten der Wand immer noch unterschiedliche Höhen haben. Wände können entlang von Leisten verlaufen, und Zellen gleicher Höhe können unterschiedliche vertikale Positionen haben. Aus diesem Grund kann die Basis des Turms in der Luft sein.


Türme in der Luft.

Tatsächlich können die Wände an den Hängen auch in der Luft hängen, dies ist jedoch nicht so auffällig wie bei den Türmen.


Wände in der Luft.

Dies kann korrigiert werden, indem die Basis der Mauern und Türme zum Boden gedehnt wird. Fügen Sie dazu einen Y-Versatz für die Wände in hinzu HexMetrics. Eine Einheit weniger wird ausreichen. Erhöhen Sie die Höhe der Türme um den gleichen Betrag.

	public const float wallHeight = 4f;
	public const float wallYOffset = -1f;

Ändern wir es HexMetrics.WallLerpso, dass bei der Bestimmung der Y-Koordinate der neue Versatz berücksichtigt wird.

		public static Vector3 WallLerp (Vector3 near, Vector3 far) {
		near.x += (far.x - near.x) * 0.5f;
		near.z += (far.z - near.z) * 0.5f;
		float v =
			near.y < far.y ? wallElevationOffset : (1f - wallElevationOffset);
		near.y += (far.y - near.y) * v + wallYOffset;
		return near;
	}

Wir müssen auch den Fertigbau des Turms ändern, da die Basis jetzt eine Einheit unter der Erde sein wird. Daher erhöhen wir die Höhe des Basiswürfels um eine Einheit und ändern die lokale Position der Würfel entsprechend.



Mauern und Türme auf dem Boden.

Einheitspaket

Brücken


Zu diesem Zeitpunkt haben wir Flüsse und Straßen, aber Straßen können Flüsse nicht überqueren. Es ist Zeit, Brücken hinzuzufügen.

Beginnen wir mit einem einfachen skalierten Würfel, der die Rolle des Fertigteils der Brücke spielt. Die Breite der Flüsse variiert, aber es gibt ungefähr sieben Abstandseinheiten zwischen den Mittelpunkten der Straßen auf beiden Seiten. Deshalb geben wir eine ungefähre Skala an (3, 1, 7). Fügen Sie das rote Stadtmaterial zum Fertighaus hinzu und entfernen Sie den Collider. Wie bei Türmen platzieren wir den Würfel mit der gleichen Skalierung im Wurzelobjekt. Aus diesem Grund ist die Geometrie der Brücke selbst nicht wichtig.

Fügen Sie dem Fertigbau der Brücke einen Link hinzu HexFeatureManagerund weisen Sie ihm einen Fertigbau zu.

	public Transform wallTower, bridge;


Zugewiesenes Fertigteil der Brücke.

Brückenplatzierung


Um die Brücke unterzubringen, brauchen wir eine Methode HexFeatureManager.AddBridge. Die Brücke sollte sich zwischen der Mitte des Flusses und einer der Seiten des Flusses befinden.

	public void AddBridge (Vector3 roadCenter1, Vector3 roadCenter2) {
		Transform instance = Instantiate(bridge);
		instance.localPosition = (roadCenter1 + roadCenter2) * 0.5f;
		instance.SetParent(container, false);
	}

Wir werden die unverzerrten Straßenmitten passieren, also müssen wir sie verzerren, bevor wir die Brücke platzieren.

		roadCenter1 = HexMetrics.Perturb(roadCenter1);
		roadCenter2 = HexMetrics.Perturb(roadCenter2);
		Transform instance = Instantiate(bridge);

Um die Brücke richtig auszurichten, können wir den gleichen Ansatz wie beim Drehen der Türme verwenden. In diesem Fall definieren die Straßenmitten den Vorwärtsvektor der Brücke. Da wir in derselben Zelle bleiben, ist dieser Vektor genau horizontal, sodass wir seine Y-Komponente nicht zurücksetzen müssen.

		Transform instance = Instantiate(bridge);
		instance.localPosition = (roadCenter1 + roadCenter2) * 0.5f;
		instance.forward = roadCenter2 - roadCenter1;
		instance.SetParent(container, false);

Wir legen Brücken über gerade Flüsse


Die einzigen Flusskonfigurationen, die Brücken erfordern, sind gerade und gekrümmt. Straßen können durch die Endpunkte verlaufen, und bei Zickzacklinien können Straßen nur in der Nähe sein.

Zuerst beschäftigen wir uns mit geraden Flüssen. Im Inneren befasst sich der HexGridChunk.TriangulateRoadAdjacentToRivererste Betreiber else ifmit der Lage von Straßen, die an solche Flüsse angrenzen. Deshalb werden wir hier Brücken hinzufügen.

Wir sind auf der gleichen Seite des Flusses. Das Zentrum der Straße entfernt sich vom Fluss, und dann verschiebt sich das Zentrum der Zelle. Um die Mitte der Straße auf der gegenüberliegenden Seite zu finden, müssen wir die entgegengesetzte Richtung um den gleichen Betrag verschieben. Dies muss erfolgen, bevor das Zentrum selbst gewechselt wird.

	void TriangulateRoadAdjacentToRiver (
		HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e
	) {
		…
		else if (cell.IncomingRiver == cell.OutgoingRiver.Opposite()) {
			…
			roadCenter += corner * 0.5f;
			features.AddBridge(roadCenter, center - corner * 0.5f);
			center += corner * 0.25f;
		}
		…
	}


Brücken über gerade Flüsse.

Brücken sind aufgetaucht! Aber jetzt haben wir eine Instanz von Brücken für jede Richtung, durch die der Fluss nicht fließt. Wir müssen sicherstellen, dass nur eine Instanz der Brücke in der Zelle generiert wird. Dies kann erreicht werden, indem eine Richtung relativ zum Fluss gewählt und auf dessen Basis eine Brücke erzeugt wird. Sie können eine beliebige Richtung wählen.

			roadCenter += corner * 0.5f;
			if (cell.IncomingRiver == direction.Next()) {
				features.AddBridge(roadCenter, center - corner * 0.5f);
			}
			center += corner * 0.25f;

Außerdem brauchen wir nur dann eine Brücke hinzuzufügen, wenn es auf beiden Seiten des Flusses eine Straße gibt. Im Moment sind wir sicher, dass es auf der jetzigen Seite eine Straße gibt. Daher müssen Sie prüfen, ob sich auf der anderen Seite des Flusses eine Straße befindet.

			if (cell.IncomingRiver == direction.Next() && (
				cell.HasRoadThroughEdge(direction.Next2()) ||
				cell.HasRoadThroughEdge(direction.Opposite())
			)) {
				features.AddBridge(roadCenter, center - corner * 0.5f);
			}


Brücken zwischen den Straßen auf beiden Seiten.

Brücken über geschwungene Flüsse


Brücken über gekrümmte Flüsse funktionieren auf ähnliche Weise, ihre Topologie unterscheidet sich jedoch geringfügig. Wir werden eine Brücke hinzufügen, wenn wir uns außerhalb der Kurve befinden. Dies geschieht im letzten Block else. Dabei wird die durchschnittliche Richtung verwendet, um die Mitte der Straße zu verschieben. Wir müssen diesen Versatz zweimal mit einer anderen Skala verwenden, also speichern wir ihn in einer Variablen.

	void TriangulateRoadAdjacentToRiver (
		HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e
	) {
		…
		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;
			}
			Vector3 offset = HexMetrics.GetSolidEdgeMiddle(middle);
			roadCenter += offset * 0.25f;
		}
		…
	}

Die Versatzskala an der Außenseite der Kurve beträgt 0,25 und an der Innenseite HexMetrics.innerToOuter * 0.7f. Verwenden Sie es, um die Brücke unterzubringen.

			Vector3 offset = HexMetrics.GetSolidEdgeMiddle(middle);
			roadCenter += offset * 0.25f;
			features.AddBridge(
				roadCenter,
				center - offset * (HexMetrics.innerToOuter * 0.7f)
			);


Brücken über die geschwungenen Flüsse.

Auch hier müssen wir doppelte Brücken vermeiden. Wir können dies tun, indem wir Brücken nur aus der mittleren Richtung hinzufügen.

			Vector3 offset = HexMetrics.GetSolidEdgeMiddle(middle);
			roadCenter += offset * 0.25f;
			if (direction == middle) {
				features.AddBridge(
					roadCenter,
					center - offset * (HexMetrics.innerToOuter * 0.7f)
				);
			}

Und wieder müssen Sie sicherstellen, dass sich die Straße auf der gegenüberliegenden Seite befindet.

			if (
				direction == middle &&
				cell.HasRoadThroughEdge(direction.Opposite())
			) {
				features.AddBridge(
					roadCenter,
					center - offset * (HexMetrics.innerToOuter * 0.7f)
				);
			}


Brücken zwischen den Straßen auf beiden Seiten.

Brücken skalieren


Da wir das Gelände verzerren, variiert der Abstand zwischen den Mittelpunkten der Straßen und den gegenüberliegenden Seiten des Flusses. Manchmal sind die Brücken zu kurz, manchmal zu lang.


Unterschiedliche Entfernungen, aber konstante Brückenlänge.

Obwohl wir eine Brücke mit einer Länge von sieben Einheiten erstellt haben, können wir sie so skalieren, dass sie dem tatsächlichen Abstand zwischen den Mittelpunkten der Straßen entspricht. Dies bedeutet, dass das Modell der Brücke deformiert ist. Da die Abstände nicht sehr stark variieren, kann die Verformung akzeptabler sein als Brücken, die für die Länge nicht geeignet sind.

Um die korrekte Skalierung durchzuführen, müssen wir die Anfangslänge des Brückenfertigteils kennen. Diese Länge speichern wir in HexMetrics.

	public const float bridgeDesignLength = 7f;

Jetzt können wir den Maßstab der Brückeninstanz dem Z-Wert der Entfernung zwischen den Mittelpunkten der Straßen dividiert durch die Anfangslänge zuweisen. Da die vorgefertigte Wurzel der Brücke den gleichen Maßstab hat, wird die Brücke korrekt gedehnt.

	public void AddBridge (Vector3 roadCenter1, Vector3 roadCenter2) {
		roadCenter1 = HexMetrics.Perturb(roadCenter1);
		roadCenter2 = HexMetrics.Perturb(roadCenter2);
		Transform instance = Instantiate(bridge);
		instance.localPosition = (roadCenter1 + roadCenter2) * 0.5f;
		instance.forward = roadCenter2 - roadCenter1;
		float length = Vector3.Distance(roadCenter1, roadCenter2);
		instance.localScale = new Vector3(
			1f,	1f, length * (1f / HexMetrics.bridgeDesignLength)
		);
		instance.SetParent(container, false);
	}


Brücken variabler Länge.

Brückenbau


Anstelle eines einfachen Würfels können wir ein interessanteres Modell der Brücke verwenden. Beispielsweise können Sie eine grobe Bogenbrücke aus drei skalierten und gedrehten Würfeln erstellen. Natürlich können Sie viel komplexere 3D-Modelle erstellen, einschließlich solcher mit Teilen der Straße. Beachten Sie jedoch, dass das gesamte Objekt leicht komprimiert und gedehnt wird.



Bogenbrücken unterschiedlicher Länge.

Einheitspaket

Besondere Gegenstände


Während unsere Zellen städtische, ländliche und pflanzliche Objekte enthalten können. Obwohl jede von ihnen drei Ebenen hat, sind alle Objekte im Vergleich zur Zellengröße ziemlich klein. Was ist, wenn wir ein großes Gebäude wie ein Schloss brauchen?

Fügen wir dem Relief eine spezielle Art von Objekten hinzu. Solche Objekte sind so groß, dass sie die gesamte Zelle einnehmen. Jedes dieser Objekte ist ein Unikat und benötigt einen eigenen Fertighaus. Beispielsweise kann ein einfaches Schloss aus einem zentralen Würfel und vier Ecktürmen erstellt werden. Die Skala (6, 4, 6) für den zentralen Würfel erzeugt ein ausreichend großes Schloss, das dennoch auch in eine stark deformierte Zelle passt.


Префаб замка.

Ещё одним специальным объектом может быть зиккурат, например, построенный из трёх поставленных друг на друга кубов. Для нижнего куба подойдёт масштаб (8, 2.5, 8).


Префаб зиккурата.

Особые объекты могут быть любыми, необязательно архитектурными. Например, группа массивных деревьев высотой до десяти единиц может обозначать ячейку, заполненную мегафлорой.


Префаб мегафлоры.

Добавим в HexFeatureManager массив, чтобы отслеживать эти префабы.

	public Transform[] special;

Сначала добавим в массив замок, затем зиккурат, а потом мегафлору.


Настройка особых объектов.

Делаем ячейки особыми


Теперь для HexCell требуется индекс особых объектов, определяющий тип особого объекта, если он там находится.

	int specialIndex;

Как и другим объектам рельефа, дадим ему свойство получать и задавать это значение.

	public int SpecialIndex {
		get {
			return specialIndex;
		}
		set {
			if (specialIndex != value) {
				specialIndex = value;
				RefreshSelfOnly();
			}
		}
	}

По умолчанию ячейка не содержит особого объекта. Мы обозначим это индексом 0. Добавим свойство, которое использует такой подход для определения того, является ли ячейка особой.

	public bool IsSpecial {
		get {
			return specialIndex > 0;
		}
	}

Для редактирования ячеек добавим поддержку индекса особых объектов в HexMapEditor. Он работает аналогично уровням городских, сельских и растительных объектов.

	int activeUrbanLevel, activeFarmLevel, activePlantLevel, activeSpecialIndex;
	…
	bool applyUrbanLevel, applyFarmLevel, applyPlantLevel, applySpecialIndex;
	…
	public void SetApplySpecialIndex (bool toggle) {
		applySpecialIndex = toggle;
	}
	public void SetSpecialIndex (float index) {
		activeSpecialIndex = (int)index;
	}
	…
	void EditCell (HexCell cell) {
		if (cell) {
			if (applyColor) {
				cell.Color = activeColor;
			}
			if (applyElevation) {
				cell.Elevation = activeElevation;
			}
			if (applyWaterLevel) {
				cell.WaterLevel = activeWaterLevel;
			}
			if (applySpecialIndex) {
				cell.SpecialIndex = activeSpecialIndex;
			}
			if (applyUrbanLevel) {
				cell.UrbanLevel = activeUrbanLevel;
			}
			…
		}
	}

Добавим в UI ползунок для управления особым объектом. Так как у нас три объекта, используем в ползунке интервал 0–3. Ноль будет означать отсутствие объекта, один — замок, два — зиккурат, три — мегафлору.


Ползунок особых объектов.

Добавление особых объектов


Теперь мы можем назначать ячейкам особые объекты. Чтобы они появились, нам нужно добавить в HexFeatureManager ещё один метод. Он просто создаёт экземпляр нужного особого объекта и размещает его в нужной позиции. Так как ноль обозначает отсутствие объекта, мы должны вычесть единицу из индекса особых объектов ячейки до получения доступа к массиву префабов.

	public void AddSpecialFeature (HexCell cell, Vector3 position) {
		Transform instance = Instantiate(special[cell.SpecialIndex - 1]);
		instance.localPosition = HexMetrics.Perturb(position);
		instance.SetParent(container, false);
	}

Придадим объекту произвольный поворот с помощью таблицы хешей.

	public void AddSpecialFeature (HexCell cell, Vector3 position) {
		Transform instance = Instantiate(special[cell.SpecialIndex - 1]);
		instance.localPosition = HexMetrics.Perturb(position);
		HexHash hash = HexMetrics.SampleHashGrid(position);
		instance.localRotation = Quaternion.Euler(0f, 360f * hash.e, 0f);
		instance.SetParent(container, false);
	}

При триангуляции ячейки в HexGridChunk.Triangulate проверим, содержит ли ячейка особый объект. Если да, то вызываем наш новый метод, точно так же, как AddFeature.

	void Triangulate (HexCell cell) {
		for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) {
			Triangulate(d, cell);
		}
		if (!cell.IsUnderwater && !cell.HasRiver && !cell.HasRoads) {
			features.AddFeature(cell, cell.Position);
		}
		if (cell.IsSpecial) {
			features.AddSpecialFeature(cell, cell.Position);
		}
	}


Особые объекты. Они намного больше обычных.

Избегаем рек


Так как особые объекты находятся в центрах ячеек, они не сочетаются с реками, потому что будут висеть над ними.


Объекты на реках.

Чтобы особые объекты не создавались поверх рек, изменим свойство HexCell.SpecialIndex. Будем менять индекс только тогда, когда в ячейке нет рек.

	public int SpecialIndex {
		…
		set {
			if (specialIndex != value && !HasRiver) {
				specialIndex = value;
				RefreshSelfOnly();
			}
		}
	}

Кроме того, при добавлении реки нам нужно будет избавляться от всех особых объектов. Река должна смывать их. Это можно сделать, присваивая в методе HexCell.SetOutgoingRiver индексам особых объектов значение 0.

	public void SetOutgoingRiver (HexDirection direction) {
		…
		hasOutgoingRiver = true;
		outgoingRiver = direction;
		specialIndex = 0;
		neighbor.RemoveIncomingRiver();
		neighbor.hasIncomingRiver = true;
		neighbor.incomingRiver = direction.Opposite();
		neighbor.specialIndex = 0;
		SetRoad((int)direction, false);
	}

Избегаем дорог


Как и реки, дороги тоже плохо сочетаются с особыми объектами, однако не всё так ужасно. Можно даже оставить дороги как есть. Возможно, некоторые объекты совместимы с дорогами, а другие нет. Поэтому можно сделать их зависящими от объекта. Но мы сделаем проще.


Объекты на дорогах.

В этом случае пусть особые объекты побеждают дороги. Поэтому при изменении индекса особых объектов мы также будем удалять из ячейки все дороги.

	public int SpecialIndex {
		…
		set {
			if (specialIndex != value && !HasRiver) {
				specialIndex = value;
				RemoveRoads();
				RefreshSelfOnly();
			}
		}
	}

Что, если мы удалим особый объект?
Если мы присвоим индексу значение 0, то это значит, что в ячейке уже есть особый объект. Поэтому в ней нет никаких дорог и другой подход использовать не нужно.

Кроме того, это значит, что при добавлении дорог нам придётся выполнять дополнительные проверки. Мы будем добавлять дороги только когда ни одна из ячейка не является ячейкой с особым объектом.

	public void AddRoad (HexDirection direction) {
		if (
			!roads[(int)direction] && !HasRiverThroughEdge(direction) &&
			!IsSpecial && !GetNeighbor(direction).IsSpecial &&
			GetElevationDifference(direction) <= 1
		) {
			SetRoad((int)direction, true);
		}
	}

Избегаем других объектов


Особые объекты нельзя смешивать и с другими типами объектов. Если они наложатся друг на друга, то выглядеть это будет неопрятно. Это тоже может зависеть от особого объекта, но мы воспользуемся одинаковым подходом.


Объект, пересекающийся с другими объектами.

В этом случае мы будем подавлять меньшие объекты, как будто они оказались под водой. На этот раз проверку будем выполнять в HexFeatureManager.AddFeature.

	public void AddFeature (HexCell cell, Vector3 position) {
		if (cell.IsSpecial) {
			return;
		}
		…
	}

Избегаем воду


Также у нас есть проблема с водой. Будут ли особые объекты сохраняться при затоплении? Так как мы уничтожаем мелкие объекты в затопленных ячейках, давайте сделаем то же самое с особыми объектами.


Объекты в воде.

В HexGridChunk.Triangulate будем выполнять одинаковую проверку затопленности и для особых, и для обычных объектов.

	void Triangulate (HexCell cell) {
		for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) {
			Triangulate(d, cell);
		}
		if (!cell.IsUnderwater && !cell.HasRiver && !cell.HasRoads) {
			features.AddFeature(cell, cell.Position);
		}
		if (!cell.IsUnderwater && cell.IsSpecial) {
			features.AddSpecialFeature(cell, cell.Position);
		}
	}

Так как оба оператора if теперь проверяют, находится ли ячейка под водой, мы можем перенести проверку и выполнять её только один раз.

	void Triangulate (HexCell cell) {
		for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) {
			Triangulate(d, cell);
		}
		if (!cell.IsUnderwater) {
			if (!cell.HasRiver && !cell.HasRoads) {
				features.AddFeature(cell, cell.Position);
			}
			if (cell.IsSpecial) {
				features.AddSpecialFeature(cell, cell.Position);
			}
		}
	}

Для экспериментов такого количества объектов нам будет достаточно.

unitypackage