An alternative State Machine

[WAY OFF TOPIC, BUT I DON’T KNOW WHERE ELSE TO ASK. PLEASE SEE IF YOU CAN HELP ME IN ANYWAY SHAPE OR FORM]

OK so… I’m trying to create an alternative player state machine system, something to avoid entirely confusing between what each system does when the player is on the mount of an animal, compared to when he is on his feet and roaming

First, I created a static class, with a single static variable called ‘isOnAnimalMount’, and went into Malbers’ Rider.cs script and modified it so it considers this script accordingly

Next, I started working on an alternative state machine, and the first thing I decided to do was test the death animations of the player when he is on the back of a horse

Now, I’m not 100% sure what’s going on, but the new state machine did make it to the death state, when an enemy kills my player on the back of a horse, but no animation is played. There are some respawn glitches as well, but that’s something for me to fix after I fix the animation issue

Any idea how to fix this? Here’s what I came up with:

‘AnimalMountManager.cs’ (a single static variable static class:

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;
}
}

then in ‘MRider.cs’, I tuned this variable accordingly

next, I created a new script for the player that’ll switch the state machines, in an Update function, based on whether the player is on an animal or not, as follows:

using RPG.Statics;
using UnityEngine;
using RPG.States.Player;
using RPG.States.PlayerOnAnimal;

public class PlayerToPlayerOnAnimalToggle : MonoBehaviour
{
    private PlayerStateMachine player;
    private PlayerOnAnimalStateMachine playerOnAnimalStateMachine;

    void Awake() 
    {
        player = GetComponent<PlayerStateMachine>();
        playerOnAnimalStateMachine = GetComponent<PlayerOnAnimalStateMachine>();
    }

    // Update is called once per frame
    void Update()
    {
        if (AnimalMountManager.isOnAnimalMount) 
        {
            player.enabled = false;
            playerOnAnimalStateMachine.enabled = true;
        }
        else 
        {
            player.enabled = true;
            playerOnAnimalStateMachine.enabled = false;
        }
    }
}

Then I started creating the state machine, and here is what I came up with so far:

The state machine itself:

using UnityEngine;
using MalbersAnimations.HAP;
using RPG.Animals;
using RPG.Attributes;
using RPG.Movement;

namespace RPG.States.PlayerOnAnimal {

public class PlayerOnAnimalStateMachine : StateMachine
{
    [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;}

    void Start() 
    {
        if (Health.IsDead()) SwitchState(new PlayerOnAnimalDeathState(this));
        else SwitchState(new PlayerOnAnimalFreeLookState(this));

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

        Health.onResurrection.AddListener(() => 
        {
            SwitchState(new PlayerOnAnimalFreeLookState(this));
        });
    }

    void OnValidate() 
    {
        if (Rider == null) Rider = GetComponent<MRider>();
        if (Health == null) Health = GetComponent<Health>();
        if (Animator == null) Animator = GetComponent<Animator>();
        if (ForceReceiver == null) ForceReceiver = GetComponent<ForceReceiver>();
        if (CharacterController == null) CharacterController = GetComponent<CharacterController>();
    }

    void OnEnable() 
    {
        // 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>();
        Animal = GetComponentInParent<Animal>();
    }

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

    void ClearAnimal() 
    {
        Animal = null;
    }

    void ClearPlayerRider() 
    {
        Rider = null;
    }

}

}

the base state:

using UnityEngine;

namespace RPG.States.PlayerOnAnimal
{
    public abstract class PlayerOnAnimalBaseState : State
    {
        protected PlayerOnAnimalStateMachine stateMachine;
        protected float AnimatorDampTime = 0.1f;

        public PlayerOnAnimalBaseState(PlayerOnAnimalStateMachine stateMachine)
        {
            this.stateMachine = stateMachine;
        }

        protected void Move(float deltaTime) 
        {
            Move(Vector3.zero, deltaTime);
        }

        protected void Move(Vector3 direction, float deltaTime)
        {
            stateMachine.CharacterController.Move((direction + stateMachine.ForceReceiver.Movement) * deltaTime);
        }

        protected float GetNormalizedTime(string tag = "Attack")
        {
            var currentInfo = stateMachine.Animator.GetCurrentAnimatorStateInfo(0);
            var nextInfo = stateMachine.Animator.GetNextAnimatorStateInfo(0);

            if (stateMachine.Animator.IsInTransition(0) && nextInfo.IsTag(tag))
            {
                return nextInfo.normalizedTime;
            }

            else if (!stateMachine.Animator.IsInTransition(0) && currentInfo.IsTag(tag))
            {
                return currentInfo.normalizedTime;
            }

            return 0; // return something
        }
    }
}

