[OPEN SOURCE]Procedural Hexagon Terrain

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

  3. Positioning each hexagon

  4. Configuring hexagons

  5. Coordinate System

  6. Rendering

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

  8. Perlin Noise

  9. 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

1 Like

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!


I. HexInfo

Let’s start off by making our first script : “HexInfo!”

public class HexInfo : MonoBehaviour
{
    //basic hexagon mesh making
    public Vector3[] Vertices;
    public Vector2[] uv;
    public int[] Triangles;

    void Start()
    {
        MeshSetup();
    }

    void MeshSetup()
    {
        #region verts
   
        #endregion

        #region triangles

        #endregion

        #region uv

        #endregion

        #region finalize

        #endregion

    }
}

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!


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!

       #region verts
         
        float floorLevel = 0;
        vertices = new Vector3[]
        {
            new Vector3(-1f , floorLevel, -.5f),
            new Vector3(-1f, floorLevel, .5f),
            new Vector3(0f, floorLevel, 1f),
            new Vector3(1f, floorLevel, .5f),
            new Vector3(1f, floorLevel, -.5f),
            new Vector3(0f, floorLevel, -1f)
        };

        #endregion

This marks our first region done! We can’t test it yet but we are 1/3 to making a hexagon!


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.

#region triangles

        triangles = new int[]
        {
            1,5,0,
            1,4,5,
            1,2,4,
            2,3,4
        };

        #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.


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.

#region UV

        uv = new Vector2[]
        {
            new Vector2(0,0.25f),
            new Vector2(0,0.75f),
            new Vector2(0.5f,1),
            new Vector2(1,0.75f),
            new Vector2(1,0.25f),
            new Vector2(0.5f,0),
        };

        #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.


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:

//add this texture field to the top near the other fields for testing our UV coords
public Texture texture;
#region finalize

        //add a mesh filter to the GO the script is attached to; cache it for later
        MeshFilter meshFilter = gameObject.AddComponent<MeshFilter>();
        //add a mesh renderer to the GO the script is attached to
        gameObject.AddComponent<MeshRenderer>();

        //create a mesh object to pass our data into
        Mesh mesh = new Mesh();

        //add our vertices to the mesh
        mesh.vertices = vertices;
        //add our triangles to the mesh
        mesh.triangles = triangles;
        //add out UV coordinates to the mesh
        mesh.uv = uv;
     
        //make it play nicely with lighting
        mesh.RecalculateNormals();

        //set the GO's meshFilter's mesh to be the one we just made
        meshFilter.mesh = mesh;

        //UV TESTING
        renderer.material.mainTexture = texture;

        #endregion

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.


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

2 Likes

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.

using UnityEngine;
using System.Collections;

public class WorldManager : MonoBehaviour
{
    #region fields

    #endregion

    #region GetHexProperties

    #endregion

    #region GenerateMap

    #endregion

    #region NewChunk

    #endregion
}

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:

#region fields
public Mesh flatHexagonSharedMesh;
public float hexRadiusSize;

//hexInstances
[HideInInspector]
public Vector3 hexExt;
[HideInInspector]
public Vector3 hexSize;
[HideInInspector]
public Vector3 hexCenter;
[HideInInspector]
public GameObject chunkHolder;

public Texture2D terrainTexture;

int xSectors;
int zSectors;

public HexChunk[,] hexChunks;

public Vector2 mapSize;
public int chunkSize;

#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.

#region awake
public void Awake()
{
    //get the flat hexagons size; we use this to space out the hexagons and chunks
    GetHexProperties();
    //generate the chunks of the world
    GenerateMap();
}
#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.

