675 lines
21 KiB
C#
675 lines
21 KiB
C#
using System.Collections.Generic;
|
|
using Game.Scripts.Runtime.Data;
|
|
using Game.Scripts.Runtime.Pooling;
|
|
using Game.Scripts.Runtime.Navigation;
|
|
using MegaKoop.Game.Combat;
|
|
using MegaKoop.Game.Networking;
|
|
using MegaKoop.Game.WeaponSystem;
|
|
using UnityEngine;
|
|
using UnityEngine.AI;
|
|
|
|
namespace MegaKoop.Game.Enemy
|
|
{
|
|
/// <summary>
|
|
/// Host-driven enemy brain that chases the closest hero, fires using the shared weapon system,
|
|
/// and mirrors movement state to peers through SteamCoopNetworkManager.
|
|
/// </summary>
|
|
[DisallowMultipleComponent]
|
|
[RequireComponent(typeof(NetworkIdentity))]
|
|
[RequireComponent(typeof(Health))]
|
|
[RequireComponent(typeof(SteamNetworkTransform))]
|
|
[RequireComponent(typeof(WeaponController))]
|
|
public class SteamEnemyController : MonoBehaviour
|
|
{
|
|
private static readonly List<Health> SharedHealthBuffer = new(32);
|
|
|
|
[Header("Movement")]
|
|
[SerializeField] private float moveSpeed = 3.5f;
|
|
[SerializeField] private float turnSpeed = 6f;
|
|
[SerializeField] private float stoppingDistance = 4f;
|
|
[SerializeField] private float attackStoppingDistance = 1.25f;
|
|
[SerializeField] private float detectionRadius = 30f;
|
|
[SerializeField] private float leashDistance = 60f;
|
|
[SerializeField] private float retargetInterval = 0.4f;
|
|
[SerializeField] private float repathThreshold = 1.5f;
|
|
[SerializeField] private bool returnToSpawnWhenIdle = true;
|
|
|
|
[Header("References")]
|
|
[SerializeField] private NavMeshAgent navMeshAgent;
|
|
[SerializeField] private WeaponController weaponController;
|
|
[SerializeField] private Health health;
|
|
[SerializeField] private NetworkIdentity identity;
|
|
[SerializeField] private SteamNetworkTransform networkTransform;
|
|
|
|
[Header("Spawner Integration")]
|
|
[SerializeField] private EnemyDefinition fallbackDefinition;
|
|
[SerializeField] private bool applyDefinitionStats = true;
|
|
[SerializeField] private float healthMultiplier = 1f;
|
|
[SerializeField] private float moveSpeedMultiplier = 1f;
|
|
[SerializeField] private float damageMultiplier = 1f;
|
|
[SerializeField] private float deathDespawnDelay = 0f;
|
|
|
|
private SteamCoopNetworkManager networkManager;
|
|
private Transform currentTargetTransform;
|
|
private NetworkIdentity currentTargetIdentity;
|
|
private Health currentTargetHealth;
|
|
private Vector3 lastTargetPosition;
|
|
private Vector3 lastKnownTargetPosition;
|
|
private float retargetTimer;
|
|
private Vector3 spawnPosition;
|
|
private bool cachedNavAgentState;
|
|
private PooledInstance pooledInstance;
|
|
private EnemyDefinition activeDefinition;
|
|
private float baseMoveSpeed;
|
|
private bool pendingDespawn;
|
|
private float despawnTimer;
|
|
|
|
private void Awake()
|
|
{
|
|
spawnPosition = transform.position;
|
|
baseMoveSpeed = moveSpeed;
|
|
|
|
if (navMeshAgent == null)
|
|
{
|
|
navMeshAgent = GetComponent<NavMeshAgent>();
|
|
}
|
|
|
|
if (weaponController == null)
|
|
{
|
|
weaponController = GetComponent<WeaponController>();
|
|
}
|
|
|
|
if (health == null)
|
|
{
|
|
health = GetComponent<Health>();
|
|
}
|
|
|
|
if (networkTransform == null)
|
|
{
|
|
networkTransform = GetComponent<SteamNetworkTransform>();
|
|
}
|
|
|
|
if (pooledInstance == null)
|
|
{
|
|
pooledInstance = GetComponent<PooledInstance>();
|
|
}
|
|
|
|
var navMeshAvailable = NavMeshRuntimeUtility.HasNavMeshData();
|
|
if (navMeshAgent != null)
|
|
{
|
|
navMeshAgent.updateRotation = false;
|
|
navMeshAgent.stoppingDistance = stoppingDistance;
|
|
navMeshAgent.speed = moveSpeed;
|
|
if (!navMeshAvailable && navMeshAgent.enabled)
|
|
{
|
|
navMeshAgent.enabled = false;
|
|
}
|
|
cachedNavAgentState = navMeshAgent.enabled;
|
|
}
|
|
|
|
lastTargetPosition = Vector3.positiveInfinity;
|
|
lastKnownTargetPosition = spawnPosition;
|
|
retargetTimer = Random.Range(0f, Mathf.Max(0.05f, retargetInterval));
|
|
}
|
|
|
|
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
|
|
private static void ResetNetworkIdCounter()
|
|
{
|
|
NetworkIdAllocator.Reset();
|
|
}
|
|
|
|
private void EnsureIdentity()
|
|
{
|
|
if (identity == null)
|
|
{
|
|
identity = GetComponent<NetworkIdentity>();
|
|
}
|
|
|
|
if (identity == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (identity.NetworkId != 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!ShouldAssignLocalNetworkId())
|
|
{
|
|
return;
|
|
}
|
|
|
|
int allocatedId = NetworkIdAllocator.AllocateEnemyId();
|
|
identity.SetNetworkId(allocatedId);
|
|
}
|
|
|
|
private void OnEnable()
|
|
{
|
|
EnsureIdentity();
|
|
networkManager = SteamCoopNetworkManager.Instance;
|
|
pooledInstance ??= GetComponent<PooledInstance>();
|
|
pendingDespawn = false;
|
|
despawnTimer = 0f;
|
|
spawnPosition = transform.position;
|
|
lastKnownTargetPosition = spawnPosition;
|
|
lastTargetPosition = Vector3.positiveInfinity;
|
|
ClearTarget();
|
|
retargetTimer = Random.Range(0f, Mathf.Max(0.05f, retargetInterval));
|
|
SyncNavMeshAgentState(ShouldSimulate());
|
|
|
|
if (health != null)
|
|
{
|
|
health.NormalizedHealthChanged += HandleHealthChanged;
|
|
}
|
|
|
|
ApplyDefinitionStats();
|
|
}
|
|
|
|
private void OnDisable()
|
|
{
|
|
if (health != null)
|
|
{
|
|
health.NormalizedHealthChanged -= HandleHealthChanged;
|
|
}
|
|
|
|
pendingDespawn = false;
|
|
weaponController?.ClearDamageOverride();
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
networkManager ??= SteamCoopNetworkManager.Instance;
|
|
|
|
bool simulate = ShouldSimulate();
|
|
SyncNavMeshAgentState(simulate);
|
|
|
|
if (weaponController != null)
|
|
{
|
|
weaponController.enabled = simulate;
|
|
}
|
|
|
|
if (pendingDespawn)
|
|
{
|
|
TickDespawn(Time.deltaTime);
|
|
return;
|
|
}
|
|
|
|
if (!simulate)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (health != null && !health.IsAlive)
|
|
{
|
|
BeginDespawnCountdown();
|
|
return;
|
|
}
|
|
|
|
retargetTimer -= Time.deltaTime;
|
|
if (!IsTargetValid() || retargetTimer <= 0f)
|
|
{
|
|
AcquireTarget();
|
|
}
|
|
|
|
if (!TryGetTargetPosition(out _))
|
|
{
|
|
HandleIdle();
|
|
return;
|
|
}
|
|
|
|
TickMovement(Time.deltaTime);
|
|
FaceTarget(Time.deltaTime);
|
|
}
|
|
|
|
private bool ShouldSimulate()
|
|
{
|
|
if (networkManager == null)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (!networkManager.IsConnected)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return networkManager.IsHost;
|
|
}
|
|
|
|
private bool ShouldAssignLocalNetworkId()
|
|
{
|
|
var manager = SteamCoopNetworkManager.Instance;
|
|
if (manager == null)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (!manager.IsConnected)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return manager.IsHost;
|
|
}
|
|
|
|
private void SyncNavMeshAgentState(bool simulate)
|
|
{
|
|
if (navMeshAgent == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!NavMeshRuntimeUtility.HasNavMeshData())
|
|
{
|
|
if (navMeshAgent.enabled)
|
|
{
|
|
navMeshAgent.enabled = false;
|
|
}
|
|
cachedNavAgentState = false;
|
|
return;
|
|
}
|
|
|
|
if (cachedNavAgentState == simulate)
|
|
{
|
|
return;
|
|
}
|
|
|
|
navMeshAgent.enabled = simulate;
|
|
cachedNavAgentState = simulate;
|
|
}
|
|
|
|
private bool IsTargetValid()
|
|
{
|
|
if (currentTargetTransform == null && currentTargetIdentity == null && currentTargetHealth == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (currentTargetHealth != null && !currentTargetHealth.IsAlive)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!TryGetTargetPosition(out Vector3 targetPosition))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
float sqrDistance = (targetPosition - transform.position).sqrMagnitude;
|
|
if (detectionRadius > 0f && sqrDistance > detectionRadius * detectionRadius)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (leashDistance > 0f)
|
|
{
|
|
float sqrLeash = (targetPosition - spawnPosition).sqrMagnitude;
|
|
if (sqrLeash > leashDistance * leashDistance)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private void AcquireTarget()
|
|
{
|
|
retargetTimer = retargetInterval;
|
|
ClearTarget();
|
|
float bestScore = float.MaxValue;
|
|
|
|
SharedHealthBuffer.Clear();
|
|
SharedHealthBuffer.AddRange(FindObjectsOfType<Health>(false));
|
|
|
|
foreach (Health candidate in SharedHealthBuffer)
|
|
{
|
|
if (candidate == null || candidate == health)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!candidate.IsAlive || candidate.Team != Team.Heroes)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
Transform candidateTransform = candidate.transform;
|
|
NetworkIdentity candidateIdentity = candidate.GetComponent<NetworkIdentity>();
|
|
Vector3 candidatePosition;
|
|
if (candidateIdentity != null && SteamCharacterStateCache.TryGetState(candidateIdentity.NetworkId, out var state))
|
|
{
|
|
candidatePosition = state.Position;
|
|
}
|
|
else
|
|
{
|
|
candidatePosition = candidateTransform.position;
|
|
}
|
|
|
|
float sqrDistance = (candidatePosition - transform.position).sqrMagnitude;
|
|
if (detectionRadius > 0f && sqrDistance > detectionRadius * detectionRadius)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (sqrDistance < bestScore)
|
|
{
|
|
bestScore = sqrDistance;
|
|
currentTargetTransform = candidateTransform;
|
|
currentTargetIdentity = candidateIdentity;
|
|
currentTargetHealth = candidate;
|
|
lastKnownTargetPosition = candidatePosition;
|
|
lastTargetPosition = Vector3.positiveInfinity;
|
|
}
|
|
}
|
|
|
|
if (currentTargetTransform == null && currentTargetIdentity == null)
|
|
{
|
|
lastTargetPosition = Vector3.positiveInfinity;
|
|
}
|
|
|
|
SharedHealthBuffer.Clear();
|
|
}
|
|
|
|
private void TickMovement(float deltaTime)
|
|
{
|
|
if (!TryGetTargetPosition(out Vector3 targetPosition))
|
|
{
|
|
ClearTarget();
|
|
return;
|
|
}
|
|
|
|
float desiredStopDistance = GetDesiredStoppingDistance();
|
|
if (navMeshAgent != null && navMeshAgent.enabled && navMeshAgent.isOnNavMesh)
|
|
{
|
|
navMeshAgent.speed = moveSpeed;
|
|
navMeshAgent.stoppingDistance = desiredStopDistance;
|
|
|
|
if (!navMeshAgent.hasPath || (targetPosition - lastTargetPosition).sqrMagnitude >= repathThreshold * repathThreshold)
|
|
{
|
|
navMeshAgent.SetDestination(targetPosition);
|
|
lastTargetPosition = targetPosition;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ManualMove(targetPosition, deltaTime, desiredStopDistance);
|
|
lastTargetPosition = targetPosition;
|
|
}
|
|
}
|
|
|
|
private void ManualMove(Vector3 targetPosition, float deltaTime, float stopDistance)
|
|
{
|
|
Vector3 currentPosition = transform.position;
|
|
Vector3 direction = targetPosition - currentPosition;
|
|
direction.y = 0f;
|
|
float distance = direction.magnitude;
|
|
|
|
float resolvedStop = Mathf.Max(0.1f, stopDistance);
|
|
if (distance <= resolvedStop)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Vector3 normalized = direction / distance;
|
|
float step = moveSpeed * deltaTime;
|
|
Vector3 newPosition = currentPosition + normalized * Mathf.Min(step, distance - resolvedStop);
|
|
transform.position = newPosition;
|
|
}
|
|
|
|
private void FaceTarget(float deltaTime)
|
|
{
|
|
if (!TryGetTargetPosition(out Vector3 targetPosition))
|
|
{
|
|
return;
|
|
}
|
|
|
|
Vector3 toTarget = targetPosition - transform.position;
|
|
toTarget.y = 0f;
|
|
if (toTarget.sqrMagnitude < 0.0001f)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Quaternion desiredRotation = Quaternion.LookRotation(toTarget.normalized, Vector3.up);
|
|
transform.rotation = Quaternion.Slerp(transform.rotation, desiredRotation, Mathf.Clamp01(turnSpeed * deltaTime));
|
|
}
|
|
|
|
private void HandleIdle()
|
|
{
|
|
StopAgentMovement();
|
|
|
|
if (!returnToSpawnWhenIdle)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if ((transform.position - spawnPosition).sqrMagnitude <= 0.25f)
|
|
{
|
|
return;
|
|
}
|
|
|
|
float stopDistance = Mathf.Max(0.1f, stoppingDistance);
|
|
if (navMeshAgent != null && navMeshAgent.enabled && navMeshAgent.isOnNavMesh)
|
|
{
|
|
navMeshAgent.SetDestination(spawnPosition);
|
|
}
|
|
else
|
|
{
|
|
ManualMove(spawnPosition, Time.deltaTime, stopDistance);
|
|
FacePoint(spawnPosition, Time.deltaTime);
|
|
}
|
|
}
|
|
|
|
private bool TryGetTargetPosition(out Vector3 position)
|
|
{
|
|
if (currentTargetIdentity != null && SteamCharacterStateCache.TryGetState(currentTargetIdentity.NetworkId, out var state))
|
|
{
|
|
position = state.Position;
|
|
lastKnownTargetPosition = position;
|
|
return true;
|
|
}
|
|
|
|
if (currentTargetTransform != null)
|
|
{
|
|
position = currentTargetTransform.position;
|
|
lastKnownTargetPosition = position;
|
|
return true;
|
|
}
|
|
|
|
if (currentTargetHealth != null)
|
|
{
|
|
position = currentTargetHealth.transform.position;
|
|
lastKnownTargetPosition = position;
|
|
return true;
|
|
}
|
|
|
|
if (lastKnownTargetPosition != Vector3.positiveInfinity)
|
|
{
|
|
position = lastKnownTargetPosition;
|
|
return true;
|
|
}
|
|
|
|
position = Vector3.zero;
|
|
return false;
|
|
}
|
|
|
|
private void ClearTarget()
|
|
{
|
|
currentTargetTransform = null;
|
|
currentTargetIdentity = null;
|
|
currentTargetHealth = null;
|
|
lastKnownTargetPosition = Vector3.positiveInfinity;
|
|
}
|
|
|
|
private void FacePoint(Vector3 point, float deltaTime)
|
|
{
|
|
Vector3 toPoint = point - transform.position;
|
|
toPoint.y = 0f;
|
|
if (toPoint.sqrMagnitude < 0.0001f)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Quaternion desiredRotation = Quaternion.LookRotation(toPoint.normalized, Vector3.up);
|
|
transform.rotation = Quaternion.Slerp(transform.rotation, desiredRotation, Mathf.Clamp01(turnSpeed * deltaTime));
|
|
}
|
|
|
|
private void StopAgentMovement()
|
|
{
|
|
if (navMeshAgent != null && navMeshAgent.enabled)
|
|
{
|
|
navMeshAgent.ResetPath();
|
|
}
|
|
}
|
|
|
|
private void HandleHealthChanged(float normalized)
|
|
{
|
|
if (normalized > 0f || pendingDespawn)
|
|
{
|
|
return;
|
|
}
|
|
|
|
BeginDespawnCountdown();
|
|
}
|
|
|
|
private void BeginDespawnCountdown()
|
|
{
|
|
if (pendingDespawn)
|
|
{
|
|
return;
|
|
}
|
|
|
|
pendingDespawn = true;
|
|
despawnTimer = Mathf.Max(0f, deathDespawnDelay);
|
|
StopAgentMovement();
|
|
}
|
|
|
|
private void TickDespawn(float deltaTime)
|
|
{
|
|
if (!pendingDespawn)
|
|
{
|
|
return;
|
|
}
|
|
|
|
despawnTimer -= deltaTime;
|
|
if (despawnTimer <= 0f)
|
|
{
|
|
CompleteDespawn();
|
|
}
|
|
}
|
|
|
|
private void CompleteDespawn()
|
|
{
|
|
pendingDespawn = false;
|
|
if (pooledInstance != null)
|
|
{
|
|
pooledInstance.ReturnToPool();
|
|
}
|
|
else
|
|
{
|
|
gameObject.SetActive(false);
|
|
}
|
|
}
|
|
|
|
private void ApplyDefinitionStats()
|
|
{
|
|
if (health == null && weaponController == null && navMeshAgent == null && !applyDefinitionStats)
|
|
{
|
|
return;
|
|
}
|
|
|
|
activeDefinition = ResolveDefinition();
|
|
|
|
if (health != null)
|
|
{
|
|
float resolvedHp = health.MaxHealth;
|
|
if (applyDefinitionStats && activeDefinition != null && activeDefinition.BaseHP > 0f)
|
|
{
|
|
resolvedHp = activeDefinition.BaseHP;
|
|
}
|
|
|
|
resolvedHp *= Mathf.Max(0.01f, healthMultiplier);
|
|
health.Revive(resolvedHp);
|
|
}
|
|
|
|
float resolvedMoveSpeed = baseMoveSpeed;
|
|
if (applyDefinitionStats && activeDefinition != null && activeDefinition.MoveSpeed > 0f)
|
|
{
|
|
resolvedMoveSpeed = activeDefinition.MoveSpeed;
|
|
}
|
|
|
|
resolvedMoveSpeed *= Mathf.Max(0.01f, moveSpeedMultiplier);
|
|
moveSpeed = resolvedMoveSpeed;
|
|
if (navMeshAgent != null)
|
|
{
|
|
navMeshAgent.speed = moveSpeed;
|
|
}
|
|
|
|
if (weaponController != null)
|
|
{
|
|
if (applyDefinitionStats && activeDefinition != null && activeDefinition.Damage > 0f)
|
|
{
|
|
float resolvedDamage = activeDefinition.Damage * Mathf.Max(0.01f, damageMultiplier);
|
|
weaponController.SetDamageOverride(resolvedDamage);
|
|
}
|
|
else
|
|
{
|
|
weaponController.ClearDamageOverride();
|
|
}
|
|
}
|
|
}
|
|
|
|
private EnemyDefinition ResolveDefinition()
|
|
{
|
|
if (pooledInstance != null && pooledInstance.Definition != null)
|
|
{
|
|
return pooledInstance.Definition;
|
|
}
|
|
|
|
return fallbackDefinition;
|
|
}
|
|
|
|
private void OnDrawGizmosSelected()
|
|
{
|
|
Gizmos.color = new Color(1f, 0.25f, 0f, 0.2f);
|
|
Gizmos.DrawWireSphere(transform.position, detectionRadius);
|
|
|
|
if (leashDistance > 0f)
|
|
{
|
|
Gizmos.color = new Color(0.2f, 0.4f, 1f, 0.2f);
|
|
Vector3 leashCenter = Application.isPlaying ? spawnPosition : transform.position;
|
|
Gizmos.DrawWireSphere(leashCenter, leashDistance);
|
|
}
|
|
}
|
|
|
|
private void OnValidate()
|
|
{
|
|
moveSpeed = Mathf.Max(0f, moveSpeed);
|
|
turnSpeed = Mathf.Max(0f, turnSpeed);
|
|
stoppingDistance = Mathf.Max(0f, stoppingDistance);
|
|
attackStoppingDistance = Mathf.Max(0f, attackStoppingDistance);
|
|
detectionRadius = Mathf.Max(0f, detectionRadius);
|
|
leashDistance = Mathf.Max(0f, leashDistance);
|
|
retargetInterval = Mathf.Max(0.05f, retargetInterval);
|
|
repathThreshold = Mathf.Max(0.1f, repathThreshold);
|
|
}
|
|
|
|
private float GetDesiredStoppingDistance()
|
|
{
|
|
if (HasAggroTarget())
|
|
{
|
|
return Mathf.Max(0.05f, attackStoppingDistance);
|
|
}
|
|
|
|
return Mathf.Max(0.05f, stoppingDistance);
|
|
}
|
|
|
|
private bool HasAggroTarget()
|
|
{
|
|
return currentTargetTransform != null || currentTargetIdentity != null || currentTargetHealth != null;
|
|
}
|
|
}
|
|
}
|