Breaking change

The intro video to the hex-grid section mentions it’s a “breaking change”. I wonder how diffiult it would be to have both types in a game. I had a vague idea of having some landscape in a hex-grid where you might find encounters that then take place in a more confided space in a x/y grid…

The biggest parts of the Hex system involve converting between GridPositions and WorldPositions, and in getting the appropriate neighbors in the Pathfinding. Perhaps you could have separate methods for Hex based and Square based conversions (and a Hex-based Square-based get neighbor).

If it weren’t for having GridSystem instances with different TGridObjects one could shift the responsibility for creating the GridSystem to the LevelGrid (or at least more easily). In some way it’s somewhat weird that all other access to the grid system’s properties is routed through the Level grid, but Pathfinding defies that abstraction layer.

Finding neighbour nodes should actually be a property of the grid system itself, and not something the Pathfinding should be concerned about how to calculate them…

Now the question is whether the LevelGrid should know about PathNodes so it could be given the task to create the pathfinding grid in place of the Pathfinding itself…

I guess for now the simplest approach would be to add a query method to the LevelGrid to get the grid type so Pathfinding can use that information.

You could have the GridSystem return a List (actually I prefer IEnumerables, but that’s just me) of neighboring nodes. That would mean the GridSystem would need to know if this was a Hex or Square grid.

I tend to say no… unless you just made a complex GridNode that included both the pathfinding hints and the Unit list. That being said, if you did that, you would automatically have the information about whether the tile is occupied.

What I eventually did was defining an interface IGridSystem<TGridObject> into which I put all public methods from the existing GridSystem class.

public interface IGridSystem<TGridObject>
{
    public int Height { get; }
    public int Width { get; }

    public Vector3 GetWorldPosition(GridPosition gridPosition);
    public GridPosition GetGridPosition(Vector3 worldPosition);
    public TGridObject GetGridObject(GridPosition gridPosition);
    public bool IsValidGridPosition(GridPosition gridPosition);
    public void CreateDebugObjects(Transform debugPrefab, Transform parent = null);
    public void Map(System.Action<TGridObject> action);
}

Then I duplicated the GridSystem to GridSystemHex (instead of just moving it).
One thing that puzzled me for quite a while later on…
Don’t forget to make the GridSystems implement IGridSystem<> otherwise they’re not really connected to the interface.

In the GridSystemVisual I added a second list of materials and another prefab field. And a private list variable to store which one of the material lists would be used.
Then I made a prefab variant for the GridSystemVisualSingle and also made clones of the materials and changed their basemap image.

In Start() I added some selection:

        Transform visualSinglePrefab;
        if (LevelGrid.Instance.IsHexGrid)
        {
            visualSinglePrefab = _gridSystemHexVisualSinglePrefab;
            _activeMaterialList = _gridHexVisualTypeMaterialList;
        }
        else
        {
            visualSinglePrefab = _gridSystemVisualSinglePrefab;
            _activeMaterialList = _gridVisualTypeMaterialList;
        }

and made sure to use visualSinglePrefab for the Instantiate() call.

Where the GridObject used to refer to the GridSystem<GridObject> I changed it to be an IGridSystem<GridObject>.

Also in the scene I made a second Plane for the hex ground material.

And finally in LevelGrid

    [SerializeField] Transform _groundPlane;
    [SerializeField] Transform _groundPlaneHex;

    [SerializeField] private bool _isHexGrid = true;
    public bool IsHexGrid => _isHexGrid;

    IGridSystem<GridObject> _gridSystem;

private void Awake()
{
//...
       if (_isHexGrid)
        {
            _gridSystem = new GridSystemHex<GridObject>(_width, _height, _cellSize, (GridSystemHex<GridObject> g, GridPosition gridPosition) => new GridObject(g, gridPosition));
            _groundPlane.gameObject.SetActive(false);
            _groundPlaneHex.gameObject.SetActive(true);
        }
        else
        {
            _gridSystem = new GridSystem<GridObject>(_width, _height, _cellSize, (GridSystem<GridObject> g, GridPosition gridPosition) => new GridObject(g, gridPosition));
            _groundPlane.gameObject.SetActive(true);
            _groundPlaneHex.gameObject.SetActive(false);
        }
}

