Player "Jittering" issue... Still an issue for me

I saw this issue in the comments here
" Player “jittering” when shooting moving enemies " from Nov 21/2021
Under the “Pickup Respawning” video section in the “Simple Weapons” sub-section of the “Unity RPG Core Combat Creator: Learn Intermediate C# Coding” course.

In this comment, I saw the user was having the same issue I was having. The player will “jitter and/or stutter step” while shooting the bow at an enemy that is walking away from them at the bow weapons max range. The video in this comment shows the exact issue in action.

Their post doesn’t state what script they were changing their code in, but I think it was in the Fighter.cs script. And I had to edit the code to get it to at least run, but now I am receiving an error I don’t know how to fix.


I can no longer target and attack enemies and they can’t attack the player either, because of this error.

The other user was able to fix this issue and attached a snippet of the code in their post to help others who had the same issue, but their solution doesn’t seem to be working for me. I’m going to attach the whole script here as that might be better to help everyone.



Can anyone help resolve the Script Error Issue I’m getting?
Thank you so much everyone. :slight_smile:

The Update method in Unity doesn’t take parameters which is why you’re getting that error message. It’s been awhile but yes I believe it was the Fighter.cs script that I was having troubles with.

A quick fix is to do something like GetComponent<Animator>() wherever you have animator referenced in your Update method. Otherwise I can’t remember the exact workflow that gamedev.tv takes for stopping an attack, but it’s something to do with the IAction interface and the Cancel method I believe.

You can try taking a look through their GitHub repo and comparing your code https://github.com/UnityRPGv2/RPG. It’s sometimes faster than re-watching the videos.

This is my code for Fighter.cs

using UnityEngine;
using RPG.Movement;
using RPG.Core;
using RPG.Saving;
using RPG.Attributes;
using RPG.Stats;
using System.Collections.Generic;
using GameDevTV.Utils;
using System;

namespace RPG.Combat
{
    public class Fighter : MonoBehaviour, IAction, ISaveable, IModifierProvider
    {
        [SerializeField] float timeBetweenAttacks = 1f;
        [SerializeField] Transform rightHandTransform = null;
        [SerializeField] Transform leftHandTransform = null;
        [SerializeField] WeaponConfig defaultWeapon = null;

        Health target;
        float timeSinceLastAttack = Mathf.Infinity;
        WeaponConfig currentWeaponConfig;
        LazyValue<Weapon> currentWeapon;

        private void Awake()
        {
            currentWeaponConfig = defaultWeapon;
            currentWeapon = new LazyValue<Weapon>(SetupDefaultWeapon);
        }

        private Weapon SetupDefaultWeapon()
        {
            return AttachWeapon(defaultWeapon);
        }

        private void Start()
        {
            currentWeapon.ForceInit();
        }

        private void Update()
        {
            timeSinceLastAttack += Time.deltaTime;

            if (target == null) return;
            if (target.IsDead()) return;

            if (!GetIsInRange(target.transform))
            {
                GetComponent<Mover>().MoveTo(target.transform.position, 1f);
            }
            else
            {
                GetComponent<Mover>().Cancel();
                AttackBehaviour();
            }
        }

        public void EquipWeapon(WeaponConfig weapon)
        {
            currentWeaponConfig = weapon;
            currentWeapon.value = AttachWeapon(weapon);
        }

        private Weapon AttachWeapon(WeaponConfig weapon)
        {
            Animator animator = GetComponent<Animator>();
            return weapon.Spawn(rightHandTransform, leftHandTransform, animator);
        }

        public Health GetTarget()
        {
            return target;
        }

        private void AttackBehaviour()
        {
            transform.LookAt(target.transform);
            if (timeSinceLastAttack > timeBetweenAttacks)
            {
                // This will trigger the Hit() event.
                TriggerAttack();
                timeSinceLastAttack = 0;
            }
        }

        private void TriggerAttack()
        {
            GetComponent<Animator>().ResetTrigger("stopAttack");
            GetComponent<Animator>().SetTrigger("attack");
        }

        // Animation Event
        void Hit()
        {
            if (target == null) { return; }

            float damage = GetComponent<BaseStats>().GetStat(Stat.Damage);

            if (currentWeapon.value != null)
            {
                currentWeapon.value.OnHit();
            }

            if (currentWeaponConfig.HasProjectile())
            {
                currentWeaponConfig.LaunchProjectile(rightHandTransform, leftHandTransform, target, gameObject, damage);
            }
            else
            {
                target.TakeDamage(gameObject, damage);
            }
        }

        void Shoot()
        {
            Hit();
        }

        private bool GetIsInRange(Transform targetTransform)
        {
            return Vector3.Distance(transform.position, targetTransform.position) < currentWeaponConfig.GetRange();
        }

