Merging the Third Person Controller Course with the RPG Course

Hi Brian, thanks for the CD Manager lesson.
Is there any chance to push the lecture’s commit?

Cheers :slight_smile:

Edit:
Thought I’m missing some instructions,
Just read the part that states that this is groundwork for the next lecture,
In that case no rush, I already added the scripts :pray:

The next lecture’s finalized code is written, and tested. Just have to write the actual lesson. That’s usually the hardest part for me. Believe it or not the actual code changes were quick, it’s explaining why they work and writing it so that it’s understandable to newbies and doesn’t get TLDR’d by intermediates and advanced students.

1 Like

I wonder what’s next… :slight_smile: - what’s the next lecture going to be about? (I’m all hyped for the ranged system because I want to add destructive gameObjects in my game. You know… everyone loves a little bit of explosion :slight_smile:, and a little bit of heavy camera vibration to match with it)

By the way, with throttling (the enemy, I plan to try and throttle the player as well and see how it feels) out of the way, this function in ‘Fighter.AttackBehaviour()’ complains whenever I try eliminate ‘timeBetweenAttacks’ in ‘WeaponConfig.cs’. Can I safely eliminate this?:

if (timeSinceLastAttack > currentWeaponConfig.timeBetweenAttacks && CheckForProjectileAndSufficientAmmo()) {

        // Triggering the Attack Event (and resetting the timer to next attack):
        TriggerAttack();
        timeSinceLastAttack = 0; // Resets timer to calculate how long we need to wait before replaying the attack animation

        }

this, and ‘timeBetweenAttacks’ in ‘WeaponConfig.cs’


Also, I tried to throttle the player, but that did NOT go well. Here’s what I tried doing:

  1. in ‘PlayerStateMachine.cs’, I added the following lines of code:
        // TEST (Variable Declaration):
        [field: SerializeField] public CooldownTokenManager CooldownTokenManager {get; private set;}

       // in 'OnValidate()':
       if (CooldownTokenManager == null) CooldownTokenManager = GetComponent<CooldownTokenManager>();

  1. in ‘PlayerAttackingState.cs’, I added the following:
// this line goes as an extra check in the FIRST 'if' condition in 'PlayerAttackingState.Tick()':
&& normalizedTime > currentAttack.Cooldown

// in 'Exit()', after the Root Motion deactivation:
            // TEST
            stateMachine.CooldownTokenManager.SetCooldown("Attack", currentAttack.Cooldown);

The player basically just freezes in time after the first swing is successfully done. Where have I gone wrong…?!

While you should be adding the cooldown in the Exit() of PlayerAttackingState, that’s really all we’re doing. If you review what we add to the EnemyAttackingState, you’ll find it’s only adding the cooldown in Exit(). There’s really no reason to add anything else to PlayerAttackingState.

Where we want to check for a cooldown is whenever we’re going to attack from outside the PlayerAttackingState. That would be within the events responding to the Attack button in PlayerTargetingState and PlayerFreelookState. In your case, I believe you’re only allowing attacks in the TargetingState, so you’ll simply want to check to see if the CooldownTokenManager has the cooldown and not attack if it does.

I’m sorry, I didn’t fully understand this part. Can you please elaborate further?

soo… just add this line in ‘PlayerAttackingState.Exit()’, right?:

    // TEST: Cooldown after an attack happened:
            stateMachine.CooldownTokenManager.SetCooldown("Attack", currentAttack.Cooldown);
        

I’m guessing we don’t need this one:

because by doing just that, he still freezes after the first swipe…

You’re correct, you don’t need it. In fact, if you have it, you’ll likely lock up your player unless the cooldown starts at less than 1. Normalized time is between 0 and 1, it’s the percentage that something is finished. But it has nothing whatsoever to do with cooldowns.

Within PlayerTargetingState(), we have an if state to see if we’re attacking:

            if (stateMachine.InputReader.IsAttacking)
            {
                stateMachine.SwitchState(new PlayerAttackingState(stateMachine, 0));
                return;
            }

