NavMeshAgent obstacle avoidance whilst Patrolling

OK so… I’m trying to fix this bug, by introducing a boolean that gets triggered when the player mounts or dismounts an animal, and I can use that to classify who gets triggered to quit using their patrol paths, and who does not (this whole thing is a big surprise in and of itself).

To do that, I created this script and placed it on each animal individually:

using UnityEngine;

namespace RPG.MalbersAccess 
{
public class UniqueAnimalProperties : MonoBehaviour
{
    [Tooltip("Is this Animal Dead?")]
    public bool isThisAnimalDead = false;
    [Tooltip("Is this Animal Mounted?")]
    public bool isThisAnimalMounted = false;

    public void MarkThisAnimalAsDead() // Used in 'AnimalDeathState.cs', to block the player from mounting dead animals
    {
        isThisAnimalDead = true;
    }

    public void MarkThisAnimalAsAlive() // Will be used in 'AnimalRespawnManager.cs', to allow mounting of respawned animals
    {
        isThisAnimalDead = false;
    }

    public bool ThisAnimalIsDead() // Is this animal dead?
    {
        return isThisAnimalDead;
    }

    public void MarkThisAnimalAsMounted() 
    {
        isThisAnimalMounted = true;
    }

    public void MarkThisAnimalAsUnmounted() 
    {
        isThisAnimalMounted = false;
    }

    public bool ThisAnimalIsMounted() 
    {
        return isThisAnimalMounted;
    }
}
}

then, I initiated it in ‘AnimalStateMachine.cs’, as such (this is my current ‘AnimalStateMachine.cs’ script):

using RPG.States;
using UnityEngine;
using RPG.Attributes;
using RPG.Animals;
using UnityEngine.AI;
using RPG.States.Enemies;
using RPG.Stats;
using RPG.Movement;
using RPG.Control;
using RPG.MalbersAccess;

public class AnimalStateMachine : StateMachine
{
    [field: SerializeField] public Animator Animator {get; private set;}
    [field: SerializeField] public Health Health {get; private set;}
    [field: SerializeField] public Animal ThisAnimal {get; private set;}
    [field: SerializeField] public NavMeshAgent Agent {get; private set;}
    [field: SerializeField] public BaseStats BaseStats {get; private set;}
    [field: SerializeField] public Rigidbody Rigidbody {get; private set;}
    [field: SerializeField] public CharacterController CharacterController {get; private set;}
    [field: SerializeField] public ForceReceiver ForceReceiver {get; private set;}
    [field: SerializeField] public PatrolPath PatrolPath {get; private set;}
    [field: SerializeField] public UniqueAnimalProperties UniqueAnimalProperties {get; private set;}

    [field: SerializeField] public float FreeLookRotationSpeed {get; private set;} = 15.0f;
    [field: SerializeField] public float MovementSpeed {get; private set;} = 2.0f;
    [field: SerializeField] public float CrossFadeDuration {get; private set;} = 0.1f;
    [field: SerializeField] public float AnimatorDampTime {get; private set;} = 0.1f;

    public Blackboard Blackboard = new Blackboard();

    [field: Header("AUTOMATICALLY UPDATED VARIABLE,\nDO NOT TOUCH!\nWILLD DELETE WHEN TESTING\nISDONE")]
    [field: Tooltip("Temporary holder for the animal's Patrol Path, so that when the Player mounts the animal, he can control it by deleting the patrol path, and when it's dismounted, the animal knows it's Patrol Path again")]
    [field: SerializeField] public PatrolPath PatrolPathHolder { get; private set; } // will hold the patrol path for when the player drives the animal

    private void OnValidate()
    {
        if (Animator == null) Animator = GetComponent<Animator>();
        if (Health == null) Health = GetComponent<Health>();
        if (ThisAnimal == null) ThisAnimal = GetComponent<Animal>();
        if (Agent == null) Agent = GetComponentInChildren<NavMeshAgent>();
        if (BaseStats == null) BaseStats = GetComponent<BaseStats>();
        if (Rigidbody == null) Rigidbody = GetComponent<Rigidbody>();
        if (CharacterController == null) CharacterController = GetComponent<CharacterController>();
        if (ForceReceiver == null) ForceReceiver = GetComponent<ForceReceiver>();
        // Patrol Path must be manually assigned from the scene to the gameObject
        if (UniqueAnimalProperties == null) UniqueAnimalProperties = GetComponent<UniqueAnimalProperties>();
    }

    private void Start()
    {
        Health.onDie.AddListener(() =>
        {
            SwitchState(new AnimalDeathState(this));
        });

        SwitchState(new AnimalIdleState(this));

        Blackboard["Level"] = BaseStats.GetLevel();

        // The animals rely on the rigidbody for collision detection, so you can't
        // turn off 'isKinematic' BY CODE in here (don't try with the hierarchy, it'll fail!).
        // So, instead, turn off the 'detectCollisions' for 'CharacterController', that'll
        // solve the problem!
        CharacterController.detectCollisions = false;
    }

    public PatrolPath GetAssignedPatrolPath()
    {
        return PatrolPath;
    }

    public void AssignPatrolPath(PatrolPath newPatrolPath)
    {
        this.PatrolPath = newPatrolPath;
    }

    public PatrolPath GetPatrolPathHolder()
    {
        return PatrolPathHolder;
    }

    public void SetPatrolPathHolder(PatrolPath PatrolPathHolder)
    {
        this.PatrolPathHolder = PatrolPathHolder;
    }
}

and when patrolling or dwelling, I try enabling it, and disabling it (when in idle, so it can return to patrolling when dismounted (IT’S FOR TEST PURPOSES. THE FINAL MECHANIC WILL MAKE THE ANIMAL YOUR PET WHEN DISMOUNTED, NOW THAT YOU OWN IT)) accordingly.

Here’s the Patrolling part where we set it up:

// in 'Enter()':

        AnimalMountManager.OnAnimalMounted += RemoveAnimalPatrolPath;

// in 'Exit()':

        AnimalMountManager.OnAnimalMounted -= RemoveAnimalPatrolPath;

// Removing the Patrol Path to make the animal idle:

    private void RemoveAnimalPatrolPath()
    {
        stateMachine.UniqueAnimalProperties.MarkThisAnimalAsMounted();

        if (stateMachine.UniqueAnimalProperties.ThisAnimalIsMounted())
        {
            stateMachine.SetPatrolPathHolder(stateMachine.PatrolPath);
            stateMachine.AssignPatrolPath(null);
            stateMachine.SwitchState(new AnimalIdleState(stateMachine));
        }
    }

and the exact same thing happens when dwelling to be able to drive the animal

Moving on, in ‘Idle’, making them patrol again does the opposite effect, as follows:

// in 'Enter()':

        AnimalMountManager.OnAnimalDismounted += ReassignAnimalPatrolPath;

// in 'Exit()':

        AnimalMountManager.OnAnimalDismounted -= ReassignAnimalPatrolPath;

// in 'ReassignAnimalPatrolPath()':

    private void ReassignAnimalPatrolPath()
    {
        stateMachine.ThisAnimal.GetComponent<UniqueAnimalProperties>().MarkThisAnimalAsUnmounted();

        if (!stateMachine.UniqueAnimalProperties.ThisAnimalIsMounted())
        {
            // DUMMY CODE JUST TO TEST THE ANIMALS PATROL STATE
            // DELETE AFTER ALL ANIMALS SUCCESSFULLY BE GOOD AT
            // NAVIGATION
            stateMachine.AssignPatrolPath(stateMachine.GetPatrolPathHolder());
            stateMachine.SwitchState(new AnimalIdleState(stateMachine));
            stateMachine.SetPatrolPathHolder(null);
        }
    }

The problem I have is, is there’s two animals in my scene (for now), a horse and a Raven

When you mount the horse, it triggers correctly as expected

When you mount the Raven, it triggers THE HORSE correctly as expected… and does NOTHING to the Raven itself, and I’m very confused as of why

Worst thing is, they both think they’re the ones being driven, regardless of who I’m driving, and I really can’t tell why. I just know it’s a flaw to do with the patrolling system

Please help @Brian_Trotter or anyone else who sees this :slight_smile:

and the same problem goes on when trying to check for the animal’s death:

using RPG.Animals;
using RPG.MalbersAccess;
using RPG.States.Animals;
using UnityEngine;

public class AnimalDeathState : AnimalBaseState
{
    private static readonly int FirstHorseDeathHash = Animator.StringToHash("Horse Death 1");
    private static readonly int SecondHorseDeathHash = Animator.StringToHash("Horse Death 1 Mirrored");
    private static readonly int ThirdHorseDeathHash = Animator.StringToHash("Horse Death 2");
    private static readonly int FourthHorseDeathHash = Animator.StringToHash("Horse Death 2 Mirrored");

    // Set up the Raven death has here as well

    public AnimalDeathState(AnimalStateMachine stateMachine) : base(stateMachine) {}

    public override void Enter()
    {
        Debug.Log($"Animal has entered death state");

        if (stateMachine.ThisAnimal.Type == AnimalType.Horse)
        {
            // in 'AnimalBaseState.cs'
            ActivateNonMalbersLayerWeight(0, 0);
            ActivateNonMalbersLayerWeight(1, 0);
            ActivateNonMalbersLayerWeight(2, 0);
            ActivateNonMalbersLayerWeight(3, 0);
            ActivateNonMalbersLayerWeight(4, 1);
        }

        if (stateMachine.ThisAnimal.Type == AnimalType.Raven)
        {
            // in 'AnimalBaseState.cs' (Will be implemented in the future)
            ActivateNonMalbersLayerWeight(0, 0);
            ActivateNonMalbersLayerWeight(1, 0);
            ActivateNonMalbersLayerWeight(2, 0);
            ActivateNonMalbersLayerWeight(3, 0);
            ActivateNonMalbersLayerWeight(4, 0);
            ActivateNonMalbersLayerWeight(5, 1);
        }
        ChooseRandomDeathAnimation();

        // Solution to disable Malbers' MountTrigger.OnTriggerEnter()' when the animal is dead
        var uniqueAnimalProperties = stateMachine.ThisAnimal.GetComponent<UniqueAnimalProperties>();
        if (uniqueAnimalProperties != null)
        {
            uniqueAnimalProperties.MarkThisAnimalAsDead();
        }
    }

    public override void Tick(float deltaTime)
    {
        // There's no 'Move(deltaTime)' here
    }