        public bool CanAttack(GameObject combatTarget)
        {
            if (combatTarget == null) { return false; }
            if (!GetComponent<Mover>().CanMoveTo(combatTarget.transform.position) && !GetIsInRange(combatTarget.transform)) { return false; }
            Health targetToTest = combatTarget.GetComponent<Health>();
            return targetToTest != null && !targetToTest.IsDead();
        }

        public void Attack(GameObject combatTarget)
        {
            GetComponent<ActionScheduler>().StartAction(this);
            target = combatTarget.GetComponent<Health>();
        }

        public void Cancel()
        {
            StopAttack();
            target = null;
            GetComponent<Mover>().Cancel();
        }

        private void StopAttack()
        {
            GetComponent<Animator>().ResetTrigger("attack");
            GetComponent<Animator>().SetTrigger("stopAttack");
        }

        public IEnumerable<float> GetAdditiveModifiers(Stat stat)
        {
            if (stat == Stat.Damage)
            {
                yield return currentWeaponConfig.GetDamage();
            }
        }

        public IEnumerable<float> GetPercentageModifiers(Stat stat)
        {
            if (stat == Stat.Damage)
            {
                yield return currentWeaponConfig.GetPercentageBonus();
            }
        }

        public object CaptureState()
        {
            return currentWeaponConfig.name;
        }

        public void RestoreState(object state)
        {
            string weaponName = (string)state;
            WeaponConfig weapon = UnityEngine.Resources.Load<WeaponConfig>(weaponName);
            EquipWeapon(weapon);
        }
    }
}

Kp4ws has the right of it.
Remove the parameter from Update (it wouldn’t help you anyways since it’s not initialized).
Instead, in the if(!GetIsInRange()) code block before the if statement, add this line:

Animator animator = GetComponent<Animator>();

Otherwise, your solution should work.

Good Day to you Brian (and Kp4ws):

Adding that line of code above the "if (!GetIsInRange()) did remove the error I was getting in Visual Studio.


But now the player and the enemies run towards each other and then just float around trying to occupy the same spot on the map and not fight. So something still isn’t quite right.

I’m going to try and change my code to what Kp4ws has, and see if that works.
Thank you to @Kp4ws for posting your code to help me out! :smiley: :+1:
And after adding that different code (only difference was the !GetIsInRange line, adding the (target.transform) argument


But as you can see it is giving a new error.

Telling me (I think… as I’m still just a beginner learning Unity) that I shouldn’t have target.transform in the definition of the method. And if I remove that… I’m back to where I started. hehe.
I love a good puzzle. I just wish I knew more about this.

Can you think of anything else to try? Or did I do something wrong in the code?

Your method already contains the target.transform in it, so there’s no need for it as a parameter. You can just remove the parameter from the GetIsInRange();

I just looked at the Code for the course on GitHub with the link Kp4ws had, and saw that further down under the GetIsInRange call there was the call I was missing.


Added that to my Fighter.cs script and the error went away. :slight_smile:
Tested again and the player still slides, but not as bad (no "jittering anyway).
Going to see if I can record a video to show you. So still not fixed 100%

Edit: I can’t seem to post a video. Says only .gif, jpeg, png

2nd Edit: The problem is still the same. The first test was done on a hill, where the player stutters quite noticeably up hill. The second test I conducted after getting help was done with an enemy that was closer to the players start position on a flat plain, and thus seemed smoother. Sorry for the misleading information.

3rd Edit: Got videos working.
Normal Slide / Jitter

Hill Slide / Jitter

I just realized that it’s still jittering for me too. I’m pretty at one point I had it fixed but I don’t remember what I did. I believe I decided to leave it this way and figure it out later if it happens again in my own game.

You can try adding a condition that checks if the target is moving. If it is, then don’t allow player to attack. This should work but I’m not sure if it’s the best approach. Although this probably wouldn’t work so well if the player is close to the enemy, since the player would just wait until they’ve stopped haha.

Another idea is to add an additional attack cooldown, which causes the player to wait longer before moving after attacking.

I think what would fix this issue would be that if the player was attacking an enemy, and they (the enemy) would turn and attack the player.

That would solve the issue for melee combat, because then the enemy would run to be in inside the bows range to fight the player.

That would also solve the issue with ranged combat as well, because the enemy (archer, mage, ect) would engage the player and the player would just get inside there own bow range and fire back.

The only scenario I can see this not working is if the enemy tries to flee from combat. An example might be the player casts “fear” on the enemy and the enemy tries to disengage and run away. The player would pursue… and we have the jitter / stutter situation again.

I don’t know if this will be covered later in the course or maybe it will be covered in one of the other courses (??), but this seems like the best option to fix this issue. At least it would work a majority of the time I feel. But I just started working with Unity and Visual Studio so I have no idea how to code something like this yet. I’m also waiting to see if @Brian_Trotter has any thoughts or insight on this.

Thank you so much for checking @Kp4ws! :grinning: :+1:

1 Like

This is the number one cause of Jittering.
I thought today about how this is handled in other games. Over 30 years of RPGs, I’ve seen the following behaviors:

  1. When a player begins the attack animation, he follows through to the end of the attack, whiffing if the target is no longer in range.
  2. When a player begins the attack animation, the enemy is immediately notified and aggros regardless of what they are doing.
  3. When a player releases a projectile, the enemy is immediately notified and aggros.

There are combinations of effects like this.

Here’s how to accomplish each of these solutions:

  1. You Gotta Follow Through on Those Swings!. This requires you to know the current state of the animator. You could do this with a tag on the state. In Fighter’s check, add an && animator.GetCurrentAnimatorStateInfo(0).IsTag("Attack") at the end of the check to see if the player should be walking. Additionally, in Hit(), you would need to do a check to ensure that the target A) exists, and B) is within attack range before applying damage or firing a projectile.
  2. Instant Aggro, just add water: As soon as the player begins the attack animation (SetTrigger(“Attack”), inform the target that it is under attack… Since you don’t want cross dependencies, this would be best handled through an interface… something like IGetAggro().Aggrevate(). The AIController would have an IGetAggro() interface, and there should already be a method Aggrevate() which simply sets timeSinceLastSeen to 0. Boom, instant aggro. Of course, this makes it a bit harder to take advantage of ranged attacks just outside of the enemy’s visual range.
  3. You shooting at me, Bro? The same interface in 2 is used, the IGetAggro with the Aggrevate, but rather than calling it in AttackBehavior when the character begins the attack, perform it in Hit(). This is the least effective anti-jittering maneuver, but it should cut down on some of it.
1 Like

Hey Brian. I would like to work through option one, but I don’t know what I should be doing. I might need a bit more help, but I’ll try to figure this out with some guidance from you.

I think I want to add your
In Fighter’s check, add an && animator.GetCurrentAnimatorStateInfo(0).IsTag("Attack") at the end of the check to see if the player should be walking.
in the “private bool GetIsInRange(Transform targetTranform)” section [ just past the 3/4 mark down through the code ] as that where we determine “if” we can attack. Right?

And then add your
Additionally, in Hit(), you would need to do a check to ensure that the target A) exists, and B) is within attack range before applying damage or firing a projectile.
in the “public void Attack(GameObject combatTarget)” section? As this is where we start our attack with the ActionScheduler [ two sections down from the GetIsInRange section above ]