Eventually I might refactor the interface into a common BaseGridSystem to hold everything both versions have in common and have a HexGridSystem and a CartGridSystem (for “cartesian”)…
But for now everything works and I can easily switch which type of grid should be active before I enter play mode…

1 Like

An excellent solution, and making fine use of Interfaces as well.

Now with the Pathfinding having been hexed, I find movement to be somewhat sluggish…
On thing that seemed to have improved it a bit was caching the value for whether it’s a hex grid or not.

As Hugo noted, we’re using a simplified Pathfinding algorithm, which isn’t as efficient as it could be. Additionally, it’s run multiple times per movement, meaning that the AI becomes very sluggish when deciding it’s actions.

One thing that could improve the algorithm would be to cache the known path once it is calculated… My idea for this would be to have a couple of fields in the PathNode

public List<GridPosition> cachedPath;
public GridPosition startPosition;

When starting a path check, first check the destination and see if the startPosition on the destination’s pathnode matches the startPosition. If it does, then return the cached path, if not, calculate a path and cache it in the PathNode (and return it, obviously).

For whatever reason, the inefficiencies of the implementation (especially the multiple iterations over the same path) became much more prevalent with my changes for the hex grid.
Caching the path seems like a good approach, indeed.
I was just wondering if I broke something because the sluggishness increased a lot also for the cartesian grid and units starting to walk into a cell and jerking a bit backwards seems very odd.

That’ behaviour doesn’t sound like it has anything to do with pathfinding… Once the action itself starts, it shouldn’t be repathing…

Well, I quickly made some builds, one for the cartesian scene and one for the hex-grid version…

At the moment they are Linux-only, though…
I added a source export of my git from this branch, but will remove the archive again later.

Actually, just checking if the start and end node are the same might be insufficient, if some other action happened in between that might have changed the grid’s state (door opened/closed, some obstacle destroyed, or similar).

On easy thing I did (after I added some debugging output on when FindPath() was called) was adding a second HasPath() implementation, that also returned the path score, so another FindPath() for the subsequently GetPathLength() that followed in the MoveAction's checks was eliminated. This immediately about halfed the amount of calls to FindPath() :slight_smile:

    public bool HasPath(GridPosition startGridPosition, GridPosition endGridPosition, out int returnPathLength)
    {
        bool hasPath = null != FindPath(startGridPosition, endGridPosition, out int pathLength);
        returnPathLength = pathLength;
        return hasPath;
    }

I also turned off the “debug visual mode” on the GridSystemVisual.

I think you’re right. Probably at the end of any given action, all cached paths should be reset.

Come to think of it, there is one more thing that might have some influence on how smooth the movement is…

In theMoveAction’s Update() I added one line to align the unit’s transform.position to compnesate for any differences to the targetposition`:

        if (Vector3.Distance(transform.position, targetPosition) > stoppingDistance)
        {
            float moveSpeed = 4f;
            transform.position += moveDirection * moveSpeed * Time.deltaTime;
        }
        else
        {
            transform.position = targetPosition;

            _currentPositionIndex++;
            if (_currentPositionIndex >= _positionList.Count)
            {
                OnStopMoving?.Invoke(this, EventArgs.Empty);
                ActionComplete();
            }
        }

As one can see, currently this is done at the end of every step. Actually that shouldn’t be necessary and aligning at the end of the whole movement would be sufficient.

Therefore I moved it into the finalizing if() clause like this:

        else
        {
            _currentPositionIndex++;
            if (_currentPositionIndex >= _positionList.Count)
            {
                transform.position = targetPosition;
                OnStopMoving?.Invoke(this, EventArgs.Empty);
                ActionComplete();
            }
        }
1 Like

Privacy & Terms