Execution Error with Pathfinding

Hi,

This is in relation to the code monkey rpg tactical pathfinding course.
When my units move the grid updates sometimes but my units will backtrack to get to certain locations that are right in front of them or the grid will show that the unit moved but still hold their previous position as unwalkable.
I believe this to be an execution error.
How should the execution order be setup in project settings to fix this?

Thank you!

I don’t have the course project in front of me (at work). This sounds more like an issue with the GridObjects not properly registering when a Unit has left it’s original position, though it is always possible that this is an execution order issue. (Usually, though, Execution Order affects setup more than actual gameplay).

Let’s take a look at the Unit.cs and Mover.cs for a start, to make sure that Units are properly announcing their changes in squares. I may ask for more scripts, but I don’t have the course in front of me.

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

public class MoveAction : BaseAction
{
    private List<Vector3> positionList;
    private int currentPositionIndex;
    public event EventHandler OnStartMoving;
    public event EventHandler OnStopMoving;

    [SerializeField]
    private int maxMoveDistance = 1;

    protected override void Awake()
    {
        base.Awake();
    }
    private void Update()
    {
        //This setups up a bool so if this code is not active then it will return
        if (!isActive)
        {
            return;
        }

        Vector3 targetPosition = positionList[currentPositionIndex];
        //Make the unit move in a certain direction and speed
        Vector3 moveDirection = (targetPosition - transform.position).normalized;

        //Setting the transform of the unit as it moves to face the space clicked
        float rotateSpeed = 15f;
        transform.forward = Vector3.Lerp(transform.position, moveDirection, Time.deltaTime * rotateSpeed);

        //Learn the distance between the object and the unit moving and also making it stop and not shake
        float stoppingDistance = .1f;
        if (Vector3.Distance(transform.position, targetPosition) > stoppingDistance)
        {
            float moveSpeed = 4.5f;
            transform.position += moveDirection * moveSpeed * Time.deltaTime;
        }
        else
        {
            currentPositionIndex++;
            if (currentPositionIndex >= positionList.Count)
            {
                OnStopMoving?.Invoke(this, EventArgs.Empty);
                //Setting isActive false after the move is complete
                ActionComplete();
            }
        }

    }

    //This function is exposed so that we can move the unit without giving the rest of the code out
    public override void TakeAction(GridPosition gridPosition, Action onActionComplete)
    {
        GridPosition unitGridPostion = unit.GetGridPostion();
        List<GridPosition> pathGridPositionList = Pathfinding.Instance.FindPath(unitGridPostion, gridPosition, out int pathLength);
        currentPositionIndex = 0;
        positionList = new List<Vector3>();
        foreach (GridPosition pathGridPosition in pathGridPositionList)
        {
            positionList.Add(LevelGrid.Instance.GetWorldPosition(pathGridPosition));

        }

        //setting up the animator from the animation script for walking
        // Invoke the OnStartMoving event if it is not null
        if (OnStartMoving != null)
        {
            OnStartMoving?.Invoke(this, EventArgs.Empty);
        }
        //setting up the delegate to call from the base class
        ActionStart(onActionComplete);

    }


