Single Instance vs Multiple Instances

OK SO SOMETHING WEIRD IS HAPPENING… IT’S ONLY THE FIRST TIME THAT IT DOES THIS SILLY GLITCH (Probably because of a delay or something somewhere)

Or so I thought… it just keeps switching stuff around as it feels like it. This is really, really bad

Isn’t there any other way to go around this. In other words, any other way to make sure that the animal gets marked as dead, but also not get in the way of the Respawn Manager?

Edit 1: Seems like it only happens when patrolling state is in action. Dwell state seems to be handling this quite well…

Edit 2: I fixed it. I ACCIDENTALLY had an extremely important line of code commented out in ‘AnimalPatrolState.cs’ from earlier testing. MY BAD… :sweat_smile:

Still, the primary bug exists though. I’ll work on that again in the morning. For now, here’s the context (with the bug):

    private void ActivateAnimalDeathChecker(Action<bool> callback)
    {
        Debug.Log($"ActivateAnimalDeathChecker called");

        if (!stateMachine)
        {
            Debug.Log($"ActivateAnimalDeathChecker Invoke called, but no state machine found");
            callback?.Invoke(true);
            return;
        }

        // Solution to disable Malbers' MountTrigger.OnTriggerEnter()' when the animal is dead
        var animalDeathChecker = stateMachine.AnimalDeathChecker;
        if (animalDeathChecker != null && stateMachine.PlayerStateMachine.GetLastFailedMountAnimal().GetComponent<Animal>().GetUniqueID() == stateMachine.GetComponent<Animal>().GetUniqueID())
        {
            Debug.Log($"ActivateAnimalDeathChecker Invoke called, state machine found");
            callback?.Invoke(false);
            animalDeathChecker.SetIsThisAnimalDead(true); // Block the Mount Triggers from working with dead
            Debug.Log($"{stateMachine.gameObject.name} has been marked as dead");
        }
        else
        {
            Debug.Log($"Last Failed Mount Animal has not been found");
        }
    }

Edit 3: Here’s the fix, in ‘PlayerStateMachine.GetLastFailedMountAnimal()’ (I need a better name for this, but it essentially tells you about what animal you last tried to mount, and if it can’t find any, get the closest animal to the player (since it’s probably what you melee-attacked last. I’ll work on a better solution for this, like who the last player attacked for example)):

        public AnimalStateMachine GetLastFailedMountAnimal()
        {
            if (LastFailedMountAnimal == null) 
            {
                LastFailedMountAnimal = SkillStore.GetNearestAnimalStateMachine();
            }

            return LastFailedMountAnimal;
        }

After some severe thought-provoking exercises, when I realized just how important it’ll be to get the last animal you attacked/attacked you, I ran into a problem. How do I get the correct animal that you were last in a fight with, (the ‘GetLastFailedMountAnimal()’ was going to be a disaster for non-melee combat targets), and still erase the data of the animal so that my follower NPCs, which rely on this kind of data, don’t end up attacking an animal beyond it’s death?

Here’s what I came up with:

  1. In ‘PlayerStateMachine.cs’, I found where I was always setting and resetting my last attacker. It was in this function:
public void SetLastAttacker(GameObject instigator)
        {
            if (LastAttacker != null)
            {
                LastAttacker.GetComponent<Health>().onDie.RemoveListener(ClearLastAttacker);
            }
        }

            LastAttacker = instigator;
            LastAttacker.GetComponent<Health>().onDie.AddListener(ClearLastAttacker);
        }

This one does not consider that data may need to be collected, because it had no reason to, so I came up with a new solution

  1. In ‘RespawnManager.cs’ (for my NPCs), I created this new event:
        public event Action OnHideCharacter;

and then I decided to skip a frame, and invoke the event before the timer for hiding the character starts, as follows:

    private IEnumerator HideCharacter()
    {
        Debug.Log($"HideCharacter in progress");
        var spawnedEnemy = GetSpawnedEnemy();
        if (spawnedEnemy == null) yield break;
        if (!spawnedEnemy.GetIsDynamic()) 
        {
            // if you're not a Dynamic Entity Variant, split the spawnedEnemy from 
            // its parent (avoids a bug caused by the nature of 'DynamicNPCSpawner.cs', 
            // where we destroy the entire AggroGroup instead of just the child (because
            // keeping that AggroGroup risks an even bigger bug when saving and restoring.
            // So that's my solution to the problem))
            spawnedEnemy.transform.SetParent(null);
        }

        yield return null;
        OnHideCharacter?.Invoke(); // Reset the last attacker in 'PlayerStateMachine.cs'
        yield return new WaitForSecondsRealtime(hideTime);
        Destroy(spawnedEnemy.gameObject);
    }

