Summons
This commit is contained in:
8
Assets/AI/_ShadowPrince.meta
Normal file
8
Assets/AI/_ShadowPrince.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e0df5053eb5137643a9894a4186b74f7
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
101
Assets/AI/_Summons/SummonDamageReceiver.cs
Normal file
101
Assets/AI/_Summons/SummonDamageReceiver.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using UnityEngine;
|
||||
using Invector;
|
||||
|
||||
public class SummonDamageReceiver : MonoBehaviour, vIHealthController
|
||||
{
|
||||
[Header("Health")]
|
||||
[SerializeField] private int maxHealth = 40;
|
||||
[SerializeField] private float currentHealth = 40f;
|
||||
|
||||
[Header("Death")]
|
||||
[SerializeField] private bool destroyOnDeath = true;
|
||||
[SerializeField] private float destroyDelay = 0.1f;
|
||||
[SerializeField] private GameObject deathEffect;
|
||||
|
||||
[Header("Debug")]
|
||||
[SerializeField] private bool enableDebug = false;
|
||||
|
||||
[SerializeField] private OnReceiveDamage _onStartReceiveDamage = new OnReceiveDamage();
|
||||
[SerializeField] private OnReceiveDamage _onReceiveDamage = new OnReceiveDamage();
|
||||
[SerializeField] private OnDead _onDead = new OnDead();
|
||||
|
||||
public OnReceiveDamage onStartReceiveDamage => _onStartReceiveDamage;
|
||||
public OnReceiveDamage onReceiveDamage => _onReceiveDamage;
|
||||
public OnDead onDead => _onDead;
|
||||
|
||||
public float currentHealth => this.currentHealth;
|
||||
public int MaxHealth => maxHealth;
|
||||
public bool isDead { get; set; }
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
ResetHealth();
|
||||
}
|
||||
|
||||
public void TakeDamage(vDamage damage)
|
||||
{
|
||||
if (isDead) return;
|
||||
_onStartReceiveDamage.Invoke(damage);
|
||||
|
||||
int dmg = Mathf.RoundToInt(damage.damageValue);
|
||||
if (dmg <= 0) return;
|
||||
|
||||
ChangeHealth(-dmg);
|
||||
_onReceiveDamage.Invoke(damage);
|
||||
|
||||
if (enableDebug)
|
||||
Debug.Log($"[SummonDamageReceiver] {gameObject.name} took {dmg}, HP {currentHealth}/{maxHealth}");
|
||||
}
|
||||
|
||||
public void AddHealth(int value)
|
||||
{
|
||||
if (isDead) return;
|
||||
currentHealth = Mathf.Min(maxHealth, currentHealth + value);
|
||||
}
|
||||
|
||||
public void ChangeHealth(int value)
|
||||
{
|
||||
if (isDead) return;
|
||||
currentHealth = Mathf.Clamp(currentHealth + value, 0f, maxHealth);
|
||||
|
||||
if (currentHealth <= 0f)
|
||||
Die();
|
||||
}
|
||||
|
||||
public void ChangeMaxHealth(int value)
|
||||
{
|
||||
maxHealth = Mathf.Max(1, maxHealth + value);
|
||||
currentHealth = Mathf.Min(currentHealth, maxHealth);
|
||||
}
|
||||
|
||||
public void ResetHealth(float health)
|
||||
{
|
||||
maxHealth = Mathf.Max(1, maxHealth);
|
||||
currentHealth = Mathf.Clamp(health, 0f, maxHealth);
|
||||
isDead = false;
|
||||
}
|
||||
|
||||
public void ResetHealth()
|
||||
{
|
||||
maxHealth = Mathf.Max(1, maxHealth);
|
||||
currentHealth = maxHealth;
|
||||
isDead = false;
|
||||
}
|
||||
|
||||
private void Die()
|
||||
{
|
||||
if (isDead) return;
|
||||
isDead = true;
|
||||
|
||||
_onDead.Invoke(gameObject);
|
||||
|
||||
if (deathEffect != null)
|
||||
{
|
||||
GameObject fx = Instantiate(deathEffect, transform.position, transform.rotation);
|
||||
Destroy(fx, 5f);
|
||||
}
|
||||
|
||||
if (destroyOnDeath)
|
||||
Destroy(gameObject, destroyDelay);
|
||||
}
|
||||
}
|
||||
@@ -284,7 +284,7 @@ Animator:
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2274584332873493075}
|
||||
m_Enabled: 1
|
||||
m_Enabled: 0
|
||||
m_Avatar: {fileID: 9000000, guid: 0120314ed622daf4f829420bf369f48d, type: 3}
|
||||
m_Controller: {fileID: 9100000, guid: f1afa426b318a544da0ef42f7fe15542, type: 2}
|
||||
m_CullingMode: 1
|
||||
@@ -936,7 +936,7 @@ MonoBehaviour:
|
||||
openCloseWindow: 1
|
||||
selectedToolbar: 0
|
||||
_fsmBehaviour: {fileID: 11400000, guid: 73af8a7d97f5f714d819d2d2f73aa449, type: 2}
|
||||
_stop: 0
|
||||
_stop: 1
|
||||
_debugMode: 0
|
||||
onStartFSM:
|
||||
m_PersistentCalls:
|
||||
|
||||
@@ -166,7 +166,7 @@ SkinnedMeshRenderer:
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 119298518639331252}
|
||||
m_Enabled: 1
|
||||
m_Enabled: 0
|
||||
m_CastShadows: 1
|
||||
m_ReceiveShadows: 1
|
||||
m_DynamicOccludee: 1
|
||||
@@ -21859,7 +21859,7 @@ MonoBehaviour:
|
||||
openCloseWindow: 1
|
||||
selectedToolbar: 0
|
||||
_fsmBehaviour: {fileID: 11400000, guid: 478bc6a7e9fd82c4a9d395af400e05e6, type: 2}
|
||||
_stop: 0
|
||||
_stop: 1
|
||||
_debugMode: 0
|
||||
onStartFSM:
|
||||
m_PersistentCalls:
|
||||
|
||||
@@ -4533,7 +4533,7 @@ ParticleSystemRenderer:
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 6585598795937982}
|
||||
m_Enabled: 1
|
||||
m_Enabled: 0
|
||||
m_CastShadows: 1
|
||||
m_ReceiveShadows: 1
|
||||
m_DynamicOccludee: 1
|
||||
@@ -13371,7 +13371,7 @@ MonoBehaviour:
|
||||
openCloseWindow: 1
|
||||
selectedToolbar: 0
|
||||
_fsmBehaviour: {fileID: 11400000, guid: d9cf652bbbb9e0c40b58b5d59170055a, type: 2}
|
||||
_stop: 0
|
||||
_stop: 1
|
||||
_debugMode: 0
|
||||
onStartFSM:
|
||||
m_PersistentCalls:
|
||||
|
||||
@@ -103,7 +103,7 @@ MonoBehaviour:
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 69635961216777874}
|
||||
m_Enabled: 1
|
||||
m_Enabled: 0
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 1b04f4ac1b776ff48b5f78a97b57f776, type: 3}
|
||||
m_Name:
|
||||
@@ -30742,7 +30742,7 @@ MonoBehaviour:
|
||||
openCloseWindow: 1
|
||||
selectedToolbar: 0
|
||||
_fsmBehaviour: {fileID: 11400000, guid: 73af8a7d97f5f714d819d2d2f73aa449, type: 2}
|
||||
_stop: 0
|
||||
_stop: 1
|
||||
_debugMode: 0
|
||||
onStartFSM:
|
||||
m_PersistentCalls:
|
||||
|
||||
@@ -39,7 +39,7 @@ SkinnedMeshRenderer:
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 226257558141750692}
|
||||
m_Enabled: 1
|
||||
m_Enabled: 0
|
||||
m_CastShadows: 1
|
||||
m_ReceiveShadows: 1
|
||||
m_DynamicOccludee: 1
|
||||
@@ -6368,7 +6368,7 @@ MonoBehaviour:
|
||||
openCloseWindow: 1
|
||||
selectedToolbar: 0
|
||||
_fsmBehaviour: {fileID: 11400000, guid: 478bc6a7e9fd82c4a9d395af400e05e6, type: 2}
|
||||
_stop: 0
|
||||
_stop: 1
|
||||
_debugMode: 0
|
||||
onStartFSM:
|
||||
m_PersistentCalls:
|
||||
|
||||
@@ -4753,7 +4753,7 @@ ParticleSystemRenderer:
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 644455550314575239}
|
||||
m_Enabled: 1
|
||||
m_Enabled: 0
|
||||
m_CastShadows: 0
|
||||
m_ReceiveShadows: 1
|
||||
m_DynamicOccludee: 1
|
||||
@@ -38453,7 +38453,7 @@ MonoBehaviour:
|
||||
openCloseWindow: 1
|
||||
selectedToolbar: 0
|
||||
_fsmBehaviour: {fileID: 11400000, guid: 478bc6a7e9fd82c4a9d395af400e05e6, type: 2}
|
||||
_stop: 0
|
||||
_stop: 1
|
||||
_debugMode: 0
|
||||
onStartFSM:
|
||||
m_PersistentCalls:
|
||||
|
||||
@@ -103,7 +103,7 @@ MonoBehaviour:
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 466729051031186190}
|
||||
m_Enabled: 1
|
||||
m_Enabled: 0
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: fb97f58541b05b24ab37661346298ff2, type: 3}
|
||||
m_Name:
|
||||
@@ -1005,7 +1005,7 @@ MonoBehaviour:
|
||||
openCloseWindow: 1
|
||||
selectedToolbar: 0
|
||||
_fsmBehaviour: {fileID: 0}
|
||||
_stop: 0
|
||||
_stop: 1
|
||||
_debugMode: 0
|
||||
onStartFSM:
|
||||
m_PersistentCalls:
|
||||
|
||||
@@ -198,7 +198,7 @@ CapsuleCollider:
|
||||
m_LayerOverridePriority: 0
|
||||
m_IsTrigger: 0
|
||||
m_ProvidesContacts: 0
|
||||
m_Enabled: 1
|
||||
m_Enabled: 0
|
||||
serializedVersion: 2
|
||||
m_Radius: 1
|
||||
m_Height: 4.25
|
||||
@@ -758,7 +758,7 @@ MonoBehaviour:
|
||||
openCloseWindow: 1
|
||||
selectedToolbar: 0
|
||||
_fsmBehaviour: {fileID: 11400000, guid: 478bc6a7e9fd82c4a9d395af400e05e6, type: 2}
|
||||
_stop: 0
|
||||
_stop: 1
|
||||
_debugMode: 0
|
||||
onStartFSM:
|
||||
m_PersistentCalls:
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
56
Assets/AI/_Summons/SwarmCoordinator.cs
Normal file
56
Assets/AI/_Summons/SwarmCoordinator.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
public class SwarmCoordinator : MonoBehaviour
|
||||
{
|
||||
[Header("Targeting")]
|
||||
[SerializeField] private bool autoFindPlayer = true;
|
||||
[SerializeField] private string playerTag = "Player";
|
||||
[SerializeField] private float targetRefreshInterval = 0.75f;
|
||||
|
||||
[Header("Swarm Tuning")]
|
||||
[SerializeField] private float updateInterval = 0.2f;
|
||||
[SerializeField] private float updateJitter = 0.05f;
|
||||
[SerializeField] private float separationRadius = 1.2f;
|
||||
[SerializeField] private float separationWeight = 1.0f;
|
||||
|
||||
private readonly List<SwarmAgent> agents = new List<SwarmAgent>();
|
||||
private Transform target;
|
||||
private float nextTargetRefreshTime;
|
||||
|
||||
public Transform Target => target;
|
||||
public float UpdateInterval => updateInterval;
|
||||
public float UpdateJitter => updateJitter;
|
||||
public float SeparationRadius => separationRadius;
|
||||
public float SeparationWeight => separationWeight;
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!autoFindPlayer) return;
|
||||
if (Time.time < nextTargetRefreshTime) return;
|
||||
nextTargetRefreshTime = Time.time + targetRefreshInterval;
|
||||
|
||||
if (target == null)
|
||||
{
|
||||
GameObject player = GameObject.FindGameObjectWithTag(playerTag);
|
||||
if (player != null) target = player.transform;
|
||||
}
|
||||
}
|
||||
|
||||
public void Register(SwarmAgent agent)
|
||||
{
|
||||
if (agent == null || agents.Contains(agent)) return;
|
||||
agents.Add(agent);
|
||||
}
|
||||
|
||||
public void Unregister(SwarmAgent agent)
|
||||
{
|
||||
if (agent == null) return;
|
||||
agents.Remove(agent);
|
||||
}
|
||||
|
||||
public List<SwarmAgent> GetAgents()
|
||||
{
|
||||
return agents;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user