Merging the Third Person Controller Course with the RPG Course

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;
        }

    }
    
}

I need to remember to go back to this bugā€¦

and before I forget, one last question regarding the respawning. What does deactivating and reactivating the character controller have to do with helping the chances of properly respawning?

        public override void Exit()
        {
            if(stateMachine.agent.enabled)
            {
                stateMachine.Agent.ResetPath();
                stateMachine.Agent.velocity = Vector3.zero;
            }
        }
stateMachine.enabled=true;
stateMachine.Agent.SetDestination(targetPatrolPoint);

is that replacement or supplement code?

I am confused, though, as to why the agent could be disabled between it working in Tick and being disabled in Exit for attackingā€¦

In Enter, find the line that sets the destination and enable the Agent (that should be stateMachine.Agent.enabled=true;, my bad)

the same errors are still showing upā€¦ (again though, I want to stress that they donā€™t hurt the game in any way shape or form, apart from the fact that the enemy attacks are still at a rapid pace and need breaks between them)

how about this question?

While the CharacterController is active, it rejects changes to the transform outright, just like the NavMeshAgent does. In fact, thatā€™s one of the reasons that the NavMeshAgent and the CharacterController donā€™t get along well and we have to manually call CharacterController.Move based on the Agentā€™s desiredVelocity.

OK now I see why that bug was thereā€¦ at first I thought I planted something somewhere and forgot to take it out, and when I deleted all sorts of movement when respawning and got my player to be where he was, I was even more shockedā€¦

basically the rejection was because of the controller behind the scenes, got it


Another question on the fly, would deleting the mesh colliders on my weapons or shields hurt my attacks in any way? I deleted them last night because they were messing with Malbersā€™ animals (collider collisions), as in the colliders had the animals move in weird ways. They donā€™t seem to be doing so badly as we speak, but I still have to ask


any other ideas for this? Thatā€™s my script right now btw:

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);
            // Enabling the NavMeshAgent
            stateMachine.enabled = true;
            // 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()
        {
            if (stateMachine.Agent.enabled) {            
            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;
        }

    }

}

speaking of this bug, I think Iā€™ll be the black sheep here and eliminate the ā€˜Attackā€™ value instead. I want my attacks to be reliant on the weapon rather than on the animation :slight_smile: - because of a few reasons:

  1. My animations are more limited than my weapons
  2. Upgrading weapons will make a whole lot more sense this way

I wonā€™t delete it, but I will keep it as zero wherever I donā€™t want to apply it

So this is what I have for the initial damage calculation in TryHit()

                    float damage = baseStats.GetStat(Stat.Damage);
                    damage *= currentAttack.DamageModifier;

So for this setup, the weapon damage is included in the GetStat(Stat.Damage) because the GetAdditiveModifiers returns the weapon damage.
Then that damage is multiplied by the attackā€™s modifierā€¦ so that some attacks can do more or less damage, a modifier of 1 is the same as the attack, .5f is 50% of the damage, 2f is double damage, etc.

You shouldnā€™t be hitting twice, only one TakeDamage should be called.

fair enough, soā€¦ Iā€™ll keep those I donā€™t want as zero I guess :slight_smile:

0 * 1 =ā€¦ :stuck_out_tongue:
So that leaves the question of why youā€™re hitting twiceā€¦ Do you have multiple TryHit animation events on the animation? Is there still a Hit animation event in your animation? Does your TryHit have multiple damage calls?

nope, weā€™re hitting twice because thereā€™s a ā€˜WeaponDamageā€™ and an ā€˜AttackForceā€™ both exerting forceā€¦ I like the weapon damage because of the easier name and that itā€™s directly accessible from when we are creating the weapon, but attack force is there as well, and I couldnā€™t be more baffled down the line :sweat_smile:

Anyway, Iā€™m still trying to figure out the sudden character generator bug before we get back into adding new stuff :slight_smile:

Force is what flows through all living beings, and can be characterized as light or dar-- erā€¦ sorry, wrong movie.

Damage is applied to health and doesā€¦ .damage
Force is applied to ForceReciever and should doā€¦ no damageā€¦

you really love classic movies, donā€™t you? May the 4th be with you :stuck_out_tongue:

OK so to recall before I go back to my annoying bug (which I am really clueless of how to solve, but Iā€™m tryingā€¦):

  • Attack Force is the force applied to the player, when they hit someone
  • Hit force is the reaction force to push them back

ā€¦ Right?

Thatā€™s correct

good day to you too kind sir :laughing:

Fair enough, for now Iā€™ll let it be. Still trying to figure out what went wrong with my Synty modelsā€¦ :sweat_smile: - link

did I miss out on something with the Impact statesā€¦? When both me and my enemy try to return to idle state after we received a hit, we both literally freeze pretty much foreverā€¦ As if the ā€˜HandleForceCompleted()ā€™ function never unsubscribed to begin withā€¦ (and yes, I made sure it was in both ā€˜Enterā€™ and ā€˜Exitā€™ states of both the Impact States)

