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

    Testnia

    Joined:
    Jul 4, 2014
    Posts:
    11
    Thanks for the apt reply. Yes, I suppose I'm looking to extrude these hexagons in set patterns. I've attached an image that might help visualize what I have in mind.
    hexagons.jpg
    I've tried editing the script myself but keep running into innumerable amount of errors due to my lack of experience. If you could write up a minimal sample script that I could perhaps reference that will be most wonderful.

    Thanks for your assistance!
     
  2. landon912

    landon912

    Joined:
    Nov 8, 2011
    Posts:
    1,579
    I wrote up a quick sample project. It's not perfect, and the triangles are wrong in a few places and this causes texture stretching. But, I've put together a diagram and if my tutorial is any good you should be able to figure out the correct order. You can just play around with it for a bit.

    Files are attached.
     

    Attached Files:

  3. Divinorium

    Divinorium

    Joined:
    Feb 4, 2014
    Posts:
    15
    Very interesting tutorial.
    I'm curious how you access the position of each hex individually? I managed to access each chunk in the array with the code below:

    public GameObject nodepointer;
    HexChunk node;

    public void createpointer () {
    int i, j;
    int sizei = hexChunks.GetLength(0);
    int sizej = hexChunks.GetLength(1);
    for (j = 0; j < sizej; j++){
    for(i = 0; i < sizei; i++){
    node = hexChunks [j, i];
    Vector3 nodeposition = node.transform.position;
    GameObject newnodepointer = (GameObject)Instantiate (nodepointer, nodeposition, Quaternion.identity);
    newnodepointer.name = ("Nodepointer:" + j + i);
    }
    }
    }
    public void Awake()
    {
    //get the flat hexagons size; we use this to space out the hexagons
    GetHexProperties();
    //generate the chunks of the world
    GenerateMap();
    createpointer ();
    }


    I use it to create a object in the start of every chunk, but i didn't manage to access each hex...

    It's possible?

    You plan to teach pathfinding in this tutorial? I'm trying to access each hex to put nodes in the center for the pathfinding.

    Anyway nice tutorial, and i can't wait for more.

    Thx for the attention.
    Divinorium
     
    Last edited: Aug 5, 2014
  4. landon912

    landon912

    Joined:
    Nov 8, 2011
    Posts:
    1,579
    You're off to the right path so far. You're simply missing a step. In order to spawn a node on each hexagon, I've written a simple example below:

    Code (CSharp):
    1. public GameObject node;
    2. public void CreateNodes()
    3. {
    4.       for(int chunkX = 0; chunkX < hexChunks.GetLength(0); chunkX++)
    5.       {
    6.            for(int chunkY = 0; chunkY < hexChunks.GetLength(1); chunkY++)
    7.            {
    8.                  HexChunk chunk = hexChunks[chunkX, chunkY];
    9.                  for(int hexX = 0; hexX < chunk.hexArray.GetLength(0); hexX++)
    10.                  {
    11.                       for(int hexY = 0; hexY < chunk.hexArray.GetLength(0); hexY++)
    12.                       {
    13.                               HexInfo hex = chunk.hexArray[hexX, hexY];
    14.                               Instantiate(node, hex.worldPosition, Quaternion.identity);
    15.                        }
    16.                   }
    17.             }  
    18.       }
    19. }
    This the long way around. You can simplify the code using foreach loops, however I've heard many bad things about them in Unity's version of compiler. But, feel free to use them, I doubt it will matter much.


    Code (CSharp):
    1. public GameObject node;
    2. public void CreateNodes()
    3. {
    4.       foreach(HexChunk chunk in hexChunks)
    5.       {
    6.                  foreach(HexInfo hex in chunk.hexArray)
    7.                  {
    8.                          Instantiate(node, hex.worldPosition, Quaternion.identity);
    9.                   }
    10.       }
    11. }
     
    Last edited: Aug 5, 2014
    Divinorium likes this.
  5. Divinorium

    Divinorium

    Joined:
    Feb 4, 2014
    Posts:
    15
    It worked like a charm!
    Thanks.

    And not trying to be pretentious, but how "deep" you plan to go with the tutorial?
    Do you plan to explain how to use it with pathfinding?
    I'm thinking in some ways to put these 2 things together, but i'm not sure if it's the "best" way. I mean it's "possible" to generate a terrain instantiating every single hex, most of the tutorials teach this way, but i don't think it's a good way to make said terrain, right?

    Anyway, thx for the answer.
    Again, good tutorial.

    Thx for the attention.
    Divinorium

    PS: You know any tutorial about the topic, "the right way to do pathfinding"?
     
    Last edited: Aug 5, 2014
  6. landon912

    landon912

    Joined:
    Nov 8, 2011
    Posts:
    1,579
    Well for pathfinding you would have to Instatiate a node for ever hexagon, you would NOT want to Instatiate each hexagon itself(the tutorial shows one of the correct methods). For the pathfinding then, I would recommend the A* Algorithm, it's been around for a while and is a proven solution. Here's my favorite article.

    For this tutorial I honestly don't know how far I am going to take it at this time. You have to remember that I'm developing a commercial asset for this topic, and plan to have the outcome of the tutorial as the free or "trial" version so I'm still trying to find balance between usefulness and the incentive to buy my asset. I want to help you guys if you want to roll your own solution from my base example and tutorial, and simply have my asset for those who wish to save time and effort.

    Right now I'm still focused on my asset, and this is sitting on the back burner for now. Pathfinding however is an interesting topic in which I've only dabbled in. It might come in the form of this tutorial, a tutorial for using CivGrid, or not at all. If more people wish to see this let me know, and I'll decide on it.

    Best Regards!
     
    Divinorium likes this.
  7. Divinorium

    Divinorium

    Joined:
    Feb 4, 2014
    Posts:
    15
    Well that's bad news for me. XD

    I'm trying to find make my own solution, and i feel like in this fight i'm losing.

    The "solution" i'm trying to make is to make the pathfinding use the HexInfo in the hexArray as a node, so i wouldn't need to have 2 arrays with the "same" information and i would add the information that i needed in the HexInfo, if it's passable, if there's a unit, what unit is(ID), etc.

    So to "start" to work with it i was trying to separate my "script" that instantiate stuff in another one.

    Code (CSharp):
    1.  
    2. using UnityEngine;
    3. using System.Collections;
    4.  
    5. public class Testenodes2 : MonoBehaviour{
    6.  
    7.     public GameObject nodepointer;
    8.     WorldManager worldManager;
    9.  
    10.     public void Awake(){
    11.         Debug.Log ("Testnode Awake");
    12.         createpointer ();
    13.     }
    14.     // Update is called once per frame
    15.     public void createpointer () {
    16.         Debug.Log ("Before Anything");
    17.         int i, j, arrayI, arrayJ;
    18.         int sizei = worldManager.hexChunks.GetLength(0);
    19.         int sizej = worldManager.hexChunks.GetLength(1);
    20.         Debug.Log ("Length Done");
    21.         for (j = 0; j < sizej; j++){
    22.             for(i = 0; i < sizei; i++){
    23.                 HexChunk chunk = worldManager.hexChunks [j, i];
    24.                 for(arrayJ = 0; arrayJ < chunk.hexArray.GetLength(0) ; arrayJ++ ){
    25.                     for(arrayI = 0; arrayI < chunk.hexArray.GetLength(1); arrayI++){
    26.                         HexInfo hex = chunk.hexArray[arrayJ,arrayI];
    27.                         Vector3 nodeposition = hex.worldPosition;
    28.                         GameObject newnodepointer = (GameObject)Instantiate (nodepointer, nodeposition, Quaternion.identity);
    29.                         newnodepointer.name = ("Nodepointer:" + arrayJ + arrayI);
    30.                     }
    31.                 }
    32.             }
    33.         }
    34.     }
    35. }
    I'm getting a error of "NullReferenceException: Object reference not set to an instance of an object"

    Is that because i'm missing something? I mean you access the hexArray inside the HexChunk script. So i don't get why i'm getting this error. o_O

    And it's a "good idea" to take this approach?

    Thx for the attention.
    Divinorium
     
    Last edited: Aug 6, 2014
  8. landon912

    landon912

    Joined:
    Nov 8, 2011
    Posts:
    1,579
    You forgot to retrieve worldManager.

    In awake do something along the lines of:

    Code (csharp):
    1. worldManager = GameObject.GetObjectOfType<WorldManager>();
    You could in theory not spawn any nodes, and use each hex's world position as the node for pathfinding, this seems like the best approach to me.
     
    Divinorium likes this.
  9. Divinorium

    Divinorium

    Joined:
    Feb 4, 2014
    Posts:
    15
    Man i feel bad for so many questions offtopic.
    But i've tried to apply my idea and made a script to "move" a selector around the screen to check if the nodes work.

    I'm using a raycast, then i get a the point of impact.

    i divide it's x and y for the Hex's radius * 2 then i use the integer to find the hex location in the hexArray

    Looks like this method "works" to 4x4 hex but not so much to hexgrid.

    What brings me to a question: Is it possible know what hexInfo in the hexArray it hit with raycast? or i will need to spawn a collider to every hexinfo?

    I assume it's not possible to make a raycast work with the mesh you created in HexInfo, right?

    Thx for the attention.
    Divinorium.

    PS: And by spawning the colliders wouldn't make the game HEAVY and kill the purpose of making big chunks?
     
    Last edited: Aug 7, 2014
  10. landon912

    landon912

    Joined:
    Nov 8, 2011
    Posts:
    1,579
    You're totally fine. This is the whole idea behind the tutorial, to get you guys thinking and making your own cool projects.

    Now, my method for this is as described:

    I generate a BoxCollider for the whole chunk, and size it to the full chunk. Don't worry about the collider intersecting with each other a little on the edges. It wont actually matter which of the two conflicting colliders is "hit". Then gather the surrounding chunks into an array. This is so that if you click on a border hex, and the incorrect chunk collider is hit(possible since chunks aren't rectangular) the correct hex is still given. Then in each chunk designated as "possible holders of the hex" cycle through the world locations of each hexagon and find the one closested the the raycast location.
     
    Divinorium likes this.
  11. Myhijim

    Myhijim

    Joined:
    Jun 15, 2012
    Posts:
    1,148
    So when are you going to optimize colliders Landog?

    *Hijacks thread*

    Nah, seriously good work.
     
    landon912 likes this.
  12. systemerror

    systemerror

    Joined:
    Sep 8, 2014
    Posts:
    1
    Hello Landon91234, thank you for the very good tutorial.

    Im new to unity (this is my first post) and learn from this and have fun.
    i searched how to program a textureatlas:
    another good tutorial is from Alexandros Stavrinou http://studentgamedev.blogspot.de/2013/08/unity-voxel-tutorial-part-1-generating.html
    and in it he uses a textureatlas with unity and i tried if its possible to get it to work with this hex-magic.
    yes it works, maybe its interesting for others (first read Alexandros tutorial):
    setting Vector2 variables for the texturetiles for example in an array:
    public Vector2[] tatlas = new Vector2[256];
    and setting tatlas[0].Set(2,3); //water ... and so on
    setting float variable for tunit
    giving class HexInfo a constructor with a Vector2 textureparameter, changing the uv
    calling (instantiate) it from GenerateHex/GenerateHexOffset with hexarray[x,y] = new HexInfo(texturevector);
     
    Last edited: Sep 15, 2014
    landon912 likes this.
  13. Deleted User

    Deleted User

    Guest

    Cool tutorial, I'm working off this to create my own turn-based strategy game, and using some of the ideas from http://catlikecoding.com/unity/tutorials/noise/ for generating a noisy, coloured texture for each type of hex.

    With a 32x32 grid, chunk sizes above 2 give me maps that look like chess boards, i.e. regularity. I think I'm going to rework this so there is no chunking, and each hex texture is determined by a pre-calculated map of hex types. Sure, it may increase generation time, but I'm only looking to create at best one map per session.

    man, I have a LOT of work to do.
     
  14. Deleted User

    Deleted User

    Guest

    Ok, no more thanks, I have a question. Tried to do what I said in my previous post and I am stumped. I understand that we are providing a texture to the chunk, that is then inherited by all the hexes in that chunk. Makes sense, I've done a few cool things with dynamic textures that repeat per-cell and give me a nice initial board with come noisy patina, so to speak. What I can't seem to figure out is this:

    After the whole grid is set up, how can I change the texture material applied to just one cell? i.e. all my cells start out as flat blue, but when a player unit is placed on one, just that one cell turns red?
     
  15. landon912

    landon912

    Joined:
    Nov 8, 2011
    Posts:
    1,579
    You need to use a texture atlas. Then scale/transform the uv coordinates to be in the correct location of the texture atlas.

    In your example, you'd use a texture half blue and half red, then when you click a hexagon you'd edit the uv coordinates to shift it to the red side.
     
  16. Gjrud

    Gjrud

    Joined:
    May 15, 2013
    Posts:
    1
    Big premise: I'm still learning C# and Unity so my question will probably sound pretty stupid.

    in section 2, when creating the various chunks, you do this
    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.         }
    the first line of the second loop will then create a new chunk with this method:
    Code (CSharp):
    1. public HexChunk NewChunk(int x, int y)
    2.     {
    3.         //if this the first chunk made?
    4.         if (x == 0 && y == 0)
    5.         {
    6.             chunkHolder = new GameObject("ChunkHolder");
    7.         }
    8.         //create the chunk object
    9.         GameObject chunkObj = new GameObject("Chunk[" + x + "," + y + "]");
    10.         //add the hexChunk script and set it's size
    11.         chunkObj.AddComponent<HexChunk>();
    12.         //allocate the hexagon array
    13.         chunkObj.GetComponent<HexChunk>().AllocateHexArray();
    14.         //set the texture map for this chunk and add the mesh renderer
    15.         chunkObj.AddComponent<MeshRenderer>().material.mainTexture = terrainTexture;
    16.         //add the mesh filter
    17.         chunkObj.AddComponent<MeshFilter>();
    18.         //make this chunk a child of "ChunkHolder"s
    19.         chunkObj.transform.parent = chunkHolder.transform;
    20.         //return the script on the new chunk
    21.         return chunkObj.GetComponent<HexChunk>();
    22.     }
    How can you call the method "AllocateHexArray()" inside "NewChunk(int x, int y)"? As far as I understand it uses "xSize" and "ySize" but those are assigned only later on by this line:
    Code (CSharp):
    1. hexChunks[x, z].SetSize(chunkSize, chunkSize);
    Am I missing something thanks to my lack of knowledge?
    Without doing any changes to the code I get Exception for the Index being out of range when the method Begin is called.
    I commented the line "chunkObj.GetComponent<HexChunk>().AllocateHexArray();" from the method NewChunk and added "hexChunks[x, z].AllocateHexArray()" right after assigning the size of the chunk and it seems to work, but I would prefer to ask if I'm doing something wrong rather than making random changes.

    Thanks in advance, I apologize for my English.
     
  17. TwentySicks

    TwentySicks

    Joined:
    Sep 6, 2012
    Posts:
    51
    Hi Landon, thanks for your tutorial, I have learned alot about generating my own meshes from your tutorial!

    Anyway, I'm playing around with your code, and I wanted to try and change the texture of a individual hex cell. I read that I need to use a texture atlas and then shift the UV's.

    Ive tried to create a new vector2 array with some new coordinates and then apply it to a hex. Nothing happened though, and im not advanced enough to even know what is wrong xD

    Here is my code. I already figured out how to access the hex that i click on, now i just need to figure out how to change the texture! I just threw in some random coordinates to see if anything changed, but nothing seems to happen when I call the method, upon clicking a hex.

    Code (CSharp):
    1. void ShiftUV(HexInfo hex)
    2.     {
    3.         Vector2[] newUV = new Vector2[]
    4.         {
    5.             new Vector2(0,0),
    6.             new Vector2(0,1),
    7.             new Vector2(0.5f,1),
    8.             new Vector2(1,1),
    9.             new Vector2(1,0),
    10.             new Vector2(0.5f,0),
    11.         };
    12.  
    13.         hex.localMesh.uv = newUV;
    14.         hex.localMesh.Optimize();
    15.         hex.localMesh.RecalculateNormals();
    16.     }
    Am i even on the right track here? Im having a hard time wrapping my head around this atm.
    I not new to coding, ive been doing it regularly for about a year now, but some things still seem to mess with me :)
     
  18. landon912

    landon912

    Joined:
    Nov 8, 2011
    Posts:
    1,579
    This is actually working, just on the figurative mesh. The local mesh is never actually rendered, instead it's sent off to the chunk to be combined with all the other neighboring chunks and then rendered. This is important here, as you edit the local mesh, you need to tell the chunk to regenerate its mesh. This in turn will grab the updated local mesh.
     
  19. TwentySicks

    TwentySicks

    Joined:
    Sep 6, 2012
    Posts:
    51
    Code (CSharp):
    1. void ShiftUV(HexInfo hex, HexChunk chunk)
    2.     {
    3.         Vector2[] newUV = new Vector2[]
    4.         {
    5.             new Vector2(0,0),
    6.             new Vector2(0,1),
    7.             new Vector2(0.5f,1),
    8.             new Vector2(1,1),
    9.             new Vector2(1,0),
    10.             new Vector2(0.5f,0),
    11.         };
    12.  
    13.         hex.localMesh.uv = newUV;
    14.         hex.localMesh.Optimize();
    15.         hex.localMesh.RecalculateNormals();
    16.  
    17.         chunk.GenerateChunk();
    18.     }
    I added a call to the GenerateChunk method, but now it gives me a NullReferenceException on line 13 :s

    No error before i added the call on line 17
     
  20. landon912

    landon912

    Joined:
    Nov 8, 2011
    Posts:
    1,579
    Note "re"-generate the mesh on the chunk. You don't want to regenerate the entire chunk! Instead make a method on the chunk that simply takes all the hex's localmeshes and combines them as seen in the latter bit of the tutorial.
     
  21. Brendonm17

    Brendonm17

    Joined:
    Dec 9, 2013
    Posts:
    43
    Hey Landon, any news on when part 3's release date? :)
     
  22. landon912

    landon912

    Joined:
    Nov 8, 2011
    Posts:
    1,579
    I wish I could say, but I'm back in school and today I just got a new programming job! I'd like to visit this again in the future, and/or release CivGrid. However, I'm simply too busy at the moment. Sorry. Anyone is free to build off of this tutorial with their own if they have anything to share! I might release some texturing code as you guys seem to be getting stuck on that, however I'd have to strip a lot of code out of it as CivGrid is simply massive and complex.
     
  23. Brendonm17

    Brendonm17

    Joined:
    Dec 9, 2013
    Posts:
    43
    Thanks for the quick reply and update! I understand, I am currently in school myself. Also, I can't wait for CivGrid!
     
  24. landon912

    landon912

    Joined:
    Nov 8, 2011
    Posts:
    1,579
    Well, I have good news on that note. Stay tuned.
     
    Brendonm17 likes this.
  25. TwentySicks

    TwentySicks

    Joined:
    Sep 6, 2012
    Posts:
    51
    Doesnt the Combine() method do just that? Cant I just change the UV's and then call Combine() ?
     
  26. landon912

    landon912

    Joined:
    Nov 8, 2011
    Posts:
    1,579
    Yep.
     
  27. matthias55

    matthias55

    Joined:
    Dec 6, 2014
    Posts:
    1
    Hello! My question was certainly asked (and probably answered) already, but I am definitely still confused as I am brand new to Unity.

    I want to access an individual hex when I click on the grid and have it appear to be selected. I also am attempting to do a pathfinding feature at least with a selector so the player can select a path/area with individual hexes.

    I guess essentially I am looking for an expansion or at least just a combined post to the answers on Divinorium's questions.

    Either way, also wanted to say thanks for this tutorial so far! I am new to all of this and learning for fun and it was nice to see this tutorial! Can't wait to see what CivGrid looks like!
     
  28. the_big_jablonski

    the_big_jablonski

    Joined:
    Oct 30, 2014
    Posts:
    1
    Hi. First I want to say thank you for the tutorial. I'm also a noob to Unity but I've learned a lot from your code. I'm finding it difficult to follow in your code where exactly the location of each individual hex is stored. Like the guy who commented above me, I also don't understand how to access the hex I click on in-game. Try as I might I could not figure out either (or both) how to find or how to use that info in a FindHex() function in another script to sort through each chunk for the right hex.
     
  29. quantumgravity

    quantumgravity

    Joined:
    Nov 11, 2014
    Posts:
    2
    Thanks for this.
    I hope you can find time to develop it further, along the lines of previous comments,
    though as it stands it's still a great intro to the subject.
     
    landon912 likes this.
  30. landon912

    landon912

    Joined:
    Nov 8, 2011
    Posts:
    1,579
    Thanks for the kind words guys.

    Here is a rudimentary example on how to get the hex nearest to any world position from within the WorldManager script.

    Code (CSharp):
    1. public Hex GetHexFromWorldPosition(Vector3 worldPosition)
    2.         {
    3.             Hex hexToReturn = null;
    4.             float minDistance = 100;
    5.  
    6.             foreach (Chunk chunk in possibleChunks)
    7.             {
    8.                 foreach (Hex hex in chunk.hexArray)
    9.                 {
    10.                     //find lowest distance to point
    11.                     float distance = Vector3.Distance(hex.worldPosition, worldPosition);
    12.                     if (distance < minDistance)
    13.                     {
    14.                         hexToReturn = hex;
    15.                         minDistance = distance;
    16.                     }
    17.                 }
    18.             }
    19.  
    20.             return hexToReturn;
    21.         }
    Now here are some ways to improve this method and expand:

    Can you get when the mouse is clicked and convert the mouse position into a world position?
    LINK

    Could you perhaps shave off time by only checking the distance in the chunk in which you clicked on and the surrounding 9?

    Thank you.
     
  31. exiguous

    exiguous

    Joined:
    Nov 21, 2010
    Posts:
    1,749
    iterating over all hexes is VERY imperformant and does scale with size of the grid ( O(n) , cache misses). Some time ago i worked on a hex manager and have made a function to directly calculate the hex position (with 3 axis hexcoordinate system, O (1) ).

    Code (csharp):
    1.  
    2.   public HexagonClass HexagonFromPosition ( Vector3 pos )
    3.    {// finds the hexagon which is closest to the given position in 3d space (mouseclicks etc)
    4.      if (pos.y > 0.5f || pos.y < -0.5f)
    5.      {
    6.        Debug.Log ( "position outside x-z plane" );
    7.        return null;
    8.      }
    9.      sbyte x = (sbyte)Mathf.Ceil ( ( pos.x - _halfEdge ) / _overlapWidth ), y, z, x2 = (sbyte)Mathf.Ceil ( ( pos.x + _halfEdge ) / _overlapWidth );
    10.      float yoffset = 0.0f, fx, fy, yline;
    11.      // the calculation is done in any case, square region = standard + overlap regions are taken care of in x == x2 if
    12.      if (x % 2 == 0)
    13.      {// even columns have an offset, uneven (else) use standard value 0.0f;
    14.        yoffset = -_halfHeight;
    15.      }
    16.      float ftempy = ( pos.z + yoffset - (float)x * _halfHeight ) / _hexHeight;
    17.      if (( x % 2 == 1 ) || ( x % 2 == -1 ))
    18.        ftempy -= 0.5f;  // all uneven columns are shifted by a half hex upward
    19.      y = (sbyte)Mathf.Ceil ( ftempy );
    20.      if (x == x2)
    21.      {// adjust calculated indizes for the overlap area by dividing it into 2 rectangles and compare relative positions
    22.        fx = ( 1.0f - (float)x + ( ( pos.x - _halfEdge ) / _overlapWidth ) ) * 1.5f;  // the x position relative to the overlap rectangle (0-1), 0 = left
    23.        yline = Mathf.Ceil ( ( pos.z + yoffset ) / _hexHeight - 0.5f ); // this is NOT the y coordinate which is alinged by 30° this is simply the line of the hex as if counted in a 2d grid
    24.        fy = ( ( pos.z + yoffset ) / _hexHeight ) - yline + 0.5f;  // the y position relative to the overlap rectangle (0-1) 0 = downwards
    25.        if (fy < 0.5f)
    26.        {//lower part
    27.          if (( fy > fx ))
    28.          {// left part
    29.            --x;  // subtract x index as it is increased already
    30.            ++y;
    31.          }// nothing to do in right part
    32.        }
    33.        else if (fy > 0.5f)
    34.        {// upper part
    35.          if (( 1.0f - fy ) > fx)
    36.          {// left part
    37.            --x;
    38.          }// nothing to do in right part
    39.        }
    40.      }
    41.      z = (sbyte)( -x - y );  // calculate the third z position from x and y as it is redundant (but required for easy distance calculations) see: http://keekerdc.com/2011/03/hexagon-grids-coordinate-systems-and-distance-calculations/
    42.      return GetHex(new HexPositionClass(x, y, z ));
    43.    }
    44.  
    the hexagons are stored in a dictionary and keyed by their hexpositionstring. hexposition and worldposition are separate classes as they should remain constant whereas the hexcontent should vary from map to map. hope that gives you an example on how to do it "better" ;).
    If you have questions just ask but be aware that its been a while so i'm not sure i remember all the details.
     
  32. landon912

    landon912

    Joined:
    Nov 8, 2011
    Posts:
    1,579
    Yes, of course. It is slow.

    However, it's difficult to give examples when my code from CivGrid uses tons of things that I haven't shown you guys.
    So I have to strip it down to extreme basics.
     
  33. Epictickle

    Epictickle

    Joined:
    Aug 12, 2012
    Posts:
    431
    You're awesome for making this, my friend :)
     
    landon912 likes this.
  34. Divinorium

    Divinorium

    Joined:
    Feb 4, 2014
    Posts:
    15
    I'm Back!

    XD

    Landon i've been messing around with your code. And I changed the code to support different "terrains" in the hex, giving advantages and costing different "prices" to move.

    To make it as light as possible i used the same file to hold multiple textures using "uv positions" so:

    Inside the Hex info there's
    Code (CSharp):
    1. switch (terrainType){
    2.             case 0:
    3.             localMesh.uv = parentChunk.worldManager.MaterialList[StatsOfMaterials * terrainType + 1];
    4.             break;
    5.             case 1:
    6.             localMesh.uv = parentChunk.worldManager.MaterialList[StatsOfMaterials * terrainType + 1];
    7.             break;
    8.             case 2:
    9.             localMesh.uv = parentChunk.worldManager.MaterialList[StatsOfMaterials * terrainType + 1];
    10.             break;
    11.         }
    I also want to use textures to change the "border" of the hex to show that it's selected and maybe change the color of the hex itself to indicate possible effects in the hex.

    I managed to do it through:
    Code (CSharp):
    1.  
    2.     public void SelectMesh()
    3.     {
    4.         localMesh.uv = parentChunk.worldManager.MaterialList[StatsOfMaterials * terrainType];
    5.         parentChunk.Combine ();
    6.     }
    7.  
    8.  
    9.     public void UnSelectMesh()
    10.     {
    11.         localMesh.uv = parentChunk.worldManager.MaterialList[StatsOfMaterials * terrainType + 1];  
    12.         parentChunk.Combine ();
    13.     }
    I modified the Combine() on the HexChunks to be public, and to delete the last "chunkMesh" created. so it always only have the same number of meshs on the game.

    But it made me wondering there's not a better way to do so? Using projector or something... Because this way looks so "wrong".

    Thx for the attention.
    Divinorium.
     
    Brendonm17 likes this.
  35. landon912

    landon912

    Joined:
    Nov 8, 2011
    Posts:
    1,579

    For the first:

    You want to be using a texture atlas, and then map the individual local mesh's UV coordinates to scale and move onto the preferred tile of the texture atlas.

    For the second bullet:

    A more modular and efficient method would be vertex colors. In the shader, you have a border texture, and then can use vertex colors to color the border any color you desire. (For city borders, selection borders, etc)

    Here is a quick example of a shader:

    Code (CSharp):
    1. Shader "Hexagon"
    2. {
    3.     Properties
    4.     {
    5.         _MainTex ("Base (RGB)", 2D) = "white" {}
    6.         _BorderTex ("Border Texture", 2D) = "white" {}
    7.     }
    8.     SubShader
    9.     {
    10.         Tags {"IgnoreProjector"="True" "RenderType"="Opaque"}
    11.         LOD 200
    12.         ZWrite Off
    13.         Blend SrcAlpha OneMinusSrcAlpha
    14.         CGPROGRAM
    15. // Upgrade NOTE: excluded shader from OpenGL ES 2.0 because it does not contain a surface program or both vertex and fragment programs.
    16.         #pragma exclude_renderers gles
    17.         #pragma surface surf Lambert
    18.      
    19.         float4 BlendMethod( float4 a, float4 b ){
    20.    
    21.             return a * b;                     // Default
    22. //            return (a+b-1);                 // Severe.
    23. //            return (1-(1-a)*b);                // Nice effect (keeps white border) but inverts & lightens colors.
    24.        
    25.         }
    26.    
    27.         sampler2D _MainTex;
    28.         sampler2D _BlendTex;
    29.        
    30.  
    31.         struct Input
    32.         {
    33.             float2 uv_MainTex;
    34.             float2 uv2_BlendTex;
    35.             float4 color: Color; // Vertex color
    36.         };
    37.  
    38.        void surf (Input IN, inout SurfaceOutput o)
    39.        {
    40.          fixed4 mainCol = tex2D(_MainTex, IN.uv_MainTex);
    41.           fixed4 texTwoCol = BlendMethod( tex2D(_BlendTex, IN.uv2_BlendTex), IN.color.rgba );
    42.  
    43.          fixed4 mainOutput = mainCol.rgba * (1.0 - texTwoCol.a);
    44.          fixed4 blendOutput = texTwoCol.rgba * texTwoCol.a;
    45.  
    46.      
    47.           o.Albedo = ( mainOutput.rgb + blendOutput.rgb);
    48.           o.Alpha = mainOutput.a + blendOutput.a;
    49.        }
    50.         ENDCG
    51.     }
    52.     FallBack "Diffuse"
    53. }
     
    Last edited: Jan 3, 2015
    Divinorium likes this.
  36. Divinorium

    Divinorium

    Joined:
    Feb 4, 2014
    Posts:
    15
    I kinda of a Texture "Atlas". All my hex are in the same file and i used uv mapping to separate them. So i think so far so good.

    About the shader how would you active it? I mean the localmesh don't have a renderer... And i wouldn't still have to use Combine() to redo the "world"?



    Thx for the attention.
    Divinorium.

    PS: you can point a good tutorial about shaders? because i looked at your code and i was like "wtf..."
     
  37. MrMadsen

    MrMadsen

    Joined:
    Jan 5, 2015
    Posts:
    1
    Great tutorial, thank you very much! Can't wait for part 3!
     
  38. landon912

    landon912

    Joined:
    Nov 8, 2011
    Posts:
    1,579
    UnityCookie has some pretty decent tutorials on shaders.
     
    Divinorium likes this.
  39. Casper_Stougaard

    Casper_Stougaard

    Joined:
    Jul 18, 2013
    Posts:
    60
    Great tut man. I was wondering if you could give some advice on changing the texture on individual hexagons?

    I've created a texture atlas, and i have a list containing the uv coordinates for each texture inside it. But i kinda got stuck from there.

    My plan at first was to create a node on each hex, which i have working.
    Then i wanted to render a texture on top of the existing hex, to show that it is "selected".
    This way i would have to create a prefab of an exact sized and rotated hexagon to match the one below. But this whole thing doesn't seem right.
     
  40. Divinorium

    Divinorium

    Joined:
    Feb 4, 2014
    Posts:
    15
    That's what i was discussing with him.
    I managed to have it "working" but you need to redo the whole map every time you change a texture to it to show up through the combine().
    I made it so combine() also delete the last mesh if it is there. But redoing the whole maps is way too heavy.

    He pointed to shader. I'm studying it so i can't say how it's done with shader.

    Thx for attention.
    Divinorium
     
  41. Casper_Stougaard

    Casper_Stougaard

    Joined:
    Jul 18, 2013
    Posts:
    60
    Thank you for your answer, however, i'd like to try and not get too much into shaders at the moment. Do you know if the positions of each individual hexagon is stored somewhere? or would i need to calculate that myself?
     
  42. Divinorium

    Divinorium

    Joined:
    Feb 4, 2014
    Posts:
    15
    Each hexagon has it's own HexInfo inside it you have the worldPosition and localPosition.

    Thx for the attention.
    Divinorium
     
  43. landon912

    landon912

    Joined:
    Nov 8, 2011
    Posts:
    1,579
    I have came to the conclusion that I won't be able to develop CivGrid any further. School, and nearly full-time work hours(Go NGS!), and trying to keep time for myself haven't allowed me to come back to this. So I would like to know if you guys would like me to put the project files up for open source? It would be under a very open license and free to use however you wish.

    Let me know guys,

    Landon
     
  44. henr0s

    henr0s

    Joined:
    Nov 6, 2014
    Posts:
    1
    Hi Landon,
    Thank you for the time you put into this tutorial.
    I would love to gain access to the project files.
     
  45. Tiatang

    Tiatang

    Joined:
    Jun 1, 2014
    Posts:
    31
    Me too!
     
  46. landon912

    landon912

    Joined:
    Nov 8, 2011
    Posts:
    1,579
    The Repository.

    This project is a now defunct WIP, it is not production ready, and is not polished. It is no longer in development and should be treated as a reference point, or a base to refactor upon. I advise against attempting to use the system out of box for your project.

    Once you have downloaded/cloned the repository to your computer, you can open up "Help/index.html" for the documentation.

    Also, I would like to give credit to Marc and Gary Whittle for their contributions to the project.
     
    Last edited: May 26, 2017
    konsnos, Tiatang and Braza like this.
  47. Ludoman

    Ludoman

    Joined:
    May 21, 2014
    Posts:
    1
    Hi Landon. First of all, I think what you've created is awesome, I'm a big Civ5-fan and came across your code a couple of months ago when I was playing around with Unity a bit.

    Like two weeks ago I was bored and opened up Unity again to play a little bit, I still had an older version of your code when I started to do some changes I wanted, so therefore what I added to the code might work a little bit weird with your updated version. Still, some changes to my code can be made to fit the current version. The algorithms should be the same.

    Anyway, to the point. I found a way to access a single hexagon in the chunks/world without looping. So, it's possible to keep a low number of colliders and still access a single hexagon with low time complexity. I haven't calculated on it and not promising anything, but it should be constant, it's at least not depending on chunk size in the same way as looping through them.

    Each individual hexagon needs to be a game object though, but at least no colliders are necessary. All that is necessary to find a hexagon in the world, is a RaycastHit and the chunk the ray did hit. The function should also be properly adjusted to work with a horizontal wrap around world. (Complete wrap around might show up later)

    I'm uploading the main file here, but be aware that the original code needs to be changed a bit in order to make it work. But I can post in detail of where it needs to be changed and what it does, if people are interested.

    Anyways, just feel free to change it to work with your own code or figuring out yourselfs where the original code needs to be changed to make it work.

    The main function to use is 'GetHexagonFromRay'.

    Have fun.
    I apologise that the code is quite messy.

    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections;
    3. using System.Collections.Generic;
    4.  
    5. public class HexFinder {
    6.  
    7.     public float hexRadius, verticeHeight, cornerHeight;
    8.  
    9.     public void SetHexNumbers(Vector3 hexSize) {
    10.         hexRadius = hexSize.x/2.0f;
    11.         verticeHeight = hexRadius/Mathf.Cos(Mathf.PI/6.0f);
    12.         cornerHeight = verticeHeight * Mathf.Sin(Mathf.PI/6.0f);
    13.     }
    14.  
    15.     public Dictionary<string, Vector2> GetHexagonEdges(Vector2 south) {
    16.      
    17.         Dictionary<string, Vector2> route = new Dictionary<string, Vector2>();
    18.      
    19.         Vector2 southWest = new Vector2(south.x - hexRadius, south.y + cornerHeight);
    20.         Vector2 northWest = new Vector2(southWest.x, southWest.y + verticeHeight);
    21.         Vector2 southEast = new Vector2(south.x + hexRadius, southWest.y);
    22.         Vector2 northEast = new Vector2(southEast.x, southEast.y + verticeHeight);
    23.         Vector2 north = new Vector2(south.x, northWest.y + cornerHeight);
    24.      
    25.         route.Add("south", south);
    26.         route.Add("north", north);
    27.         route.Add("southWest", southWest);
    28.         route.Add("northWest", northWest);
    29.         route.Add("southEast", southEast);
    30.         route.Add("northEast", northEast);
    31.      
    32.         return route;
    33.     }
    34.  
    35.     private string FindHitHexagonByAngle(Vector2 south, Vector2 localMousePosition) {
    36.         Dictionary<string, Vector2> routes = GetHexagonEdges(south);
    37.      
    38.         string hexPlace = "N";
    39.         if(localMousePosition.y < routes["southWest"].y) {
    40.             if(localMousePosition.x > south.x) {
    41.                 float bottomRightCorner = Vector2.Angle(south-localMousePosition, south-routes["southWest"]);
    42.                 if(bottomRightCorner > 120.0f) {
    43.                     hexPlace = "SE";
    44.                 }
    45.             } else {
    46.                 float bottomLeftCorner = Vector2.Angle(south-localMousePosition, south-routes["southEast"]);
    47.                 if(bottomLeftCorner > 120.0f) {
    48.                     hexPlace = "SW";
    49.                 }
    50.             }
    51.         }
    52.         return hexPlace;
    53.     }
    54.  
    55.     private HexInfo FindFocusedHex(HexInfo hex, Vector2 localMousePosition, int rowType) {
    56.         Vector2 south = new Vector2(hex.localPosition.x, hex.localPosition.z);
    57.         if(rowType == 1) {
    58.             south.x += hexRadius;
    59.         }
    60.      
    61.         string hexPlace = FindHitHexagonByAngle(south, localMousePosition);
    62.      
    63.         float hexX = hex.arrayPositioning.x;
    64.         float hexY = hex.arrayPositioning.y;
    65.      
    66.         Dictionary<string, Vector4> neighbours = GetAligned(hex);
    67.         Vector4 hexCoord = new Vector4(hexX, hexY, (float)hex.parentChunk.xSector, (float)hex.parentChunk.ySector);
    68.      
    69.         if((hexPlace == "SE") && (rowType == 1)) {
    70.             hexCoord = neighbours["south"];
    71.          
    72.         } else if((hexPlace == "SW") && (rowType == 1)) {
    73.             Vector4 hexWest = neighbours["west"];
    74.             Vector4 hexSouth = neighbours["south"];
    75.             hexCoord = new Vector4(hexWest.x, hexSouth.y, hexWest.z, hexSouth.w);
    76.          
    77.         } else if((hexPlace == "SE") && (rowType == 0)) {
    78.             Vector4 hexEast = neighbours["east"];
    79.             Vector4 hexSouth = neighbours["south"];
    80.             hexCoord = new Vector4(hexEast.x, hexSouth.y, hexEast.z, hexSouth.w);
    81.          
    82.         } else if((hexPlace == "SW") && (rowType == 0)) {
    83.             hexCoord = neighbours["south"];
    84.          
    85.         }
    86.         return hex.parentChunk.worldManager.hexChunks[(int)hexCoord.z, (int)hexCoord.w].hexArray[(int)hexCoord.x, (int)hexCoord.y];
    87.     }
    88.  
    89.     private Dictionary<string, Vector4> GetAligned(HexInfo hex) {
    90.         HexChunk chunk = hex.parentChunk;
    91.         Vector2 modder = hex.parentChunk.chunkSize;
    92.         Dictionary<string, Vector4> aligned = new Dictionary<string, Vector4>();
    93.      
    94.         float x = hex.arrayPositioning.x;
    95.         float y = hex.arrayPositioning.y;
    96.      
    97.         float nextX = Mathf.Repeat((x + 1f), modder.x);
    98.         float nextY = Mathf.Repeat((y + 1f), modder.y);
    99.         float prevX = Mathf.Repeat((x - 1f + modder.x), modder.x);
    100.         float prevY = Mathf.Repeat((y - 1f + modder.y), modder.y);
    101.      
    102.         float chunkNextX;
    103.         float chunkPrevX;
    104.         if(nextX < prevX) {
    105.             if(nextX == 0f) {
    106.                 chunkNextX = (chunk.xSector + 1) % chunk.worldManager.xSectors;
    107.                 chunkPrevX = (float)chunk.xSector;
    108.             } else {
    109.                 chunkNextX = (float)chunk.xSector;
    110.                 chunkPrevX = (chunk.xSector - 1 + chunk.worldManager.xSectors) % chunk.worldManager.xSectors;
    111.             }
    112.         } else {
    113.             chunkNextX = (float)chunk.xSector;
    114.             chunkPrevX = (float)chunk.xSector;
    115.         }
    116.      
    117.         float chunkNextY;
    118.         float chunkPrevY;
    119.         if(nextY < prevY) {
    120.             if(nextY == 0f) {
    121.                 chunkNextY = (chunk.ySector + 1) % chunk.worldManager.zSectors;
    122.                 chunkPrevY = (float)chunk.ySector;
    123.             } else {
    124.                 chunkNextY = (float)chunk.ySector;
    125.                 chunkPrevY = (chunk.ySector - 1 + chunk.worldManager.zSectors) % chunk.worldManager.zSectors;
    126.             }
    127.         } else {
    128.             chunkNextY = (float)chunk.ySector;
    129.             chunkPrevY = (float)chunk.ySector;
    130.         }
    131.      
    132.         Vector4 north = new Vector4(x, nextY, (float)chunk.xSector, chunkNextY);
    133.         Vector4 east = new Vector4(nextX, y, chunkNextX, (float)chunk.ySector);
    134.         Vector4 south = new Vector4(x, prevY, (float)chunk.xSector, chunkPrevY);
    135.         Vector4 west = new Vector4(prevX, y, chunkPrevX, (float)chunk.ySector);
    136.      
    137.         aligned.Add("north", north);
    138.         aligned.Add("east", east);
    139.         aligned.Add("south", south);
    140.         aligned.Add("west", west);
    141.      
    142.         return aligned;
    143.     }
    144.  
    145.     public HexInfo GetHexagonFromRay(RaycastHit hit, HexChunk chunk) {
    146.         HexInfo hex;
    147.         Vector2 localMousePosition;
    148.         Bounds bounds = chunk.collider.bounds;
    149.         float localMousePosY = (hit.point.z - bounds.min.z);
    150.      
    151.         int hexY = (int)((localMousePosY - Mathf.Repeat(localMousePosY, chunk.hexSize.z - cornerHeight))/(chunk.hexSize.z - cornerHeight)) % (int)chunk.chunkSize.y;
    152.         int rowType = hexY % 2;
    153.         if(rowType == 1) {
    154.             float localMousePosX = (hit.point.x - bounds.min.x);
    155.             localMousePosition = new Vector2(localMousePosX, localMousePosY);
    156.  
    157.             int hexX = (int)((localMousePosition.x - Mathf.Repeat(localMousePosition.x, chunk.hexSize.x))/chunk.hexSize.x);
    158.             if(hexX < (int)chunk.chunkSize.x) {
    159.                 hex = chunk.hexArray[hexX, hexY];
    160.             } else {
    161.                 float xChunks = (float)chunk.worldManager.xSectors;
    162.                 int nextChunkX = (int)Mathf.Repeat((float)(chunk.xSector + 1), xChunks);
    163.                 int newHexX = hexX % (int)chunk.chunkSize.x;
    164.                 hex = chunk.worldManager.hexChunks[nextChunkX, chunk.ySector].hexArray[newHexX, hexY];
    165.             }
    166.         } else {
    167.             float localMousePosX = (hit.point.x - (bounds.min.x + hexRadius));
    168.             localMousePosition = new Vector2(localMousePosX, localMousePosY);
    169.      
    170.             int hexX = (int)((localMousePosition.x - Mathf.Repeat(localMousePosition.x, chunk.hexSize.x))/chunk.hexSize.x) % (int)chunk.chunkSize.x;
    171.             if(hexX > -1) {
    172.                 float yChunks = (float)chunk.worldManager.zSectors;
    173.                 if(hexY < yChunks) {
    174.                     hex = chunk.hexArray[hexX, hexY];
    175.                 } else {
    176.                     int nextChunkY = (int)Mathf.Repeat((float)(chunk.ySector + 1), yChunks);
    177.                     int newHexY = hexY % (int)chunk.chunkSize.y;
    178.                  
    179.                     hex = chunk.worldManager.hexChunks[chunk.xSector, nextChunkY].hexArray[hexX, newHexY];
    180.                 }
    181.             } else {
    182.                 float xChunks = (float)chunk.worldManager.xSectors;
    183.                 int prevChunkX = (int)Mathf.Repeat((float)(chunk.xSector - 1) + xChunks, xChunks);
    184.                 int newHexX = (hexX + (int)chunk.chunkSize.x) % (int)chunk.chunkSize.x;
    185.                 int newHexY = hexY % (int)chunk.chunkSize.y;
    186.              
    187.                 hex = chunk.worldManager.hexChunks[prevChunkX, chunk.ySector].hexArray[newHexX, newHexY];
    188.             }
    189.         }
    190.         return FindFocusedHex(hex, localMousePosition, rowType);
    191.     }
    192. }
    193.  
     

    Attached Files:

    Last edited: Feb 27, 2015
    landon912 likes this.
  48. TheProfessor

    TheProfessor

    Joined:
    Nov 2, 2014
    Posts:
    74
    I imported the project for Unity v5 and I can seem to get the terrain to display; the Hex shader has errors that there's too much interpolation unless it's on Shader Model 5.

    Code (csharp):
    1.  
    2. Tile not found for this specific setting; Temperature: 0.93 ; Rainfall: 0.43
    3. UnityEngine.Debug:LogError(Object)
    4. CivGrid.TileManager:DetermineTile(Single, Single, Single) (at Assets/CivGrid/Core/Scripts/Terrain/TileManager.cs:332)
    5. CivGrid.WorldManager:GenerateTileType(Int32, Int32) (at Assets/CivGrid/Core/Scripts/Terrain/WorldManager.cs:692)
    6. CivGrid.InternalChunk:GenerateHexOffset(Int32, Int32) (at Assets/CivGrid/Core/Scripts/Terrain/InternalChunk.cs:212)
    7. CivGrid.InternalChunk:GenerateChunk() (at Assets/CivGrid/Core/Scripts/Terrain/InternalChunk.cs:134)
    8. CivGrid.InternalChunk:Begin() (at Assets/CivGrid/Core/Scripts/Terrain/InternalChunk.cs:69)
    9. CivGrid.WorldManager:GenerateMap() (at Assets/CivGrid/Core/Scripts/Terrain/WorldManager.cs:649)
    10. CivGrid.WorldManager:StartGeneration(Boolean) (at Assets/CivGrid/Core/Scripts/Terrain/WorldManager.cs:421)
    11. CivGrid.WorldManager:GenerateNewMap(Boolean) (at Assets/CivGrid/Core/Scripts/Terrain/WorldManager.cs:355)
    12. CivGrid.WorldManager:Awake() (at Assets/CivGrid/Core/Scripts/Terrain/WorldManager.cs:339)
    13.  
    And this is the main error I'm getting now, a similar one occurs regarding "Tundra" being missing.
     
  49. landon912

    landon912

    Joined:
    Nov 8, 2011
    Posts:
    1,579
    The second error is simply the biome settings in the WorldManager being wrong. The first one about the shader, I'm not sure without seeing it and unfortunately I don't have Unity 5 on my current computer.
     
  50. TheProfessor

    TheProfessor

    Joined:
    Nov 2, 2014
    Posts:
    74
    What do I need to change? I don't see anything regarding Biomes here.

    With the Shader Model switched to 5 all the textures are just black. With Shader Model 2 it's all pink unless I switch it to the standard shader.