OK so… a bit of an update. Here’s my latest script, Ignore the previous one:
using System.Collections.Generic;
using RPG.Control;
using UnityEngine;
using RPG.States.Enemies;
using RPG.Animals;
using RPG.Combat;
using UnityEngine.AI;
using GameDevTV.Saving;
using Newtonsoft.Json.Linq;
namespace RPG.DynamicSpawner
{
[RequireComponent(typeof(JSONSaveableEntity))]
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);
// 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);
}
}
}
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 'EnemyStateMachine'
private void OnValidate()
{
for (int i = NPCPrefabs.Count - 1; i >= 0; i--)
{
GameObject NPCPrefab = NPCPrefabs[i];
if (NPCPrefab != null && !NPCPrefab.TryGetComponent(out AggroGroup _ /* don't care about the name */) && !NPCPrefab.TryGetComponent(out Animal _ /* don't care about the name */))
{
Debug.LogError($"Invalid NPC Prefab: {NPCPrefab.name} does not have an AggroGroup or an Animal Component on it", NPCPrefab);
NPCPrefabs.RemoveAt(i);
}
}
}
// SAVING
public JToken CaptureAsJToken()
{
JArray stateArray = new JArray();
Debug.Log($"Capturing NPC States...");
foreach (var npc in spawnedNPCs)
{
JObject npcState = new JObject();
npcState["prefabIndex"] = NPCPrefabs.IndexOf(npc);
npcState["position"] = new JArray(npc.transform.position.x, npc.transform.position.y, npc.transform.position.z);
var enemyStateMachine = npc.GetComponentInChildren<EnemyStateMachine>();
npcState["patrolPathIndex"] = patrolPaths.IndexOf(enemyStateMachine.GetAssignedPatrolPath());
stateArray.Add(npcState);
}
Debug.Log($"Capturing Completed");
return stateArray;
}
// RESTORING
public void RestoreFromJToken(JToken state)
{
JArray stateArray = (JArray) state;
Debug.Log($"Restoring NPC State...");
foreach (JObject npcState in stateArray)
{
int prefabIndex = npcState["prefabIndex"].ToObject<int>();
/* if (prefabIndex < 0 || prefabIndex >= NPCPrefabs.Count)
{
Debug.LogError($"Invalid Prefab Index: {prefabIndex}");
continue;
} */
JArray positionArray = (JArray)npcState["position"];
Vector3 position = new Vector3(
positionArray[0].ToObject<float>(),
positionArray[1].ToObject<float>(),
positionArray[2].ToObject<float>()
);
int patrolPathIndex = npcState["patrolPathIndex"].ToObject<int>();
/* if (patrolPathIndex < 0 || patrolPathIndex >= patrolPaths.Count)
{
Debug.LogError($"Invalid Patrol Path Index: {patrolPathIndex}");
continue;
} */
GameObject npcPrefab = NPCPrefabs[prefabIndex];
GameObject spawnedNPC = Instantiate(npcPrefab, position, Quaternion.identity);
PatrolPath patrolPath = patrolPaths[patrolPathIndex];
EnemyStateMachine enemyStateMachine = spawnedNPC.GetComponentInChildren<EnemyStateMachine>();
enemyStateMachine.AssignPatrolPath(patrolPath);
spawnedNPCs.Add(spawnedNPC);
}
Debug.Log($"Restore Completed");
}
}
}
To avoid confusing myself, I decided I want to stick to my own version of dynamically spawning NPCs.
The problem I have is, is that whilst this saving system does not give me any sort of runtime errors (whenever I keep the ‘if’ statements that I commented out, because they’re the source of a logical error, and deleting them gives me an ‘ArgumentOutOfRangeIndex’), it does not restore them to their precise precisions or what not (@Brian_Trotter please help me out )
I know this is a bit of a vague question, but… what should I do to get this system to restore the health, position and all of that stuff with the correct values? (These will be the important variables for me: their health, the position, the patrol paths and the type of enemy on that list of ‘NPCPrefabs’)… I’m not sure if I want dialogues in this or not)
And like I said earlier, I’ll stick to programming mine from scratch. Not to sound too arrogant, but I just find setting up the other one a bit confusing )
At this point in time, I can’t really think of a lot of advantages of saving and restoring dynamically spawned NPCs, but I’m doing it JUST IN CASE in the future something happens that warrants this system to be functional
My plan, assuming Brian doesn’t come up with something amazing in the future, is that Quest Givers, Team mates and other important NPCs in my game shall be Fixed Entities (which can respawn. Essentially, they’ll be the type of characters that my current guards are at), and other NPCs that keep the game alive and fun will be dynamic.
The idea behind those dynamic characters will be, in the future, that whenever you enter a different biome, your list of enemies, animals and patrol paths will be changed through an ‘OnTriggerEnter’ that triggers when a biome is entered, so that creatures from each biome don’t accidentally spawn in another one, and they’ll be done on a much larger scale than the current prototype scale
Personally, I think having dynamic spawning occur suddenly and disappear suddenly can remind you that you’re in a video game. I don’t know to be honest if there should be fading, keeping it when the player isn’t looking and what not, etc…
AT THE VERY VERY LEAST I WANT TO SAVE WHAT THE BIOME CURRENTLY HAS IN DATA, SO THAT MY CREATURES KNOW WHAT TO SPAWN, AND WHERE TO GO