Merging the Third Person Controller Course with the RPG Course

a little bit better. The news? If you push him around, he will want to return to wherever his point was (as long as it doesn’t get too far to the point that he gets lazy about patrolling). However, beyond a specific point of distance, he won’t even bother trying to patrol again, and I’m guessing that’s why he’s not in a patrol state…

That sounds like the acceptance radius is too small in the Patrol State.

OK so… for now he patrols. The changes I did were simple, after making him a third duplicate of my main enemy:

  1. Assign his own patrol path to him, in his own RespawnManager.cs script (I deleted the patrol paths off the EnemyStateMachine.cs script, works fine)
  2. Get him as close as possible to his own patrol points’ starting point (although that’s not the case for my original enemy that I copied off from, and that original enemy is somehow working just fine…)
  3. adjust the acceptance radius in the ‘EnemyPatrolState.cs’ script

BUT… there are some new issues:

a. if you attack and evade him, he won’t even bother trying to hunt you down, or even return to his normal patrolling state. He’ll just stand there, waiting for life to happen. If you get back into his radius he will attack you, otherwise he’s not going to do anything…

In further details, if you enter his range and attack him and leave, he will play the idle animation, wait for some time (as expected), turn to the next patrolling point, but he won’t move a muscle, and neither will he play the movement animations (because he’s not moving… I’m guessing speed was supposed to be applied somewhere, but wasn’t?)

Edit: From live testing during gameplay, if you punch the guard or beat him up somehow, and then evade his sight, the FreeLookSpeed variable on that specific enemy literally keeps dropping to values way below zero, to beyond 40 decimal points below zero for some reason… That’s why he won’t move a muscle. How do I fix this?


b. If we don’t have the ‘EnemyPatrolState.cs’ script attached to the enemy gameObject, how can you make the acceptanceRadius a variable that can be tuned for each enemy individually? Perhaps bring that value up in the hierarchy to ‘EnemyStateMachine.cs’ instead? (Edit: This one was solved by adding ‘PatrolPoint.cs’ to each Patrolling Waypoint)

Paste in your EnemyPatrolState…

Yep, you could do exactly that. And no, there’s no way to attach EnemyPatrolState to the enemy, it’s not a MonoBehaviour

using RPG.Control;
using UnityEngine;

namespace RPG.States.Enemies 
{
    public class EnemyPatrolState : EnemyBaseState 
    {
        private const string NextPatrolPointIndexKey = "NextPatrolPointIndex";

        public EnemyPatrolState(EnemyStateMachine stateMachine) : base(stateMachine) {}

        private float movementSpeed = 0.5f;
        private float acceptanceRadius = 2f;
        private float dwellTime = 2f;
        private Vector3 targetPatrolPoint;

        public override void Enter()
        {
            if (stateMachine.PatrolPath == null) 
            {
                stateMachine.SwitchState(new EnemyIdleState(stateMachine));
                return;
            }

            int index;
            // Check blackboard for key, set index if key is set
            if (stateMachine.Blackboard.ContainsKey(NextPatrolPointIndexKey)) 
            {
                index = stateMachine.Blackboard.GetValueAsInt(NextPatrolPointIndexKey);
            }

            else 
            {
                // The first time we enter a Patrol state, the index will not be set in the blackboard
                // So we get it from the PatrolPath's GetNearestIndex
                // We will also be resetting the index if the enemy goes into 'EnemyChasingState.cs'
                index = stateMachine.PatrolPath.GetNearestIndex(stateMachine.transform.position);
            }

            // Set our goal
            targetPatrolPoint = stateMachine.PatrolPath.GetWaypoint(index);
            PatrolPoint patrolPoint = stateMachine.PatrolPath.GetPatrolPoint(index);

            if (patrolPoint) // If the current index has a PatrolPoint attached, then we can adjust the settings from the default
            {
                movementSpeed = stateMachine.MovementSpeed * patrolPoint.SpeedModifier;
                acceptanceRadius = patrolPoint.AcceptanceRadius;
                dwellTime = patrolPoint.DwellTime;
            }

            else // if not, calculate the movementSpeed as a percentage of the stateMachine's movement speed
            {
                movementSpeed = stateMachine.MovementSpeed;
            }

            // Squaring the acceptanceRadius (to save calculation time when we use 'SqrMagnitude')
            acceptanceRadius *= acceptanceRadius;
            // next waypoint index setup
            stateMachine.Blackboard[NextPatrolPointIndexKey] = stateMachine.PatrolPath.GetNextIndex(index);
            // Since the waypoint won't move, set the destination here on the Agent
            stateMachine.Agent.SetDestination(targetPatrolPoint);
            // Set the animation
            stateMachine.Animator.CrossFadeInFixedTime(FreeLookBlendTreeHash, stateMachine.CrossFadeDuration);

        }