freelook (for now. It sounds pointless to me in this coniditon tbh):

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

namespace RPG.States.PlayerOnAnimal {

public class PlayerOnAnimalFreeLookState : PlayerOnAnimalBaseState
{
    public PlayerOnAnimalFreeLookState(PlayerOnAnimalStateMachine stateMachine) : base(stateMachine) {}

        public override void Enter()
        {
            Debug.Log($"PlayerOnAnimalFreeLookState entered");
        }

        public override void Exit()
        {
            
        }

        public override void Tick(float deltaTime)
        {
            
        }
    }

}

and then death, where I’m doing all the testing (again, the debugger says it gets here, but the animations are not played at all):

using RPG.States.PlayerOnAnimal;
using UnityEngine;

public class PlayerOnAnimalDeathState : PlayerOnAnimalBaseState
{
    public PlayerOnAnimalDeathState(PlayerOnAnimalStateMachine stateMachine) : base(stateMachine) {}

    private static readonly int PlayerOnAnimalDeathHash = Animator.StringToHash("PlayerOnAnimalDeath");

    public override void Enter()
    {
        Debug.Log($"PlayerOnAnimal Entered Death State");
        stateMachine.Animator.CrossFadeInFixedTime(PlayerOnAnimalDeathHash, AnimatorDampTime);
    }

    public override void Tick(float deltaTime)
    {
        Move(deltaTime);
    }

    public override void Exit() {}
}

that’s all I did so far. If anyone knows what I should do next, please let me know

my main problem is that it does not play any animations

Tbh, I don’t see how you got to the death state. There’s nothing that puts you in a death state unless you started the game dead on a horse…

for a very long time, I honestly thought having a subscription to the death event was done at the start, and so far I still believe it is, because the death state does get called through the debugger. I believe this is the line responsible for it:

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

The only problem is, and that’s really really baffling me, is that no death animation is being played. It makes me wonder if there’s any overriding happening by chance. Any ideas how to solve this one?

and there’s also this YELLOW Error being called whenever I die:

CharacterController.Move called on inactive controller
UnityEngine.CharacterController:Move (UnityEngine.Vector3)
RPG.States.PlayerOnAnimal.PlayerOnAnimalBaseState:Move (UnityEngine.Vector3,single) (at Assets/Project Backup/Scripts/State Machines/PlayerOnAnimal/PlayerOnAnimalBaseState.cs:22)
RPG.States.PlayerOnAnimal.PlayerOnAnimalBaseState:Move (single) (at Assets/Project Backup/Scripts/State Machines/PlayerOnAnimal/PlayerOnAnimalBaseState.cs:17)
PlayerOnAnimalDeathState:Tick (single) (at Assets/Project Backup/Scripts/State Machines/PlayerOnAnimal/PlayerOnAnimalDeathState.cs:18)
RPG.States.StateMachine:Update () (at Assets/Project Backup/Scripts/State Machines/StateMachine.cs:18)

(to get rid of it I will search for ways to activate the Character Controller through code, but doing it manually didn’t seem to solve the problem either)

Edit 1: I just noticed something else. The Third Person Humanoid layer of mine has a ‘Mounted’ layer on it. I’m guessing MalberS hard-coded his Rider script (which is on my player) to have it, and I just noticed that over there, it keeps playing idle regardless of what’s happening

I’m guessing I need to connect my scripts to that somehow

And… I think I solved the problem. Like I said in my previous edit, it was because of the layers that everything was acting upon. In other words, when we drive the animal, by default Malbers’ script switches the animation layer to the Mounted Layer, and so I went to the script where I do all the updates and added a switcher as well between the animation layers, that also relies on the ‘isOnAnimalMount’ static variable that I have created. Here’s the updated script:

using RPG.Statics;
using UnityEngine;
using RPG.States.Player;
using RPG.States.PlayerOnAnimal;

public class PlayerToPlayerOnAnimalToggle : MonoBehaviour
{
    private PlayerStateMachine player;
    private PlayerOnAnimalStateMachine playerOnAnimalStateMachine;
    private Animator animator;

    private const int BaseLayer = 0;
    private const int Mounted = 1;