    public override void Exit()
    {
        if (stateMachine.ThisAnimal.Type == AnimalType.Horse)
        {
            // in 'AnimalBaseState.cs'
            ActivateNonMalbersLayerWeight(0, 1);
            ActivateNonMalbersLayerWeight(1, 1);
            ActivateNonMalbersLayerWeight(2, 1);
            ActivateNonMalbersLayerWeight(3, 1);
            ActivateNonMalbersLayerWeight(4, 0);
        }

        if (stateMachine.ThisAnimal.Type == AnimalType.Raven)
        {
            // in 'AnimalBaseState.cs' (Will be implemented in the future)
            ActivateNonMalbersLayerWeight(0, 1);
            ActivateNonMalbersLayerWeight(1, 1);
            ActivateNonMalbersLayerWeight(2, 1);
            ActivateNonMalbersLayerWeight(3, 1);
            ActivateNonMalbersLayerWeight(4, 1);
            ActivateNonMalbersLayerWeight(5, 0);
        }
    }

    private void ChooseRandomDeathAnimation()
    {
        if (stateMachine.ThisAnimal.Type == AnimalType.Horse)
        {
            var randomAnimationSelector = Random.Range(0, 4); // includes the zero, excludes the 4

            switch (randomAnimationSelector)
            {
                case 0:
                stateMachine.Animator.CrossFadeInFixedTime(FirstHorseDeathHash, AnimatorDampTime);
                break;

                case 1:
                stateMachine.Animator.CrossFadeInFixedTime(SecondHorseDeathHash, AnimatorDampTime);
                break;

                case 2:
                stateMachine.Animator.CrossFadeInFixedTime(ThirdHorseDeathHash, AnimatorDampTime);
                break;

                case 3:
                stateMachine.Animator.CrossFadeInFixedTime(FourthHorseDeathHash, AnimatorDampTime);
                break;
            }
        }

        // Repeat for other animals here
    }
}

For this one, the horse doesn’t get marked as dead for the bird’s death, but the bird doesn’t get marked dead either… :sweat_smile:

What’s driving me to pure insanity, is where is this link coming from? How do I seperate each animal to take it’s own variables without tangling them up?

That Raven by Malbers is seriously testing my nerves… 600%+ of all the problems I have with the animals is from one specific, oversized, Raven :sweat_smile:

Or… in simpler terms, the fact that I got more than one animal in my scene is a severe headache…

Anyway, Instead of trying to modify the animal from it’s state machine, I decided to modify the animal from the player’s state machine, when he’s mounting an animal, from a script called ‘PlayerOnAnimalStateMachine.cs’ which I created a while ago. I’m trying to create a new ‘RemoveMountPatrolPointsDelayed’ Enum, but it’s failing miserably with one animal, and I have zero idea why:

using UnityEngine;
using MalbersAnimations.HAP;
using RPG.Animals;
using RPG.Attributes;
using RPG.Movement;
using System.Collections;
using RPG.InputReading;
using RPG.Core;
using RPG.Combat;
using Cinemachine;
using UnityEngine.UI;
using RPG.States.Enemies;
using System;
using RPG.States.Player;
using RPG.Statics;

// -----------------------------------------------------------------------------------------------------------

// NOTE 1: IF THERE'S A MAJOR COLLISION BETWEEN THE SCRIPTS OF THE 'OnAnimal'
// STATE MACHINE, AND THE ON-FOOT STATE MACHINE, THEN CREATE A BRAND
// NEW INPUTREADER SCRIPT, DEDICATED TO THE 'OnAnimal' STATE MACHINE,
// AND DISCONNECT THE 'OnFoot' VERSION FROM THE 'OnAnimal' VERSION, IN
// 'PlayerToPlayerOnAnimalToggle.cs'

// NOTE 2: All animations for the 'PlayerOnAnimalStateMachine' are handled in the 'MountAttack' animation 
// layer, and the switch is done in the 'PlayerToPlayerOnAnimalToggle.cs' script, in the 'OnEnable()'
// and 'OnDisable()' function (along with the reverse switch, when the player gets off the animal)

// -----------------------------------------------------------------------------------------------------------

namespace RPG.States.PlayerOnAnimal {

public class PlayerOnAnimalStateMachine : StateMachine, IOnAnimalAimReticleTargetProvider
{
    [field: Tooltip("Keeping track of 'isOnAnimalMount' ONLY, toggled in 'PlayerToPlayerOnAnimalToggle.cs (DO NOT TOUCH!). If you want to make any modifications, just do it straight in 'AnimalMountManager.isOnAnimalMount', and this variable will show you the changes you're making")]
    [field: SerializeField] public bool IsOnAnimalMount {get; set;}

    [field: SerializeField] public Animal Animal {get; private set;}
    [field: SerializeField] public MRider Rider {get; private set;}
    [field: SerializeField] public Health Health {get; private set;}
    [field: SerializeField] public Animator Animator {get; private set;}
    [field: SerializeField] public ForceReceiver ForceReceiver {get; private set;}
    [field: SerializeField] public CharacterController CharacterController {get; private set;}
    [field: SerializeField] public PlayerToPlayerOnAnimalToggle StateMachineToggle {get; private set;}
    [field: SerializeField] public InputReader InputReader {get; private set;}
    [field: SerializeField] public CooldownTokenManager CooldownTokenManager {get; private set;}
    [field: SerializeField] public Fighter Fighter {get; private set;}
    [field: SerializeField] public float CrossFadeDuration {get; private set;} = 0.15f;
    [field: SerializeField] public float ImpactCooldown {get; private set;} = 1.0f;

    // TEST: Cinemachine Virtual Camera:
    // [field: SerializeField] public CinemachineVirtualCamera RangerAimingCamera {get; private set;}
    [field: SerializeField] public CinemachineFreeLook RangerAimingCamera {get; private set;}

    // TEST: Aim Reticle Image:
    [field: SerializeField] public Image AimReticle {get; private set;}

    // TEST: Player's Right Hand Transform (for PlayerOnAnimalRangerAimingState) to find a starting point:
    [field: SerializeField] public Transform RightHandTransform {get; private set;}

    // TEST: UPPER SPINE
    [field: SerializeField] public Transform PlayerBodyRoot {get; private set;}
    [field: SerializeField] public float UpperBodySpineRotationSpeed {get; private set;} = 5.0f;

    [field: SerializeField] public EnemyStateMachine CurrentAimReticleVictim;

    void Start()
    {
        Health.onDie.AddListener(() =>
        {
            SwitchState(new PlayerOnAnimalDeathState(this));
        });

        ForceReceiver.OnForceApplied += HandleForceApplied;
    }

        private void HandleForceApplied(Vector3 force)
    {
        force = Vector3.zero; // you ain't flying off the damn animal...! (but you need the parameter for the delegate to work)
        if (Health.IsDead()) return;
        if (CooldownTokenManager.HasCooldown("OnAnimalImpact")) return;
        SwitchState(new PlayerOnAnimalImpactState(this));
    }

    void OnDestroy()
    {
        Health.onDie.RemoveListener(() =>
        {
            SwitchState(new PlayerOnAnimalDeathState(this));
        });

        ForceReceiver.OnForceApplied -= HandleForceApplied;

        AnimalMountManager.OnAnimalMounted -= RemoveMountPatrolPoints;

    }

        void OnValidate()
    {
        // you can't get what you're not driving, so skip getting the Animal
        // rider is taken care of in 'OnEnable'. Don't need to do it twice
        if (Health == null) Health = GetComponent<Health>();
        if (Animator == null) Animator = GetComponent<Animator>();
        if (ForceReceiver == null) ForceReceiver = GetComponent<ForceReceiver>();
        if (CharacterController == null) CharacterController = GetComponent<CharacterController>();
        if (StateMachineToggle == null) StateMachineToggle = GetComponent<PlayerToPlayerOnAnimalToggle>();
        if (InputReader == null) InputReader = GetComponent<InputReader>();
        if (CooldownTokenManager == null) CooldownTokenManager = GetComponent<CooldownTokenManager>();
        if (Fighter == null) Fighter = GetComponent<Fighter>();
    }

    void OnEnable()
    {
        if (Health.IsDead()) SwitchState(new PlayerOnAnimalDeathState(this)); // don't really need that in 'Start()', just in 'OnEnable()'
        else SwitchState(new PlayerOnAnimalFreeLookState(this));

        // You can't get the animal in 'Awake', since it's dynamic, so I decided to do the work in 'OnEnable', since this is a toggleable script
        Rider = GetComponent<MRider>();
        StartCoroutine(GetAnimalDelayed(0.01f));
        if (!CharacterController.enabled) CharacterController.enabled = true; // (TEMP SOL) on a serious note, find why it gets deactivated and turn the deactivator off there

        RemoveMountPatrolPoints();
    }

        void OnDisable() 
    {
        // To delete the changes when the script is turned off
        ClearAnimal();
        ClearPlayerRider();
    }

    void ClearAnimal() 
    {
        Animal = null;
    }

    void ClearPlayerRider() 
    {
        Rider = null;
    }

    private IEnumerator GetAnimalDelayed(float delay) // delay allows the computer to get the animal first, before trying to get it (won't work otherwise)
    {
        yield return new WaitForSeconds(delay);
        Animal = GetComponentInParent<Animal>();
    }

    public Animal GetAnimal() // Implementing the 'IAnimal' interface requirements
    {
        return Animal;
    }

    public EnemyStateMachine GetOnAnimalCurrentAimReticleVictim()
    {
        return CurrentAimReticleVictim;
    }

    public void RemoveMountPatrolPoints() 
    {
        StartCoroutine(RemoveMountPatrolPointsDelayed(0.02f));
    }

    private IEnumerator RemoveMountPatrolPointsDelayed(float delay) 
    {
        yield return new WaitForSeconds(delay);

        var mountedAnimal = GetAnimal();
        if (mountedAnimal != null)
        {
            Debug.Log($"Delayed mount patrol point removal - step 1");
            mountedAnimal.GetComponent<AnimalStateMachine>().AssignPatrolPath(null);
            mountedAnimal.GetComponent<AnimalStateMachine>().SwitchState(new AnimalIdleState(mountedAnimal.GetComponent<AnimalStateMachine>()));
        }
    }

    }
}

For the moment, since I have to go, I just need to keep in mind that the bird does not respond the same way as the horse to cancelling patrolling for some reason, which means ‘MRider.cs’ or ‘MountTriggers.cs’ probably had something wrong injected. I’ll investigate it when I’m home

This if statement is what we like to call completely redundant. It will always be true because you set it to be so on the line before.