        public override void Tick(float deltaTime)
        {
            if (IsInChaseRange() && IsAggro()) // if you want to consider the optional aggregation, add "&& IsAggro()" to the if statement of this line
            {

                // if you want the vision system to work, uncomment the if statement below:

                // if (CanSeePlayer()) {
                // Clearing key to ensure that at the end of the battle, the enemy finds the nearest waypoint
                stateMachine.Blackboard.Remove(NextPatrolPointIndexKey);
                stateMachine.SwitchState(new EnemyChasingState(stateMachine));
                return;
                // }
            }

            if (IsInAcceptanceRange()) 
            {
                // Once we're close enough to the waypoint, we head to a Dwell state
                stateMachine.SwitchState(new EnemyDwellState(stateMachine, dwellTime));
                return;
            }

            // Chasing Code
            Vector3 lastPosition = stateMachine.transform.position;
            MoveToWayPoint(deltaTime);
            Vector3 deltaMovement = lastPosition - stateMachine.transform.position;
            float deltaMagnitude = deltaMovement.magnitude;
            float grossSpeed = deltaMagnitude / deltaTime;

            stateMachine.Animator.SetFloat(FreeLookSpeedHash, grossSpeed / stateMachine.MovementSpeed, stateMachine.AnimatorDampTime, deltaTime);
            if (deltaMagnitude > 0) 
            {
                FaceTarget(stateMachine.transform.position - deltaMovement, deltaTime);
            }
            else 
            {
                FaceTarget(targetPatrolPoint, deltaTime);
            }

        }

        private bool IsInAcceptanceRange() 
        {
            return (stateMachine.transform.position - targetPatrolPoint).sqrMagnitude < acceptanceRadius;
        }

        public override void Exit()
        {
            stateMachine.Agent.ResetPath();
            stateMachine.Agent.velocity = Vector3.zero;
        }

        void MoveToWayPoint(float deltaTime) 
        {
            Vector3 direction = stateMachine.Agent.desiredVelocity.normalized;
            Move(direction * movementSpeed, deltaTime);
            stateMachine.Agent.velocity = stateMachine.CharacterController.velocity;
            stateMachine.Agent.nextPosition = stateMachine.transform.position;
        }
    }
}

the stuff you learn here everyday is beyond what I ever thought I’d be learning, and I’m incredibly happy about that

What/Where is IsAggro()?

‘IsAggro()’ is something I developed with bixarrio to basically make aggrevating the enemy optional for the developer (me). Here it is:

// 'EnemyStateMachine.cs':

    [field: SerializeField] public bool IsAggro {get; private set;} = false;

    private void OnTakenHit(GameObject instigator) 
    {
        if (instigator.TryGetComponent(out Fighter fighter)) 
        {
            IsAggro = true;
        }
    }

I had plans to introduce ‘Aggrevate Others’ and deal with my aggroGroup down the line, but for now I’m just trying to fix the big bugs (basically get the features I used to love about the point-and-click system back over time)

Well… that would make much more sense if it was

if(IsInChaseRange() || IsAggro())