    void Awake() 
    {
        player = GetComponent<PlayerStateMachine>();
        playerOnAnimalStateMachine = GetComponent<PlayerOnAnimalStateMachine>();
        animator = GetComponent<Animator>();
    }

    // Update is called once per frame
    void Update()
    {
        if (AnimalMountManager.isOnAnimalMount) 
        {
            player.enabled = false;
            playerOnAnimalStateMachine.enabled = true;
            SetLayerWeight(Mounted, 1);
        }
        else 
        {
            player.enabled = true;
            playerOnAnimalStateMachine.enabled = false;
            SetLayerWeight(BaseLayer, 1);
        }
    }

    void SetLayerWeight(int layerIndex, float weight) 
    {
        if (animator != null) 
        {
            animator.SetLayerWeight(layerIndex, weight);
            Debug.Log($"Animation layer: {animator.GetLayerName(layerIndex)}");
        }
    }
}

I will most likely work on changing this to an event first (I’m not a big fan of updates tbh, at least now that I have the hard rules of Events in my mind: 1. Invoke (and use a question mark for that to ensure it’s not null), 2. Subscribe, 3. Unsubscribe), but I can foresee a huge problem of tight coupling (Brian warned me against this. Brian, if you see this, I hope you’re doing well after the surgery) coming up soon…

Edit 1: And now I learned that events are only invoked from the class declaring them… To call them from an outside class, you place them in a function. New lesson learned, xD

That’s my bad. I didn’t see that. I guess my brain stopped registering after I saw that you are trying to switch to the death state in Start which - as I said - would generally mean you start the game dead on a horse

'cos you are trying to move the player who’s controller has been disabled. I know nothing of this malbers fellow, but you probably need to control the horse, now. I dunno. Or don’t disable the player controller

Also check your PlayerOnAnimalBaseState. You have GetNormalizedTime that is checking layer 0. You probably need to change those to layer 1.

Yes. The main reason is because you are not technically meant to be raising events from elsewhere.

That’s what I’m trying to do. For the time being, I deleted all code he has that is non-relevant to the riding system, and I’m trying to install my own instead. Hopefully, this will make debugging easier down the line

It’s a little scary though, because more likely than not, future assembly references might cause some issues. I’m honestly hoping for the best right now

Oh it got a whole lot more complex in two hours, with enumerables (by getting the type of animal through an enum, I can customize a lot of things that I can control, so I figured I’d try implementing it), and other data types scattered around now :sweat_smile: (in a neat order though. As neat as I can possibly make it!) - I’m still trying to figure out what in the world am I doing. For now, I honestly don’t like the death animation, so I’m trying a new approach, which… eventually lead me to a pitfall in my own code.

Somewhere in my code, the sequence of execution got messy, so I can’t get the type of animal when I mount it anymore. I’m trying to figure out what went wrong

The point is to get you shot by an arrow off the horse. He doesn’t have an animation for that, so I’m trying to mix the force receiver with an animation that I found to achieve that

if you do want to have a look and see if you can help though, here’s what I came up with:

Animal.cs:

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

namespace RPG.Animals {

public enum AnimalType
{
    Horse,
    Raven,
}

public class Animal : MonoBehaviour
{
    [SerializeField] private AnimalType animalType;
    public AnimalType Type => animalType;
}

}

Two animals for the time being, as they are my prototype

The toggling script, so I can toggle between the on-foot player state machine, and on-animal state machine:

using RPG.Statics;
using UnityEngine;
using RPG.States.Player;
using RPG.States.PlayerOnAnimal;

public class PlayerToPlayerOnAnimalToggle : MonoBehaviour
{
    private PlayerStateMachine playerStateMachine;
    private PlayerOnAnimalStateMachine playerOnAnimalStateMachine;
    private Animator animator;

    private const int BaseLayer = 0;
    private const int Mounted = 1;

    void Awake() 
    {
        playerStateMachine = GetComponent<PlayerStateMachine>();
        playerOnAnimalStateMachine = GetComponent<PlayerOnAnimalStateMachine>();
        animator = GetComponent<Animator>();

        AnimalMountManager.OnAnimalMounted += AnimalMounted;
        AnimalMountManager.OnAnimalDismounted += AnimalDismounted;
    }

    void OnDestroy()
    {
        AnimalMountManager.OnAnimalMounted -= AnimalMounted;
        AnimalMountManager.OnAnimalDismounted -= AnimalDismounted;
    }

