// Copyright (c) Pixel Crushers. All rights reserved. using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; namespace PixelCrushers.QuestMachine { /// /// Spawner. Methods are virtual so you can override them if you need custom behavior. /// [AddComponentMenu("")] public class Spawner : MonoBehaviour, IMessageHandler { #region Subtypes [Serializable] public class PrefabInfo { public GameObject prefab; [Tooltip("Relative probability with which to spawn this prefab.")] public float weight = 1; } [Serializable] public class PositionInfo { public enum PositionType { Radius, Spawnpoints } public enum Plane { X_Y, X_Z } [Tooltip("Position in a radius around the spawner or at specific spawnpoints.")] public PositionType positionType = PositionType.Radius; [Tooltip("If radius, the maximum distance from spawner at which to spawn entities.")] public float radius = 10; [Tooltip("If radius, the X-Z plane (3D) or X-Y plane (2D)")] public Plane plane = Plane.X_Z; [Tooltip("If Spawnpoints, place entities at these exact spawnpoints.")] public GameObject[] spawnpoints; } #endregion #region Serialized Fields [Tooltip("Name by which Quest Machine can reference this spawner.")] [SerializeField] private StringField m_spawnerName = new StringField(); [Tooltip("Prefabs to spawn.")] [SerializeField] private PrefabInfo[] m_prefabs = new PrefabInfo[0]; [Tooltip("Where to spawn.")] [SerializeField] private PositionInfo m_positionInfo = new PositionInfo(); [Tooltip("Entities that have been spawned.")] [SerializeField] private List m_spawnedEntities = new List(); [Tooltip("Make spawned entities root objects instead of children of spawner.")] [SerializeField] private bool m_spawnAsRootObjects = false; [Tooltip("Minimum number of entities to spawn.")] [SerializeField] private int m_min = 1; [Tooltip("Maximum number of entities to spawn.")] [SerializeField] private int m_max = 5; [Tooltip("Once above the minimum, spawn one entity at this frequency in seconds.")] [SerializeField] private float m_spawnRate = 5; [Tooltip("Start spawning as soon as this component starts.")] [SerializeField] private bool m_autoStart = false; [Tooltip("If Auto Start is ticked, wait for save data to be applied if loading a saved game or changing scenes.")] [SerializeField] private bool m_autoStartAfterSaveDataApplied = false; [Tooltip("Stop spawning as soon as the minimum number of entities has been reached.")] [SerializeField] private bool m_stopWhenMinReached = true; [Tooltip("Despawn all spawned entities when this component is destroyed.")] [SerializeField] private bool m_despawnOnDestroy = false; #endregion #region Accessor Properties to Serialized Fields /// /// Name by which Quest Machine can reference this spawner. /// public StringField spawnerName { get { return m_spawnerName; } set { m_spawnerName = value; } } /// /// Prefabs to spawn. /// public PrefabInfo[] prefabs { get { return m_prefabs; } set { m_prefabs = value; } } /// /// Where to spawn. /// public PositionInfo positionInfo { get { return m_positionInfo; } set { m_positionInfo = value; } } /// /// Entities that have been spawned. /// public List spawnedEntities { get { return m_spawnedEntities; } set { m_spawnedEntities = value; } } /// /// Make spawned entities root objects instead of children of spawner. /// public bool spawnAsRootObjects { get { return m_spawnAsRootObjects; } set { m_spawnAsRootObjects = value; } } /// /// Minimum number of entities to spawn. /// public int min { get { return m_min; } set { m_min = value; } } /// /// Maximum number of entities to spawn. /// public int max { get { return m_max; } set { m_max = value; } } /// /// Once above the minimum, spawn one entity at this frequency in seconds. /// public float spawnRate { get { return m_spawnRate; } set { m_spawnRate = value; } } /// /// Start spawning as soon as this component starts. /// public bool autoStart { get { return m_autoStart; } set { m_autoStart = value; } } /// /// If Auto Start is ticked, wait for save data to be applied if loading a saved game or changing scenes. /// public bool autoStartAfterSaveDataApplied { get { return m_autoStartAfterSaveDataApplied; } set { m_autoStartAfterSaveDataApplied = value; } } /// /// Stop spawning as soon as the minimum number of entities has been reached. /// public bool stopWhenMinReached { get { return m_stopWhenMinReached; } set { m_stopWhenMinReached = value; } } /// /// Despawn all spawned entities when this component is destroyed. /// public bool despawnOnDestroy { get { return m_despawnOnDestroy; } set { m_despawnOnDestroy = value; } } private int m_spawnCount = 0; protected int spawnCount { get { return m_spawnCount; } set { m_spawnCount = value; } } #endregion #region Private Variables protected List m_availablePositions = new List(); protected static List m_spawners = new List(); #endregion #region Initialization protected virtual void Awake() { m_spawners.Add(this); } protected virtual void Start() { RegisterWithMessageSystem(); if (autoStart) { if (autoStartAfterSaveDataApplied && SaveSystem.hasInstance) { StartCoroutine(StartSpawningAfterSaveDataApplied()); } else { StartSpawning(); } } } protected virtual IEnumerator StartSpawningAfterSaveDataApplied() { for (int i = 0; i <= (2 * SaveSystem.framesToWaitBeforeApplyData); i++) { yield return null; } yield return new WaitForEndOfFrame(); StartSpawning(); } protected virtual void OnDestroy() { m_spawners.Remove(this); UnregisterWithMessageSystem(); if (despawnOnDestroy) DespawnAll(); } public static Spawner FindSpawner(string spawnerName) { return m_spawners.Find(spawner => StringField.Equals(spawner.spawnerName, spawnerName)); } protected virtual void RegisterWithMessageSystem() { // Listen for Start, Stop, and Despawn messages: MessageSystem.AddListener(this, QuestMachineMessages.StartSpawnerMessage, spawnerName); MessageSystem.AddListener(this, QuestMachineMessages.StopSpawnerMessage, spawnerName); MessageSystem.AddListener(this, QuestMachineMessages.DespawnSpawnerMessage, spawnerName); } protected virtual void UnregisterWithMessageSystem() { MessageSystem.RemoveListener(this); } public virtual void OnMessage(MessageArgs messageArgs) { switch (messageArgs.message) { case QuestMachineMessages.StartSpawnerMessage: StartSpawning(); break; case QuestMachineMessages.StopSpawnerMessage: StopSpawning(); break; case QuestMachineMessages.DespawnSpawnerMessage: DespawnAll(); break; } } #endregion #region Spawning /// /// Starts spawning. May stop automatically if stopWhenMinReached is true. /// public virtual void StartSpawning() { StartCoroutine(SpawnCoroutine()); } /// /// Stops spawning. /// public virtual void StopSpawning() { StopAllCoroutines(); } /// /// Stops spawning and despawns all spawned entities. /// public virtual void DespawnAll() { StopAllCoroutines(); for (int i = 0; i < spawnedEntities.Count; i++) { var spawnedEntity = spawnedEntities[i]; if (spawnedEntity == null) continue; spawnedEntity.disabled -= OnSpawnedEntityDisabled; RemoveSpawnedEntity(spawnedEntity); DespawnEntity(spawnedEntity); } spawnCount = 0; } /// /// This coroutine runs until killed, spawning entities so the count /// remains between min and max. /// protected virtual IEnumerator SpawnCoroutine() { SetupSpawnpoints(); // Keep spawning until made to stop: var secondsToWait = new WaitForSeconds(spawnRate); while (true) { // Fill out min count: for (int i = spawnCount; i < min; i++) { SpawnAndPlaceEntity(); } if (stopWhenMinReached) yield break; // Then keep spawning at the specified rate if under max count: yield return secondsToWait; if (spawnCount < max) SpawnAndPlaceEntity(); } } /// /// Prepares for spawning. /// protected virtual void SetupSpawnpoints() { // If spawning around radius, use the existing spawnedEntities list: if (positionInfo.positionType == PositionInfo.PositionType.Radius) { spawnCount = spawnedEntities.Count; for (int i = 0; i < spawnedEntities.Count; i++) { var spawnedEntity = spawnedEntities[i]; if (spawnedEntity == null) continue; spawnedEntity.disabled -= OnSpawnedEntityDisabled; spawnedEntity.disabled += OnSpawnedEntityDisabled; } return; } // If spawning at spawnpoints, allocate empty spaces in the spawnedEntities list: for (int i = spawnedEntities.Count; i < positionInfo.spawnpoints.Length; i++) { spawnedEntities.Add(null); } // If spawnpoints point to any SpawnedEntities, record them in the spawnedEntities // list and replace them in the spawnpoints list with an empty GameObject. for (int i = 0; i < positionInfo.spawnpoints.Length; i++) { var spawnpoint = positionInfo.spawnpoints[i]; if (spawnpoint == null) continue; var spawnedEntity = spawnpoint.GetComponent(); if (spawnedEntity != null) { RemoveSpawnedEntity(spawnedEntities[i]); spawnedEntities[i] = spawnedEntity; spawnedEntity.disabled -= OnSpawnedEntityDisabled; spawnedEntity.disabled += OnSpawnedEntityDisabled; var emptyGameObject = new GameObject("Spawnpoint " + i); emptyGameObject.transform.SetParent(this.transform); emptyGameObject.transform.position = spawnedEntity.transform.position; emptyGameObject.transform.rotation = spawnedEntity.transform.rotation; positionInfo.spawnpoints[i] = emptyGameObject; spawnCount++; } } } public virtual void AddRestoredEntity(SpawnedEntity spawnedEntity) { if (spawnedEntity == null) return; spawnedEntities.Add(spawnedEntity); spawnedEntity.disabled -= OnSpawnedEntityDisabled; spawnedEntity.disabled += OnSpawnedEntityDisabled; spawnCount++; } /// /// Spawns an entity and places it in the scene. /// protected virtual void SpawnAndPlaceEntity() { if (!IsThereSpaceForEntity()) return; var spawnedEntity = SpawnEntity(); if (spawnedEntity == null) return; spawnedEntity.disabled -= OnSpawnedEntityDisabled; spawnedEntity.disabled += OnSpawnedEntityDisabled; PlaceSpawnedEntity(spawnedEntity); } /// /// Checks if there is space (e.g., an available spawnpoint) for the entity. /// protected virtual bool IsThereSpaceForEntity() { if (positionInfo.positionType == PositionInfo.PositionType.Radius) return true; for (int i = 0; i < spawnedEntities.Count; i++) { if (spawnedEntities[i] == null) return true; } return false; } /// /// Places a spawned entity in the scene. /// protected virtual void PlaceSpawnedEntity(SpawnedEntity spawnedEntity) { if (spawnedEntity == null) return; if (positionInfo.positionType == PositionInfo.PositionType.Radius) { PlaceSpawnedEntityInRadius(spawnedEntity); } else { PlaceSpawnedEntityInSpawnpoint(spawnedEntity); } } /// /// Places an entity within the specified radius of the spawner. /// protected void PlaceSpawnedEntityInRadius(SpawnedEntity spawnedEntity) { var rand1 = UnityEngine.Random.Range(-positionInfo.radius, positionInfo.radius); var rand2 = UnityEngine.Random.Range(-positionInfo.radius, positionInfo.radius); var position = (positionInfo.plane == PositionInfo.Plane.X_Z) ? new Vector3(transform.position.x + rand1, transform.position.y, transform.position.z + rand2) : new Vector3(transform.position.x + rand1, transform.position.y + rand2, transform.position.z); var navMeshAgent = spawnedEntity.GetComponent(); if (navMeshAgent != null) { navMeshAgent.Warp(position); } else { spawnedEntity.transform.position = position; } spawnedEntities.Add(spawnedEntity); } /// /// Places an entity at an available spawnpoint. /// protected virtual void PlaceSpawnedEntityInSpawnpoint(SpawnedEntity spawnedEntity) { // Get available positions: m_availablePositions.Clear(); for (int i = 0; i < spawnedEntities.Count; i++) { if (spawnedEntities[i] == null) { m_availablePositions.Add(i); } } if (m_availablePositions.Count == 0) return; // Move entity to a random available position: var index = m_availablePositions[UnityEngine.Random.Range(0, m_availablePositions.Count - 1)]; spawnedEntities[index] = spawnedEntity; var spawnpoint = positionInfo.spawnpoints[Mathf.Min(index, positionInfo.spawnpoints.Length - 1)]; if (spawnpoint != null) { var navMeshAgent = spawnedEntity.GetComponent(); if (navMeshAgent != null) { navMeshAgent.Warp(spawnpoint.transform.position); } else { spawnedEntity.transform.position = spawnpoint.transform.position; } spawnedEntity.transform.rotation = spawnpoint.transform.rotation; } } /// /// Removes an entity from the spawnedEntities list. /// /// protected virtual void RemoveSpawnedEntity(SpawnedEntity spawnedEntity) { if (spawnedEntity == null) return; spawnCount--; if (positionInfo.positionType == PositionInfo.PositionType.Radius) { // If using radius, just remove from list: spawnedEntities.Remove(spawnedEntity); } else { // If using spawnpoints, assign null to the list element. for (int i = 0; i < spawnedEntities.Count; i++) { if (spawnedEntities[i] == spawnedEntity) { spawnedEntities[i] = null; } } } } /// /// Spawns an entity and returns the SpawnedEntity component on it, adding /// the component if necessary. /// /// A random spawned entity, or null on error. protected virtual SpawnedEntity SpawnEntity() { var prefab = ChooseWeightedRandomPrefab(); if (prefab == null) { if (Debug.isDebugBuild) Debug.LogWarning("Quest Machine: A prefab entry is blank in this spawner. Not spawning.", this); return null; } spawnCount++; var instance = InstantiateEntity(prefab.prefab); instance.transform.SetParent(m_spawnAsRootObjects ? null : this.transform); var spawnedEntity = instance.GetComponent() ?? instance.AddComponent(); spawnedEntity.spawnerName = StringField.GetStringValue(spawnerName); return spawnedEntity; } protected virtual PrefabInfo ChooseWeightedRandomPrefab() { if (prefabs == null || prefabs.Length == 0) return null; float totalWeight = 0; for (int i = 0; i < prefabs.Length; i++) { if (prefabs[i] == null) continue; totalWeight += prefabs[i].weight; } float remainingWeight = UnityEngine.Random.Range(0, totalWeight); for (int i = 0; i < prefabs.Length; i++) { if (prefabs[i] == null) continue; remainingWeight -= prefabs[i].weight; if (remainingWeight <= 0) { return prefabs[i]; } } return prefabs[0]; //--- Previously chose prefab randomly with equal weight: //var prefab = prefabs[UnityEngine.Random.Range(0, prefabs.Length)]; } /// /// Despawns an entity. /// /// protected virtual void DespawnEntity(SpawnedEntity spawnedEntity) { DestroyEntity(spawnedEntity.gameObject); } /// /// Returns an instance of a prefab. Override this method if you want to /// use a pooling system instead of Instantiate. /// protected virtual GameObject InstantiateEntity(GameObject prefab) { return Instantiate(prefab); } /// /// Destroys an instance of a prefab. Override this method if you want to /// use a pooling system to return the instance to the pool. /// protected virtual void DestroyEntity(GameObject go) { Destroy(go); } /// /// Invoked by a SpawnedEntity when it's disabled. Removes it from the spawnedEntities list. /// /// protected virtual void OnSpawnedEntityDisabled(SpawnedEntity spawnedEntity) { RemoveSpawnedEntity(spawnedEntity); } #endregion } }