new spawning manager, spawner improvements - now with multiple enemies to spawn, KillTrigger

This commit is contained in:
2025-05-21 13:40:14 +02:00
parent 777c90ce50
commit 07eff8a72c
7 changed files with 705 additions and 155 deletions

View File

@@ -3177,6 +3177,18 @@ Transform:
type: 3}
m_PrefabInstance: {fileID: 789096691}
m_PrefabAsset: {fileID: 0}
--- !u!114 &579328271 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 5278190478906961763, guid: dbf14178717e1164086bd444126f4446,
type: 3}
m_PrefabInstance: {fileID: 789096691}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 01a49bbd2406d4a5f8ae5ad40326117b, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!21 &581839214
Material:
serializedVersion: 8
@@ -11216,7 +11228,7 @@ PrefabInstance:
- target: {fileID: 5278190478906961763, guid: dbf14178717e1164086bd444126f4446,
type: 3}
propertyPath: m_distance
value: 20
value: 200
objectReference: {fileID: 0}
- target: {fileID: 5278190478906961763, guid: dbf14178717e1164086bd444126f4446,
type: 3}
@@ -12619,7 +12631,7 @@ PrefabInstance:
- target: {fileID: 5278190478906961763, guid: dbf14178717e1164086bd444126f4446,
type: 3}
propertyPath: m_distance
value: 20
value: 200
objectReference: {fileID: 0}
- target: {fileID: 5278190478906961763, guid: dbf14178717e1164086bd444126f4446,
type: 3}
@@ -12640,6 +12652,7 @@ GameObject:
serializedVersion: 6
m_Component:
- component: {fileID: 796152178}
- component: {fileID: 796152179}
m_Layer: 0
m_Name: 1= ENEMIES Spawners
m_TagString: Untagged
@@ -12667,6 +12680,40 @@ Transform:
- {fileID: 579328270}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &796152179
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 796152177}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 6ce0b24e87d404b299a748ccc1671160, type: 3}
m_Name:
m_EditorClassIdentifier:
spawnerConfigurations:
- spawner: {fileID: 1804595575}
overridePrefab: {fileID: 145182807891766407, guid: e115436bfe06bd447a266ca75621bbdd,
type: 3}
overrideEnemiesPerWave: 3
- spawner: {fileID: 1238364584}
overridePrefab: {fileID: 8406365487746723040, guid: fcfa2b03d43105e43a8c6b1931305099,
type: 3}
overrideEnemiesPerWave: 5
- spawner: {fileID: 1491841298}
overridePrefab: {fileID: 3165479696603231053, guid: 5e1c7bd492ab28047bf1e68ef6ae8b9f,
type: 3}
overrideEnemiesPerWave: 4
- spawner: {fileID: 579328271}
overridePrefab: {fileID: 974380812503253259, guid: fe3f8cbdaa5d03147a0de1c74712d32b,
type: 3}
overrideEnemiesPerWave: 4
globalOverridePrefab: {fileID: 0}
globalOverrideEnemiesPerWave: -1
overrideOnStart: 1
autoEnableSpawners: 1
disableSpawnersOnStart: 1
--- !u!1 &799294618
GameObject:
m_ObjectHideFlags: 0
@@ -13389,7 +13436,7 @@ Material:
m_Offset: {x: 0, y: 0}
m_Ints: []
m_Floats:
- _AlphaPow: -1.89
- _AlphaPow: 2
- _BlendMode: 1
- _BumpScale: 1
- _CullMode: 2
@@ -15472,6 +15519,11 @@ PrefabInstance:
propertyPath: m_Layer
value: 8
objectReference: {fileID: 0}
- target: {fileID: 4460069056851369441, guid: 851e8e61247888340bdec90fc8aa37f5,
type: 3}
propertyPath: m_IsActive
value: 1
objectReference: {fileID: 0}
- target: {fileID: 4460112240072912095, guid: 851e8e61247888340bdec90fc8aa37f5,
type: 3}
propertyPath: m_Layer
@@ -16660,12 +16712,12 @@ PrefabInstance:
- target: {fileID: 5657452459766331955, guid: 851e8e61247888340bdec90fc8aa37f5,
type: 3}
propertyPath: m_AnchorMax.x
value: 0
value: 1
objectReference: {fileID: 0}
- target: {fileID: 5657452459766331955, guid: 851e8e61247888340bdec90fc8aa37f5,
type: 3}
propertyPath: m_AnchorMax.y
value: 0
value: 1
objectReference: {fileID: 0}
- target: {fileID: 5671630659928648966, guid: 851e8e61247888340bdec90fc8aa37f5,
type: 3}
@@ -16707,6 +16759,11 @@ PrefabInstance:
propertyPath: selectedToolbar
value: 0
objectReference: {fileID: 0}
- target: {fileID: 5935883485110830931, guid: 851e8e61247888340bdec90fc8aa37f5,
type: 3}
propertyPath: m_IsActive
value: 1
objectReference: {fileID: 0}
- target: {fileID: 6116517633565365760, guid: 851e8e61247888340bdec90fc8aa37f5,
type: 3}
propertyPath: m_Layer
@@ -16792,6 +16849,11 @@ PrefabInstance:
propertyPath: m_Layer
value: 8
objectReference: {fileID: 0}
- target: {fileID: 6783963330927412624, guid: 851e8e61247888340bdec90fc8aa37f5,
type: 3}
propertyPath: m_IsActive
value: 1
objectReference: {fileID: 0}
- target: {fileID: 6786312951323477463, guid: 851e8e61247888340bdec90fc8aa37f5,
type: 3}
propertyPath: m_IsActive
@@ -17704,6 +17766,18 @@ Material:
- _SpecColor: {r: 1, g: 1, b: 1, a: 1}
m_BuildTextureStacks: []
m_AllowLocking: 1
--- !u!114 &1238364584 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 5278190478906961763, guid: dbf14178717e1164086bd444126f4446,
type: 3}
m_PrefabInstance: {fileID: 636119092}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 01a49bbd2406d4a5f8ae5ad40326117b, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!21 &1242034730
Material:
serializedVersion: 8
@@ -18394,7 +18468,7 @@ PrefabInstance:
- target: {fileID: 5278190478906961763, guid: dbf14178717e1164086bd444126f4446,
type: 3}
propertyPath: m_distance
value: 20
value: 200
objectReference: {fileID: 0}
- target: {fileID: 5278190478906961763, guid: dbf14178717e1164086bd444126f4446,
type: 3}
@@ -19395,6 +19469,18 @@ Material:
- _SpecColor: {r: 1, g: 1, b: 1, a: 1}
m_BuildTextureStacks: []
m_AllowLocking: 1
--- !u!114 &1491841298 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 5278190478906961763, guid: dbf14178717e1164086bd444126f4446,
type: 3}
m_PrefabInstance: {fileID: 1316222306}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 01a49bbd2406d4a5f8ae5ad40326117b, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!4 &1516777619 stripped
Transform:
m_CorrespondingSourceObject: {fileID: 6425420852750441961, guid: 851e8e61247888340bdec90fc8aa37f5,
@@ -20885,7 +20971,7 @@ PrefabInstance:
- target: {fileID: 5278190478906961763, guid: dbf14178717e1164086bd444126f4446,
type: 3}
propertyPath: m_distance
value: 20
value: 200
objectReference: {fileID: 0}
- target: {fileID: 5278190478906961763, guid: dbf14178717e1164086bd444126f4446,
type: 3}
@@ -20903,6 +20989,18 @@ Transform:
type: 3}
m_PrefabInstance: {fileID: 1804595573}
m_PrefabAsset: {fileID: 0}
--- !u!114 &1804595575 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 5278190478906961763, guid: dbf14178717e1164086bd444126f4446,
type: 3}
m_PrefabInstance: {fileID: 1804595573}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 01a49bbd2406d4a5f8ae5ad40326117b, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!1001 &1819555269
PrefabInstance:
m_ObjectHideFlags: 0