    //This list helps create the max move distance a unit can make from their current postion
    //You get the grid position to make the logic for understanding where the unit is and the max distance from that location
    public new bool IsValidActionGridPosition(GridPosition gridPosition)
    {
        List<GridPosition> validGridPositionList = GetValidActionGridPositionList();
        return validGridPositionList.Contains(gridPosition);
    }
    public override List<GridPosition> GetValidActionGridPositionList()
    {
        List<GridPosition> validGridPositionList = new List<GridPosition>();
        GridPosition unitGridPostion = unit.GetGridPostion();
        //This needs to cycle through the grid
        for (int x = -maxMoveDistance; x <= maxMoveDistance; x++)
        {
            for (int z = -maxMoveDistance; z <= maxMoveDistance; z++)
            {
                GridPosition offsetGridPostion = new GridPosition(x, z);
                GridPosition testGridPosition = unitGridPostion + offsetGridPostion;

                //This is the logic for moving the unit on the grid


                //If position is valid we want to keep it. If not we want to discard
                //that is what this boundary does. Calling from levelgrid
                if (!LevelGrid.Instance.IsValidGridPosition(testGridPosition))
                {
                    //continue skips forward if the IF statement says that the iteration is not valid
                    //if it is valid then the code goes into the next step
                    continue;
                }
                if (unitGridPostion == testGridPosition)
                {
                    //Same gridPosition where the unit is already at
                    continue;
                }

                if (LevelGrid.Instance.HasAnyUnitOnGridPosition(testGridPosition))
                {
                    //Making sure units don't stack on the same position
                    continue;
                }


                if (!Pathfinding.Instance.IsWalkAbleGridPosition(testGridPosition))
                {
                    //If tile is not walkable then pathfinding will ignore the tile
                    continue;
                }


                if (!Pathfinding.Instance.HasPath(unitGridPostion, testGridPosition))
                {
                    //If tile is not walkable then pathfinding will ignore the tile
                    continue;
                }

                //This prevents the path from reading tiles through walls as walkable for the player
                int pathfindingDistanceMultiplier = 10;
                if (Pathfinding.Instance.GetPathLength(unitGridPostion, testGridPosition) > maxMoveDistance * pathfindingDistanceMultiplier)
                {
                    //Path length is too long
                    continue;
                }
                validGridPositionList.Add(testGridPosition);
            }
        }

        return validGridPositionList;
    }

    //Overrides the name for the action button
    public override string GetActionName()
    {
        return "Move";
    }

    public override int GetActionPointsCost()
    {
        return 1;
    }