In ā€˜EnemyImpactState.csā€™ (SPOILER), this script:

        using UnityEngine;

namespace RPG.States.Enemies {

    public class EnemyImpactState : EnemyBaseState
    {
        private readonly int ImpactHash = Animator.StringToHash("Impact");
        
        public EnemyImpactState(EnemyStateMachine stateMachine) : base(stateMachine) {}

        public override void Enter()
        {
            stateMachine.Animator.CrossFadeInFixedTime(ImpactHash, stateMachine.CrossFadeDuration);
            stateMachine.ForceReceiver.OnForceCompleted += HandleOnForceCompleted;
        }

        public override void Tick(float deltaTime)
        {
            Move(deltaTime);
        }

        public override void Exit()
        {
            stateMachine.ForceReceiver.OnForceCompleted -= HandleOnForceCompleted;
        }

        private void HandleOnForceCompleted() 
        {
            stateMachine.SwitchState(new EnemyIdleState(stateMachine));
        }
    }
}

and my ā€˜PlayerImpactState.csā€™ (ANOTHER SPOILER):

using UnityEngine;

namespace RPG.States.Player {

    public class PlayerImpactState : PlayerBaseState
    {
        private readonly int ImpactHash = Animator.StringToHash("Impact");
        
        public PlayerImpactState(PlayerStateMachine stateMachine) : base(stateMachine) {}

        public override void Enter()
        {
            stateMachine.Animator.CrossFadeInFixedTime(ImpactHash, stateMachine.CrossFadeDuration);
            stateMachine.ForceReceiver.OnForceCompleted += HandleOnForceCompleted;
        }

        public override void Tick(float deltaTime)
        {
            Move(deltaTime);
        }

        public override void Exit()
        {
            stateMachine.ForceReceiver.OnForceCompleted -= HandleOnForceCompleted;
        }

        private void HandleOnForceCompleted() 
        {
            SetLocomotionState();
        }

    }

}

Placing debugs around seems to work, butā€¦ they both still freeze. I went through the tutorial twice so far, I still canā€™t find where I went wrong (apart from placing ā€˜HandleForceAppliedā€™ in the StateMachine script (because the ā€˜Start()ā€™ function needs that), instead of the ā€˜ImpactStateā€™ scriptsā€¦

and I also have to mention that if we donā€™t give the ā€˜OnForceAppliedā€™ event in ā€˜ForceReceiver.csā€™ a type of Vector3, we will get errors, so this is how itā€™s written (since the force type using it in ā€˜AddForceā€™ is of type Vector3):

public event System.Action <Vector3> OnForceApplied;

without doing that, ā€˜Invokeā€™ wonā€™t take in the force Input :slight_smile: (the stuff bixarrio and you teach meā€¦ :laughing:)

It sounds like OnForceCompleted isnā€™t getting calledā€¦
Letā€™s take a look at your ForceReceiver.cs scriptā€¦

using UnityEngine;

namespace RPG.Movement
{
    public class ForceReceiver : MonoBehaviour
    {
        [SerializeField] private float drag = 0.3f;
        [SerializeField] private float minimumImpactVelocity = 0.1f;
        
        private CharacterController controller;
        private UnityEngine.AI.NavMeshAgent agent;

        public event System.Action <Vector3> OnForceApplied;
        public event System.Action OnForceCompleted;

        bool forceActive;

        private void Awake()
        {
            controller = GetComponent<CharacterController>();
            agent = GetComponent<UnityEngine.AI.NavMeshAgent>();
        }

        private float verticalVelocity;

        private Vector3 impact;
        private Vector3 dampingVelocity;

        public Vector3 Movement => impact + Vector3.up * verticalVelocity;

        private void Update()
        {
            if (verticalVelocity < 0f && controller.isGrounded)
            {
                verticalVelocity = Physics.gravity.y * Time.deltaTime;
            }
            else
            {
                verticalVelocity += Physics.gravity.y * Time.deltaTime;
            }
            
            impact = Vector3.SmoothDamp(impact, Vector3.zero, ref dampingVelocity, drag);

            // if the squared magnitude of the impact is below the 'minimumImpactVelocity',
            // reset the impact and enable the agent (i.e: he took the impact, now get back to normal)
            if (forceActive) 
            {
                if (impact.sqrMagnitude < minimumImpactVelocity) 
                {
                    impact = Vector3.zero;
                    OnForceCompleted?.Invoke();
                    forceActive = false;
                    if (agent) agent.enabled = false;
                }
            }
        }

        public void AddForce(Vector3 force, bool triggerKnockbackEvent = false)
        {
            impact += force;
            if (agent) agent.enabled = false;
            if (triggerKnockbackEvent) OnForceApplied?.Invoke(force);
        }
    }
}

Privacy & Terms