OK so, a little update (again)… I found a way to actually eliminate my AggroGroup and get rid of the problem I have alltogether, where missing enemies blocks my AggroGroup’s Capture and Restore states, by simply ensuring that my ‘RespawnManager’ does not Set the Parent of the dead enemies to null (this wasn’t a nuisance, until now. I just hope I didn’t break anything as a result), and using that info to get the parent AggroGroup, and hiding it after ‘hideTime’, as follows:
private void SpawnNPCs()
{
if (spawnedNPCs.Count < maxNPCCount)
{
// NPC SPAWNING:
Vector3 spawnPosition = GetRandomPointWithinRadius(playerTransform.position, spawnRadius);
GameObject NPCPrefab = NPCPrefabs[Random.Range(0, NPCPrefabs.Count)];
GameObject spawnedNPC = Instantiate(NPCPrefab, spawnPosition, Quaternion.identity);
// NPC PATROL PATH ASSIGNING:
PatrolPath patrolPath = patrolPaths[Random.Range(0, patrolPaths.Count)];
EnemyStateMachine enemyStateMachine = spawnedNPC.GetComponentInChildren<EnemyStateMachine>();
enemyStateMachine.AssignPatrolPath(patrolPath);
// TEST:
Health health = enemyStateMachine.Health;
if (health != null)
{
health.onDie.AddListener(() =>
{
if (health.IsDead())
{
Debug.Log($"{health.gameObject.name} Died, starting delayed AggroGroup destroy");
RespawnManager respawnManager = enemyStateMachine.GetComponentInParent<RespawnManager>();
if (respawnManager != null)
{
Debug.Log($"RespawnManager name: {respawnManager.gameObject.name}");
}
else
{
Debug.Log($"RespawnManager not found");
}
AggroGroup aggroGroup = enemyStateMachine.GetComponentInParent<AggroGroup>();
if (aggroGroup != null)
{
Debug.Log($"AggroGroup name: {aggroGroup.gameObject.name}");
}
else
{
Debug.Log($"AggroGroup not found");
}
if (respawnManager != null && aggroGroup != null)
{
Debug.Log($"AggroGroup and RespawnManager found");
float hideTime = respawnManager.GetHideTime();
StartCoroutine(DelayedAggroGroupDestroy(hideTime, aggroGroup));
}
else
{
Debug.Log($"RespawnManager and/or AggroGroup not found");
}
}
});
}
// END OF TEST
// ADDING THE NPC TO THE 'spawnedNPCs' LIST:
spawnedNPCs.Add(spawnedNPC);
}
}
private void DespawnNPCs()
{
// DELETE NPCs WHO ARE FAR AWAY FROM THE PLAYER (PERFORMANCE SAVIOR):
for (int i = spawnedNPCs.Count - 1; i >= 0; i--)
{
if (Vector3.Distance(playerTransform.position, spawnedNPCs[i].transform.position) > spawnRadius)
{
Destroy(spawnedNPCs[i]);
spawnedNPCs.RemoveAt(i);
}
}
}
// TEST FUNCTION:
private IEnumerator DelayedAggroGroupDestroy(float delay, AggroGroup enemyAggroGroup)
{
yield return new WaitForSecondsRealtime(delay);
Debug.Log($"Destroying {enemyAggroGroup.gameObject.name} in {delay} seconds");
Destroy(enemyAggroGroup.gameObject);
}
moving on, I tried re-writing the saving system to something that I can understand (no offence @Brian_Trotter , just trying to get things to work properly ):
public JToken CaptureAsJToken()
{
JObject state = new JObject();
JArray npcArray = new JArray();
JArray patrolPathArray = new JArray();
// NPCs:
foreach (var npc in NPCPrefabs)
{
JObject npcState = new JObject();
npcState["name"] = npc.name;
npcState["position"] = new JObject
{
["x"] = npc.transform.position.x,
["y"] = npc.transform.position.y,
["z"] = npc.transform.position.z
};
npcState["rotation"] = new JObject
{
["x"] = npc.transform.rotation.x,
["y"] = npc.transform.rotation.y,
["z"] = npc.transform.rotation.z,
["w"] = npc.transform.rotation.w
};
Health health = npc.GetComponentInChildren<Health>();
if (health != null)
{
npcState["health"] = health.GetHealthPoints();
}
npcArray.Add(npcState);
}
// Patrol Paths:
foreach (var patrolPath in patrolPaths)
{
JObject patrolPathState = new JObject();
patrolPathState["name"] = patrolPath.name;
// Other PatrolPath properties here, if any exist and matter
patrolPathArray.Add(patrolPathState);
}
state["NPCs"] = npcArray;
state["patrolPaths"] = patrolPathArray;
return state;
}
public void RestoreFromJToken(JToken state)
{
JObject stateObject = (JObject)state;
JArray npcsArray = (JArray)stateObject["NPCs"];
JArray patrolPathArray = (JArray)stateObject["patrolPaths"];
// Clear up the current NPCPrefabs and PatrolPath lists
spawnedNPCs.Clear();
patrolPaths.Clear();
// Load all NPC AggroGroup Prefabs from the Resources Folder:
AggroGroup[] npcPrefabs = Resources.LoadAll<AggroGroup>("NPCs");
// Resource folder-based restoration search:
foreach (var npcState in npcsArray)
{
string npcName = npcState["name"].ToString();
JObject positionObject = (JObject)npcState["position"];
Vector3 position = new Vector3
(
positionObject["x"].ToObject<float>(),
positionObject["y"].ToObject<float>(),
positionObject["z"].ToObject<float>()
);
JObject rotationObject = (JObject)npcState["rotation"];
Quaternion rotation = new Quaternion
(
rotationObject["x"].ToObject<float>(),
rotationObject["y"].ToObject<float>(),
rotationObject["z"].ToObject<float>(),
rotationObject["w"].ToObject<float>()
);
float healthValue = npcState["health"].ToObject<float>();
AggroGroup npcPrefab = npcPrefabs.FirstOrDefault(npc => npc.name == npcName);
if (npcPrefab != null)
{
GameObject spawnedNPC = Instantiate(npcPrefab.gameObject, position, rotation);
Health health = spawnedNPC.GetComponentInChildren<Health>();
if (health != null)
{
health.SetHealthPoints(healthValue); // <-- find a way to write a 'SetHealth' function in 'Health.cs'
}
spawnedNPCs.Add(spawnedNPC);
}
}
// Scene-based PatrolPath restoration search:
foreach (var patrolPathState in patrolPathArray)
{
string patrolPathName = patrolPathState["name"].ToString();
PatrolPath patrolPath = FindPatrolPathByName(patrolPathName);
if (patrolPath != null)
{
patrolPaths.Add(patrolPath);
}
}
}
private PatrolPath FindPatrolPathByName(string name)
{
PatrolPath[] allPatrolPaths = FindObjectsOfType<PatrolPath>();
foreach (PatrolPath patrolPath in allPatrolPaths)
{
if (patrolPath.name == name)
{
// if you found a patrol path
return patrolPath;
}
}
// if you can't find a patrol path
return null;
}
now, for me, I tried to keep the Patrol Path capturing and restoring scene-based, as they will stay on the game scene, but for the NPCs I tried a Resources-folder approach, by creating a new “Resources” folder for them, and placing them in a subfolder named “NPCs” for them, as follows:
but still… something is a little wrong, and I can’t identify what it is. For now, here’s my most (semi-functional) advanced script:
using System.Collections.Generic;
using RPG.Control;
using UnityEngine;
using RPG.States.Enemies;
using UnityEngine.AI;
using RPG.Combat;
using RPG.Animals;
using GameDevTV.Saving;
using Newtonsoft.Json.Linq;
using RPG.Respawnables;
using System.Collections;
using RPG.Attributes;
using System.Linq;
namespace RPG.DynamicSpawner
{
public class DynamicNPCSpawner : MonoBehaviour, IJsonSaveable
{
[SerializeField] float spawnRadius = 50f; // Radius around the player, to spawn NPCs
[SerializeField] float checkInterval = 3f; // Interval, in seconds, to check for spawning/Despawning
[SerializeField] int maxNPCCount = 4; // Maximum number of NPCs to spawn, in 'checkInterval' seconds (if this value is maxed out, don't respawn anymore)
[SerializeField] List<GameObject> NPCPrefabs; // List of NPC Prefabs to Spawn (Type 'GameObject' because even Animals can get involved down the line)
[SerializeField] List<PatrolPath> patrolPaths; // List of Patrol Paths to follow
private List<GameObject> spawnedNPCs = new List<GameObject>();
private Transform playerTransform;
private void Start()
{
playerTransform = GameObject.FindGameObjectWithTag("Player").transform;
InvokeRepeating(nameof(CheckSpawningAndDespawning), 0f, checkInterval);
}
private void CheckSpawningAndDespawning()
{
SpawnNPCs();
DespawnNPCs();
}
private void SpawnNPCs()
{
if (spawnedNPCs.Count < maxNPCCount)
{
// NPC SPAWNING:
Vector3 spawnPosition = GetRandomPointWithinRadius(playerTransform.position, spawnRadius);
GameObject NPCPrefab = NPCPrefabs[Random.Range(0, NPCPrefabs.Count)];
GameObject spawnedNPC = Instantiate(NPCPrefab, spawnPosition, Quaternion.identity);
// NPC PATROL PATH ASSIGNING:
PatrolPath patrolPath = patrolPaths[Random.Range(0, patrolPaths.Count)];
EnemyStateMachine enemyStateMachine = spawnedNPC.GetComponentInChildren<EnemyStateMachine>();
enemyStateMachine.AssignPatrolPath(patrolPath);
// TEST:
Health health = enemyStateMachine.Health;
if (health != null)
{
health.onDie.AddListener(() =>
{
if (health.IsDead())
{
Debug.Log($"{health.gameObject.name} Died, starting delayed AggroGroup destroy");
RespawnManager respawnManager = enemyStateMachine.GetComponentInParent<RespawnManager>();
if (respawnManager != null)
{
Debug.Log($"RespawnManager name: {respawnManager.gameObject.name}");
}
else
{
Debug.Log($"RespawnManager not found");
}
AggroGroup aggroGroup = enemyStateMachine.GetComponentInParent<AggroGroup>();
if (aggroGroup != null)
{
Debug.Log($"AggroGroup name: {aggroGroup.gameObject.name}");
}
else
{
Debug.Log($"AggroGroup not found");
}
if (respawnManager != null && aggroGroup != null)
{
Debug.Log($"AggroGroup and RespawnManager found");
float hideTime = respawnManager.GetHideTime();
StartCoroutine(DelayedAggroGroupDestroy(hideTime, aggroGroup));
}
else
{
Debug.Log($"RespawnManager and/or AggroGroup not found");
}
}
});
}
// END OF TEST
// ADDING THE NPC TO THE 'spawnedNPCs' LIST:
spawnedNPCs.Add(spawnedNPC);
}
}
private void DespawnNPCs()
{
// DELETE NPCs WHO ARE FAR AWAY FROM THE PLAYER (PERFORMANCE SAVIOR):
for (int i = spawnedNPCs.Count - 1; i >= 0; i--)
{
if (Vector3.Distance(playerTransform.position, spawnedNPCs[i].transform.position) > spawnRadius)
{
Destroy(spawnedNPCs[i]);
spawnedNPCs.RemoveAt(i);
}
}
}
// TEST FUNCTION:
private IEnumerator DelayedAggroGroupDestroy(float delay, AggroGroup enemyAggroGroup)
{
yield return new WaitForSecondsRealtime(delay);
Debug.Log($"Destroying {enemyAggroGroup.gameObject.name} in {delay} seconds");
Destroy(enemyAggroGroup.gameObject);
}
private Vector3 GetRandomPointWithinRadius(Vector3 center, float radius)
{
Vector3 randomPoint = Vector3.zero;
int maxAttempts = 30; // 30 Attempts to find a valid position, at most (Give up otherwise)
for (int i = 0; i < maxAttempts; i++)
{
Vector3 point = Random.insideUnitSphere * radius + center;
point.y = center.y; // spawned NPC Height = player height
// Make sure you don't Instantiate the character on a Rock, tree,
// or even underground. ONLY ON THE NAVMESHAGENT:
if (NavMesh.SamplePosition(point, out NavMeshHit hit, radius, NavMesh.AllAreas))
{
randomPoint = hit.position;
break; // Exit loop if a valid position is found
}
}
return randomPoint;
}
// ONLY ACCEPT DATATYPES OF 'Animal' OR 'AggroGroup' (ensures that everything below works as expected)
private void OnValidate()
{
for (int i = NPCPrefabs.Count - 1; i >= 0; i--)
{
GameObject NPCPrefab = NPCPrefabs[i];
if (NPCPrefab != null && !NPCPrefab.TryGetComponent(out AggroGroup _) && !NPCPrefab.TryGetComponent(out Animal _))
{
Debug.LogError($"Invalid NPC Prefab: {NPCPrefab.name} does not have an AggroGroup or an Animal Component on it", NPCPrefab);
NPCPrefabs.RemoveAt(i);
}
}
}
public JToken CaptureAsJToken()
{
JObject state = new JObject();
JArray npcArray = new JArray();
JArray patrolPathArray = new JArray();
// NPCs:
foreach (var npc in NPCPrefabs)
{
JObject npcState = new JObject();
npcState["name"] = npc.name;
npcState["position"] = new JObject
{
["x"] = npc.transform.position.x,
["y"] = npc.transform.position.y,
["z"] = npc.transform.position.z
};
npcState["rotation"] = new JObject
{
["x"] = npc.transform.rotation.x,
["y"] = npc.transform.rotation.y,
["z"] = npc.transform.rotation.z,
["w"] = npc.transform.rotation.w
};
Health health = npc.GetComponentInChildren<Health>();
if (health != null)
{
npcState["health"] = health.GetHealthPoints();
}
npcArray.Add(npcState);
}
// Patrol Paths:
foreach (var patrolPath in patrolPaths)
{
JObject patrolPathState = new JObject();
patrolPathState["name"] = patrolPath.name;
// Other PatrolPath properties here, if any exist and matter
patrolPathArray.Add(patrolPathState);
}
state["NPCs"] = npcArray;
state["patrolPaths"] = patrolPathArray;
return state;
}
public void RestoreFromJToken(JToken state)
{
JObject stateObject = (JObject)state;
JArray npcsArray = (JArray)stateObject["NPCs"];
JArray patrolPathArray = (JArray)stateObject["patrolPaths"];
// Clear up the current NPCPrefabs and PatrolPath lists
spawnedNPCs.Clear();
patrolPaths.Clear();
// Load all NPC AggroGroup Prefabs from the Resources Folder:
AggroGroup[] npcPrefabs = Resources.LoadAll<AggroGroup>("NPCs");
// Resource folder-based restoration search:
foreach (var npcState in npcsArray)
{
string npcName = npcState["name"].ToString();
JObject positionObject = (JObject)npcState["position"];
Vector3 position = new Vector3
(
positionObject["x"].ToObject<float>(),
positionObject["y"].ToObject<float>(),
positionObject["z"].ToObject<float>()
);
JObject rotationObject = (JObject)npcState["rotation"];
Quaternion rotation = new Quaternion
(
rotationObject["x"].ToObject<float>(),
rotationObject["y"].ToObject<float>(),
rotationObject["z"].ToObject<float>(),
rotationObject["w"].ToObject<float>()
);
float healthValue = npcState["health"].ToObject<float>();
AggroGroup npcPrefab = npcPrefabs.FirstOrDefault(npc => npc.name == npcName);
if (npcPrefab != null)
{
GameObject spawnedNPC = Instantiate(npcPrefab.gameObject, position, rotation);
Health health = spawnedNPC.GetComponentInChildren<Health>();
if (health != null)
{
health.SetHealthPoints(healthValue); // <-- find a way to write a 'SetHealth' function in 'Health.cs'
}
spawnedNPCs.Add(spawnedNPC);
}
}
// Scene-based PatrolPath restoration search:
foreach (var patrolPathState in patrolPathArray)
{
string patrolPathName = patrolPathState["name"].ToString();
PatrolPath patrolPath = FindPatrolPathByName(patrolPathName);
if (patrolPath != null)
{
patrolPaths.Add(patrolPath);
}
}
}
private PatrolPath FindPatrolPathByName(string name)
{
PatrolPath[] allPatrolPaths = FindObjectsOfType<PatrolPath>();
foreach (PatrolPath patrolPath in allPatrolPaths)
{
if (patrolPath.name == name)
{
// if you found a patrol path
return patrolPath;
}
}
// if you can't find a patrol path
return null;
}
}
}
@Brian_Trotter please have a look and see if this Capture and Restore attempt works or not. I’m still trying to make it work (again, Resources-folder based NPC search, and scene-based Patrol Path search)
I also placed this Setter in ‘Health.cs’, just for this saving system part:
// Used ONLY IN 'DynamicNPCSpawner.cs', to restore DYNAMIC NPCs properly:
public void SetHealthPoints(float healthPoints)
{
this.healthPoints.value = healthPoints;
}
(but because it uses a ‘.value’ (which I’m not very familiar with where it comes from and what not), I’m not sure if it’s right or wrong)
Restoring the health as we speak right now is an absolute disaster. It ALWAYS ALWAYS returns an NRE for some reason
(And one last favour… how do I ‘RemoveListener()’ what I did in ‘SpawnNPC’? I don’t think I ever encountered a scenario where I have to remove the listener of something this big, it was usually just a small function name)
So, what I want to store in the saving system, is the following (this is all that matters to me right now):
- The correct health of each NPC (optional)
- The correct position of each NPC (optional)
- The correct ‘NPCPrefabs’ list of gameObjects (mandatory)
- The correct ‘patrolPaths’ list of gameObject (mandatory)
- The correct ‘PickupSpawner’ information (mandatory). In simple terms, save and restore our pickups, just like normal fixed entity NPCs would do it
The reason the first two are ‘optional’, is because I genuinely don’t care about the status of the Spawned NPCs themselves, since they’ll be replaced anyway by a respawn when the game restarts from a clean save
And 3 and 4 will be replaced when we enter new biomes, so knowing what they are, with respect to our biomes, will be essential