    void AnimalMounted() 
    {
        playerStateMachine.enabled = false;
        playerOnAnimalStateMachine.enabled = true;
        SetLayerWeight(Mounted, 1);
    }

    void AnimalDismounted() 
    {
        playerStateMachine.enabled = true;
        playerOnAnimalStateMachine.enabled = false;
        SetLayerWeight(BaseLayer, 1);
    }

    void SetLayerWeight(int layerIndex, float weight) 
    {
        if (animator != null) 
        {
            animator.SetLayerWeight(layerIndex, weight);
            Debug.Log($"Animation layer: {animator.GetLayerName(layerIndex)}");
        }
    }
}

and my current ‘PlayerOnAnimal’ state machine:

using UnityEngine;
using MalbersAnimations.HAP;
using RPG.Animals;
using RPG.Attributes;
using RPG.Movement;

namespace RPG.States.PlayerOnAnimal {

public class PlayerOnAnimalStateMachine : StateMachine
{
    [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;}

    void Start() 
    {
        if (Health.IsDead()) SwitchState(new PlayerOnAnimalDeathState(this));
        else SwitchState(new PlayerOnAnimalFreeLookState(this));

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

        Health.onResurrection.AddListener(() => 
        {
            SwitchState(new PlayerOnAnimalFreeLookState(this));
        });
    }

    void OnValidate() 
    {
        if (Rider == null) Rider = GetComponent<MRider>();
        if (Health == null) Health = GetComponent<Health>();
        if (Animator == null) Animator = GetComponent<Animator>();
        if (ForceReceiver == null) ForceReceiver = GetComponent<ForceReceiver>();
        if (CharacterController == null) CharacterController = GetComponent<CharacterController>();
    }

    void OnEnable() 
    {
        // 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>();
        Animal = GetComponentInParent<Animal>();
    }

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

    void ClearAnimal() 
    {
        Animal = null;
    }

    void ClearPlayerRider() 
    {
        Rider = null;
    }
}

}

I have my suspicions that something is wrong between the toggler, and the on-animal state machine

And to clear any suspicions, I modified a little of Malbers’ code to ensure that we can always get the animal as a parent of the player. It worked when I was still using an Update function

And I’m persistent on making the events work, because I want to get more familiar with them

OK fine… I admit, I have absolutely no idea how to enable and disable the scripts through an event. The starting point seems inevitable without an Update function getting involved, so I’ll leave this for @Brian_Trotter (when he’s back), @bixarrio or any other professional, because I genuinely have no clue how to get around that. Here’s what I ended up with, for the time being for the state machine and the toggler:

Toggler:

using RPG.Statics;
using UnityEngine;
using RPG.States.Player;
using RPG.States.PlayerOnAnimal;

public class PlayerToPlayerOnAnimalToggle : MonoBehaviour
{
    private PlayerStateMachine playerStateMachine;
    private PlayerOnAnimalStateMachine playerOnAnimalStateMachine;

    void Awake() 
    {
        playerStateMachine = GetComponent<PlayerStateMachine>();
        playerOnAnimalStateMachine = GetComponent<PlayerOnAnimalStateMachine>();
    }

    void Update() 
    {
        if (AnimalMountManager.isOnAnimalMount) 
        {
            playerStateMachine.enabled = false;
            playerOnAnimalStateMachine.enabled = true;
            return;
        }
        else 
        {
            playerOnAnimalStateMachine.enabled = false;
            playerStateMachine.enabled = true;
            return;
        }
    }
}

StateMachine:

using UnityEngine;
using MalbersAnimations.HAP;
using RPG.Animals;
using RPG.Attributes;
using RPG.Movement;

namespace RPG.States.PlayerOnAnimal {

public class PlayerOnAnimalStateMachine : StateMachine
{
    [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;}

    private const int BaseLayer = 0;
    private const int Mounted = 1;

    void Start() 
    {
        if (Health.IsDead()) SwitchState(new PlayerOnAnimalDeathState(this));
        else SwitchState(new PlayerOnAnimalFreeLookState(this));

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

    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>();
    }

    void OnEnable() 
    {
        // 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>();
        Animal = GetComponentInParent<Animal>();
        SetLayerWeight(Mounted, 1);
    }

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

    void ClearAnimal() 
    {
        Animal = null;
    }

    void ClearPlayerRider() 
    {
        Rider = null;
    }

