new spawning manager, spawner improvements - now with multiple enemies to spawn, KillTrigger
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
161
Assets/Scripts/Characters/EnemySpawnerManager.cs
Normal file
161
Assets/Scripts/Characters/EnemySpawnerManager.cs
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Characters/EnemySpawnerManager.cs.meta
Normal file
2
Assets/Scripts/Characters/EnemySpawnerManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6ce0b24e87d404b299a748ccc1671160
|
||||
@@ -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(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
21
Assets/Scripts/Characters/SpawnerOverrideConfig.cs
Normal file
21
Assets/Scripts/Characters/SpawnerOverrideConfig.cs
Normal 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.
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Characters/SpawnerOverrideConfig.cs.meta
Normal file
2
Assets/Scripts/Characters/SpawnerOverrideConfig.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 40b3ccfac6d0f46f0bb77671c0057209
|
||||
Reference in New Issue
Block a user