That way, we have enough time to get the data, and then delete the last attacker, getting both the ‘AnimalDeathState.cs’ and my ‘delete the last attacker’ problem solved simultaneously

  1. Back in the function of step 1, I subscribed to the events, depending on who is calling:
public void SetLastAttacker(GameObject instigator)
        {
            if (LastAttacker != null)
            {
                // LastAttacker.GetComponent<Health>().onDie.RemoveListener(ClearLastAttacker);
                if (LastAttacker.GetComponent<EnemyStateMachine>()) 
                {
                    LastAttacker.GetComponentInParent<RespawnManager>().OnHideCharacter -= ClearLastAttacker;
                }
                else if (LastAttacker.GetComponent<AnimalStateMachine>())
                {
                    LastAttacker.GetComponentInParent<AnimalRespawnManager>().OnHideAnimalCharacter -= ClearLastAttacker;
                }
            }

            LastAttacker = instigator;
            // LastAttacker.GetComponent<Health>().onDie.AddListener(ClearLastAttacker);
            if (LastAttacker.GetComponent<EnemyStateMachine>()) 
            {
                LastAttacker.GetComponentInParent<RespawnManager>().OnHideCharacter += ClearLastAttacker;
            }
            else if (LastAttacker.GetComponentInParent<AnimalStateMachine>()) 
            {
                LastAttacker.GetComponentInParent<AnimalRespawnManager>().OnHideAnimalCharacter += ClearLastAttacker;
            }
        }

and 4. The same goes for animals, but instead of skipping a frame, I decided to skip 1/100th of a second instead, and I have no idea why :slight_smile: - but in the end, the animal got marked as dead before the data was erased:

    private IEnumerator HideAnimal()
    {
        var spawnedAnimal = GetSpawnedAnimal();
        if (spawnedAnimal == null) yield break;

        yield return new WaitForSeconds(0.01f);
        OnHideAnimalCharacter?.Invoke(); // Reset the Last Attacker Animal on the player after a tiny delay, ensuring the animal gets marked as 'AnimalDeathChecker.GetIsThisAnimalDead()' to true, before the animal is erased from the player's Last Attacker variable
        yield return new WaitForSecondsRealtime(hideTime);
        if (spawnedAnimal != null)
        {
            Destroy(spawnedAnimal.gameObject);
        }
    }

The only reason I did that for the enemies too, was for consistency, nothing more

I’m pretty sure I probably hurt other animal systems in the process by not doing this any earlier. I’m just not sure where or how yet. So much for a small problem that nobody will probably notice, but I don’t want to mess things up on the long run of this code…

@Brian_Trotter can I please seek your help with a bit of a complex situation that I want to implement? Long story short, I want to implement a solution for animals, where the player is blocked from mounting them if they are aggrevated, and this includes messing with the ‘CooldownTokenManager.cs’ script (which, I never touched, since you taught us how to use it in the RPG to Third Person Series)

The idea is simple. If you try to mount an animal that’s in combat, you’ll be given a notification that says that this is not allowed, as the animal is busy in a fight. The benefit out of this, is that you can’t easily just mount an animal during a fight and run away, especially if that’s the animal that’s fighting you (I managed to get that to work, and let’s just say that it completely destroyed the balance of the gameplay)

