OK so… I have a bit of a weird issue with this one. Here’s the problem:
In very simple terms, if my enemy respawns when I’m in Time.TimeScale = 0, and then I re-run my game’s time, he waits for a second and then permanently disappears unless I save and quit the game and then return. Why is this the case? Here’s what my code currently looks like:
using System.Collections.Generic;
using GameDevTV.Saving;
using RPG.Control;
using UnityEngine;
using RPG.Combat;
using Newtonsoft.Json.Linq;
using RPG.States.Enemies;
using System.Collections;
using System;
namespace RPG.Respawnables
{
[RequireComponent(typeof(JSONSaveableEntity))]
public class RespawnManager : MonoBehaviour, IJsonSaveable
{
[SerializeField] EnemyStateMachine spawnableEnemy; // prefab of the enemy (was 'AIController.cs' type previously)
[SerializeField] private float hideTime; // time before hiding our dead character
[SerializeField] private float respawnTime; // time before respawning our hidden dead character, as another alive, respawned character
[SerializeField] PatrolPath patrolPath; // the path our character will follow, from his spawn point
[SerializeField] AggroGroup aggroGroup; // aggrevated group of guards, based on wrong dialogue player has said
[SerializeField] bool hasBeenRestored; // checks if the enemy has been restored before fading in from the main menu, or a load scene, or not
// private AIController spawnedEnemy; // changed to EnemyStateMachine type below
// private EnemyStateMachine spawnedEnemy; // in-game instance of the enemy
// TEST (TimeKeeper)
private double destroyedTime;
private TimeKeeper timeKeeper;
// --------------------------- NOTE: RestoreState() occurs BEFORE Start(), hence we need to change Start() to Awake() --------------
private void Awake()
{
// TEST
timeKeeper = TimeKeeper.GetTimeKeeper();
// Check if the Enemy has been restored first or not, prior to Respawning him (ensuring RestoreState(), which occurs first, works properly)
if (!hasBeenRestored) Respawn();
}
// --------------------------------------------------------------------------------------------------------------------------------
private void Respawn()
{
var spawnedEnemy = GetSpawnedEnemy();
if (spawnedEnemy)
{
// Dude is not dead no longer, so delete his previous 'onDeath' record after he's respawned
spawnedEnemy.Health.onDie.RemoveListener(OnDeath);
}
foreach (Transform child in transform)
{
// Start the Respawn by deleting any existing gameObjects
Destroy(child.gameObject);
}
// Respawn the enemy, and parent the enemy to our respawnManagers' transform
spawnedEnemy = Instantiate(spawnableEnemy, transform);
// Get the spawned/respawned enemies' health, and listen for death notifications
spawnedEnemy.Health.onDie.AddListener(OnDeath);
if (patrolPath != null)
{
Debug.Log($"Assigning Patrol Path {patrolPath} to {spawnedEnemy.name}");
// spawnedEnemy.AssignPatrolPath(patrolPath); // Swapped to state machine on next line
spawnedEnemy.AssignPatrolPath(patrolPath);
spawnedEnemy.SwitchState(new EnemyPatrolState(spawnedEnemy));
}
else
{
Debug.Log($"No Patrol Path to assign");
}
// --------------------------- Extra Functionality: Setting up Aggro Group + Adding Fighters ---------------
if (aggroGroup != null)
{
aggroGroup.AddFighterToGroup(spawnedEnemy.Fighter);
if (spawnedEnemy.TryGetComponent(out DialogueAggro dialogueAggro)) //aggrogroup is at this point valid
{
dialogueAggro.SetAggroGroup(aggroGroup);
}
}
// ---------------------------------------------------------------------------------------------------------
}
void HideCharacter()
{
var spawnedEnemy = GetSpawnedEnemy();
if (spawnedEnemy == null) return;
spawnedEnemy.transform.SetParent(null);
Destroy(spawnedEnemy.gameObject);
}
void OnDeath()
{
var spawnedEnemy = GetSpawnedEnemy();
spawnedEnemy.Health.onDie.RemoveListener(OnDeath);
// TEST (Uncomment hide and respawn functions below if failed):
Invoke(nameof(HideCharacter), hideTime); // don't need to convert 'HideCharacter' to an IEnumerable, it won't take long for it to work anyway...
destroyedTime = timeKeeper.GetGlobalTime();
StartCoroutine(WaitAndRespawn());
if (aggroGroup != null)
{
aggroGroup.RemoveFighterFromGroup(spawnedEnemy.Fighter);
}
}
// TEST Function
private IEnumerator WaitAndRespawn()
{
var elapsedTime = (float)(timeKeeper.GetGlobalTime() - destroyedTime);
yield return new WaitForSecondsRealtime(respawnTime - elapsedTime);
Respawn();
}
// TEST Boolean
private bool IsDead()
{
var spawnedEnemy = GetSpawnedEnemy();
return spawnedEnemy == null || spawnedEnemy.Health.IsDead();
}
// TEST Enemy getter
private EnemyStateMachine GetSpawnedEnemy()
{
return GetComponentInChildren<EnemyStateMachine>();
}
public JToken CaptureAsJToken()
{
JObject state = new JObject();
IDictionary<string, JToken> stateDict = state;
// TEST (Adding data to the JObject Dictionary):
var isDead = IsDead();
var data = new RespawnData(destroyedTime, isDead);
stateDict["RespawnData"] = JToken.FromObject(data);
// we only care about data of alive enemies
if (!isDead)
{
var spawnedEnemy = GetSpawnedEnemy();
foreach (IJsonSaveable JSONSaveable in spawnedEnemy.GetComponents<IJsonSaveable>())
{
JToken token = JSONSaveable.CaptureAsJToken();
string component = JSONSaveable.GetType().ToString();
Debug.Log($"{name} Capture {component} = {token.ToString()}");
stateDict[component] = token;
}
}
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>();
}
var isDead = data.IsDead;
destroyedTime = data.DestroyedTime;
// Should be dead
if (isDead && !IsDead())
{
Debug.Log("Should be dead, but isn't...");
var spawnedEnemy = GetSpawnedEnemy();
Debug.Log($"Listeners before: {spawnedEnemy.Health.onDie.GetPersistentEventCount()}");
spawnedEnemy.Health.onDie.RemoveListener(OnDeath);
Debug.Log($"Listeners after: {spawnedEnemy.Health.onDie.GetPersistentEventCount()}");
Debug.Log($"Health Before: {spawnedEnemy.Health.GetHealthPoints()}");
spawnedEnemy.Health.Kill();
Debug.Log($"Health After: {spawnedEnemy.Health.GetHealthPoints()}");
StartCoroutine(WaitAndRespawn());
if (aggroGroup != null)
{
aggroGroup.RemoveFighterFromGroup(spawnedEnemy.Fighter);
}
HideCharacter();
Debug.Log($"Spawned Enemy: {GetSpawnedEnemy()}");
}
else if (isDead && IsDead())
{
Debug.Log("Should be dead, and is indeed dead...");
StopAllCoroutines();
StartCoroutine(WaitAndRespawn());
HideCharacter();
}
// Should be alive
else if (!isDead && IsDead())
{
Debug.Log("Shouldn't be dead, but is dead...");
Respawn();
LoadEnemyState(stateDict);
}
else
{
Debug.Log("Shouldn't be dead, and isn't dead...");
LoadEnemyState(stateDict);
}
}
private void LoadEnemyState(IDictionary<string, JToken> stateDict)
{
var spawnedEnemy = GetSpawnedEnemy();
foreach (IJsonSaveable jsonSaveable in spawnedEnemy.GetComponents<IJsonSaveable>())
{
string component = jsonSaveable.GetType().ToString();
if (stateDict.ContainsKey(component))
{
// NORMAL CODE (Don't delete if test failed):
Debug.Log($"{name} Restore {component} => {stateDict[component].ToString()}");
jsonSaveable.RestoreFromJToken(stateDict[component]);
}
}
}
}
[Serializable]
public struct RespawnData
{
public bool IsDead;
public double DestroyedTime;
public RespawnData(double destroyedTime, bool isDead)
{
IsDead = isDead;
DestroyedTime = destroyedTime;
}
}
}
Sorry about this again (@bixarrio you also may want to have a look at this)