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:
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.