using UnityEngine;
using RPG.Movement;
using RPG.Core;
using RPG.Saving;
using RPG.Attributes;

namespace RPG.Combat
{
    public class Fighter : MonoBehaviour, IAction, ISaveable
    {
        [SerializeField] float timeBetweenAttacks = 1f;
        [SerializeField] Transform rightHandTransform = null;
        [SerializeField] Transform leftHandTransform = null;
        [SerializeField] Weapon defaultWeapon = null;

        Health target;
        float timeSinceLastAttack = Mathf.Infinity;
        Weapon currentWeapon = null;

        private void Start()
        {
            if (currentWeapon == null)
            {
                EquipWeapon(defaultWeapon);
            }
        }

        private void Update()
        {
            timeSinceLastAttack += Time.deltaTime;
            
            if (target == null) return;
            if (target.IsDead()) return;

            // testing this fix code from the comments
            if (!GetIsInRange(target.transform))
            {
                GetComponent<Mover>().MoveTo(target.transform.position, 1f);
            }
            else
            {
                GetComponent<Mover>().Cancel();
                AttackBehaviour();
            }
            
            // This is what the code in this section was before testing this fix
            //   if (!GetIsInRange())
            //   {
            //       GetComponent<Mover>().MoveTo(target.transform.position, 1f);
            //   }
            //   else
            //   {
            //       GetComponent<Mover>().Cancel();
            //       AttackBehaviour();
            //   }
        }
        public void EquipWeapon(Weapon weapon)
        {
            currentWeapon = weapon;
            Animator animator = GetComponent<Animator>();
            weapon.Spawn(rightHandTransform, leftHandTransform, animator);
        }

        private void AttackBehaviour()
        {
            transform.LookAt(target.transform);
            if (timeSinceLastAttack > timeBetweenAttacks)
            {
                // This will trigger the Hit() Event
                TriggerAttack();
                timeSinceLastAttack = 0;
            }
        }

        private void TriggerAttack()
        {
            GetComponent<Animator>().ResetTrigger("stopAttack");
            GetComponent<Animator>().SetTrigger("attack");
        }

