Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

[OPEN SOURCE]Procedural Hexagon Terrain

Discussion in 'Community Learning & Teaching' started by landon912, Mar 10, 2014.

  1. landon912

    landon912

    Joined:
    Nov 8, 2011
    Posts:
    1,579
    What is up guys this is Landon!

    Here's a little background knowledge:

    Over the last few months I've been developing a plugin that creates terrains like seen in Civilization 5, called CivGrid. The plugin will be available with features not shown here in the coming months. This thread is a way to give back to the community for all that it has helped me and raise some awareness about my plugin. This tutorial will get you started towards making a complex yet fast hexagon terrain generated procedurally.

    Over the last few weeks I've gotten a LOT of requests on how to make a more advanced procedural hexagon terrain, so instead of replying individually to every question I thought it would be easier for me to just write up a small tutorial series on how to make everything from scratch. I'll say it only once : "I'm not a master ninja coder." Wheffph! Now that's over with, please feel free to point out ANY questionable coding practices and/or ways to optimize the code.


    Alright! Let's get started.

    Tutorial Outline:
    I. HexInfo
    A. Vertices
    B. Triangles
    C. UV Mapping
    D. Finalization
    II. Making our first world
    A. WorldManager

    1. Basic Setup
    2. Caching hexagon dimensions​
    B. HexChunk

    1. Positioning each hexagon

    2. Configuring hexagons

    3. Coordinate System

    4. Rendering

    5. Collider Setup​
    III. Making our first realistic(er) world
    A. NoiseGenerator

    1. Perlin Noise
    2. Converting to usable values
    B. Terrain Features
    1. Water
    a. Types of land
    2. Land

    a. Types of water
    C. Texture Atlasing

    1. Making our atlas
    2. Using our atlas


    Hope you learn and enjoy as I have done. Hexagons will always have a spot in my heart as the little bugger whom caused me much more pain then parallelograms.

    Best Regards,

    Landon.

    Edit: For source of my major project:

    LINK

     
    Last edited: Feb 17, 2015
    EliasMasche likes this.
  2. landon912

    landon912

    Joined:
    Nov 8, 2011
    Posts:
    1,579
    So you want to make a procedurally generated hexagon terrain? That nice and intimidating name can scare you off but have no worries; This first section will be nice and easy, hopefully!

    [HR][/HR]
    I. HexInfo

    Let's start off by making our first script : "HexInfo!"

    Code (csharp):
    1. public class HexInfo : MonoBehaviour
    2. {
    3.     //basic hexagon mesh making
    4.     public Vector3[] Vertices;
    5.     public Vector2[] uv;
    6.     public int[] Triangles;
    7.  
    8.     void Start()
    9.     {
    10.         MeshSetup();
    11.     }
    12.  
    13.     void MeshSetup()
    14.     {
    15.         #region verts
    16.    
    17.         #endregion
    18.  
    19.         #region triangles
    20.  
    21.         #endregion
    22.  
    23.         #region uv
    24.  
    25.         #endregion
    26.  
    27.         #region finalize
    28.  
    29.         #endregion
    30.  
    31.     }
    32. }
    Alright that's the skeleton of the class of for now! Within each of the regions we will be adding code that does what the region states!

    [HR][/HR]
    A. Vertices

    Now we are going to venture into the dark work of Vector3's and vertex positioning!

    What is a vertex?
    A vertex is a 3D point in space represented by a Vector3 that signifies points of a mesh.

    So how do we determine where to put these things within code?
    We are going to do some good old drawing!

    How I think while creating meshes manually is the following:

    We start off in a 3D point of (0,0,0) and in our case we will be ignoring the "y" axis so that should always be a constant, leaving only two axis - something we can draw on paper!

    Note: We move in a clockwise motion.



    Now that we have everything planned out it should be easy! Remember to move in clockwise motion!

    Code (csharp):
    1.  
    2.        #region verts
    3.          
    4.         float floorLevel = 0;
    5.         vertices = new Vector3[]
    6.         {
    7.             new Vector3(-1f , floorLevel, -.5f),
    8.             new Vector3(-1f, floorLevel, .5f),
    9.             new Vector3(0f, floorLevel, 1f),
    10.             new Vector3(1f, floorLevel, .5f),
    11.             new Vector3(1f, floorLevel, -.5f),
    12.             new Vector3(0f, floorLevel, -1f)
    13.         };
    14.  
    15.         #endregion
    This marks our first region done! We can't test it yet but we are 1/3 to making a hexagon!

    [HR][/HR]
    B. Triangles

    Ok, now that we have our vertices we have to tell it how to connect them!

    Once again, I recommend getting the o'l pen and paper out and planning it out. The general gist of it is to just fill out the area with groups of three vertices to make triangles.



    Code (csharp):
    1. #region triangles
    2.  
    3.         triangles = new int[]
    4.         {
    5.             1,5,0,
    6.             1,4,5,
    7.             1,2,4,
    8.             2,3,4
    9.         };
    10.  
    11.         #endregion
    As you can see, it as simple as listing the vertex number in an order relating to triangles. We usually organize our code in the above suit for clarity on the triangles.

    [HR][/HR] C. UV Coordinates

    What are UV coordinates?
    UV Coordinates is a type of mapping that tells the computer how to apply a 2D image(texture) to a 3D object. It does so by giving each vertex a position on the image, that corresponds to were that vertex should be pulling it's color from the image.

    UV mapping is executed almost exactly like how we did vertices; except this time we use a (0,0)-(1,1) plane, instead of (-1,-1,)-(1,1) like we did with vertices. Once again lets go get some ink and parchment, or a digital version.



    The stuff should be a review and I didn't take the time to write each a paragraph but it should be easy to follow.

    So now we just list each point out and we are done?
    Not quite! Remember how each UV coordinate relates to a vertex? We have to list them in the same order as the vertices were done in. So point 0 for UV must be in the same array position as point 0 in the vertex array.

    Code (csharp):
    1. #region UV
    2.  
    3.         uv = new Vector2[]
    4.         {
    5.             new Vector2(0,0.25f),
    6.             new Vector2(0,0.75f),
    7.             new Vector2(0.5f,1),
    8.             new Vector2(1,0.75f),
    9.             new Vector2(1,0.25f),
    10.             new Vector2(0.5f,0),
    11.         };
    12.  
    13.         #endregion
    Notice that they are listed in the order as the vertices were originally. So, vertex 0 is on the texture at (0,0.25) and so forth for the others.

    [HR][/HR]
    D. Finalization

    So now we have all our needed data how do we tell Unity to draw it?

    We need to actually pass all this data in Unity's mesh system. Fortunately this is made very easy, no playing with VBOs or flipping buffers to play nicely; this is all done by Unity.

    I think the code can explain better so I'll shut up and let it do so:

    Code (csharp):
    1.  
    2. //add this texture field to the top near the other fields for testing our UV coords
    3. public Texture texture;
    Code (csharp):
    1. #region finalize
    2.  
    3.         //add a mesh filter to the GO the script is attached to; cache it for later
    4.         MeshFilter meshFilter = gameObject.AddComponent<MeshFilter>();
    5.         //add a mesh renderer to the GO the script is attached to
    6.         gameObject.AddComponent<MeshRenderer>();
    7.  
    8.         //create a mesh object to pass our data into
    9.         Mesh mesh = new Mesh();
    10.  
    11.         //add our vertices to the mesh
    12.         mesh.vertices = vertices;
    13.         //add our triangles to the mesh
    14.         mesh.triangles = triangles;
    15.         //add out UV coordinates to the mesh
    16.         mesh.uv = uv;
    17.      
    18.         //make it play nicely with lighting
    19.         mesh.RecalculateNormals();
    20.  
    21.         //set the GO's meshFilter's mesh to be the one we just made
    22.         meshFilter.mesh = mesh;
    23.  
    24.         //UV TESTING
    25.         renderer.material.mainTexture = texture;
    26.  
    27.         #endregion
    28.  
    So now the engine has all our mesh data in its format!

    For testing I made a simple texture to showcase our UV magic powers; here.

    [HR][/HR]
    Now for the fun, testing it! Simply drop the script on an empty GameObject, assign the test texture to the "texture" field, and run the game.

    You should now be able to reap the benefits of your work! Congratulations on sticking it through the tutorial and hopefully you learned some interesting things!

    Cya next time,

    Landon
     
    Last edited: Jul 19, 2014
    PalmtreeWhale and badkraft like this.
  3. landon912

    landon912

    Joined:
    Nov 8, 2011
    Posts:
    1,579
    Section 2 - Making our first world

    Hey guys! I'm finally back at you with section two of our tutorial. Hopefully, this one can be as good, or hopefully better than the last! In this section we talk about transfering our one hexagon into a hexagon grid, with a full chunking system in place. Sounds fun!

    A. WorldManager:
    WorldManager is going to be our base and starting class for our project. This will hold all of the world values, cache values, and spawn our chunks.

    1. Basic Setup:

    So what do we need? First we need to spawn our chunks, which will then in turn spawn their hexagons within them.

    Lets make a new script to spawn these chunks and hold our world values. "WorldManager" sounds nice.

    Code (CSharp):
    1.  
    2. using UnityEngine;
    3. using System.Collections;
    4.  
    5. public class WorldManager : MonoBehaviour
    6. {
    7.     #region fields
    8.  
    9.     #endregion
    10.  
    11.     #region GetHexProperties
    12.  
    13.     #endregion
    14.  
    15.     #region GenerateMap
    16.  
    17.     #endregion
    18.  
    19.     #region NewChunk
    20.  
    21.     #endregion
    22. }
    Here's the outline for our script. Here is a quick warning before we really start. Things are going to get very long, very quickly. I'm going to be moving over points more quickly as we have a LOT to cover in a little amount of post.

    Lets dump all the fields we need into the fields region:

    Code (CSharp):
    1.  
    2. #region fields
    3. public Mesh flatHexagonSharedMesh;
    4. public float hexRadiusSize;
    5.  
    6. //hexInstances
    7. [HideInInspector]
    8. public Vector3 hexExt;
    9. [HideInInspector]
    10. public Vector3 hexSize;
    11. [HideInInspector]
    12. public Vector3 hexCenter;
    13. [HideInInspector]
    14. public GameObject chunkHolder;
    15.  
    16. public Texture2D terrainTexture;
    17.  
    18. int xSectors;
    19. int zSectors;
    20.  
    21. public HexChunk[,] hexChunks;
    22.  
    23. public Vector2 mapSize;
    24. public int chunkSize;
    25.  
    26. #endregion
    Starting to see what I mean? This is probably more code than in the whole last section and it's only the first region! The fields region! I'm not going to be going over what each one is right now, I think it will explain themselves throughout, and the name should point you in the right direction.

    2. Start up

    So when we first start off what do we want to happen? Well we need to spawn the chunks, but first make a hexagon and cache all it's size information so that we can spawn them out correctly later with correctly spaced chunks.

    Code (CSharp):
    1.  
    2. #region awake
    3. public void Awake()
    4. {
    5.     //get the flat hexagons size; we use this to space out the hexagons and chunks
    6.     GetHexProperties();
    7.     //generate the chunks of the world
    8.     GenerateMap();
    9. }
    10. #endregion
    So whenever we fire up the scene, we shall call GetHexProperties() and then GenerateMap(). Now the fun part - making those methods.

    3. Caching Hexagon Dimensions

    What we need to do is spawn one hexagon and store its dimensions. We know what we need to do, so lets get to work.

    Code (CSharp):
    1.  
    2. #region GetHexProperties
    3. private void GetHexProperties()
    4. {
    5.     //Creates mesh to calculate bounds
    6.     GameObject inst = new GameObject("Bounds Set Up: Flat");
    7.     //add mesh filter to our temp object
    8.     inst.AddComponent<MeshFilter>();
    9.     //add a renderer to our temp object
    10.     inst.AddComponent<MeshRenderer>();
    11.     //add a mesh collider to our temp object; this is for determining dimensions cheaply and easily
    12.     inst.AddComponent<MeshCollider>();
    13.     //reset the position to global zero
    14.     inst.transform.position = Vector3.zero;
    15.     //reset all rotation
    16.     inst.transform.rotation = Quaternion.identity;
    Ok, this is the first part of the method. Here we create a new GameObject and assign the basic stuff to it that is needed. We also, create a MeshCollider since we will use that component to retrieve our dimension(size, extents, center). Then we make sure everything is at zero to prevent any confusion in local/world math conversion later on.

    Code (CSharp):
    1.  
    2. Vector3[] vertices;
    3. int[] triangles;
    4.  
    5. float floorLevel = 0;
    6. //positions vertices of the hexagon to make a normal hexagon
    7. vertices = new Vector3[]
    8. {
    9.     /*0*/new Vector3((hexRadiusSize * Mathf.Cos((float)(2*Mathf.PI*(3+0.5)/6))), floorLevel, (hexRadiusSize * Mathf.Sin((float)(2*Mathf.PI*(3+0.5)/6)))),
    10.     /*1*/new Vector3((hexRadiusSize * Mathf.Cos((float)(2*Mathf.PI*(2+0.5)/6))), floorLevel, (hexRadiusSize * Mathf.Sin((float)(2*Mathf.PI*(2+0.5)/6)))),
    11.     /*2*/new Vector3((hexRadiusSize * Mathf.Cos((float)(2*Mathf.PI*(1+0.5)/6))), floorLevel, (hexRadiusSize * Mathf.Sin((float)(2*Mathf.PI*(1+0.5)/6)))),
    12.     /*3*/new Vector3((hexRadiusSize * Mathf.Cos((float)(2*Mathf.PI*(0+0.5)/6))), floorLevel, (hexRadiusSize * Mathf.Sin((float)(2*Mathf.PI*(0+0.5)/6)))),
    13.    /*4*/new Vector3((hexRadiusSize * Mathf.Cos((float)(2*Mathf.PI*(5+0.5)/6))), floorLevel, (hexRadiusSize * Mathf.Sin((float)(2*Mathf.PI*(5+0.5)/6)))),
    14.    /*5*/new Vector3((hexRadiusSize * Mathf.Cos((float)(2*Mathf.PI*(4+0.5)/6))), floorLevel, (hexRadiusSize * Mathf.Sin((float)(2*Mathf.PI*(4+0.5)/6))))
    15.             };
    16.  
    17. //triangles connecting the verts
    18. triangles = new int[]
    19. {
    20.     1,5,0,
    21.     1,4,5,
    22.     1,2,4,
    23.     2,3,4
    24. };
    25.  
    26. uv = new Vector2[]
    27. {
    28.      new Vector2(0,0.25f),
    29.      new Vector2(0,0.75f),
    30.      new Vector2(0.5f,1),
    31.      new Vector2(1,0.75f),
    32.      new Vector2(1,0.25f),
    33.      new Vector2(0.5f,0),
    34. };
    This is the next part. Actually making the hexagon. I've gone over this in full in the last section so I'll make it quick. However, this time we use an algorithm to generate our vertex positioning. See below posts for more details on the math side from our friend, MrPhil. Check this post.

    Code (CSharp):
    1.  
    2. //create new mesh to hold the data for the flat hexagon
    3. flatHexagonSharedMesh = new Mesh();
    4. //assign verts
    5. flatHexagonSharedMesh.vertices = vertices;
    6. //assign triangles
    7. flatHexagonSharedMesh.triangles = triangles;
    8. //assign uv
    9. flatHexagonSharedMesh.uv = uv;
    10. //set temp gameObject's mesh to the flat hexagon mesh
    11. inst.GetComponent<MeshFilter>().mesh = flatHexagonSharedMesh;
    12. //make object play nicely with lighting
    13. inst.GetComponent<MeshFilter>().mesh.RecalculateNormals();
    14. //set mesh collider's mesh to the flat hexagon
    15. inst.GetComponent<MeshCollider>().sharedMesh = flatHexagonSharedMesh;
    16.  
    17. //calculate the extents of the flat hexagon
    18. hexExt = new Vector3(inst.gameObject.collider.bounds.extents.x, inst.gameObject.collider.bounds.extents.y, inst.gameObject.collider.bounds.extents.z);
    19. //calculate the size of the flat hexagon
    20. hexSize = new Vector3(inst.gameObject.collider.bounds.size.x, inst.gameObject.collider.bounds.size.y, inst.gameObject.collider.bounds.size.z);
    21. //calculate the center of the flat hexagon
    22. hexCenter = new Vector3(inst.gameObject.collider.bounds.center.x, inst.gameObject.collider.bounds.center.y, inst.gameObject.collider.bounds.center.z);
    23. //destroy the temp object that we used to calculate the flat hexagon's size
    24. Destroy(inst);
    25. #endregion
    Alright, now this is another massive code block. Lets once again break it down.

    Code (CSharp):
    1.  
    2.         //create new mesh to hold the data for the flat hexagon
    3.         flatHexagonSharedMesh = new Mesh();
    4.         //assign verts
    5.         flatHexagonSharedMesh.vertices = vertices;
    6.         //assign triangles
    7.         flatHexagonSharedMesh.triangles = triangles;
    8.         //assign uv
    9.         flatHexagonSharedMesh.uv = uv;
    This part we create a new mesh and assign it to flatHexagonSharedMesh, this field will be used in the future as the mesh for all the HexInfo scripts instead of them remaking each hexagon. We simply plug in all of our data into this new mesh.

    Code (CSharp):
    1.  
    2.         //set temp gameObject's mesh to the flat hexagon mesh
    3.         inst.GetComponent<MeshFilter>().mesh = flatHexagonSharedMesh;
    4.         //make object play nicely with lighting
    5.         inst.GetComponent<MeshFilter>().mesh.RecalculateNormals();
    6.         //set mesh collider's mesh to the flat hexagon
    7.         inst.GetComponent<MeshCollider>().sharedMesh = flatHexagonSharedMesh;
    8.        
    Here we start to assign our temporary hexagon, the one in which we will use to pull our dimensions from, the flatHexagonSharedMesh. We have it create a mesh collider from it and recalculate our normals on this mesh. To put it in layman's term : "We made a hexagon from our flatHexagonSharedMesh and are about to get some value from this.

    Code (CSharp):
    1. //calculate the extents of the flat hexagon
    2. hexExt = new Vector3(inst.gameObject.collider.bounds.extents.x, inst.gameObject.collider.bounds.extents.y, inst.gameObject.collider.bounds.extents.z);
    3. //calculate the size of the flat hexagon
    4. hexSize = new Vector3(inst.gameObject.collider.bounds.size.x, inst.gameObject.collider.bounds.size.y, inst.gameObject.collider.bounds.size.z);
    5. //calculate the center of the flat hexagon
    6. hexCenter = new Vector3(inst.gameObject.collider.bounds.center.x, inst.gameObject.collider.bounds.center.y, inst.gameObject.collider.bounds.center.z);
    7. //destroy the temp object that we used to calculate the flat hexagon's size
    8. Destroy(inst);
    9. #endregion
    Now we finally get to do what the whole point of this method was. We get to cache some values. We assign our three Vector3's to the temporary collider's values. I don't know how to explain it better. It should be pretty self-explanatory. After we have gained what we wanted, we dispose of the source. (Like all good thugs)

    So now we have completed that method. We have all the needed values to start on spawning some chunks!

    4. Spawning meh some chunks baby!

    Finally we are ready to start spawning the chunks(which btw we haven't made yet)! Chunks are GameObjects that group other objects into one, while still giving the look and functionality of separate parts. Here, we are going to have a chunk that will contain many hexagons, yet combine them into one mesh and read from only one texture. We will still have all of the functionality of separate GameObjects for each hexagon, yet with vastly improved performance with one mesh/texture. Here is more information on chunks, in this case a voxel engine. However, the concepts still apply. CHUNK INFO

    Lets dive right in and I'll try to explain as we go.

    Code (CSharp):
    1. #region generateMap
    2.     /// <summary>
    3.     /// Generate Chunks to make the map
    4.     /// </summary>
    5.     void GenerateMap()
    6.     {
    7.  
    8.         //determine number of chunks
    9.         xSectors = Mathf.CeilToInt(mapSize.x / chunkSize);
    10.         zSectors = Mathf.CeilToInt(mapSize.y / chunkSize);
    Here we calculate the number of chunks we are going to have to make, in both x/y. We do this by taking the number of total hexagons in a direction and dividing it by the chunk size; its then rounded up to the nearest int. For example: if our mapSize is 512x256 and our chunkSize is 8 it would be: xSectors = (512/8) = 64, and ySectors = (256/8) = 32.

    Code (CSharp):
    1. //allocate chunk array
    2.         hexChunks = new HexChunk[xSectors, zSectors];
    Here we allocate all the memory and create the necessary amount of HexChunks in an array. We use a two-dimensional array for clarity.

    Code (CSharp):
    1. //cycle through all chunks
    2.         for (int x = 0; x < xSectors; x++)
    3.         {
    4.             for (int z = 0; z < zSectors; z++)
    5.             {
    6.                 //create the new chunk
    7.                 hexChunks[x, z] = NewChunk(x, z);
    8.                 //set the position of the new chunk
    9.                 hexChunks[x, z].gameObject.transform.position = new Vector3(x * (chunkSize * hexSize.x), 0f, (z * (chunkSize * hexSize.z) * (.75f)));
    10.                 //set hex size for hexagon positioning
    11.                 hexChunks[x, z].hexSize = hexSize;
    12.                 //set the number of hexagons for the chunk to generate
    13.                 hexChunks[x, z].SetSize(chunkSize, chunkSize);
    14.                 //the width interval of the chunk
    15.                 hexChunks[x, z].xSector = x;
    16.                 //set the height interval of the chunk
    17.                 hexChunks[x, z].ySector = z;
    18.                 //assign the world manager(this)
    19.                 hexChunks[x, z].worldManager = this;
    20.             }
    21.         }
    We cycle through all of our chunks in memory and then actually create the chunk. Note that we use another uncreated method NewChunk(), this will be next. We set the position of the chunk using the following method: (x(the current array position of the chunk)*(chunkSize * hexSize.x)). For example if we are at the third chunk sector, a chunkSize of 8, and a hexSize.x of 1, then we would place the object in the x axis at 24. Since each chunk is 8 wide and we are the third. Next, we set the hexSize field in our chunk so that we can use it again for placing the individual hexagons. We set the size of the chunk so that it generates the amount we want(in this case 8x8). We then set the chunks sector location. Last, we tell it who we are and cache the worldManager.

    Code (CSharp):
    1. //cycle through all chunks
    2.         foreach (HexChunk chunk in hexChunks)
    3.         {
    4.             //begin chunk operations since we are done with value generation
    5.             chunk.Begin();
    6.         }
    7. }
    To finish our method we start chunk operations on all of our newly created chunks.

    5. New Chunk

    Code (CSharp):
    1. /// <summary>
    2.     /// Creates a new chunk
    3.     /// </summary>
    4.     /// <param name="x">The width interval of the chunks</param>
    5.     /// <param name="y">The height interval of the chunks</param>
    6.     /// <returns>The new chunk's script</returns>
    7.     public HexChunk NewChunk(int x, int y)
    8.     {
    9.         //if this the first chunk made?
    10.         if (x == 0 && y == 0)
    11.         {
    12.             chunkHolder = new GameObject("ChunkHolder");
    13.         }
    14.         //create the chunk object
    15.         GameObject chunkObj = new GameObject("Chunk[" + x + "," + y + "]");
    16.         //add the hexChunk script and set it's size
    17.         chunkObj.AddComponent<HexChunk>();
    18.         //allocate the hexagon array
    19.         chunkObj.GetComponent<HexChunk>().AllocateHexArray();
    20.         //set the texture map for this chunk and add the mesh renderer
    21.         chunkObj.AddComponent<MeshRenderer>().material.mainTexture = terrainTexture;
    22.         //add the mesh filter
    23.         chunkObj.AddComponent<MeshFilter>();
    24.         //make this chunk a child of "ChunkHolder"s
    25.         chunkObj.transform.parent = chunkHolder.transform;
    26.  
    27.         //return the script on the new chunk
    28.         return chunkObj.GetComponent<HexChunk>();
    29.     }
    If this is the first chunk to be made, we make a new GameObject to hold all of them. We then create a new GameObject to be the chunk and add the HexChunk script to it, along with calling the method AllocateHexArray() on the new chunk. This will just create all the HexInfo classes in memory so that we can go through and edit them later. We add a MeshRenderer and assign a texture to the chunk. Last, we add a MeshFilter, so that later we can assign a mesh to the chunk. We parent it to the create chunkHolder object. Last, we return the newly created HexChunk.

    Checkup

    Finally! ..... We are done with the first half. Disappointed or excited that we have so much more to do?


    Here is the full WorldManager source below to make sure you are following along.

    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections;
    3.  
    4. public enum Tile { Main, Ocean }
    5.  
    6. public class WorldManager : MonoBehaviour {
    7.  
    8.     #region fields
    9.     public Mesh flatHexagonSharedMesh;
    10.     public float hexRadiusSize;
    11.  
    12.     //hexInstances
    13.     [HideInInspector]
    14.     public Vector3 hexExt;
    15.     [HideInInspector]
    16.     public Vector3 hexSize;
    17.     [HideInInspector]
    18.     public Vector3 hexCenter;
    19.     [HideInInspector]
    20.     public GameObject chunkHolder;
    21.  
    22.     public Texture2D terrainTexture;
    23.  
    24.     int xSectors;
    25.     int zSectors;
    26.  
    27.     public HexChunk[,] hexChunks;
    28.  
    29.     public Vector2 mapSize;
    30.     public int chunkSize;
    31.  
    32.     #endregion
    33.  
    34.     #region awake
    35.     public void Awake()
    36.     {
    37.         //get the flat hexagons size; we use this to space out the hexagons
    38.         GetHexProperties();
    39.         //generate the chunks of the world
    40.         GenerateMap();
    41.     }
    42.     #endregion
    43.  
    44.     #region getHexProperties
    45.     private void GetHexProperties()
    46.     {
    47.         //Creates mesh to calculate bounds
    48.         GameObject inst = new GameObject("Bounds Set Up: Flat");
    49.         //add mesh filter to our temp object
    50.         inst.AddComponent<MeshFilter>();
    51.         //add a renderer to our temp object
    52.         inst.AddComponent<MeshRenderer>();
    53.         //add a mesh collider to our temp object; this is for determining dimensions cheaply and easily
    54.         inst.AddComponent<MeshCollider>();
    55.         //reset the position to global zero
    56.         inst.transform.position = Vector3.zero;
    57.         //reset all rotation
    58.         inst.transform.rotation = Quaternion.identity;
    59.  
    60.  
    61.         Vector3[] vertices;
    62.         int[] triangles;
    63.         Vector2[] uv;
    64.  
    65.         #region verts
    66.  
    67.         float floorLevel = 0;
    68.         //positions vertices of the hexagon to make a normal hexagon
    69.         vertices = new Vector3[]
    70.             {
    71.                 /*0*/new Vector3((hexRadiusSize * Mathf.Cos((float)(2*Mathf.PI*(3+0.5)/6))), floorLevel, (hexRadiusSize * Mathf.Sin((float)(2*Mathf.PI*(3+0.5)/6)))),
    72.                 /*1*/new Vector3((hexRadiusSize * Mathf.Cos((float)(2*Mathf.PI*(2+0.5)/6))), floorLevel, (hexRadiusSize * Mathf.Sin((float)(2*Mathf.PI*(2+0.5)/6)))),
    73.                 /*2*/new Vector3((hexRadiusSize * Mathf.Cos((float)(2*Mathf.PI*(1+0.5)/6))), floorLevel, (hexRadiusSize * Mathf.Sin((float)(2*Mathf.PI*(1+0.5)/6)))),
    74.                 /*3*/new Vector3((hexRadiusSize * Mathf.Cos((float)(2*Mathf.PI*(0+0.5)/6))), floorLevel, (hexRadiusSize * Mathf.Sin((float)(2*Mathf.PI*(0+0.5)/6)))),
    75.                 /*4*/new Vector3((hexRadiusSize * Mathf.Cos((float)(2*Mathf.PI*(5+0.5)/6))), floorLevel, (hexRadiusSize * Mathf.Sin((float)(2*Mathf.PI*(5+0.5)/6)))),
    76.                 /*5*/new Vector3((hexRadiusSize * Mathf.Cos((float)(2*Mathf.PI*(4+0.5)/6))), floorLevel, (hexRadiusSize * Mathf.Sin((float)(2*Mathf.PI*(4+0.5)/6))))
    77.             };
    78.  
    79.         #endregion
    80.  
    81.         #region triangles
    82.  
    83.         //triangles connecting the verts
    84.         triangles = new int[]
    85.         {
    86.             1,5,0,
    87.             1,4,5,
    88.             1,2,4,
    89.             2,3,4
    90.         };
    91.  
    92.         #endregion
    93.  
    94.         #region uv
    95.         //uv mappping
    96.         uv = new Vector2[]
    97.             {
    98.                 new Vector2(0,0.25f),
    99.                 new Vector2(0,0.75f),
    100.                 new Vector2(0.5f,1),
    101.                 new Vector2(1,0.75f),
    102.                 new Vector2(1,0.25f),
    103.                 new Vector2(0.5f,0),
    104.             };
    105.         #endregion
    106.  
    107.         #region finalize
    108.         //create new mesh to hold the data for the flat hexagon
    109.         flatHexagonSharedMesh = new Mesh();
    110.         //assign verts
    111.         flatHexagonSharedMesh.vertices = vertices;
    112.         //assign triangles
    113.         flatHexagonSharedMesh.triangles = triangles;
    114.         //assign uv
    115.         flatHexagonSharedMesh.uv = uv;
    116.         //set temp gameObject's mesh to the flat hexagon mesh
    117.         inst.GetComponent<MeshFilter>().mesh = flatHexagonSharedMesh;
    118.         //make object play nicely with lighting
    119.         inst.GetComponent<MeshFilter>().mesh.RecalculateNormals();
    120.         //set mesh collider's mesh to the flat hexagon
    121.         inst.GetComponent<MeshCollider>().sharedMesh = flatHexagonSharedMesh;
    122.         #endregion
    123.  
    124.         //calculate the extents of the flat hexagon
    125.         hexExt = new Vector3(inst.gameObject.collider.bounds.extents.x, inst.gameObject.collider.bounds.extents.y, inst.gameObject.collider.bounds.extents.z);
    126.         //calculate the size of the flat hexagon
    127.         hexSize = new Vector3(inst.gameObject.collider.bounds.size.x, inst.gameObject.collider.bounds.size.y, inst.gameObject.collider.bounds.size.z);
    128.         //calculate the center of the flat hexagon
    129.         hexCenter = new Vector3(inst.gameObject.collider.bounds.center.x, inst.gameObject.collider.bounds.center.y, inst.gameObject.collider.bounds.center.z);
    130.         //destroy the temp object that we used to calculate the flat hexagon's size
    131.         Destroy(inst);
    132.     }
    133.     #endregion
    134.  
    135.     #region generateMap
    136.     /// <summary>
    137.     /// Generate Chunks to make the map
    138.     /// </summary>
    139.     void GenerateMap()
    140.     {
    141.  
    142.         //determine number of chunks
    143.         xSectors = Mathf.CeilToInt(mapSize.x / chunkSize);
    144.         zSectors = Mathf.CeilToInt(mapSize.y / chunkSize);
    145.  
    146.         //allocate chunk array
    147.         hexChunks = new HexChunk[xSectors, zSectors];
    148.  
    149.         //cycle through all chunks
    150.         for (int x = 0; x < xSectors; x++)
    151.         {
    152.             for (int z = 0; z < zSectors; z++)
    153.             {
    154.                 //create the new chunk
    155.                 hexChunks[x, z] = NewChunk(x, z);
    156.                 //set the position of the new chunk
    157.                 hexChunks[x, z].gameObject.transform.position = new Vector3(x * (chunkSize * hexSize.x), 0f, (z * (chunkSize * hexSize.z) * (.75f)));
    158.                 //set hex size for hexagon positioning
    159.                 hexChunks[x, z].hexSize = hexSize;
    160.                 //set the number of hexagons for the chunk to generate
    161.                 hexChunks[x, z].SetSize(chunkSize, chunkSize);
    162.                 //the width interval of the chunk
    163.                 hexChunks[x, z].xSector = x;
    164.                 //set the height interval of the chunk
    165.                 hexChunks[x, z].ySector = z;
    166.                 //assign the world manager(this)
    167.                 hexChunks[x, z].worldManager = this;
    168.             }
    169.         }
    170.  
    171.         //cycle through all chunks
    172.         foreach (HexChunk chunk in hexChunks)
    173.         {
    174.             //begin chunk operations since we are done with value generation
    175.             chunk.Begin();
    176.         }
    177.  
    178.     }
    179.     #endregion
    180.  
    181.     #region NewChunk
    182.     /// <summary>
    183.     /// Creates a new chunk
    184.     /// </summary>
    185.     /// <param name="x">The width interval of the chunks</param>
    186.     /// <param name="y">The height interval of the chunks</param>
    187.     /// <returns>The new chunk's script</returns>
    188.     public HexChunk NewChunk(int x, int y)
    189.     {
    190.         //if this the first chunk made?
    191.         if (x == 0 && y == 0)
    192.         {
    193.             chunkHolder = new GameObject("ChunkHolder");
    194.         }
    195.         //create the chunk object
    196.         GameObject chunkObj = new GameObject("Chunk[" + x + "," + y + "]");
    197.         //add the hexChunk script and set it's size
    198.         chunkObj.AddComponent<HexChunk>();
    199.         //allocate the hexagon array
    200.         chunkObj.GetComponent<HexChunk>().AllocateHexArray();
    201.         //set the texture map for this chunk and add the mesh renderer
    202.         chunkObj.AddComponent<MeshRenderer>().material.mainTexture = terrainTexture;
    203.         //add the mesh filter
    204.         chunkObj.AddComponent<MeshFilter>();
    205.         //make this chunk a child of "ChunkHolder"s
    206.         chunkObj.transform.parent = chunkHolder.transform;
    207.  
    208.         //return the script on the new chunk
    209.         return chunkObj.GetComponent<HexChunk>();
    210.     }
    211.     #endregion
    212.  
    213. }
    B. HexChunk

    The main point of the HexChunk script is to house all of our HexInfo's and to combine them into one efficient mesh for runtime.

    Leggo!

    1. Setup to chunk

    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections;
    3.  
    4. public class HexChunk : MonoBehaviour
    5. {
    6.     #region fields
    7.     [SerializeField]
    8.     public HexInfo[,] hexArray;
    9.     public int xSize;
    10.     public int ySize;
    11.     public Vector3 hexSize;
    12.  
    13.     //set by world master
    14.     public int xSector;
    15.     public int ySector;
    16.     public WorldManager worldManager;
    17.  
    18.     private MeshFilter filter;
    19.     private new BoxCollider collider;
    20.     #endregion
    Here is all of our fields for this class. Once again they should explain themselves as we go.

    Code (CSharp):
    1.  
    2. public void SetSize(int x, int y)
    3.     {
    4.         xSize = x;
    5.         ySize = y;
    6.     }
    This is just a little helper method to set the size of the chunk. Say 8x8 like in our example.

    Code (CSharp):
    1.  
    2. public void OnDestroy()
    3.     {
    4.         Destroy(renderer.material);
    5.     }
    This method is called from Unity's internals when the object, in this case a chunk, is destroyed. We just want it to clean up behind itself and delete it's material.

    Code (CSharp):
    1.  
    2. public void AllocateHexArray()
    3.     {
    4.         hexArray = new HexInfo[xSize, ySize];
    5.     }
    This simply sets up the array for modification later of all of our hex's. Remember xSize/ySize? Here they are now; they are used to determine the size of the arrays.

    Code (CSharp):
    1.  
    2. public void Begin()
    3.     {
    4.         GenerateChunk();
    5.         for (int x = 0; x < xSize; x++)
    6.         {
    7.             for (int z = 0; z < ySize; z++)
    8.             {
    9.                 if (hexArray[x, z] != null)
    10.                 {
    11.                     hexArray[x, z].parentChunk = this;
    12.                     hexArray[x, z].Start();
    13.                 }
    14.                 else
    15.                 {
    16.                     print("null hexagon found in memory");
    17.                 }
    18.             }
    19.         }
    20.         Combine();
    21.     }
    This method is called from our WorldManager and starts all the operations on our HexChunk. First we call GenerateChunk(), which we will make next. Then, we cycle through all of the hexs and sets the parentChunk to the chunk that is creating it.(this) Then we start operations on the hex. Last, once all the hex's are done with their thing we call Combine() which will combine all our hex meshes into one.

    2. Chunking those mofos

    We combine the hexagons into chunks instead of rendering each hexagon separately to conserve drawcalls and multiple other overheads. If we had each hexagons its own GameObject the overhead for each transform, texture, and mesh would be insanely high.

    Code (CSharp):
    1. public void GenerateChunk()
    2.     {
    3.         bool odd;
    4.  
    5.         for (int y = 0; y < ySize; y++)
    6.         {
    7.             odd = (y % 2) == 0;
    8.             if (odd == true)
    9.             {
    10.                 for (int x = 0; x < xSize; x++)
    11.                 {
    12.                     GenerateHex(x, y);
    13.                 }
    14.             }
    15.             else
    16.             {
    17.                     for (int x = 0; x < xSize; x++)
    18.                     {
    19.                         GenerateHexOffset(x, y);
    20.                     }
    21.             }
    22.         }
    23.     }
    This is handling the offsets of hexagons on odd rows. Each calls a GenerateHex(int, int) method that generates a new hex for the specific location(x,y). GenerateHexOffset() is the same method, just with a little bit of positioning difference to account for offset rows.

    3. Actually making a new hex

    Code (CSharp):
    1. public void GenerateHex(int x, int y)
    2.     {
    3.         //cache and create hex hex
    4.         HexInfo hex;
    5.         Vector2 worldArrayPosition;
    6.         hexArray[x, y] = new HexInfo();
    7.         hex = hexArray[x, y];
    8.  
    9.         //set world array position for real texture positioning
    10.         worldArrayPosition.x = x + (xSize * xSector);
    11.         worldArrayPosition.y = y + (ySize * ySector);
    12.  
    13.         hex.CubeGridPosition = new Vector3(worldArrayPosition.x - Mathf.Round((worldArrayPosition.y / 2) + .1f), worldArrayPosition.y, -(worldArrayPosition.x - Mathf.Round((worldArrayPosition.y / 2) + .1f) + worldArrayPosition.y));
    14.         //set local position of hex; this is the hex cord postion local to the chunk
    15.         hex.localPosition = new Vector3((x * (worldManager.hexExt.x * 2) + worldManager.hexExt.x), 0, (y * worldManager.hexExt.z) * 1.5f);
    16.         //set world position of hex; this is the hex cord postion local to the world
    17.         hex.worldPosition = new Vector3(hex.localPosition.x + (xSector * (xSize * hexSize.x)), hex.localPosition.y, hex.localPosition.z + ((ySector * (ySize * hexSize.z)) * (.75f)));
    18.  
    19.         ///Set Hex values
    20.         hex.hexExt = worldManager.hexExt;
    21.         hex.hexCenter = worldManager.hexCenter;
    22.     }
    Like always, lets break it down.

    Code (CSharp):
    1.  
    2. void GenerateHex(int x, int y){
    3. bool odd;
    4. for (int y = 0; y < ySize; y++)
    5. {
    6.     odd = (y % 2) == 0;
    7.     if (odd == true)
    8.             {
    9.                  for (int x = 0; x < xSize; x++)
    10.                  {
    11.  
    This simply checks if we are in an offset row or not. Don't be intimidated. If we are - we take the first path, otherwise we take the second. We begin to cycle through all of our hex's in memory and start doing something with them!

    Code (CSharp):
    1.  
    2. //cache and create hex hex
    3. HexInfo hex;
    4. Vector2 worldArrayPosition;
    5. hexArray[x, y] = new HexInfo();
    6. hex = hexArray[x, y];
    We create a new HexInfo in memory and cache it for later use. Also notice worldArrayPosition.

    Code (CSharp):
    1.  
    2. //set world array position
    3. worldArrayPosition.x = x + (xSize * xSector);
    4. worldArrayPosition.y = y + (ySize * ySector);
    This is setting worldArrayPosition to the location of the HexInfo in memory of the world hexArray. Basically the indexes to find this hex in the world array.

    Code (CSharp):
    1.  
    2. hex.CubeGridPosition = new Vector3(worldArrayPosition.x - Mathf.Round((worldArrayPosition.y / 2) + .1f), worldArrayPosition.y, -(worldArrayPosition.x - Mathf.Round((worldArrayPosition.y / 2) + .1f) + worldArrayPosition.y));
    3. //set local position of hex; this is the hex cord postion local to the chunk
    4. hex.localPosition = new Vector3(x * ((worldManager.hexExt.x * 2)), 0, (y * worldManager.hexExt.z) * 1.5f);
    5. //set world position of hex; this is the hex cord postion local to the world
    6. hex.worldPosition = new Vector3(hex.localPosition.x + (xSector * (xSize * hexSize.x)), hex.localPosition.y, hex.localPosition.z + ((ySector * (ySize * hexSize.z)) * (.75f)));
    7.  
    This sets all of our hex's internal positioning. I'm NOT going to go into the math behind this, however feel free to investigate as it should be pretty easy to figure out. CubeGridPosition is the hex's coordinates in the Cube coordinate system. This is not a positional location, more of a representative location. localPosition is the location of the hex local to the chunk. If the chunk is at (10,10,10) and the hex is at (12,12,12); then the localPosition of the hex would be (2,2,2) as it is relative to the chunk. This is useful for calculations working in the chunk. worldPosition is what it seems and is the global position of the hex.

    Code (CSharp):
    1.  
    2. ///Set Hex values
    3. hex.hexExt = worldManager.hexExt;
    4. hex.hexCenter = worldManager.hexCenter;
    5. }
    The only difference with the method GenerateHexOffset() is the one line:

    Code (CSharp):
    1. hex.localPosition = new Vector3((x * (worldManager.hexExt.x * 2)) + worldManager.hexExt.x, 0, (y * worldManager.hexExt.z) * 1.5f);
    This is due to the fact that these rows need to be offset so, they are shifted half a hex over by adding hexEct.x to the x axis.

    Finally done with GenerateChunk(). We start on Combine() from the Begin() method of earlier.

    4. Combining those hexes

    Code (CSharp):
    1. private void Combine()
    2. {
    3.     CombineInstance[,] combine = new CombineInstance[xSize, ySize];
    4.  
    5.     for (int x = 0; x < xSize; x++)
    6.     {
    7.         for (int z = 0; z < ySize; z++)
    8.         {
    9.  
    10.             combine[x, z].mesh = hexArray[x, z].localMesh;
    11.             Matrix4x4 matrix = new Matrix4x4();
    12.             matrix.SetTRS(hexArray[x, z].localPosition, Quaternion.identity, Vector3.one);
    13.             combine[x, z].transform = matrix;
    14.         }
    15.     }
    16.  
    17.     filter = gameObject.GetComponent<MeshFilter>();
    18.     filter.mesh = new Mesh();
    19.  
    20.     CombineInstance[] final;
    21.  
    22.     CivGridUtility.ToSingleArray(combine, out final);
    23.  
    24.     filter.mesh.CombineMeshes(final);
    25.     filter.mesh.RecalculateNormals();
    26.     filter.mesh.RecalculateBounds();
    27.     MakeCollider();
    28. }
    Break down #351616545654:

    Code (CSharp):
    1. private void Combine()
    2. {
    3.     CombineInstance[,] combine = new CombineInstance[xSize, ySize];
    CombineInstance is a cool little class that are object that get combined in a mesh.CombineMeshes() method. They are pretty cool.

    Code (CSharp):
    1. for (int x = 0; x < xSize; x++)
    2. {
    3.     for (int z = 0; z < ySize; z++)
    4.     {
    5.  
    6.         combine[x, z].mesh = hexArray[x, z].localMesh;
    7.         Matrix4x4 matrix = new Matrix4x4();
    8.         matrix.SetTRS(hexArray[x, z].localPosition, Quaternion.identity, Vector3.one);
    9.         combine[x, z].transform = matrix;
    10.      }
    11. }
    Here we cycle through all of the hexs in our chunk and assign the localMesh to the CombineInstance mesh. We make a new Matrix4x4 which is a complex topic. Plenty of other resources. SetTRS() is basically setting a transform component with (position, rotation, and scale). We assign our hex's world position to the matrices position, no rotations, and a scale of one. We then assign the matrix to our CombineInstance's trasnform. Essentially setting the hex's mesh to it's worldLocation.

    Code (CSharp):
    1. filter = gameObject.GetComponent<MeshFilter>();
    2. filter.mesh = new Mesh();
    3.  
    4. CombineInstance[] final;
    5.  
    6. CivGridUtility.ToSingleArray(combine, out final);
    7.  
    8. filter.mesh.CombineMeshes(final);
    9. filter.mesh.RecalculateNormals();
    10. MakeCollider();
    We get the MeshFIlter and create a new mesh on it. We shift all of the CombineInstance[,] into a single array using a helper method we will create soon. We then combine all of meshes in CombineInstance[] into one using the CombineMeshes() method in the mesh class. We recalculate the normal to place nicely with lighting and then call MakeCollider().

    5. Making those colliders

    Code (CSharp):
    1. void MakeCollider()
    2. {
    3.     if (collider == null)
    4.     {
    5.         collider = gameObject.AddComponent<BoxCollider>();
    6.      }
    7.      collider.center = filter.mesh.bounds.center;
    8.      collider.size = filter.mesh.bounds.size;
    9. }
    Ahh, thank whoever is close to you that this method is short. If this chunk doesn't have a collider we make a BoxCollider and assign it's size to our meshes'. Simple and short.

    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections;
    3. using System.Collections.Generic;
    4.  
    5. public static class CivGridUtility
    6. {
    7.     public static void ToSingleArray(CombineInstance[,] doubleArray, out CombineInstance[] singleArray)
    8.     {
    9.         List<CombineInstance> combineList = new List<CombineInstance>();
    10.  
    11.         foreach (CombineInstance combine in doubleArray)
    12.         {
    13.             combineList.Add(combine);
    14.         }
    15.  
    16.         singleArray = combineList.ToArray();
    17.     }
    18. }
    Here is our helper class to combine the two-dimensional array into an one-dimensional array. I don't think this tutorial should be covering this and I'm pretty tired. So figure it out yourself! :p

    C. HexInfo

    Last, part guys! I promise. You're a trooper for making it this far, you don't have much longer to go.

    HexInfo is simply a hexagon that stores some extra data. We are going to start fresh from last section so delete your version, but don't worry, all that work increased our knowledge and a lot of it made it into WorldManager.

    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections;
    3.  
    4. public class HexInfo
    5. {
    6.     private Vector3 gridPosition;//cube cordinates stored(x,y == axial)
    7.     public Vector3 localPosition;
    8.     public Vector3 worldPosition;
    9.  
    10.     public Vector3 hexExt;
    11.     public Vector3 hexCenter;
    12.  
    13.     public HexChunk parentChunk;
    14.  
    15.     public Mesh localMesh;
    16.  
    17.  
    18.     //basic hexagon mesh making
    19.     public Vector3[] vertices;
    20.     public Vector2[] uv;
    21.     public int[] triangles;
    22.  
    23.     //get axial grid position
    24.     public Vector2 AxialGridPosition
    25.     {
    26.         get { return new Vector2(CubeGridPosition.x, CubeGridPosition.y); }
    27.     }
    28.     //get/set cube grid position
    29.     public Vector3 CubeGridPosition
    30.     {
    31.         get { return gridPosition; }
    32.         set { gridPosition = value; }
    33.     }
    All of our fields and two properties.Axial coordinates are simply cube coordinates without the z axis, therefore the contained property.

    Code (CSharp):
    1. public void Start()
    2.     {
    3.         MeshSetup();
    4.     }
    This method is called by our parentChunk as soon as everything else is ready for us to start generating. We call MeshSetup().

    Code (CSharp):
    1. void MeshSetup()
    2.     {
    3.         localMesh = new Mesh();
    4.  
    5.         localMesh.vertices = parentChunk.worldManager.flatHexagonSharedMesh.vertices;
    6.         localMesh.triangles = parentChunk.worldManager.flatHexagonSharedMesh.triangles;
    7.         localMesh.uv = parentChunk.worldManager.flatHexagonSharedMesh.uv;
    8.  
    9.         localMesh.RecalculateNormals();
    10.     }
    We reset the localMesh to a new mesh and assign vertices, triangles, and uv from the cached hexagon in WorldManager, flatHexagonSharedMesh. Basically, we copy the base hexagon from earlier so that we don't waste time recalculating data that is constant and already done. We then make it place nicely with lighting with RecalculateNormals().

    D. Conclusion

    Finally! I really hope I didn't lose you guys or drop in quality anywhere. It was a pretty epic tutorial section and I forced myself to write it up in two sections. So let me know if I moved too fast over a part or any mistakes. It's been fun, and let me know what you guys think and if it helped you at all.

    Here are source files for anyone having trouble : Files

    Time to flipping enjoy your new (chunked) procedural hexagon grid. Have fun guys, and thanks for being an awesome community. Once again, any feedback is amazing. ("Your a f***ing moron" is valid feedback and pretty ironic. :p )

    Best Regards!
    Landon
     
    Last edited: Jul 5, 2014
    Acatriki, zhuchun and EliasMasche like this.
  4. landon912

    landon912

    Joined:
    Nov 8, 2011
    Posts:
    1,579
    Reserved for Section 3!
     
    czkczm and EliasMasche like this.
  5. Sartoris

    Sartoris

    Joined:
    Dec 8, 2013
    Posts:
    21
    Just dropping by to express my enthusiasm :) This is something that will interest many of us, I think, so thanks for making the tutorial!
     
  6. landon912

    landon912

    Joined:
    Nov 8, 2011
    Posts:
    1,579
    First tutorial section done!
     
  7. Shirken

    Shirken

    Joined:
    Sep 5, 2013
    Posts:
    7
    Please tell me you are going to continue on with this tutorial? I have been trying to learn this exact kind of thing for several weeks without much luck. This first one really helped clear up a lot of things that have been troubling me.

    Very very much appreciated!
     
  8. IK

    IK

    Joined:
    Oct 29, 2012
    Posts:
    34
    +1, I'd love to see more of this! Thanks you what you have done already.
     
  9. landon912

    landon912

    Joined:
    Nov 8, 2011
    Posts:
    1,579
    I plan to get around to this soon guys! If you don't hear anything from me; keep posting and I'll be forced to do it. Been working on the plugin in my now small amount of free time.
     
  10. niltoid

    niltoid

    Joined:
    Jan 26, 2014
    Posts:
    2
    more please!
     
  11. MrPhil

    MrPhil

    Joined:
    Sep 22, 2010
    Posts:
    40
    Do not stop!!! Please!
     
  12. MarioSantoso

    MarioSantoso

    Joined:
    May 6, 2013
    Posts:
    47
    Next section please, thanks and great work!
     
  13. dolecekj

    dolecekj

    Joined:
    Apr 9, 2014
    Posts:
    2
    +1 Nice tutorial. First section helped me a lot. Next section ASAP (as soon as possible) please.

    Sorry for my bad english
     
  14. MrPhil

    MrPhil

    Joined:
    Sep 22, 2010
    Posts:
    40
    I just noticed this line has a potential bug:

    Code (csharp):
    1.     renderer.material.mainTexture = texture;
    In some circumstances, that will clone the material. To prevent a memory leak you need to destroy it yourself, Unity can't clean it up automatically.

    Code (csharp):
    1. void OnDestory()
    2.     {
    3.         Object.Destroy(renderer.material);
    4.     }
    Here's the warning in the docs: http://docs.unity3d.com/Documentation/ScriptReference/Renderer-material.html
     
  15. landon912

    landon912

    Joined:
    Nov 8, 2011
    Posts:
    1,579
    I was not aware of this. Fortunately, our system will NOT be doing any GameObject.Instatiate()/Destroy() on the per hexagon level, therefore to the best of my knowledge that line is safe(and not even in the final product). This line will also be removed in an upcoming section when we impliment the texture atlas. I've tested CivGrid heavily and am proud to say it is 100% "runtime allocation free" after the first frame and plays nicely with our friend the GC. Thanks for bringing this to my attention, I'll remember to keep it in mind in the future.
     
  16. MrPhil

    MrPhil

    Joined:
    Sep 22, 2010
    Posts:
    40
    Happy to help. It's one of those obscure gotchas.

    I have a question about the math behind the Vertices. I was using the mesh generated with some Assets from the Store and they didn't seem to agree on the corner positions. I tried to make heads or tails of this link http://www.redblobgames.com/grids/hexagons/ comparing with yours, but it still not clear to me what is going on. Does it have to do with flat top versus point top?

    Here's an image of my problem. The green is their Mesh and the blue is mine: http://imgur.com/jr6Oebn
     
    Last edited: May 19, 2014
  17. landon912

    landon912

    Joined:
    Nov 8, 2011
    Posts:
    1,579
    Im not sure what your asking....

    That link is a very good resource and I used it to gain THEORY on some topics. However, the document does require a fair amount of prior knowledge and time to grasp. Make sure to opt into "Pointy Topped" as that changes all of the pictures and some text.

    Im not sure why the difference in hexagons is an issue, unless you want to combine the two into one map for some reason. Their mesh however is using a fan-triangulation style, of which I can not suggest. I find the rectangular style to be much more efficient. (That two triangle difference will be a big issue when we handle 10,000+ hexagons). The other difference in size shouldn't be an issue and is just personal preference and this method is very simple to grasp with nice even numbers. Feel free to mess around with the vertex positioning; it's pretty easy!
     
    Last edited: May 21, 2014
  18. MrPhil

    MrPhil

    Joined:
    Sep 22, 2010
    Posts:
    40
    OH! So you aren't using hexagons with equal edges! That explains it. I was using the Meshes created from this Tutorial with a Meshes from a different system for Navigation (Path Finding.) It didn't work, and now I understand why! Thanks!
     
  19. Sorcerak

    Sorcerak

    Joined:
    May 19, 2013
    Posts:
    7
    Waiting patiently for section 2 :)
     
  20. MrPhil

    MrPhil

    Joined:
    Sep 22, 2010
    Posts:
    40
    First, let me say I apologize for hijacking this thread away from the tutorial, but I did a fair amount of work and it could save someone else the trouble. Sorry ;)

    Okay, I figured out the math. Below is the basic formula for calculating the corners (or Points) in a Hexagon with edges all the same. FYI these are called Regular Hexagons

    Terms:
    r = radius or edge length (the six outside lines)
    N = N-sided polygon
    i = vertex index
    Centered at (0,0)

    Then i vertices are given by:
    x = r * cos(2*pi*i/N)
    y = r * sin(2*pi*i/N)

    WAIT! There is a problem here... which style of hexagon is this describing Pointy Topped like the tutorial's or Flat Top? Well, as it turns out this is the Flat Top style. Lucky it super simple to adjust it to Pointy Topped! Just add a half turn to the n term:
    x = r * cos(2*pi*(i+0.5)/N)
    y = r * sin(2*pi*(i+0.5)/N)

    Using values from the tutorial:
    r = 1 (the distance from the Point #0 at (-1,-0.5) to Point #1 at (-1,0.5) aka the length of the edge)
    N = 6 (the sides of a hexagon)

    The formula reduces to:
    x = 1 * cos(2*pi*(i+0.5)/6)
    y = 1 * sin(2*pi*(i+0.5)/6)

    And 1 * anything = anything, so
    x = cos(2*pi*(i+0.5)/6)
    y = sin(2*pi*(i+0.5)/6)

    You could also write this as:
    (x,y) = (cos(2*pi*(i+0.5)/6), sin(2*pi*(i+0.5)/6))

    Now, there is another trick. The order of the vertices matters! So when i = 0 that is NOT Point #0 for the tutorial. Plotting out the points makes the mapping obvious.

    $Points.gif

    So, finally, substituting this into code:
    Code (csharp):
    1.  
    2.             Vertices = new Vector3[]
    3.             {
    4.                 new Vector3(-0.866025f, floorLevel, -0.5f), // i = 3, Point #0
    5.                 new Vector3(-0.866025f, floorLevel,  0.5f), // i = 2, Point #1
    6.                 new Vector3(0, floorLevel, 1), // i = 1, Point #2
    7.                 new Vector3(0.866025f, floorLevel,  0.5f), // i = 0, Point #3
    8.                 new Vector3(0.866025f, floorLevel,  -0.5f), // i = 5, Point #4
    9.                 new Vector3(0, floorLevel, -1), // i = 4, Point #5
    10.             };
    11.  
    Note 0.0.866025 is sqrt(3)/2 rounded up.
     

    Attached Files:

    Last edited: May 29, 2014
    stolverko and landon912 like this.
  21. landon912

    landon912

    Joined:
    Nov 8, 2011
    Posts:
    1,579
    Good work, I'll add this into the main tutorial tonight. I'll also make sure to maintain both in future sections, so you won't have to always be converting values. This is within CivGrid, but I thought it would be better to keep things simple. I might add a "Intermediate" part to the tutorials to add things like this for guys with a little more knowledge. Over all, great work and you seem like you've got the stuff needed to get this job done wether I help or not. ;)

    Here is some working code that you can plug into our tutorial so far to get resizable regular hexagons:

    Code (csharp):
    1.  
    2. #region verts
    3.            
    4.             float floorLevel = 0;
    5.             vertices = new Vector3[]
    6.             {
    7.                 /*0*/new Vector3((hexRadiusSize * Mathf.Cos((float)(2*Mathf.PI*(3+0.5)/6))), floorLevel, (hexRadiusSize * Mathf.Sin((float)(2*Mathf.PI*(3+0.5)/6)))),
    8.                 /*1*/new Vector3((hexRadiusSize * Mathf.Cos((float)(2*Mathf.PI*(2+0.5)/6))), floorLevel, (hexRadiusSize * Mathf.Sin((float)(2*Mathf.PI*(2+0.5)/6)))),
    9.                 /*2*/new Vector3((hexRadiusSize * Mathf.Cos((float)(2*Mathf.PI*(1+0.5)/6))), floorLevel, (hexRadiusSize * Mathf.Sin((float)(2*Mathf.PI*(1+0.5)/6)))),
    10.                 /*3*/new Vector3((hexRadiusSize * Mathf.Cos((float)(2*Mathf.PI*(0+0.5)/6))), floorLevel, (hexRadiusSize * Mathf.Sin((float)(2*Mathf.PI*(0+0.5)/6)))),
    11.                 /*4*/new Vector3((hexRadiusSize * Mathf.Cos((float)(2*Mathf.PI*(5+0.5)/6))), floorLevel, (hexRadiusSize * Mathf.Sin((float)(2*Mathf.PI*(5+0.5)/6)))),
    12.                 /*5*/new Vector3((hexRadiusSize * Mathf.Cos((float)(2*Mathf.PI*(4+0.5)/6))), floorLevel, (hexRadiusSize * Mathf.Sin((float)(2*Mathf.PI*(4+0.5)/6))))
    13.             };
    14.            
    15.   #endregion
    Edit: Also for clarity, I think it would be easier to use "i" for the indexer variable. Using "N"/"n" may throw some people off.
     
    Last edited: May 21, 2014
    EliasMasche likes this.
  22. Tzan

    Tzan

    Joined:
    Apr 5, 2009
    Posts:
    733
    If you want a specific distance between hexagon centers you can set hexRadiusSize.

    radius = wantedDistance / sqrt(3)

    The center to center distance is also the hex side to side distance.
    Printed sheets of hex paper usually have a set side to side distance, like 1 inch.
     
  23. Sartoris

    Sartoris

    Joined:
    Dec 8, 2013
    Posts:
    21
    Please continue working on this tutorial, it's really helpful!:)
     
  24. Tzan

    Tzan

    Joined:
    Apr 5, 2009
    Posts:
    733
    Hi Landon, you had this line in your code.
    Code (csharp):
    1.  
    2. //recalcualte the dimensions of our mesh
    3. mesh.RecalculateBounds();
    4.  
    In the Script reference for RecalculateBounds():
    So you dont need to do that, because its already being done.
    If you where moving existing verts then you would want to do it.
    Its no big deal for one hex, but you'll probably be making hundreds.

    http://docs.unity3d.com/ScriptReference/Mesh.RecalculateBounds.html
     
    landon912 likes this.
  25. landon912

    landon912

    Joined:
    Nov 8, 2011
    Posts:
    1,579
    Good catch! The more you know. I always wondered why that method never seemed to effect anything, should have checked it myself.
     
  26. MrPhil

    MrPhil

    Joined:
    Sep 22, 2010
    Posts:
    40
    Thanks! My project is moving forward :)

    This is a good point, I'll change my earlier post.
     
  27. landon912

    landon912

    Joined:
    Nov 8, 2011
    Posts:
    1,579
    Section Two will be around this weekend. I'm all caught up with school work and am about to send out a new build for CviGrid. So, I'll be able to find the necessary(a lot) of time to complete the next section for you guys! Cya in a day or two!
     
  28. Braza

    Braza

    Joined:
    Oct 11, 2013
    Posts:
    136
    Hi!
    Will this end up with somewhat like below?
     
  29. Tzan

    Tzan

    Joined:
    Apr 5, 2009
    Posts:
    733
    Landon's hex tutorial has 4 triangles to a hex, so its harder to make bendy terrain like that.

    I've been messing around with 6 triangles, for no real reason :)
     
    landon912 likes this.
  30. landon912

    landon912

    Joined:
    Nov 8, 2011
    Posts:
    1,579
    No. We are going for a product of more like Civilization 5, with an even terrain, and hills/mountains confined.

    Yep, the more triangles, the more you can do with them! (Our mountain hex's are 1024*1024) ;)
     
    Braza likes this.
  31. nkarnick

    nkarnick

    Joined:
    Jun 3, 2014
    Posts:
    1
    I'm having trouble puzzling out the UV positions for this modified regular hexagon. Any chance we could get updated code like above?

    Edit: Sorry, I should probably expand on that question. If I have a square texture, but the part I want to use is in the shape of the hex we've drawn (maybe I want a black border on the edges), how can I adapt the math above to correspond to the 0,0 - 1,1 ranges of the UV map?
     
    Last edited: Jun 10, 2014
  32. landon912

    landon912

    Joined:
    Nov 8, 2011
    Posts:
    1,579
    The UV mapping algorithm should be the exact same. For the UV mapping we were actually using regular hexagons. If you make a texture of any size, that has straight(maybe bold) lines from each of our points as shown in the UV mapping diagram it SHOULD line up. You can try to use the provided diagram as the texture, but I'm not sure if that is a good reference as it may be out of proportion.

    Also, just a disclaimer: I'm running all different versions of code from you guys. It can be pretty confusing juggling both these at the same time. I didn't test the above statement, so feel free to dispute.

    Section 2 is coming soon. I'm trying to get another build of CivGrid out to the alpha testers before I update this.
     
    Last edited: Jun 12, 2014
  33. landon912

    landon912

    Joined:
    Nov 8, 2011
    Posts:
    1,579
    Second Section is half way done! Posted.The rest will be edited into it tomorrow.

    Enjoy! Let me know what you think so far.
     
  34. landon912

    landon912

    Joined:
    Nov 8, 2011
    Posts:
    1,579
    Section Two is now done! It's long guys!
     
    dolecekj likes this.
  35. dolecekj

    dolecekj

    Joined:
    Apr 9, 2014
    Posts:
    2
    +1 Now section 3 please
     
  36. Khoram

    Khoram

    Joined:
    Nov 7, 2012
    Posts:
    3
    Thanks so much for this. I have a prototype of something similar though more basic as it's only 2D written in PyGame that I was just about to start porting to Unity, and this is perfect for learning how to do that. Keep up the good work!
     
  37. Khoram

    Khoram

    Joined:
    Nov 7, 2012
    Posts:
    3
    I have a couple newbie questions if you don't mind.

    What exactly is the purpose of separating the map into chunks? I ask because I want to modify so that I can define the texture on a per-hex basis, and your code currently doesn't calculate the offset correctly if chunk size is 1. So I was going to re-write it, but would then lose whatever functionality the chunk system is providing.
     
  38. landon912

    landon912

    Joined:
    Nov 8, 2011
    Posts:
    1,579
    We combine the hexagons into chunks instead of rendering each hexagon separately to conserve drawcalls and multiple other overheads. If we had each hexagons its own GameObject the overhead for each transform, texture, and mesh would be insanely high. What you're looking for to texture these hexagons is called texture atlasing. We will cover this in the next section and how to use a texture atlas to render each hexagon it's correct texture.
     
  39. Khoram

    Khoram

    Joined:
    Nov 7, 2012
    Posts:
    3
    Ah I see, that makes sense. Thanks for the response, I look forward to the next section! :)
     
  40. landon912

    landon912

    Joined:
    Nov 8, 2011
    Posts:
    1,579
    I updated the tutorial to make the chunking section a little more explicit on the reason/use of chunking.Let me know if anyone still has issues understanding.
     
  41. landon912

    landon912

    Joined:
    Nov 8, 2011
    Posts:
    1,579
    I know this is some what off topic, but hang in there. I'm currently looking to add a small number of testers to the private CivGrid alpha/beta testing. I am looking for testers of all backgrounds and technical knowledge; please PM me if you are interested.
     
  42. Dingham

    Dingham

    Joined:
    Jul 10, 2014
    Posts:
    1
    First off, thank you for this. I have been really struggling to find decent information on how to generate a hex grid until i found this. You are awesome for putting in the effect into making this tutorial.

    I have been going through the tutorial this morning. I have discovered some issues. Some I have been able to resolve. The last I am stuck on as I am a unity noob and too numb to figure out right now.

    First issue I came across were only typo's. In your first script HexInfo.cs you mis spelt RecalculateNormals in finalize. As a noob it took 20mins to figure out why the script wasn't working :p I know its later resolved. At the beginning of the script you also reference Vertices & Triangles in the code they are down as lower case. These are not major, clearly just typo's thought I would point out though.


    The major issue I'm having is in HexChunk.cs. Under GenerateHex and GenerateHexOffset I am getting this error.
    and the same for x. In both instances. I understand,ish what its' saying. Can't get my head round on how to fix though.

    Also you link to final project doesn't contain the code.


    Also on a slightly unrelated note to errors...

    I assume its going to be a square map when it generates. Would it be possible to generate as a hex shape itself?
     
  43. RockoDyne

    RockoDyne

    Joined:
    Apr 10, 2014
    Posts:
    2,234
    It's a conflict in variable names. Presumably x and y are declared as a public/private variable but some area also declared an x and y variable. The quick fix is to find the local x and y variable and change their name, but make sure that the main x and y variables weren't what was intended.

    Given the way this stores hexes, it will be easiest to make square maps. The biggest difficulty with hexes usually comes from mapping coordinates. A lot of solutions end up actually using three axes. If you want some reading http://www.redblobgames.com/grids/hexagons/ covers a lot of the issues, but it's more theory and less immediately applicable.
     
  44. landon912

    landon912

    Joined:
    Nov 8, 2011
    Posts:
    1,579
    Make sure your braces ( {} ) close correctly. There shouldn't be a naming conflict if the for loops or methods aren't flowing into each other.

    Also, I just tested the like to the files and I can download the .unitypackage just fine. Anyone having this issue?

    I'd be perfectly possible in our current setup to alter the placement algorithm to make a hexagon, however you'd have to add catches for null hexagons in the array. Not too hard.
     
  45. konsnos

    konsnos

    Joined:
    Feb 13, 2012
    Posts:
    121
    Nice! I'm really optimistic about this one. My main concern is how you will get to blend the meshes of the tiles (if we have o hill and a field how will they fade to each other), and the same for their materials. I hope you have already thought of that :D

    Keep it up, Landon!
     
  46. landon912

    landon912

    Joined:
    Nov 8, 2011
    Posts:
    1,579
    Unfortunately, I have no plans to cover that in this tutorial. One of the reasons I have made this tutorial is a code review and to develop a free version of CivGrid. Right now, the described topics will only be included in the pro version of CivGrid and not covered in the tutorial. However, in the next and final section we will cover how to start on a basic version of the realistic world.
     
    konsnos likes this.
  47. konsnos

    konsnos

    Joined:
    Feb 13, 2012
    Posts:
    121
    Yeah, sorry for the misunderstanding. I know you said that you won't explain that. I just expressed my hope that you have found a solution, which you did and hope it really turns out well, because I will probably have a look at it :D

    I'm trying to create my own custom solution atm but it's always preferable to pick one already working ( with reasonable cost that is ). I can figure out a solution for combining tile meshes for the above example to make a realistic crossfade. What takes time is to figure out a solution to support many textures and materials for the hexes to which I thought the solution of vertex blending but haven't put that one to the test just yet due to heavy work load ( thus the mood to buy an already made asset :p )

    Anyway, just dropping my thoughts here. I'm looking forward to your third post :)
     
  48. Testnia

    Testnia

    Joined:
    Jul 4, 2014
    Posts:
    11
    Would like to applaud you on such a fine tutorial, especially for a beginner like myself. The guide was concise and helped me understand much more about Unity scripting.

    I have a question, although it might seem a little off-topic. I'm looking to generate similar hexagonal walls scattered across the terrain. Trouble is I'm unsure where to start and how to align them to the shape of the map. Could I somehow edit the file such that they spawn in three dimensions instead of two? And if so, if you could point me to which part of the code I should look into editing I'll be really grateful.

    Once again, love the guide and keep it up!
     
  49. landon912

    landon912

    Joined:
    Nov 8, 2011
    Posts:
    1,579
    This is what is currently being worked on actually and holding me back from a v1.0 release. I can recommend to you NOT to try to use a vertex buffer for each border vertex. It simply doesn't work well and is extremly slow/memory hog. Would this however be a feature needed in a v1.0 release in your opinion? Or would it be better to release it without and add it in a future "graphics update"?

    Are you asking about making a raised hexagon? Basically, an extruded hexagon? If so, you need to update the vertices, triangles, and UV data. Try to play around and the ideas should be the same as in tutorial section one. I'm very busy everyday until Thursday. So if you can't get it, drop a post here Thursday and I'll write up a sample project for you. However, be sure to clarify what you mean.

    Thanks for the kind words everyone.
     
  50. konsnos

    konsnos

    Joined:
    Feb 13, 2012
    Posts:
    121
    To me yes, it's a must. It wouldn't be up to the game's standards if not. I'll put that to a test hopefully today and if I find anything interesting I'll drop a line.