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

2 Replies 2

So what you have is really a set of actions: setting a target location you want to be at, determining a path to that location, and moving your enemy. The question becomes "where do I want to go?", which in your case sounds like it is decided by "am I aware of the player?". Thus you have a fourth requirement: determining when your enemies have spotted a player (usually line-of-sight).

Putting this all together, your unaware enemies may "patrol" an area (simply following a pre-determined path, which can be represented as a set of points that are travelled to). Upon becoming aware of the player, they switch to pathing directly to the current location of the player. As the player moves, this target updates, and so does the path the enemy will try to follow.

Adding variations to the movement (e.g. "maximum rotation per game tick" or "flying vs not") can be done in numerous ways, but it all comes down to a pretty solid reasoning of physical space (2D or 3D).

Wouldn't be patrolling and chasing two separate states? I would expect the enemy to loop through waypoints as long as in the first -> update to the next destination (waypoint) once the current one is reached. If we get close enough to the player enter chasing state in which you refresh the destination to the current player position continuously, not only every x seconds. I would then also not use a random spot in range around the player .. when chasing you want to reach the exact player position

Your Reply

By clicking “Post Your Reply”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.