I am currently working on AI Behaviour in a Beat-Em-Up game in Unity for my beat-em-up project, I am currently using my own statemachine system (with states that don't inherit from monobehaviour) to control my actors behaviour and use the free version of the A* Pathfinding Project all of this combines into a system that right now looks like this:
using System;
using System.Collections.Generic;
using AYellowpaper.SerializedCollections;
using Lean.Pool;
using UnityEngine;
using UnityEngine.Rendering;
public class EnemyController : MonoBehaviour, IDamagable
{
public float MovementSpeed => movementSpeed;
public float ChaseDistance => chaseDistance;
public float StoppingDistance => stoppingDistance;
public Transform PlayerTransform => playerTransform;
public float NextWaypointDistance => nextWaypointDistance;
[SerializeField] private Stats enemyStats;
[SerializeField] private CustomStateMachine _customStateMachine;
[Header("Movement parameters")]
[SerializeField] private float movementSpeed = 1.5f;
[SerializeField] private float chaseDistance = 5f;
[SerializeField] private float stoppingDistance = 1f;
[SerializeField] private float nextWaypointDistance = 1f;
[SerializedDictionary("Drop Item", "MaxRange")]
public AYellowpaper.SerializedCollections.SerializedDictionary<GameObject, int> drops;
public Stats EnemyStats => enemyStats;
private GameObject playerGameObject;
private Transform playerTransform;
// Start is called once before the first execution of Update after the MonoBehaviour is created
void Awake()
{
if (_customStateMachine == null)
{
_customStateMachine = GetComponent<CustomStateMachine>();
}
if (enemyStats == null)
{
enemyStats = GetComponent<Stats>();
}
playerGameObject = GameObject.FindWithTag("Player");
if (playerGameObject != null)
{
playerTransform = playerGameObject.transform;
Debug.Log("Player found at: " + playerTransform.position);
}
else
{
Debug.LogWarning("No player object found");
}
_customStateMachine.SetInitialState(new Enemy_WalkingState());
}
private void Start()
{
}
// Update is called once per frame
void Update()
{
}
public void TakeDamage(float strength, AttackData attackData)
{
float randomVariance = UnityEngine.Random.Range(0.8f, 1.2f);
float defense = (enemyStats.GetBaseDefense() * enemyStats.GetModifier("DefenseModifier"));
float damage = ((attackData.BaseDamage *
(strength / (strength + defense)))) ;
if (enemyStats.GetWeakness() == attackData.Element && attackData.Element != AttackData.ElementType.NONE)
{
damage *= 1.5f;
}
damage *= randomVariance;
damage = Mathf.RoundToInt(damage);
enemyStats.ApplyDamage(damage);
Debug.Log("Damage taken: " + damage + ", strength: " + strength + ", defense: " + defense);
if (enemyStats.GetCurrentHealth() <= 0)
{
Die();
}
}
public virtual void Die()
{
foreach (KeyValuePair<GameObject, int> kvp in drops)
{
int randomRange = UnityEngine.Random.Range(1, kvp.Value);
for (int i = 0; i < randomRange; i++)
{
LeanPool.Spawn(kvp.Key, transform.position, Quaternion.identity);
}
}
Destroy(gameObject);
}
}
using System;
using System.Numerics;
using Pathfinding;
using Unity.VisualScripting;
using UnityEngine;
using Random = UnityEngine.Random;
using Vector2 = UnityEngine.Vector2;
using Vector3 = UnityEngine.Vector3;
public class Enemy_WalkingState : State
{
private EnemyController _enemyController;
private Rigidbody2D _rb;
private CircleCollider2D _capsule;
private Seeker _seeker;
private float _movementSpeed;
private float _chasingDistance;
private float _stoppingDistance;
private float _nextWaypointDistance;
private Transform _playerTransform;
private Path _path;
private int _currentWaypoint = 0;
private bool _hasReachedEndOfPoint;
private GameObject _owner;
private float timer;
protected internal override void EnterState(CustomStateMachine customStateMachine, GameObject owner)
{
base.EnterState(customStateMachine, owner);
_owner = owner;
if (owner.GetComponent<EnemyController>() != null)
{
_enemyController = owner.GetComponent<EnemyController>();
_rb = owner.GetComponent<Rigidbody2D>();
_movementSpeed = _enemyController.MovementSpeed;
_chasingDistance = _enemyController.ChaseDistance;
_stoppingDistance = _enemyController.StoppingDistance;
_nextWaypointDistance = _enemyController.NextWaypointDistance;
_playerTransform = _enemyController.PlayerTransform;
}
if (owner.GetComponent<Seeker>() != null)
{
_seeker = owner.GetComponent<Seeker>();
}
if (owner.GetComponent<CircleCollider2D>() != null)
{
_capsule = owner.GetComponent<CircleCollider2D>();
}
ChooseDestination();
}
private void ChooseDestination()
{
Debug.Log("Choose destination");
//Choose point in range 5f around player and move there, use CheckIfValid if point is a valid direction
if (_seeker.IsDone())
{
Vector2 desiredPosition = _playerTransform.position + new Vector3(Random.Range(-_stoppingDistance, _stoppingDistance), Random.Range(-_stoppingDistance, _stoppingDistance), 0);
Debug.Log("Desired position: " + desiredPosition);
_seeker.StartPath(_rb.position, desiredPosition, OnPathComplete);
}
}
private void OnPathComplete(Path p)
{
if (!p.error)
{
_path = p;
_currentWaypoint = 0;
}
else
{
Debug.LogError($"Path error: {p.error}");
}
}
private void CheckIfValid()
{
//Use RayCast to check if point is valid
}
protected internal override void ExitState()
{
_owner.GetComponent<MonoBehaviour>().CancelInvoke("ChooseDestination");
}
protected internal override void UpdateState()
{
if (timer <= 0.5f)
{
timer += Time.deltaTime;
}
else
{
timer = 0;
ChooseDestination();
}
}
protected internal override void FixedUpdateState()
{
MoveEnemy();
}
private void MoveEnemy()
{
if (_path == null)
{
return;
}
if (_currentWaypoint >= _path.vectorPath.Count)
{
_hasReachedEndOfPoint = true;
//ChooseDestination();
return;
}
else
{
Debug.Log("Destination reached");
_hasReachedEndOfPoint = false;
}
if (Vector2.Distance(_rb.position, _playerTransform.position) > _stoppingDistance)
{
Vector2 direction = ((Vector2)_path.vectorPath [_currentWaypoint] - _rb.position).normalized;
Vector2 velocity = direction * _movementSpeed;
_rb.linearVelocity = velocity;
float distance = Vector2.Distance(_rb.position, _path.vectorPath[_currentWaypoint]);
if (distance < _nextWaypointDistance)
{
_currentWaypoint++;
Debug.Log("NextWaypoint");
}
}
else
{
_rb.linearVelocity = Vector2.MoveTowards(_rb.linearVelocity, Vector2.zero, 25 *Time.deltaTime);
}
}
}
At the moment my enemies are pretty dumb, just choosing a point in a range inside the stopping distance from the player every .5 seconds This is a janky and by no means perfect solution
What I would like to have is a system similar to old beat-em-ups like Double Dragon or retrostyle revivals like Scott Pilgrim vs. The World: The Game or River City Girls where enemies get to patrol before they attack or chase the player. I am kinda of stuck at the moment and would love if someone can lend me a hand or point me in the right direction