Hero has twitching

Hello everyone, I have a small problem which I can not solve and wanted to ask for help.
The problem is that whenever I get just a little bit closer to my opponent, the protagonist starts to have strange twitches which looks quite strange. I would like to stop this but unfortunately I do not know how.

It seems as if the protagonist always wants to turn to the opponent even in further distance and is always interrupted. I don’t get any errors in the code? Could someone help me? Thanks in advance!

AIController

using System;  
using System.Collections;
using System.Collections.Generic;
using RPG.Combat;
using RPG.Core;
using RPG.Movement;
using UnityEngine;

namespace RPG.Control
{
        public class AIController : MonoBehaviour
    {
        [SerializeField] float chaseDistance = 5f;
        [SerializeField] float suspicionTime = 3f;
        [SerializeField] PatrolPath patrolPath;
        [SerializeField] float waypointTolerance = 1f;
        [SerializeField] float waypointDwellTime = 3f;

        Fighter fighter;
        Health health;
        Mover mover;
        GameObject player;

        Vector3 guardPosition;
        float timeSinceLastSawPlayer = Mathf.Infinity;
        float timeSinceArrivedAtWaypoint = Mathf.Infinity;
        int currentWaypointIndex = 0;

        private void Start() {
            fighter = GetComponent<Fighter>();
            health = GetComponent<Health>();
            mover = GetComponent<Mover>();
            player = GameObject.FindWithTag("Player");
            
            guardPosition = transform.position;
        }
        private void Update()
        {
            if (health.IsDead()) return;

            if (InAttackRangeOfPlayer() && fighter.CanAttack(player))
            {
                AttackBehaviour();
            }
            else if (timeSinceLastSawPlayer < suspicionTime)
            {
                SuspicionBehaviour();
            }
            else
            {
                PatrolBehaviour();
            }

            UpdateTimers();
        }

        private void UpdateTimers()
        {
            timeSinceLastSawPlayer += Time.deltaTime;
            timeSinceArrivedAtWaypoint += Time.deltaTime;
        }

        private void PatrolBehaviour()
        {
            Vector3 nextPosition = guardPosition;
            if (patrolPath != null) 
            {
                if (AtWaypoint())
                {
                    timeSinceArrivedAtWaypoint = 0;
                    CycleWaypoint();
                }
                nextPosition = GetCurrentWaypoint();
            }

            if (timeSinceArrivedAtWaypoint > waypointDwellTime)
            {
                mover.StartMoveAction(nextPosition);
            }
        }

        private bool AtWaypoint()
        {
            float distanceToWaypoint = Vector3.Distance(transform.position, GetCurrentWaypoint());
            return distanceToWaypoint < waypointTolerance;
        }
        private void CycleWaypoint()
        {
            currentWaypointIndex = patrolPath.GetNextIndex(currentWaypointIndex);
        }
        private Vector3 GetCurrentWaypoint()
        {
            return patrolPath.GetWaypoint(currentWaypointIndex);
        }
        private void SuspicionBehaviour()
        {
            GetComponent<AktionScheduler>().CancelCurrentAction();
        }

        private void AttackBehaviour()
        {
            timeSinceLastSawPlayer = 0;
            fighter.Attack(player);
        }

        private bool InAttackRangeOfPlayer()
        {
            float distanceToPlayer = Vector3.Distance(player.transform.position, transform.position);
            return distanceToPlayer < chaseDistance;
        }
        // Called by Unity
        private void OnDrawGizmosSelected() {
            Gizmos.color = Color.blue;
            Gizmos.DrawWireSphere(transform.position, chaseDistance);
        }

PatrolPath

using System.Collections;
using System.Collections.Generic;
using UnityEngine; 

namespace RPG.Control
{
    public class PatrolPath : MonoBehaviour
    {
        const float waypointGizmoRadius = 0.3f;
        private void OnDrawGizmos() {
            for (int i = 0; i < transform.childCount; i++)
            {
                int j = GetNextIndex(i);
                Gizmos.DrawSphere(GetWaypoint(i), waypointGizmoRadius);
                Gizmos.DrawLine(GetWaypoint(i), GetWaypoint(j));
            }
        }

        public int GetNextIndex(int i)
        {
            if (i + 1 == transform.childCount)
            {
                return 0;
            }
            return i + 1;
        }

        public Vector3 GetWaypoint(int i)
        {
            return transform.GetChild(i).position;
        }
    }
}

PlayerController

using System; 
using RPG.Movement;
using RPG.Combat;
using RPG.Core;
using UnityEngine;


namespace RPG.Control
{
    public class PlayerController : MonoBehaviour
    {
        Health health;