AnimalMountManager… is that something that exists on each mountable or is it a Singleton or static type thing… i.e. is that OnAnimalMounted something that will fire only if it’s your animal, or something that fires when any animal is mounted?

True, but it would also ensure that we only call this function on the animal that we have just driven, and not every single animal on the scene, the bug I’m trying to fix

Anyway, I deleted that, and what I got below is what I currently am working on right now :slight_smile:

Apologies for the late response (I was moving between cities, again). I made a few changes and started calling the function to clean up the patrol path, to allow the animal to be driven, from the player on animal state machine, instead of trying to modify the animal’s state machine itself. I figured that would make life easier to code down the line

It does the same effect as before, where it cleans the horse’s patrol paths, but fails for some reason with the Raven, and I am yet to investigate why

Here’s the current code as is:

using UnityEngine;
using MalbersAnimations.HAP;
using RPG.Animals;
using RPG.Attributes;
using RPG.Movement;
using System.Collections;
using RPG.InputReading;
using RPG.Core;
using RPG.Combat;
using Cinemachine;
using UnityEngine.UI;
using RPG.States.Enemies;
using System;
using RPG.States.Player;
using RPG.Statics;

// -----------------------------------------------------------------------------------------------------------

// NOTE 1: IF THERE'S A MAJOR COLLISION BETWEEN THE SCRIPTS OF THE 'OnAnimal'
// STATE MACHINE, AND THE ON-FOOT STATE MACHINE, THEN CREATE A BRAND
// NEW INPUTREADER SCRIPT, DEDICATED TO THE 'OnAnimal' STATE MACHINE,
// AND DISCONNECT THE 'OnFoot' VERSION FROM THE 'OnAnimal' VERSION, IN
// 'PlayerToPlayerOnAnimalToggle.cs'

// NOTE 2: All animations for the 'PlayerOnAnimalStateMachine' are handled in the 'MountAttack' animation 
// layer, and the switch is done in the 'PlayerToPlayerOnAnimalToggle.cs' script, in the 'OnEnable()'
// and 'OnDisable()' function (along with the reverse switch, when the player gets off the animal)

// -----------------------------------------------------------------------------------------------------------

namespace RPG.States.PlayerOnAnimal {

public class PlayerOnAnimalStateMachine : StateMachine, IOnAnimalAimReticleTargetProvider
{
    [field: Tooltip("Keeping track of 'isOnAnimalMount' ONLY, toggled in 'PlayerToPlayerOnAnimalToggle.cs (DO NOT TOUCH!). If you want to make any modifications, just do it straight in 'AnimalMountManager.isOnAnimalMount', and this variable will show you the changes you're making")]
    [field: SerializeField] public bool IsOnAnimalMount {get; set;}

    [field: SerializeField] public Animal Animal {get; private set;}
    [field: SerializeField] public MRider Rider {get; private set;}
    [field: SerializeField] public Health Health {get; private set;}
    [field: SerializeField] public Animator Animator {get; private set;}
    [field: SerializeField] public ForceReceiver ForceReceiver {get; private set;}
    [field: SerializeField] public CharacterController CharacterController {get; private set;}
    [field: SerializeField] public PlayerToPlayerOnAnimalToggle StateMachineToggle {get; private set;}
    [field: SerializeField] public InputReader InputReader {get; private set;}
    [field: SerializeField] public CooldownTokenManager CooldownTokenManager {get; private set;}
    [field: SerializeField] public Fighter Fighter {get; private set;}
    [field: SerializeField] public float CrossFadeDuration {get; private set;} = 0.15f;
    [field: SerializeField] public float ImpactCooldown {get; private set;} = 1.0f;

    // TEST: Cinemachine Virtual Camera:
    // [field: SerializeField] public CinemachineVirtualCamera RangerAimingCamera {get; private set;}
    [field: SerializeField] public CinemachineFreeLook RangerAimingCamera {get; private set;}

    // TEST: Aim Reticle Image:
    [field: SerializeField] public Image AimReticle {get; private set;}

    // TEST: Player's Right Hand Transform (for PlayerOnAnimalRangerAimingState) to find a starting point:
    [field: SerializeField] public Transform RightHandTransform {get; private set;}

    // TEST: UPPER SPINE
    [field: SerializeField] public Transform PlayerBodyRoot {get; private set;}
    [field: SerializeField] public float UpperBodySpineRotationSpeed {get; private set;} = 5.0f;

    [field: SerializeField] public EnemyStateMachine CurrentAimReticleVictim;

    void Start()
    {
        Health.onDie.AddListener(() =>
        {
            SwitchState(new PlayerOnAnimalDeathState(this));
        });

        ForceReceiver.OnForceApplied += HandleForceApplied;
    }

        private void HandleForceApplied(Vector3 force)
    {
        force = Vector3.zero; // you ain't flying off the damn animal...! (but you need the parameter for the delegate to work)
        if (Health.IsDead()) return;
        if (CooldownTokenManager.HasCooldown("OnAnimalImpact")) return;
        SwitchState(new PlayerOnAnimalImpactState(this));
    }

    void OnDestroy()
    {
        Health.onDie.RemoveListener(() =>
        {
            SwitchState(new PlayerOnAnimalDeathState(this));
        });

        ForceReceiver.OnForceApplied -= HandleForceApplied;

        AnimalMountManager.OnAnimalMounted -= RemoveMountPatrolPoints;

    }

        void OnValidate()
    {
        // you can't get what you're not driving, so skip getting the Animal
        // rider is taken care of in 'OnEnable'. Don't need to do it twice
        if (Health == null) Health = GetComponent<Health>();
        if (Animator == null) Animator = GetComponent<Animator>();
        if (ForceReceiver == null) ForceReceiver = GetComponent<ForceReceiver>();
        if (CharacterController == null) CharacterController = GetComponent<CharacterController>();
        if (StateMachineToggle == null) StateMachineToggle = GetComponent<PlayerToPlayerOnAnimalToggle>();
        if (InputReader == null) InputReader = GetComponent<InputReader>();
        if (CooldownTokenManager == null) CooldownTokenManager = GetComponent<CooldownTokenManager>();
        if (Fighter == null) Fighter = GetComponent<Fighter>();
    }

    void OnEnable()
    {
        if (Health.IsDead()) SwitchState(new PlayerOnAnimalDeathState(this)); // don't really need that in 'Start()', just in 'OnEnable()'
        else SwitchState(new PlayerOnAnimalFreeLookState(this));

        // You can't get the animal in 'Awake', since it's dynamic, so I decided to do the work in 'OnEnable', since this is a toggleable script
        Rider = GetComponent<MRider>();
        StartCoroutine(GetAnimalDelayed(0.01f));
        if (!CharacterController.enabled) CharacterController.enabled = true; // (TEMP SOL) on a serious note, find why it gets deactivated and turn the deactivator off there

        RemoveMountPatrolPoints();
    }

        void OnDisable() 
    {
        // To delete the changes when the script is turned off
        ClearAnimal();
        ClearPlayerRider();
    }

    void ClearAnimal() 
    {
        Animal = null;
    }

    void ClearPlayerRider() 
    {
        Rider = null;
    }

    private IEnumerator GetAnimalDelayed(float delay) // delay allows the computer to get the animal first, before trying to get it (won't work otherwise)
    {
        yield return new WaitForSeconds(delay);
        Animal = GetComponentInParent<Animal>();
    }

    public Animal GetAnimal() // Implementing the 'IAnimal' interface requirements
    {
        return Animal;
    }

    public EnemyStateMachine GetOnAnimalCurrentAimReticleVictim()
    {
        return CurrentAimReticleVictim;
    }

    public void RemoveMountPatrolPoints() 
    {
        StartCoroutine(RemoveMountPatrolPointsDelayed(0.02f));
    }

    private IEnumerator RemoveMountPatrolPointsDelayed(float delay) 
    {
        yield return new WaitForSeconds(delay);

        var mountedAnimal = GetAnimal();
        Debug.Log($"Mounted animal is {mountedAnimal.gameObject.name}");
        if (mountedAnimal != null)
        {
            Debug.Log($"Delayed mount patrol point removal - step 1");
            mountedAnimal.GetComponent<AnimalStateMachine>().AssignPatrolPath(null);
            mountedAnimal.GetComponent<AnimalStateMachine>().SwitchState(new AnimalIdleState(mountedAnimal.GetComponent<AnimalStateMachine>()));
        }
    }

    }
}

The script above is a variant of the Player State Machine, and is what gets called when the player starts riding an animal

(RemoveMountPatrolPointsDelay is what matters, along with getting the correct animal (which I can easily do, because each time the player mounts an animal, the animal becomes the parent of the player on the hierarchy))

It’ll fire when any animal is mounted. Here’s the script itself:

using System;

namespace RPG.Statics {

public static class AnimalMountManager 
{
    // Similar to DodgeManager, this static class is carefully controlled from multiple scripts
    // to make sure the player can act accordingly, and perform other complex operations, based
    // on whether the player is mounting an animal or not
    public static bool isOnAnimalMount = false;

    // EVENTS (because I hate 'Update'):
    public static event Action OnAnimalMounted;
    public static event Action OnAnimalDismounted;

    // EVENT FUNCTIONS (Because you can't call them from outside the 
    // script that created the event without that):
    public static void InvokeOnAnimalMounted() 
    {
        OnAnimalMounted?.Invoke();
    }

    public static void InvokeOnAnimalDismounted() 
    {
        OnAnimalDismounted?.Invoke();
    }

    // isOnAnimalMount is used in:
    // MRider.cs, to toggle it
    // 'PlayerToPlayerOnAnimalToggle.cs', TO TOGGLE IT, AND FORCE IT TO BE FALSE ON 'Start()'
    // EVERY SINGLE STATE OF 'PlayerOnAnimalStateMachine.cs', AND 'PlayerFreeLookState.cs' TO AVOID COLLISIONS WHEN TOGGLING STATES IN BOTH MACHINES
    // Fighter.cs, for safety checks in 'TryHit'
    // 'WeaponConfig.cs', to determine whether a hit is treated with the assumptions of the player on the back of an animal, or on foot
}
}

As you noticed, it’s static, and it gets called each time any animal is mounted (and there’s only one. It’s not placed on every animal. In fact, it’s not placed on any animal at all. It just exists to be subscribed and unsubscribed to, and invoked in Malbers’ code)

