Hello again @Brian_Trotter I have a bit of a funky problem
Recently, I started working on the Respawn Manager for my animals, so they can respawn after their death and all of that stuff, and here’s my current code:
using System;
using System.Collections;
using System.Collections.Generic;
using GameDevTV.Saving;
using Newtonsoft.Json.Linq;
using RPG.Control;
using UnityEngine;
namespace RPG.Animals.Respawnables
{
[RequireComponent(typeof(JSONSaveableEntity))]
public class AnimalRespawnManager : MonoBehaviour, IJsonSaveable
{
[SerializeField] AnimalStateMachine spawnableAnimal; // the animal to be respawned
[SerializeField] private float hideTime; // how long will it take for this animal to hide before respawning
[SerializeField] private float respawnTime; // how long will this animal take until it respawns
[SerializeField] private PatrolPath patrolPath; // the patrol path for the respawned animal to follow
// AggroGroup, if you're building that
[SerializeField] bool hasBeenRestored; // has this animal been restored from a save, or do we need to restore it?
// TimeKeeper
private double destroyedTime;
private TimeKeeper timeKeeper;
private void Awake()
{
// Time Keeper, to keep track of the in-game time
timeKeeper = TimeKeeper.GetTimeKeeper();
// Respawn the NPC, if it hasn't been respawned yet
if (!hasBeenRestored)
{
Respawn();
}
}
private void Respawn()
{
var spawnedAnimal = GetSpawnedAnimal();
if (spawnedAnimal != null)
{
spawnedAnimal.Health.onDie.RemoveListener(OnDeath);
}
foreach (Transform child in transform)
{
// Destroy all components of the animal on Respawn
Destroy(child.gameObject);
}
spawnedAnimal = Instantiate(spawnableAnimal, transform);
spawnedAnimal.Health.onDie.AddListener(OnDeath);
if (patrolPath != null)
{
spawnedAnimal.AssignPatrolPath(patrolPath);
// Add Hostility down the line, depending on the type of animal
spawnedAnimal.SwitchState(new AnimalIdleState(spawnedAnimal));
}
}
// No AggroGroups, skip that mechanic
public float GetHideTime()
{
return hideTime;
}
private IEnumerator HideAnimal()
{
var spawnedAnimal = GetSpawnedAnimal();
if (spawnedAnimal == null) yield break;
yield return new WaitForSecondsRealtime(hideTime);
if (spawnedAnimal != null)
{
Destroy(spawnedAnimal.gameObject);
}
}
private void OnDeath()
{
var spawnedAnimal = GetSpawnedAnimal();
if (spawnedAnimal != null)
{
spawnedAnimal.Health.onDie.RemoveListener(OnDeath);
StartCoroutine(HideAnimal());
destroyedTime = timeKeeper.GetGlobalTime();
StartCoroutine(WaitAndRespawn());
// No AggroGroup for the animals just yet
}
}
// Animal is not an ally of the player yet, so skip this mechanic
// Animal has it's own following state, so let this go too
private IEnumerator WaitAndRespawn()
{
var elapsedTime = (float)(timeKeeper.GetGlobalTime() - destroyedTime);
yield return new WaitForSecondsRealtime(respawnTime - elapsedTime);
Respawn();
}
private bool IsDead()
{
var spawnedEnemy = GetSpawnedAnimal();
return spawnedEnemy == null || spawnedEnemy.Health.IsDead();
}
private AnimalStateMachine GetSpawnedAnimal()
{
return GetComponentInChildren<AnimalStateMachine>();
}
public JToken CaptureAsJToken()
{
JObject state = new JObject();
IDictionary<string, JToken> stateDict = state;
var isDead = IsDead();
var data = new RespawnData(destroyedTime, isDead);
stateDict["RespawnData"] = JToken.FromObject(data);
// No AggroGroup for animals just yet
// No Conversations for animals
// Save the data of the alive children of this script holder
if (!isDead)
{
var spawnedAnimal = GetSpawnedAnimal();
foreach (IJsonSaveable JSONSaveable in spawnedAnimal.GetComponents<IJsonSaveable>())
{
JToken token = JSONSaveable.CaptureAsJToken();
string component = JSONSaveable.GetType().ToString();
stateDict[component] = token;
}
}
// Animals are not allies of the player just yet
return state;
}
public void RestoreFromJToken(JToken s)
{
JObject state = s.ToObject<JObject>();
IDictionary<string, JToken> stateDict = state;
var data = default(RespawnData);
if (stateDict.TryGetValue("RespawnData", out var dataToken))
{
data = dataToken.ToObject<RespawnData>();
}
// No AggroGroup for the animal just yet
var isDead = data.IsDead;
destroyedTime = data.DestroyedTime;
// No Conversations or AIConversants for animals
// 'isPlayerAlly' setup, probably won't be there in the end
// No Target Points, MalberS will be handling the following system
// Death Checker:
if (isDead && !IsDead())
{
Debug.Log($"Should be dead, but isn't");
var spawnedAnimal = GetSpawnedAnimal();
spawnedAnimal.Health.onDie.AddListener(OnDeath);
spawnedAnimal.Health.Kill();
StartCoroutine(WaitAndRespawn());
// No AggroGroup yet
StartCoroutine(HideAnimal());
}
else if (isDead && IsDead())
{
Debug.Log($"Should be dead, and is indeed dead");
StopAllCoroutines();
StartCoroutine(WaitAndRespawn());
StartCoroutine(HideAnimal());
}
else if (!isDead && IsDead())
{
Debug.Log($"Should be alive, but is dead");
Respawn();
LoadEnemyState(stateDict);
}
else
{
Debug.Log($"Should be alive, and is alive");
LoadEnemyState(stateDict);
}
}
private void LoadEnemyState(IDictionary<string, JToken> stateDict)
{
var spawnedAnimal = GetSpawnedAnimal();
foreach (IJsonSaveable jsonSaveable in spawnedAnimal.GetComponents<IJsonSaveable>())
{
string component = jsonSaveable.GetType().ToString();
if (stateDict.ContainsKey(component))
{
jsonSaveable.RestoreFromJToken(stateDict[component]);
}
}
}
// No AggroGroup for the animal yet, so don't try to find it
[Serializable]
public struct RespawnData
{
public bool IsDead;
public double DestroyedTime;
public RespawnData(double destroyedTime, bool isDead)
{
IsDead = isDead;
DestroyedTime = destroyedTime;
}
}
}
}
But I have a major problem. Ever since this code got introduced, along with it’s architecture in the game (i.e: the Respawn Manager is an empty gameObject right above the animal, and it handles the death and re-instantiating of the animals), anywhere I go in the code, let’s say for death state for example, when the animal dies, I get this error:
NullReferenceException: Object reference not set to an instance of an object
AnimalDeathState.Enter () (at Assets/Project Backup/Scripts/State Machines/Animals/AnimalDeathState.cs:58)
RPG.States.StateMachine.SwitchState (RPG.States.State newState) (at Assets/Project Backup/Scripts/State Machines/StateMachine.cs:13)
AnimalStateMachine.<Start>b__122_0 () (at Assets/Project Backup/Scripts/State Machines/Animals/AnimalStateMachine.cs:86)
UnityEngine.Events.InvokableCall.Invoke () (at <ba783288ca164d3099898a8819fcec1c>:0)
UnityEngine.Events.UnityEvent.Invoke () (at <ba783288ca164d3099898a8819fcec1c>:0)
RPG.Attributes.Health.TakeDamage (UnityEngine.GameObject instigator, System.Single damage, RPG.Skills.Skill skill) (at Assets/Project Backup/Scripts/Attributes/Health.cs:182)
RPG.Combat.Fighter.TryHit (System.Int32 slot) (at Assets/Project Backup/Scripts/Combat/Fighter.cs:743)
Which leads to this line in ‘AnimalDeathState.Enter()’:
// 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())
{
animalDeathChecker.SetIsThisAnimalDead(true);
Debug.Log($"{stateMachine.gameObject.name} has been marked as dead");
}
(The insanely long line is the troublesome one)
And let’s say the animal comes back from a save, and try mount an animal again, I get this error:
MissingReferenceException: The object of type 'AnimalStateMachine' has been destroyed but you are still trying to access it.
Your script should either check if it is null or you should not destroy the object.
UnityEngine.MonoBehaviour.StartCoroutine (System.Collections.IEnumerator routine) (at <ba783288ca164d3099898a8819fcec1c>:0)
AnimalPatrolState.DetermineChanceOfMountSuccess (System.Action`1[T] callback) (at Assets/Project Backup/Scripts/State Machines/Animals/AnimalPatrolState.cs:290)
EquestrianismEvents.InvokeOnSkillBasedMountAttempt (System.Action`1[T] callback) (at Assets/Project Backup/Scripts/MalbersAccess/EquestrianismEvents.cs:41)
MalbersAnimations.HAP.MRider.MountAnimal () (at Assets/Malbers Animations/Common/Scripts/Riding System/Rider/MRider.cs:432)
UnityEngine.Events.InvokableCall.Invoke () (at <ba783288ca164d3099898a8819fcec1c>:0)
UnityEngine.Events.UnityEvent.Invoke () (at <ba783288ca164d3099898a8819fcec1c>:0)
MalbersAnimations.InputRow.get_GetValue () (at Assets/Malbers Animations/Common/Scripts/Input/MInput.cs:622)
MalbersAnimations.MInput.SetInput () (at Assets/Malbers Animations/Common/Scripts/Input/MInput.cs:208)
MalbersAnimations.MalbersInput.SetInput () (at Assets/Malbers Animations/Common/Scripts/Input/MalbersInput.cs:138)
MalbersAnimations.MalbersInput.Update () (at Assets/Malbers Animations/Common/Scripts/Input/MalbersInput.cs:116)
Which leads to this block in ‘AnimalPatrolState.cs’:
public void DetermineChanceOfMountSuccess(Action<bool> callback)
{
stateMachine.StartCoroutine(DelayedDetermineChanceOfMountSuccess(callback));
}
private IEnumerator DelayedDetermineChanceOfMountSuccess(Action<bool> callback)
{
yield return null;
if (stateMachine.PlayerStateMachine.SkillStore.GetNearestAnimalStateMachine().GetComponent<Animal>().GetUniqueID() == stateMachine.GetComponent<Animal>().GetUniqueID())
{
Debug.Log($"DetermineChanceOfMountSuccess called from {stateMachine.gameObject.name} Patrol 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}");
callback?.Invoke(false);
}
}
}
with line 290 being the line in ‘DetermineChanceOfMountSuccess’ (THE FUNCTION, NOT THE IENUMERATOR).
This tells me that the respawn manager failed to address ‘AnimalStateMachine’ when respawning…?! I honestly don’t know
And that second problem then goes on for literally every single animal, which tells me it’s something to do with saving and restoring the animals in the scene
What seems to be the problem here? I know it’s something because death management for these animals was not done before, but I don’t know what it is
In other words, how do we handle destroying the data and re-instantiating them after resurrection, AND MAKE SURE THAT WE LOSE THE STATE MACHINE ON DEATH ONLY AFTER WE HAVE SET THE ‘IsThisAnimalDead’ TO TRUE, IN THE ‘AnimalDeathState.Enter()’?
(I’m not yelling, just trying to highlight something anyone can potentially accidentally miss out on, xD)