More than 1 AI turn crashes Unity Editor

As soon as I implemented the new code the more actions the AI take the slower the editor got. In fact if I left the AI take a 2nd turn the editor became unresponsive, event the profiler stops updating.

Eventually the editor will either crash or just sit there in a frozen state. CPU usage = 10% and Memory is at around 2800MB.

Sometimes it will come back to life at a very low frame rate, you can see the editor freezing.

I figured I would finish this lesson to see if it was a know issue but there wasn’t just an additional change. Which interestingly as soon as I did the enemies stopped shooting all together and just wanted to walk.

I can’t figure out what is causing the crash, any help would be great. I have added a few of the scripts below.

I have also recorded a video showing the behaviour: https://youtu.be/dP7qkyBmB6Q

Code below

EnemyAI.cs

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

public class EnemyAI : MonoBehaviour
{

    private enum State
    {
        WaitingForEnemyTurn,
        TakingTurn,
        Busy
    }

    private State state;
    private float timer;
    private void Awake() {
        state = State.WaitingForEnemyTurn;
    }
    private void Start() {
        TurnSystem.Instance.OnTurnChanged += TurnSystem_OnTurnChanged;
        
    }
    private void Update() {
        if(TurnSystem.Instance.IsPlayerTurn())
        {
            return;
        }

        switch (state)
        {
            case State.WaitingForEnemyTurn:
                break;
            case State.TakingTurn:
                timer -= Time.deltaTime;
                if(timer <= 0f)
                {
                    
                    if(TryTakeEnemyAIAction(SetStateTakingTurn))
                    {
                        state = State.Busy;
                    } else 
                    {
                        // No more enemies have actions they can take, end enemy turn
                        TurnSystem.Instance.NextTurn();
                    }
                    

                }
                break;
            case State.Busy:
                break;
        }




    }

    private void SetStateTakingTurn()
    {
        timer = 0.5f;
        state = State.TakingTurn;
    }
    private void TurnSystem_OnTurnChanged(object sender, EventArgs e)
    {
        if(!TurnSystem.Instance.IsPlayerTurn())
        {
            state = State.TakingTurn;
            timer = 2f;
        }
        
    }

    private bool TryTakeEnemyAIAction(Action onEmemyActionComplete)
    {
        foreach(Unit enemnyUnit in UnitManager.Instance.GetEnemyUnitList())
        {
            if(TryTakeEnemyAIAction(enemnyUnit, onEmemyActionComplete))
            {
                return true;
            }
            
        }
        return false;
    }

    private bool TryTakeEnemyAIAction(Unit enemyUnit, Action onEmemyAIActionComplete)
    {
        EnemyAIAction bestEnemyAIAction = null;
        BaseAction bestBaseAction = null;

        foreach(BaseAction baseAction in enemyUnit.GetBaseActionArray())
        {
            if(!enemyUnit.CanSpendActionPointsToTakeAction(baseAction))
            {
                // Enemy cannot afford this action
                continue;
            }

            if(bestEnemyAIAction == null)
            {
                bestEnemyAIAction = baseAction.GetBestEmemyAIAction();
                bestBaseAction = baseAction;
            } else 
            {
                EnemyAIAction testEnemyAIAction = baseAction.GetBestEmemyAIAction();
                if(testEnemyAIAction != null && testEnemyAIAction.actionValue > bestEnemyAIAction.actionValue)
                {
                    bestEnemyAIAction = testEnemyAIAction;
                    bestBaseAction = baseAction;
                }
            }
            
        }

        if(bestEnemyAIAction != null && enemyUnit.TrySpendActionPointsToTakeAction(bestBaseAction))
        {
            bestBaseAction.TakeAction(bestEnemyAIAction.gridPosition, onEmemyAIActionComplete);
            return true;
        } else 
        {
            return false;
        }

    }

}

ShootAction.cs

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

public class ShootAction : BaseAction
{
    public event EventHandler<OnShootEventArgs> OnShoot;

    public class OnShootEventArgs : EventArgs
    {
        public Unit targetUnit;
        public Unit shootingUnit;
    }
    private enum State
    {
        Aiming,
        Shooting,
        Cooloff
    }

