Potential Solutions for Square Rooms on Hex Grid?

I ran into the same issue that someone else posted about (Obstacle detection doesn't work well with square rooms and hex grid). basically, with the hex grid, it is impossible to build square rooms without leaving walkable gaps. This is because we are only checking for an obstacle being the very center of the grid.

I thought of two solutions that I wanted to get feedback on, but I wanted to see if there were other options as well.

First, I thought of doing a ray cast from more points, like the center and each corner. I’m sure there is a mathematical way to do that, but I was going to be lazy and just put empty game objects where I want the ray casts to originate. I would do 7 ray casts instead of 1. So that was option 1.

The second idea I had was to put a collider on the tile itself and just use OnCollision to set isWalkable. I’m not sure if that is easier (or even possible, lol) or how it would compare to the ray cast option.

Both of those seem like a lot of work to try and implement as an experiment. Has anyone tried any other methods? If not, which of these seems like the best method?

Something like this is what I meant by more raycasts. The math wasn’t that hard… thanks ChatGPT! :wink:

image

Before:

After:

Some unintended consquences, maybe.

Here’s how I implemented it. There’s probably a better way.

    private void SetIsWalkable2() {
        for (int x = 0; x < pathNodeGridSystem.GetWidth(); x++) {
            for (int z = 0; z < pathNodeGridSystem.GetHeight(); z++) {

                GridPosition testGridPosition = new(x, z);
                Vector3 centerPosition = LevelGrid.Instance.GetWorldPosition(testGridPosition);

                float raycastVerticalOffsetDistance = .5f;
                Vector3 offsetCenterPosition = centerPosition + Vector3.down * raycastVerticalOffsetDistance;

                List<Vector3> positionsToCheck = GetPositionsToCheck(offsetCenterPosition);

                foreach (Vector3 position in positionsToCheck) {
                    //Debug.Log("checking position " + position);
                    //Color color = Color.blue;
                    //if (position == offsetCenterPosition) {
                    //    color = Color.red;
                    //}
                    //Debug.DrawLine(position, position + Vector3.up * raycastVerticalOffsetDistance * 2, color, 10f);

                    if (Physics.Raycast(position, Vector3.up, raycastVerticalOffsetDistance * 2, obstaclesLayerMask)) {
                        if (TryGetPathNode(testGridPosition, out PathNode pathNode)) {
                            pathNode.IsWalkable = false;
                            continue;
                        }
                    }

                }

            }
        }
    }

    private List<Vector3> GetPositionsToCheck(Vector3 offsetCenterPosition) {
        List<Vector3> positionsToCheck = new() {
              offsetCenterPosition
        };

        for (int i = 0; i < 6; i++) {

            float angle = Mathf.Deg2Rad * (60 * i + 30);
            float hexagonRadius = .9f;

            float cornerX = offsetCenterPosition.x + Mathf.Cos(angle) * hexagonRadius;
            float cornerZ = offsetCenterPosition.z + Mathf.Sin(angle) * hexagonRadius;

            Vector3 cornerPosition = new(cornerX, offsetCenterPosition.y, cornerZ);
            positionsToCheck.Add(cornerPosition);

        }

        return positionsToCheck;
    }

There are no One Size Fits All solutions to this issue, unfortunately. Raycasting corner of each gridPosition would catch the issue found in the original post, but would miss a Cylindrical object that never actually touches the corner.

Placing colliders on the tiles themselves (placing them on the visual perhaps?) could work, though you have to be careful that the colliders don’t also intersect with the floor, and this could complicate things on a multi-floor or a map with simple elevations.

I use a trick that actually works well for both square and hex tiles. I use a script that can be searched for by the Pathfinding

public class PathfindingObstacle : MonoBehaviour
{
     public GridPosition GetPosition()=>LevelGrid.Instance.GetGridPosition(transform.position);
}

This simply acts as a tag. Then Pathfinding can generate it’s list of obstacles by simply searching for them.

        foreach (var obstacle in FindObjectsByType<PathfindingObstacle>(FindObjectsSortMode.None))
        {
            GridPosition position = obstacle.GetGridPosition();
            GetNode(position.x, position.z).SetIsWalkable(false);
        }

This still leaves us a problem, however. This will work fantastically well for obstacles that are single units, but it doesn’t work quite so well for a wall of multiple units. For this, we have to get a bit creative…

Let’s take a look at the OP’s drawing:
image

In this scenario, I"m actually going to assume that each of these walls is the same prefab, copied once horizontally and once vertically. What we need are child GameObjects at each potential GridPosition. So what I did was add pairs of GameObjects with PathFindingObstacles spaced left and right of the lengthwise center of the wall, just enough to be into the adjacent gridpositions if they overlap the bar, and close enough in distance from the other pairs to ensure that every GridPosition is covered.

Consider these two walls (each using the same prefab)…

For this next image, I turned the renderer off to show the locations of the PathfindingObstacles
The outlines are the Box Colliders, which as the OP reported don’t reach the centers when the wall North/South. As you can see these colliders are spaced so that they are each within a hex tile.

Now here’s the best part… if two or more PathfindingObstacles are in the same GridPosition, it doesn’t matter. We’re only using these to mark a position as unwalkable. Multiple entries will just keep calling the tile unwalkable.

1 Like

Correct. We still have to check the center.

I don’t think any solution is ideal. We can control the false unwalkable risk a bit by changing the diameter of the hex grid used for ray casting (or of the collider in a solution that went that way).

The raycasting solution seems pretty solid so I’ll probably end up with a variation of that, but I do need to toy with the collision range to avoid blocking too many tiles E.g. below. But most of this can be controlled during level design, I think.

For the spheres, I would definitely say that centering them in a tile is the best solution. For example, the sphere in the lower right should just be in GridPostion(10,4).

Absolutely. In a game like this it only produces unnecessary edge cases and headaches to allow obstacle placement that doesn’t fit into the grid. If one wanted to be really fancy about it one could create even an editor script that would enforce the placement to clean grid positions…

As for the walls, it wouldn’t be that difficult to create just a variant of the wall adjusted for placement with a 90° rotation applied…

I can also imagine placing circular colliders that either cover the whole inside of a hex grid (so the circle touches the edges from the inside) or maybe even the outer perimeter (touching the grid position’s corners)…

I find the raycast inefficient compared to my GridObstacle trick, but as it should just be done once at the beginning of the level, it’s not significant.

Privacy & Terms