Any ideas how to do this? I’d do it myself, but with the whole new ‘event needs to know whose calling it’, I’d be lying if I said I know exactly what to do. I want to do the blocking when the animal is angry in the following function, which I extracted from ‘MRider.cs’:

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

            // CODE ADDED BY BAHAA - MAKE SURE THE PLAYER HAS A HIGH ENOUGH LEVEL
            // OF EQUESTRIANISM BEFORE BEING ALLOWED TO DRIVE THE ANIMAL, AND RETURN
            // FALSE OTHERWISE
            if (!EquestrianismEvents.HasEquestrianismLevel())
            {
                // If your level is not high enough, you can't mount this animal
                EquestrianismEvents.InvokeOnLowLevelMount(isMountSuccessful => {});
                MalbersAnimations.InventorySystem.NotificationManager.Instance.OpenNotification("Action Not Allowed:\nYour Equestrianism Level\nis not high enough");
                return;
            }

            // CODE ADDED BY BAHAA - IF YOU MADE IT THIS FAR, IT MEANS YOUR
            // EQUESTRIANISM LEVEL IS HIGH ENOUGH TO WARRANT AN ATTEMPT TO
            // MOUNT AN ANIMAL. INVOKE AN EVENT HERE TO DETERMINE 
            // THE PROBABILITY OF FAILURE. IF IT'S HIGH, GET THE ANIMAL 
            // TO ATTACK THE PLAYER AGAIN. IF NOT, THEN ALLOW THE MOUNTING
            EquestrianismEvents.InvokeOnSkillBasedMountAttempt(isMountSuccessful =>
            {
                if (isMountSuccessful)
                {
                    // CODE ADDED BY BAHAA - SOLUTION TO ENSURE ANIMALS AND
                    // CHARACTERS (NPCs, PLAYER, etc) DON'T COLLIDE WHEN THE ANIMAL
                    // IS DRIVEN, BUT CAN COLLIDE WHEN THE ANIMAL IS NOT DRIVEN, SO
                    // COMBAT DOESN'T HAVE ANY ISSUES, AND A MORE REALISTIC INTERACTION
                    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.isPlayerOnAnimalMount = true;
                        AnimalMountManager.InvokeOnAnimalMounted();
                        Debug.Log($"isPlayerOnAnimalMount: {AnimalMountManager.isPlayerOnAnimalMount}");

                        Montura.Rider.transform.SetParent(Montura.transform);
                    }
                }
                else
                {
                    // CODE BY BAHAA - IF MOUNTING WAS NOT SUCCESSFUL,
                    // YOU INVOKE THE SAME 'OnFailedMount' THAT YOU CALL
                    // WHEN THE LEVEL IS NOT HIGH ENOUGH AGAIN (Starting an
                    // Animal Attacking System against the player)
                    EquestrianismEvents.InvokeOnLowLevelMount(isMountSuccessful => {});
                }
            });
        }

And for the record, the cooldown token manager I want to block the mounting against is called “AnimalAggro”, and for the sequence of attacks, it’s called “Attack”, if that matters in anyway

But if you’re close enough to try to mount them, they likely ARE aggrevated…

Probably because it’s now impossible to mount an animal, I’m guessing…

