490 lines
20 KiB
C#
490 lines
20 KiB
C#
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using Invector; // Assuming this is for vDamage, if not used, can be removed
|
|
using Invector.vCharacterController.AI;
|
|
using PixelCrushers; // For Saver and SaveSystem
|
|
using UnityEngine;
|
|
using UnityEngine.Events;
|
|
using UnityEngine.AI; // Required for NavMesh
|
|
|
|
namespace Beyond
|
|
{
|
|
public class EnemySpawner : Saver
|
|
{
|
|
[Header("Spawning Configuration")]
|
|
public GameObject m_prefab;
|
|
public int m_enemiesPerSpawnWave = 1;
|
|
public float m_spawnRadius = 5f;
|
|
public int m_maxSpawnAttemptsPerEnemy = 10;
|
|
public float m_navMeshSampleDistance = 1.0f;
|
|
|
|
[Header("Activation & Respawn")]
|
|
public Transform m_distanceFrom;
|
|
public float m_spawnCheckInterval = .5f;
|
|
public float m_distance = 20f;
|
|
|
|
[Tooltip("Maximum number of times a full wave can be (re)spawned. Set to -1 for infinite respawns. This count increments each time a wave is successfully initiated.")]
|
|
public int m_maxWaveRespawns = -1;
|
|
|
|
public bool m_respawnOnPlayersDeath = true;
|
|
public LayerMask raycastMask = 1; // Default layer
|
|
|
|
[Header("Events")]
|
|
public UnityEvent<EnemySpawner> m_OnDead; // Invoked when an enemy spawned by THIS spawner dies
|
|
public UnityEvent<EnemySpawner> m_onReceivedDamage; // Invoked when an enemy from THIS spawner receives damage
|
|
|
|
private SaveData m_spawnData;
|
|
private List<vControlAI> m_activeEnemies = new List<vControlAI>(); // Initialized here
|
|
private bool _initialWaveCheckPassedThisActivation = false;
|
|
|
|
const float RAY_Y_OFFSET = 30f; // How high above candidate point to start raycast
|
|
const float GROUND_Y_OFFSET = .1f; // How high above ground to place enemy pivot
|
|
|
|
// Internal class for saving spawner state
|
|
class SaveData
|
|
{
|
|
public int wavesSpawnedCount;
|
|
public bool waveActive;
|
|
// Note: _initialWaveCheckPassedThisActivation is transient and reset based on game state, not directly saved.
|
|
}
|
|
|
|
public override void Awake()
|
|
{
|
|
#if ENEMIES_DISABLED && UNITY_EDITOR
|
|
enabled = false;
|
|
Debug.LogWarning($"EnemySpawner ({name}): Disabled via ENEMIES_DISABLED directive in Awake. Critical members like m_spawnData will not be initialized by Awake if this return is hit.", this);
|
|
return; // If this path is taken, m_spawnData, m_activeEnemies, etc., below are NOT initialized by Awake.
|
|
#endif
|
|
base.Awake(); // Important: Call Saver's Awake
|
|
|
|
// Initialize critical members
|
|
m_spawnData = new SaveData
|
|
{
|
|
wavesSpawnedCount = 0,
|
|
waveActive = false
|
|
};
|
|
// m_activeEnemies is already initialized at declaration, but ensuring it if not:
|
|
if (m_activeEnemies == null) m_activeEnemies = new List<vControlAI>();
|
|
_initialWaveCheckPassedThisActivation = false; // Initialize flag
|
|
}
|
|
|
|
public override void Start()
|
|
{
|
|
// Defensive initialization: If Awake() didn't initialize m_spawnData
|
|
// (e.g., due to ENEMIES_DISABLED causing Awake to return early,
|
|
// or an exception in base.Awake()), and this component was
|
|
// subsequently enabled (or was already enabled) causing Start() to run.
|
|
if (m_spawnData == null)
|
|
{
|
|
Debug.LogWarning($"EnemySpawner ({name}): m_spawnData was null at the beginning of Start(). Initializing now. This might indicate an issue with Awake's execution path (e.g., ENEMIES_DISABLED directive was met and component re-enabled, or an exception in base.Awake()).", this);
|
|
m_spawnData = new SaveData { wavesSpawnedCount = 0, waveActive = false };
|
|
|
|
// Ensure other members typically initialized in Awake are also safe
|
|
if (m_activeEnemies == null)
|
|
{
|
|
m_activeEnemies = new List<vControlAI>();
|
|
}
|
|
_initialWaveCheckPassedThisActivation = false;
|
|
}
|
|
|
|
base.Start(); // Important: Call Saver's Start
|
|
|
|
// Set up m_distanceFrom
|
|
if (Player.Instance != null)
|
|
m_distanceFrom = Player.Instance.transform;
|
|
else if (Camera.main != null)
|
|
m_distanceFrom = Camera.main.transform;
|
|
else
|
|
{
|
|
Debug.LogError($"EnemySpawner ({name}): m_distanceFrom could not be set (Player.Instance and Camera.main are null). Disabling spawner.", this);
|
|
enabled = false;
|
|
// If disabled here, InvokeRepeating below won't be set up.
|
|
}
|
|
|
|
// Subscribe to player respawn events
|
|
if (m_respawnOnPlayersDeath && Player.Instance != null)
|
|
{
|
|
var respawner = Player.Instance.GetComponent<Respawner>(); // Assuming PixelCrushers Respawner
|
|
if (respawner != null)
|
|
{
|
|
respawner.m_onRespawned.AddListener(OnPlayerRespawned);
|
|
}
|
|
else
|
|
{
|
|
Debug.LogWarning($"EnemySpawner ({name}): Player.Instance does not have a Respawner component. m_respawnOnPlayersDeath will not function fully.", this);
|
|
}
|
|
}
|
|
|
|
// Start checking for spawn conditions only if the component is still enabled and active
|
|
if (enabled && gameObject.activeInHierarchy)
|
|
{
|
|
InvokeRepeating("CheckSpawn", m_spawnCheckInterval + (Random.value * m_spawnCheckInterval), m_spawnCheckInterval);
|
|
}
|
|
else if (!enabled)
|
|
{
|
|
Debug.Log($"EnemySpawner ({name}): Spawner is disabled at the end of Start(). CheckSpawn will not be invoked repeatedly.", this);
|
|
}
|
|
}
|
|
|
|
void OnDestroy()
|
|
{
|
|
// Unsubscribe from player respawn events
|
|
if (m_respawnOnPlayersDeath && Player.Instance != null)
|
|
{
|
|
var respawner = Player.Instance.GetComponent<Respawner>();
|
|
if (respawner != null)
|
|
{
|
|
respawner.m_onRespawned.RemoveListener(OnPlayerRespawned);
|
|
}
|
|
}
|
|
|
|
// Clean up listeners from active enemies
|
|
// Ensure m_activeEnemies is not null before iterating
|
|
if (m_activeEnemies != null)
|
|
{
|
|
foreach (var ai in m_activeEnemies)
|
|
{
|
|
if (ai != null)
|
|
{
|
|
ai.onDead.RemoveListener(OnEnemyDead);
|
|
ai.onReceiveDamage.RemoveListener(OnEnemyReceiveDamage);
|
|
}
|
|
}
|
|
m_activeEnemies.Clear();
|
|
}
|
|
|
|
CancelInvoke("CheckSpawn"); // Ensure InvokeRepeating is stopped
|
|
}
|
|
|
|
|
|
private void OnPlayerRespawned()
|
|
{
|
|
if (!m_respawnOnPlayersDeath) return;
|
|
|
|
// Ensure m_spawnData is not null
|
|
if (m_spawnData == null)
|
|
{
|
|
Debug.LogError($"EnemySpawner ({name}): m_spawnData is null in OnPlayerRespawned. Cannot proceed with respawn logic.", this);
|
|
return;
|
|
}
|
|
if (m_activeEnemies == null) m_activeEnemies = new List<vControlAI>(); // Should be initialized, but defensive
|
|
|
|
|
|
// If a wave was previously active and its enemies are now gone, mark the wave as inactive.
|
|
if (m_spawnData.waveActive && m_activeEnemies.Count == 0)
|
|
{
|
|
m_spawnData.waveActive = false;
|
|
}
|
|
|
|
_initialWaveCheckPassedThisActivation = false;
|
|
|
|
if (!m_spawnData.waveActive && CanSpawnMoreWaves())
|
|
{
|
|
if (IsPlayerInRange())
|
|
{
|
|
SpawnWave();
|
|
if (m_spawnData.waveActive)
|
|
{
|
|
_initialWaveCheckPassedThisActivation = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public override string RecordData()
|
|
{
|
|
// Ensure m_spawnData is not null before serializing
|
|
if (m_spawnData == null)
|
|
{
|
|
Debug.LogWarning($"EnemySpawner ({name}): Attempting to record data, but m_spawnData is null. Returning empty string.", this);
|
|
// Optionally, initialize it here to a default state if that makes sense for your save system logic
|
|
// m_spawnData = new SaveData { wavesSpawnedCount = 0, waveActive = false };
|
|
return string.Empty;
|
|
}
|
|
return SaveSystem.Serialize(m_spawnData);
|
|
}
|
|
|
|
public override void ApplyData(string s)
|
|
{
|
|
// Ensure m_spawnData is initialized before trying to apply data,
|
|
// though Awake or Start should have done this.
|
|
if (m_spawnData == null)
|
|
{
|
|
m_spawnData = new SaveData { wavesSpawnedCount = 0, waveActive = false };
|
|
Debug.LogWarning($"EnemySpawner ({name}): m_spawnData was null when ApplyData was called. Initialized to default.", this);
|
|
}
|
|
if (string.IsNullOrEmpty(s)) return; // No data to apply
|
|
|
|
var data = SaveSystem.Deserialize<SaveData>(s);
|
|
if (data != null)
|
|
{
|
|
m_spawnData = data;
|
|
if (m_activeEnemies == null) m_activeEnemies = new List<vControlAI>(); // Defensive
|
|
|
|
if (m_spawnData.waveActive && m_activeEnemies.Count == 0)
|
|
{
|
|
m_spawnData.waveActive = false;
|
|
}
|
|
|
|
if (m_spawnData.waveActive || m_spawnData.wavesSpawnedCount > 0) {
|
|
_initialWaveCheckPassedThisActivation = true;
|
|
} else {
|
|
_initialWaveCheckPassedThisActivation = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
public bool IsAnyEnemyAlive()
|
|
{
|
|
if (m_activeEnemies == null) return false;
|
|
return m_activeEnemies.Count > 0;
|
|
}
|
|
|
|
public bool CanSpawnMoreWaves()
|
|
{
|
|
if (m_spawnData == null) return false;
|
|
return m_maxWaveRespawns < 0 || m_spawnData.wavesSpawnedCount < m_maxWaveRespawns;
|
|
}
|
|
|
|
public bool IsExhausted()
|
|
{
|
|
return !CanSpawnMoreWaves() && !IsAnyEnemyAlive();
|
|
}
|
|
public int GetWavesSpawnedCount()
|
|
{
|
|
if (m_spawnData != null)
|
|
{
|
|
return m_spawnData.wavesSpawnedCount;
|
|
}
|
|
Debug.LogWarning($"EnemySpawner ({name}): GetWavesSpawnedCount called but m_spawnData is null. Returning 0.", this);
|
|
return 0;
|
|
}
|
|
|
|
public void SpawnWave()
|
|
{
|
|
if (m_prefab == null)
|
|
{
|
|
Debug.LogError($"EnemySpawner ({name}): m_prefab is not set!", this);
|
|
return;
|
|
}
|
|
|
|
// Ensure m_spawnData is not null
|
|
if (m_spawnData == null)
|
|
{
|
|
Debug.LogError($"EnemySpawner ({name}): m_spawnData is null in SpawnWave. Cannot proceed.", this);
|
|
return;
|
|
}
|
|
if (m_activeEnemies == null) m_activeEnemies = new List<vControlAI>(); // Defensive
|
|
|
|
if (!CanSpawnMoreWaves())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (m_spawnData.waveActive)
|
|
{
|
|
return;
|
|
}
|
|
|
|
int enemiesSuccessfullySpawned = 0;
|
|
for (int i = 0; i < m_enemiesPerSpawnWave; i++)
|
|
{
|
|
Vector3 spawnPosition = Vector3.zero;
|
|
bool foundValidPosition = false;
|
|
|
|
for (int attempt = 0; attempt < m_maxSpawnAttemptsPerEnemy; attempt++)
|
|
{
|
|
Vector2 randomCircleOffset = Random.insideUnitCircle * m_spawnRadius;
|
|
Vector3 candidateBasePosition = transform.position + new Vector3(randomCircleOffset.x, 0, randomCircleOffset.y);
|
|
Vector3 raycastStartPos = candidateBasePosition + Vector3.up * RAY_Y_OFFSET;
|
|
|
|
RaycastHit hit;
|
|
if (Physics.Raycast(raycastStartPos, Vector3.down, out hit, RAY_Y_OFFSET * 2, raycastMask))
|
|
{
|
|
Vector3 potentialSpawnPoint = hit.point + Vector3.up * GROUND_Y_OFFSET;
|
|
NavMeshHit navHit;
|
|
if (NavMesh.SamplePosition(potentialSpawnPoint, out navHit, m_navMeshSampleDistance, NavMesh.AllAreas))
|
|
{
|
|
spawnPosition = navHit.position;
|
|
foundValidPosition = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (foundValidPosition)
|
|
{
|
|
GameObject enemyGO = Instantiate(m_prefab, spawnPosition, transform.rotation, transform);
|
|
vControlAI spawnedAI = enemyGO.GetComponent<vControlAI>();
|
|
if (spawnedAI != null)
|
|
{
|
|
m_activeEnemies.Add(spawnedAI);
|
|
spawnedAI.onDead.AddListener(OnEnemyDead);
|
|
spawnedAI.onReceiveDamage.AddListener(OnEnemyReceiveDamage);
|
|
enemiesSuccessfullySpawned++;
|
|
}
|
|
else
|
|
{
|
|
Debug.LogError($"EnemySpawner ({name}): Spawned prefab '{m_prefab.name}' does not have a vControlAI component!", enemyGO);
|
|
Destroy(enemyGO);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Debug.LogWarning($"EnemySpawner ({name}): Failed to find a valid spawn position for an enemy after {m_maxSpawnAttemptsPerEnemy} attempts.", this);
|
|
}
|
|
}
|
|
|
|
if (enemiesSuccessfullySpawned > 0)
|
|
{
|
|
m_spawnData.waveActive = true;
|
|
m_spawnData.wavesSpawnedCount++;
|
|
Debug.Log($"EnemySpawner ({name}): Spawned wave {m_spawnData.wavesSpawnedCount}/{(m_maxWaveRespawns < 0 ? "infinite" : m_maxWaveRespawns.ToString())} with {enemiesSuccessfullySpawned} enemies.", this);
|
|
}
|
|
else
|
|
{
|
|
m_spawnData.waveActive = false;
|
|
Debug.LogWarning($"EnemySpawner ({name}): Attempted to spawn a wave, but no enemies were successfully placed.", this);
|
|
}
|
|
}
|
|
|
|
private void OnEnemyReceiveDamage(vDamage damageData)
|
|
{
|
|
m_onReceivedDamage?.Invoke(this);
|
|
}
|
|
|
|
private void OnEnemyDead(GameObject deadEnemyObject)
|
|
{
|
|
// Ensure m_spawnData and m_activeEnemies are not null
|
|
if (m_spawnData == null)
|
|
{
|
|
Debug.LogError($"EnemySpawner ({name}): m_spawnData is null in OnEnemyDead.", this);
|
|
return;
|
|
}
|
|
if (m_activeEnemies == null)
|
|
{
|
|
Debug.LogError($"EnemySpawner ({name}): m_activeEnemies is null in OnEnemyDead.", this);
|
|
return;
|
|
}
|
|
|
|
|
|
vControlAI deadAI = deadEnemyObject.GetComponent<vControlAI>();
|
|
if (deadAI != null && m_activeEnemies.Contains(deadAI))
|
|
{
|
|
deadAI.onDead.RemoveListener(OnEnemyDead);
|
|
deadAI.onReceiveDamage.RemoveListener(OnEnemyReceiveDamage);
|
|
m_activeEnemies.Remove(deadAI);
|
|
m_OnDead?.Invoke(this);
|
|
}
|
|
|
|
if (m_activeEnemies.Count == 0 && m_spawnData.waveActive)
|
|
{
|
|
m_spawnData.waveActive = false;
|
|
Debug.Log($"EnemySpawner ({name}): All enemies in wave died. Wave inactive. Waiting for player respawn or manual reset.", this);
|
|
}
|
|
}
|
|
|
|
private bool IsPlayerInRange()
|
|
{
|
|
if (m_distanceFrom == null)
|
|
{
|
|
if (Player.Instance != null) m_distanceFrom = Player.Instance.transform;
|
|
else if (Camera.main != null) m_distanceFrom = Camera.main.transform;
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
return (transform.position - m_distanceFrom.position).sqrMagnitude < m_distance * m_distance;
|
|
}
|
|
|
|
public void CheckSpawn() // Called by InvokeRepeating
|
|
{
|
|
if (!enabled || !gameObject.activeInHierarchy || m_prefab == null) return;
|
|
|
|
// Critical Safety check: If m_spawnData is somehow still null here
|
|
if (m_spawnData == null)
|
|
{
|
|
Debug.LogError($"EnemySpawner ({name}): m_spawnData is unexpectedly null in CheckSpawn! This indicates a severe initialization problem. Cancelling spawn checks for this spawner to prevent further errors.", this);
|
|
CancelInvoke("CheckSpawn"); // Stop trying to check
|
|
enabled = false; // Disable the component to be safe
|
|
return;
|
|
}
|
|
// Ensure m_activeEnemies is not null (should be guaranteed by declaration/Awake/Start)
|
|
if (m_activeEnemies == null)
|
|
{
|
|
Debug.LogError($"EnemySpawner ({name}): m_activeEnemies is unexpectedly null in CheckSpawn! Reinitializing. This is unusual.", this);
|
|
m_activeEnemies = new List<vControlAI>();
|
|
}
|
|
|
|
|
|
if (_initialWaveCheckPassedThisActivation)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// This is the line (or one like it) that likely caused the original NRE if m_spawnData was null
|
|
if (!m_spawnData.waveActive && CanSpawnMoreWaves() && IsPlayerInRange())
|
|
{
|
|
SpawnWave(); // SpawnWave itself has null checks for m_spawnData now
|
|
// Check m_spawnData again as SpawnWave might have failed to initialize it if an error occurred there (though unlikely with current SpawnWave structure)
|
|
if (m_spawnData != null && m_spawnData.waveActive)
|
|
{
|
|
_initialWaveCheckPassedThisActivation = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
public void ResetSpawner()
|
|
{
|
|
CancelInvoke("CheckSpawn");
|
|
|
|
// Destroy active enemies
|
|
if (m_activeEnemies != null)
|
|
{
|
|
for (int i = m_activeEnemies.Count - 1; i >= 0; i--)
|
|
{
|
|
if (m_activeEnemies[i] != null)
|
|
{
|
|
m_activeEnemies[i].onDead.RemoveListener(OnEnemyDead);
|
|
m_activeEnemies[i].onReceiveDamage.RemoveListener(OnEnemyReceiveDamage);
|
|
Destroy(m_activeEnemies[i].gameObject);
|
|
}
|
|
}
|
|
m_activeEnemies.Clear();
|
|
}
|
|
else
|
|
{
|
|
m_activeEnemies = new List<vControlAI>(); // Ensure it's not null for future use
|
|
}
|
|
|
|
|
|
// Reset spawn data
|
|
if (m_spawnData != null)
|
|
{
|
|
m_spawnData.wavesSpawnedCount = 0;
|
|
m_spawnData.waveActive = false;
|
|
}
|
|
else
|
|
{
|
|
m_spawnData = new SaveData { wavesSpawnedCount = 0, waveActive = false };
|
|
Debug.LogWarning($"EnemySpawner ({name}): m_spawnData was null during ResetSpawner. Initialized to default.", this);
|
|
}
|
|
|
|
_initialWaveCheckPassedThisActivation = false;
|
|
|
|
if (enabled && gameObject.activeInHierarchy)
|
|
{
|
|
if (Player.Instance != null) m_distanceFrom = Player.Instance.transform;
|
|
else if (Camera.main != null) m_distanceFrom = Camera.main.transform;
|
|
|
|
InvokeRepeating("CheckSpawn", m_spawnCheckInterval + (Random.value * m_spawnCheckInterval), m_spawnCheckInterval);
|
|
Debug.Log($"Spawner ({name}) has been reset. CheckSpawn restarted. Will attempt to spawn when conditions met.", this);
|
|
}
|
|
else
|
|
{
|
|
Debug.Log($"Spawner ({name}) has been reset but is currently disabled or inactive. CheckSpawn not restarted.", this);
|
|
}
|
|
}
|
|
}
|
|
} |