Set this up as a question by itself under Dialogues and Quests. I can tell you that it’s not properly fixed because this is not a problem in the course version of the code. You shouldn’t need a variant, you should just be able to check if the Fighter is enabled, and return false if it isn’t.
Yup, that worked. Here’s my code if anyone wants to have a look at it:
In SerializeField zone:
[SerializeField] Health thisCharacterHealth;
In the HandleRaycast Function, under Dialogue Check:
if (thisCharacterHealth.IsDead()) { return false; }
and don’t forget ‘using RPG.Attributes’ at the top
Hi Brian, thank you for the script, but the instructions on that are a bit unclear. How do we use this new knowledge to make this system work?
On Udemy? Will do
Ok I hope this isn’t considered necroing a thread but my console is getting spammed with warnings and errors after trying this. I am not trying to do anything with Patrol Paths – just doing the basics. I also applied Brian’s fix to OnValidate but it just moved the errors and warnings around but didn’t actually get rid of them.
It seems to work fine but the spamming of errors and warnings is too much. It makes it hard to use my logs.
here’s what I’m seeing
- I’m getting multiple instances of this error upon entering play mode. 2 separate batches of 5-6 instances per RespawnManager
Destroying GameObjects immediately is not permitted during physics trigger/contact, animation event callbacks, rendering callbacks or OnValidate. You must use Destroy instead.
UnityEngine.Object:DestroyImmediate (UnityEngine.Object)
RPG.Respawnables.RespawnManager:OnValidate () (at Assets/Scripts/Respawnables/RespawnManager.cs:90)
- Also getting multiple variations of this warning upon entering play mode. 2 sets of ~13 warnings
SendMessage cannot be called during Awake, CheckConsistency, or OnValidate (Canvas: OnCanvasHierarchyChanged)
UnityEngine.Object:Instantiate<RPG.Control.AIController> (RPG.Control.AIController,UnityEngine.Transform)
RPG.Respawnables.RespawnManager:OnValidate () (at Assets/Scripts/Respawnables/RespawnManager.cs:92)
- A few instances of this warning after entering playmode
Failed to create agent because there is no valid NavMesh
UnityEngine.Object:Instantiate<RPG.Control.AIController> (RPG.Control.AIController,UnityEngine.Transform)
RPG.Respawnables.RespawnManager:OnValidate () (at Assets/Scripts/Respawnables/RespawnManager.cs:92)
- A few instances of this one after exiting playmode
Destroying GameObjects immediately is not permitted during physics trigger/contact, animation event callbacks, rendering callbacks or OnValidate. You must use Destroy instead.
UnityEngine.Object:DestroyImmediate (UnityEngine.Object)
RPG.Respawnables.RespawnManager:OnValidate () (at Assets/Scripts/Respawnables/RespawnManager.cs:90)
- A few instances of this one again
SendMessage cannot be called during Awake, CheckConsistency, or OnValidate (Canvas: OnCanvasHierarchyChanged)
UnityEngine.Object:Instantiate<RPG.Control.AIController> (RPG.Control.AIController,UnityEngine.Transform)
RPG.Respawnables.RespawnManager:OnValidate () (at Assets/Scripts/Respawnables/RespawnManager.cs:92)
- and then a few instances of this other one triggered on Awake
Destroy may not be called from edit mode! Use DestroyImmediate instead.
Destroying an object in edit mode destroys it permanently.
UnityEngine.Object:Destroy (UnityEngine.Object)
RPG.Respawnables.RespawnManager:Respawn () (at Assets/Scripts/Respawnables/RespawnManager.cs:63)
RPG.Respawnables.RespawnManager:Awake () (at Assets/Scripts/Respawnables/RespawnManager.cs:28)
My “fix” is to
- Convert the Awake() to a Start. Same code otherwise.
private void Start()
{
Respawn();
}
- Remove OnValidate()
This fix seems to work but it has some behaviour I am confused by.
When I start a play mode session with no child objects and then exit playmode, the SpawnableEnemy it spawns is retained as a child object even after play mode ends. Why does it not disappear at the end of play mode (like a spawned weapon or spawned fireball?). Again I’m starting off with no child objects in edit mode so I don’t understand why something created during run time persists when I exit play mode?
EDIT: It seems what’s happening is that the Start method is being called when I exit playmode which is triggering a Respawn. Ok but why is start being called? Is it because we’re inheriting from SaveableEntity which has this [ExecuteAlways] at the top?
I’ll note that I still get this error right when I exit play mode even after my “fix” which seems to provide evidence that Start is being called when I exit play mode.
Destroy may not be called from edit mode! Use DestroyImmediate instead.
Destroying an object in edit mode destroys it permanently.
UnityEngine.Object:Destroy (UnityEngine.Object)
RPG.Respawnables.RespawnManager:Respawn () (at Assets/Scripts/Respawnables/RespawnManager.cs:69)
RPG.Respawnables.RespawnManager:Start () (at Assets/Scripts/Respawnables/RespawnManager.cs:33)
I’m now looking into this Procedurally Spawned Characters as a possible solution.
This no longer works in Unity 2022 and later. I learned this the hard way when I moved SpellbornHunter from Unity 2021 to Unity 2022.
The whole trick was designed so that you could change the prefab and then it would automagically spawn the cloned character as a place holder. This addresses pretty much your entire thread of issues. You simply can’t use this OnValidate logic at all.
What you can do is in Start(), you can destroy everything under the spawner and then spawn the character. This means, of course, though, that you’ll have to add the preview character manually.
Thanks. What explains Start getting called after I exit play mode? Is it my guess of having [ExecuteAlways] on the class we inherit from?
Assuming it’s that, then I will resolve this.
I think my path will be to use the “Procedurally Spawned Characters” solution you created. I tried it and added a reply to your post on a few places I got stuck.
See what happens when you pull ExecuteAlways…
There is actually another flaw in the ointment… The character isn’t spawned until Start() but if you call RestoreState() you’ll get an NRE… (Was just covering this with another student on Udemy)
The solution:
bool hasBeenRestored;
void Start()
{
if(!hasBeenRestored) Respawn();
}
And as the first line in RestoreState()
Respawn();
hasBeenRestored=true;
What did you mean by “pull”. As is use [ExecuteAlways] or remove [ExecuteAlways]. I think you meant the former because I didn’t remove it.
I was uncomfortable with [ExecuteAlways] and how we’re generating GUIDs from the moment I saw it I was totally expecting something like this thread would happen at some point in the not too distant future.
Pull as in remove it… American English for “Pull that out of the code”
I don’t recall putting it in the code in the first place.
OnValidate does not require ExecuteAlways. It’s only really useful for Update() type setups (sort of like what Sam did in SaveableEntity.) It’s actually more efficient to just let OnValidate() generate a UUID.
Ah I misinterpreted your comment because I didn’t have it on my derived class. I didn’t think of modifying the base class. When I tested with it off the base class and it the Start behavior function as expected. But then I have to add it back to make the rest of the game work. I think the solution will be using your RPG.Dynamic stuff – working through your feedback there. Thanks!
Hey again Brian. This is not a problem (believe it or not, my life cycle is slowly coming to an end… but I have a major challenge coming up, kinda like the final boss of all of this), just a quick check-up on whether what I’m doing is right or wrong. I recently started trying to figure out how to re-do this tutorial for my third person integration, and whilst most of the issues were simply solved by changing ‘AIController’ data types to ‘EnemyStateMachine’ (I think… not sure yet, there’s a plethora of testing that I haven’t done yet), one of them stood out as a unique problem
Assigning PatrolPath was a serious pain, mainly because… well… the function is in our deleted ‘AIController.cs’ (which I ended up placing back on my NPCs and deactivating, because of a stored value in there I couldn’t be bothered hunting down). So what I did instead, was before assigning the enemy to his patrolling state, I assigned him a patrol path function that I created in ‘EnemyStateMachine.cs’, and that path is gained from the ‘EnemyStateMachine.cs’ script in the inspector itself, something this simple (in ‘EnemyStateMachine.cs’):
// Test function
public void AssignPatrolPath(PatrolPath newPatrolPath)
{
PatrolPath = newPatrolPath;
}
and in ‘RespawnManager.cs’, this is what it looks like:
if (patrolPath != null)
{
Debug.Log($"Assigning Patrol Path {patrolPath} to {spawnedEnemy.name}");
// spawnedEnemy.AssignPatrolPath(patrolPath); // Swapped to state machine on next line
// TEST
spawnedEnemy.AssignPatrolPath(patrolPath);
// Switching to Patrolling State
spawnedEnemy.SwitchState(new EnemyPatrolState(spawnedEnemy));
// Update();
}
It works, just checking that it won’t cause any unexpected issues… right? (although if we can integrate a random waiting time between patrol points again, that would be nice)
This should work fine, although as long as you’re in the same frame, simply assigning the Patrol Path should cause the IdleState to pick up that there is a patrol path on enter and call the PatrolState (IdleState is sort of a dispatcher state in that regard)
any better alternatives?
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)
Time.TimeScale=0 and WaitForSecondsRealtime don’t mix very well…
No guarantees, but try this:
private IEnumerator WaitAndRespawn()
{
var elapsedTime = (float)(timeKeeper.GetGlobalTime() - destroyedTime);
yield return new WaitForSecondsRealtime(respawnTime - elapsedTime);
while(Time.TimeScale==0)
{
yield return null;
}
Respawn();
}
mmm… nope, he still vanishes if he reloads and I’m pausing my game, and this one has been a bug for quite a bit - any other ideas? Maybe we can eliminate the RealTime solution, it was something I quickly thought of back then for a hot fix, unknowing its issues
Yesterday I tampered with unscaled time to try fix this and failed again. Let’s just say… I need help with this one (it’s an obvious bug unfortunately, happens 100% of the time)
That would generally be my suggestion… Unless you’re running a client-server based game (i.e. multiplayer), you’re generally better off not messing with Realtime.
The only problem I currently have is that if I use ‘WaitForSeconds’ instead, the respawner works on local time, then that means the enemy timer won’t countdown, and he won’t respawn unless the player is not paused… not exactly how I want things to work for me - if you have any updates about this, please let me know