World of Warcraft has a class called a Hunter, in which the hunter has a pet who can attack or tank for you. The best source of pets is taming… You initiate from a distance the taming action, and here’s the fun part… the animal attacks you, we’re talking full on aggro. If you’re unlucky, he also calls a few of his close friends to help him (in which case taming will likely fail because you’ll get distracted). The animal’s attacks can break the attempt to tame, depending on the level of the animal compared to the level of the character (but you could use the EquestrianismLevel for this chance roll. If you can survive for 30 seconds or so without getting distracted or winding up in the animal’s digestive tract, then you tame the animal and it immediately becomes your pet.

Not really in my case. For them to be aggrevated, your chance of mounting them must be lower than whatever random number they generate (assuming you got the level. If you don’t, an entirely different event that tells you your level is low gets into play). I wrote a simple math formula from that (I cheated a bit from my resource gathering system), and it’s how I developed equestrianism from the start, xD (you gain experience by driving the animals, and soon you will also gain experience by hijacking them, and later you’ll be able to buy your own pets from the shop. But first, I want to fix this bug)

Not yet, but there are some bugs. For example, this is the one and only bug that I still can’t solve, in ‘AnimalChasingState.Enter()’:

        // If the animal is mounted, don't even think about
        // getting into chasing or fighting state. The player is in
        // control, so keep it that way
        if (AnimalMountManager.isPlayerOnAnimalMount)
        {
            if (stateMachine.PlayerOnAnimalStateMachine.GetAnimalDelayed().GetUniqueID() == stateMachine.GetComponent<Animal>().GetUniqueID())
            {
                stateMachine.SwitchState(new AnimalIdleState(stateMachine));
                return;
            }
        }

Whilst this works to make sure that the animal does not go chasing NPCs that attack it when the player is mounting it (I wanted it that way), if the player mounts the animal when it’s angry, it becomes a serious bug, and thanks to the ton of other bugs that I’m fixing today (I forgot my glider out of the resources folder, let’s not get into this one), I ran a little late on fixing this one

I was hoping to fix this problem by blocking the mounting when the animal is angry above

The whole point out of all of this, is you’ll only be able to try and mount the animal only when it’s not angry, to not only avoid bugs, but also to avoid breaking the game balance (I don’t want someone whose fighting an animal to simply mount it whilst it’s in combat and run away for example)

I did fix that bug using this code in ‘AnimalAttackingState.cs’:

    // The two functions below will force the animal to stop combat entirely if it's
    // mounted whilst it's aggressive. It's a fully working option,
    // but I prefer entirely blocking any attempts of mounting an animal whilst
    // it's aggrevated

    /* private void SwitchToIdleState()
    {
        stateMachine.StartCoroutine(RemovePatrolPathDelayed(0.05f));
    }

    private IEnumerator RemovePatrolPathDelayed(float delay)
    {
        yield return new WaitForSeconds(delay);
        stateMachine.CooldownTokenManager.ClearCooldown("AnimalAggro");
        stateMachine.CooldownTokenManager.ClearCooldown("Attack");
        stateMachine.SetLastAttacker(null);
        stateMachine.SetPatrolPathHolder(stateMachine.PatrolPath);
        stateMachine.AssignPatrolPath(null);
        stateMachine.SwitchState(new AnimalIdleState(stateMachine));
    } */

But because it broke the game balance and made it too easy, I discarded the code

I do that too, and it relies on an ‘Equestrianism’ skill I am working on

that’s what my AggroGroup does, except that in my case I want players to buy animals (for animals to defend them. Wild animals don’t care about you the moment you dismount them), and for NPCs, that’s exactly what I was developing for my NPC aggrogroup system

OK I don’t see exactly how this works, but if it makes sense, I might down the line. For me personally though, I want to encourage players to get pets that they own by buying them rather than taming them, for the time being at least. That way wild animals can help them navigate, but they’re still wild, so they’ll always hate you unless your equestrianism level is high

It might be worth considering giving purchased pets their own unique stateMachine class… This would pull a whole heap of problems out of the mix… and if you tame an animal, then you replace it with a prefab containing that animal’s model with the unique stateMachine class…

  • You won’t have to worry about the animal trying to attack the player because of misunderstandings
  • If you follow the common game model, only one animal is out for riding at a time… so if you tame an animal for riding, your regular mount fades away until you call it again (and your tamed mount would fade away for good when you call your regular mount).

@Brian_Trotter Will keep that in consideration. One of the main challenges I’m facing, is I probably never played a game with pets, so I have to imagine the scenarios as they happen

Although open fire is part of the game. Your team mates will attack you if you attack them, and they definitely will attack you if you’re nearby and you attack one of them (I’ll need to return to ignoring that part if you attack someone who isn’t their ally, and you’re near them, otherwise they’ll attack you… as if you’re the source of their life problems), as long as they don’t have someone who is currently attacking them. It encourages you to think wisely of your next move :slight_smile: - I’m not sure how team mates and animals will work together just yet, and honestly, I’m terrified of the NavMesh Agent for that one

But my big problem now is, I still can’t figure out exactly how to block the animal from being driven if it’s aggressive. This by itself will eliminate a ton of mounting issues, where the AI and the player controller would not clash (they clash right now when you mount an animal that’s either chasing or attacking something/someone), and it honestly makes a lot of sense, but I genuinely can’t figure it out… (I’m sure it’s a simple thing, but I recently dealt with some severe issues that really did demand a lot of brain power :sweat_smile: - the most tiring one was the mathematics behind a re-write of part of the saving system of their respawn manager).

The fifth state machine on the way, got it :stuck_out_tongue: - although I’m sure we can fix a ton of behaviour through a simple ‘isPlayerFriend’ boolean or something (I got one for the player on his feet, one for the player when mounting animals (they toggle and block one another when the other is in progress), one for the enemy NPCs, and one for the animals so far)

  1. So to solve this, I will leverage the ‘LastAttacker’ variable. What I have just done, is found a way to clean up the Last Attacker when the animal is not aggressive, using this code in ‘AnimalIdleState’ (I’ll rename it to mounting state as soon as I’m done fixing these bugs):
        if (stateMachine.GetLastAttacker() != null && !stateMachine.CooldownTokenManager.HasCooldown("AnimalAggro") && stateMachine.animalAttackDone)
        {
            stateMachine.SetLastAttacker(null);
        }

(‘animalAttackDone’ will be dealt with at the end of this post)

We can use that to tell the ‘MountAnimal()’ function, probably through a static event (I don’t know yet), is that if the animal has a last attacker, and the player attempts to mount it, to block that mount attempt

  1. The next thing I did, was create two new events, ‘OnAnimalAggrevated’ and ‘OnAnimalNeutralized’ in ‘AnimalMountManager.cs’:
    // When the Animal gets aggrevated
    public static event Action OnAnimalAggrevated;
    // When the Animal loses its aggression
    public static event Action OnAnimalNeutralized;

    // When the animal is aggrevated (by taking a hit from someone)
    public static void InvokeOnAnimalAggrevated(GameObject animal)
    {
        OnAnimalAggrevated?.Invoke(animal);
    }

    // When the animal is neutralized (after the Last Attacker is cleaned up)
    public static void InvokeOnAnimalNeutralized(GameObject animal)
    {
        OnAnimalNeutralized?.Invoke(animal);
    }

Don’t worry about getting the correct data just yet, I have a plan for this one

  1. To invoke these two events, we will tell the animal it’s neutral when the conditions for the function in step 1 are met, and the animal is cool again, so the block from step 1 in ‘AnimalIdleState.cs’ will look like this:
        if (stateMachine.GetLastAttacker() != null && !stateMachine.CooldownTokenManager.HasCooldown("AnimalAggro"))
        {
            stateMachine.SetLastAttacker(null);
// I'll also probably invoke the neutralized code for when the animal has just respawned or something
            AnimalMountManager.InvokeOnAnimalNeutralized(); // Allow the animal to be mounted again, now that it has no enemies to fight
        }

and in ‘AnimalStateMachine.OnTakenHit’, we will invoke ‘OnAnimalAggrevated’:

    public void OnTakenHit(GameObject instigator)
    {
        SetLastAttacker(instigator);
        CooldownTokenManager.SetCooldown("AnimalAggro", CooldownTimer, true);

        if (instigator.GetComponent<PlayerStateMachine>()) 
        {
            // We will use this to tell the Player who he just last hit, so that his last victim
            // can be properly marked as 'isThisAnimalDead = true' to be used in 'AnimalDeathState.cs'
            // to allow the correct operations to flow
            // (and eventually, have the NPCs supporting the player attack this animal too)
            instigator.GetComponent<PlayerStateMachine>().SetLastAttacker(this.gameObject);
        }
// I'll also invoke the aggrevated state for all scenarios that may aggrevate the animal down the line
        AnimalMountManager.InvokeOnAnimalAggrevated();
    }
  1. Next up, I realized I have a problem of not knowing which animal exactly am I trying to get here. After a little bit of blinking at the inspector, I realized all of the animals have an ‘MAnimal.cs’ script on them, and fortunately for me, these two scripts, ‘MRider.cs’ and ‘MAnimal.cs’, both have no correlation, so I figured I’d shake things up a little bit

Anyway, I ended up writing this function in ‘MAnimal.cs’ to determine if this animal is mountable or not:

        // ---------------------------- CODE ADDED BY BAHAA ----------------------------
        public bool canMountThisAnimal = true;

        public bool GetCanMountThisAnimal() 
        {
            return canMountThisAnimal;
        }

        public void SetCanMountThisAnimal(bool canMountThisAnimal) 
        {
            this.canMountThisAnimal = canMountThisAnimal;
        }
        // -----------------------------------------------------------------------------
  1. In ‘MRider.cs’, I introduced solutions to subscribe and unsubscribe from AnimalMountManager.cs’s events, as follows (you will need to realize the fact that we are leveraging the idea of ‘the animal you’re currently trying to mount, is the closest animal to you right now’ (which will be, for 100% of the time, true)), and so… I developed a formula to get the closest animal, and leveraged it with ‘MAnimal.cs’:
        // ----------------------------------- MORE CODE ADDED BY BAHAA ------------------------------------------

        public bool GetCanMountThisAnimal()
        {
            return GetNearestAnimal().GetCanMountThisAnimal();
        }

        public void SetCanMountAnimal(bool canMountAnimal, MAnimal targetAnimal)
        {
            targetAnimal.GetComponent<MAnimal>().SetCanMountThisAnimal(canMountAnimal);
        }

        private void EnableMounting()
        {
            var nearestAnimal = GetNearestAnimal();
            SetCanMountAnimal(true, nearestAnimal);
        }

        private void DisableMounting()
        {
            var nearestAnimal = GetNearestAnimal();
            SetCanMountAnimal(false, nearestAnimal);
        }

        private MAnimal GetNearestAnimal() 
        {
            float radius = 5.0f;
            Vector3 playerPosition = transform.position;

            Collider[] colliders = Physics.OverlapSphere(playerPosition, radius);

            MAnimal nearestAnimal = null;
            float closestDistance = Mathf.Infinity;

            foreach (Collider collider in colliders) 
            {
                MAnimal animal = collider.GetComponent<MAnimal>();
                if (animal != null) 
                {
                    float distance = Vector3.Distance(playerPosition, animal.transform.position);
                    if (distance < closestDistance) 
                    {
                        closestDistance = distance;
                        nearestAnimal = animal;
                    }
                }
            }

            return nearestAnimal;
        }

        // -------------------------------------------------------------------------------------------------------
  1. And the final step to bring this all together, is leverage step 5 to check if this animal is aggressive, or it can be mounted:
            // CODE ADDED BY BAHAA - BLOCK THE PLAYER FROM MOUNTING THIS ANIMAL, IF IT'S
            // IN A FIGHT OR IS AGGREVATED FOR ANY REASON
            if (!GetCanMountThisAnimal())
            {
                MalbersAnimations.InventorySystem.NotificationManager.Instance.OpenNotification("Action Not Allowed:\nAnimal is too\nAggressive right\nnow");
                return;
            }

And… Yeah, that solves the problem I was too tired to solve yesterday, and it gets rid of the ton of bugs that would be introduced if you managed to mount an animal that’s in a fight anyway :slight_smile:

I’m not 100% sure if this matters or not, but for step 1 and step 3, if you got an attack cooldown timer, to indicate what your next attack will be, you may want to make sure that this is disabled as well

I’ll thoroughly test this and keep you updated, although it seems to be fine to me for the time being (it’s not… there are some sneaky areas that need to be addressed, but for the time being I think this should do it’s job just fine. In simple terms, it’s incomplete, mainly because the events are not invoked everywhere they are supposed to be invoked at just yet)


So the first thing I did, is introduce this in ‘AnimalStateMachine.cs’:

    [field: Tooltip("Variable used in 'AnimalAttackingState.cs' to ensure the animation is done, before the animal can be mounted again. This ensures no mounting bugs happen by the player")]
    [field: SerializeField] public bool animalAttackDone { get; set; }

This variable will check that whatever attacking animation you got playing is done, before it can give you permission to mount the animal again. This avoids bugs that can come from the NPC being extremely confused as of what to do next, because you mounted the animal in a state it was not ready for

To apply that, place this line in ‘AnimalAttackingState.cs’:

// in 'Enter()':
        stateMachine.animalAttackDone = false;

// in 'Tick()' (I boosted the float from 0.98f to 0.99f just to be extra safe):
        if (normalizedTime > 0.99f)
        {
            stateMachine.animalAttackDone = true;
            stateMachine.SwitchState(new AnimalIdleState(stateMachine));
            return;
        }

Now I’m looking for a way to aggrevate the animal, similar to when it gets attacked, but when it gets a failed mounting attempt, to ensure that you don’t mess the logic again

Something to place in here (because the invoke in here, does not get the correct results):

    private IEnumerator DelayedDetermineChanceOfMountSuccess(Action<bool> callback)
    {
        yield return null; // Spare a frame for 'SkillStore.GetNearestAnimalStateMachine()' to get ready
        if (stateMachine.PlayerStateMachine.SkillStore.GetNearestAnimalStateMachine().GetComponent<Animal>().GetUniqueID() == stateMachine.GetComponent<Animal>().GetUniqueID())
        {
            Debug.Log($"DetermineChanceOfMountSuccess called from {stateMachine.gameObject.name} Idle State");
            // If the animal was not mounted before, there's a chance of resistance
            var playerEquestrianismLevel = stateMachine.PlayerStateMachine.SkillStore.GetSkillLevel(Skill.Equestrianism);

            // Values between 0 and 1
            // Chance of successful mounting
            float successfulMountChance = Mathf.Clamp01
            (
                ((float)playerEquestrianismLevel - (float)stateMachine.MinimumEquestrianismLevelToMount) /
                ((float)stateMachine.MaximumMountResistanceEquestrianismLevel - (float)stateMachine.MinimumEquestrianismLevelToMount)
            );

            // Chance of random mounting
            float randomMountChance = UnityEngine.Random.value;

            Debug.Log($"player equestrianism level: {playerEquestrianismLevel}, minimum level: {stateMachine.MinimumEquestrianismLevelToMount}, max mount level: {stateMachine.MaximumMountResistanceEquestrianismLevel}, minimum level: {stateMachine.MinimumEquestrianismLevelToMount}, successful mount chance: {(float)((playerEquestrianismLevel - stateMachine.MinimumEquestrianismLevelToMount) / (stateMachine.MaximumMountResistanceEquestrianismLevel - stateMachine.MinimumEquestrianismLevelToMount))}, random mount chance: {randomMountChance}");

            if (randomMountChance <= successfulMountChance)
            {
                MalbersAnimations.InventorySystem.NotificationManager.Instance.OpenNotification($"Mounting Success:\nRandomChance: {randomMountChance}\nSuccess Chance: {successfulMountChance}");
                callback?.Invoke(true);
            }
            else
            {
                MalbersAnimations.InventorySystem.NotificationManager.Instance.OpenNotification($"Mounting Failed:\nRandom Chance: {randomMountChance}\nSuccess Chance: {successfulMountChance}");
                AnimalMountManager.InvokeOnAnimalAggrevated();
                callback?.Invoke(false);
            }
        }
    }

AND… I just noticed, I’m trying to mount an animal that I already mounted. No wonder it wasn’t working

In other words, implement the invoke (‘AnimalMountManager.InvokeOnAnimalAggrevated();’) on other states but not the idle one, because you only get into idle state when you mounted the animal

And now comes part 2 of the bugs. Somewhere, I messed up the chasing, and now the moment the horse is hit, whilst mounted, he can’t be driven anymore


Edit: Just keeping track of things here, IGNORE:

// ----------------------------------- MORE CODE ADDED BY BAHAA ------------------------------------------

            // FUNCTION ADDED BY BAHAA - FORCE THE PLAYER TO DISMOUNT THE ANIMAL IF THE ANIMAL IS KILLED
            AnimalMountManager.OnAnimalKilled += DismountAnimal;
            // FUNCTION ADDED BY BAHAA - DISABLE MOUNTING WHEN THE ANIMAL IS UNDER ATTACK
            AnimalMountManager.OnLastMountAttemptAnimalAggrevated += DisableLastMountedAnimalMounting;
            // FUNCTION ADDED BY BAHAA - ENABLE MOUNTING WHEN THE ANIMAL IS NO LONGER UNDER ATTACK
            AnimalMountManager.OnLastMountAttemptAnimalNeutralized += EnableLastMountedAnimalMounting;
        }

        // FUNCTION ADDED BY BAHAA
        private void OnDisable()
        {
            // FUNCTION ADDED BY BAHAA - FORCE THE PLAYER TO DISMOUNT THE ANIMAL IF THE ANIMAL IS KILLED
            AnimalMountManager.OnAnimalKilled -= DismountAnimal;
            // FUNCTION ADDED BY BAHAA - DISABLE MOUNTING WHEN THE ANIMAL IS UNDER ATTACK
            AnimalMountManager.OnLastMountAttemptAnimalAggrevated -= DisableLastMountedAnimalMounting;
            // FUNCTION ADDED BY BAHAA - ENABLE MOUNTING WHEN THE ANIMAL IS NO LONGER UNDER ATTACK
            AnimalMountManager.OnLastMountAttemptAnimalNeutralized -= EnableLastMountedAnimalMounting;
        }

        public bool GetCanMountThisAnimal()
        {
            return GetNearestAnimal().GetCanMountThisAnimal();
        }

        public void SetCanMountAnimal(bool canMountAnimal, MAnimal targetAnimal)
        {
            targetAnimal.GetComponent<MAnimal>().SetCanMountThisAnimal(canMountAnimal);
        }

        private void EnableLastMountedAnimalMounting(GameObject aggrevatedAnimal)
        {
            if (GetLastMountedAnimal() == null)
            {
                var nearestAnimal = GetNearestAnimal();
                SetCanMountAnimal(true, nearestAnimal);
            }
            else
            {
                var lastMountedAnimal = GetLastMountedAnimal();
                SetCanMountAnimal(true, lastMountedAnimal);
            }
        }

        private void DisableLastMountedAnimalMounting(GameObject aggrevatedAnimal)
        {
            if (GetLastMountedAnimal() == null)
            {
                var nearestAnimal = GetNearestAnimal();
                SetCanMountAnimal(false, nearestAnimal);
            }
            else
            {
                var lastMountedAnimal = GetLastMountedAnimal();
                SetCanMountAnimal(false, lastMountedAnimal);
            }
        }

        private MAnimal GetNearestAnimal()
        {
            float radius = 5.0f;
            Vector3 playerPosition = transform.position;

            Collider[] colliders = Physics.OverlapSphere(playerPosition, radius);

            MAnimal nearestAnimal = null;
            float closestDistance = Mathf.Infinity;

            foreach (Collider collider in colliders)
            {
                MAnimal animal = collider.GetComponent<MAnimal>();
                if (animal != null)
                {
                    float distance = Vector3.Distance(playerPosition, animal.transform.position);
                    if (distance < closestDistance)
                    {
                        closestDistance = distance;
                        nearestAnimal = animal;
                    }
                }
            }

            return nearestAnimal;
        }

        MAnimal lastMountedAnimal;

        private MAnimal GetLastMountedAnimal() 
        {
            return lastMountedAnimal;
        }

        private void SetLastMountedAnimal(MAnimal lastMountedAnimal) 
        {
            this.lastMountedAnimal = lastMountedAnimal;
        }

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