        private void Start() {
            health = GetComponent<Health>();
        }

        private void Update()
        {
            if (health.IsDead()) return;

            if (InteractWithCombat()) return;
            if (InteractWithMovement()) return;  
        }

        private bool InteractWithCombat()
        {
            RaycastHit[] hits = Physics.RaycastAll(GetMouseRay());
            foreach (RaycastHit hit in hits)
            {
                CombatTarget target = hit.transform.GetComponent<CombatTarget>();
                if (target == null) continue;

                if (!GetComponent<Fighter>().CanAttack(target.gameObject))
                {
                    continue;
                }

                if (Input.GetMouseButton(0))
                {
                    GetComponent<Fighter>().Attack(target.gameObject);
                }
                return true;
            }
            return false;
        }

        private bool InteractWithMovement()
        {
            RaycastHit hit;
            bool hasHit = Physics.Raycast(GetMouseRay(), out hit);
            if (hasHit)
            {
                if (Input.GetMouseButton(0))
                {
                    GetComponent<Mover>().StartMoveAction(hit.point);
                }
                return true;
            }
            return false;
        }

        private static Ray GetMouseRay()
        {
            return Camera.main.ScreenPointToRay(Input.mousePosition);
        }
    }
}


Mover

using System.Collections; 
using System.Collections.Generic;
using RPG.Core;

using UnityEngine; 
using UnityEngine.AI;

namespace RPG.Movement
{
    public class Mover : MonoBehaviour, IAction
    {
        [SerializeField] Transform target;

        NavMeshAgent navMeshAgent;
        Health health;

        private void Start() 
        {
            navMeshAgent = GetComponent<NavMeshAgent>();
            health = GetComponent<Health>();
        }

        void Update()
        {
            navMeshAgent.enabled = !health.IsDead();
            
            UpdateAnimator();
        }

        public void StartMoveAction(Vector3 destination)
        {
            GetComponent<AktionScheduler>().StartAction(this);
            MoveTo(destination);
        }

        public void MoveTo(Vector3 destination)
        {
            navMeshAgent.destination = destination;
            navMeshAgent.isStopped = false;
        }

        public void Cancel()
        {
            navMeshAgent.isStopped = true;
        }

        private void UpdateAnimator()
        {
            Vector3 velocity = navMeshAgent.velocity;
            Vector3 localVelocity = transform.InverseTransformDirection(velocity);
            float speed = localVelocity.z;
            GetComponent<Animator>().SetFloat("forwardSpeed", speed);
        }
    }
}

AktionScheduler

using UnityEngine; 

namespace RPG.Core 
{
    public class AktionScheduler : MonoBehaviour
{
    IAction currentAction;
    
    public void StartAction(IAction action)
    { 
        if (currentAction == action) return;
        if (currentAction != null)
        {
            currentAction.Cancel(); 
        }
        currentAction = action;
    }
     
    public void CancelCurrentAction()
    {
        StartAction(null);
    }
}
}

FollowCamera

using System.Collections; 
using System.Collections.Generic;
using UnityEngine;

namespace RPG.Core
{
    public class FollowCamera : MonoBehaviour 
    {
        [SerializeField] Transform target;
        
        void LateUpdate()
        {
            transform.position = target.position;
        }
    }

}

Health

using UnityEngine; 

namespace RPG.Core
{
    public class Health : MonoBehaviour
    {
        [SerializeField] float healthPoints = 100f;

        bool isDead = false;

        public bool IsDead()
        {
            return isDead;
        }

        public void TakeDamage(float damage)
        {
             healthPoints = Mathf.Max(healthPoints - damage, 0);
            if (healthPoints == 0)
            {
                Die();
            }
        }

        private void Die()
        {
            if (isDead) return;

            isDead = true;
            GetComponent<Animator>().SetTrigger("die");
            GetComponent<AktionScheduler>().CancelCurrentAction();
        }
    }
}

IAction

namespace RPG.Core  
{
    public interface IAction
    {
    void Cancel(); 
    }
}

RotatedCamera

using System.Collections; 
using System.Collections.Generic;
using UnityEngine;

namespace RPG.Core
{
    public class RotatedCamera : MonoBehaviour
{
    public Vector2 turn;

    // Update is called once per frame
    void Update()
    {
        turn.x += Input.GetAxis("Mouse X");
        transform.localRotation = Quaternion.Euler(turn.y, turn.x, 0);
    }
}

}


CombatTarget

using System.Collections; 
using System.Collections.Generic;
using RPG.Core;
using UnityEngine;


namespace RPG.Combat
{
    [RequireComponent(typeof(Health))]
    public class CombatTarget : MonoBehaviour
    {

    }

}
using UnityEngine;  
using RPG.Movement;
using RPG.Core;

namespace RPG.Combat
{
    public class Fighter : MonoBehaviour, IAction 
    {

