379 lines
9.6 KiB
C#
379 lines
9.6 KiB
C#
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
using UnityEngine.AI;
|
|
using Invector.vMelee;
|
|
|
|
public class SwarmAgent : MonoBehaviour
|
|
{
|
|
public enum SwarmProfile
|
|
{
|
|
Default,
|
|
Spider,
|
|
Wolf,
|
|
Dragonfly
|
|
}
|
|
|
|
private enum SwarmState
|
|
{
|
|
Idle,
|
|
Chase,
|
|
Attack,
|
|
Retreat
|
|
}
|
|
|
|
[Header("Profile")]
|
|
[SerializeField] private bool autoProfileByName = true;
|
|
[SerializeField] private SwarmProfile profileOverride = SwarmProfile.Default;
|
|
|
|
[Header("Targeting")]
|
|
[SerializeField] private SwarmCoordinator coordinator;
|
|
[SerializeField] private string playerTag = "Player";
|
|
[SerializeField] private float targetRefreshInterval = 1.0f;
|
|
|
|
[Header("Movement")]
|
|
[SerializeField] private float moveSpeed = 3.5f;
|
|
[SerializeField] private float acceleration = 8f;
|
|
[SerializeField] private float angularSpeed = 360f;
|
|
[SerializeField] private float stoppingDistance = 1.6f;
|
|
[SerializeField] private float repathInterval = 0.25f;
|
|
|
|
[Header("Attack")]
|
|
[SerializeField] private float attackRange = 1.8f;
|
|
[SerializeField] private Vector2Int attacksPerBurst = new Vector2Int(1, 2);
|
|
[SerializeField] private float attackCooldown = 0.8f;
|
|
[SerializeField] private float damageEnableDelay = 0.1f;
|
|
[SerializeField] private float damageWindow = 0.25f;
|
|
[SerializeField] private string[] attackTriggers = new string[] { "Attack" };
|
|
|
|
[Header("Retreat")]
|
|
[SerializeField] private float retreatDuration = 1.25f;
|
|
[SerializeField] private float retreatDistance = 3.5f;
|
|
|
|
[Header("Spider Strafe")]
|
|
[SerializeField] private float strafeRadius = 2.5f;
|
|
[SerializeField] private float strafeSpeed = 2.0f;
|
|
|
|
[Header("Wolf Flank")]
|
|
[SerializeField] private float flankDistance = 2.5f;
|
|
[SerializeField] private float flankSideOffset = 2.0f;
|
|
[SerializeField] private float flankReplanInterval = 0.6f;
|
|
|
|
[Header("Dragonfly Figure 8")]
|
|
[SerializeField] private float figureEightRadius = 3.0f;
|
|
[SerializeField] private float figureEightSpeed = 2.2f;
|
|
|
|
private SwarmProfile profile;
|
|
private SwarmState state = SwarmState.Idle;
|
|
private NavMeshAgent agent;
|
|
private Animator animator;
|
|
private vMeleeAttackObject[] meleeAttackObjects;
|
|
private Transform target;
|
|
private float nextTargetRefreshTime;
|
|
private float nextRepathTime;
|
|
private float nextTickTime;
|
|
private float lastAttackTime;
|
|
private float retreatEndTime;
|
|
private float flankSide = 1f;
|
|
private float nextFlankReplanTime;
|
|
private Vector3 retreatCenter;
|
|
private float retreatStartTime;
|
|
private Coroutine attackRoutine;
|
|
|
|
private void Awake()
|
|
{
|
|
agent = GetComponent<NavMeshAgent>();
|
|
animator = GetComponentInChildren<Animator>();
|
|
meleeAttackObjects = GetComponentsInChildren<vMeleeAttackObject>(true);
|
|
|
|
if (coordinator == null)
|
|
coordinator = GetComponentInParent<SwarmCoordinator>();
|
|
|
|
if (autoProfileByName)
|
|
profile = ResolveProfileByName(gameObject.name);
|
|
else
|
|
profile = profileOverride;
|
|
}
|
|
|
|
private void OnEnable()
|
|
{
|
|
if (coordinator != null) coordinator.Register(this);
|
|
DisableDamage();
|
|
ConfigureAgent();
|
|
}
|
|
|
|
private void OnDisable()
|
|
{
|
|
if (coordinator != null) coordinator.Unregister(this);
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
if (Time.time < nextTickTime) return;
|
|
nextTickTime = Time.time + GetUpdateInterval();
|
|
Tick();
|
|
}
|
|
|
|
private void Tick()
|
|
{
|
|
RefreshTargetIfNeeded();
|
|
if (target == null)
|
|
{
|
|
state = SwarmState.Idle;
|
|
if (agent != null) agent.isStopped = true;
|
|
return;
|
|
}
|
|
|
|
if (state == SwarmState.Retreat)
|
|
{
|
|
UpdateRetreat();
|
|
return;
|
|
}
|
|
|
|
if (state == SwarmState.Attack)
|
|
return;
|
|
|
|
float sqrDist = (target.position - transform.position).sqrMagnitude;
|
|
float attackRangeSqr = attackRange * attackRange;
|
|
|
|
if (sqrDist <= attackRangeSqr && Time.time >= lastAttackTime + attackCooldown)
|
|
{
|
|
StartAttackBurst();
|
|
return;
|
|
}
|
|
|
|
UpdateChase();
|
|
}
|
|
|
|
private void UpdateChase()
|
|
{
|
|
if (agent == null) return;
|
|
state = SwarmState.Chase;
|
|
agent.isStopped = false;
|
|
|
|
if (Time.time < nextRepathTime) return;
|
|
nextRepathTime = Time.time + repathInterval;
|
|
|
|
Vector3 desired = GetDesiredPosition();
|
|
agent.SetDestination(desired);
|
|
}
|
|
|
|
private void StartAttackBurst()
|
|
{
|
|
if (attackRoutine != null) StopCoroutine(attackRoutine);
|
|
attackRoutine = StartCoroutine(AttackBurstCoroutine());
|
|
}
|
|
|
|
private IEnumerator AttackBurstCoroutine()
|
|
{
|
|
state = SwarmState.Attack;
|
|
lastAttackTime = Time.time;
|
|
|
|
if (agent != null) agent.isStopped = true;
|
|
|
|
int minAttacks = Mathf.Max(1, attacksPerBurst.x);
|
|
int maxAttacks = Mathf.Max(minAttacks, attacksPerBurst.y);
|
|
int count = Random.Range(minAttacks, maxAttacks + 1);
|
|
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
TriggerAttackAnimation();
|
|
yield return StartCoroutine(DamageWindowCoroutine());
|
|
yield return new WaitForSeconds(attackCooldown);
|
|
}
|
|
|
|
StartRetreat();
|
|
}
|
|
|
|
private IEnumerator DamageWindowCoroutine()
|
|
{
|
|
if (damageEnableDelay > 0f)
|
|
yield return new WaitForSeconds(damageEnableDelay);
|
|
|
|
EnableDamage();
|
|
|
|
if (damageWindow > 0f)
|
|
yield return new WaitForSeconds(damageWindow);
|
|
|
|
DisableDamage();
|
|
}
|
|
|
|
private void StartRetreat()
|
|
{
|
|
state = SwarmState.Retreat;
|
|
retreatStartTime = Time.time;
|
|
retreatEndTime = retreatStartTime + retreatDuration;
|
|
|
|
if (target != null)
|
|
retreatCenter = target.position;
|
|
else
|
|
retreatCenter = transform.position;
|
|
|
|
if (agent != null) agent.isStopped = false;
|
|
}
|
|
|
|
private void UpdateRetreat()
|
|
{
|
|
if (agent == null) return;
|
|
if (Time.time >= retreatEndTime)
|
|
{
|
|
state = SwarmState.Chase;
|
|
return;
|
|
}
|
|
|
|
if (Time.time < nextRepathTime) return;
|
|
nextRepathTime = Time.time + repathInterval;
|
|
|
|
Vector3 retreatTarget;
|
|
if (profile == SwarmProfile.Dragonfly)
|
|
{
|
|
float t = (Time.time - retreatStartTime) * figureEightSpeed;
|
|
float x = Mathf.Sin(t);
|
|
float z = Mathf.Sin(t * 2f) * 0.5f;
|
|
Vector3 offset = new Vector3(x, 0f, z) * figureEightRadius;
|
|
retreatTarget = retreatCenter + offset;
|
|
}
|
|
else
|
|
{
|
|
Vector3 away = (transform.position - target.position).normalized;
|
|
retreatTarget = transform.position + away * retreatDistance;
|
|
}
|
|
|
|
agent.SetDestination(retreatTarget);
|
|
}
|
|
|
|
private Vector3 GetDesiredPosition()
|
|
{
|
|
Vector3 baseTarget = target.position;
|
|
Vector3 separation = GetSeparationOffset();
|
|
|
|
switch (profile)
|
|
{
|
|
case SwarmProfile.Spider:
|
|
return baseTarget + GetStrafeOffset() + separation;
|
|
case SwarmProfile.Wolf:
|
|
return GetFlankPosition() + separation;
|
|
case SwarmProfile.Dragonfly:
|
|
return baseTarget + separation;
|
|
default:
|
|
return baseTarget + separation;
|
|
}
|
|
}
|
|
|
|
private Vector3 GetSeparationOffset()
|
|
{
|
|
if (coordinator == null) return Vector3.zero;
|
|
float radius = coordinator.SeparationRadius;
|
|
if (radius <= 0f) return Vector3.zero;
|
|
|
|
List<SwarmAgent> agents = coordinator.GetAgents();
|
|
if (agents == null || agents.Count == 0) return Vector3.zero;
|
|
|
|
Vector3 offset = Vector3.zero;
|
|
float radiusSqr = radius * radius;
|
|
Vector3 pos = transform.position;
|
|
|
|
for (int i = 0; i < agents.Count; i++)
|
|
{
|
|
SwarmAgent other = agents[i];
|
|
if (other == null || other == this) continue;
|
|
Vector3 delta = pos - other.transform.position;
|
|
float distSqr = delta.sqrMagnitude;
|
|
if (distSqr > 0f && distSqr < radiusSqr)
|
|
offset += delta.normalized * (1f - (distSqr / radiusSqr));
|
|
}
|
|
|
|
return offset * coordinator.SeparationWeight;
|
|
}
|
|
|
|
private Vector3 GetStrafeOffset()
|
|
{
|
|
Vector3 toTarget = (target.position - transform.position).normalized;
|
|
Vector3 right = new Vector3(-toTarget.z, 0f, toTarget.x);
|
|
float wave = Mathf.Sin(Time.time * strafeSpeed);
|
|
return right * (wave * strafeRadius);
|
|
}
|
|
|
|
private Vector3 GetFlankPosition()
|
|
{
|
|
if (Time.time >= nextFlankReplanTime)
|
|
{
|
|
nextFlankReplanTime = Time.time + flankReplanInterval;
|
|
flankSide = Random.value < 0.5f ? -1f : 1f;
|
|
}
|
|
|
|
Vector3 behind = -target.forward * flankDistance;
|
|
Vector3 side = target.right * (flankSide * flankSideOffset);
|
|
Vector3 desired = target.position + behind + side;
|
|
return desired;
|
|
}
|
|
|
|
private void ConfigureAgent()
|
|
{
|
|
if (agent == null) return;
|
|
agent.speed = moveSpeed;
|
|
agent.acceleration = acceleration;
|
|
agent.angularSpeed = angularSpeed;
|
|
agent.stoppingDistance = stoppingDistance;
|
|
}
|
|
|
|
private void RefreshTargetIfNeeded()
|
|
{
|
|
if (coordinator != null && coordinator.Target != null)
|
|
{
|
|
target = coordinator.Target;
|
|
return;
|
|
}
|
|
|
|
if (Time.time < nextTargetRefreshTime) return;
|
|
nextTargetRefreshTime = Time.time + targetRefreshInterval;
|
|
|
|
GameObject player = GameObject.FindGameObjectWithTag(playerTag);
|
|
if (player != null) target = player.transform;
|
|
}
|
|
|
|
private void TriggerAttackAnimation()
|
|
{
|
|
if (animator == null || attackTriggers == null || attackTriggers.Length == 0) return;
|
|
string trigger = attackTriggers[Random.Range(0, attackTriggers.Length)];
|
|
if (!string.IsNullOrEmpty(trigger))
|
|
animator.SetTrigger(trigger);
|
|
}
|
|
|
|
private void EnableDamage()
|
|
{
|
|
for (int i = 0; i < meleeAttackObjects.Length; i++)
|
|
{
|
|
if (meleeAttackObjects[i] != null)
|
|
meleeAttackObjects[i].SetActiveDamage(true);
|
|
}
|
|
}
|
|
|
|
private void DisableDamage()
|
|
{
|
|
for (int i = 0; i < meleeAttackObjects.Length; i++)
|
|
{
|
|
if (meleeAttackObjects[i] != null)
|
|
meleeAttackObjects[i].SetActiveDamage(false);
|
|
}
|
|
}
|
|
|
|
private float GetUpdateInterval()
|
|
{
|
|
if (coordinator == null) return 0.2f;
|
|
float jitter = coordinator.UpdateJitter;
|
|
return coordinator.UpdateInterval + (jitter > 0f ? Random.Range(-jitter, jitter) : 0f);
|
|
}
|
|
|
|
private SwarmProfile ResolveProfileByName(string objectName)
|
|
{
|
|
string nameLower = objectName.ToLowerInvariant();
|
|
if (nameLower.Contains("spider")) return SwarmProfile.Spider;
|
|
if (nameLower.Contains("dog") || nameLower.Contains("barghest") || nameLower.Contains("golem"))
|
|
return SwarmProfile.Wolf;
|
|
if (nameLower.Contains("bug") || nameLower.Contains("flying"))
|
|
return SwarmProfile.Dragonfly;
|
|
return SwarmProfile.Default;
|
|
}
|
|
}
|