I was honestly… umm… TERRIBLY WRONG

You need to know who is calling the static event. It took me 12 hours to get it :sweat_smile:

(Still haven’t fixed the ‘animal fails to move if attacked when mounted’ issue)

Edit: I fixed that too. So… today was a world tour in my scripts over these two functions in ‘AnimalStateMachine.cs’:

    public void OnTakenHit(GameObject instigator)
    {
        SetLastAttacker(instigator);
        CooldownTokenManager.SetCooldown("AnimalAggro", CooldownTimer, true);

        if (instigator.GetComponent<PlayerStateMachine>())
        {
            // We will use this to tell the Player who he just last hit, so that his last victim
            // can be properly marked as 'isThisAnimalDead = true' to be used in 'AnimalDeathState.cs'
            // to allow the correct operations to flow
            // (and eventually, have the NPCs supporting the player attack this animal too)
            instigator.GetComponent<PlayerStateMachine>().SetLastAttacker(this.gameObject);
        }

        // Block the Player from being capable of mounting an angry animal (neutralized in 'AnimalStateMachine.AnimalClearLastAttacker()')
        AnimalMountManager.InvokeOnLastMountAttemptAnimalAggrevated(this.gameObject);
    }

    private void HandleForceApplied(Vector3 force)
    {
        // If the animal is dead, do nothing. If the animal is not dead, switch
        // the animal to the animal impact state
        if (Health.IsDead()) return;

        // Make sure the animal does not come to a frozen stop if he's mounted, and attacked by someone
        if (AnimalMountManager.isPlayerOnAnimalMount)
        {
            return;
        }

        SwitchState(new AnimalImpactState(this));
    }