    private State state;
    private int maxShootDistance = 7;
    private float stateTimer;
    private Unit targetUnit;
    private bool canShootBullet;

    void Update()
    {
        if(!isActive)
        {
            return;
        }

        stateTimer -= Time.deltaTime;
        switch(state)
        {
            case State.Aiming:
                Vector3 aimDir = (targetUnit.GetWordPosition() - unit.GetWordPosition()).normalized;
                float rotateSpeed = 10f;
                transform.forward = Vector3.Lerp(transform.forward, aimDir, rotateSpeed * Time.deltaTime);
                break;
            case State.Shooting:
                if(canShootBullet)
                {
                    Shoot();
                    canShootBullet = false;
                }
                break;
            case State.Cooloff:

                break;
        }

        if(stateTimer <= 0f)
        {
            NextState();
        }


    }

    private void NextState()
    {
        switch(state)
        {
            case State.Aiming:
                state = State.Shooting;
                float shootingStateTime = 0.1f;
                stateTimer = shootingStateTime;
                break;
            case State.Shooting:
                state = State.Cooloff;
                float coolOffStateTime = 0.5f;
                stateTimer = coolOffStateTime;
                break;
            case State.Cooloff:
                ActionComplete();
                break;
        }


    }

    private void Shoot()
    {
        OnShoot?.Invoke(this, new OnShootEventArgs {
            targetUnit = targetUnit,
            shootingUnit = unit
        });
        targetUnit.Damage(40);
    }
    public override string GetActionName()
    {
        return "Shoot";
    }

    public override List<GridPosition> GetValidActionGrisPositionList()
    {
        GridPosition unitGridPosition = unit.GetGridPosition();
        return GetValidActionGrisPositionList(unitGridPosition);
    }
    public List<GridPosition> GetValidActionGrisPositionList(GridPosition unitGridPosition)
    {
        List<GridPosition> validGridPositionList = new List<GridPosition>();
        
        for (int x = -maxShootDistance; x <= maxShootDistance; x++)
        {
            for (int z = -maxShootDistance; z <= maxShootDistance; z++)
            {
                GridPosition offsetGridPosition = new GridPosition(x,z);
                GridPosition testGridPosition = unitGridPosition + offsetGridPosition;

                if(!LevelGrid.Instance.IsValidGridPosition(testGridPosition))
                {
                    // Is position in bounds
                    continue;
                }



                // UPDATE TO CHANGE TO VECTOR3.DISTANCE FOR AN ACTUAL CIRCLE
                int testDistance = Mathf.Abs(x) + Mathf.Abs(z);
                if(testDistance > maxShootDistance)
                {
                    continue;
                }


                if(!LevelGrid.Instance.HasAnyUnitOnGridPosition(testGridPosition))
                {
                    // Is position occupied
                    continue;
                }
                
                Unit targetUnit = LevelGrid.Instance.GetUnitAtGridPosition(testGridPosition);
                if(targetUnit.IsEmeny() == unit.IsEmeny())
                {
                    // Both units are on the same team
                    continue;
                }



                validGridPositionList.Add(testGridPosition);
                
            }
        }
        return validGridPositionList;
    }

    public override void TakeAction(GridPosition gridPosition, Action onActionComplete)
    {
        targetUnit = LevelGrid.Instance.GetUnitAtGridPosition(gridPosition);
        canShootBullet = true;
        state = State.Aiming;
        float aimingStateTime = 1f;
        stateTimer = aimingStateTime;        

        ActionStart(onActionComplete);
    }

    public Unit GetTargetUnit(){
        return targetUnit;
    }

    public int GetMaxShootDistance(){
        return maxShootDistance;
    }

    public override EnemyAIAction GetEnemyAIAction(GridPosition gridPosition)
    {

        Unit targetUnit = LevelGrid.Instance.GetUnitAtGridPosition(gridPosition);
        

        return new EnemyAIAction {
            gridPosition = gridPosition,
            actionValue = 100 + Mathf.RoundToInt((1 - targetUnit.GetHealthNormalized() * 100f))
        };
    }