    void SetLayerWeight(int layerIndex, float weight)
    {
        if (Animator != null)
        {
            Animator.SetLayerWeight(layerIndex, weight);
            Debug.Log($"Animation Layer: {Animator.GetLayerName(layerIndex)}");
        }
    }
    
    }
}

What I want to do, is completely avoid using the ‘Update()’ function to switch between states, in the ‘Toggle’ script, because I genuinely have no idea when will I want to do any changes between them, which can be disrupted because of the Update function

I’d appreciate if we can turn this into an event, because I don’t know when, down the line, will I need that. But I know it’s bothering me tbh, more than anything :sweat_smile:

I solved the event issue, by using a static event that I previously linked to Malbers’ Mounting and Dismounting functions in his ‘MRider.cs’ script (FOR THIS EXACT REASON), as follows:

using RPG.Statics;
using UnityEngine;
using RPG.States.Player;
using RPG.States.PlayerOnAnimal;

public class PlayerToPlayerOnAnimalToggle : MonoBehaviour
{
    private PlayerStateMachine playerStateMachine;
    private PlayerOnAnimalStateMachine playerOnAnimalStateMachine;

    void Awake() 
    {
        playerStateMachine = GetComponent<PlayerStateMachine>();
        playerOnAnimalStateMachine = GetComponent<PlayerOnAnimalStateMachine>();
    }

    void OnEnable() 
    {
        AnimalMountManager.OnAnimalMounted += SwitchToAnimalMounted;
        AnimalMountManager.OnAnimalDismounted += SwitchToPlayerStateMachine;
    }

    void OnDisable() 
    {
        AnimalMountManager.OnAnimalMounted -= SwitchToAnimalMounted;
        AnimalMountManager.OnAnimalDismounted -= SwitchToPlayerStateMachine;
    }

    void SwitchToAnimalMounted() 
    {
        playerOnAnimalStateMachine.enabled = true;
        playerStateMachine.enabled = false;
        Debug.Log($"Switched to Animal Mount");
    }

    void SwitchToPlayerStateMachine() 
    {
        playerOnAnimalStateMachine.enabled = false;
        playerStateMachine.enabled = true;
        Debug.Log($"Switched to Player State Machine");
    }
}

HOWEVER, Now I have the problem of the character not being able to get a hold of the animal. To solve this, I’ll use a Coroutine. It’s a little expensive, but better than Update tbh

Edit: the Coroutine solved it, with a 0.1 second delay

Now I’m stuck with a brand new bug… my NormalizedTime in my ‘PlayerOnAnimalAttackingState.cs’ script is stuck at zero. It never goes up, so the player can never return to the freelook state. Can someone please have a look?:

using RPG.Combat;
using RPG.States.PlayerOnAnimal;
using UnityEngine;

public class PlayerOnAnimalAttackingState : PlayerOnAnimalBaseState
{
    public Attack currentAttack;
    private bool hasCombo;

    public PlayerOnAnimalAttackingState(PlayerOnAnimalStateMachine stateMachine, Attack attack) : base(stateMachine)
    {
        currentAttack = attack;
        stateMachine.Fighter.SetOnAnimalCurrentAttack(attack);
    }

    public PlayerOnAnimalAttackingState(PlayerOnAnimalStateMachine stateMachine, int attack) : base(stateMachine) 
    {
        currentAttack = stateMachine.Fighter.GetCurrentOnAnimalAttack(attack);
    }

    public override void Enter()
    {
        Debug.Log($"PlayerOnAnimalAttackingState entered");

        if (currentAttack == null) 
        {
            Debug.Log($"currentAttack is null, setting locomotion state");
            SetLocomotionState();
            return;
        }

        if (currentAttack.ApplyRootMotion) stateMachine.Animator.applyRootMotion = true;
        hasCombo = currentAttack.NextComboAttack != null;
        stateMachine.Animator.CrossFadeInFixedTime(currentAttack.AnimationName, currentAttack.TransitionDuration);
    }

    public override void Tick(float deltaTime)
    {
        float normalizedTime = GetNormalizedTime();
        Debug.Log($"Normalized Time: {normalizedTime}");

        if (hasCombo && normalizedTime > currentAttack.ComboAttackTime && stateMachine.InputReader.IsAttacking) 
        {
            Debug.Log($"Combo detected, switching to next combo attack");
            stateMachine.SwitchState(new PlayerOnAnimalAttackingState(stateMachine, currentAttack.NextComboAttack));
            return;
        }

        if (normalizedTime > 0.99f) 
        {
            Debug.Log($"Attack animation completed, setting locomotion state");
            SetLocomotionState();
            // return;
        }

        Move(deltaTime);
    }

