I have been attempting to create a souls-like character controller in Unity, using this tutorial with the use a Finite State Machine using the following tutorial.
So far, everything has been working as planned, I have been able to use some of the principles introduced in the tutorial and manipulate it in a way to fit the FSM. Character was switching between states with no problem.
However, somewhere in the middle of adding the the Inventory and Equipment logic, the state machine suddenly just... stopped working. No longer was it transitioning between states, and even weirder, not being able to properly access the CharacterStatsManager to calculate the players Max Health and Max Stamina, nor being able to change the HUD.
Every single state I have has a "Entered XXX State" log to the console to ensure that the right State has been accessed. And since this issue has begun, despite the script initializing the state machine to the Idle State, the FSM immediately switches to the Move State and does not switch to any other state.
Here are my scripts at the current moment.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Character : MonoBehaviour
{
[HideInInspector]public CharacterController controller { get; set; }
[HideInInspector]public InputHandler inputs;
[HideInInspector]public Animator animator;
[HideInInspector]public CharacterAnimationManager animationManager;
[HideInInspector]public CharacterStatsManager statsManager;
[HideInInspector]public Camera CharacterMoveCamera;
[HideInInspector]public CharacterUIManager CharacterUIManager;
[HideInInspector]public CharacterEffectsManager characterEffectsManager;
[Header("Stats")]
public int endurance;
public float currentStamina;
public int maxStamina;
[Space]
public int vitality;
public float currentHealth;
public int maxHealth;
[Header("Flags")]
public bool isPerformingAction = false;
public bool isDead = false;
#region State Machine Variables
public CharacterStateMachine StateMachine { get; set; }
public CharacterIdleState IdleState { get; set; }
public CharacterMoveState MoveState { get; set; }
public CharacterAttackState AttackState { get; set; }
public CharacterDodgeState DodgeState { get; set; }
public CharacterHurtState HurtState { get; set; }
public CharacterDeathState DeathState { get; set; }
#endregion State Machine Variables
#region ScriptableObject Variables
[SerializeField] private CharacterIdleSOBase CharacterIdleBase;
[SerializeField] private CharacterMoveSOBase CharacterMoveBase;
[SerializeField] private CharacterAttackSOBase CharacterAttackBase;
[SerializeField] private CharacterHurtSOBase CharacterHurtBase;
[SerializeField] private CharacterDodgeBaseSO CharacterDodgeBase;
[SerializeField] private DeathStateSOBase CharacterDeathBase;
public CharacterIdleSOBase CharacterIdleBaseInstance { get; set; }
public CharacterMoveSOBase CharacterMoveBaseInstance { get; set; }
public CharacterAttackSOBase CharacterAttackBaseInstance { get; set; }
public CharacterHurtSOBase CharacterHurtBaseInstance { get; set; }
public CharacterDodgeBaseSO CharacterDodgeBaseInstance { get; set; }
public DeathStateSOBase CharacterDeathBaseInstance { get; set; }
#endregion ScriptableObject Variables
protected virtual void Awake()
{
CharacterIdleBaseInstance = Instantiate(CharacterIdleBase);
CharacterMoveBaseInstance = Instantiate(CharacterMoveBase);
CharacterAttackBaseInstance = Instantiate(CharacterAttackBase);
CharacterHurtBaseInstance = Instantiate(CharacterHurtBase);
CharacterDodgeBaseInstance = Instantiate(CharacterDodgeBase);
CharacterDeathBaseInstance = Instantiate(CharacterDeathBase);
StateMachine = new CharacterStateMachine();
IdleState = new CharacterIdleState(this, StateMachine);
MoveState = new CharacterMoveState(this, StateMachine);
AttackState = new CharacterAttackState(this, StateMachine);
HurtState = new CharacterHurtState(this, StateMachine);
DodgeState = new CharacterDodgeState(this, StateMachine);
DeathState = new CharacterDeathState(this, StateMachine);
controller = GetComponent<CharacterController>();
GameObject cameraGO = GameObject.FindGameObjectWithTag("Camera");
CharacterMoveCamera = cameraGO.GetComponent<Camera>();
inputs = GetComponent<InputHandler>();
animator = GetComponent<Animator>();
animationManager = GetComponent<CharacterAnimationManager>();
statsManager = GetComponent<CharacterStatsManager>();
GameObject playerUI = GameObject.FindGameObjectWithTag("UI");
CharacterUIManager = playerUI.GetComponent<CharacterUIManager>();
characterEffectsManager = GetComponent<CharacterEffectsManager>();
}
private void Start()
{
CharacterIdleBaseInstance.Initialize(gameObject, this, inputs);
CharacterMoveBaseInstance.Initialize(gameObject, this, inputs, CharacterMoveCamera);
CharacterAttackBaseInstance.Initialize(gameObject, this, inputs);
CharacterHurtBaseInstance.Initialize(gameObject, this, inputs);
CharacterDodgeBaseInstance.Initialize(gameObject, this, inputs, CharacterMoveCamera);
CharacterDeathBaseInstance.Initialize(gameObject, this, inputs);
StateMachine.Initialize(IdleState);
maxStamina = statsManager.CalculateStaminaBasedOnEndurance(endurance);
CharacterUIManager.CharacterUI_HUD_Manager.SetMaxStaminaValue(maxStamina);
CharacterUIManager.CharacterUI_HUD_Manager.SetNewStaminaValue(maxStamina);
maxHealth = statsManager.CalculateHealthBasedOnVitality(vitality);
CharacterUIManager.CharacterUI_HUD_Manager.SetMaxHealthValue(maxHealth);
CharacterUIManager.CharacterUI_HUD_Manager.SetNewHealthValue(maxHealth);
currentStamina = maxStamina;
currentHealth = maxHealth;
}
private void Update()
{
maxStamina = statsManager.CalculateStaminaBasedOnEndurance(endurance);
CharacterUIManager.CharacterUI_HUD_Manager.SetMaxStaminaValue(maxStamina);
CharacterUIManager.CharacterUI_HUD_Manager.SetNewStaminaValue((int)currentStamina);
maxHealth = statsManager.CalculateHealthBasedOnVitality(vitality);
CharacterUIManager.CharacterUI_HUD_Manager.SetMaxHealthValue(maxHealth);
CharacterUIManager.CharacterUI_HUD_Manager.SetNewHealthValue((int)currentHealth);
statsManager.RegenerateStamina();
statsManager.ResetStaminaRegenTimer();
if(currentHealth > maxHealth)
{
currentHealth = maxHealth;
}
if(currentStamina > maxStamina)
{
currentStamina = maxStamina;
}
if(currentHealth <= 0)
{
StateMachine.ChangeState(DeathState);
}
StateMachine.CurrentCharacterState.FrameUpdate();
}
private void FixedUpdate()
{
StateMachine.CurrentCharacterState.PhysicsUpdate();
}
#region Move Functions
public void TakeAwayStamina(float staminaCost, bool isSingleCost)
{
if (isSingleCost == true)
{
currentStamina -= staminaCost;
}
else
{
currentStamina -= Time.deltaTime * staminaCost;
}
}
#endregion
#region Animation Triggers
private void AnimationTriggerEvent(AnimationTriggerType triggerType)
{
StateMachine.CurrentCharacterState.AnimationTriggerEvent(triggerType);
}
public enum AnimationTriggerType
{
Example1,
Example2
}
#endregion Animation Triggers
#region Events
#region CoRoutines
private IEnumerator RewardPlayerCoRoutine()
{
yield return new WaitForSeconds(5);
// AWARD SOULS/RUNES/CURRENCY
Debug.Log(gameObject.name + " is defeated!");
// DISABLE CHARACTER
Destroy(gameObject);
}
#endregion
public void RewardPlayer()
{
StartCoroutine(RewardPlayerCoRoutine());
}
#endregion
}
using UnityEngine;
public class Player : Character
{
[Space]
[HideInInspector]public PlayerInventoryManager playerInventoryManager;
[HideInInspector]public PlayerEquipmentManager playerEquipmentManager;
[HideInInspector]public PlayerCombatManager playerCombatManager;
[Space]
public bool isUsingRightHand;
public bool isUsingLeftHand;
[Header("DEBUG")]
public bool switchRightWeapon;
public bool switchLeftWeapon;
protected override void Awake()
{
base.Awake();
playerInventoryManager = GetComponent<PlayerInventoryManager>();
playerEquipmentManager = GetComponent<PlayerEquipmentManager>();
playerCombatManager = GetComponent<PlayerCombatManager>();
}
private void Update()
{
if (switchRightWeapon == true)
{
switchRightWeapon = false;
playerEquipmentManager.SwitchRightWeapon();
}
else if (switchLeftWeapon == true)
{
switchLeftWeapon = false;
playerEquipmentManager.SwitchLeftWeapon();
}
}
public void SetCharacterActionHand(bool UsingRightHand)
{
if(UsingRightHand == true)
{
isUsingRightHand = true;
isUsingLeftHand = false;
}
else
{
isUsingRightHand = false;
isUsingLeftHand = true;
}
}
}
As mentioned, I have a console log at the start of each state to make sure it gets transitioned to, but as soon as the game plays, the state machine immediately switches to the Move State
I have removed [HideInInspector]
from the references in both scripts to make sure that there wasn't a NullReferenceException error, however that is not the case
I have also included this logic, despite the fact all current States, including the Move State, have this logic. I wanted to see if I could force the player to transition to the Death State, but unfortunately the players currentHealth
drops below 0 and the player still remains alive, as well as the HUD being unresponsive.
if(currentHealth <= 0)
{
StateMachine.ChangeState(DeathState);
}
It looks like the Update method of the base class is being hidden by the derived class.
public class Character : MonoBehaviour
{
private void Update()
{
// ..base class code
}
}
public class Player : Character
{
private void Update()
{
// ..derived class code
}
}
Since the base class is not allowing the derived class to override the Update method, the base class Update will not run. In this way, the base classes Update method is hidden by the redefined Update method on the derived class.
To resolve the issue, expose the Update method of the base class via protected virtual Update
signature and override in the derived class.
public class Character : MonoBehaviour
{
protected virtual void Update()
{
// ..base class code
}
}
public class Player : Character
{
protected override void Update()
{
base.Update(); // < call the base classes Update method
// ..derived class code
}
}