2
\$\begingroup\$

I have generated a grid with hex tiles. I am using two different types of tiles and would like to at some points ask the tiles, how many tiles of each type they have as neighbours (or really, it would be enough to know if they have neighbours of one type at all).

I have managed to get this running by getting each tile to fire six rays in the neighbouring directions and checking the tags of the returned objects. This worked but it seems quite overly complicated for what I want to achieve. What would be a better way to do this? Should I use colliders? Any other way?

Screenshot of Grid

\$\endgroup\$
1
  • 2
    \$\begingroup\$ How many different tags do you need to distinguish? Do you have the current state of the grid stored in an array or similar data structure? \$\endgroup\$
    – DMGregory
    Oct 3 at 16:35
1
\$\begingroup\$

Sure there is!

If you have a grid you do not need a collider. By definition grid works instead of collider. What you need to do is:

Store (and retrieve) each tile address into (from) data structure (hashtable, dictionary or array, list). So now when you select (or do something) with one tile you can immediately access its neighbors!

For simplicity lets assume you got rectangle grid 100x100 and each cell size is 1m. So we will have to lay down 100 rows each containing 100 cells.. Let's programmatically place all cells - cell1 will have address(x=1,y=1), cell2(2,1), cell3(3,1), ... , cell100(100,1). So we placed 1 row! now lets do another. It will look something like cell101(1,2), cell3(2,2), cell3(3,2) ... , cell100(100,2). Lets do this for all the rows.

Store the address of each tile in Vector2. And add it (as a key) to dictionary. As value you can add string (or sth else) with type of this tile.

It would look something like this:

Dictionary<Vector2,string> allMyNiceTiles= new Dictionary<Vector2,string>();
... 
allMyNiceTiles.Add(new Vector2(456,183), "grass"); // to add
...
theType= allMyNiceTiles[new Vector2(15,132)];  // to take out tile at adress 15,132

Thus you can access any tile (or its neighbors by address).

P.S. hexagonal will be very similar, just always store discreet addresses (integers NOT floats), convert to integers before storing in dictionary.

\$\endgroup\$
1
  • \$\begingroup\$ Your P.S. contradicts your code sample. A Vector2 in Unity is a vector of two floats. But there is also Vector2Int. \$\endgroup\$
    – Philipp
    Oct 5 at 10:52
1
\$\begingroup\$

TL;DR: WebGL Demo & UnityPackage Download

Recursive Nodes

In the past I have tackled a similar problem by attaching a node component to each grid object. Each node component in turn keeps track of its neighbor nodes in a what could be described as a recursive manner.

In my experience this approach works with thousands of nodes in a game world in a WebGL project, with no appreciable impact on performance as compared to other methods I've tried.

public class Node : Monobehaviour 
{
public List<Node> neighbors;
public NodeType type; // an enum defined elsewhere

// other properties and methods as needed...
}

Populating the Nodes

You could use your raycasting method in an editor script to do a one-time data population of these node instances, or any number of other methods depending on whether performance is an issue. Once you have your nodes populated it's straightforward to set up A* or other kinds of path following logic.

Example 1: Generating Tiles & Finding Neighbors

We'll start a simple scene with just a Controller object added -- this will be used to hold the script to generate our grid and populate the neighbor data of each tile. The assets we need for the demo are some materials to differentiate the tiles, two scripts, and a hextile prefab.

bare project

The hexTile prefab has the usual usual rendering components and a convex mesh collider. Importantly, it has a HexTile component added:

hexTile inspector

In this example HexTile is the equivalent of a node as described above. For this first example the only thing that really matters is the list of neighbors which are also HexTiles. There could be all kinds of other properties and functionality, and you could derive other classes from this for specialized behavior (maybe a volcano that erupts or who knows what)... anyway:

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

public class HexTile : MonoBehaviour
{
    public List<HexTile> neighbors = new List<HexTile>();
}

Now on to the controller script. I have some variables for generating the hex grid that I can set in the inspector:

 [SerializeField] private int rows;
 [SerializeField] private int columns;
 [SerializeField] private float xOffset;
 [SerializeField] private float zOffset;

I also have a reference to the prefab, a list of "standard" materials, and a special highlight material for demo purposes.

 [SerializeField] private GameObject hexTilePrefab;
 [SerializeField] private Material[] hexMaterials;
 [SerializeField] private Material hilightMaterial;

Finally, a keep an internal list of the hext tiles I generate:

 List<HexTile> tiles = new List<HexTile>();

The code to generate the grid is simple-- a nested loop that instantiates gamebojects and adds their HexTile component to the list. I won't dwell on this as you already have your own working solution:

