HexMetrics
. So let's add an array of colors to it. public static Color[] colors;
HexGrid
. public Color[] colors; … void Awake () { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); HexMetrics.colors = colors; … } … void OnEnable () { if (!HexMetrics.noiseSource) { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); HexMetrics.colors = colors; } }
// public Color defaultColor = Color.white; … void CreateCell (int x, int z, int i) { … HexCell cell = cells[i] = Instantiate<HexCell>(cellPrefab); cell.transform.localPosition = position; cell.coordinates = HexCoordinates.FromOffsetCoordinates(x, z); // cell.Color = defaultColor; … }
HexCell
. Instead, we will store the index. And instead of the color index, we use a more general relief type index. // Color color; int terrainTypeIndex;
public Color Color { get { return HexMetrics.colors[terrainTypeIndex]; } // set { // … // } }
public int TerrainTypeIndex { get { return terrainTypeIndex; } set { if (terrainTypeIndex != value) { terrainTypeIndex = value; Refresh(); } } }
HexMapEditor
remove all code related to colors. This will fix the compilation error. // public Color[] colors; … // Color activeColor; … // bool applyColor; … // public void SelectColor (int index) { // applyColor = index >= 0; // if (applyColor) { // activeColor = colors[index]; // } // } … // void Awake () { // SelectColor(0); // } … void EditCell (HexCell cell) { if (cell) { // if (applyColor) { // cell.Color = activeColor; // } … } }
int activeTerrainTypeIndex; … public void SetTerrainTypeIndex (int index) { activeTerrainTypeIndex = index; }
SelectColor
method. Connect the color widgets in the UI with SetTerrainTypeIndex
, leaving everything else unchanged. This means that a negative index is still in use and indicates that the color should not change.EditCell
so that the relief type index is assigned to the cell being edited. void EditCell (HexCell cell) { if (cell) { if (activeTerrainTypeIndex >= 0) { cell.TerrainTypeIndex = activeTerrainTypeIndex; } … } }
HexMapEditor
. We will create two methods that will deal with this, and for the time being we will leave them empty. public void Save () { } public void Load () { }
Application.persistentDataPath
. You can check how it will be with you in the Save
method by outputting it to the console and pressing the button in the Play mode. public void Save () { Debug.Log(Application.persistentDataPath); }
System.IO
namespace. Therefore, we add a using
statement for it over the HexMapEditor
class. using UnityEngine; using UnityEngine.EventSystems; using System.IO; public class HexMapEditor : MonoBehaviour { … }
Path.Combine
method. public void Save () { string path = Path.Combine(Application.persistentDataPath, "test.map"); }
File.Open
method. Since we want to write data to this file, we need to use its create mode. At the same time, at the specified path, either a new file will be created, or an existing file will be replaced. string path = Path.Combine(Application.persistentDataPath, "test.map"); File.Open(path, FileMode.Create);
string path = Path.Combine(Application.persistentDataPath, "test.map"); Stream fileStream = File.Open(path, FileMode.Create); fileStream.Close();
BinaryWriter
. These objects allow you to write primitive data to any stream.BinaryWriter
object, and its argument will be our file stream. Closing the writer closes the stream that it uses. Therefore, we no longer need to store a direct link to the stream. string path = Path.Combine(Application.persistentDataPath, "test.map"); BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create)); writer.Close();
BinaryWriter.Write
method. There is a variant of the Write
method for all primitive types, such as integer and float. It can also write strings. Let's try write integer 123. BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create)); writer.Write(123); writer.Close();
BinaryWriter
uses little-endian format to save numbers. This means that the least significant bytes are written first. This format has been used by Microsoft in developing the .Net framework. It was probably chosen because little-endian format is used in Intel CPUs.using
block for this. It defines the scope within which the writer is valid. When executable code goes beyond this scope, the writer is deleted and the stream is closed. using ( BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create)) ) { writer.Write(123); } // writer.Close();
IDisposable
interface. These objects have a Dispose
method, which is indirectly called when going outside the scope of using
.using
is that it works no matter how the execution of the program goes out of scope. Early returns, exceptions and errors do not interfere with it. In addition, it is very concise.Load
method. As with saving, we need to create a path and open the file stream. The difference is that now we open the file for reading, not for writing. And instead of writer, we will need a BinaryReader
. public void Load () { string path = Path.Combine(Application.persistentDataPath, "test.map"); using ( BinaryReader reader = new BinaryReader(File.Open(path, FileMode.Open)) ) { } }
File.OpenRead
method to open the file to read it. using (BinaryReader reader = new BinaryReader(File.OpenRead(path))) { }
BinaryReader.ReadInt32
. This method reads a 32-bit integer, that is, four bytes. using (BinaryReader reader = new BinaryReader(File.OpenRead(path))) { Debug.Log(reader.ReadInt32()); }
MonoBehaviour
classes that have data that we do not need to save. Therefore, we need to use a separate hierarchy of objects, which eliminates the simplicity of automatic serialization. In addition, it will be more difficult to maintain future code changes. Therefore, we will adhere to full control through manual serialization. In addition, it will make us really understand what is happening.Save
and Load
methods to HexCell
. Since they need a writer or reader to work, we add them as parameters. using UnityEngine; using System.IO; public class HexCell : MonoBehaviour { … public void Save (BinaryWriter writer) { } public void Load (BinaryReader reader) { } }
Save
and Load
methods to HexGrid
. These methods simply bypass all the cells by calling their Load
and Save
methods. using UnityEngine; using UnityEngine.UI; using System.IO; public class HexGrid : MonoBehaviour { … public void Save (BinaryWriter writer) { for (int i = 0; i < cells.Length; i++) { cells[i].Save(writer); } } public void Load (BinaryReader reader) { for (int i = 0; i < cells.Length; i++) { cells[i].Load(reader); } } }
public void Load (BinaryReader reader) { for (int i = 0; i < cells.Length; i++) { cells[i].Load(reader); } for (int i = 0; i < chunks.Length; i++) { chunks[i].Refresh(); } }
HexMapEditor
with calls to the Save
and Load
methods of the grid, passing with them a writer or reader. public void Save () { string path = Path.Combine(Application.persistentDataPath, "test.map"); using ( BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create)) ) { hexGrid.Save(writer); } } public void Load () { string path = Path.Combine(Application.persistentDataPath, "test.map"); using (BinaryReader reader = new BinaryReader(File.OpenRead(path))) { hexGrid.Load(reader); } }
HexCell
type relief HexCell
.terrainTypeIndex
field. We will not use properties. Since we are explicitly updating all fragments, the Refresh
property calls are not needed. In addition, since we only keep the correct maps, we will assume that all the maps that are loaded are also correct. Therefore, for example, we will not check whether the river or the road is permissible. public void Save (BinaryWriter writer) { writer.Write(terrainTypeIndex); } public void Load (BinaryReader reader) { terrainTypeIndex = reader.ReadInt32(); }
public void Save (BinaryWriter writer) { writer.Write(terrainTypeIndex); writer.Write(elevation); writer.Write(waterLevel); writer.Write(urbanLevel); writer.Write(farmLevel); writer.Write(plantLevel); writer.Write(specialIndex); } public void Load (BinaryReader reader) { terrainTypeIndex = reader.ReadInt32(); elevation = reader.ReadInt32(); waterLevel = reader.ReadInt32(); urbanLevel = reader.ReadInt32(); farmLevel = reader.ReadInt32(); plantLevel = reader.ReadInt32(); specialIndex = reader.ReadInt32(); }
Elevation
setter and insert it into a separate RefreshPosition
method. The only change that needs to be made here is to replace value
reference to the elevation
field. void RefreshPosition () { Vector3 position = transform.localPosition; position.y = elevation * HexMetrics.elevationStep; position.y += (HexMetrics.SampleNoise(position).y * 2f - 1f) * HexMetrics.elevationPerturbStrength; transform.localPosition = position; Vector3 uiPosition = uiRect.localPosition; uiPosition.z = -position.y; uiRect.localPosition = uiPosition; }
public int Elevation { … set { if (elevation == value) { return; } elevation = value; RefreshPosition(); ValidateRivers(); … } } … public void Load (BinaryReader reader) { terrainTypeIndex = reader.ReadInt32(); elevation = reader.ReadInt32(); RefreshPosition(); … }
public void Save (BinaryWriter writer) { writer.Write(terrainTypeIndex); writer.Write(elevation); writer.Write(waterLevel); writer.Write(urbanLevel); writer.Write(farmLevel); writer.Write(plantLevel); writer.Write(specialIndex); writer.Write(walled); writer.Write(hasIncomingRiver); writer.Write(hasOutgoingRiver); for (int i = 0; i < roads.Length; i++) { writer.Write(roads[i]); } }
HexDirection
. The HexDirection
type is an enumeration that is internally stored as several integer values. Therefore, we can serialize them too as an integer using an explicit conversion. writer.Write(hasIncomingRiver); writer.Write((int)incomingRiver); writer.Write(hasOutgoingRiver); writer.Write((int)outgoingRiver);
BinaryReader.ReadBoolean
method. River directions are integer, which we need to convert back to HexDirection
. public void Load (BinaryReader reader) { terrainTypeIndex = reader.ReadInt32(); elevation = reader.ReadInt32(); RefreshPosition(); waterLevel = reader.ReadInt32(); urbanLevel = reader.ReadInt32(); farmLevel = reader.ReadInt32(); plantLevel = reader.ReadInt32(); specialIndex = reader.ReadInt32(); walled = reader.ReadBoolean(); hasIncomingRiver = reader.ReadBoolean(); incomingRiver = (HexDirection)reader.ReadInt32(); hasOutgoingRiver = reader.ReadBoolean(); outgoingRiver = (HexDirection)reader.ReadInt32(); for (int i = 0; i < roads.Length; i++) { roads[i] = reader.ReadBoolean(); } }
writer.Write((byte)terrainTypeIndex); writer.Write((byte)elevation); writer.Write((byte)waterLevel); writer.Write((byte)urbanLevel); writer.Write((byte)farmLevel); writer.Write((byte)plantLevel); writer.Write((byte)specialIndex); writer.Write(walled); writer.Write(hasIncomingRiver); writer.Write((byte)incomingRiver); writer.Write(hasOutgoingRiver); writer.Write((byte)outgoingRiver);
BinaryReader.ReadByte
. The conversion from byte to integer is implicit, so we don’t need to add explicit conversions. terrainTypeIndex = reader.ReadByte(); elevation = reader.ReadByte(); RefreshPosition(); waterLevel = reader.ReadByte(); urbanLevel = reader.ReadByte(); farmLevel = reader.ReadByte(); plantLevel = reader.ReadByte(); specialIndex = reader.ReadByte(); walled = reader.ReadBoolean(); hasIncomingRiver = reader.ReadBoolean(); incomingRiver = (HexDirection)reader.ReadByte(); hasOutgoingRiver = reader.ReadBoolean(); outgoingRiver = (HexDirection)reader.ReadByte();
// writer.Write(hasIncomingRiver); // writer.Write((byte)incomingRiver); if (hasIncomingRiver) { writer.Write((byte)(incomingRiver + 128)); } else { writer.Write((byte)0); } // writer.Write(hasOutgoingRiver); // writer.Write((byte)outgoingRiver); if (hasOutgoingRiver) { writer.Write((byte)(outgoingRiver + 128)); } else { writer.Write((byte)0); }
HexDirection
. // hasIncomingRiver = reader.ReadBoolean(); // incomingRiver = (HexDirection)reader.ReadByte(); byte riverData = reader.ReadByte(); if (riverData >= 128) { hasIncomingRiver = true; incomingRiver = (HexDirection)(riverData - 128); } else { hasIncomingRiver = false; } // hasOutgoingRiver = reader.ReadBoolean(); // outgoingRiver = (HexDirection)reader.ReadByte(); riverData = reader.ReadByte(); if (riverData >= 128) { hasOutgoingRiver = true; outgoingRiver = (HexDirection)(riverData - 128); } else { hasOutgoingRiver = false; }
<<
. Then combine them using the bitwise OR operator. For example, if the first, second, third and sixth roads are used, then the finished byte will be 100111. int roadFlags = 0; for (int i = 0; i < roads.Length; i++) { // writer.Write(roads[i]); if (roads[i]) { roadFlags |= 1 << i; } } writer.Write((byte)roadFlags);
1 << n
2 n , . int roadFlags = reader.ReadByte(); for (int i = 0; i < roads.Length; i++) { roads[i] = (roadFlags & (1 << i)) != 0; }
HexMapEditor
record an integer zero. public void Save () { string path = Path.Combine(Application.persistentDataPath, "test.map"); using ( BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create)) ) { writer.Write(0); hexGrid.Save(writer); } }
public void Load () { string path = Path.Combine(Application.persistentDataPath, "test.map"); using (BinaryReader reader = new BinaryReader(File.OpenRead(path))) { reader.ReadInt32(); hexGrid.Load(reader); } }
using (BinaryReader reader = new BinaryReader(File.OpenRead(path))) { int header = reader.ReadInt32(); if (header == 0) { hexGrid.Load(reader); } else { Debug.LogWarning("Unknown map format " + header); } }
HexGrid
, some metrics are initialized, then the number of cells is determined and the necessary fragments and cells are created. Creating a new set of fragments and cells, we create a new map. Let's divide HexGrid.Awake
into two parts - the initialization source code and the general method CreateMap
. void Awake () { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); HexMetrics.colors = colors; CreateMap(); } public void CreateMap () { cellCountX = chunkCountX * HexMetrics.chunkSizeX; cellCountZ = chunkCountZ * HexMetrics.chunkSizeZ; CreateChunks(); CreateCells(); }
CreateMap
our object HexGrid
. That is, we will not go through the Hex Map Editor , but directly call the method of the Hex Grid object .CreateMap
. public void CreateMap () { if (chunks != null) { for (int i = 0; i < chunks.Length; i++) { Destroy(chunks[i].gameObject); } } … }
chunkCountX
and the chunkCountZ
object HexGrid
. But it will be much more convenient to indicate the size of the map in the cells. At the same time, we can even further change the size of the fragment without changing the size of the maps. So let's swap the field for the number of cells and the number of fragments. // public int chunkCountX = 4, chunkCountZ = 3; public int cellCountX = 20, cellCountZ = 15; … // int cellCountX, cellCountZ; int chunkCountX, chunkCountZ; … public void CreateMap () { … // cellCountX = chunkCountX * HexMetrics.chunkSizeX; // cellCountZ = chunkCountZ * HexMetrics.chunkSizeZ; chunkCountX = cellCountX / HexMetrics.chunkSizeX; chunkCountZ = cellCountZ / HexMetrics.chunkSizeZ; CreateChunks(); CreateCells(); }
HexMapCamera
uses fragment sizes to limit its position . Let's change it HexMapCamera.ClampPosition
so that it uses directly the number of cells that it still needs. Vector3 ClampPosition (Vector3 position) { float xMax = (grid.cellCountX - 0.5f) * (2f * HexMetrics.innerRadius); position.x = Mathf.Clamp(position.x, 0f, xMax); float zMax = (grid.cellCountZ - 1) * (1.5f * HexMetrics.outerRadius); position.z = Mathf.Clamp(position.z, 0f, zMax); return position; }
HexGrid.CreateMap
parameters X and Z. They will replace the existing number of cells. Inside, Awake
we'll just call them with the current numbers of cells. void Awake () { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); HexMetrics.colors = colors; CreateMap(cellCountX, cellCountZ); } public void CreateMap (int x, int z) { … cellCountX = x; cellCountZ = z; chunkCountX = cellCountX / HexMetrics.chunkSizeX; chunkCountZ = cellCountZ / HexMetrics.chunkSizeZ; CreateChunks(); CreateCells(); }
%
to calculate the remainder of dividing the number of cells by the number of fragments. If it is not equal to zero, then there is a discrepancy and we will not create a new map. And while we are doing this, let's add protection from zero and negative sizes. public void CreateMap (int x, int z) { if ( x <= 0 || x % HexMetrics.chunkSizeX != 0 || z <= 0 || z % HexMetrics.chunkSizeZ != 0 ) { Debug.LogError("Unsupported map size."); return; } … }
HexGrid.CreateMap
now has two parameters. We cannot directly connect Unity events with such methods. In addition, to support different sizes of maps we need a few buttons. Instead of adding all these buttons to the main UI, let's create a separate popup menu.NewMapMenu
and add it to the canvas New Map Menu object . To create a new map, we need access to a Hex Grid object . Therefore, we add a common field to it and connect it. using UnityEngine; public class NewMapMenu : MonoBehaviour { public HexGrid hexGrid; }
NewMapMenu
two general methods that will deal with this. public void Open () { gameObject.SetActive(true); } public void Close () { gameObject.SetActive(false); }
Open
in the New Map Menu object .Close
. This will allow us to open and close the popup menu.CreateMap
. In addition, after that we need to close the pop-up menu. Add to the NewMapMenu
method that deals with this, taking into account an arbitrary size. void CreateMap (int x, int z) { hexGrid.CreateMap(x, z); Close(); }
CreateMap
with the specified size. For a small map, I used a size of 20 by 15, corresponding to the size of the default map. For an average card, I decided to double this size, getting 40 by 30, and double it again for a large card. Connect the buttons with the appropriate methods. public void CreateSmallMap () { CreateMap(20, 15); } public void CreateMediumMap () { CreateMap(40, 30); } public void CreateLargeMap () { CreateMap(80, 60); }
Locked
. For widespread use, this solution is not very suitable, but for our simple interface it is enough. This requires that we track the static instance inside HexMapCamera
, which is set when the camera is awake. static HexMapCamera instance; … void Awake () { instance = this; swivel = transform.GetChild(0); stick = swivel.GetChild(0); }
Locked
can be a simple static boolean property only with a setter. All it does is disable the instance HexMapCamera
when it is locked, and turn it on when it is unlocked. public static bool Locked { set { instance.enabled = !value; } }
NewMapMenu.Open
can block the camera, and NewMapMenu.Close
- unlock it. public void Open () { gameObject.SetActive(true); HexMapCamera.Locked = true; } public void Close () { gameObject.SetActive(false); HexMapCamera.Locked = false; }
HexMapCamera
static method ValidatePosition
. Invoking a AdjustPosition
zero offset instance method will force the camera to the map boundaries. If the camera is already inside the borders of the new card, then it will remain in place. public static void ValidatePosition () { instance.AdjustPosition(0f, 0f); }
NewMapMenu.CreateMap
after creating a new map. void CreateMap (int x, int z) { hexGrid.CreateMap(x, z); HexMapCamera.ValidatePosition(); Close(); }
HexGrid.Load
a 20 by 15 map at the beginning . public void Load (BinaryReader reader) { CreateMap(20, 15); for (int i = 0; i < cells.Length; i++) { cells[i].Load(reader); } for (int i = 0; i < chunks.Length; i++) { chunks[i].Refresh(); } }
public void Save (BinaryWriter writer) { writer.Write(cellCountX); writer.Write(cellCountZ); for (int i = 0; i < cells.Length; i++) { cells[i].Save(writer); } }
public void Load (BinaryReader reader) { CreateMap(reader.ReadInt32(), reader.ReadInt32()); … }
HexMapEditor.Load
after loading the map. public void Load () { string path = Path.Combine(Application.persistentDataPath, "test.map"); using (BinaryReader reader = new BinaryReader(File.OpenRead(path))) { int header = reader.ReadInt32(); if (header == 0) { hexGrid.Load(reader, header); HexMapCamera.ValidatePosition(); } else { Debug.LogWarning("Unknown map format " + header); } } }
HexMapEditor.Save
, instead of 0, it should write 1 public void Save () { string path = Path.Combine(Application.persistentDataPath, "test.map"); using ( BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create)) ) { writer.Write(1); hexGrid.Save(writer); } }
HexMapEditor.Load
so that it accepts the new version. public void Load () { string path = Path.Combine(Application.persistentDataPath, "test.map"); using (BinaryReader reader = new BinaryReader(File.OpenRead(path))) { int header = reader.ReadInt32(); if (header == 1) { hexGrid.Load(reader); HexMapCamera.ValidatePosition(); } else { Debug.LogWarning("Unknown map format " + header); } } }
HexMapEditor.Load
must pass the header to the method HexGrid.Load
. if (header <= 1) { hexGrid.Load(reader, header); HexMapCamera.ValidatePosition(); }
HexGrid.Load
header to the parameter and use it to make decisions about further actions. If the header is at least 1, then you need to read the card size data. Otherwise, use the old fixed card size 20 by 15 and skip reading the size data. public void Load (BinaryReader reader, int header) { int x = 20, z = 15; if (header >= 1) { x = reader.ReadInt32(); z = reader.ReadInt32(); } CreateMap(x, z); … }
HexGrid.CreateMap
already refuses to create a map and displays an error to the console. To tell this to the caller, let's return a bool indicating whether a map has been created. public bool CreateMap (int x, int z) { if ( x <= 0 || x % HexMetrics.chunkSizeX != 0 || z <= 0 || z % HexMetrics.chunkSizeZ != 0 ) { Debug.LogError("Unsupported map size."); return false; } … return true; }
HexGrid.Load
too, can stop executing if the map creation fails. public void Load (BinaryReader reader, int header) { int x = 20, z = 15; if (header >= 1) { x = reader.ReadInt32(); z = reader.ReadInt32(); } if (!CreateMap(x, z)) { return; } … }
if (x != cellCountX || z != cellCountZ) { if (!CreateMap(x, z)) { return; } }
SaveLoadMenu
. As well NewMapMenu
, it needs a reference to the grid, as well as methods Open
and Close
. using UnityEngine; public class SaveLoadMenu : MonoBehaviour { public HexGrid hexGrid; public void Open () { gameObject.SetActive(true); HexMapCamera.Locked = true; } public void Close () { gameObject.SetActive(false); HexMapCamera.Locked = false; } }
Open
boolean parameter to the method . It determines whether the menu should be in save mode. We will track this mode in the field to know what action to take later. bool saveMode; public void Open (bool saveMode) { this.saveMode = saveMode; gameObject.SetActive(true); HexMapCamera.Locked = true; }
Open
of the object Save Load the Menu . Check the boolean parameter for the Save button only .Close
. Now Save Load Menu can open and close. using UnityEngine; using UnityEngine.UI; public class SaveLoadMenu : MonoBehaviour { public Text menuLabel, actionButtonLabel; … }
public void Open (bool saveMode) { this.saveMode = saveMode; if (saveMode) { menuLabel.text = "Save Map"; actionButtonLabel.text = "Save"; } else { menuLabel.text = "Load Map"; actionButtonLabel.text = "Load"; } gameObject.SetActive(true); HexMapCamera.Locked = true; }
InputField
of the Name Input object . public InputField nameInput;
null
. using UnityEngine; using UnityEngine.UI; using System.IO; public class SaveLoadMenu : MonoBehaviour { … string GetSelectedPath () { string mapName = nameInput.text; if (mapName.Length == 0) { return null; } return Path.Combine(Application.persistentDataPath, mapName + ".map"); } }
SaveLoadMenu
. Therefore we will move methods Save
and Load
from HexMapEditor
to SaveLoadMenu
. They are no longer required to be shared, and will work with the path parameter instead of the fixed path. void Save (string path) { // string path = Path.Combine(Application.persistentDataPath, "test.map"); using ( BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create)) ) { writer.Write(1); hexGrid.Save(writer); } } void Load (string path) { // string path = Path.Combine(Application.persistentDataPath, "test.map"); using (BinaryReader reader = new BinaryReader(File.OpenRead(path))) { int header = reader.ReadInt32(); if (header <= 1) { hexGrid.Load(reader, header); HexMapCamera.ValidatePosition(); } else { Debug.LogWarning("Unknown map format " + header); } } }
void Load (string path) { if (!File.Exists(path)) { Debug.LogError("File does not exist " + path); return; } … }
Action
. It starts with getting the path selected by the user. If there is a path, then we save or load it. Then close the menu. public void Action () { string path = GetSelectedPath(); if (path == null) { return; } if (saveMode) { Save(path); } else { Load(path); } Close(); }
SaveLoadMenu
for this a common method. public void SelectItem (string name) { nameInput.text = name; }
SelectItem
. He also needs to keep track of the name of the card he represents, and set his text. Create a small component for this SaveLoadItem
. using UnityEngine; using UnityEngine.UI; public class SaveLoadItem : MonoBehaviour { public SaveLoadMenu menu; public string MapName { get { return mapName; } set { mapName = value; transform.GetChild(0).GetComponent<Text>().text = value; } } string mapName; public void Select () { menu.SelectItem(mapName); } }
Select
.SaveLoadMenu
need a reference to Content inside the Viewport of the File List object . He also needs a link to the prefab point. public RectTransform listContent; public SaveLoadItem itemPrefab;
Directory.GetFiles
. This method has a second parameter that allows filtering files. In our case, only files corresponding to the * .map mask are required . void FillList () { string[] paths = Directory.GetFiles(Application.persistentDataPath, "*.map"); }
System.Array.Sort
. using UnityEngine; using UnityEngine.UI; using System; using System.IO; public class SaveLoadMenu : MonoBehaviour { … void FillList () { string[] paths = Directory.GetFiles(Application.persistentDataPath, "*.map"); Array.Sort(paths); } … }
Array.Sort(paths); for (int i = 0; i < paths.Length; i++) { SaveLoadItem item = Instantiate(itemPrefab); item.menu = this; item.MapName = paths[i]; item.transform.SetParent(listContent, false); }
Directory.GetFiles
returns the full paths to the files, we need to clear them. Fortunately, this is exactly what a convenient method does Path.GetFileNameWithoutExtension
. item.MapName = Path.GetFileNameWithoutExtension(paths[i]);
public void Open (bool saveMode) { … FillList(); gameObject.SetActive(true); HexMapCamera.Locked = true; }
void FillList () { for (int i = 0; i < listContent.childCount; i++) { Destroy(listContent.GetChild(i).gameObject); } … }
File.Delete
. public void Delete () { string path = GetSelectedPath(); if (path == null) { return; } File.Delete(path); }
if (File.Exists(path)) { File.Delete(path); }
if (File.Exists(path)) { File.Delete(path); } nameInput.text = ""; FillList();
HexGridChunk
it to create these splat maps, rather than using the colors of the cells. Since we will often use three colors, we will create static fields for them. static Color color1 = new Color(1f, 0f, 0f); static Color color2 = new Color(0f, 1f, 0f); static Color color3 = new Color(0f, 0f, 1f);
void TriangulateWithoutRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { TriangulateEdgeFan(center, e, color1); … }
void TriangulateAdjacentToRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … TriangulateEdgeStrip(m, color1, e, color1); TriangulateEdgeFan(center, m, color1); … }
void TriangulateWithRiverBeginOrEnd ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … TriangulateEdgeStrip(m, color1, e, color1); TriangulateEdgeFan(center, m, color1); … }
void TriangulateWithRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … TriangulateEdgeStrip(m, color1, e, color1); terrain.AddTriangle(centerL, m.v1, m.v2); // terrain.AddTriangleColor(cell.Color); terrain.AddQuad(centerL, center, m.v2, m.v3); // terrain.AddQuadColor(cell.Color); terrain.AddQuad(center, centerR, m.v3, m.v4); // terrain.AddQuadColor(cell.Color); terrain.AddTriangle(centerR, m.v4, m.v5); // terrain.AddTriangleColor(cell.Color); terrain.AddTriangleColor(color1); terrain.AddQuadColor(color1); terrain.AddQuadColor(color1); terrain.AddTriangleColor(color1); … }
void TriangulateConnection ( HexDirection direction, HexCell cell, EdgeVertices e1 ) { … if (cell.GetEdgeType(direction) == HexEdgeType.Slope) { TriangulateEdgeTerraces(e1, cell, e2, neighbor, hasRoad); } else { TriangulateEdgeStrip(e1, color1, e2, color2, hasRoad); } … }
void TriangulateEdgeTerraces ( EdgeVertices begin, HexCell beginCell, EdgeVertices end, HexCell endCell, bool hasRoad ) { EdgeVertices e2 = EdgeVertices.TerraceLerp(begin, end, 1); Color c2 = HexMetrics.TerraceLerp(color1, color2, 1); TriangulateEdgeStrip(begin, color1, e2, c2, hasRoad); for (int i = 2; i < HexMetrics.terraceSteps; i++) { EdgeVertices e1 = e2; Color c1 = c2; e2 = EdgeVertices.TerraceLerp(begin, end, i); c2 = HexMetrics.TerraceLerp(color1, color2, i); TriangulateEdgeStrip(e1, c1, e2, c2, hasRoad); } TriangulateEdgeStrip(e2, c2, end, color2, hasRoad); }
void TriangulateCorner ( Vector3 bottom, HexCell bottomCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { … else { terrain.AddTriangle(bottom, left, right); terrain.AddTriangleColor(color1, color2, color3); } features.AddWall(bottom, bottomCell, left, leftCell, right, rightCell); }
void TriangulateCornerTerraces ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { Vector3 v3 = HexMetrics.TerraceLerp(begin, left, 1); Vector3 v4 = HexMetrics.TerraceLerp(begin, right, 1); Color c3 = HexMetrics.TerraceLerp(color1, color2, 1); Color c4 = HexMetrics.TerraceLerp(color1, color3, 1); terrain.AddTriangle(begin, v3, v4); terrain.AddTriangleColor(color1, c3, c4); for (int i = 2; i < HexMetrics.terraceSteps; i++) { Vector3 v1 = v3; Vector3 v2 = v4; Color c1 = c3; Color c2 = c4; v3 = HexMetrics.TerraceLerp(begin, left, i); v4 = HexMetrics.TerraceLerp(begin, right, i); c3 = HexMetrics.TerraceLerp(color1, color2, i); c4 = HexMetrics.TerraceLerp(color1, color3, i); terrain.AddQuad(v1, v2, v3, v4); terrain.AddQuadColor(c1, c2, c3, c4); } terrain.AddQuad(v3, v4, left, right); terrain.AddQuadColor(c3, c4, color2, color3); }
TriangulateBoundaryTriangle
. This method received as parameters the initial and left cell. However, now we need appropriate splat colors, which can vary depending on the topology. Therefore, we replace these parameters with colors. void TriangulateBoundaryTriangle ( Vector3 begin, Color beginColor, Vector3 left, Color leftColor, Vector3 boundary, Color boundaryColor ) { Vector3 v2 = HexMetrics.Perturb(HexMetrics.TerraceLerp(begin, left, 1)); Color c2 = HexMetrics.TerraceLerp(beginColor, leftColor, 1); terrain.AddTriangleUnperturbed(HexMetrics.Perturb(begin), v2, boundary); terrain.AddTriangleColor(beginColor, c2, boundaryColor); for (int i = 2; i < HexMetrics.terraceSteps; i++) { Vector3 v1 = v2; Color c1 = c2; v2 = HexMetrics.Perturb(HexMetrics.TerraceLerp(begin, left, i)); c2 = HexMetrics.TerraceLerp(beginColor, leftColor, i); terrain.AddTriangleUnperturbed(v1, v2, boundary); terrain.AddTriangleColor(c1, c2, boundaryColor); } terrain.AddTriangleUnperturbed(v2, HexMetrics.Perturb(left), boundary); terrain.AddTriangleColor(c2, leftColor, boundaryColor); }
TriangulateCornerTerracesCliff
so that it uses the right colors. void TriangulateCornerTerracesCliff ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { … Color boundaryColor = Color.Lerp(color1, color3, b); TriangulateBoundaryTriangle( begin, color1, left, color2, boundary, boundaryColor ); if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) { TriangulateBoundaryTriangle( left, color2, right, color3, boundary, boundaryColor ); } else { terrain.AddTriangleUnperturbed( HexMetrics.Perturb(left), HexMetrics.Perturb(right), boundary ); terrain.AddTriangleColor(color2, color3, boundaryColor); } }
TriangulateCornerCliffTerraces
. void TriangulateCornerCliffTerraces ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { … Color boundaryColor = Color.Lerp(color1, color2, b); TriangulateBoundaryTriangle( right, color3, begin, color1, boundary, boundaryColor ); if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) { TriangulateBoundaryTriangle( left, color2, right, color3, boundary, boundaryColor ); } else { terrain.AddTriangleUnperturbed( HexMetrics.Perturb(left), HexMetrics.Perturb(right), boundary ); terrain.AddTriangleColor(color2, color3, boundaryColor); } }
Texture2DArray
which is supported in Unity since version 5.4.TextureArrayWizard
and place it inside the Editor folder . Instead, MonoBehaviour
it must extend the type ScriptableWizard
from the namespace UnityEditor
. using UnityEditor; using UnityEngine; public class TextureArrayWizard : ScriptableWizard { }
ScriptableWizard.DisplayWizard
. Its parameters are the names of the wizard window and its creation buttons. We will call this method in a static method CreateWizard
. static void CreateWizard () { ScriptableWizard.DisplayWizard<TextureArrayWizard>( "Create Texture Array", "Create" ); }
MenuItem
. Let's add it to the Assets menu , and more specifically in the Assets / Create / Texture Array . [MenuItem("Assets/Create/Texture Array")] static void CreateWizard () { … }
public Texture2D[] textures;
OnWizardCreate
. This is the method that is called when the create button is clicked, so we need to add it to the wizard. void OnWizardCreate () { }
void OnWizardCreate () { if (textures.Length == 0) { return; } }
EditorUtility.SaveFilePanelInProject
. Its parameters define the panel name, default file name, file extension, and description. For texture arrays, the common asset file extension is used . if (textures.Length == 0) { return; } EditorUtility.SaveFilePanelInProject( "Save Texture Array", "Texture Array", "asset", "Save Texture Array" );
SaveFilePanelInProject
returns the user-selected file path. If the user clicked cancel on this panel, the path will be an empty string. Therefore, in this case, we must interrupt the work. string path = EditorUtility.SaveFilePanelInProject( "Save Texture Array", "Texture Array", "asset", "Save Texture Array" ); if (path.Length == 0) { return; }
Texture2DArray
. His constructor method requires specifying the width and height of the texture, the length of the array, the format of the textures, and the need for mipmapping. These parameters must be the same for all textures in the array. To configure the object, we use the first texture. The user himself must check that all textures have the same format. if (path.Length == 0) { return; } Texture2D t = textures[0]; Texture2DArray textureArray = new Texture2DArray( t.width, t.height, textures.Length, t.format, t.mipmapCount > 1 );
Texture2DArray textureArray = new Texture2DArray( t.width, t.height, textures.Length, t.format, t.mipmapCount > 1 ); textureArray.anisoLevel = t.anisoLevel; textureArray.filterMode = t.filterMode; textureArray.wrapMode = t.wrapMode;
Graphics.CopyTexture
. The method copies raw texture data, one mip level at a time. Therefore, we need to loop around all the textures and their mip levels. The parameters of the method are two sets consisting of a texture resource, an index, and a mip-level. Since the original textures are not arrays, their index is always zero. textureArray.wrapMode = t.wrapMode; for (int i = 0; i < textures.Length; i++) { for (int m = 0; m < t.mipmapCount; m++) { Graphics.CopyTexture(textures[i], 0, m, textureArray, i, m); } }
AssetDatabase.CreateAsset
with an array and its path. The data will be written to a file in our project, and it will appear in the project window. for (int i = 0; i < textures.Length; i++) { … } AssetDatabase.CreateAsset(textureArray, path);
Texture2DArray
no methods or properties to access this parameter.Texture2DArray
has an additional parameter to specify the color space, but Texture2D
does not indicate whether it is in linear space or not, therefore, in any case, you need to specify value manually. Shader "Custom/Terrain" { Properties { _Color ("Color", Color) = (1,1,1,1) _MainTex ("Terrain Texture Array", 2DArray) = "white" {} _Glossiness ("Smoothness", Range(0,1)) = 0.5 _Metallic ("Metallic", Range(0,1)) = 0.0 } … }
#pragma target 3.5
_MainTex
now refers to an array of textures, we need to change its type. The type depends on the target platform and the macro will take care of this UNITY_DECLARE_TEX2DARRAY
. // sampler2D _MainTex; UNITY_DECLARE_TEX2DARRAY(_MainTex);
struct Input { // float2 uv_MainTex; float4 color : COLOR; float3 worldPos; };
UNITY_SAMPLE_TEX2DARRAY
. It requires three coordinates to sample the array. The first two are the usual UV coordinates. We will use the XZ coordinates of the world, scaled to 0.02. So we get a good resolution of textures at full magnification. Textures will be repeated approximately every four cells. void surf (Input IN, inout SurfaceOutputStandard o) { float2 uv = IN.worldPos.xz * 0.02; fixed4 c = UNITY_SAMPLE_TEX2DARRAY(_MainTex, float3(uv, 0)); Albedo = c.rgb * _Color; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; }
HexMesh
second list Vector3
, which we will refer to as relief types. public bool useCollider, useColors, useUVCoordinates, useUV2Coordinates; public bool useTerrainTypes; [NonSerialized] List<Vector3> vertices, terrainTypes;
Vector3
for terrain types during mesh cleaning. public void Clear () { … if (useTerrainTypes) { terrainTypes = ListPool<Vector3>.Get(); } triangles = ListPool<int>.Get(); }
public void Apply () { … if (useTerrainTypes) { hexMesh.SetUVs(2, terrainTypes); ListPool<Vector3>.Add(terrainTypes); } hexMesh.SetTriangles(triangles, 0); … }
Vector3
. Since the same triangle is the same for the whole triangle, we simply add the same data three times. public void AddTriangleTerrainTypes (Vector3 types) { terrainTypes.Add(types); terrainTypes.Add(types); terrainTypes.Add(types); }
public void AddQuadTerrainTypes (Vector3 types) { terrainTypes.Add(types); terrainTypes.Add(types); terrainTypes.Add(types); terrainTypes.Add(types); }
HexGridChunk
. Let's start with TriangulateEdgeFan
. First, for the sake of better readability, we divide the calls to the vertex and color methods. Recall that with each call of this method we pass to it color1
, so we can use this color directly, and not use the parameter. void TriangulateEdgeFan (Vector3 center, EdgeVertices edge, Color color) { terrain.AddTriangle(center, edge.v1, edge.v2); // terrain.AddTriangleColor(color); terrain.AddTriangle(center, edge.v2, edge.v3); // terrain.AddTriangleColor(color); terrain.AddTriangle(center, edge.v3, edge.v4); // terrain.AddTriangleColor(color); terrain.AddTriangle(center, edge.v4, edge.v5); // terrain.AddTriangleColor(color); terrain.AddTriangleColor(color1); terrain.AddTriangleColor(color1); terrain.AddTriangleColor(color1); terrain.AddTriangleColor(color1); }
Vector3
. Only the first four channels are important to us, because in this case the splat map is always red. Since all three components of the vector need to be assigned something, let's assign one type to them. void TriangulateEdgeFan (Vector3 center, EdgeVertices edge, float type) { … Vector3 types; types.x = types.y = types.z = type; terrain.AddTriangleTerrainTypes(types); terrain.AddTriangleTerrainTypes(types); terrain.AddTriangleTerrainTypes(types); terrain.AddTriangleTerrainTypes(types); }
TriangulateWithoutRiver
, TriangulateAdjacentToRiver
and TriangulateWithRiverBeginOrEnd
. // TriangulateEdgeFan(center, e, color1); TriangulateEdgeFan(center, e, cell.TerrainTypeIndex);
HexGridChunk
. void TriangulateEdgeStrip ( EdgeVertices e1, Color c1, float type1, EdgeVertices e2, Color c2, float type2, bool hasRoad = false ) { terrain.AddQuad(e1.v1, e1.v2, e2.v1, e2.v2); terrain.AddQuad(e1.v2, e1.v3, e2.v2, e2.v3); terrain.AddQuad(e1.v3, e1.v4, e2.v3, e2.v4); terrain.AddQuad(e1.v4, e1.v5, e2.v4, e2.v5); terrain.AddQuadColor(c1, c2); terrain.AddQuadColor(c1, c2); terrain.AddQuadColor(c1, c2); terrain.AddQuadColor(c1, c2); Vector3 types; types.x = types.z = type1; types.y = type2; terrain.AddQuadTerrainTypes(types); terrain.AddQuadTerrainTypes(types); terrain.AddQuadTerrainTypes(types); terrain.AddQuadTerrainTypes(types); if (hasRoad) { TriangulateRoadSegment(e1.v2, e1.v3, e1.v4, e2.v2, e2.v3, e2.v4); } }
TriangulateEdgeStrip
. First TriangulateAdjacentToRiver
, TriangulateWithRiverBeginOrEnd
and TriangulateWithRiver
must use the cell type for both sides of the edge strip. // TriangulateEdgeStrip(m, color1, e, color1); TriangulateEdgeStrip( m, color1, cell.TerrainTypeIndex, e, color1, cell.TerrainTypeIndex );
TriangulateConnection
should use the cell type for the nearest edge and the neighbor type for the far edge. They may be the same or different. void TriangulateConnection ( HexDirection direction, HexCell cell, EdgeVertices e1 ) { … if (cell.GetEdgeType(direction) == HexEdgeType.Slope) { TriangulateEdgeTerraces(e1, cell, e2, neighbor, hasRoad); } else { // TriangulateEdgeStrip(e1, color1, e2, color2, hasRoad); TriangulateEdgeStrip( e1, color1, cell.TerrainTypeIndex, e2, color2, neighbor.TerrainTypeIndex, hasRoad ); } … }
TriangulateEdgeTerraces
who triggers TriangulateEdgeStrip
. Types for ledges are the same. void TriangulateEdgeTerraces ( EdgeVertices begin, HexCell beginCell, EdgeVertices end, HexCell endCell, bool hasRoad ) { EdgeVertices e2 = EdgeVertices.TerraceLerp(begin, end, 1); Color c2 = HexMetrics.TerraceLerp(color1, color2, 1); float t1 = beginCell.TerrainTypeIndex; float t2 = endCell.TerrainTypeIndex; TriangulateEdgeStrip(begin, color1, t1, e2, c2, t2, hasRoad); for (int i = 2; i < HexMetrics.terraceSteps; i++) { EdgeVertices e1 = e2; Color c1 = c2; e2 = EdgeVertices.TerraceLerp(begin, end, i); c2 = HexMetrics.TerraceLerp(color1, color2, i); TriangulateEdgeStrip(e1, c1, t1, e2, c2, t2, hasRoad); } TriangulateEdgeStrip(e2, c2, t1, end, color2, t2, hasRoad); }
void TriangulateCorner ( Vector3 bottom, HexCell bottomCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { … else { terrain.AddTriangle(bottom, left, right); terrain.AddTriangleColor(color1, color2, color3); Vector3 types; types.x = bottomCell.TerrainTypeIndex; types.y = leftCell.TerrainTypeIndex; types.z = rightCell.TerrainTypeIndex; terrain.AddTriangleTerrainTypes(types); } features.AddWall(bottom, bottomCell, left, leftCell, right, rightCell); }
TriangulateCornerTerraces
, only here we create a group of quad-s. void TriangulateCornerTerraces ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { Vector3 v3 = HexMetrics.TerraceLerp(begin, left, 1); Vector3 v4 = HexMetrics.TerraceLerp(begin, right, 1); Color c3 = HexMetrics.TerraceLerp(color1, color2, 1); Color c4 = HexMetrics.TerraceLerp(color1, color3, 1); Vector3 types; types.x = beginCell.TerrainTypeIndex; types.y = leftCell.TerrainTypeIndex; types.z = rightCell.TerrainTypeIndex; terrain.AddTriangle(begin, v3, v4); terrain.AddTriangleColor(color1, c3, c4); terrain.AddTriangleTerrainTypes(types); for (int i = 2; i < HexMetrics.terraceSteps; i++) { Vector3 v1 = v3; Vector3 v2 = v4; Color c1 = c3; Color c2 = c4; v3 = HexMetrics.TerraceLerp(begin, left, i); v4 = HexMetrics.TerraceLerp(begin, right, i); c3 = HexMetrics.TerraceLerp(color1, color2, i); c4 = HexMetrics.TerraceLerp(color1, color3, i); terrain.AddQuad(v1, v2, v3, v4); terrain.AddQuadColor(c1, c2, c3, c4); terrain.AddQuadTerrainTypes(types); } terrain.AddQuad(v3, v4, left, right); terrain.AddQuadColor(c3, c4, color2, color3); terrain.AddQuadTerrainTypes(types); }
TriangulateBoundaryTriangle
. Simply give it a type vector parameter and add it to all its triangles. void TriangulateBoundaryTriangle ( Vector3 begin, Color beginColor, Vector3 left, Color leftColor, Vector3 boundary, Color boundaryColor, Vector3 types ) { Vector3 v2 = HexMetrics.Perturb(HexMetrics.TerraceLerp(begin, left, 1)); Color c2 = HexMetrics.TerraceLerp(beginColor, leftColor, 1); terrain.AddTriangleUnperturbed(HexMetrics.Perturb(begin), v2, boundary); terrain.AddTriangleColor(beginColor, c2, boundaryColor); terrain.AddTriangleTerrainTypes(types); for (int i = 2; i < HexMetrics.terraceSteps; i++) { Vector3 v1 = v2; Color c1 = c2; v2 = HexMetrics.Perturb(HexMetrics.TerraceLerp(begin, left, i)); c2 = HexMetrics.TerraceLerp(beginColor, leftColor, i); terrain.AddTriangleUnperturbed(v1, v2, boundary); terrain.AddTriangleColor(c1, c2, boundaryColor); terrain.AddTriangleTerrainTypes(types); } terrain.AddTriangleUnperturbed(v2, HexMetrics.Perturb(left), boundary); terrain.AddTriangleColor(c2, leftColor, boundaryColor); terrain.AddTriangleTerrainTypes(types); }
TriangulateCornerTerracesCliff
we will create a vector of types on the basis of the transferred cells. Then add it to the same triangle and transfer it to TriangulateBoundaryTriangle
. void TriangulateCornerTerracesCliff ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { float b = 1f / (rightCell.Elevation - beginCell.Elevation); if (b < 0) { b = -b; } Vector3 boundary = Vector3.Lerp( HexMetrics.Perturb(begin), HexMetrics.Perturb(right), b ); Color boundaryColor = Color.Lerp(color1, color3, b); Vector3 types; types.x = beginCell.TerrainTypeIndex; types.y = leftCell.TerrainTypeIndex; types.z = rightCell.TerrainTypeIndex; TriangulateBoundaryTriangle( begin, color1, left, color2, boundary, boundaryColor, types ); if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) { TriangulateBoundaryTriangle( left, color2, right, color3, boundary, boundaryColor, types ); } else { terrain.AddTriangleUnperturbed( HexMetrics.Perturb(left), HexMetrics.Perturb(right), boundary ); terrain.AddTriangleColor(color2, color3, boundaryColor); terrain.AddTriangleTerrainTypes(types); } }
TriangulateCornerCliffTerraces
. void TriangulateCornerCliffTerraces ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { float b = 1f / (leftCell.Elevation - beginCell.Elevation); if (b < 0) { b = -b; } Vector3 boundary = Vector3.Lerp( HexMetrics.Perturb(begin), HexMetrics.Perturb(left), b ); Color boundaryColor = Color.Lerp(color1, color2, b); Vector3 types; types.x = beginCell.TerrainTypeIndex; types.y = leftCell.TerrainTypeIndex; types.z = rightCell.TerrainTypeIndex; TriangulateBoundaryTriangle( right, color3, begin, color1, boundary, boundaryColor, types ); if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) { TriangulateBoundaryTriangle( left, color2, right, color3, boundary, boundaryColor, types ); } else { terrain.AddTriangleUnperturbed( HexMetrics.Perturb(left), HexMetrics.Perturb(right), boundary ); terrain.AddTriangleColor(color2, color3, boundaryColor); terrain.AddTriangleTerrainTypes(types); } }
TriangulateWithRiver
. Since here we are in the center of the cell, we deal only with the type of the current cell. Therefore, create a vector for it and add it to the triangles and quad-am. void TriangulateWithRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … terrain.AddTriangleColor(color1); terrain.AddQuadColor(color1); terrain.AddQuadColor(color1); terrain.AddTriangleColor(color1); Vector3 types; types.x = types.y = types.z = cell.TerrainTypeIndex; terrain.AddTriangleTerrainTypes(types); terrain.AddQuadTerrainTypes(types); terrain.AddQuadTerrainTypes(types); terrain.AddTriangleTerrainTypes(types); … }
float3 terrain
and copy it into it v.texcoord2.xyz
. #pragma surface surf Standard fullforwardshadows vertex:vert #pragma target 3.5 … struct Input { float4 color : COLOR; float3 worldPos; float3 terrain; }; void vert (inout appdata_full v, out Input data) { UNITY_INITIALIZE_OUTPUT(Input, data); data.terrain = v.texcoord2.xyz; }
float4 GetTerrainColor (Input IN, int index) { float3 uvw = float3(IN.worldPos.xz * 0.02, IN.terrain[index]); float4 c = UNITY_SAMPLE_TEX2DARRAY(_MainTex, uvw); return c * IN.color[index]; } void surf (Input IN, inout SurfaceOutputStandard o) { … }
color[0]
color.r
. color[1]
color.g
, . void surf (Input IN, inout SurfaceOutputStandard o) { // float2 uv = IN.worldPos.xz * 0.02; fixed4 c = GetTerrainColor(IN, 0) + GetTerrainColor(IN, 1) + GetTerrainColor(IN, 2); o.Albedo = c.rgb * _Color; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; }
HexCell
no longer need the color property, so remove it. // public Color Color { // get { // return HexMetrics.colors[terrainTypeIndex]; // } // }
HexGrid
can also delete an array of colors and its associated code. // public Color[] colors; … void Awake () { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); // HexMetrics.colors = colors; CreateMap(cellCountX, cellCountZ); } … … void OnEnable () { if (!HexMetrics.noiseSource) { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); // HexMetrics.colors = colors; } }
HexMetrics
. // public static Color[] colors;
Properties { _Color ("Color", Color) = (1,1,1,1) _MainTex ("Terrain Texture Array", 2DArray) = "white" {} _GridTex ("Grid Texture", 2D) = "white" {} _Glossiness ("Smoothness", Range(0,1)) = 0.5 _Metallic ("Metallic", Range(0,1)) = 0.0 }
sampler2D _GridTex; … void surf (Input IN, inout SurfaceOutputStandard o) { fixed4 c = GetTerrainColor(IN, 0) + GetTerrainColor(IN, 1) + GetTerrainColor(IN, 2); fixed4 grid = tex2D(_GridTex, IN.worldPos.xz); o.Albedo = c.rgb * grid * _Color; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; }
float2 gridUV = IN.worldPos.xz; gridUV.x *= 1 / (4 * 8.66025404); gridUV.y *= 1 / (2 * 15.0); fixed4 grid = tex2D(_GridTex, gridUV);
GRID_ON
. The conditional compilation of shaders is described in the Rendering 5 tutorial , Multiple Lights . #pragma surface surf Standard fullforwardshadows vertex:vert #pragma target 3.5 #pragma multi_compile _ GRID_ON
grid
first assign it a value of 1. As a result, the grid will be disabled. Then we will sample the mesh texture only for the variant with a specific keyword GRID_ON
. fixed4 grid = 1; #if defined(GRID_ON) float2 gridUV = IN.worldPos.xz; gridUV.x *= 1 / (4 * 8.66025404); gridUV.y *= 1 / (2 * 15.0); grid = tex2D(_GridTex, gridUV); #endif o.Albedo = c.rgb * grid * _Color;
GRID_ON
not included in the relief shader, the grid will disappear. To enable it again, we will add a switch to the map editor UI. For this to be possible, you HexMapEditor
must receive a link to the Terrain material and a method for enabling or disabling a keyword GRID_ON
. public Material terrainMaterial; … public void ShowGrid (bool visible) { if (visible) { terrainMaterial.EnableKeyword("GRID_ON"); } else { terrainMaterial.DisableKeyword("GRID_ON"); } }
ShowGrid
.GRID_ON
in Awake HexMapEditor
. void Awake () { terrainMaterial.DisableKeyword("GRID_ON"); }
HexMapEditor
boolean field editMode
, as well as the method that defines it. Then add another switch to the UI to control it. Let's start with the navigation mode, that is, the edit mode will be disabled by default. bool editMode; … public void SetEditMode (bool toggle) { editMode = toggle; }
EditCells
dependent on editMode
. void HandleInput () { Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(inputRay, out hit)) { HexCell currentCell = hexGrid.GetCell(hit.point); if (previousCell && previousCell != currentCell) { ValidateDrag(currentCell); } else { isDrag = false; } if (editMode) { EditCells(currentCell); } previousCell = currentCell; } else { previousCell = null; } }
public void SetEditMode (bool toggle) { editMode = toggle; hexGrid.ShowUI(!toggle); }
HexGridChunk.Awake
turning them off, but he should not do this anymore. void Awake () { gridCanvas = GetComponentInChildren<Canvas>(); cells = new HexCell[HexMetrics.chunkSizeX * HexMetrics.chunkSizeZ]; // ShowUI(false); }
HexGrid.CreateCell
assignment value label.text
. void CreateCell (int x, int z, int i) { … Text label = Instantiate<Text>(cellLabelPrefab); label.rectTransform.anchoredPosition = new Vector2(position.x, position.z); // label.text = cell.coordinates.ToStringOnSeparateLines(); cell.uiRect = label.rectTransform; … }
HexMapEditor.ShowUI
. // public void ShowUI (bool visible) { // hexGrid.ShowUI(visible); // }
HexCell
integer field distance
. It will indicate the distance between this cell and the selected one. Therefore, it will be zero for the selected cell itself, 1 for the immediate neighbor, and so on. int distance;
HexCell
has a link to the RectTransform
UI object. We will need to call him GetComponent<Text>
to get to the cell. Consider what Text
is in the namespace UnityEngine.UI
, so use it at the beginning of the script. void UpdateDistanceLabel () { Text label = uiRect.GetComponent<Text>(); label.text = distance.ToString(); }
public int Distance { get { return distance; } set { distance = value; UpdateDistanceLabel(); } }
HexGrid
general method FindDistancesTo
with the cell parameter. For now, we will simply set the zero distance to each cell. public void FindDistancesTo (HexCell cell) { for (int i = 0; i < cells.Length; i++) { cells[i].Distance = 0; } }
HexMapEditor.HandleInput
call the new method with the current cell. if (editMode) { EditCells(currentCell); } else { hexGrid.FindDistancesTo(currentCell); }
HexCoordinates
have a method DistanceTo
and use it in HexGrid.FindDistancesTo
. public void FindDistancesTo (HexCell cell) { for (int i = 0; i < cells.Length; i++) { cells[i].Distance = cell.coordinates.DistanceTo(cells[i].coordinates); } }
HexCoordinates
method DistanceTo
. He must compare his own coordinates with the coordinates of another set. Let's start with just measuring X, and subtract X coordinates from each other. public int DistanceTo (HexCoordinates other) { return x - other.x; }
return x < other.x ? other.x - x : x - other.x;
return (x < other.x ? other.x - x : x - other.x) + (Y < other.Y ? other.Y - Y : Y - other.Y) + (z < other.z ? other.z - z : z - other.z);
return ((x < other.x ? other.x - x : x - other.x) + (Y < other.Y ? other.Y - Y : Y - other.Y) + (z < other.z ? other.z - z : z - other.z)) / 2;
System.Collections
. The refresh rate of 60 iterations per second is small enough for us to see what is happening, and the search on a small map does not take too much time. public void FindDistancesTo (HexCell cell) { StartCoroutine(Search(cell)); } IEnumerator Search (HexCell cell) { WaitForSeconds delay = new WaitForSeconds(1 / 60f); for (int i = 0; i < cells.Length; i++) { yield return delay; cells[i].Distance = cell.coordinates.DistanceTo(cells[i].coordinates); } }
public void FindDistancesTo (HexCell cell) { StopAllCoroutines(); StartCoroutine(Search(cell)); }
public void Load (BinaryReader reader, int header) { StopAllCoroutines(); … }
int.MaxValue
to indicate that we have not visited it yet. We need to do this for all the cells right before performing the search. IEnumerator Search (HexCell cell) { for (int i = 0; i < cells.Length; i++) { cells[i].Distance = int.MaxValue; } … }
HexCell.UpdateDistanceLabel
. After that we will begin each search on an empty map. void UpdateDistanceLabel () { Text label = uiRect.GetComponent<Text>(); label.text = distance == int.MaxValue ? "" : distance.ToString(); }
Queue
, which is part of the namespace System.Collections.Generic
. The selected cell will be the first to be placed in this queue, and will have a distance of 0. IEnumerator Search (HexCell cell) { for (int i = 0; i < cells.Length; i++) { cells[i].Distance = int.MaxValue; } WaitForSeconds delay = new WaitForSeconds(1 / 60f); Queue<HexCell> frontier = new Queue<HexCell>(); cell.Distance = 0; frontier.Enqueue(cell); // for (int i = 0; i < cells.Length; i++) { // yield return delay; // cells[i].Distance = // cell.coordinates.DistanceTo(cells[i].coordinates); // } }
frontier.Enqueue(cell); while (frontier.Count > 0) { yield return delay; HexCell current = frontier.Dequeue(); }
while (frontier.Count > 0) { yield return delay; HexCell current = frontier.Dequeue(); for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = current.GetNeighbor(d); if (neighbor != null) { neighbor.Distance = current.Distance + 1; frontier.Enqueue(neighbor); } } }
if (neighbor != null && neighbor.Distance == int.MaxValue) { neighbor.Distance = current.Distance + 1; frontier.Enqueue(neighbor); }
for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = current.GetNeighbor(d); if (neighbor == null || neighbor.Distance != int.MaxValue) { continue; } neighbor.Distance = current.Distance + 1; frontier.Enqueue(neighbor); }
if (neighbor == null || neighbor.Distance != int.MaxValue) { continue; } if (neighbor.IsUnderwater) { continue; }
if (neighbor.IsUnderwater) { continue; } if (current.GetEdgeType(neighbor) == HexEdgeType.Cliff) { continue; }
int distance = current.Distance; if (current.HasRoadThroughEdge(d)) { distance += 1; } else { distance += 10; } neighbor.Distance = distance;
Sort
. List<HexCell> frontier = new List<HexCell>(); cell.Distance = 0; frontier.Add(cell); while (frontier.Count > 0) { yield return delay; HexCell current = frontier[0]; frontier.RemoveAt(0); for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { … neighbor.Distance = distance; frontier.Add(neighbor); } }
frontier.Add(neighbor); frontier.Sort((x, y) => x.Distance.CompareTo(y.Distance));
frontier.Sort(CompareDistances); … static int CompareDistances (HexCell x, HexCell y) { return x.Distance.CompareTo(y.Distance); }
HexCell neighbor = current.GetNeighbor(d); if (neighbor == null) { continue; } if (neighbor.IsUnderwater) { continue; } if (current.GetEdgeType(neighbor) == HexEdgeType.Cliff) { continue; } int distance = current.Distance; if (current.HasRoadThroughEdge(d)) { distance += 1; } else { distance += 10; } if (neighbor.Distance == int.MaxValue) { neighbor.Distance = distance; frontier.Add(neighbor); } else if (distance < neighbor.Distance) { neighbor.Distance = distance; } frontier.Sort((x, y) => x.Distance.CompareTo(y.Distance));
HexEdgeType edgeType = current.GetEdgeType(neighbor); if (edgeType == HexEdgeType.Cliff) { continue; } int distance = current.Distance; if (current.HasRoadThroughEdge(d)) { distance += 1; } else { distance += edgeType == HexEdgeType.Flat ? 5 : 10; }
if (current.HasRoadThroughEdge(d)) { distance += 1; } else { distance += edgeType == HexEdgeType.Flat ? 5 : 10; distance += neighbor.UrbanLevel + neighbor.FarmLevel + neighbor.PlantLevel; }
if (current.HasRoadThroughEdge(d)) { distance += 1; } else if (current.Walled != neighbor.Walled) { continue; } else { distance += edgeType == HexEdgeType.Flat ? 5 : 10; distance += neighbor.UrbanLevel + neighbor.FarmLevel + neighbor.PlantLevel; }
Source: https://habr.com/ru/post/425919/
All Articles