and in Malbers’ code, when we mount any animal at all, it gets called, as you can see from this script:

        public virtual void MountAnimal()
        {
            if (!CanMount || !enabled) return; 

            if (!Montura.InstantMount)                                           //If is instant Mount play it      
            {
                Debbuging("Mount Animal", "cyan");
                Mounted = true;                                                  //Update MountSide Parameter In the Animator
                SetMountSide(MountTrigger.MountID);                              //Update MountSide Parameter In the Animator
                // Anim?.Play(MountTrigger.MountAnimation, MountLayerIndex);      //Play the Mounting Animations
            }
            else
            {
                Debbuging("Instant Mount", "cyan");

                Anim?.Play(Montura.MountIdle, MountLayerIndex);                //Ingore the Mounting Animations
                Anim?.Update(0);                             //Update the Animator ????

                Start_Mounting();
                End_Mounting();

                // ADDED BY BAHAA (next 3 lines, that is):
                AnimalMountManager.isOnAnimalMount = true;
                AnimalMountManager.InvokeOnAnimalMounted();
                Debug.Log($"isOnAnimalMount: {AnimalMountManager.isOnAnimalMount}");

                Montura.Rider.transform.SetParent(Montura.transform);
            }
        }

        public virtual void DismountAnimal()
        {
            if (!CanDismount || !enabled) return;

            Debbuging("Dismount Animal", "cyan");

            Debug.Log($"Animal Dismount Called");

            Montura.Mounted = Mounted = false;                                  //Unmount the Animal
            MountTrigger = GetDismountTrigger();

            SetMountSide(MountTrigger.DismountID);                               //Update MountSide Parameter In the Animator

            if (Montura.InstantDismount)                                         //Use for Instant Dismount
            {
                Anim.Play(EmptyHash, MountLayerIndex);
                SetMountSide(0);                                                //Update MountSide Parameter In the Animator

                Start_Dismounting();

                var MT = MountTrigger;
                End_Dismounting();
                RiderRoot.position = MT.transform.position + (MT.transform.forward * -0.2f);   //Move the rider directly to the mounttrigger
                RiderRoot.rotation = MT.transform.rotation;

                // ADDED BY BAHAA (next 2 lines, that is):
                AnimalMountManager.isOnAnimalMount = false;
                AnimalMountManager.InvokeOnAnimalDismounted();
                Debug.Log($"isOnAnimalMount: {AnimalMountManager.isOnAnimalMount}");
                // Somewhere in Bahaa's code, I told it to send the player to 'PlayerAggroGroup' on dismount, so alls good
            }
        }

From my current understanding, it looks like whatever is called in one script for one animal, is called for every other animal with the script as well… The bird I have gets called to patrol although it doesn’t have a patrolling path for itself, and now I’m just insanely confused. Something is horribly wrong here

I HATE THIS…! I HAD TWO INSTANCES OF ‘ANIMALSTATEMACHINE’ ON THE BIRD, AND THIS IS WHAT WAS CAUSING THIS DISASTER FOR DAYS NOW!

That, and the whole code being in ‘AnimalStateMachine.cs’

Apparently placing the code in ‘AnimalStateMachine.cs’ was half the problem, and whilst this was solved by moving the code to ‘PlayerOnAnimalStateMachine.cs’, so we can do the magic there, it makes other stuff not work for the animals…

SO… I came up with a hybrid solution that (temporarily) seems to be working, but I don’t know how it will scale. I have a feeling like each animal will need it’s own unique identifier for it to work properly in the end

Anyway, here’s what I came up with:

  1. Delete all I just wrote in ‘PlayerOnAnimalStateMachine.cs’

  2. Let’s leverage the ‘GetAnimal()’ in ‘PlayerOnAnimalStateMachine.cs’, and use it in our event subscriptions in both ‘AnimalPatrolState.cs’ and ‘AnimalDwellState.cs’, as follows (JUST TO KEEP THINGS IN ORDER, THIS MEANS THAT ‘AnimalStateMachine’ RELIES ON ‘PlayerOnAnimalStateMachine’ NOW):

// in 'Enter()', subscribe to 'RemoveAnimalPatrolPath', and in 'Exit()', unsubscribe to 'RemoveAnimalPatrolPath'

// Code:

    private void RemoveAnimalPatrolPath()
    {
        stateMachine.StartCoroutine(RemovePatrolPathDelayed(0.05f)); // a little longer than the 10ms time to get the animal in 'PlayerOnAnimalStateMachine.cs'
    }

    private IEnumerator RemovePatrolPathDelayed(float delay) 
    {
        yield return new WaitForSeconds(delay);
        if (stateMachine.PlayerOnAnimalStateMachine.GetAnimal() == stateMachine.GetComponent<Animal>())
        {
            stateMachine.SetPatrolPathHolder(stateMachine.PatrolPath);
            stateMachine.AssignPatrolPath(null);
            stateMachine.SwitchState(new AnimalIdleState(stateMachine));
        }
    }

All these subscriptions are done on ‘AnimalMountManager.OnAnimalMounted’

and if you want to reassign a patrol path when the animal is unmounted for them to return to patrolling, which I’m only testing right now to make sure animals don’t act weird on the NavMesh, you can sub and unsub this function in ‘AnimalIdleState.cs’:

    public void ReassignPatrolPath()
    {
        // REMOVE THIS FUNCTION WHEN NAVMESH TESTING IS DONE!
        if (stateMachine.PatrolPath == null)
        {
            stateMachine.AssignPatrolPath(stateMachine.GetPatrolPathHolder());
            stateMachine.SetPatrolPathHolder(null);
            stateMachine.SwitchState(new AnimalIdleState(stateMachine));
        }
    }

And these subscriptions are done on ‘AnimalMountManager.OnAnimalDismounted’


SO… Just to keep things clean, I took the extra step of ensuring that we only modify the gameObject with the same Unique ID as the one we are driving, just so we don’t end up deactivating every single horse in the scene because we drove one horse, or every single raven in the scene because we drove one single Raven. Here’s what I did

  1. in my personal ‘Animal.cs’ script, I created a new Guid field, and a solution to ensure it’s unique. Here’s my current ‘Animal.cs’ script after that modification:
using System;
using UnityEngine;

namespace RPG.Animals {

public enum AnimalType
{
    Horse,
    Raven,
}

public class Animal : MonoBehaviour, IAnimal, ISerializationCallbackReceiver
{
    [SerializeField] private AnimalType animalType;
    public AnimalType Type => animalType;
    [SerializeField] string uniqueID;

    public Animal GetAnimal()
    {
        return this;
    }

    public void OnBeforeSerialize()
    {
        if (string.IsNullOrEmpty(uniqueID))
        {
            uniqueID = Guid.NewGuid().ToString();
        }
    }

    public void OnAfterDeserialize()
    {
        // Nothing here, just to obey the 'ISerializationCallbackReceiver' Interface rules
    }

    public string GetUniqueID() 
    {
        return uniqueID;
    }
}
}
  1. in both the patrolling and dwelling states of the animals, I have updated things a little bit, to compensate for the new Unique Identifiers:
// in 'AnimalPatrolState.cs':

    private void RemoveAnimalPatrolPath()
    {
        stateMachine.StartCoroutine(RemovePatrolPathDelayed(0.05f));
    }

    private IEnumerator RemovePatrolPathDelayed(float delay)
    {
        yield return new WaitForSeconds(delay);
        if (stateMachine.PlayerOnAnimalStateMachine.GetAnimal().GetUniqueID() == stateMachine.GetComponent<Animal>().GetUniqueID())
        {
            stateMachine.SetPatrolPathHolder(stateMachine.PatrolPath);
            stateMachine.AssignPatrolPath(null);
            stateMachine.SwitchState(new AnimalIdleState(stateMachine));
        }
    }

// in 'AnimalDwellState.cs':

    private void RemoveAnimalPatrolPath()
    {
        stateMachine.StartCoroutine(RemovePatrolPathDelayed(0.05f));
    }

    private IEnumerator RemovePatrolPathDelayed(float delay)
    {
        yield return new WaitForSeconds(delay);
        if (stateMachine.PlayerOnAnimalStateMachine.GetAnimal().GetUniqueID() == stateMachine.GetComponent<Animal>().GetUniqueID())
        {
            stateMachine.SetPatrolPathHolder(stateMachine.PatrolPath);
            stateMachine.AssignPatrolPath(null);
            stateMachine.SwitchState(new AnimalIdleState(stateMachine));
        }
    }

Now, it does a unique ID comparison instead of a script comparison. This way, you don’t end up deactivating every single animal species because you mounted one of them for example

Again, these are functions that use events to sub and unsub towards, in their respected classes

I WAS DEAD CONFUSED WHY MY DEATH CHECKER WASN’T RETURNING TRUE, BUT THE CODE SAID IT WAS RETURNING A CORRECT VALUE. WHY? BECAUSE I HAD MORE THAN ONE INSTANCE ON THE RAVEN FOR THAT AS WELL :stuck_out_tongue:

ANYWAY, I solved all my problems this far (except one, but that’s animation-related). Let’s move on to cleaning things up

  1. Ensure no duplicate script instances on any animal
  2. Clean the Raven up, and give it some animations to work with
  3. Get the NavMeshAgent to act right for the Raven
  4. Move on to chasing once done

and to close the horse off, here’s my final, after countless try and errors, NavMeshAgent values:

@Brian_Trotter question. Suppose for animals I want to introduce some Strafe animations, something to make them rotate more elegantly. What’s the best way to do this through code, apart from adding the animations to the blend trees?

Obviously, for the animations themselves, they’ll need to be in the Animator… in terms of strafing vs moving forward, you’ll need to move on the X axis instead of the Y axis, but you’ll not want to rotate the character… So a Strafing state would move to the right (transform.right * speed * deltaTime) or to the left (-transform.right * speed * deltaTime).

Technically, when the player is in PlayerTargetingState, he is Strafing.

OK I just woke up, so I’m probably unable to grasp what was just mentioned, or I’m not sure what to do, so…

Raven Malbers Locomotion Tree

OK let’s assume this is my Locomotion Blend Tree (it’s not, it’s Malbers’… but I don’t mind copy-pasting it :slight_smile:)

And this is the Blend Tree, a 2D Cartesian Freeform blend tree (again, Malbers’… I don’t mind copy-pasting it as well)

How exactly would you program ‘AnimalPatrolState.cs’ to help with that? For the time being, this is what my code looks like:

using System.Collections;
using RPG.Animals;
using RPG.Control;
using RPG.States.Animals;
using RPG.Statics;
using UnityEngine;

public class AnimalPatrolState : AnimalBaseState
{
    private const string NextPatrolPointIndexKey = "NextPatrolPointIndex";

