enemy ai
This commit is contained in:
362
Game/Scripts/Enemy/SteamEnemyController.cs
Normal file
362
Game/Scripts/Enemy/SteamEnemyController.cs
Normal file
@@ -0,0 +1,362 @@
|
||||
using System.Collections.Generic;
|
||||
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 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;
|
||||
|
||||
private SteamCoopNetworkManager networkManager;
|
||||
private Transform currentTarget;
|
||||
private Health currentTargetHealth;
|
||||
private Vector3 lastTargetPosition;
|
||||
private float retargetTimer;
|
||||
private Vector3 spawnPosition;
|
||||
private bool cachedNavAgentState;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
spawnPosition = transform.position;
|
||||
|
||||
if (navMeshAgent == null)
|
||||
{
|
||||
navMeshAgent = GetComponent<NavMeshAgent>();
|
||||
}
|
||||
|
||||
if (weaponController == null)
|
||||
{
|
||||
weaponController = GetComponent<WeaponController>();
|
||||
}
|
||||
|
||||
if (health == null)
|
||||
{
|
||||
health = GetComponent<Health>();
|
||||
}
|
||||
|
||||
if (identity == null)
|
||||
{
|
||||
identity = GetComponent<NetworkIdentity>();
|
||||
}
|
||||
|
||||
if (networkTransform == null)
|
||||
{
|
||||
networkTransform = GetComponent<SteamNetworkTransform>();
|
||||
}
|
||||
|
||||
if (navMeshAgent != null)
|
||||
{
|
||||
navMeshAgent.updateRotation = false;
|
||||
navMeshAgent.stoppingDistance = stoppingDistance;
|
||||
navMeshAgent.speed = moveSpeed;
|
||||
cachedNavAgentState = navMeshAgent.enabled;
|
||||
}
|
||||
|
||||
lastTargetPosition = Vector3.positiveInfinity;
|
||||
retargetTimer = Random.Range(0f, Mathf.Max(0.05f, retargetInterval));
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
networkManager = SteamCoopNetworkManager.Instance;
|
||||
SyncNavMeshAgentState(ShouldSimulate());
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
networkManager ??= SteamCoopNetworkManager.Instance;
|
||||
|
||||
bool simulate = ShouldSimulate();
|
||||
SyncNavMeshAgentState(simulate);
|
||||
|
||||
if (weaponController != null)
|
||||
{
|
||||
weaponController.enabled = simulate;
|
||||
}
|
||||
|
||||
if (!simulate)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (health != null && !health.IsAlive)
|
||||
{
|
||||
StopAgentMovement();
|
||||
return;
|
||||
}
|
||||
|
||||
retargetTimer -= Time.deltaTime;
|
||||
if (!IsTargetValid() || retargetTimer <= 0f)
|
||||
{
|
||||
AcquireTarget();
|
||||
}
|
||||
|
||||
if (currentTarget == null)
|
||||
{
|
||||
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 void SyncNavMeshAgentState(bool simulate)
|
||||
{
|
||||
if (navMeshAgent == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (cachedNavAgentState == simulate)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
navMeshAgent.enabled = simulate;
|
||||
cachedNavAgentState = simulate;
|
||||
}
|
||||
|
||||
private bool IsTargetValid()
|
||||
{
|
||||
if (currentTarget == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (currentTargetHealth != null && !currentTargetHealth.IsAlive)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
float sqrDistance = (currentTarget.position - transform.position).sqrMagnitude;
|
||||
if (detectionRadius > 0f && sqrDistance > detectionRadius * detectionRadius)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (leashDistance > 0f)
|
||||
{
|
||||
float sqrLeash = (currentTarget.position - spawnPosition).sqrMagnitude;
|
||||
if (sqrLeash > leashDistance * leashDistance)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void AcquireTarget()
|
||||
{
|
||||
retargetTimer = retargetInterval;
|
||||
currentTarget = null;
|
||||
currentTargetHealth = null;
|
||||
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;
|
||||
float sqrDistance = (candidateTransform.position - transform.position).sqrMagnitude;
|
||||
if (detectionRadius > 0f && sqrDistance > detectionRadius * detectionRadius)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sqrDistance < bestScore)
|
||||
{
|
||||
bestScore = sqrDistance;
|
||||
currentTarget = candidateTransform;
|
||||
currentTargetHealth = candidate;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentTarget == null)
|
||||
{
|
||||
lastTargetPosition = Vector3.positiveInfinity;
|
||||
}
|
||||
|
||||
SharedHealthBuffer.Clear();
|
||||
}
|
||||
|
||||
private void TickMovement(float deltaTime)
|
||||
{
|
||||
Vector3 targetPosition = currentTarget.position;
|
||||
|
||||
if (navMeshAgent != null && navMeshAgent.enabled && navMeshAgent.isOnNavMesh)
|
||||
{
|
||||
navMeshAgent.speed = moveSpeed;
|
||||
navMeshAgent.stoppingDistance = stoppingDistance;
|
||||
|
||||
if (!navMeshAgent.hasPath || (targetPosition - lastTargetPosition).sqrMagnitude >= repathThreshold * repathThreshold)
|
||||
{
|
||||
navMeshAgent.SetDestination(targetPosition);
|
||||
lastTargetPosition = targetPosition;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ManualMove(targetPosition, deltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
private void ManualMove(Vector3 targetPosition, float deltaTime)
|
||||
{
|
||||
Vector3 currentPosition = transform.position;
|
||||
Vector3 direction = targetPosition - currentPosition;
|
||||
direction.y = 0f;
|
||||
float distance = direction.magnitude;
|
||||
|
||||
if (distance <= Mathf.Max(0.1f, stoppingDistance))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Vector3 normalized = direction / distance;
|
||||
float step = moveSpeed * deltaTime;
|
||||
Vector3 newPosition = currentPosition + normalized * Mathf.Min(step, distance - stoppingDistance);
|
||||
transform.position = newPosition;
|
||||
}
|
||||
|
||||
private void FaceTarget(float deltaTime)
|
||||
{
|
||||
Vector3 toTarget = currentTarget.position - 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;
|
||||
}
|
||||
|
||||
if (navMeshAgent != null && navMeshAgent.enabled && navMeshAgent.isOnNavMesh)
|
||||
{
|
||||
navMeshAgent.SetDestination(spawnPosition);
|
||||
}
|
||||
else
|
||||
{
|
||||
ManualMove(spawnPosition, Time.deltaTime);
|
||||
FacePoint(spawnPosition, Time.deltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
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 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);
|
||||
detectionRadius = Mathf.Max(0f, detectionRadius);
|
||||
leashDistance = Mathf.Max(0f, leashDistance);
|
||||
retargetInterval = Mathf.Max(0.05f, retargetInterval);
|
||||
repathThreshold = Mathf.Max(0.1f, repathThreshold);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Game/Scripts/Enemy/SteamEnemyController.cs.meta
Normal file
2
Game/Scripts/Enemy/SteamEnemyController.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 44c91a3744d2af1e4b9ee0f82451691a
|
||||
Reference in New Issue
Block a user