    public override void Exit()
    {
        stateMachine.Animator.applyRootMotion = false;
        stateMachine.CooldownTokenManager.SetCooldown("Attack", currentAttack.Cooldown);
    }


}

We do enter the attacking state, but we can never get out of it…

Did you make the change I mentioned earlier? Your animations are running in a different layer here so you have to have the GetNormalizedTime() look at the correct layer

wait, are we talking about changing the zeros in this function to 1?:

        protected float GetNormalizedTime(string tag = "Attack")
        {
            var currentInfo = stateMachine.Animator.GetCurrentAnimatorStateInfo(0);
            var nextInfo = stateMachine.Animator.GetNextAnimatorStateInfo(0);

            if (stateMachine.Animator.IsInTransition(0) && nextInfo.IsTag(tag))
            {
                return nextInfo.normalizedTime;
            }

            else if (!stateMachine.Animator.IsInTransition(0) && currentInfo.IsTag(tag))
            {
                return currentInfo.normalizedTime;
            }

            return 0; // return something
        }

apologies, I probably missed out on it earlier. I just went back and re-read it

yup, that absolutely fixed it. All I did was change all the zeros in the previous function to ones. Thank you so much @bixarrio, I never would’ve thought about this one alone anytime soon… :sweat_smile: Anything I shouldn’t have done though?

OK I’m not sure where I went wrong, BUT… sometimes the normalized time does get stuck on 0, and the only way to fix it is to dismount and re-mount the horse. Can you please check for any pitfalls I may have missed out on?:

using RPG.Combat;
using RPG.States.PlayerOnAnimal;
using UnityEngine;

public class PlayerOnAnimalAttackingState : PlayerOnAnimalBaseState
{
    public Attack currentAttack;
    private bool hasCombo;

    public PlayerOnAnimalAttackingState(PlayerOnAnimalStateMachine stateMachine, Attack attack) : base(stateMachine)
    {
        currentAttack = attack;
        stateMachine.Fighter.SetOnAnimalCurrentAttack(attack);
    }

    public PlayerOnAnimalAttackingState(PlayerOnAnimalStateMachine stateMachine, int attack) : base(stateMachine) 
    {
        currentAttack = stateMachine.Fighter.GetCurrentOnAnimalAttack(attack);
    }

    public override void Enter()
    {
        Debug.Log($"PlayerOnAnimalAttackingState entered");

        if (currentAttack == null) 
        {
            Debug.Log($"currentAttack is null, setting locomotion state");
            SetLocomotionState();
            return;
        }

        if (currentAttack.ApplyRootMotion) stateMachine.Animator.applyRootMotion = true;
        hasCombo = currentAttack.NextComboAttack != null;
        stateMachine.Animator.CrossFadeInFixedTime(currentAttack.AnimationName, currentAttack.TransitionDuration);
    }

    public override void Tick(float deltaTime)
    {
        float normalizedTime = GetNormalizedTime();
        Debug.Log($"Normalized Time: {normalizedTime}");

        if (hasCombo && normalizedTime > currentAttack.ComboAttackTime && stateMachine.InputReader.IsAttacking) 
        {
            Debug.Log($"Combo detected, switching to next combo attack");
            stateMachine.SwitchState(new PlayerOnAnimalAttackingState(stateMachine, currentAttack.NextComboAttack));
            return;
        }

        if (normalizedTime > 0.99f) 
        {
            Debug.Log($"Attack animation completed, setting locomotion state");
            SetLocomotionState();
        }

        Move(deltaTime);
    }

    public override void Exit()
    {
        stateMachine.Animator.applyRootMotion = false;
        stateMachine.CooldownTokenManager.SetCooldown("Attack", currentAttack.Cooldown);
    }
}

It’s rare, but it still happens occasionally

and 2 other problems:

a. The faster I drive the horse, or any animal, the less the animation takes to finish. So, playing a hit-with-sword-from-top-of-mount animation, for example, takes much less time when running than it is with walking

b. I tried throwing on a ‘TryHit’, with exactly the same sword as normal, but it doesn’t deal any damage from on top of the horse (for this problem, I noticed the radius of the sword is just too small. I’ll find a way to expand it, for the special case of trying to strike an NPC from a horse, so damage can be dealt)