    public int GetTargetCountAtPosition(GridPosition gridPosition)
    {
        return GetValidActionGrisPositionList(gridPosition).Count;
    }

}

BaseAction.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
public abstract class BaseAction : MonoBehaviour
{
    public static event EventHandler OnAnyActionStarted;
    public static event EventHandler OnAnyActionCompleted;
    protected Unit unit;
    protected bool isActive;

    protected Action onActionComplete;
    protected virtual void Awake() {
        unit = GetComponent<Unit>();
    }

    public abstract string GetActionName();

    public abstract void TakeAction(GridPosition gridPosition, Action onActionComplete);
    public virtual bool IsValidActionGridPosition(GridPosition gridPosition)
    {
        List<GridPosition> validGridPositionList = GetValidActionGrisPositionList();
        return validGridPositionList.Contains(gridPosition);
    }

    public abstract List<GridPosition> GetValidActionGrisPositionList();

    public virtual int GetActionPointsCost()
    {
        return 1;
    }

    protected void ActionStart(Action onActionCompelte)
    {
        isActive = true;
        this.onActionComplete = onActionCompelte;
        OnAnyActionStarted?.Invoke(this, EventArgs.Empty);
    }

    protected void ActionComplete()
    {
        isActive = false;
        onActionComplete();
        OnAnyActionCompleted?.Invoke(this, EventArgs.Empty);
    }

    public Unit GetUnit()
    {
        return unit;
    }

    public EnemyAIAction GetBestEmemyAIAction()
    {
        List<EnemyAIAction> enemyAIActionList = new List<EnemyAIAction>();
        List<GridPosition> validActionGridPositionList =  GetValidActionGrisPositionList();

        foreach (GridPosition gridPosition in validActionGridPositionList)
        {
            EnemyAIAction enemyAIAction = GetEnemyAIAction(gridPosition);
            enemyAIActionList.Add(enemyAIAction);
        }

        if(enemyAIActionList.Count > 0)
        {
            enemyAIActionList.Sort((EnemyAIAction a, EnemyAIAction b) => b.actionValue - a.actionValue);

            return enemyAIActionList[0];
        } else 
        {
            // NO possible enemy AI actions
            return null;
        }
    }

    public abstract EnemyAIAction GetEnemyAIAction(GridPosition gridPosition);

}

Further Investigation 1
After stopping the action and setting the enemy turn to this:
image
I was only able to make to Turn 8 before the editor became unresponsive. I don’t know how far spread this issue is.

Further Investigation 2
Compiled the game and had the same behaviour. I am running windows 11 so tested on another machine with windows 10 and the issue persists.

On the Profiler, click on those huge spikes to see the Timeline which will show you what scripts are taking that long. I suspect you are somehow generating tons of garbage in some function.

Looking at the code in the video doing the Spin action all seems correct. Maybe the SpinAction.TakeAction(); is doing something strange?

1 Like

Here is the info from the profiler:

It seems to point to the enemyAI update but I can’t see anything wrong here, at least I seemed to have followed the lesson correctly after re-watching, I might be missing something.

    private void Update() {
        if(TurnSystem.Instance.IsPlayerTurn())
        {
            return;
        }

        switch (state)
        {
            case State.WaitingForEnemyTurn:
                break;
            case State.TakingTurn:
                timer -= Time.deltaTime;
                if(timer <= 0f)
                {
                    
                    if(TryTakeEnemyAIAction(SetStateTakingTurn))
                    {
                        state = State.Busy;
                    } else 
                    {
                        // No more enemies have actions they can take, end enemy turn
                        state = State.WaitingForEnemyTurn;
                        TurnSystem.Instance.NextTurn();
                    }
                    

                }
                break;
            case State.Busy:
                break;
        }




    }

    private void SetStateTakingTurn()
    {
        timer = 0.5f;
        state = State.TakingTurn;
    }
    private void TurnSystem_OnTurnChanged(object sender, EventArgs e)
    {
        if(!TurnSystem.Instance.IsPlayerTurn())
        {
            state = State.TakingTurn;
            timer = 2f;
        }
        
    }

    private bool TryTakeEnemyAIAction(Action onEmemyActionComplete)
    {
        foreach(Unit enemnyUnit in UnitManager.Instance.GetEnemyUnitList())
        {
            if(TryTakeEnemyAIAction(enemnyUnit, onEmemyActionComplete))
            {
                return true;
            }
            
        }
        return false;
    }

    private bool TryTakeEnemyAIAction(Unit enemyUnit, Action onEmemyActionComplete)
    {

        EnemyAIAction bestEnemyAIAction = null;
        BaseAction bestBaseAction = null;

        foreach(BaseAction baseAction in enemyUnit.GetBaseActionArray())
        {
            if (!enemyUnit.CanSpendActionPointsToTakeAction(baseAction))
            {
                // Enemy cannot afford this action
                continue;
            }

            if (bestEnemyAIAction == null)
            {
                bestEnemyAIAction = baseAction.GetBestEmemyAIAction();
                bestBaseAction = baseAction;
            }
            else
            {
                EnemyAIAction testEnemyAIAction = baseAction.GetBestEmemyAIAction();
                if (testEnemyAIAction != null && testEnemyAIAction.actionValue > bestEnemyAIAction.actionValue)
                {
                    bestEnemyAIAction = testEnemyAIAction;
                    bestBaseAction = baseAction;
                }
            }

            
        }

        if(bestEnemyAIAction != null && enemyUnit.TrySpendActionPointsToTakeAction(bestBaseAction))
        {
            bestBaseAction.TakeAction(bestEnemyAIAction.gridPosition, onEmemyActionComplete);
            return true;
        } else 
        {
            return false;
        }

    }

I tried to change the Update() to the following

    private void Update() {
        if(TurnSystem.Instance.IsPlayerTurn())
        {
            return;
        }

        timer -= Time.deltaTime;
        if(timer <= 0f)
        {
            TurnSystem.Instance.NextTurn();
        }
        return;

By turn 11 this was how long it got stuck for:

UPDATE
I changed the TurnSystem NextTurn() to just refresh action points

    public void NextTurn()
    {
        //turnNumber++;
        //isPlayerTurn = !isPlayerTurn;
        OnTurnChanged?.Invoke(this, EventArgs.Empty);
    }

And after a number of actions and resets the performance degrades exponentially

It seems to point to garbage collection but this is entering the area out of my knowledge :frowning: this could be the end for me.

Nothing stood out to me as an obvious issue but I had one question. How many unts/enemies do you have in the scene? Did you happen to make a really large scene with lots of units?

It has even less than the lesson, two units and one enemy unit. The more units/enemies the faster it locks up

Here is a video of nothing but player units having the same result:
https://youtu.be/lWrVnD95gh4

Sounds really odd. How much ram do you have in your machine? The reason I ask is whether it’s doing some memory swapping.

32GB, it doesn’t seem the system is running low while it is running:

image

You have 1.7 GB of garbage being allocated, at most it should be in the order of Kilobytes, so you definitely have some weird endless loop in your logic.

On the Profiler, on the top bar click on the “Deep Profile” button, then run the game again, trigger the bug again and click to select the spike frame again. It will let you see exactly what function is generating all that garbage.

Since it happens the more times you end the turn check to see how many listeners you have on the OnTurnChanged event. Maybe you have some logic that is adding more and more subscribers on every turn. In VS Code, right click on the event definition and Find References to see where you’re subscribing.

2 Likes

Thank you both for the help. The deep profile option was very helpful and narrowed it down to having a duplicate event subscription sat inside a function which was causing it to be exponentially subscribed and called.

What a few days this has been! I really do like events but man can you cause yourself some real problems really easily :smiley:

Back on track :+1:

2 Likes

That’ll do it. Damn, almost as bad as a while loop that never breaks.

Grats on figuring it out.

1 Like

Thanks again for chiming in, I really thought I was going to have to restart the course!

2 Likes

This topic was automatically closed 20 days after the last reply. New replies are no longer allowed.

Privacy & Terms