You should also check (with an && in the if statement) to make sure there is no “Attack” cooldown in progress… something like !stateMachine.CooldownManager.HasCooldown("Attack)… you know, the same check we make in EnemyChasingState(). We don’t need to worry about the Move() stuff, in this case because we’re already calculating movement with the controller… you’re just adding not having a cooldown to the condition for entering Attack state.

ironically, even without it, my player still locks up. I keep some of my cooldown times below 1 second for the intense wars… Do I need it longer than 1 second to avoid locking up? How can we avoid locking up overall?

AND… Placing this code:

in the if statement mentioned in ‘PlayerTargetingState.Tick()’, results in this NRE:

NullReferenceException: Object reference not set to an instance of an object
RPG.States.Player.PlayerTargetingState.Tick (System.Single deltaTime) (at Assets/Project Backup/Scripts/State Machines/Player/PlayerTargetingState.cs:23)
RPG.States.StateMachine.Update () (at Assets/Project Backup/Scripts/State Machines/StateMachine.cs:18)

which is basically the line where we added the Cooldown checking condition

Edit: I forgot to add the ‘CooldownTokenManager’ back to the player and check for the cooldown token manager in ‘OnValidate()’. My bad… :sweat_smile:

albeit the cooldown only occurs when the attack combo is done, right? And the randomizer we had in one of the previous lessons ensures we don’t always reach the end?

Sometimes it’s just easier to wait until you figure out what weird thing you did and it’s all solved before I can get to it…

I have to assume it’s locking up because of the quote above… a shorter cooldown should just mean you can attack quicker.

Correct, the combos should bypass the cooldown or they will never actually happen.

I only had the randomizer in the Enemies, but yeah, that’s the idea.

yup, that was the problem… If one day I decide to unlock my attack out of combat mode though, will this system have any issues I need to be aware of? (when it comes to ranged attacks, this will be something to think of)

I’ll find where it was and try replicate it in my player and keep it commented out, JUST IN CASE :slight_smile:

You’ll just add the same check in PlayerFreelookState before entering PlayerAttackingState, just like you did in PlayerTargetingState. It really is that easy.

OK so let me get this straight, because this one was a whacko job. The condition must be met in the Attack State introduced in ‘tick()’, right? I introduced it in the attack switch in ‘HandleAttackButtonPressed()’ as well (I really need to change that to ‘HandleInteractButtonPressed()’ instead, and set that to “E”…), and it was as good as pointless, but it worked when I added the condition to the state switch in’Tick()’

I can delete the condition check from the switch that happens in ‘HandleAttackButtonPress’, right? Apart from that, what’s the next step on this roadmap? :stuck_out_tongue:

        void HandleAttackButtonPressed()
        {
            if (stateMachine.Targeter.HasTargets)
            {
                if (!stateMachine.CooldownTokenManager.HasCooldown("Attack"))
                {
                    stateMachine.SwitchState(new PlayerAttackingState(stateMachine, 0));
                }
                return;
            }

            if (stateMachine.PickupFinder.HasTargets)
            {
                InputReader_HandlePickupEvent();
                if (stateMachine.PickupFinder.CurrentTarget) return;
            }

            if (stateMachine.ConversantFinder.HasTargets)
            {
                InputReader_HandleDialogueEvent();
                if (stateMachine.ConversantFinder.CurrentTarget) return;
            }
            stateMachine.SwitchState(new PlayerAttackingState(stateMachine, 0));
        }

that didn’t work either… It’s all good though, as long as the one in ‘Tick()’ works. I don’t think they make a big difference :slight_smile:

and it still baffles me why we have two switches to the attack in ‘HandleAttackButtonPressed()’, and I’m guessing that’s why it’s not working (the switch ignores the check)…

Anyway, day 1 of asking Brian what the next topic will be :stuck_out_tongue:

https://community.gamedev.tv/t/more-states/242327/17?u=edc237

Brian already told us what the roadmap looks like. I believe that jumping is next.

My impatience got me to work on these solo… I’m probably trying ranged next, mainly because mine is a little more complex than the tutorials… :skull:

I just need help with my construction system above anything else first

Hey, just a heads up for lesson 38
anyone trying to get the TargetingBlendTree switch, your enemy will slide instead of walk forward if you move your player away from the enemy.

In EnemyChaseState.cs
I added

public readonly int TargetingForwardSpeedHash = Animator.StringToHash("TargetingForwardSpeed");

and switched from FreeLookSpeedHash to TargetingForwardSpeedHash in the tick method.
Now the enemies will move in targeting mode right from the beginning (and not run / target based on distance, which is the next step perhaps), but it does look better than the slide.

My heart skipped a HUGE beat reading this. I got incredibly excited… Development was on hold for a bit because of this :sweat_smile:!

oh I thought I was the one going crazy because his enemy was sliding as he was patrolling… :sweat_smile: - @Brian_Trotter please address this for us, since it turns out it’s not just my fault :smiley:

In my case at least, he slides when he’s patrolling, but walks like normal again when I attack him and get him into combat mode, and he starts pursuing me.

If it helps, here is my ‘EnemyPatrolState.cs’ script:

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

Well we didn’t really touch anything to do with Patrolling state in either of these lectures.

My patrol state is the same as it was. Little bit of sliding (import settings / speed mismatch), but not like the one in the error which I mentioned earlier, where the walking animation wasn’t playing at all and the enemy was moving in idle (same happens in the git project).

Privacy & Terms