Following suit of what I did earlier today, with some post-heavy modifications to get the code to work, I made the first function work, and eliminated the bug of the player interrupting with the combat of animals, by telling the ‘InvokeOnLastMountAttemptAnimalAggrevated’ exactly who is calling the script. Prior to that, each time you aggrevate any animal in the scene, all animals would be angry and you won’t be able to mount any calm animal, because of one angry guy. You can imagine how bad that would be in intense battles (and thanks to this, I slowly understood why telling an event what data type is calling matters. Nothing beats real-world experience!)

Next, after another world tour on ‘why is my animal frozen the moment it takes a hit whilst mounted’, I realized that when the animal is mounted, it should never leave that state to go to another, and after endless tests, I figured it was because of the force applied sending it to impact state. I lost the impact animations whilst mounted, but there are no serious bugs where the animal freezes

There’s just one more bug. Here’s the scenario:

Sometimes the mounted animal would be attacked by another unmounted animal, and the cooldown would run out whilst it’s mounted, and when dismounted it won’t clear that data, because the cooldown timer, according to these conditions:

        if (stateMachine.GetAnimalLastAttacker() != null && !stateMachine.CooldownTokenManager.HasCooldown("AnimalAggro") && stateMachine.animalAttackDone)
        {
            Debug.Log($"{stateMachine.gameObject.name} Patrol State cleared the animal last attacker");
            stateMachine.ClearAnimalLastAttacker();
        }