Edit: To try and compensate that, I reversed the changes to the animation layers that I did, so I can easily deal with the ‘horse is moving too fast to play the animation’ problem. Now, no matter what I do, the data is correct, but the animation never visually plays for some reason…

Instead, when he gets off the horse, that animation that was supposed to be playing whilst he’s driving the horse plays, and as a result, it’s completely off. The only way to turn it off, is to play another attack animation off the horse

This is getting really weird…

To fix this thing, I learned about something known as ‘Animation Layers’, and discovered they can be either overridden or additive (obviously, when driving an animal, it’ll be overridden. The mounting combat, though, is additive to that). To help with that, I created a brand new additive animation layer (to “add” to my horse driving state, to allow me to strike enemies as I drive by them without the earlier issues of “the animation ends too quickly the faster I drive the horse”, because instead of trying to run against it, THEY ADD ON TOP OF IT)

At the start, I placed an ‘Empty’ (on my brand new Animation Layer, that is), so it doesn’t go wild on the sword animation

And I set up the function which is invoked to sub and unsub to driving animals to work with this accordingly

And ‘GetNormalizedTime’ numbers were set to 2, so it can check for whatever is in the brand new layer, when the player is on top of a horse. The ones in 1 are taken care of by Malbers, so I won’t touch 'em, and I’ll leave whatever is in 0, the base layer, for when the player is on foot and not disturb them


BUT… My problem is, the moment I click that attack button, the ‘MountAttack’ layer is running repetitively, and I have absolutely no idea why. It keeps going and going and going, endlessly… That’s where I’m currently stuck!

I’m not sure where I messed up, but yes it’s an existing problem

Edit, 2 minutes later: I made sure my code returns to freelook (i.e: I replaced the ‘empty’ with something the player can return to. In my case, that would be the idle animation of the rider provided by Malbers for his horses), and gave that new Animation Layer a freelook that it can crossfade to, and it absolutely solved the problem!

Ahh, what a day… :sweat_smile:


I multiplied the damage radius by a large factor to help fix that

I don’t know what I’m doing tomorrow, but I know it’ll be exciting :slight_smile:

Edit 3: New bug… why do I have to click twice to get the Attack animation in the new layer working, and why is the first layer when my player is on the animal still running…?

I still have one major problem though, and I have no idea why it even exists. It’s a minor bug, but I want to clean off my bugs, before they become a major problem.

In order to be able to deal any sort of ‘TryHit’ damage whilst on the back of a horse (or any animal in this case) when the game starts, you literally have to deal any sort of ‘TryHit’ animation (i.e: attack anything or nothing, just launch any type of attack) whilst off the animal first, and only then will ‘TryHit’ work on the back of an animal

This happens whenever we start the game, or whenever we return to a saved file

Any idea why this is happening? This doesn’t happen with ground combat, but it happens with mounted combat, and I have absolutely zero clue as to why, but I know it needs to be fixed

Edit 1: After a little bit of debugging, in ‘TryHit’ (at the top of it), using this function:

        // if no current attack (or follow up) exists, return null:
        if (currentAttack == null) 
        {
            Debug.Log($"TryHit called, but current attack is null");
            return;
        }

I realized the ‘currentAttack’ is null (although it’s not in the inspector) if we don’t do some sort of swing off the horse first. I need to find a way to properly initialize it

Edit 2: Nevermind, I fixed it:

        // if no current attack (or follow up) exists, return null:
        if (currentAttack == null) 
        {
            if (AnimalMountManager.isOnAnimalMount) // <-- carefully controlled static variable I created to classify between when you're on an animal, and when you're not. It's linked to Malbers' scripts via Assembly References
            {
                // Assign the correct 'currentAttack' for the animal mount:
                currentAttack = GetCurrentWeaponConfig().OnAnimalAttacks[0];
            }
            else 
            {
                Debug.Log($"TryHit is called, but current attack is null");
                return;
            }
        }

and I think now would be a good time to state that I split my attack arrays into mount ones (as you can tell from the name ‘OnAnimalAttacks’), and on-foot ones, so I can get them to act accordingly (just like how I split my animation layers)

I’ll get back to this topic in a bit

this one sentence has saved me like a thousand times so far… Including today’s major bug with Getting the Normalized Time (where there’s 0, 1, 2 and 3 now), which really drilled the idea of what this function was doing properly into my mind today

Privacy & Terms