View File

@@ -1,171 +1,411 @@
using System.Collections;
using System.Collections.Generic;
using Invector;
using Invector; // Assuming this is for vDamage, if not used, can be removed
using Invector.vCharacterController.AI;
using PixelCrushers;
using PixelCrushers; // For Saver and SaveSystem
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.AI; // Required for NavMesh
namespace Beyond
{
public class EnemySpawner : Saver
{
public GameObject m_prefab;
public Transform m_distanceFrom;
//public bool m_distanceFromMainCam;
public float m_spawnCheckInterval = .5f;
public float m_distance = 20f;
public int m_numToSpawn = -1;
public bool m_respawnOnPlayersDeath = true;
public LayerMask raycastMask = 1; //default
private SaveData m_spawnData;
//private bool m_spawned;
private vControlAI m_spawnedAI;
public UnityEvent<EnemySpawner> m_OnDead;
public UnityEvent<EnemySpawner> m_onReceivedDamage;
class SaveData
[Header("Spawning Configuration")]
public GameObject m_prefab;
public int m_enemiesPerSpawnWave = 3;
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>();
private bool _initialWaveCheckPassedThisActivation = false; // Flag to control CheckSpawn behavior per activation cycle
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;
return;
#endif
base.Awake(); // Important: Call Saver's Awake
m_spawnData = new SaveData
{
wavesSpawnedCount = 0,
waveActive = false
};
m_activeEnemies = new List<vControlAI>();
_initialWaveCheckPassedThisActivation = false; // Initialize flag
}
public override void Start()
{
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;
return;
}
// 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
InvokeRepeating("CheckSpawn", m_spawnCheckInterval + (Random.value * m_spawnCheckInterval), m_spawnCheckInterval);
}
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
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;
// 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
{
_initialWaveCheckPassedThisActivation = true; // Mark as done for this cycle
}
}
// 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()
{
return SaveSystem.Serialize(m_spawnData);
}
public override void ApplyData(string s)
{
var data = SaveSystem.Deserialize<SaveData>(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
{
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 {
_initialWaveCheckPassedThisActivation = false;
}
}
}
public bool IsAnyEnemyAlive()
{
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()
{
public int numSpawned;
public bool spawned;
if (m_spawnData != null)
{
return m_spawnData.wavesSpawnedCount;
}
return 0; // Should ideally not happen if Awake ran
}
public override void Awake()
{
#if ENEMIES_DISABLED && UNITY_EDITOR
enabled = false;
return;
#endif
base.Awake();
//m_spawned = false;
m_spawnData = new SaveData();
m_spawnData.numSpawned = 0;
m_spawnData.spawned = false;
}
public void SpawnWave()
{
if (m_prefab == null)
{
Debug.LogError($"EnemySpawner ({name}): m_prefab is not set!", this);
return;
}
void OnDestroy()
{
if (m_respawnOnPlayersDeath && Player.Instance != null)
{
var respawner = Player.Instance.GetComponent<Respawner>();
respawner.m_onRespawned.RemoveListener(OnPlayerRespawned);
}
if (m_spawnedAI)
{
m_spawnedAI.onDead.RemoveListener(OnDead);
}
}
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;
}
public override void Start()
{
base.Start();
if (Player.Instance != null)
m_distanceFrom = Player.Instance.transform;
else
{
m_distanceFrom = Camera.main.transform;
}
if (m_spawnData.waveActive)
{
// Debug.Log($"EnemySpawner ({name}): Wave already active, not spawning another.", this);
return;
}
if (m_respawnOnPlayersDeath)
{
var respawner = Player.Instance.GetComponent<Respawner>();
respawner.m_onRespawned.AddListener(OnPlayerRespawned);
}
InvokeRepeating("CheckSpawn", m_spawnCheckInterval + (Random.value * m_spawnCheckInterval), m_spawnCheckInterval);
}
int enemiesSuccessfullySpawned = 0;
for (int i = 0; i < m_enemiesPerSpawnWave; i++)
{
Vector3 spawnPosition = Vector3.zero;
bool foundValidPosition = false;
void CheckRespawnCondition()
{
if ((m_numToSpawn < 0 || m_numToSpawn > m_spawnData.numSpawned) && m_spawnedAI == null )
m_spawnData.spawned = false;
}
private void OnPlayerRespawned()
{
CheckRespawnCondition();
}
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;
public override string RecordData()
{
return SaveSystem.Serialize(m_spawnData);
}
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;
}
}
}
public override void ApplyData(string s)
{
var data = SaveSystem.Deserialize<SaveData>(s);
if (data != null)
{
m_spawnData = data;
//m_spawned = true;
CheckRespawnCondition();
}
}
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);
}
}
public bool SpawnedAndLive()
{
return m_spawnedAI != null;
}
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
{
// 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);
}
}
public bool SpawnedAndDead()
{
return m_spawnData.spawned && m_spawnedAI == null;
}
private void OnEnemyReceiveDamage(vDamage damageData) // Assuming vDamage from Invector
{
m_onReceivedDamage?.Invoke(this);
}
public void Spawn()
{
if (m_prefab == null)
return;
Vector3 pos;
const float RAY_OFFSET = 30f;
const float Y_OFFSET = .1f;
pos = transform.position;
pos.y += RAY_OFFSET;
RaycastHit hit = new RaycastHit();
if (Physics.Raycast(pos, Vector3.down, out hit, 100f, raycastMask))
{
pos.y -= RAY_OFFSET;
if (pos.y < hit.point.y + Y_OFFSET)
{
pos.y = hit.point.y + Y_OFFSET;
}
}
else
{
Debug.LogError("EnemySpawner error: raycast failed");
return;
}
private void OnEnemyDead(GameObject deadEnemyObject)
{
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);
}
//m_spawned = true;
m_spawnData.spawned = true;
m_spawnData.numSpawned++;
var enemy = Instantiate(m_prefab, pos, transform.rotation, transform);
m_spawnedAI = enemy.GetComponent<vControlAI>();
if (m_spawnedAI)
{
m_spawnedAI.onDead.AddListener(OnDead);
m_spawnedAI.onReceiveDamage.AddListener(OnReceiveDamage);
}
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).
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 (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;
}
}
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;
private void OnReceiveDamage(vDamage arg0)
{
m_onReceivedDamage?.Invoke(this);
}
// 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).
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;
}
private void OnDead(GameObject arg0)
{
m_spawnedAI = null;
m_OnDead?.Invoke(this);
}
// Try to spawn if: no wave active, can spawn more, player in range, AND initial check hasn't passed.
if (!m_spawnData.waveActive && CanSpawnMoreWaves() && IsPlayerInRange())
{
SpawnWave();
if (m_spawnData.waveActive) // If wave was successfully spawned
{
_initialWaveCheckPassedThisActivation = true; // Mark as done for this cycle
}
// 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 CheckSpawn(){
if (!m_spawnData.spawned && gameObject.activeInHierarchy && (transform.position - m_distanceFrom.position).sqrMagnitude < m_distance * m_distance){
Spawn();
}
}
public void ResetSpawner()
{
CancelInvoke("CheckSpawn"); // Stop existing repeating call
// Destroy active enemies
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();
// Reset spawn data
if (m_spawnData != null)
{
m_spawnData.wavesSpawnedCount = 0;
m_spawnData.waveActive = false;
}
else // Should not happen if Awake ran
{
m_spawnData = new SaveData { wavesSpawnedCount = 0, waveActive = false };
}
_initialWaveCheckPassedThisActivation = false; // Reset the flag for a new cycle
// 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;
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);
}
}
}
}