    public AnimalPatrolState(AnimalStateMachine stateMachine) : base(stateMachine) {}

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

    public override void Enter()
    {
        if (!stateMachine.Agent.enabled) 
        {
            stateMachine.Agent.enabled = true;
        }

        if (stateMachine.PatrolPath == null)
        {
            stateMachine.SwitchState(new AnimalIdleState(stateMachine));
            return;
        }

        int index; // Index of the Patrol Point

        // Check the blackboard for the key, and set the index if the key is setup
        if (stateMachine.Blackboard.ContainsKey(NextPatrolPointIndexKey))
        {
            index = stateMachine.Blackboard.GetValueAsInt(NextPatrolPointIndexKey);
        }
        else
        {
            // If we are coming from chasing or somewhere without prior patrolling,
            // we will need to set it up for the first time, and that's what we are
            // doing here
            index = stateMachine.PatrolPath.GetNearestIndex(stateMachine.transform.position);
        }

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

        if (patrolPoint) // if you have a patrol point
        {
            movementSpeed = stateMachine.MovementSpeed * patrolPoint.SpeedModifier;
            acceptanceRadius = patrolPoint.AcceptanceRadius;
            dwellTime = patrolPoint.DwellTime;
        }
        else // if no patrol point, calculate the movement speed to be the state machine's movement speed
        {
            movementSpeed = stateMachine.MovementSpeed;
        }

        if (stateMachine.ThisAnimal.Type == AnimalType.Horse)
        {
            // in 'AnimalBaseState.cs'
            ActivateNonMalbersLayerWeight(0, 0);
            ActivateNonMalbersLayerWeight(1, 0);
            ActivateNonMalbersLayerWeight(2, 0);
            ActivateNonMalbersLayerWeight(3, 0);
            ActivateNonMalbersLayerWeight(4, 1);
        }

        if (stateMachine.ThisAnimal.Type == AnimalType.Raven)
        {
            // in 'AnimalBaseState.cs' (Will be implemented in the future)
            ActivateNonMalbersLayerWeight(0, 0);
            ActivateNonMalbersLayerWeight(1, 0);
            ActivateNonMalbersLayerWeight(2, 0);
            ActivateNonMalbersLayerWeight(3, 0);
            ActivateNonMalbersLayerWeight(4, 0);
            ActivateNonMalbersLayerWeight(5, 1);
        }

        // Squaring the acceptance radius, to save on calculation time
        acceptanceRadius *= acceptanceRadius;
        // next waypoint index setup
        stateMachine.Blackboard[NextPatrolPointIndexKey] = stateMachine.PatrolPath.GetNextIndex(index);
        // Set the destination for the agent (since waypoints don't move)
        stateMachine.Agent.SetDestination(targetPatrolPoint);
        // Set the animation
        PlayCorrectBlendTreeHash();

        // Remove the Patrol Path for Mounted Animals
        AnimalMountManager.OnAnimalMounted += RemoveAnimalPatrolPath;
    }

    public override void Tick(float deltaTime)
    {
        // if you should pursue or you're aggrevated, remove the next point key index,
        // and switch to chasing state. We'll take care of that later

        if (IsInAcceptanceRange())
        {
            stateMachine.SwitchState(new AnimalDwellState(stateMachine, dwellTime));
            return;
        }

        // Patrolling code
        Vector3 lastPosition = stateMachine.transform.position;
        MoveToWayPoint(deltaTime);
        Vector3 deltaMovement = lastPosition - stateMachine.transform.position;
        float deltaMagnitude = deltaMovement.magnitude;

        Vector3 direction = stateMachine.Agent.desiredVelocity.normalized; // TEST - DELETE IF FAILED

        if (deltaMagnitude > 0)
        {
            // When the game is not paused
            // FaceTarget(targetPatrolPoint, deltaTime);
            FaceMovementDirection(direction, deltaTime); // TEST - DELETE IF FAILED
            float grossSpeed = deltaMagnitude / deltaTime;
            SetCorrectFloatValue(deltaTime);
        }
        else
        {
            // When the game is paused
            // FaceTarget(targetPatrolPoint, deltaTime);
            FaceMovementDirection(direction, deltaTime); // TEST - DELETE IF FAILED
            SetCorrectFloatValue(deltaTime);
        }
    }

    public override void Exit()
    {
        if (!stateMachine.Agent.enabled)
        {
            stateMachine.Agent.enabled = true;
            stateMachine.Agent.ResetPath();
            stateMachine.Agent.velocity = Vector3.zero;
            stateMachine.Agent.enabled = false;
        }
        else
        {
            stateMachine.Agent.ResetPath();
            stateMachine.Agent.velocity = Vector3.zero;
        }

        if (stateMachine.ThisAnimal.Type == AnimalType.Horse)
        {
            // in 'AnimalBaseState.cs'
            ActivateNonMalbersLayerWeight(0, 1);
            ActivateNonMalbersLayerWeight(1, 1);
            ActivateNonMalbersLayerWeight(2, 1);
            ActivateNonMalbersLayerWeight(3, 1);
            ActivateNonMalbersLayerWeight(4, 0);
        }

        if (stateMachine.ThisAnimal.Type == AnimalType.Raven)
        {
            // in 'AnimalBaseState.cs' (Will be implemented in the future)
            ActivateNonMalbersLayerWeight(0, 1);
            ActivateNonMalbersLayerWeight(1, 1);
            ActivateNonMalbersLayerWeight(2, 1);
            ActivateNonMalbersLayerWeight(3, 1);
            ActivateNonMalbersLayerWeight(4, 1);
            ActivateNonMalbersLayerWeight(5, 0);
        }

        // Remove the Patrol Path for Mounted Animals
        AnimalMountManager.OnAnimalMounted -= RemoveAnimalPatrolPath;
    }

    private void PlayCorrectBlendTreeHash()
    {
        if (stateMachine.ThisAnimal.Type == AnimalType.Horse)
        {
            stateMachine.Animator.CrossFadeInFixedTime(HorseFreeLookBlendTreeHash, stateMachine.CrossFadeDuration);
        }

        if (stateMachine.ThisAnimal.Type == AnimalType.Raven)
        {
            stateMachine.Animator.CrossFadeInFixedTime(RavenFreeLookBlendTreeHash, stateMachine.CrossFadeDuration);
        }
    }

    private void SetCorrectFloatValue(float deltaTime) 
    {
        if (stateMachine.ThisAnimal.Type == AnimalType.Horse) 
        {
            stateMachine.Animator.SetFloat(FreeLookSpeedHash, 0.7f /* grossSpeed / stateMachine.MovementSpeed */, stateMachine.AnimatorDampTime, deltaTime); // blend tree plays the horse walking animation
        }

        if (stateMachine.ThisAnimal.Type == AnimalType.Raven) 
        {
            stateMachine.Animator.SetFloat(FreeLookSpeedHash, 0.5f, stateMachine.AnimatorDampTime, deltaTime);
        }
    }

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

    private void MoveToWayPoint(float deltaTime)
    {
        // turn on the NavMesh if it's not turned on
        if (!stateMachine.Agent.enabled) stateMachine.Agent.enabled = true;

        // TEST - DELETE IF FAILED (AVOIDS JITTERING WHILST PATROLLING OF THE HORSE)
        stateMachine.Agent.updatePosition = false;
        stateMachine.Agent.updateRotation = false;

        Vector3 direction = stateMachine.Agent.desiredVelocity;

        // TEST - DELETE IF FAILED (AVOIDS JITTERING WHILST PATROLLING OF THE HORSE)
        direction.y = 0;
        direction.Normalize();

        Move(direction * movementSpeed, deltaTime);
        stateMachine.Agent.velocity = stateMachine.CharacterController.velocity;
        stateMachine.Agent.nextPosition = stateMachine.transform.position;

        // get the destination, or you will struggle to find anywhere to go
        stateMachine.Agent.destination = targetPatrolPoint;
    }

    private void FaceMovementDirection(Vector3 forward, float deltaTime)
    {
        if (forward == Vector3.zero) return;
        Quaternion desiredRotation = Quaternion.LookRotation(forward, Vector3.up);
        stateMachine.transform.rotation = Quaternion.Slerp(stateMachine.transform.rotation, desiredRotation, stateMachine.FreeLookRotationSpeed * deltaTime);
    }

    private void RemoveAnimalPatrolPath()
    {
        stateMachine.StartCoroutine(RemovePatrolPathDelayed(0.05f));
    }

    private IEnumerator RemovePatrolPathDelayed(float delay)
    {
        yield return new WaitForSeconds(delay);
        if (stateMachine.PlayerOnAnimalStateMachine.GetAnimal().GetUniqueID() == stateMachine.GetComponent<Animal>().GetUniqueID())
        {
            stateMachine.SetPatrolPathHolder(stateMachine.PatrolPath);
            stateMachine.AssignPatrolPath(null);
            stateMachine.SwitchState(new AnimalIdleState(stateMachine));
        }
    }
}

I’d take the targeting state for reference, but the problem is that it’s AI-Controlled, so ‘InputReader.cs’ is out of the story for the time being (I let Malbers’ code control the cases when the player is mounted on an animal, since it seems to be clean enough to make it till the end, and it honestly looks like a lot of work)

If I figure it out I’ll let you know

[IGNORE. I SOLVED THIS, AND THEN REALIZED IT’S NOT THE PROBLEM]

Something else I noticed, which is a bigger problem for the time being, is that Malbers’ animations in the 0th animation layer keep playing even when I deactivated the layer. I recently learned Unity never deactivates the weight of the 0th layer, and as a result, now I have Malbers’ animations and mine playing simultaneously, and that’s… just… not acceptable to me

The effect is visual as well, which is what troubles me more (as in, the bird is walking 2 units above the floor thanks to the animation collision)

Any idea how to turn off any sort of animations playing on the 0th layer when I enter a state that demands a different animation layer? The code below doesn’t exactly do the trick for the 0th layer:

        if (stateMachine.ThisAnimal.Type == AnimalType.Raven)
        {
            // in 'AnimalBaseState.cs' (Will be implemented in the future)
            ActivateNonMalbersLayerWeight(0, 1);
            ActivateNonMalbersLayerWeight(1, 1);
            ActivateNonMalbersLayerWeight(2, 1);
            ActivateNonMalbersLayerWeight(3, 1);
            ActivateNonMalbersLayerWeight(4, 1);
            ActivateNonMalbersLayerWeight(5, 0);
        }

I tried creating a layer on top of Malbers’, and use that to deactivate his layers when needed, but… it caused a lot of unwanted issues