        [SerializeField] float weaponRange = 2f;
        [SerializeField] float timeBetweenAttacks = 1f;
        [SerializeField] float weaponDamage = 10f;

        Health target;
        float timeSinceLastAttack = Mathf.Infinity;

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

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

            if (!GetIsInRange())
            {
                GetComponent<Mover>().MoveTo(target.transform.position);
            }
            else
            {
                GetComponent<Mover>().Cancel();
                AttackBehavior();
            }
        }

        private void AttackBehavior()
        {
            transform.LookAt(target.transform);
            if (timeSinceLastAttack > timeBetweenAttacks)
            {   // this will trigger the hit event.
                
                TriggerAttack();
                timeSinceLastAttack = 0;
            }
        }

        public void TriggerAttack()
        {
            GetComponent<Animator>().ResetTrigger("attack");
            GetComponent<Animator>().SetTrigger("attack");
        }
        // Animotin Event
        void Hit()
        { 
            if(target ==  null) 
                { 
                    return; 
                }
            target.TakeDamage(weaponDamage);
        }

        private bool GetIsInRange()
        {
            return Vector3.Distance(transform.position, target.transform.position) < weaponRange;
        }

        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<AktionScheduler>().StartAction(this);
            target = combatTarget.GetComponent<Health>();
        }

        public void Cancel()
        {
            
            StopAttack();
            target = null;
        }

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


You might try raising the weaponRange a small amount. Depending on the agent sizes, it’s not always possible to get too close to another character (the NavMeshAgents actually try very hard not to be in the same place at the same time). The agent will still try it’s darndest to get you there, though, and this can cause this emergent behaviour.

Bear in mind that even though you have the distance set to 2 in the code, you may have overriden it in the inspector and it whatever number was entered will override the serialized value in code.

Thank you I will try this

Unfortunately, it didn’t really solve the problem. Do you also have the problem that your protagonist now and then tries to turn towards the opponent and thus the character turns strangely?

I didn’t notice the video on the first look at this, and am not sure what I’m seeing… You can’t really tell what state the character is in, or if a clicking is happening, etc…

One thing to try first… it’s possible that this is a NavMeshAgent issue…
Let’s try a minor tweak in Mover.Update()

void Update()
{
    if(health.IsDead())
    {
         navMeshAgent.isStopped=true;
         return;
     }
    if (navMeshAgent.isStopped == false && navMeshAgent.remainingDistance < 1) navMeshAgent.isStopped = true;
    UpdateAnimator();

This can prevent some emergent issues. Make sure with things like PatrolPath and Fighter, that the threshold distance (“In Range”) is greater than 1.

If this doesn’t do the trick, then let’s add some Debugs to make sure our Actions are wired up correctly.

In PlayerController.InteractWithMovement()

if(Input.GetMouseButton(0)
{
    Debug.Log($"InteractWithCombat = Attack({target}");
    GetComponent<Fighter>().Attack(target.gameObject);
}

In InteractWithMovement

if(Input.GetMouseButton(0)
{
    Debug.Log($"InteractWithMovement - StartMoveAction({hit.point})");
    GetComponent<Mover>().StartMoveAction(hit.point);
}

In ActionScheduler.cs:

    public void StartAction(IAction action)
    { 
        if (currentAction == action) return;
        if (currentAction != null)
        {
            currentAction.Cancel(); 
        }
        currentAction = action;
        Debug.Log($"ActionScheduler - Action changed to {currentAction}");
    }

When using the Debugs, turn off Collapse Messages in the Console, and be sure the Console is open and visible. What you’re looking to see is that the character is entering the appropriate states at the appropriate times.

1 Like

now works! Thank you.

I now see that there is no more walk state only run can that have been the problem?

Screenshot_20230114_203520

Would it be possible in the update void mover to set 2 conditions? once that when the mouse has desired distance from the character to walk and when it is still far away to run?

In the course, walking vs. running is a matter of setting the navMeshAgent.speed, which we handle in a later lecture.

Once you get to the lecture Tweak Enemy Patrol, you’ll have variable speed available. At this point, when you call StartMoveAction in PlayerController, you can check the distance between transform.position and the hit.point and set the speed appropriately.

thank you very much

This topic was automatically closed 24 hours after the last reply. New replies are no longer allowed.

Privacy & Terms