#region GetHexProperties
private void GetHexProperties()
{
    //Creates mesh to calculate bounds
    GameObject inst = new GameObject("Bounds Set Up: Flat");
    //add mesh filter to our temp object
    inst.AddComponent<MeshFilter>();
    //add a renderer to our temp object
    inst.AddComponent<MeshRenderer>();
    //add a mesh collider to our temp object; this is for determining dimensions cheaply and easily
    inst.AddComponent<MeshCollider>();
    //reset the position to global zero
    inst.transform.position = Vector3.zero;
    //reset all rotation
    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.

Vector3[] vertices;
int[] triangles;

float floorLevel = 0;
//positions vertices of the hexagon to make a normal hexagon
vertices = new Vector3[]
{
    /*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)))),
    /*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)))),
    /*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)))),
    /*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)))),
   /*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)))),
   /*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))))
            };

//triangles connecting the verts
triangles = new int[]
{
    1,5,0,
    1,4,5,
    1,2,4,
    2,3,4
};

uv = new Vector2[]
{
     new Vector2(0,0.25f),
     new Vector2(0,0.75f),
     new Vector2(0.5f,1),
     new Vector2(1,0.75f),
     new Vector2(1,0.25f),
     new Vector2(0.5f,0),
};

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.

//create new mesh to hold the data for the flat hexagon
flatHexagonSharedMesh = new Mesh();
//assign verts
flatHexagonSharedMesh.vertices = vertices;
//assign triangles
flatHexagonSharedMesh.triangles = triangles;
//assign uv
flatHexagonSharedMesh.uv = uv;
//set temp gameObject's mesh to the flat hexagon mesh
inst.GetComponent<MeshFilter>().mesh = flatHexagonSharedMesh;
//make object play nicely with lighting
inst.GetComponent<MeshFilter>().mesh.RecalculateNormals();
//set mesh collider's mesh to the flat hexagon
inst.GetComponent<MeshCollider>().sharedMesh = flatHexagonSharedMesh;

//calculate the extents of the flat hexagon
hexExt = new Vector3(inst.gameObject.collider.bounds.extents.x, inst.gameObject.collider.bounds.extents.y, inst.gameObject.collider.bounds.extents.z);
//calculate the size of the flat hexagon
hexSize = new Vector3(inst.gameObject.collider.bounds.size.x, inst.gameObject.collider.bounds.size.y, inst.gameObject.collider.bounds.size.z);
//calculate the center of the flat hexagon
hexCenter = new Vector3(inst.gameObject.collider.bounds.center.x, inst.gameObject.collider.bounds.center.y, inst.gameObject.collider.bounds.center.z);
//destroy the temp object that we used to calculate the flat hexagon's size
Destroy(inst);
#endregion

Alright, now this is another massive code block. Lets once again break it down.

        //create new mesh to hold the data for the flat hexagon
        flatHexagonSharedMesh = new Mesh();
        //assign verts
        flatHexagonSharedMesh.vertices = vertices;
        //assign triangles
        flatHexagonSharedMesh.triangles = triangles;
        //assign uv
        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.

        //set temp gameObject's mesh to the flat hexagon mesh
        inst.GetComponent<MeshFilter>().mesh = flatHexagonSharedMesh;
        //make object play nicely with lighting
        inst.GetComponent<MeshFilter>().mesh.RecalculateNormals();
        //set mesh collider's mesh to the flat hexagon
        inst.GetComponent<MeshCollider>().sharedMesh = flatHexagonSharedMesh;

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.

//calculate the extents of the flat hexagon
hexExt = new Vector3(inst.gameObject.collider.bounds.extents.x, inst.gameObject.collider.bounds.extents.y, inst.gameObject.collider.bounds.extents.z);
//calculate the size of the flat hexagon
hexSize = new Vector3(inst.gameObject.collider.bounds.size.x, inst.gameObject.collider.bounds.size.y, inst.gameObject.collider.bounds.size.z);
//calculate the center of the flat hexagon
hexCenter = new Vector3(inst.gameObject.collider.bounds.center.x, inst.gameObject.collider.bounds.center.y, inst.gameObject.collider.bounds.center.z);
//destroy the temp object that we used to calculate the flat hexagon's size
Destroy(inst);
#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.

#region generateMap
    /// <summary>
    /// Generate Chunks to make the map
    /// </summary>
    void GenerateMap()
    {

        //determine number of chunks
        xSectors = Mathf.CeilToInt(mapSize.x / chunkSize);
        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.

//allocate chunk array
        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.

//cycle through all chunks
        for (int x = 0; x < xSectors; x++)
        {
            for (int z = 0; z < zSectors; z++)
            {
                //create the new chunk
                hexChunks[x, z] = NewChunk(x, z);
                //set the position of the new chunk
                hexChunks[x, z].gameObject.transform.position = new Vector3(x * (chunkSize * hexSize.x), 0f, (z * (chunkSize * hexSize.z) * (.75f)));
                //set hex size for hexagon positioning
                hexChunks[x, z].hexSize = hexSize;
                //set the number of hexagons for the chunk to generate
                hexChunks[x, z].SetSize(chunkSize, chunkSize);
                //the width interval of the chunk
                hexChunks[x, z].xSector = x;
                //set the height interval of the chunk
                hexChunks[x, z].ySector = z;
                //assign the world manager(this)
                hexChunks[x, z].worldManager = this;
            }
        }

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.

//cycle through all chunks
        foreach (HexChunk chunk in hexChunks)
        {
            //begin chunk operations since we are done with value generation
            chunk.Begin();
        }
}

To finish our method we start chunk operations on all of our newly created chunks.

5. New Chunk

/// <summary>
    /// Creates a new chunk
    /// </summary>
    /// <param name="x">The width interval of the chunks</param>
    /// <param name="y">The height interval of the chunks</param>
    /// <returns>The new chunk's script</returns>
    public HexChunk NewChunk(int x, int y)
    {
        //if this the first chunk made?
        if (x == 0 && y == 0)
        {
            chunkHolder = new GameObject("ChunkHolder");
        }
        //create the chunk object
        GameObject chunkObj = new GameObject("Chunk[" + x + "," + y + "]");
        //add the hexChunk script and set it's size
        chunkObj.AddComponent<HexChunk>();
        //allocate the hexagon array
        chunkObj.GetComponent<HexChunk>().AllocateHexArray();
        //set the texture map for this chunk and add the mesh renderer
        chunkObj.AddComponent<MeshRenderer>().material.mainTexture = terrainTexture;
        //add the mesh filter
        chunkObj.AddComponent<MeshFilter>();
        //make this chunk a child of "ChunkHolder"s
        chunkObj.transform.parent = chunkHolder.transform;

        //return the script on the new chunk
        return chunkObj.GetComponent<HexChunk>();
    }

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.

using UnityEngine;
using System.Collections;

public enum Tile { Main, Ocean }

public class WorldManager : MonoBehaviour {

    #region fields
    public Mesh flatHexagonSharedMesh;
    public float hexRadiusSize;

    //hexInstances
    [HideInInspector]
    public Vector3 hexExt;
    [HideInInspector]
    public Vector3 hexSize;
    [HideInInspector]
    public Vector3 hexCenter;
    [HideInInspector]
    public GameObject chunkHolder;

    public Texture2D terrainTexture;

    int xSectors;
    int zSectors;

    public HexChunk[,] hexChunks;

    public Vector2 mapSize;
    public int chunkSize;

    #endregion

    #region awake
    public void Awake()
    {
        //get the flat hexagons size; we use this to space out the hexagons
        GetHexProperties();
        //generate the chunks of the world
        GenerateMap();
    }
    #endregion

    #region getHexProperties
    private void GetHexProperties()
    {
        //Creates mesh to calculate bounds
        GameObject inst = new GameObject("Bounds Set Up: Flat");
        //add mesh filter to our temp object
        inst.AddComponent<MeshFilter>();
        //add a renderer to our temp object
        inst.AddComponent<MeshRenderer>();
        //add a mesh collider to our temp object; this is for determining dimensions cheaply and easily
        inst.AddComponent<MeshCollider>();
        //reset the position to global zero
        inst.transform.position = Vector3.zero;
        //reset all rotation
        inst.transform.rotation = Quaternion.identity;


        Vector3[] vertices;
        int[] triangles;
        Vector2[] uv;

        #region verts

        float floorLevel = 0;
        //positions vertices of the hexagon to make a normal hexagon
        vertices = new Vector3[]
            {
                /*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)))),
                /*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)))),
                /*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)))),
                /*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)))),
                /*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)))),
                /*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))))
            };

        #endregion

        #region triangles

        //triangles connecting the verts
        triangles = new int[]
        {
            1,5,0,
            1,4,5,
            1,2,4,
            2,3,4
        };

        #endregion

        #region uv
        //uv mappping
        uv = new Vector2[]
            {
                new Vector2(0,0.25f),
                new Vector2(0,0.75f),
                new Vector2(0.5f,1),
                new Vector2(1,0.75f),
                new Vector2(1,0.25f),
                new Vector2(0.5f,0),
            };
        #endregion

        #region finalize
        //create new mesh to hold the data for the flat hexagon
        flatHexagonSharedMesh = new Mesh();
        //assign verts
        flatHexagonSharedMesh.vertices = vertices;
        //assign triangles
        flatHexagonSharedMesh.triangles = triangles;
        //assign uv
        flatHexagonSharedMesh.uv = uv;
        //set temp gameObject's mesh to the flat hexagon mesh
        inst.GetComponent<MeshFilter>().mesh = flatHexagonSharedMesh;
        //make object play nicely with lighting
        inst.GetComponent<MeshFilter>().mesh.RecalculateNormals();
        //set mesh collider's mesh to the flat hexagon
        inst.GetComponent<MeshCollider>().sharedMesh = flatHexagonSharedMesh;
        #endregion

        //calculate the extents of the flat hexagon
        hexExt = new Vector3(inst.gameObject.collider.bounds.extents.x, inst.gameObject.collider.bounds.extents.y, inst.gameObject.collider.bounds.extents.z);
        //calculate the size of the flat hexagon
        hexSize = new Vector3(inst.gameObject.collider.bounds.size.x, inst.gameObject.collider.bounds.size.y, inst.gameObject.collider.bounds.size.z);
        //calculate the center of the flat hexagon
        hexCenter = new Vector3(inst.gameObject.collider.bounds.center.x, inst.gameObject.collider.bounds.center.y, inst.gameObject.collider.bounds.center.z);
        //destroy the temp object that we used to calculate the flat hexagon's size
        Destroy(inst);
    }
    #endregion

    #region generateMap
    /// <summary>
    /// Generate Chunks to make the map
    /// </summary>
    void GenerateMap()
    {

        //determine number of chunks
        xSectors = Mathf.CeilToInt(mapSize.x / chunkSize);
        zSectors = Mathf.CeilToInt(mapSize.y / chunkSize);

        //allocate chunk array
        hexChunks = new HexChunk[xSectors, zSectors];

        //cycle through all chunks
        for (int x = 0; x < xSectors; x++)
        {
            for (int z = 0; z < zSectors; z++)
            {
                //create the new chunk
                hexChunks[x, z] = NewChunk(x, z);
                //set the position of the new chunk
                hexChunks[x, z].gameObject.transform.position = new Vector3(x * (chunkSize * hexSize.x), 0f, (z * (chunkSize * hexSize.z) * (.75f)));
                //set hex size for hexagon positioning
                hexChunks[x, z].hexSize = hexSize;
                //set the number of hexagons for the chunk to generate
                hexChunks[x, z].SetSize(chunkSize, chunkSize);
                //the width interval of the chunk
                hexChunks[x, z].xSector = x;
                //set the height interval of the chunk
                hexChunks[x, z].ySector = z;
                //assign the world manager(this)
                hexChunks[x, z].worldManager = this;
            }
        }

        //cycle through all chunks
        foreach (HexChunk chunk in hexChunks)
        {
            //begin chunk operations since we are done with value generation
            chunk.Begin();
        }

    }
    #endregion

    #region NewChunk
    /// <summary>
    /// Creates a new chunk
    /// </summary>
    /// <param name="x">The width interval of the chunks</param>
    /// <param name="y">The height interval of the chunks</param>
    /// <returns>The new chunk's script</returns>
    public HexChunk NewChunk(int x, int y)
    {
        //if this the first chunk made?
        if (x == 0 && y == 0)
        {
            chunkHolder = new GameObject("ChunkHolder");
        }
        //create the chunk object
        GameObject chunkObj = new GameObject("Chunk[" + x + "," + y + "]");
        //add the hexChunk script and set it's size
        chunkObj.AddComponent<HexChunk>();
        //allocate the hexagon array
        chunkObj.GetComponent<HexChunk>().AllocateHexArray();
        //set the texture map for this chunk and add the mesh renderer
        chunkObj.AddComponent<MeshRenderer>().material.mainTexture = terrainTexture;
        //add the mesh filter
        chunkObj.AddComponent<MeshFilter>();
        //make this chunk a child of "ChunkHolder"s
        chunkObj.transform.parent = chunkHolder.transform;

        //return the script on the new chunk
        return chunkObj.GetComponent<HexChunk>();
    }
    #endregion

}

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

using UnityEngine;
using System.Collections;

public class HexChunk : MonoBehaviour
{
    #region fields
    [SerializeField]
    public HexInfo[,] hexArray;
    public int xSize;
    public int ySize;
    public Vector3 hexSize;

    //set by world master
    public int xSector;
    public int ySector;
    public WorldManager worldManager;

    private MeshFilter filter;
    private new BoxCollider collider;
    #endregion

Here is all of our fields for this class. Once again they should explain themselves as we go.

public void SetSize(int x, int y)
    {
        xSize = x;
        ySize = y;
    }

This is just a little helper method to set the size of the chunk. Say 8x8 like in our example.

public void OnDestroy()
    {
        Destroy(renderer.material);
    }

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.

public void AllocateHexArray()
    {
        hexArray = new HexInfo[xSize, ySize];
    }

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.

public void Begin()
    {
        GenerateChunk();
        for (int x = 0; x < xSize; x++)
        {
            for (int z = 0; z < ySize; z++)
            {
                if (hexArray[x, z] != null)
                {
                    hexArray[x, z].parentChunk = this;
                    hexArray[x, z].Start();
                }
                else
                {
                    print("null hexagon found in memory");
                }
            }
        }
        Combine();
    }

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.

public void GenerateChunk()
    {
        bool odd;

        for (int y = 0; y < ySize; y++)
        {
            odd = (y % 2) == 0;
            if (odd == true)
            {
                for (int x = 0; x < xSize; x++)
                {
                    GenerateHex(x, y);
                }
            }
            else
            {
                    for (int x = 0; x < xSize; x++)
                    {
                        GenerateHexOffset(x, y);
                    }
            }
        }
    }

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

public void GenerateHex(int x, int y)
    {
        //cache and create hex hex
        HexInfo hex;
        Vector2 worldArrayPosition;
        hexArray[x, y] = new HexInfo();
        hex = hexArray[x, y];

        //set world array position for real texture positioning
        worldArrayPosition.x = x + (xSize * xSector);
        worldArrayPosition.y = y + (ySize * ySector);

        hex.CubeGridPosition = new Vector3(worldArrayPosition.x - Mathf.Round((worldArrayPosition.y / 2) + .1f), worldArrayPosition.y, -(worldArrayPosition.x - Mathf.Round((worldArrayPosition.y / 2) + .1f) + worldArrayPosition.y));
        //set local position of hex; this is the hex cord postion local to the chunk
        hex.localPosition = new Vector3((x * (worldManager.hexExt.x * 2) + worldManager.hexExt.x), 0, (y * worldManager.hexExt.z) * 1.5f);
        //set world position of hex; this is the hex cord postion local to the world
        hex.worldPosition = new Vector3(hex.localPosition.x + (xSector * (xSize * hexSize.x)), hex.localPosition.y, hex.localPosition.z + ((ySector * (ySize * hexSize.z)) * (.75f)));

        ///Set Hex values
        hex.hexExt = worldManager.hexExt;
        hex.hexCenter = worldManager.hexCenter;
    }

Like always, lets break it down.

void GenerateHex(int x, int y){
bool odd;
for (int y = 0; y < ySize; y++)
{
    odd = (y % 2) == 0;
    if (odd == true)
            {
                 for (int x = 0; x < xSize; x++)
                 {

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!

//cache and create hex hex
HexInfo hex;
Vector2 worldArrayPosition;
hexArray[x, y] = new HexInfo();
hex = hexArray[x, y];

We create a new HexInfo in memory and cache it for later use. Also notice worldArrayPosition.

//set world array position
worldArrayPosition.x = x + (xSize * xSector);
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.

hex.CubeGridPosition = new Vector3(worldArrayPosition.x - Mathf.Round((worldArrayPosition.y / 2) + .1f), worldArrayPosition.y, -(worldArrayPosition.x - Mathf.Round((worldArrayPosition.y / 2) + .1f) + worldArrayPosition.y));
//set local position of hex; this is the hex cord postion local to the chunk
hex.localPosition = new Vector3(x * ((worldManager.hexExt.x * 2)), 0, (y * worldManager.hexExt.z) * 1.5f);
//set world position of hex; this is the hex cord postion local to the world
hex.worldPosition = new Vector3(hex.localPosition.x + (xSector * (xSize * hexSize.x)), hex.localPosition.y, hex.localPosition.z + ((ySector * (ySize * hexSize.z)) * (.75f)));

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.

///Set Hex values
hex.hexExt = worldManager.hexExt;
hex.hexCenter = worldManager.hexCenter;
}

The only difference with the method GenerateHexOffset() is the one line:

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

private void Combine()
{
    CombineInstance[,] combine = new CombineInstance[xSize, ySize];

    for (int x = 0; x < xSize; x++)
    {
        for (int z = 0; z < ySize; z++)
        {

            combine[x, z].mesh = hexArray[x, z].localMesh;
            Matrix4x4 matrix = new Matrix4x4();
            matrix.SetTRS(hexArray[x, z].localPosition, Quaternion.identity, Vector3.one);
            combine[x, z].transform = matrix;
        }
    }

    filter = gameObject.GetComponent<MeshFilter>();
    filter.mesh = new Mesh();

    CombineInstance[] final;

    CivGridUtility.ToSingleArray(combine, out final);

    filter.mesh.CombineMeshes(final);
    filter.mesh.RecalculateNormals();
    filter.mesh.RecalculateBounds();
    MakeCollider();
}

Break down #351616545654:

private void Combine()
{
    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.

for (int x = 0; x < xSize; x++)
{
    for (int z = 0; z < ySize; z++)
    {

        combine[x, z].mesh = hexArray[x, z].localMesh;
        Matrix4x4 matrix = new Matrix4x4();
        matrix.SetTRS(hexArray[x, z].localPosition, Quaternion.identity, Vector3.one);
        combine[x, z].transform = matrix;
     }
}

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.

filter = gameObject.GetComponent<MeshFilter>();
filter.mesh = new Mesh();

CombineInstance[] final;

CivGridUtility.ToSingleArray(combine, out final);

filter.mesh.CombineMeshes(final);
filter.mesh.RecalculateNormals();
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

void MakeCollider()
{
    if (collider == null)
    {
        collider = gameObject.AddComponent<BoxCollider>();
     }
     collider.center = filter.mesh.bounds.center;
     collider.size = filter.mesh.bounds.size;
}

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.

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public static class CivGridUtility
{
    public static void ToSingleArray(CombineInstance[,] doubleArray, out CombineInstance[] singleArray)
    {
        List<CombineInstance> combineList = new List<CombineInstance>();

        foreach (CombineInstance combine in doubleArray)
        {
            combineList.Add(combine);
        }

        singleArray = combineList.ToArray();
    }
}

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! :stuck_out_tongue:

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.

using UnityEngine;
using System.Collections;

public class HexInfo
{
    private Vector3 gridPosition;//cube cordinates stored(x,y == axial)
    public Vector3 localPosition;
    public Vector3 worldPosition;

    public Vector3 hexExt;
    public Vector3 hexCenter;

    public HexChunk parentChunk;

    public Mesh localMesh;


    //basic hexagon mesh making
    public Vector3[] vertices;
    public Vector2[] uv;
    public int[] triangles;

    //get axial grid position
    public Vector2 AxialGridPosition
    {
        get { return new Vector2(CubeGridPosition.x, CubeGridPosition.y); }
    }
    //get/set cube grid position
    public Vector3 CubeGridPosition
    {
        get { return gridPosition; }
        set { gridPosition = value; }
    }

All of our fields and two properties.Axial coordinates are simply cube coordinates without the z axis, therefore the contained property.

public void Start()
    {
        MeshSetup();
    }

This method is called by our parentChunk as soon as everything else is ready for us to start generating. We call MeshSetup().

void MeshSetup()
    {
        localMesh = new Mesh();

        localMesh.vertices = parentChunk.worldManager.flatHexagonSharedMesh.vertices;
        localMesh.triangles = parentChunk.worldManager.flatHexagonSharedMesh.triangles;
        localMesh.uv = parentChunk.worldManager.flatHexagonSharedMesh.uv;

        localMesh.RecalculateNormals();
    }

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. :stuck_out_tongue: )

Best Regards!
Landon

3 Likes

Reserved for Section 3!

2 Likes

Just dropping by to express my enthusiasm :slight_smile: This is something that will interest many of us, I think, so thanks for making the tutorial!

First tutorial section done!

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!

+1, I’d love to see more of this! Thanks you what you have done already.

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.

more please!

Do not stop!!! Please!

Next section please, thanks and great work!

+1 Nice tutorial. First section helped me a lot. Next section ASAP (as soon as possible) please.

Sorry for my bad english

I just noticed this line has a potential bug:

    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.

void OnDestory()
    {
        Object.Destroy(renderer.material);
    }

Here’s the warning in the docs: Unity - Scripting API: Renderer.material

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.

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: Imgur: The magic of the Internet

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!

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!

Waiting patiently for section 2 :slight_smile:

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 :wink:

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(2pii/N)
y = r * sin(2pii/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(2pi(i*+0.5*)/N)
y = r * sin(2pi(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(2pi(i+0.5)/6)
y = 1 * sin(2pi(i+0.5)/6)
And 1 * anything = anything, so
x = cos(2pi(i+0.5)/6)
y = sin(2pi(i+0.5)/6)
You could also write this as:
(x,y) = (cos(2pi(i+0.5)/6), sin(2pi(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.
1633688--102316--$Points.gif
So, finally, substituting this into code:
*_</em></em></em></em></em></em></em> <em><em><em><em><em><em><em><em>* Vertices = new Vector3[] { new Vector3(-0.866025f, floorLevel, -0.5f), // i = 3, Point #0 new Vector3(-0.866025f, floorLevel, 0.5f), // i = 2, Point #1 new Vector3(0, floorLevel, 1), // i = 1, Point #2 new Vector3(0.866025f, floorLevel, 0.5f), // i = 0, Point #3 new Vector3(0.866025f, floorLevel, -0.5f), // i = 5, Point #4 new Vector3(0, floorLevel, -1), // i = 4, Point #5 };*</em></em></em></em></em></em></em></em> <em><em><em><em><em><em><em>_*
Note 0.0.866025 is sqrt(3)/2 rounded up.

1633688--100772--$Points.gif

2 Likes