NavMeshAgent obstacle avoidance whilst Patrolling

will do in a bit, for now I’m fixing a bug that’s unlikely to find a solution neither on this side, or on Malbers’ side… so, I’ll do it myself

I’m never bored with this project. Bugs are coming out of every single angle lol

Last hour I fixed like 3 fall-damage-related bugs because of a system I created not too long ago, xD

Maybe you should stop adding new stuff and focus on getting everything you already have working completely, correctly. :woozy_face:

I have a solid rule of not adding new stuff until the previous mechanics work properly. The bugs I have are because of the new system, xD

Unless a tester finds something I missed out on, that is

If I’m fixing old bugs in the midst of a new system, it just means this bug wasn’t caught early on

Because it couldn’t find a NavMeshSurface baked for Horses

Floors have nothing to do with it.
One Agent Type == One Surface required.

You can even put multiple Surfaces on the same game object though I don’t recommend it.

No, one Surface per Agent Type will suffice.

image

1 Like

and then you can safely duplicate that surface around? Arghh this is quite hard for me to explain… :sweat_smile:

For now I’m just fixing the mini bugs that I find here and there

(And yeah I caught a rare bug not so long ago with the death, probably something to do with my 200-line long ‘EnemyStateMachine.OnTakenHit()’, where the enemy fails to die for some reason (A while ago, I made a death-proof damage system, where dead enemies do no damage, when I was tracing an even worse bug like 7 months ago. I fixed that back then, but kept the death-proof solution in ‘Fighter.cs’ (it was 2 lines of code). So this bug just keeps him playing hit animations but doing no damage, it’s just annoying to look at), but it’s so rare it’s almost impossible to replicate, so I quietly put it under the carpet, xD - so if santa doesn’t send me a gift this year, you can guess why)

No. You simply BAKE that surface. Here’s an example:


In this example, I set the Horse radius to 1.5 (that’s probably a bit excessive, but it demonstrates what I’m after.
Each surface has a type. Then each surface is baked. The surface in light blue is the Humanoid surface. . It’s closer to objects, and is considered by the industry to be just right for an average height and weight human.
The second surface in bolder blue is the Horse’s NavMesh. You’ll have to tweak those settings, but Horses can’t walk everywhere Humans can walk. Those tight corners you’ve worried about while on the Humanoid setting are because the Horse is bigger than a human and comes unreasonably close to the edge (because a NavMesh does exactly that, finds the most direct route when possible).

I love my GTX1650 (desktop version). It’s not ideal for Unreal Engine, but it’s a whole lot better than the Intel chipset that came on the machine.

I tweaked the horse quite a bit. The current downfall I have, is that the horsey will take a lengthy path for something that seems relatively straightforward

BUT… He can now climb some quite steep paths with my current values, so that’s a big win for me, unless I got the some guards or something patrolling into the city. We’ll investigate this further down the line, for now I got 2 major bugs with the horse and Malbers’ systems that need to be fixed, probably tomorrow because I’m falling asleep now (eventually I started sleeping from 9PM-5AM. I’m 56 minutes late now, xD)

  1. Turn off my systems when the horse is whistled for (Malbers’ Code will take care of that part) - ACTUALLY, I’ll delete the Patrol Paths for the animals once you drive them. You drive them, (if they don’t hold up a fight - that will rely on your equestrianism skill) you own them, simple

That first one will be a little complex, but I’m sure I can think of a way to do it :slight_smile:

  1. I have a bug where all animals stop moving, because they all discard their patrol paths, when the player rides an animal. I’ll fix that after the one above

That second one needs a fix, though

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?

Privacy & Terms