void CreateGrid()
    {
        float xPos = 0;
        for (int x = 0; x < rows; x++)
        {
            float zPos = x % 2 == 1 ? zOffset / 2 : 0;
            for (int z = 0; z < columns; z++)
            {
                var go = Instantiate(hexTilePrefab);
                go.name = "Hex_" + x + "_" + z;
                go.transform.position = new Vector3(xPos, 0, zPos);
                go.GetComponent<Renderer>().sharedMaterial = hexMaterials[Random.Range(0, hexMaterials.Length)];
                tiles.Add(go.GetComponent<HexTile>());
                zPos += zOffset;
            }
            xPos += xOffset;
        }
    }

My code for populating each HexTile with its neighbors is dumb and inefficient -- but it works. I'm simply looping thru all the tiles and checking whether they are within a unit distance, if so they are added. There are some obvious optimizations that I'll leave for an excercise for the reader (hint: each tile only has at most 6 neighbors.) And note this has a hard-coded magic number that will break if the tiles aren't approximately 1 unit in diameter.

If this code was only used in the editor to bake values it should be fine -- if not and there are huge numbers of tiles at runtime there are no doubt other faster and more efficient approaches to tackle this...

void PopulateHexTiles()
    {
        foreach (HexTile tile in tiles)
        {
            foreach(HexTile testedTile in tiles)
            {
                if (testedTile == tile)
                {
                    continue;
                }
                if ((tile.transform.position - testedTile.transform.position).magnitude < 1) //TODO Fix magic number
                {
                    tile.neighbors.Add(testedTile);
                }
            }
        } 
    }

Finally, I have function to call a tile and "light up" its neighbors -- for demo purposes.

void ColorNeighboringHexes(HexTile tile)
    {
        foreach (HexTile neighbor in tile.neighbors)
        {
            neighbor.gameObject.GetComponent<Renderer>().sharedMaterial = hilightMaterial;
        }
    }

Here's the entire script:

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

public class HexGridController : MonoBehaviour
{
    [SerializeField] private int rows;
    [SerializeField] private int columns;
    [SerializeField] private float xOffset;
    [SerializeField] private float zOffset;
    [SerializeField] private GameObject hexTilePrefab;
    [SerializeField] private Material[] hexMaterials;
    [SerializeField] private Material hilightMaterial;
    List<HexTile> tiles = new List<HexTile>();
  

    void Start()
    {
        CreateGrid();
        PopulateHexTiles();
        //Demo accessing neighbors
        ColorNeighboringHexes(tiles[Random.Range(0, tiles.Count)]);
    }


    void CreateGrid()
    {
        float xPos = 0;
        for (int x = 0; x < rows; x++)
        {
            float zPos = x % 2 == 1 ? zOffset / 2 : 0;
            for (int z = 0; z < columns; z++)
            {
                var go = Instantiate(hexTilePrefab);
                go.name = "Hex_" + x + "_" + z;
                go.transform.position = new Vector3(xPos, 0, zPos);
                go.GetComponent<Renderer>().sharedMaterial = hexMaterials[Random.Range(0, hexMaterials.Length)];
                tiles.Add(go.GetComponent<HexTile>());
                zPos += zOffset;
            }
            xPos += xOffset;
        }
    }


    void PopulateHexTiles()
    {
        foreach (HexTile tile in tiles)
        {
            foreach(HexTile testedTile in tiles)
            {
                if (testedTile == tile)
                {
                    continue;
                }
                if ((tile.transform.position - testedTile.transform.position).magnitude < 1)
                {
                    tile.neighbors.Add(testedTile);
                }
            }
        } 
    }


    void ColorNeighboringHexes(HexTile tile)
    {
        foreach (HexTile neighbor in tile.neighbors)
        {
            neighbor.gameObject.GetComponent<Renderer>().sharedMaterial = hilightMaterial;
        }
    }
}

And here is how the HexGridController component is configured: GridController in inspector

Here is a sample result when the thing runs:

HexGrid Demo

Example 2: Illuminating Neighbors by Tag (or Type)

Now we'll extend this to address the OPs specific problem -- getting neighbors that match a certain tag. To do that we'll create a HexTile.TileType enum for this.

public enum TileType
    {
        grass,
        water,
        sand
    }

I'm also going to add a public property to hold this value for each tile...

    public TileType type;

and we'll add a method that returns all neighboring tiles that match the requested type:

 public List<HexTile> GetNeighborsWithTag(TileType requestedType)
    {
        return neighbors.FindAll(x => x.type == requestedType);
    }

Here's the complete, revised HexTile script:

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

public class HexTile : MonoBehaviour
{

    public enum TileType
    {
        grass,
        water,
        sand
    }
    public TileType type;


    public List<HexTile> neighbors = new List<HexTile>();

    public List<HexTile> GetNeighborsWithTag(TileType requestedType)
    {
        return neighbors.FindAll(x => x.type == requestedType);
    }
}

Next we want to modify our controller to take tile type into account when generating tiles. The first step is to define a struct we can serialize that will associate a material with a HexTile.TileType:

  [System.Serializable]
    public struct HexTileData
    {
        public Material material;
        public HexTile.TileType type;
    }