View File

@@ -0,0 +1,161 @@
using System.Collections.Generic;
using Sirenix.OdinInspector;
using UnityEngine;
namespace Beyond
{
public class EnemySpawnerManager : MonoBehaviour
{
[Header("Spawner Configurations")]
[Tooltip("List of spawners to manage and their override settings.")]
public List<SpawnerOverrideConfig> spawnerConfigurations = new List<SpawnerOverrideConfig>();
[Header("Global Settings (Optional)")]
[Tooltip("If assigned, this prefab will be used for ALL spawners in the list that don't have their own overridePrefab set.")]
public GameObject globalOverridePrefab;
[Tooltip("If > 0, this will be used for ALL spawners that don't have overrideEnemiesPerWave set. Set to 0 or less to ignore.")]
public int globalOverrideEnemiesPerWave = -1;
public bool overrideOnStart = true;
public bool autoEnableSpawners = true;
public bool disableSpawnersOnStart = true;
void Awake()
{
if (disableSpawnersOnStart)
{
// Disable all spawners initially if needed
foreach (var config in spawnerConfigurations)
{
if (config.spawner != null)
{
config.spawner.enabled = false;
}
}
}
ApplySpawnerConfigurations();
}
// You could also call this from Start() if you want to ensure all other Awakes have run,
// though for setting properties on other components, Awake() is generally fine.
// void Start()
// {
// ApplySpawnerConfigurations();
// }
[Button]
public void ApplySpawnerConfigurations()
{
if (spawnerConfigurations == null || spawnerConfigurations.Count == 0)
{
Debug.LogWarning("EnemySpawnerManager: No spawner configurations assigned.", this);
return;
}
foreach (SpawnerOverrideConfig config in spawnerConfigurations)
{
if (config == null || config.spawner == null)
{
Debug.LogWarning("EnemySpawnerManager: A spawner configuration or its target spawner is null. Skipping.", this);
continue;
}
EnemySpawner targetSpawner = config.spawner;
// Apply specific override prefab if set
if (config.overridePrefab != null)
{
targetSpawner.m_prefab = config.overridePrefab;
// Debug.Log($"Manager overriding prefab for {targetSpawner.name} to {config.overridePrefab.name}", this);
}
// Else, if a global override prefab is set, apply that
else if (globalOverridePrefab != null)
{
targetSpawner.m_prefab = globalOverridePrefab;
// Debug.Log($"Manager applying global override prefab to {targetSpawner.name} ({globalOverridePrefab.name})", this);
}
// If neither specific nor global override is set, the spawner uses its own m_prefab.
// Apply specific override for enemies per wave if set (and > 0)
if (config.overrideEnemiesPerWave > -1)
{
targetSpawner.m_enemiesPerSpawnWave = config.overrideEnemiesPerWave;
// Debug.Log($"Manager overriding enemies per wave for {targetSpawner.name} to {config.overrideEnemiesPerWave}", this);
}
// Else, if a global override for enemies per wave is set (and > 0), apply that
else if (globalOverrideEnemiesPerWave > -1)
{
targetSpawner.m_enemiesPerSpawnWave = globalOverrideEnemiesPerWave;
// Debug.Log($"Manager applying global override enemies per wave to {targetSpawner.name} ({globalOverrideEnemiesPerWave})", this);
}
if (autoEnableSpawners)
{
targetSpawner.enabled = true; // Enable the spawner if it was disabled
}
// If neither specific nor global override is set, the spawner uses its own m_enemiesPerSpawnWave.
// --- IMPORTANT ---
// Ensure the spawner hasn't already started its InvokeRepeating for CheckSpawn
// if its settings are being changed.
// One way is to disable the spawner initially and let the manager enable it.
// Or, if spawners might already be active, you might need to CancelInvoke and Restart it.
// For simplicity, let's assume spawners are either configured to not auto-start
// or this manager runs early enough (e.g., via Script Execution Order).
// If you want to ensure spawners don't start before manager configures them:
// Option A: Set spawners to be disabled by default in the editor, then enable them here.
// targetSpawner.enabled = true; // If they were disabled by default
// Option B: Make sure EnemySpawnerManager's Awake runs before EnemySpawner's Start.
// You can do this via Edit > Project Settings > Script Execution Order.
// Add EnemySpawnerManager and set it to an earlier value (e.g., -100)
// than the default time or EnemySpawner.
}
Debug.Log("EnemySpawnerManager: Applied configurations to spawners.", this);
}
// Optional: Helper to add spawners programmatically if needed
public void AddSpawnerToManage(EnemySpawner spawner, GameObject prefabOverride = null, int enemiesPerWaveOverride = 0)
{
if (spawner == null) return;
SpawnerOverrideConfig newConfig = new SpawnerOverrideConfig
{
spawner = spawner,
overridePrefab = prefabOverride,
overrideEnemiesPerWave = enemiesPerWaveOverride
};
spawnerConfigurations.Add(newConfig);
}
/// <summary>
/// Resets all managed spawners to their initial state.
/// This will destroy their current enemies and restart their spawning cycles.
/// </summary>
[ContextMenu("Reset All Managed Spawners")] // Adds a right-click option in Inspector for easy testing
[Button]
public void ResetAllManagedSpawners()
{
if (spawnerConfigurations == null || spawnerConfigurations.Count == 0)
{
Debug.LogWarning("EnemySpawnerManager: No spawners configured to reset.", this);
return;
}
Debug.Log("EnemySpawnerManager: Initiating reset for all managed spawners...", this);
int resetCount = 0;
foreach (SpawnerOverrideConfig config in spawnerConfigurations)
{
if (config != null && config.spawner != null)
{
config.spawner.ResetSpawner(); // Call the spawner's own reset method
resetCount++;
}
}
Debug.Log($"EnemySpawnerManager: Reset command sent to {resetCount} spawners.", this);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6ce0b24e87d404b299a748ccc1671160

View File

@@ -10,7 +10,7 @@ namespace Beyond
public class KillTrigger : Saver
{
[SerializeField] EnemySpawner[] m_spawners;
public UnityEvent<KillTrigger> OnTrigger;
[Serializable]
@@ -22,9 +22,13 @@ namespace Beyond
private SaveData m_data = new SaveData();
void Start()
{
if (m_spawners == null) return; // Guard clause
foreach (var s in m_spawners)
{
s.m_OnDead.AddListener(OnEnemyKilled);
if (s != null) // Good practice to check for null spawners in array
{
s.m_OnDead.AddListener(OnEnemyKilled);
}
}
}
@@ -32,24 +36,44 @@ namespace Beyond
{
if (m_data.wasTriggered)
return;
if (m_spawners == null) return; // Guard clause
foreach (var spawner in m_spawners)
{
if (!spawner.SpawnedAndDead())
return;
if (spawner == null) continue; // Skip if a spawner in the array is null
bool hasSpawnedAWave = spawner.GetWavesSpawnedCount() > 0; // We'll need to add this getter to EnemySpawner
bool noEnemiesCurrentlyAlive = !spawner.IsAnyEnemyAlive();
if (hasSpawnedAWave && noEnemiesCurrentlyAlive)
{
// This spawner has done its part for *this current engagement*
}
else
{
return; // If ANY spawner doesn't meet this, the trigger condition isn't met
}
}
OnTrigger?.Invoke(this);
m_data.wasTriggered = true;
}
private void OnEnemyKilled(EnemySpawner arg0)
{
// arg0 is the spawner from which an enemy died.
// We need to check all spawners associated with this trigger.
CheckSpawners();
}
private void OnDestroy()
{
if (m_spawners == null) return; // Guard clause
foreach (var s in m_spawners)
{
s.m_OnDead.RemoveListener(OnEnemyKilled);
if (s != null) // Good practice
{
s.m_OnDead.RemoveListener(OnEnemyKilled);
}
}
}
@@ -63,6 +87,8 @@ namespace Beyond
var data = SaveSystem.Deserialize<SaveData>(s);
if (data != null)
m_data = data;
// Optional: if not triggered, re-check on load in case state was met while inactive
// if (!m_data.wasTriggered) { CheckSpawners(); }
}
}
}
}

View File

@@ -0,0 +1,21 @@
// This can be in its own file (e.g., SpawnerConfig.cs) or inside the EnemySpawnerManager.cs file if you prefer.
// For better organization, a separate file is often good.
using UnityEngine;
namespace Beyond
{
[System.Serializable] // Makes it show up in the Inspector when used in a list
public class SpawnerOverrideConfig
{
public EnemySpawner spawner; // Reference to the EnemySpawner instance in the scene
[Tooltip("If assigned, this prefab will override the spawner's own m_prefab.")]
public GameObject overridePrefab;
[Tooltip("If > -1, this will override the spawner's m_enemiesPerSpawnWave. Set to 0 or less to use spawner's default.")]
public int overrideEnemiesPerWave = -1;
// You could add more overrides here if needed, e.g., overrideSpawnRadius, overrideMaxWaveRespawns, etc.
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 40b3ccfac6d0f46f0bb77671c0057209