        // Animation Event
        void Hit()
        {
            if(target == null) { return; }

            if (currentWeapon.HasProjectile())
            {
                currentWeapon.LaunchProjectile(rightHandTransform, leftHandTransform, target);
            }
            else
            {
                target.TakeDamage(currentWeapon.GetDamage());
            }
        }

        void Shoot()
        {
            Hit();
        }

        private bool GetIsInRange(Transform targetTransform)
        {
            return Vector3.Distance(transform.position, target.transform.position) < currentWeapon.GetRange();
        }

        public bool CanAttack(GameObject combatTarget)
        {
            if (combatTarget == null) { return false; }
            Health targetToTest = combatTarget.GetComponent<Health>();
            return targetToTest != null && !targetToTest.IsDead();
        }
        public void Attack(GameObject combatTarget)
        {
            GetComponent<ActionScheduler>().StartAction(this);
            target = combatTarget.GetComponent<Health>();
        }

        public void Cancel()
        {
            StopAttack();
            target = null;
            GetComponent<Mover>().Cancel();
        }

        private void StopAttack()
        {
            GetComponent<Animator>().ResetTrigger("attack");
            GetComponent<Animator>().SetTrigger("stopAttack");
        }

        public object CaptureState()
        {
            return currentWeapon.name;
        }

        public void RestoreState(object state)
        {
            string weaponName = (string)state;
            Weapon weapon = Resources.Load<Weapon>(weaponName);
            EquipWeapon(weapon);
        }
    }
}
            if (!GetIsInRange(target.transform))
            {
                GetComponent<Mover>().MoveTo(target.transform.position, 1f);
            }
            else if(!animator.GetCurrentAnimatorStateInfo(0).IsTag("Attack"))
            {
                GetComponent<Mover>().Cancel();
                AttackBehaviour();
            }
        void Hit()
        {
            if(target == null) { return; }
            if(!GetIsInRange(target.transform)) return; <---
            if (currentWeapon.HasProjectile())
            {
                currentWeapon.LaunchProjectile(rightHandTransform, leftHandTransform, target);
            }
            else
            {
                target.TakeDamage(currentWeapon.GetDamage());
            }
        }

Good Day to you Brian:

Copied and pasted in your code, but I get an ‘animator’ does not exist in the current context error.



Tried a few of the suggestions from visual studio, but can’t seem to fix it.

Replace animator with GetComponent<Animator>()

else if(!GetComponent<Animator>().GetCurrentAnimatorStateInfo(0).IsTag("Attack"))

Good Day to you Brian.

The error is fixed, but the enemies still stand there and take damage without attacking. Which in turn still causes the jittering/stuttering when they are walking away. Do we maybe need to add something in the AI Controller script which tells the enemy if they are being attacked… to then attack? I’m thinking that might be a good place as in there we are telling the enemy to attack the player → then Suspicion → then back to Patrol. But this event only triggers when the player is standing in “range” (circle) of the enemy not if the enemy is “being attacked” by the player.

I have included the two areas we changed in our Fighter.cs script so you can see that everything is in there.


Here is the code from the AI Controller script where it says InAttackRangeOfPlayer and doesn’t have the “If I’m being attacked by the player”


and the Attack Player section, and the In Attack Range Of Player section

A bit later in the course, we’re going to be introducing Aggro… The idea is when an enemy is attacked, he tells his close friends “Hey, I’ve been attacked”, and the enemies within a certain distance start chasing the Player.

That particular mechanism is sort of what we want here…

Now we don’t want the enemy attacking the moment we click on them, but we also don’t want them wandering off after we’ve started swinging, so the ideal place to “aggrevate” the enemy is in the Fighter’s AttackBehavior… The problem we have is that we already have the AI Controller dependent on the Fighter, so we really don’t want to make the Fighter dependent on the AI Controller. But, we do have a reference to the target, and we can take advantage of that with an interface

public interface IAggrevate
{
     void Aggrevate();
}

I’m using this because later in the course, we’re going to have a method Aggrevate() on the AIController, which is simply going to reset a timer to zero which is the time since aggrevated. For now, let’s implement the IAggrevate interface on the AI Controller and give it this method:

public void Aggrevate()
{
    timeSinceLastSawPlayer=0;
}

Now in Fighter AttackBehavior, in the same code block that we SetTrigger(“attack”) on the animator, add this little block of code:

IAggrevate aggrevatable = target.GetComponent<IAggrevate>();
if(aggrevatable!=null)
{
    aggrevatable.Aggrevate();
}

This will fit seamlessly into the Aggrevate() we use later in the course, just change this Aggrevate() method to match the one in Sam’s code. What we’re doing for now is just getting the enemy to move to the last known position of the player.

Good Day to you Brian.

Ok no worries. I will leave this as still an issue on my end and will continue through the course. :slight_smile: Looking forward to learning more as we go.

Thank you so much Brian for all your help and guidance. :slight_smile: :+1:
Cheers.