Edit: I came up with the solution of ‘boolean gates’, something to block the transition for when the time is not right.

First, include a new boolean parameter in your parameters for your animations, and obviously place them around on the animation transition states (if you got any. Malbers has some, fortunately). Then in-code, when you enter the state, turn this gate to false. When you exit the state, turn this gate back to true, so you don’t mess with the logic

and as it turns out, this wasn’t the problem. Feel free to ignore this one :slight_smile:

The problem was that my character controller was a little too big it collided with the ground, causing this major headache

Still, it’s great to learn something new by accident along the way

Correct. Any bone that is not overriden in higher layer by an Avatar mask will default to Layer1 and be played in the current state of Layer 1

Swap animators? AnimationOverrides? Ensure that a higher layer’s mask covers everything?

Just on time as I fix the damage bug with the Raven. MalberS really has a lot of stuff going on there, and it was a series of debuggers until I realized this thing was causing the problems:

(Apparently that’s how MalberS gets his bird to pick stuff off the ground. About 50% of arrow hits were not registered as damage for some reason because this thing was in the way, although the box collider is a trigger. When I create my own variant, I’ll ensure this doesn’t get in the way)

I ended up with a boolean trigger to fix the problem, and then learned that it wasn’t the cause. A higher layer was not possible, as it would disable so many functionalities that I needed to work, that it didn’t sound like it was worth the headache

The problem with swapping animator and animation overrides is that I’d have to modify Malbers’ code, which, as you can guess, I’m trying to minimize to avoid as many unexpected issues, as possible (besides, I don’t know where to go in there)

Whilst the animation do play in the background, they don’t seem to be troubling me, so I let it go for the time being so that I don’t create an absolute mess down the line. The last thing I want right now is unwanted functionalities over the place

Anyway, we can safely return to this problem, which I am about to start working on (after I create an agent variant for the Raven, that is). I spent all day fixing other minor bugs that I didn’t know exist until now, mainly to do with Malbers’ stuff

Hello @Brian_Trotter I’m currently developing the chasing and attacking state of my animals, but I have a bit of a weird problem with the chasing state… When the animals are chasing me to attack me, they rotate like absolute crazy for some reason, and I’m not sure why

Can you please have a look and see if you can identify any abnormal code in here? Thank you :slight_smile:

using UnityEngine;
using RPG.Animals;
using RPG.States.Animals;
using UnityEngine.AI;

public class AnimalChasingState : AnimalBaseState
{
    private float attackingRange;

    public AnimalChasingState(AnimalStateMachine stateMachine) : base(stateMachine) {}

    public override void Enter()
    {
        PlayCorrectBlendTreeHash();
        attackingRange = stateMachine.AttackingRange;
        attackingRange *= attackingRange;

        if (stateMachine.ThisAnimal.Type == AnimalType.Horse) 
        {
            ActivateNonMalbersLayerWeight(0, 0);
            ActivateNonMalbersLayerWeight(1, 0);
            ActivateNonMalbersLayerWeight(2, 0);
            ActivateNonMalbersLayerWeight(3, 0);
            ActivateNonMalbersLayerWeight(4, 1);
        }

        if (stateMachine.ThisAnimal.Type == AnimalType.Raven)
        {
            ActivateNonMalbersLayerWeight(0, 0);
            ActivateNonMalbersLayerWeight(1, 0);
            ActivateNonMalbersLayerWeight(2, 0);
            ActivateNonMalbersLayerWeight(3, 0);
            ActivateNonMalbersLayerWeight(4, 0);
            ActivateNonMalbersLayerWeight(5, 1);
        }
    }

    public override void Tick(float deltaTime)
    {
        if (!IsInChaseRange() && !IsAggrevated())
        {
            // IsInChaseRange either gets an enemy that hit this NPC from a distance,
            // or the player if he's within 'PlayerChasingRangedSquared'

            // IsAggrevated confirms you have both an 'AnimalAggro' cooldown timer,
            // and a last attacker (i.e: you're mad at someone)

            // stateMachine.TriggerOnPlayerOutOfChaseRange();
            stateMachine.SwitchState(new AnimalIdleState(stateMachine));
            return;
        }

        // Last Position of this NPC
        Vector3 lastPosition = stateMachine.transform.position;

        if (IsInChaseRange())
        {
            if (stateMachine.GetLastAttacker() != null) // This will always return true, because if you got here, it means you got a LastAttacker from 'IsInChaseRange()' confirmations (double check, and consider deleting it)
            {
                if (!HasLineOfSight()) 
                {
                    if (CanMoveTo(stateMachine.GetLastAttacker().transform.position)) 
                    {
                        stateMachine.Animator.SetFloat(FreeLookSpeedHash, GetCorrectFloatSpeedValue(), stateMachine.AnimatorDampTime, deltaTime);
                        Vector3 movementDirection = stateMachine.Agent.desiredVelocity.normalized;
                        FaceMovementDirection(movementDirection, deltaTime);
                        MoveToTarget(deltaTime, lastPosition);
                        return;
                    }
                }
            }

        if (!stateMachine.CooldownTokenManager.HasCooldown("AnimalAttack")) // DOUBLE CHECK IF THIS HAS A "!" OR NOT
        {
            stateMachine.SwitchState(new AnimalAttackingState(stateMachine));
            return;
        }
        else 
        {
            Move(deltaTime);
            stateMachine.Animator.SetFloat(FreeLookSpeedHash, 0f); // DOUBLE CHECK IF IT SHOULD BE A ZERO OR A ONE HERE, BECAUSE YOU DO HAVE RARE OCCASIONS WHILST CHASING WHERE THE NPC COMES SLIDING
            if (stateMachine.GetLastAttacker() == null) 
            {
                stateMachine.SwitchState(new AnimalIdleState(stateMachine));
            }
            else 
            {
                FaceTarget(stateMachine.GetLastAttacker().transform.position, deltaTime);
            }
            return;
        }
        }
        else 
        {
            MoveToTarget(deltaTime, lastPosition);
        }

        UpdateMovementAnimation(deltaTime, lastPosition);
    }

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

        if (stateMachine.ThisAnimal.Type == AnimalType.Horse)
        {
            ActivateNonMalbersLayerWeight(0, 1);
            ActivateNonMalbersLayerWeight(1, 1);
            ActivateNonMalbersLayerWeight(2, 1);
            ActivateNonMalbersLayerWeight(3, 1);
            ActivateNonMalbersLayerWeight(4, 0);
        }

        if (stateMachine.ThisAnimal.Type == AnimalType.Raven)
        {
            ActivateNonMalbersLayerWeight(0, 1);
            ActivateNonMalbersLayerWeight(1, 1);
            ActivateNonMalbersLayerWeight(2, 1);
            ActivateNonMalbersLayerWeight(3, 1);
            ActivateNonMalbersLayerWeight(4, 1);
            ActivateNonMalbersLayerWeight(5, 0);
        }
    }

    private void PlayCorrectBlendTreeHash()
    {
        if (stateMachine.ThisAnimal.Type == AnimalType.Horse)
        {
            stateMachine.Animator.CrossFadeInFixedTime(HorseFreeLookBlendTreeHash, stateMachine.CrossFadeDuration);
        }

        if (stateMachine.ThisAnimal.Type == AnimalType.Raven)
        {
            stateMachine.Animator.CrossFadeInFixedTime(RavenFreeLookBlendTreeHash, stateMachine.CrossFadeDuration);
        }
    }

    private bool HasLineOfSight()
    {
        // Raycast, from the front of the NPC, aiming to the direction of the Last Attacker
        Ray ray = new Ray(stateMachine.transform.position + Vector3.up * 0.5f, stateMachine.GetLastAttacker().transform.position - stateMachine.transform.position);

        // If the raycast hits something, from the position of the NPC, to the position of the Last Attacker, of LayerMask "Obstacle" (i.e: an Obstacle has been found), return false
        // (i.e: you found an obstacle, so you didn't detect the Last Attacker)
        if (Physics.Raycast(ray, out RaycastHit hit, Vector3.Distance(stateMachine.transform.position, stateMachine.LastAttacker.transform.position), stateMachine.LayerMask))
        {
            return false;
        }

        // If the 'if' statement above is not met (i.e: you made it here), it means you're in the clear to aim for the player
        return true;
    }

    private bool CanMoveTo(Vector3 destination)
    {
        var path = new NavMeshPath();
        var hasPath = NavMesh.CalculatePath(stateMachine.transform.position, destination, NavMesh.AllAreas, path);
        if (!hasPath) return false;
        if (path.status != NavMeshPathStatus.PathComplete) return false;
        if (GetPathLength(path) > stateMachine.maxNavPathLength) return false;
        return true;
    }

    private float GetPathLength(NavMeshPath path) // ASK AGAIN HOW EXACTLY DOES THIS WORK...!
    {
        float total = 0;
        if (path.corners.Length < 2) return total; // No corners, short path

        for (int i = 0; i < path.corners.Length - 1; i++) 
        {
            // Total corners
            total += Vector3.Distance(path.corners[i], path.corners[i + 1]);
        }

        return total;
    }

    private void FaceMovementDirection(Vector3 forward, float deltaTime)
    {
        if (forward == Vector3.zero) return; // No difference from your facing direction, so just aim for that
        Quaternion desiredRotation = Quaternion.LookRotation(forward, Vector3.up);
        stateMachine.transform.rotation = Quaternion.Slerp(stateMachine.transform.rotation, desiredRotation, stateMachine.FreeLookRotationSpeed * deltaTime);
    }

    private void MoveToTarget(float deltaTime, Vector3 lastPosition) 
    {
        if (!stateMachine.Agent.enabled) stateMachine.Agent.enabled = true;

        Vector3 direction = stateMachine.Agent.desiredVelocity;

        Move(direction * stateMachine.MovementSpeed, deltaTime);
        stateMachine.Agent.velocity = direction;
        stateMachine.Agent.nextPosition = stateMachine.transform.position;

        stateMachine.Agent.destination = stateMachine.LastAttacker != null ? stateMachine.LastAttacker.transform.position : lastPosition;
    }

    private void UpdateMovementAnimation(float deltaTime, Vector3 lastPosition) 
    {
        Vector3 deltaMovement = lastPosition - stateMachine.transform.position;
        float deltaMagnitude = deltaMovement.magnitude;

        if (deltaMagnitude > 0) 
        {
            FaceTarget(stateMachine.transform.position - deltaMovement, deltaTime);
            float grossSpeed = deltaMagnitude / deltaTime;
            stateMachine.Animator.SetFloat(FreeLookSpeedHash, GetCorrectFloatSpeedValue() /* grossSpeed / stateMachine.MovementSpeed */, stateMachine.AnimatorDampTime, deltaTime);
        }
        else 
        {
            FaceTarget(stateMachine.GetLastAttacker().transform.position, deltaTime);
            stateMachine.Animator.SetFloat(FreeLookSpeedHash, 0f);
        }
    }

    private float GetCorrectFloatSpeedValue() 
    {
        if (stateMachine.ThisAnimal.Type == AnimalType.Horse) 
        {
            return 0.7f;
        }

        // OTHER ANIMALS (EXCEPT RAVEN, IT'S IN THE 'else' HERE) CAN BE DONE HERE

        else return 0.5f;
    }
}