Now, we need to create a set of these HexTileData that the generation script can choose from that we can configure in the inspector:

[SerializeField] private HexTileData[] tileData;

Then, a modification of our generation code to chose from these data randomly, and assign materials and tile types accordingly:

  void CreateGrid()
    {
        float xPos = 0;
        for (int x = 0; x < rows; x++)
        {
            float zPos = x % 2 == 1 ? zOffset / 2 : 0;
            for (int z = 0; z < columns; z++)
            {
                var go = Instantiate(hexTilePrefab);
                go.name = "Hex_" + x + "_" + z;
                go.transform.position = new Vector3(xPos, 0, zPos);
                var data = tileData[Random.Range(0, tileData.Length)];
                go.GetComponent<Renderer>().sharedMaterial = data.material;
                go.GetComponent<HexTile>().type = data.type;
                tiles.Add(go.GetComponent<HexTile>());
                zPos += zOffset;
            }
            xPos += xOffset;
        }
    }

We'll add a new demo test method to light up only neighboring tiles that match the requested type:

void ColorNeigborTilesMatchingType(HexTile tile, HexTile.TileType type)
    {
        foreach (HexTile neighbor in tile.GetNeighborsWithTag(type))
        {
            neighbor.gameObject.GetComponent<Renderer>().sharedMaterial = hilightMaterial;
        }
    }

And with that, here's the complete, modified HexGridController script:

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

public class HexGridController : MonoBehaviour
{
    [SerializeField] private int rows;
    [SerializeField] private int columns;
    [SerializeField] private float xOffset;
    [SerializeField] private float zOffset;
    [SerializeField] private GameObject hexTilePrefab;
    [SerializeField] private HexTileData[] tileData;
    [SerializeField] private Material hilightMaterial;
    List<HexTile> tiles = new List<HexTile>();

    [System.Serializable]
    public struct HexTileData
    {
        public Material material;
        public HexTile.TileType type;
    }
  
    void Start()
    {
        CreateGrid();
        PopulateHexTiles();
        ColorNeigborTilesMatchingType(tiles [Random.Range(0, tiles.Count)], HexTile.TileType.sand);
    }

    void CreateGrid()
    {
        float xPos = 0;
        for (int x = 0; x < rows; x++)
        {
            float zPos = x % 2 == 1 ? zOffset / 2 : 0;
            for (int z = 0; z < columns; z++)
            {
                var go = Instantiate(hexTilePrefab);
                go.name = "Hex_" + x + "_" + z;
                go.transform.position = new Vector3(xPos, 0, zPos);
                var data = tileData[Random.Range(0, tileData.Length)];
                go.GetComponent<Renderer>().sharedMaterial = data.material;
                go.GetComponent<HexTile>().type = data.type;
                tiles.Add(go.GetComponent<HexTile>());
                zPos += zOffset;
            }
            xPos += xOffset;
        }
    }


    void PopulateHexTiles()
    {
        foreach (HexTile tile in tiles)
        {
            foreach(HexTile testedTile in tiles)
            {
                if (testedTile == tile)
                {
                    continue;
                }
                if ((tile.transform.position - testedTile.transform.position).magnitude < 1)
                {
                    tile.neighbors.Add(testedTile);
                }
            }
        } 
    }


    void ColorNeighboringHexes(HexTile tile)
    {
        foreach (HexTile neighbor in tile.neighbors)
        {
            neighbor.gameObject.GetComponent<Renderer>().sharedMaterial = hilightMaterial;
        }
    }

    void ColorNeigborTilesMatchingType(HexTile tile, HexTile.TileType type)
    {
        foreach (HexTile neighbor in tile.GetNeighborsWithTag(type))
        {
            neighbor.gameObject.GetComponent<Renderer>().sharedMaterial = hilightMaterial;
        }

Now the GridController game object with this controller is configured like so in the inspector:

grid controller in inspector

And a sample output looks like this: sample output

Optimization: What about all those Monobehaviours?

You might be concerned that having hundreds or thousands of objects with Monobehaviours might be a serious drag on performance. I don't think this is a big issue, assuming the HexTile behaviours aren't responding to events like Update and so on. To test this, I rewrote the scripts to leave the individual tiles "dumb" (no extra components attached) and instead stored the data in the controller. Performance was, as far I could tell, identical up to 100 x 100 tiles; by far a bigger drag is all the extra draw calls, which I demonstrated by turning off all the renderers. Optimizing the draw calls is a different subject (mesh combination, etc.) and probably off topic. But if anyone is interested I can share the version of the project that does not rely on individual Monobehaviours attached to each tile.

More Info

You can try a slightly enhanced version of this demo (mouse position determines which tiles are polled, and you can choose tile type to highlight as well as generate larger or smaller grids) here on my itch.io page. The project with code is available there as a unitypackage download, as well.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.