Summons
This commit is contained in:
378
Assets/AI/_Summons/SwarmAgent.cs
Normal file
378
Assets/AI/_Summons/SwarmAgent.cs
Normal file
@@ -0,0 +1,378 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user