whilst it does solve the whole return to patrolling issue, this completely eliminates the purpose of the aggro function. Any other way to go around that?

and the same check goes in base state, dwelling state and idle state too…

The purpose of the other function is to make it so that the character attacks when attacked.
if(IsInChaseRange() && IsAggro()) == only chase when you’ve been hit and are in range.
if(IsInChaseRange() !! IsAggro()) == Chase if in range or if you hit me with that arrow even if you’re out of range.

well… that’s kind of what I’m trying to do. I don’t want some specific enemies chasing me down just because I’m nearby :sweat_smile: - only chase the player down if you’re attacked basically

but I also want the patrolling to work

Step away from that idea for a bit until you actually patrolling working the way it’s intended. All these extra tweaks are taking a toll making it

  • Not work the way you want
  • Hard to diagnose what’s wrong because I didnt’ even know about that IsAggro things

fair enough, I’ll try again with that later. For now patrolling works again, but there’s something I just noticed regarding that, and somehow it’s only for the ranger enemy

After he detects you the first time and then you go away and come back, he won’t react to your presence and try attacking you again until his dwelling time ends. In other words, he’ll decide to chase you ONLY WHEN HE’S DONE DWELLING (so now instead of patrolling being his next decision, it is to address your presence and attack you). Is that normal? (Again, this issue only exists for the ranger. The melee warrior doesn’t have that kind of problem)

Let’s take a look at your Dwell state… I’m going to place a small wager, though… I’ll tell you if I win after you post.

using UnityEngine;

namespace RPG.States.Enemies 
{
    public class EnemyDwellState : EnemyBaseState 
    {
        private const string NextPatrolPointIndexKey = "NextPatrolPointIndex";
        private float dwellTime;

        public EnemyDwellState(EnemyStateMachine stateMachine, float dwellTime) : base(stateMachine)
        {
            this.dwellTime = dwellTime;
        }

        public override void Enter()
        {
            // You can place an animation randomizer here, so enemies can randomly play any animations they want, if you want to
            stateMachine.Animator.CrossFadeInFixedTime(IdleHash, stateMachine.CrossFadeDuration);
        }

        public override void Tick(float deltaTime)
        {
            Move(deltaTime);
            if (IsInChaseRange()) // && IsAggro()) // if you want to consider the optional aggregation, add "&& IsAggro()" to the if statement of this line
            {

                // if you want the vision system to work, uncomment the if statement below:

                // if (CanSeePlayer()) { 
                // Clear the key, so that after leaving chase range, we get the nearest waypoint to where we end up
                stateMachine.Blackboard.Remove(NextPatrolPointIndexKey);
                stateMachine.SwitchState(new EnemyChasingState(stateMachine));
                return;
                // }
            }

            dwellTime -= deltaTime;
            if (dwellTime <= 0) 
            {
                stateMachine.SwitchState(new EnemyIdleState(stateMachine));
            }
        }

        public override void Exit() {}

    }

}

I’d be down to bet as well if it wasn’t for Ramadan that just started yesterday (the fasting month) :stuck_out_tongue:

No reason that should be waiting… my bet was that it was still considering && IsAggro()

That absolutely should have the chase as soon as the player is in range while dwelling.

I did fix it :slight_smile:, but I didn’t know whether to tell you that it was fixed or still proceed with pasting the code… so I pasted in the code

How do we plug in the ‘IsAggro()’ back in though, without causing some wild problems like this one?

he’s not fully fixed as of yet… run out of a fight, and he’s never ever patrolling again until he is killed and respawned

You’ll need a bool in the StateMachine IsHostile //Will always attack player in range

if((IsInChaseRange() && stateMachine.IsHostile) || IsAggro())

this becomes a severe problem if you hit him at least once, and that hit impacted his health (talk about being permanently stunned). If you run away while he’s chasing you, and didn’t deal any sort of damage, he’ll patrol with no issues at all…

Privacy & Terms