적 AI에 어그로 시스템 추가
This commit is contained in:
@@ -2,6 +2,7 @@ using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
using UnityEngine.AI;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Northbound
|
||||
{
|
||||
@@ -65,6 +66,25 @@ namespace Northbound
|
||||
[Tooltip("이 크립이 속한 캠프 (null이면 독립 크립)")]
|
||||
public CreepCamp creepCamp;
|
||||
|
||||
[Header("Aggro System")]
|
||||
[Tooltip("어그로(위협도) 시스템 활성화")]
|
||||
public bool enableAggroSystem = true;
|
||||
|
||||
[Tooltip("데미지당 어그로 증가량")]
|
||||
public float damageAggroMultiplier = 2f;
|
||||
|
||||
[Tooltip("어그로 감쇠율 (초당)")]
|
||||
public float aggroDecayRate = 5f;
|
||||
|
||||
[Tooltip("새 타겟 전환에 필요한 최소 어그로 차이")]
|
||||
public float aggroSwitchThreshold = 20f;
|
||||
|
||||
[Tooltip("타겟 재평가 주기 (초)")]
|
||||
public float targetReevaluateInterval = 1f;
|
||||
|
||||
[Tooltip("거리 기반 타겟 전환 허용 거리차")]
|
||||
public float distanceSwitchThreshold = 5f;
|
||||
|
||||
private NavMeshAgent _agent;
|
||||
private EnemyUnit _enemyUnit;
|
||||
private Core _core;
|
||||
@@ -96,6 +116,11 @@ namespace Northbound
|
||||
private float _attackStartTime;
|
||||
private const float ATTACK_TIMEOUT = 1f; // 애니메이션 이벤트 미발생 시 타임아웃
|
||||
|
||||
// 어그로 시스템
|
||||
private Dictionary<ulong, float> _aggroTable = new Dictionary<ulong, float>();
|
||||
private float _lastTargetReevaluateTime;
|
||||
private System.Action<ulong, int> _damageTakenHandler;
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
base.OnNetworkSpawn();
|
||||
@@ -131,6 +156,13 @@ namespace Northbound
|
||||
if (_enemyUnit != null)
|
||||
{
|
||||
_enemyUnit.OnDeath += HandleDeath;
|
||||
|
||||
// 데미지 받음 이벤트 구독 (어그로 시스템)
|
||||
if (enableAggroSystem)
|
||||
{
|
||||
_damageTakenHandler = (attackerId, damage) => HandleDamageTaken(attackerId, damage);
|
||||
_enemyUnit.OnDamageTaken += _damageTakenHandler;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -140,6 +172,11 @@ namespace Northbound
|
||||
if (_enemyUnit != null)
|
||||
{
|
||||
_enemyUnit.OnDeath -= HandleDeath;
|
||||
|
||||
if (_damageTakenHandler != null)
|
||||
{
|
||||
_enemyUnit.OnDamageTaken -= _damageTakenHandler;
|
||||
}
|
||||
}
|
||||
base.OnNetworkDespawn();
|
||||
}
|
||||
@@ -149,6 +186,12 @@ namespace Northbound
|
||||
if (!IsServer) return;
|
||||
if (!_agent.isOnNavMesh) return;
|
||||
|
||||
// 어그로 시스템 업데이트
|
||||
if (enableAggroSystem)
|
||||
{
|
||||
ReevaluateTarget();
|
||||
}
|
||||
|
||||
switch (_currentState.Value)
|
||||
{
|
||||
case EnemyAIState.Idle: UpdateIdle(); break;
|
||||
@@ -425,8 +468,20 @@ namespace Northbound
|
||||
_cachedTargetPlayer = target;
|
||||
|
||||
var networkObject = target.GetComponentInParent<NetworkObject>();
|
||||
if (networkObject != null) _targetPlayerId.Value = networkObject.NetworkObjectId;
|
||||
else _targetPlayerId.Value = 0;
|
||||
if (networkObject != null)
|
||||
{
|
||||
_targetPlayerId.Value = networkObject.NetworkObjectId;
|
||||
|
||||
// 타겟 설정 시 기본 어그로 부여 (최초 감지)
|
||||
if (enableAggroSystem && !_aggroTable.ContainsKey(networkObject.NetworkObjectId))
|
||||
{
|
||||
_aggroTable[networkObject.NetworkObjectId] = 10f; // 기본 어그로
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_targetPlayerId.Value = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearTargetPlayer()
|
||||
@@ -587,6 +642,242 @@ namespace Northbound
|
||||
|
||||
#endregion
|
||||
|
||||
#region Aggro System
|
||||
|
||||
private void HandleDamageTaken(ulong attackerId, int damage)
|
||||
{
|
||||
if (!IsServer) return;
|
||||
if (!enableAggroSystem) return;
|
||||
|
||||
// 공격자에게 어그로 추가
|
||||
AddAggro(attackerId, damage * damageAggroMultiplier);
|
||||
|
||||
// 즉시 타겟 전환 고려
|
||||
ConsiderTargetSwitch(attackerId);
|
||||
}
|
||||
|
||||
private void AddAggro(ulong attackerId, float amount)
|
||||
{
|
||||
if (_aggroTable.ContainsKey(attackerId))
|
||||
{
|
||||
_aggroTable[attackerId] += amount;
|
||||
}
|
||||
else
|
||||
{
|
||||
_aggroTable[attackerId] = amount;
|
||||
}
|
||||
}
|
||||
|
||||
private void DecayAggro()
|
||||
{
|
||||
List<ulong> keysToRemove = new List<ulong>();
|
||||
|
||||
foreach (var kvp in _aggroTable)
|
||||
{
|
||||
float newAggro = kvp.Value - aggroDecayRate * Time.deltaTime;
|
||||
if (newAggro <= 0)
|
||||
{
|
||||
keysToRemove.Add(kvp.Key);
|
||||
}
|
||||
else
|
||||
{
|
||||
_aggroTable[kvp.Key] = newAggro;
|
||||
}
|
||||
}
|
||||
|
||||
// 어그로가 0 이하인 항목 제거
|
||||
foreach (var key in keysToRemove)
|
||||
{
|
||||
_aggroTable.Remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
private void ConsiderTargetSwitch(ulong attackerId)
|
||||
{
|
||||
// 현재 ChasePlayer 또는 Attack 상태가 아니면 무시
|
||||
if (_currentState.Value != EnemyAIState.ChasePlayer &&
|
||||
_currentState.Value != EnemyAIState.Attack)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 현재 타겟의 어그로 확인
|
||||
ulong currentTargetId = _targetPlayerId.Value;
|
||||
float currentAggro = 0f;
|
||||
if (currentTargetId != 0 && _aggroTable.TryGetValue(currentTargetId, out float currAgg))
|
||||
{
|
||||
currentAggro = currAgg;
|
||||
}
|
||||
|
||||
// 공격자의 어그로 확인
|
||||
if (!_aggroTable.TryGetValue(attackerId, out float attackerAggro))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 어그로 차이가 임계값 이상이면 타겟 전환
|
||||
if (attackerAggro >= currentAggro + aggroSwitchThreshold)
|
||||
{
|
||||
// 공격자를 NetworkObject로 찾기
|
||||
if (NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(attackerId, out NetworkObject attackerObj))
|
||||
{
|
||||
GameObject attacker = attackerObj.gameObject;
|
||||
|
||||
// 유효한 타겟인지 확인 (사망하지 않음, 거리 내에 있음)
|
||||
IDamageable damageable = attacker.GetComponentInParent<IDamageable>();
|
||||
if (damageable != null && !damageable.IsDead())
|
||||
{
|
||||
float distance = GetDistanceToTarget(attacker);
|
||||
if (distance <= maxChaseDistance)
|
||||
{
|
||||
SetTargetPlayer(attacker);
|
||||
|
||||
// 캠프에 알림
|
||||
if (creepCamp != null)
|
||||
{
|
||||
creepCamp.AlertAllCreeps(attacker, this);
|
||||
}
|
||||
|
||||
if (_currentState.Value != EnemyAIState.ChasePlayer)
|
||||
{
|
||||
TransitionToState(EnemyAIState.ChasePlayer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ReevaluateTarget()
|
||||
{
|
||||
if (!enableAggroSystem) return;
|
||||
if (Time.time - _lastTargetReevaluateTime < targetReevaluateInterval) return;
|
||||
_lastTargetReevaluateTime = Time.time;
|
||||
|
||||
// 어그로 감쇠
|
||||
DecayAggro();
|
||||
|
||||
// 현재 추적 중이 아니면 감지 로직에 맡김
|
||||
if (_currentState.Value != EnemyAIState.ChasePlayer &&
|
||||
_currentState.Value != EnemyAIState.Attack)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GameObject currentTarget = GetTargetPlayer();
|
||||
if (currentTarget == null) return;
|
||||
|
||||
// 최고 어그로 타겟 찾기
|
||||
ulong highestAggroId = 0;
|
||||
float highestAggro = 0f;
|
||||
|
||||
foreach (var kvp in _aggroTable)
|
||||
{
|
||||
if (kvp.Value > highestAggro)
|
||||
{
|
||||
// 유효한 타겟인지 확인
|
||||
if (NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(kvp.Key, out NetworkObject netObj))
|
||||
{
|
||||
IDamageable damageable = netObj.GetComponentInParent<IDamageable>();
|
||||
if (damageable != null && !damageable.IsDead())
|
||||
{
|
||||
float distance = GetDistanceToTarget(netObj.gameObject);
|
||||
if (distance <= maxChaseDistance)
|
||||
{
|
||||
highestAggroId = kvp.Key;
|
||||
highestAggro = kvp.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 현재 타겟이 최고 어그로가 아니면 전환 고려
|
||||
ulong currentTargetNetId = 0;
|
||||
var currentNetObj = currentTarget.GetComponentInParent<NetworkObject>();
|
||||
if (currentNetObj != null)
|
||||
{
|
||||
currentTargetNetId = currentNetObj.NetworkObjectId;
|
||||
}
|
||||
|
||||
if (highestAggroId != 0 && highestAggroId != currentTargetNetId)
|
||||
{
|
||||
float currentAggro = 0f;
|
||||
_aggroTable.TryGetValue(currentTargetNetId, out currentAggro);
|
||||
|
||||
if (highestAggro >= currentAggro + aggroSwitchThreshold)
|
||||
{
|
||||
if (NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(highestAggroId, out NetworkObject newTargetObj))
|
||||
{
|
||||
SetTargetPlayer(newTargetObj.gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 거리 기반 타겟 전환 (어그로가 비슷하면 더 가까운 타겟으로)
|
||||
ConsiderDistanceBasedSwitch();
|
||||
}
|
||||
|
||||
private void ConsiderDistanceBasedSwitch()
|
||||
{
|
||||
GameObject currentTarget = GetTargetPlayer();
|
||||
if (currentTarget == null) return;
|
||||
|
||||
float currentDistance = GetDistanceToTarget(currentTarget);
|
||||
|
||||
// 주변 플레이어 검색
|
||||
Collider[] colliders = Physics.OverlapSphere(transform.position, detectionRange, playerLayer);
|
||||
|
||||
foreach (Collider col in colliders)
|
||||
{
|
||||
if (col.transform.root == transform.root) continue;
|
||||
if (col.gameObject == currentTarget) continue;
|
||||
|
||||
IDamageable damageable = col.GetComponentInParent<IDamageable>();
|
||||
if (damageable == null || damageable.IsDead()) continue;
|
||||
|
||||
ITeamMember teamMember = col.GetComponentInParent<ITeamMember>();
|
||||
if (teamMember != null && teamMember.GetTeam() != TeamType.Player) continue;
|
||||
|
||||
float newDistance = GetDistanceToTarget(col.gameObject);
|
||||
|
||||
// 현재 타겟보다 충분히 가까우면 전환 고려
|
||||
if (currentDistance - newDistance >= distanceSwitchThreshold)
|
||||
{
|
||||
// 현재 타겟의 어그로 가져오기
|
||||
ulong currentTargetId = _targetPlayerId.Value;
|
||||
float currentAggro = 0f;
|
||||
if (currentTargetId != 0)
|
||||
{
|
||||
_aggroTable.TryGetValue(currentTargetId, out currentAggro);
|
||||
}
|
||||
|
||||
// 새 타겟이 어그로가 비슷하거나 높으면 전환
|
||||
var newNetObj = col.GetComponentInParent<NetworkObject>();
|
||||
if (newNetObj != null)
|
||||
{
|
||||
float newAggro = 0f;
|
||||
_aggroTable.TryGetValue(newNetObj.NetworkObjectId, out newAggro);
|
||||
|
||||
// 거리 우선: 어그로가 크게 차이나지 않으면 더 가까운 타겟으로
|
||||
if (newAggro >= currentAggro - aggroSwitchThreshold * 0.5f)
|
||||
{
|
||||
SetTargetPlayer(col.gameObject);
|
||||
|
||||
if (creepCamp != null)
|
||||
{
|
||||
creepCamp.AlertAllCreeps(col.gameObject, this);
|
||||
}
|
||||
|
||||
return; // 한 번에 하나만 전환
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Utilities & Distance
|
||||
|
||||
private float GetDistanceToTarget(GameObject target)
|
||||
@@ -818,7 +1109,19 @@ namespace Northbound
|
||||
distanceInfo = $"\nDistToCore: {distToCore:F1} (Range: {attackRange})";
|
||||
}
|
||||
|
||||
string fullInfo = $"{gameObject.name}\n{stateInfo}\n{targetInfo}{attackInfo}{distanceInfo}";
|
||||
// 어그로 정보 표시
|
||||
string aggroInfo = "";
|
||||
if (enableAggroSystem && _aggroTable.Count > 0)
|
||||
{
|
||||
float currentAggro = 0f;
|
||||
if (_targetPlayerId.Value != 0 && _aggroTable.TryGetValue(_targetPlayerId.Value, out float agg))
|
||||
{
|
||||
currentAggro = agg;
|
||||
}
|
||||
aggroInfo = $"\nAggro: {currentAggro:F0} (Targets: {_aggroTable.Count})";
|
||||
}
|
||||
|
||||
string fullInfo = $"{gameObject.name}\n{stateInfo}\n{targetInfo}{attackInfo}{distanceInfo}{aggroInfo}";
|
||||
|
||||
UnityEditor.Handles.Label(
|
||||
transform.position + Vector3.up * 3f,
|
||||
|
||||
@@ -41,6 +41,11 @@ namespace Northbound
|
||||
/// </summary>
|
||||
public event System.Action<ulong> OnDeath;
|
||||
|
||||
/// <summary>
|
||||
/// 데미지를 받았을 때 발생하는 이벤트 (매개변수: attackerId, damage)
|
||||
/// </summary>
|
||||
public event System.Action<ulong, int> OnDamageTaken;
|
||||
|
||||
private UnitHealthBar _healthBar;
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
@@ -120,6 +125,9 @@ namespace Northbound
|
||||
// 데미지 이펙트
|
||||
ShowDamageEffectClientRpc();
|
||||
|
||||
// 데미지 받음 이벤트 발생 (AI 어그로 시스템용)
|
||||
OnDamageTaken?.Invoke(attackerId, actualDamage);
|
||||
|
||||
// 체력이 0이 되면 파괴
|
||||
if (_currentHealth.Value <= 0)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user