(I skipped the strafing for the time being)

Edit: Found it… that pesky ‘FaceTarget()’ line…:

    private void UpdateMovementAnimation(float deltaTime, Vector3 lastPosition) 
    {
        Vector3 deltaMovement = lastPosition - stateMachine.transform.position;
        float deltaMagnitude = deltaMovement.magnitude;

        if (deltaMagnitude > 0) 
        {
            // FaceTarget(stateMachine.GetLastAttacker().transform.position, deltaTime); // REPLACED BY 'FaceMovementDirection' BELOW
            Vector3 movementDirection = stateMachine.Agent.desiredVelocity.normalized;
            FaceMovementDirection(movementDirection, deltaTime);
            float grossSpeed = deltaMagnitude / deltaTime;
            stateMachine.Animator.SetFloat(FreeLookSpeedHash, GetCorrectFloatSpeedValue() /* grossSpeed / stateMachine.MovementSpeed */, stateMachine.AnimatorDampTime, deltaTime);
        }
        else 
        {
            FaceTarget(stateMachine.GetLastAttacker().transform.position, deltaTime);
            stateMachine.Animator.SetFloat(FreeLookSpeedHash, 0f);
        }
    }

At first I included “.GetLastAttacker()” to help out, then I swapped it out for ‘FaceMovementDirection()’ because facing the target and moving sideways was a little creepy

I have another problem though @Brian_Trotter - as I’m trying to integrate damage done by the Raven to the Player when an attack animation is done, the Raven is quite a big bird, and frankly speaking, the character controller on the player is quite well-fit for the player’s size, which means that sometimes the Raven will pass through the character controller and aim to hit in the air, dealing no damage to the player

One solution I can think of, for this, is to just multiply the character controller by a 2-3x factor when the damage is about to be dealt, and then reduce the radius to it’s original value, but… that didn’t work out, mainly because it not only was a small headache to program, but also because it looked exceptionally unreal that the player flies for a second and then gets grounded again. Are there any other solutions?

TLDR: The Raven’s big size means sometimes it’ll strike the player beyond the position of the character controller, making it not hit him most of the time

This will have to be solved ASAP, otherwise countless animations will fail at registering a hit :sweat_smile:


Edit: I fixed that problem too. I went to Malbers’ code, and made the collision between characters (player, NPCs, etc) and the animals (Raven, Horse, etc) be ignored when the player has mounted the animal, and unignored when the player has dismounted the animal, as follows:

        public virtual void MountAnimal()
        {
            if (!CanMount || !enabled) return; 

            // TEST - DELETE IF FAILED
            int animalLayer = LayerMask.NameToLayer("Animal");
            int charactersLayer = LayerMask.NameToLayer("Characters");
            Physics.IgnoreLayerCollision(animalLayer, charactersLayer, true);
            Debug.Log($"[MountAnimal] Ignoring collisions between 'Animal' layer (Layer {animalLayer}) and 'Character' Layer (Layer {charactersLayer})");

            if (!Montura.InstantMount)                                           //If is instant Mount play it      
            {
                Debbuging("Mount Animal", "cyan");
                Mounted = true;                                                  //Update MountSide Parameter In the Animator
                SetMountSide(MountTrigger.MountID);                              //Update MountSide Parameter In the Animator
                // Anim?.Play(MountTrigger.MountAnimation, MountLayerIndex);      //Play the Mounting Animations
            }
            else
            {
                Debbuging("Instant Mount", "cyan");

                Anim?.Play(Montura.MountIdle, MountLayerIndex);                //Ingore the Mounting Animations
                Anim?.Update(0);                             //Update the Animator ????

                Start_Mounting();
                End_Mounting();

                // ADDED BY BAHAA (next 3 lines, that is):
                AnimalMountManager.isOnAnimalMount = true;
                AnimalMountManager.InvokeOnAnimalMounted();
                Debug.Log($"isOnAnimalMount: {AnimalMountManager.isOnAnimalMount}");

                Montura.Rider.transform.SetParent(Montura.transform);
            }
        }

        public virtual void DismountAnimal()
        {
            if (!CanDismount || !enabled) return;

            Debbuging("Dismount Animal", "cyan");

            Debug.Log($"Animal Dismount Called");

            Montura.Mounted = Mounted = false;                                  //Unmount the Animal
            MountTrigger = GetDismountTrigger();

            SetMountSide(MountTrigger.DismountID);                               //Update MountSide Parameter In the Animator

            if (Montura.InstantDismount)                                         //Use for Instant Dismount
            {
                Anim.Play(EmptyHash, MountLayerIndex);
                SetMountSide(0);                                                //Update MountSide Parameter In the Animator

                Start_Dismounting();

                var MT = MountTrigger;
                End_Dismounting();
                RiderRoot.position = MT.transform.position + (MT.transform.forward * -0.2f);   //Move the rider directly to the mounttrigger
                RiderRoot.rotation = MT.transform.rotation;

                // ADDED BY BAHAA (next 2 lines, that is):
                AnimalMountManager.isOnAnimalMount = false;
                AnimalMountManager.InvokeOnAnimalDismounted();
                Debug.Log($"isOnAnimalMount: {AnimalMountManager.isOnAnimalMount}");
                // Somewhere in Bahaa's code, I told it to send the player to 'PlayerAggroGroup' on dismount, so alls good

                // TEST - DELETE IF FAILED
                int animalLayer = LayerMask.NameToLayer("Animal");
                int charactersLayer = LayerMask.NameToLayer("Characters");
                Physics.IgnoreLayerCollision(animalLayer, charactersLayer, false);
                Debug.Log($"[DismountAnimal] Re-enabling collisions between 'Animal' layer (Layer {animalLayer}) and 'Character' layer (Layer {charactersLayer})");
            }
        }

This way, they can collide and don’t end up entering each others’ bodies like they used to, fixing both my long-term collision problem, and my short-term fighting problem, which was caused because there was no collision between them

(Initially, I forced them to be permanently off through the Physics Matrix, because the character controller would push the animals badly into the ground and just ruin the entire experience… but now that I know better, I turned them on and became smart about this)

Just make sure not to make a spelling mistake, because this one took me a while to figure out…

I can still see a problem, though. When I get to the stage of making NPCs mount animals (I’m crazy, I know!), I can see every single NPC getting their collision layers ignored too

How do I make sure only the script holder, and not every script holder in the scene, gets their collision ignored?

And now I have a new problem. When the Player is killed by an animal, the player does not transition to death state for some reason, 100% of the time, and naturally no respawning either, and it’s driving me nuts as of why (the enemy does not have this problem. In other words, if the player gets killed by an NPC, there’s no problems with that. If the Player is killed by an animal, the bug happens)

(along with an ‘Animal does not chase down the NPC that attacked them, if it’s not the player’ problem, but I’ll get to that later)