has run out. As a result, you have to beat it once (so that the cooldown timer knows what to do) before you can mount the animal again

I’ll work on this soon…

After that, I can work on differentiating between wild animals, animals that only care for itself and will ditch you the moment you dismount them, and pets that’ll help you in battle

And then move on to flying states (or the other way around)

I call today a big win, and hopefully the end of the basic animal implementation :smiley:

1 Like

to fix this, just clear the last attacker whilst mounted if the timer runs out. Something like this in ‘AnimalIdleState.cs’ (if you don’t do this, the last attacker will stay for longer than it is invited for, and will destroy the logic when patrolling enters, for my scenario at least (because for my case, you can’t mount an animal with a ‘LastAttacker’ (I did this to stop a truck load of unwanted bugs), so this step is crucial)):

        // If the animal is mounted (i.e: It's in this state), and it was aggrevated,
        // and the cooldown ended, then clean up the Last Attacker when the cooldown ends
        // (without this, the Last Attacker will stay for longer than it needs to,
        // and lead to the animal being unmountable because it can't clear the Last Attacker,
        // because the cooldown no longer exists, and it'll be a serious logical bug)
        if (!stateMachine.CooldownTokenManager.HasCooldown("AnimalAggro")
        && stateMachine.GetAnimalLastAttacker() != null)
        {
            stateMachine.ClearAnimalLastAttacker();
            AnimalMountManager.InvokeOnLastMountAttemptAnimalNeutralized(stateMachine.gameObject);
        }

This is NOT the same as ‘IsAggrevated()’, since this one has a ‘!’ before checking for the cooldown. IsAggrevated does not (I want to confirm the animal is free from aggression before deleting the last attacker)

Now I’m dealing with getting the impact animations back, and a problem with flying animals when assigning them patrol states (They need to fall first, so their character controller, or better, the distance between them and the floor, must be small first)

And… that Respawn Manager just forced me to create my own animal flying states. Now I have no other option :sweat_smile:

AND NOW WE HAVE A BRAND NEW PROBLEM. WHEN I SAVE AND RESTORE THE GAME, THE PATROL STATE LITERALLY HAS NO CLUE WHAT ‘stateMachine’ (THE INSTANCE HOLDING THE SCRIPT) EXACTLY IS… So now I got completely false behaviour after restoring the game

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

Privacy & Terms