In Roman Revenant I made AI behavior for the enemies (see code). But in my next game I wanted to improve the AI. So in this game I did just that. In stead of only making the AI for the enemies, this AI worked for all units, so both friendly and enemy.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public abstract class BaseState
{
public StateHandler stateHandler;
public BaseState(StateHandler statehandler)
{
this.stateHandler = statehandler;
}
public virtual void Enter()
{
}
public virtual void Exit()
{
}
public virtual void Update()
{
}
// Returns the closest unit that hasn't the same id
protected (Unit enemy, float distance) GetClosestEnemy()
{
float closestDistance = float.MaxValue;
Unit closestTarget = null;
foreach (Unit unit in SpawnManager.Instance.units)
{
if (unit.factionId == stateHandler.Unit.factionId)
{
continue;
}
float magnitude = (stateHandler.transform.position - unit.transform.position).sqrMagnitude;
if (magnitude < closestDistance)
{
closestTarget = unit;
closestDistance = magnitude;
}
}
if (closestTarget != null)
{
return (closestTarget, closestDistance);
}
else
{
return (null, 0f);
}
}
public virtual bool AbsoluteState()
{
return false;
}
}
So instead of one enum that keeps track of all the states. I have an abstract BaseState that every state can inherit from. That way my code keeps clean and readable if I add a lot more states. The AbsoluteState is a function that has to do with priority. If the unit is in a state, for example: attack state. You don't want the unit to cancel their attack animation in the middle of attacking by switching to another state. So if the attack state has the AbsoluteState function and it returns true, then if the an other state wants to be the current state, it can't.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
#if UNITY_EDITOR
using UnityEditor;
#endif
using UnityEngine;
public class StateHandler : MonoBehaviour
{
public Unit Unit { get; private set; }
public BaseState currentState;
#if UNITY_EDITOR
[SerializeField, ReadOnly] private string debugStateActive;
#endif
public void SwitchState(BaseState state)
{
if (currentState == null)
{
currentState = state;
return;
}
Type stateScriptType = state.GetType();
MethodInfo stateMethod = stateScriptType.GetMethod
("AbsoluteState", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly);
if (!currentState.AbsoluteState() || stateMethod == null)
{
currentState.Exit();
currentState = state;
currentState.Enter();
}
}
private void Awake()
{
Unit = GetComponent<Unit>();
}
private void Update()
{
if (currentState != null)
{
currentState.Update();
}
#if UNITY_EDITOR
if (currentState != null)
{
debugStateActive = currentState.GetType().Name;
}
else
{
debugStateActive = "null";
}
#endif
}
}
Each unit has their own StateHandler. It's task it to manage and switch the unit's state. I have also add a debugStateActive, for testing purposes. That way I can always check in which state the unit is in.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MoveAndAttackEnemyState : BaseState
{
// This script/state is for both units
private Unit currentTarget;
private bool canAttack = true;
public MoveAndAttackEnemyState(StateHandler statehandler) : base(statehandler)
{
}
public override void Update()
{
if (!stateHandler.Unit.Attack)
{
return;
}
(Unit unit, float distance) targetData = GetClosestEnemy();
if (targetData.unit != null && targetData.distance < stateHandler.Unit.unitStats.NoticeRange)
{
stateHandler.Unit.agent.SetDestination(targetData.unit.transform.position);
} else
{
if (stateHandler.Unit.factionId == 0)
{
stateHandler.SwitchState(new MoveToGoalState(stateHandler));
}
else
{
stateHandler.SwitchState(new WanderState(stateHandler));
}
}
currentTarget = targetData.unit;
if (canAttack && currentTarget != null && targetData.distance < stateHandler.Unit.unitStats.AttackRange)
{
Attack();
}
}
private void Attack()
{
canAttack = false;
stateHandler.StartCoroutine(AttackCooldown());
currentTarget.currentHealth -= stateHandler.Unit.Damage * TypeChart.GetEffectiveness(stateHandler.Unit.Type, currentTarget.Type);
Debug.Log("hit:" + currentTarget + "for:" + stateHandler.Unit.Damage * TypeChart.GetEffectiveness(stateHandler.Unit.Type, currentTarget.Type));
}
private IEnumerator AttackCooldown()
{
yield return new WaitForSeconds(stateHandler.Unit.unitStats.AttackCooldown);
canAttack = true;
}
}
The problem was that the enemies waited until the group before them was done with all the spawning before they started spawning. So I could only spawn 1 group at a time. And that's a big deal, because let's say if I constantly spawn a group and I want to spawn a boss in between the spawning of the group, that wouldn't be possible.
private IEnumerator SpawnUnits()
{
foreach(SpawnInfo group in spawnInfo)
{
//Code for spawning
int currentSpawnAmountLeft = group.spawnAmount;
yield return new WaitForSeconds(group.firstUnitSpawnTime);
for (int i = 0; i < group.spawnAmount; i++)
{
if (currentSpawnAmountLeft <= 0)
{
yield return null;
}
GameObject newUnitGO = Instantiate
(group.enemyUnitGroup, spawnLocations.position, spawnLocations.rotation);
Unit newUnit = newUnitGO.GetComponent<Unit>();
newUnit.SetFaction(0); // 0 are the enemies, 1 are the friendlies
enemyUnits.Add(newUnit);
currentSpawnAmountLeft -= 1;
yield return new WaitForSeconds(group.delayBetweenSpawns);
}
}
yield return null;
}
The solution is to go in a second coroutine and spawn the enemies there. Different from how it was, now every group starts its own coroutine. And I just give the variable of the foreach to the second coroutine so it still knows its data from the struct.
private IEnumerator SpawnUnits()
{
foreach(SpawnInfo group in spawnInfo)
{
StartCoroutine(PerGroup(group));
}
yield return null;
}
private IEnumerator PerGroup(SpawnInfo group)
{
//Code for spawning
}
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
public class SpawnManager : MonoBehaviour
{
// Instantiating this script
public static SpawnManager Instance { get; private set; }
public List<Transform> spawnLocations = new();
public List<Unit> enemyUnits = new();
public List<SpawnInfo> spawnInfo = new();
private void Awake()
{
// Making it a singleton
if (Instance == null)
{
Instance = this;
}
else
{
Debug.LogError("Another SpawnManager is in the scene!!");
}
}
private void Start()
{
StartCoroutine(SpawnUnitGroups());
}
private IEnumerator SpawnUnitGroups()
{
foreach(SpawnInfo group in spawnInfo)
{
StartCoroutine(PerGroup(group));
}
yield return null;
}
private IEnumerator PerGroup(SpawnInfo group)
{
// Code for spawning each enemy
int currentSpawnAmountLeft = group.spawnAmount;
yield return new WaitForSeconds(group.firstUnitSpawnTime);
for (int i = 0; i < group.spawnAmount; i++)
{
if (currentSpawnAmountLeft <= 0)
{
yield return null;
}
GameObject newUnitGO = Instantiate
(group.enemyUnitGroup, spawnLocations[0].position, spawnLocations.rotation);
Unit newUnit = newUnitGO.GetComponent<Unit>();
newUnit.SetFaction(0); // 0 are the enemies, 1 are the friendlies
enemyUnits.Add(newUnit);
currentSpawnAmountLeft -= 1;
yield return new WaitForSeconds(group.delayBetweenSpawns);
}
yield return null;
}
}