I’m not sure if this is relevant or not, but here it goes anyway. For the Horse, I haven’t done any tests yet, but for the Raven, the attacks are done either through the beak of the bird, or it’s legs. I modified ‘Fighter.TryHit’ a little to adjust for accepting legs and beak, but I’m not 100% sure if that’s the reason of the failure of the death state of the player or not, so here’s the code. Would appreciate any sort of help @Brian_Trotter :slight_smile:

    // For the brand new Attack Array System (part of the Point-And-Click -> Third Person System implementation)
    // this function is called by animations to perform hits, based on the hand/foot/weapon, doing the hit:
    private void TryHit(int slot)
    {
        // if no current attack (or follow up) exists, return null:
        if (currentAttack == null) 
        {
            if (AnimalMountManager.isOnAnimalMount) 
            {
                // Assign the correct 'currentAttack' for the animal mount:
                currentAttack = GetCurrentWeaponConfig().OnAnimalAttacks[0];
            }
            else 
            {
                Debug.Log($"TryHit is called, but current attack is null");
                return;
            }
        }

        Debug.Log($"TryHit has been called");

        // radius of the damage of the weapon:
        float damageRadius = 0.5f;

        // To trigger this animation, the event in the animation takes the 1 and 2 values in 'Int' slot
        Vector3 transformPoint;
        switch (slot)
        {
            case 0: 
            transformPoint = currentWeapon.value.DamagePoint;   // weapon damage
            if (AnimalMountManager.isOnAnimalMount) // for cases when you need a larger hit radius whilst attacking by mounting
            {
               damageRadius = currentWeapon.value.DamageRadius * 2.0f; // you'll need a bigger radius to strike others whilst on the run on a mount
            }
            else damageRadius = currentWeapon.value.DamageRadius; // weapon damage radius
            break;
            case 1: transformPoint = rightHandTransform.position;   // for right-handed cases
            break;
            case 2: transformPoint = leftHandTransform.position;    // for left-handed cases
            break;
            case 3: 
            if (GetComponent<PlayerStateMachine>()) return; // the player's Parrying is handled in 'ShieldTriggerEnter.cs'
            if (GetCurrentShieldConfig() != null && GetComponent<EnemyStateMachine>() != null)
            {
                // use this call on the Animation Event Line on Shield Parrying animations
                Debug.Log($"{GetCurrentShieldConfig().name} has been found in shield hand");
                transformPoint = currentEquippedShield.value.DamagePoint;
                damageRadius = currentEquippedShield.value.DamageRadius;
            }
            else
            {
                transformPoint = rightHandTransform.position; // no shield? Just use your rightHandTransform
            }
            break;
            case 4: transformPoint = rightLegTransform.position;
            break;
            case 5: transformPoint = leftLegTransform.position;
            break;
            case 6:
            transformPoint = beakTransform.position; // The beak of the animal, a Raven for example or something (OPTIONAL)
            break;
            default: transformPoint = rightHandTransform.position;  // for cases when things make no sense
            break;
        }
        Debug.Log($"Attacking with slot {slot}, position {transformPoint}");

        // This list ensures that the health component of the player is accessed once only. This problem
        // came up when I introduced a second Capsule Collider on my player, so he can mount animals using
        // Malbers' Scripts (basically, 2 colliders on the player = 2x damage dealt to him... 
        // we needed to get rid of that by ensuring the health component is accessed once, through this list):
        List<Health> alreadyHit = new List<Health>();

            foreach (Collider other in Physics.OverlapSphere(transformPoint, damageRadius))
            {
                if (other.gameObject == gameObject) continue;   // don't hit yourself

                // Stop the player from hitting the animal he's currently mounting:
                if (AnimalMountManager.isOnAnimalMount && this.gameObject.CompareTag("Player"))
                {
                    var mountedAnimal = GetComponent<PlayerOnAnimalStateMachine>()?.GetAnimal();
                    if (other.gameObject == mountedAnimal.gameObject)
                    {
                        // If you got this far, it means the player is mounted to an animal, and he's
                        // trying to hit his mount with a melee weapon, which should be ignored!
                        continue;
                    }
                }

                if (other.TryGetComponent(out Health otherHealth) && !otherHealth.IsDead())
                {
                    // If one of the players' colliders (he has a Character Controller and a Capsule Collider
                    // on him) is hit, don't take damage again (fixes the 2 colliders-on-player-taking-damage problem):
                    if (alreadyHit.Contains(otherHealth)) continue;
                    // if you didn't take a hit yet, go ahead and do so now:
                    alreadyHit.Add(otherHealth);
                    
                    float damage = GetDamage();
                    damage *= currentAttack.DamageModifier;

                    // player/enemy
                    if (other.TryGetComponent(out BaseStats otherBaseStats))
                    {
                        float defence;
                        // if its the player (the only in-game character with a skillStore)
                        if (other.TryGetComponent(out SkillStore skillStore))
                        {
                            Debug.Log($"{other.gameObject.name} has a SkillStore");
                            defence = otherBaseStats.GetStatBySpecifiedLevel(Stat.Defence, skillStore.GetSkillLevel(Skill.Defence));
                        }
                        // the enemy
                        else 
                        {
                            if (other.GetComponent<Fighter>().GetCurrentShieldConfig() != null)
                            {
                                Debug.Log($"{other.gameObject.name} is an enemy with a shield");
                                defence = otherBaseStats.GetStat(Stat.Defence) + (other.GetComponent<Fighter>().GetCurrentShieldConfig().GetDefenseBonus() * (1 + other.GetComponent<Fighter>().GetCurrentShieldConfig().GetPercentageBonus()/100));
                            }
                            else
                            {
                                Debug.Log($"{other.gameObject.name} has no shield");
                                defence = otherBaseStats.GetStat(Stat.Defence);
                            }
                        }
                        damage /= 1 + defence/damage; // Replaced by a 'TEST' below

                        // Randomize the Damage:
                        // damage *= UnityEngine.Random.Range(0f, 1.25f);
                        // if (UnityEngine.Random.Range(0,100) > 95) damage *= 2.0f;
                    }

                    // if the player is invulnerable, ignore trying to damage him, and continue to the next cycle of damaging enemies in the radius, which was hurt by the sword:
                    if (other.CompareTag("Player") && other.GetComponent<Health>().GetInvulnerable())
                    {
                        continue; // later on, integrate logic here to accept a percentage of damage, before continuing to the next Foreach loop
                    }

                    // (TEMP - GetComponent<Health>().IsDead()) Temporarily here, to ensure ghost enemies deal no damage and mess with the NPC hunting for enemies system:
                    if (otherHealth.IsDead() || GetComponent<Health>().IsDead())
                    {
                        return;
                    }

                    otherHealth.TakeDamage(gameObject, damage, currentWeaponConfig.GetSkill());
                    TryApplyHitForce(other, transform.position);
                }
            }
        }

and a screenshot of the hierarchy of the Raven’s fighter.cs script (in case the differences in there are of any help):

Through changing the Physics layer collision matrix? You don’t. It applies to everything on that layer. What you need instead is check to see if the NPC is mounted on the animal before applying damage.

You might consider turning gravity off in the ForceReceiver (or outright ignoring the ForceReceiver’s Y component when in a Riding state… This is tricky, because you are trying to force the integration of two systems that were designed independently and use different theory of motion.

This makes no sense at all… as going to Death State (and eventually respawning) should be completely independent of the source of the damage).

The culprit behind this is likely something in your TakeDamage in Health… it’s registering the damage, but for some reason not calling OnDeath which is what the StateMachine uses to transition to the Death state.

For that one, surprise surprise, it’s because of code I wrote independently. The code was structured for Enemy State Machines, and the introduction of Animal State Machines has not been considered yet. Here’s the code block that’s responsible for this last bug (ahh… when I finally decide to have a look at the debugger):

            // if only the enemy hitting the player is left, get all available player Allies to aim for that enemy:
            if (instigator.GetComponent<EnemyStateMachine>().GetAggroGroup() != null && instigator.GetComponent<EnemyStateMachine>().GetAggroGroup().GetGroupMembers().Count == 1)
            {
                foreach (EnemyStateMachine playerAllyEnemy in this.GetAggroGroup().GetGroupMembers().Where(playerAllyEnemy => playerAllyEnemy != null && playerAllyEnemy.GetOpponent() == null && !playerAllyEnemy.GetHasOpponent()))
                {
                    if (playerAllyEnemy != instigator.GetComponent<EnemyStateMachine>()) // This if statement fixed a 3-month old 'Self-Attack' bug that's been bothering me because of how hard it was to replicate
                    {
                        playerAllyEnemy.SetHostile(true, instigator.gameObject);
                        playerAllyEnemy.SetHasOpponent(true);
                        playerAllyEnemy.SetOpponent(instigator.gameObject);
                    }
                }
            }

The ‘if’ statement line is the source of the bug. The code was written for Enemy State Machines, and has not been optimized for Animal State Machines just yet.

If you recall, this exact same block of code was also responsible once for my Player getting attacked by his allies, because they are told to attack whoever last hit the player (which, if he’s taking fall damage, he’s the one hurting himself, so now his allies are attacking him :rofl:). Unfortunately, ignoring the damage because an animal has done the damage right now won’t work for this scenario. I have to be smart about this!

Check for NPCs, got it!

Not every problem requires a complex solution. Some solutions are significantly easier than we expect, and cleaner as well. This is something I eventually learned due to my huge fear of heavily modifying code that doesn’t belong to me :smiley: - if I was solely programming this game, trust me this wouldn’t have ever happened. Manipulating me into complex and wrong solutions would’ve been extremely easy!

In the end, like I said, just get the characters and animals collision layer to ignore each other when the player mounts an animal, and that fixes a ton of problems!

I set the damage for falls to be null (and of course null check this before attempting to assign experience or in your case blame). QED.

Only if the player is the only one on the characters layer.

I completely forgot about that, and went ahead and assigned damage for falls with some complex code in ‘PlayerFallingState.cs’ (I don’t think you made a tutorial for that, but yeah… I created one and started making ways around problems for that). Right now it’s a rough algorithm, because it calculates based on addition rather than as a percentage of the total health, which, on the long term, will make a lot more sense

What’s QED?

I will eventually strengthen this part of the code. For the time being I’m still making sure animals can properly fight the player and other last attackers. Right now the goal is to almost eliminate the probability of the Raven having missed hits because of the lack of a collider, but also ensure that the animals don’t go crazy because of collider collision when they’re mounted


Anyway, here’s the solution to my if statement bug from above:

            // if only the enemy hitting the player is left, get all available player Allies to aim for that enemy:
            if (instigator.GetComponent<EnemyStateMachine>() != null /* <-- check for enemies first, you don't want animals involved in this */ && instigator.GetComponent<EnemyStateMachine>().GetAggroGroup() != null && instigator.GetComponent<EnemyStateMachine>().GetAggroGroup().GetGroupMembers().Count == 1)
            {
                foreach (EnemyStateMachine playerAllyEnemy in this.GetAggroGroup().GetGroupMembers().Where(playerAllyEnemy => playerAllyEnemy != null && playerAllyEnemy.GetOpponent() == null && !playerAllyEnemy.GetHasOpponent()))
                {
                    if (playerAllyEnemy != instigator.GetComponent<EnemyStateMachine>()) // This if statement fixed a 3-month old 'Self-Attack' bug that's been bothering me because of how hard it was to replicate
                    {
                        playerAllyEnemy.SetHostile(true, instigator.gameObject);
                        playerAllyEnemy.SetHasOpponent(true);
                        playerAllyEnemy.SetOpponent(instigator.gameObject);
                    }
                }
            }

(All I did was check that the enemy state machine was not null)

I didn’t, but there’s really no change from the Third Person State for Jumping, Falling, or Landing

It’s one of those old folk sayings… at least in England and America “Quite Easily Done”

Those mounted Malbers don’t seem worth the trouble to me.

First of all, good to hear from you again :smiley:

I fixed a ton of bugs with getting the damage when falling, but still… somehow some of them escaped my radar, and soon enough I will start investigating them again. For the time being I’m just working on the last step for equestrianism

My biggest (and honestly, only remaining bug) is that if you hit the right mouse button a lot when mounting an animal, the moment you dismount the animal, you take a significant ton of damage, and I still don’t know why

That one was a huge headache to integrate, because getting Malbers’ code and mine to communicate was nearly impossible. Again, Mediators saved the day, combined with smart tactics to get the correct information, and my recent setup of Unique Identifiers for the animals

But in the end, it all worked for the greater good (but the mathematical formula for reducing probability of failures will be quite hard to figure out, but I can fix it. xD)

I had to google it, it gave me a weird result, but yeah it’s good to know :slight_smile:

For that one, I managed to code the system to get them to ignore each other when mounted. I just need to introduce a small dismount delay, and ensure dismounting is only on the sides of the animal, because right now Malbers also has an option for “on-top-of” the animal, which has a small chance (but it exists) of burying an animal underground

The problem is, making flying states will be an absolute nightmare, because his systems are incredibly complex, but hopefully that can be worked out too (I’ll get to that on the proper forum link)

Honestly speaking though, you should see how lively the game got all of a sudden with interactive animals. Hands-down, it’s really worth the headache (the new Equestrianism skill is a game changer in my opinion. Players who want to fly a dragon now will really have a reason to grind, because they’ll want to blow up places with better animals, evade areas quicker, etc… depending on the strength of the animal!)

Privacy & Terms