diff --git a/Assets/Prefabs/PlayerWithUI.prefab b/Assets/Prefabs/PlayerWithUI.prefab index c0c5c8870..2a6bce58b 100644 --- a/Assets/Prefabs/PlayerWithUI.prefab +++ b/Assets/Prefabs/PlayerWithUI.prefab @@ -13455,7 +13455,7 @@ PrefabInstance: - target: {fileID: 1322085931752940309, guid: 62f5a4342bff4fc479aeeb8f946c8e59, type: 3} propertyPath: m_AnchoredPosition.y - value: 0.000035671357 + value: -0.00086940586 objectReference: {fileID: 0} - target: {fileID: 1322085931763303459, guid: 62f5a4342bff4fc479aeeb8f946c8e59, type: 3} @@ -18997,7 +18997,7 @@ MonoBehaviour: deselectHighlightColor: {r: 0, g: 0, b: 0, a: 1} highlightFadeDuration: 0.3 preferSkinnedMeshRenderer: 1 - autoLockSelectedTarget: 0 + autoLockSelectedTarget: 1 targetLockSystem: {fileID: 0} manualSwitchCooldownDuration: 0.75 --- !u!4 &6425420852750441961 stripped @@ -24293,7 +24293,7 @@ PrefabInstance: - target: {fileID: 1549731741953142080, guid: 38ca8b4bc26702b40a70a342950990ee, type: 3} propertyPath: m_IsActive - value: 0 + value: 1 objectReference: {fileID: 0} - target: {fileID: 1613961274298732141, guid: 38ca8b4bc26702b40a70a342950990ee, type: 3} @@ -24305,6 +24305,11 @@ PrefabInstance: propertyPath: disableObjectOnEmpty value: 1 objectReference: {fileID: 0} + - target: {fileID: 1714913128145838672, guid: 38ca8b4bc26702b40a70a342950990ee, + type: 3} + propertyPath: m_AnchoredPosition.y + value: -3.5100076 + objectReference: {fileID: 0} - target: {fileID: 1771614490565708014, guid: 38ca8b4bc26702b40a70a342950990ee, type: 3} propertyPath: m_OnClick.m_PersistentCalls.m_Calls.Array.size @@ -26475,6 +26480,11 @@ PrefabInstance: propertyPath: m_AnchoredPosition.y value: 383 objectReference: {fileID: 0} + - target: {fileID: 6241634227694161650, guid: 38ca8b4bc26702b40a70a342950990ee, + type: 3} + propertyPath: m_AnchoredPosition.y + value: -6.95 + objectReference: {fileID: 0} - target: {fileID: 6365562694919996555, guid: 38ca8b4bc26702b40a70a342950990ee, type: 3} propertyPath: m_Layer @@ -26485,6 +26495,21 @@ PrefabInstance: propertyPath: m_Layer value: 5 objectReference: {fileID: 0} + - target: {fileID: 6486847052756301725, guid: 38ca8b4bc26702b40a70a342950990ee, + type: 3} + propertyPath: m_LocalPosition.z + value: -1.3131022 + objectReference: {fileID: 0} + - target: {fileID: 6486847052756301725, guid: 38ca8b4bc26702b40a70a342950990ee, + type: 3} + propertyPath: m_AnchoredPosition.x + value: 15.71 + objectReference: {fileID: 0} + - target: {fileID: 6486847052756301725, guid: 38ca8b4bc26702b40a70a342950990ee, + type: 3} + propertyPath: m_AnchoredPosition.y + value: -5.210007 + objectReference: {fileID: 0} - target: {fileID: 6492528895270899910, guid: 38ca8b4bc26702b40a70a342950990ee, type: 3} propertyPath: m_Layer diff --git a/Assets/Scripts/Characters/EnemySpawner.cs b/Assets/Scripts/Characters/EnemySpawner.cs index 6752e5e55..7f66f1afe 100644 --- a/Assets/Scripts/Characters/EnemySpawner.cs +++ b/Assets/Scripts/Characters/EnemySpawner.cs @@ -34,8 +34,8 @@ namespace Beyond public UnityEvent m_onReceivedDamage; // Invoked when an enemy from THIS spawner receives damage private SaveData m_spawnData; - private List m_activeEnemies = new List(); - private bool _initialWaveCheckPassedThisActivation = false; // Flag to control CheckSpawn behavior per activation cycle + private List m_activeEnemies = new List(); // 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 @@ -52,20 +52,41 @@ namespace Beyond { #if ENEMIES_DISABLED && UNITY_EDITOR enabled = false; - return; + 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 = new List(); + // m_activeEnemies is already initialized at declaration, but ensuring it if not: + if (m_activeEnemies == null) m_activeEnemies = new List(); _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(); + } + _initialWaveCheckPassedThisActivation = false; + } + base.Start(); // Important: Call Saver's Start // Set up m_distanceFrom @@ -77,7 +98,7 @@ namespace Beyond { Debug.LogError($"EnemySpawner ({name}): m_distanceFrom could not be set (Player.Instance and Camera.main are null). Disabling spawner.", this); enabled = false; - return; + // If disabled here, InvokeRepeating below won't be set up. } // Subscribe to player respawn events @@ -94,8 +115,15 @@ namespace Beyond } } - // Start checking for spawn conditions - InvokeRepeating("CheckSpawn", m_spawnCheckInterval + (Random.value * m_spawnCheckInterval), m_spawnCheckInterval); + // 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() @@ -111,15 +139,19 @@ namespace Beyond } // Clean up listeners from active enemies - foreach (var ai in m_activeEnemies) + // Ensure m_activeEnemies is not null before iterating + if (m_activeEnemies != null) { - if (ai != null) + foreach (var ai in m_activeEnemies) { - ai.onDead.RemoveListener(OnEnemyDead); - ai.onReceiveDamage.RemoveListener(OnEnemyReceiveDamage); + if (ai != null) + { + ai.onDead.RemoveListener(OnEnemyDead); + ai.onReceiveDamage.RemoveListener(OnEnemyReceiveDamage); + } } + m_activeEnemies.Clear(); } - m_activeEnemies.Clear(); CancelInvoke("CheckSpawn"); // Ensure InvokeRepeating is stopped } @@ -129,54 +161,71 @@ namespace Beyond { 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(); // 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; } - // Reset the flag to allow CheckSpawn (or direct spawn here) to work for this new "life" _initialWaveCheckPassedThisActivation = false; - // Attempt to spawn a new wave if conditions are met if (!m_spawnData.waveActive && CanSpawnMoreWaves()) { if (IsPlayerInRange()) { SpawnWave(); - if (m_spawnData.waveActive) // If wave was successfully spawned by this call + if (m_spawnData.waveActive) { - _initialWaveCheckPassedThisActivation = true; // Mark as done for this cycle + _initialWaveCheckPassedThisActivation = true; } } - // else Debug.Log($"Player respawned, but spawner {name} is out of range. No wave spawned immediately by OnPlayerRespawned."); } - // else if (m_spawnData.waveActive) Debug.Log($"Player respawned, but spawner {name} still has an active wave."); - // else if (!CanSpawnMoreWaves()) Debug.Log($"Player respawned, but spawner {name} has reached its wave limit."); } 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(s); if (data != null) { m_spawnData = data; - // If loaded save indicates a wave was active but no enemies are present (they weren't saved/restored), - // mark wave as inactive. - if (m_spawnData.waveActive && m_activeEnemies.Count == 0) // m_activeEnemies is runtime only + if (m_activeEnemies == null) m_activeEnemies = new List(); // Defensive + + if (m_spawnData.waveActive && m_activeEnemies.Count == 0) { m_spawnData.waveActive = false; } - // Determine the state of _initialWaveCheckPassedThisActivation based on loaded data - // If a wave was active, or any waves had been spawned, assume the "initial check" for the - // current game session (before this load) effectively passed. - // A Reset or PlayerRespawn will clear this flag if needed for a new cycle. if (m_spawnData.waveActive || m_spawnData.wavesSpawnedCount > 0) { _initialWaveCheckPassedThisActivation = true; } else { @@ -187,6 +236,7 @@ namespace Beyond public bool IsAnyEnemyAlive() { + if (m_activeEnemies == null) return false; return m_activeEnemies.Count > 0; } @@ -206,7 +256,8 @@ namespace Beyond { return m_spawnData.wavesSpawnedCount; } - return 0; // Should ideally not happen if Awake ran + Debug.LogWarning($"EnemySpawner ({name}): GetWavesSpawnedCount called but m_spawnData is null. Returning 0.", this); + return 0; } public void SpawnWave() @@ -217,16 +268,21 @@ namespace Beyond 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(); // Defensive + if (!CanSpawnMoreWaves()) { - // This check is also in CheckSpawn/OnPlayerRespawned, but good for direct calls too - // Debug.Log($"EnemySpawner ({name}): Max wave respawn limit reached or cannot spawn more waves.", this); return; } if (m_spawnData.waveActive) { - // Debug.Log($"EnemySpawner ({name}): Wave already active, not spawning another.", this); return; } @@ -287,20 +343,31 @@ namespace Beyond } else { - // No enemies were spawned in this attempt, so the wave isn't truly "active". - // And we shouldn't count it towards wavesSpawnedCount if nothing came out. 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) // Assuming vDamage from Invector + 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(); if (deadAI != null && m_activeEnemies.Contains(deadAI)) { @@ -312,22 +379,19 @@ namespace Beyond if (m_activeEnemies.Count == 0 && m_spawnData.waveActive) { - m_spawnData.waveActive = false; // Mark wave as inactive - // DO NOT call CheckSpawn() here to prevent immediate respawn. - // DO NOT reset _initialWaveCheckPassedThisActivation here. That's for new cycles (player respawn/reset). + 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) // Should be set in Start, but as a fallback + if (m_distanceFrom == null) { if (Player.Instance != null) m_distanceFrom = Player.Instance.transform; else if (Camera.main != null) m_distanceFrom = Camera.main.transform; else { - // Debug.LogWarning($"EnemySpawner ({name}): Cannot check range, m_distanceFrom is null.", this); return false; } } @@ -338,46 +402,62 @@ namespace Beyond { if (!enabled || !gameObject.activeInHierarchy || m_prefab == null) return; - // If the "initial" spawn for this activation cycle has already happened (or attempted), - // CheckSpawn should not try again until the cycle is reset (by player death or ResetSpawner). + // 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(); + } + + if (_initialWaveCheckPassedThisActivation) { - // If a wave is active, great, CheckSpawn's job for this cycle is done. - // If no wave is active BUT wavesSpawnedCount > 0, it means the wave spawned by this cycle died. - // In this case, CheckSpawn also shouldn't immediately respawn. - // It waits for OnPlayerRespawned or ResetSpawner to clear the flag. return; } - // Try to spawn if: no wave active, can spawn more, player in range, AND initial check hasn't passed. + // 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(); - if (m_spawnData.waveActive) // If wave was successfully spawned + 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; // Mark as done for this cycle + _initialWaveCheckPassedThisActivation = true; } - // If SpawnWave failed to actually spawn anyone (e.g. no valid spots), - // waveActive will be false, and _initialWaveCheckPassedThisActivation will remain false, - // allowing CheckSpawn to try again on the next interval. } } public void ResetSpawner() { - CancelInvoke("CheckSpawn"); // Stop existing repeating call + CancelInvoke("CheckSpawn"); // Destroy active enemies - for (int i = m_activeEnemies.Count - 1; i >= 0; i--) + if (m_activeEnemies != null) { - if (m_activeEnemies[i] != null) + for (int i = m_activeEnemies.Count - 1; i >= 0; i--) { - m_activeEnemies[i].onDead.RemoveListener(OnEnemyDead); - m_activeEnemies[i].onReceiveDamage.RemoveListener(OnEnemyReceiveDamage); - Destroy(m_activeEnemies[i].gameObject); + 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(); } - m_activeEnemies.Clear(); + else + { + m_activeEnemies = new List(); // Ensure it's not null for future use + } + // Reset spawn data if (m_spawnData != null) @@ -385,17 +465,16 @@ namespace Beyond m_spawnData.wavesSpawnedCount = 0; m_spawnData.waveActive = false; } - else // Should not happen if Awake ran + 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; // Reset the flag for a new cycle + _initialWaveCheckPassedThisActivation = false; - // Restart InvokeRepeating if spawner is active if (enabled && gameObject.activeInHierarchy) { - // Re-initialize m_distanceFrom if (Player.Instance != null) m_distanceFrom = Player.Instance.transform; else if (Camera.main != null) m_distanceFrom = Camera.main.transform;