diff --git a/Assets/BossRoom/Prefabs/Game/EnemySpawner.prefab b/Assets/BossRoom/Prefabs/Game/EnemySpawner.prefab index 5892f68a6..3399460bf 100644 --- a/Assets/BossRoom/Prefabs/Game/EnemySpawner.prefab +++ b/Assets/BossRoom/Prefabs/Game/EnemySpawner.prefab @@ -60,6 +60,36 @@ Transform: m_Father: {fileID: 8727022540156222958} m_RootOrder: 6 m_LocalEulerAnglesHint: {x: 0, y: 90, z: 0} +--- !u!1 &1898120149362029613 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 894520739242503044} + m_Layer: 0 + m_Name: SpawnPoint (5) + m_TagString: Untagged + m_Icon: {fileID: 7148428337604731935, guid: 0000000000000000d000000000000000, type: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &894520739242503044 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1898120149362029613} + m_LocalRotation: {x: 0, y: 0.7071068, z: 0, w: 0.7071068} + m_LocalPosition: {x: -0.87499994, y: 0, z: 1.73} + m_LocalScale: {x: 1.3539857, y: 1.1857388, z: 1.2418212} + m_Children: [] + m_Father: {fileID: 8727022540156222958} + m_RootOrder: 10 + m_LocalEulerAnglesHint: {x: 0, y: 90, z: 0} --- !u!1 &2091269911110589480 GameObject: m_ObjectHideFlags: 0 @@ -120,6 +150,36 @@ Transform: m_Father: {fileID: 8727022540156222958} m_RootOrder: 9 m_LocalEulerAnglesHint: {x: 0, y: 90, z: 0} +--- !u!1 &3496698763770893301 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 5949595974590154644} + m_Layer: 0 + m_Name: SpawnPoint (9) + m_TagString: Untagged + m_Icon: {fileID: 7148428337604731935, guid: 0000000000000000d000000000000000, type: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &5949595974590154644 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3496698763770893301} + m_LocalRotation: {x: 0, y: 0.7071068, z: 0, w: 0.7071068} + m_LocalPosition: {x: -1.722057, y: 0, z: 2.298} + m_LocalScale: {x: 1.3539857, y: 1.1857388, z: 1.2418212} + m_Children: [] + m_Father: {fileID: 8727022540156222958} + m_RootOrder: 14 + m_LocalEulerAnglesHint: {x: 0, y: 90, z: 0} --- !u!1 &3529278057740772152 GameObject: m_ObjectHideFlags: 0 @@ -302,6 +362,11 @@ Transform: - {fileID: 7959506842896624360} - {fileID: 5974522673422337960} - {fileID: 1274569494783434218} + - {fileID: 894520739242503044} + - {fileID: 7124730538960977854} + - {fileID: 2077514793330709529} + - {fileID: 3127061543440710862} + - {fileID: 5949595974590154644} m_Father: {fileID: 0} m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} @@ -341,17 +406,26 @@ MonoBehaviour: - {fileID: 7959506842896624360} - {fileID: 5974522673422337960} - {fileID: 1274569494783434218} + - {fileID: 894520739242503044} + - {fileID: 7124730538960977854} + - {fileID: 2077514793330709529} + - {fileID: 3127061543440710862} + - {fileID: 5949595974590154644} m_BlockingMask: serializedVersion: 2 m_Bits: 1 m_PlayerProximityValidationTimestep: 2 + m_SpawnedEntityDetectDistance: 30 + m_DetectStealthyPlayers: 1 m_NumberOfWaves: 2 m_SpawnsPerWave: 2 m_TimeBetweenSpawns: 0.5 m_TimeBetweenWaves: 5 m_RestartDelay: 10 m_ProximityDistance: 30 - m_MaxActiveSpawns: 5 + m_MinSpawnCap: 2 + m_MaxSpawnCap: 10 + m_SpawnCapIncreasePerPlayer: 1 --- !u!114 &2847004539442057774 MonoBehaviour: m_ObjectHideFlags: 0 @@ -495,6 +569,96 @@ MeshCollider: m_Convex: 0 m_CookingOptions: 30 m_Mesh: {fileID: 10209, guid: 0000000000000000e000000000000000, type: 0} +--- !u!1 &6585344981094142531 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 7124730538960977854} + m_Layer: 0 + m_Name: SpawnPoint (6) + m_TagString: Untagged + m_Icon: {fileID: 7148428337604731935, guid: 0000000000000000d000000000000000, type: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &7124730538960977854 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6585344981094142531} + m_LocalRotation: {x: 0, y: 0.7071068, z: 0, w: 0.7071068} + m_LocalPosition: {x: -0.875, y: 0, z: 3.167} + m_LocalScale: {x: 1.3539857, y: 1.1857388, z: 1.2418212} + m_Children: [] + m_Father: {fileID: 8727022540156222958} + m_RootOrder: 11 + m_LocalEulerAnglesHint: {x: 0, y: 90, z: 0} +--- !u!1 &8845616318696809149 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2077514793330709529} + m_Layer: 0 + m_Name: SpawnPoint (7) + m_TagString: Untagged + m_Icon: {fileID: 7148428337604731935, guid: 0000000000000000d000000000000000, type: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &2077514793330709529 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8845616318696809149} + m_LocalRotation: {x: 0, y: 0.7071068, z: 0, w: 0.7071068} + m_LocalPosition: {x: -0.8760569, y: 0, z: 4.495} + m_LocalScale: {x: 1.3539857, y: 1.1857388, z: 1.2418212} + m_Children: [] + m_Father: {fileID: 8727022540156222958} + m_RootOrder: 12 + m_LocalEulerAnglesHint: {x: 0, y: 90, z: 0} +--- !u!1 &9017715703642230324 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 3127061543440710862} + m_Layer: 0 + m_Name: SpawnPoint (8) + m_TagString: Untagged + m_Icon: {fileID: 7148428337604731935, guid: 0000000000000000d000000000000000, type: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &3127061543440710862 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 9017715703642230324} + m_LocalRotation: {x: 0, y: 0.7071068, z: 0, w: 0.7071068} + m_LocalPosition: {x: -1.722057, y: 0, z: 3.961} + m_LocalScale: {x: 1.3539857, y: 1.1857388, z: 1.2418212} + m_Children: [] + m_Father: {fileID: 8727022540156222958} + m_RootOrder: 13 + m_LocalEulerAnglesHint: {x: 0, y: 90, z: 0} --- !u!1001 &1263729836617029720 PrefabInstance: m_ObjectHideFlags: 0 diff --git a/Assets/BossRoom/Scripts/Server/Game/AIState/IdleAIState.cs b/Assets/BossRoom/Scripts/Server/Game/AIState/IdleAIState.cs index e5846b1de..8d66e8701 100644 --- a/Assets/BossRoom/Scripts/Server/Game/AIState/IdleAIState.cs +++ b/Assets/BossRoom/Scripts/Server/Game/AIState/IdleAIState.cs @@ -29,7 +29,7 @@ public override void Update() protected void DetectFoes() { - float detectionRange = m_Brain.CharacterData.DetectRange; + float detectionRange = m_Brain.DetectRange; // we are doing this check every Update, so we'll use square-magnitude distance to avoid the expensive sqrt (that's implicit in Vector3.magnitude) float detectionRangeSqr = detectionRange * detectionRange; Vector3 position = m_Brain.GetMyServerCharacter().transform.position; diff --git a/Assets/BossRoom/Scripts/Server/Game/Character/AIBrain.cs b/Assets/BossRoom/Scripts/Server/Game/Character/AIBrain.cs index 67b85da54..d84137be4 100644 --- a/Assets/BossRoom/Scripts/Server/Game/Character/AIBrain.cs +++ b/Assets/BossRoom/Scripts/Server/Game/Character/AIBrain.cs @@ -25,6 +25,12 @@ private enum AIStateType private Dictionary m_Logics; private List m_HatedEnemies; + /// + /// If we are created by a spawner, the spawner might override our detection radius + /// -1 is a sentinel value meaning "no override" + /// + private float m_DetectRangeOverride = -1; + public AIBrain(ServerCharacter me, ActionPlayer myActionPlayer) { m_ServerCharacter = me; @@ -148,5 +154,23 @@ public CharacterClass CharacterData } } + /// + /// The range at which this character can detect enemies, in meters. + /// This is usually the same value as is indicated by our game data, but it + /// can be dynamically overridden. + /// + public float DetectRange + { + get + { + return (m_DetectRangeOverride == -1) ? CharacterData.DetectRange : m_DetectRangeOverride; + } + + set + { + m_DetectRangeOverride = value; + } + } + } } diff --git a/Assets/BossRoom/Scripts/Server/Game/Character/ServerCharacter.cs b/Assets/BossRoom/Scripts/Server/Game/Character/ServerCharacter.cs index 76d3c129b..1c9e783e6 100644 --- a/Assets/BossRoom/Scripts/Server/Game/Character/ServerCharacter.cs +++ b/Assets/BossRoom/Scripts/Server/Game/Character/ServerCharacter.cs @@ -254,5 +254,10 @@ public IDamageable.SpecialDamageFlags GetSpecialDamageFlags() { return IDamageable.SpecialDamageFlags.None; } + + /// + /// This character's AIBrain. Will be null if this is not an NPC. + /// + public AIBrain AIBrain { get { return m_AIBrain; } } } } diff --git a/Assets/BossRoom/Scripts/Server/Game/Entity/ServerWaveSpawner.cs b/Assets/BossRoom/Scripts/Server/Game/Entity/ServerWaveSpawner.cs index 69d113c77..eb99ec380 100644 --- a/Assets/BossRoom/Scripts/Server/Game/Entity/ServerWaveSpawner.cs +++ b/Assets/BossRoom/Scripts/Server/Game/Entity/ServerWaveSpawner.cs @@ -20,51 +20,66 @@ public class ServerWaveSpawner : NetworkBehaviour [Tooltip("Each spawned enemy appears at one of the points in this list")] List m_SpawnPositions; - // cache reference to our own transform - Transform m_Transform; - - // track wave index and reset once all waves are complete - int m_WaveIndex; - - // keep reference to our current watch-for-players coroutine - Coroutine m_WatchForPlayers; - - // keep reference to our wave spawning coroutine - Coroutine m_WaveSpawning; - - // cache array of RaycastHit as it will be reused for player visibility - RaycastHit[] m_Hit; - [Tooltip("Select which layers will block visibility.")] [SerializeField] LayerMask m_BlockingMask; [Tooltip("Time between player distance & visibility scans, in seconds.")] [SerializeField] - float m_PlayerProximityValidationTimestep; + float m_PlayerProximityValidationTimestep = 2; + + [SerializeField] + [Tooltip("The detection range of spawned entities. Only meaningful for NPCs (not breakables). -1 = \"use default for this NPC\"")] + float m_SpawnedEntityDetectDistance = -1; [Header("Wave parameters")] [Tooltip("Total number of waves.")] [SerializeField] - int m_NumberOfWaves; + int m_NumberOfWaves = 2; [Tooltip("Number of spawns per wave.")] [SerializeField] - int m_SpawnsPerWave; + int m_SpawnsPerWave = 2; [Tooltip("Time between individual spawns, in seconds.")] [SerializeField] - float m_TimeBetweenSpawns; + float m_TimeBetweenSpawns = 0.5f; [Tooltip("Time between waves, in seconds.")] [SerializeField] - float m_TimeBetweenWaves; + float m_TimeBetweenWaves = 5; [Tooltip("Once last wave is spawned, the spawner waits this long to restart wave spawns, in seconds.")] [SerializeField] - float m_RestartDelay; + float m_RestartDelay = 10; [Tooltip("A player must be within this distance to commence first wave spawn.")] [SerializeField] - float m_ProximityDistance; + float m_ProximityDistance = 30; + [SerializeField] + [Tooltip("When looking for players within proximity distance, should we count players in stealth mode?")] + bool m_DetectStealthyPlayers = true; + + [Header("Spawn Cap (i.e. number of simultaneously spawned entities)")] + [SerializeField] + [Tooltip("The minimum number of entities this spawner will try to maintain (regardless of player count)")] + int m_MinSpawnCap = 2; + [SerializeField] + [Tooltip("The maximum number of entities this spawner will try to maintain (regardless of player count)")] + int m_MaxSpawnCap = 10; [SerializeField] - [Tooltip("The spawner won't create more than this many entities at a time. 0 = don't track spawn count")] - int m_MaxActiveSpawns; + [Tooltip("For each player in the game, the Spawn Cap is raised above the minimum by this amount. (Rounds up to nearest whole number.)")] + float m_SpawnCapIncreasePerPlayer = 1; + + // cache reference to our own transform + Transform m_Transform; + + // track wave index and reset once all waves are complete + int m_WaveIndex; + + // keep reference to our current watch-for-players coroutine + Coroutine m_WatchForPlayers; + + // keep reference to our wave spawning coroutine + Coroutine m_WaveSpawning; + + // cache array of RaycastHit as it will be reused for player visibility + RaycastHit[] m_Hit; // indicates whether NetworkStart() has been called on us yet bool m_IsStarted; @@ -189,10 +204,7 @@ IEnumerator SpawnWave() if (IsRoomAvailableForAnotherSpawn()) { var newSpawn = SpawnPrefab(); - if (m_MaxActiveSpawns > 0) // 0 = no limit on spawns, so we don't bother tracking 'em - { - m_ActiveSpawns.Add(newSpawn); - } + m_ActiveSpawns.Add(newSpawn); } yield return new WaitForSeconds(m_TimeBetweenSpawns); @@ -217,20 +229,45 @@ NetworkObject SpawnPrefab() { clone.Spawn(); } + + if (m_SpawnedEntityDetectDistance > -1) + { + // need to override the spawned creature's detection range (if they even have a detection range!) + var serverChar = clone.GetComponent(); + if (serverChar && serverChar.AIBrain != null) + { + serverChar.AIBrain.DetectRange = m_SpawnedEntityDetectDistance; + } + } + return clone; } bool IsRoomAvailableForAnotherSpawn() { - if (m_MaxActiveSpawns <= 0) - { - // no max-spawn limit - return true; - } // references to spawned components that no longer exist will become null, // so clear those out. Then we know how many we have left m_ActiveSpawns.RemoveAll(spawnedNetworkObject => { return spawnedNetworkObject == null; }); - return m_ActiveSpawns.Count < m_MaxActiveSpawns; + return m_ActiveSpawns.Count < GetCurrentSpawnCap(); + } + + /// + /// Returns the current max number of entities we should try to maintain. + /// This can change based on the current number of living players; if the cap goes below + /// our current number of active spawns, we don't spawn anything new until we're below the cap. + /// + int GetCurrentSpawnCap() + { + int numPlayers = 0; + foreach (var serverCharacter in PlayerServerCharacter.GetPlayerServerCharacters()) + { + if (serverCharacter.NetState.NetworkLifeState.Value == LifeState.Alive) + { + ++numPlayers; + } + } + + return Mathf.CeilToInt(Mathf.Min(m_MinSpawnCap + (numPlayers * m_SpawnCapIncreasePerPlayer), m_MaxSpawnCap)); } /// @@ -250,6 +287,12 @@ bool IsAnyPlayerNearbyAndVisible() // and is not occluded by a blocking collider. foreach (var serverCharacter in PlayerServerCharacter.GetPlayerServerCharacters()) { + if (!m_DetectStealthyPlayers && serverCharacter.NetState.IsStealthy.Value) + { + // we don't detect stealthy players + continue; + } + var playerPosition = serverCharacter.transform.position; var direction = playerPosition - spawnerPosition;