    //This is setup for the enemy action override
    public override EnemyAIAction GetEnemyAIAction(GridPosition gridPosition)
    {
        //This helps determine to shoot before moving and the importance of it
        int targetCountAtGridPosition = unit.GetAction<ShootAction>().GetTargetCountAtPosition(gridPosition);
        return new EnemyAIAction
        {
            gridPosition = gridPosition,
            actionValue = targetCountAtGridPosition * 10,
        };
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;

public class Unit : MonoBehaviour
{
    private GridPosition gridPosition;
    private BaseAction[] baseActionArray;
    private int actionPoints = ACTION_POINTS_MAX;
    [SerializeField]
    private const int ACTION_POINTS_MAX = 2;
    public static event EventHandler OnAnyActionPointsChanged;
    //Creating event to cycle through friendly and enemy unit lists
    public static event EventHandler OnAnyUnitSpawned;
    public static event EventHandler OnAnyUnitDead;
    [SerializeField]
    private bool isEnemy;
    private HealthSystem healthSystem;

    private void Awake()
    {
        healthSystem = GetComponent<HealthSystem>();
        //Gets all components of a certain type that extends BaseAction
        baseActionArray = GetComponents<BaseAction>();

    }

    private void Start()
    {
        gridPosition = LevelGrid.Instance.GetGridPostion(transform.position);
        LevelGrid.Instance.AddUnitAtGridPostion(gridPosition, this);
        TurnSystem.Instance.OnTurnChanged += TurnSystem_OnTurnChanged;
        healthSystem.OnDead += HealthSystem_OnDead;
        OnAnyUnitSpawned?.Invoke(this, EventArgs.Empty);
    }
    private void Update()
    {

        GridPosition newGridPosition = LevelGrid.Instance.GetGridPostion(transform.position);
        if (newGridPosition != gridPosition)
        {
            GridPosition oldGridPosition = gridPosition;
            //unit change grid postion
            gridPosition = newGridPosition;

            LevelGrid.Instance.UnitMovedGridPosition(this, gridPosition, newGridPosition);

        }
    }

    // The : after T means "extends" so this says where T extends baseaction
    //T is a generic 
    public T GetAction<T>() where T : BaseAction
    {
        foreach (BaseAction baseAction in baseActionArray)
        {

            if (baseAction is T)
            {
                return (T)baseAction;
            }
        }
        return null;
    }

    public GridPosition GetGridPostion()
    {
        return gridPosition;
    }

    //expose baseaction array to be used in the UI
    public BaseAction[] GetBaseActionArray()
    {
        return baseActionArray;
    }

    public bool TrySpendActionPointsToTakeAction(BaseAction baseAction)
    {
        if (CanSpinActionPointsToTakeAction(baseAction))
        {
            SpendActionPoints(baseAction.GetActionPointsCost());
            return true;
        }
        else
        {
            return false;
        }
    }
    //setting up action points!!!!!!!!!!!!!!!
    public bool CanSpinActionPointsToTakeAction(BaseAction baseAction)
    {
        if (actionPoints >= baseAction.GetActionPointsCost())
        {
            return true;
        }
        else
        {
            return false;
        }
    }

    private void SpendActionPoints(int amount)
    {
        actionPoints -= amount;
        OnAnyActionPointsChanged?.Invoke(this, EventArgs.Empty);
    }

    //exposing the action points for the UI
    public int GetActionPoints()
    {
        return actionPoints;
    }

    private void TurnSystem_OnTurnChanged(object sender, EventArgs e)
    {
        if ((IsEnemy() && !TurnSystem.Instance.IsPlayerTurn()) ||
          (!IsEnemy() && TurnSystem.Instance.IsPlayerTurn()))
        {
            actionPoints = ACTION_POINTS_MAX;
            OnAnyActionPointsChanged?.Invoke(this, EventArgs.Empty);

        }
    }

    public bool IsEnemy()
    {
        return isEnemy;
    }

    //Setup HealthSystem Event to call for unit death

    private void HealthSystem_OnDead(object sender, EventArgs e)
    {
        //when unit dies this helps prevent the null reference that is created by destorying the gameobject
        LevelGrid.Instance.RemoveUnitAtGridPostion(gridPosition, this);
        OnAnyUnitDead?.Invoke(this, EventArgs.Empty);
    }

    public void Damage(int damageAmount)
    {
        healthSystem.Damage(damageAmount);
    }

    public Vector3 GetWorldPosition()
    {
        return transform.position;
    }



    //Expose the health system to use in enemy AI 
    public float GetHealthNormalized()
    {
        return healthSystem.GetHealthNormalized();
    }


}

I read a separate comment of yours elsewhere where you mention not to mess too much with the execution order. I think this may have been a bug early on I didn’t notice, because I had a separate bug with my Pathfinding and I just didn’t notice this one sooner.
Thanks for the help and the quick reply.

So far, the code looks good. Since the issue appears from your description to be related to the position not being cleared when a character leaves it, we need to follow the chain. In this case, we’re registering the move in LevelGrid.Instance.UnitMovedGridPosition(), so that’s the next place to look.
Paste in your LevelGrid class and we’ll look further.

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

public class LevelGrid : MonoBehaviour
{
    public static LevelGrid Instance { get; private set; }

    public event EventHandler OnAnyUnitMovedGridPosition;

    [SerializeField]
    private int width;
    [SerializeField]
    private int height;
    [SerializeField]
    private float cellSize;
    [SerializeField]
    private Transform gridDebugObjectPrefab;
    private GridSystem<GridObject> gridSystem;
    //SingleTon system, grabbing a prefab

    private void Awake()
    {
        //Part of setting up the singleton
        if (Instance != null)
        {
            Debug.LogError("There's more than one Instance on LevelGrid!" + transform + " - " + Instance);
            Destroy(gameObject);
            return;
        }
        Instance = this;

        gridSystem = new GridSystem<GridObject>(width, height, cellSize, (GridSystem<GridObject> g, GridPosition gridPosition) => new GridObject(g, gridPosition));
        //gridSystem.CreateDebugObjects(gridDebugObjectPrefab);

    }

    private void Start()
    {
        //parameters for the size of the grid. this can be changed in the editor
        Pathfinding.Instance.Setup(width, height, cellSize);
    }
    public void AddUnitAtGridPostion(GridPosition gridPostion, Unit unit)
    {
        GridObject gridObject = gridSystem.GetGridObject(gridPostion);
        gridObject.AddUnit(unit);
    }

    public List<Unit> GetUnitAtGridPostion(GridPosition gridPostion)
    {
        GridObject gridObject = gridSystem.GetGridObject(gridPostion);
        return gridObject.GetUnitList();
    }

    public void RemoveUnitAtGridPostion(GridPosition gridPosition, Unit unit)
    {
        GridObject gridObject = gridSystem.GetGridObject(gridPosition);
        gridObject.RemoveUnit(unit);
    }
    public void UnitMovedGridPosition(Unit unit, GridPosition fromGridPosition, GridPosition toGridPosition)
    {
        RemoveUnitAtGridPostion(fromGridPosition, unit);
        AddUnitAtGridPostion(toGridPosition, unit);
        OnAnyUnitMovedGridPosition?.Invoke(this, EventArgs.Empty);

    }
    //This line of code is the same as creating a function with brackers. Using the => is the same as this {return gridSystem.GetGridPostion(worldPostion);}
    // => automactically calls return
    public GridPosition GetGridPostion(Vector3 worldPosition) => gridSystem.GetGridPostion(worldPosition);

    public Vector3 GetWorldPosition(GridPosition gridPosition) => gridSystem.GetWorldPosition(gridPosition);
    //This is calling the function from the gridSystem
    public bool IsValidGridPosition(GridPosition gridPosition) => gridSystem.IsValidGridPosition(gridPosition);

    //These are pass through functions so that width and height can be accessed by gridSystem Visual
    public int GetWidth() => gridSystem.GetWidth();
    public int GetHeight() => gridSystem.GetHeight();

    //
    public bool HasAnyUnitOnGridPosition(GridPosition gridPosition)
    {
        GridObject gridObject = gridSystem.GetGridObject(gridPosition);
        return gridObject.HasAnyUnit();
    }
    //Setting up the shoot action
    //checking if an enemy is in the required distance of the shoot action
    public Unit GetUnitAtGridPosition(GridPosition gridPosition)
    {
        GridObject gridObject = gridSystem.GetGridObject(gridPosition);
        return gridObject.GetUnit();
    }
}

Two more scripts, which will hopefully tell us what’s going on (or not going on). LevelGrid’s methods are basically passthroughs to GridSystem and GridObject. GridSystem to find the correct GridObject to reference, and GridObject which contains the list of units.

Here is the GridObject

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

public class GridObject
{
    private GridSystem<GridObject> gridSystem;
    private GridPosition gridPosition;
    //The list allows for multiple units and not just one unit
    private List<Unit> unitList;

    public GridObject(GridSystem<GridObject> gridSystem, GridPosition gridPostion)
    {
        //This is storing the values to be used easily elsewhere
        this.gridSystem = gridSystem;
        this.gridPosition = gridPostion;
        unitList = new List<Unit>();
    }

    public override string ToString()
    {
        string unitString = "";
        foreach (Unit unit in unitList)
        {
            unitString += unit + "\n";
        }
        return gridPosition.ToString() + "\n" + unitString;
    }

    public void AddUnit(Unit unit)
    {
        unitList.Add(unit);
    }
    public void RemoveUnit(Unit unit)
    {
        unitList.Remove(unit);
    }

    public List<Unit> GetUnitList()
    {
        return unitList;
    }

    //This is setting up to make sure that units can be on the same space
    public bool HasAnyUnit()
    {
        return unitList.Count > 0;
    }

    //This is for setting up the shoot action
    public Unit GetUnit()
    {
        if (HasAnyUnit())
        {
            return unitList[0];
        }
        else
        {
            return null;
        }
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;

//Make this into a constructor because using monobehavior doesnt allow constructors and you need a constructor for parameters of the grid system
public class GridSystem<TGridObject>
{
    //A constructor is like a function with no return type. The name of Constructor is the same as the class name
    private int width;
    private int height;
    private float cellSize;

    //this is creating a 2d array because the coma makes an x and y
    private TGridObject[,] gridObjectArray;

    public GridSystem(int width, int height, float cellSize, Func<GridSystem<TGridObject>, GridPosition, TGridObject> createGridObject)
    {
        this.width = width;
        this.height = height;
        this.cellSize = cellSize;

        //creating the 2d grid array parameters
        gridObjectArray = new TGridObject[width, height];
        for (int x = 0; x < width; x++)
        {
            for (int z = 0; z < height; z++)
            {
                //This creates gridObjects 
                GridPosition gridPosition = new GridPosition(x, z);
                gridObjectArray[x, z] = createGridObject(this, gridPosition);
            }
        }
    }
    public Vector3 GetWorldPosition(GridPosition gridPosition)
    {
        return new Vector3(gridPosition.x, 0, gridPosition.z) * cellSize;
    }

    public GridPosition GetGridPostion(Vector3 worldPositiom)
    {
        return new GridPosition
    (
        Mathf.RoundToInt(worldPositiom.x / cellSize),
        Mathf.RoundToInt(worldPositiom.z / cellSize)
    );
    }

    public void CreateDebugObjects(Transform gridDebugObjectPrefab)
    {
        for (int x = 0; x < width; x++)
        {
            for (int z = 0; z < height; z++)
            {
                GridPosition gridPosition = new GridPosition(x, z);

                Transform debugTransform = GameObject.Instantiate(gridDebugObjectPrefab, GetWorldPosition(gridPosition), Quaternion.identity);
                GridDebugObject gridDebugObject = debugTransform.GetComponent<GridDebugObject>();
                gridDebugObject.SetGridOjbect(GetGridObject(gridPosition));
            }
        }
    }
    public TGridObject GetGridObject(GridPosition gridPosition)
    {
        return gridObjectArray[gridPosition.x, gridPosition.z];
    }

    //This is setting up boundraies for the unit
    public bool IsValidGridPosition(GridPosition gridPosition)
    {
        return gridPosition.x >= 0 &&
            gridPosition.z >= 0 &&
            gridPosition.x < width &&
            gridPosition.z < height;
    }

    //exposing the width and height
    public int GetWidth()
    {
        return width;
    }

    public int GetHeight()
    {
        return height;
    }


}

I’m not seeing a reason here why a GridPosition would still be marked as occupied. I’ll have to take a closer look at the project.
Zip up your project and upload it to https://gdev.tv/projectupload/ and I’ll take a look.
Be sure to remove the Library folder to conserve space.

Thank you for taking a closer look at this. I uploaded the zip to the link you shared.

You will see I sent two projects. The HOTFR ZIP is the correct file. The other one was a mistake.

I have the project, and am installing the relevant version of Unity, so I probably won’t be able to get to it till tomorrow.

Thank you for the update!

I’m not sure what I’m seeing here… The game isn’t quite what I expected. Nevertheless, a 2d tile based game should work with our code just fine, as ultimately, before the expansion to multi-floors, our course is a 2d game that looks 3d.

The only scene that seemed to involve characters was the GrassBattleField. I was able to move the Player Hero or Noble anywhere on the scene I wanted (no distance restriction), but no path at all was shown and the characters moved instantly. Likewise, if I clicked on a Hero or Noble, and then on a Bandit, the bandit was instantly removed, regardless of distance (or obstacles). No tiles changed color to indicate they were moveable locations with the pathfinding, nor were any tiles but mountains off limits at all.

In short, I’m confused…

Yeah I mistakingly sent you the old version of the project before taking this course. I sent a second version called hotfr that has the actual code I was referring.
Im sorry that you worked through the old project.

That was the HOTFR ZIP which was the second project uploaded.

Dang it. I will resubmit with proper code. Sorry about the time waste!

I submitted TurnBasedZIP this is the project that Hugo taught. Hopefully this one shows you the problem I’m experiencing.

Privacy & Terms