Compare commits
2 Commits
17457b2e7e
...
5bb8cd533e
| Author | SHA1 | Date | |
|---|---|---|---|
| 5bb8cd533e | |||
| 047c115f95 |
@@ -80,8 +80,8 @@ AnimatorController:
|
|||||||
m_DefaultInt: 0
|
m_DefaultInt: 0
|
||||||
m_DefaultBool: 0
|
m_DefaultBool: 0
|
||||||
m_Controller: {fileID: 9100000}
|
m_Controller: {fileID: 9100000}
|
||||||
- m_Name: bIsDeath
|
- m_Name: Die
|
||||||
m_Type: 4
|
m_Type: 9
|
||||||
m_DefaultFloat: 0
|
m_DefaultFloat: 0
|
||||||
m_DefaultInt: 0
|
m_DefaultInt: 0
|
||||||
m_DefaultBool: 0
|
m_DefaultBool: 0
|
||||||
@@ -242,7 +242,7 @@ AnimatorStateTransition:
|
|||||||
m_Name:
|
m_Name:
|
||||||
m_Conditions:
|
m_Conditions:
|
||||||
- m_ConditionMode: 1
|
- m_ConditionMode: 1
|
||||||
m_ConditionEvent: bIsDeath
|
m_ConditionEvent: Die
|
||||||
m_EventTreshold: 0
|
m_EventTreshold: 0
|
||||||
m_DstStateMachine: {fileID: 0}
|
m_DstStateMachine: {fileID: 0}
|
||||||
m_DstState: {fileID: 3895323774234557799}
|
m_DstState: {fileID: 3895323774234557799}
|
||||||
|
|||||||
@@ -93,6 +93,8 @@ namespace Northbound
|
|||||||
int actualDamage = Mathf.Min(damage, _currentHealth.Value);
|
int actualDamage = Mathf.Min(damage, _currentHealth.Value);
|
||||||
_currentHealth.Value -= actualDamage;
|
_currentHealth.Value -= actualDamage;
|
||||||
|
|
||||||
|
Debug.Log($"<color=red>[Core] 코어가 {actualDamage} 데미지를 받았습니다. 남은 체력: {_currentHealth.Value}/{maxHealth}</color>");
|
||||||
|
|
||||||
// 데미지 이펙트
|
// 데미지 이펙트
|
||||||
ShowDamageEffectClientRpc();
|
ShowDamageEffectClientRpc();
|
||||||
|
|
||||||
|
|||||||
@@ -119,9 +119,24 @@ namespace Northbound
|
|||||||
{
|
{
|
||||||
TransitionToState(EnemyAIState.Idle);
|
TransitionToState(EnemyAIState.Idle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 사망 이벤트 구독
|
||||||
|
if (_enemyUnit != null)
|
||||||
|
{
|
||||||
|
_enemyUnit.OnDeath += HandleDeath;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override void OnNetworkDespawn()
|
||||||
|
{
|
||||||
|
if (_enemyUnit != null)
|
||||||
|
{
|
||||||
|
_enemyUnit.OnDeath -= HandleDeath;
|
||||||
|
}
|
||||||
|
base.OnNetworkDespawn();
|
||||||
|
}
|
||||||
|
|
||||||
private void Update()
|
private void Update()
|
||||||
{
|
{
|
||||||
if (!IsServer) return;
|
if (!IsServer) return;
|
||||||
@@ -134,6 +149,7 @@ namespace Northbound
|
|||||||
case EnemyAIState.ChasePlayer: UpdateChasePlayer(); break;
|
case EnemyAIState.ChasePlayer: UpdateChasePlayer(); break;
|
||||||
case EnemyAIState.Attack: UpdateAttack(); break;
|
case EnemyAIState.Attack: UpdateAttack(); break;
|
||||||
case EnemyAIState.ReturnToOrigin: UpdateReturnToOrigin(); break;
|
case EnemyAIState.ReturnToOrigin: UpdateReturnToOrigin(); break;
|
||||||
|
case EnemyAIState.Dead: break; // 사망 상태에서는 아무것도 하지 않음
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -471,11 +487,28 @@ namespace Northbound
|
|||||||
if (state == EnemyAIState.ChasePlayer) _chaseStartPosition = transform.position;
|
if (state == EnemyAIState.ChasePlayer) _chaseStartPosition = transform.position;
|
||||||
if (state == EnemyAIState.ReturnToOrigin) _agent.SetDestination(_originPosition);
|
if (state == EnemyAIState.ReturnToOrigin) _agent.SetDestination(_originPosition);
|
||||||
break;
|
break;
|
||||||
|
case EnemyAIState.Dead:
|
||||||
|
_agent.isStopped = true;
|
||||||
|
_agent.ResetPath();
|
||||||
|
_agent.enabled = false; // NavMeshAgent 비활성화
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnExitState(EnemyAIState state) { }
|
private void OnExitState(EnemyAIState state) { }
|
||||||
|
|
||||||
|
private void HandleDeath(ulong killerId)
|
||||||
|
{
|
||||||
|
if (!IsServer) return;
|
||||||
|
|
||||||
|
// 사망 상태로 전환
|
||||||
|
TransitionToState(EnemyAIState.Dead);
|
||||||
|
ClearTargetPlayer();
|
||||||
|
|
||||||
|
if (showDebugInfo)
|
||||||
|
Debug.Log($"<color=red>[EnemyAI] {gameObject.name}이(가) 사망했습니다. (killer: {killerId})</color>");
|
||||||
|
}
|
||||||
|
|
||||||
private void OnLostTarget()
|
private void OnLostTarget()
|
||||||
{
|
{
|
||||||
ClearTargetPlayer();
|
ClearTargetPlayer();
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ namespace Northbound
|
|||||||
MoveToCore, // 코어로 이동 (몬스터 기본 상태)
|
MoveToCore, // 코어로 이동 (몬스터 기본 상태)
|
||||||
ChasePlayer, // 플레이어 추적
|
ChasePlayer, // 플레이어 추적
|
||||||
Attack, // 공격
|
Attack, // 공격
|
||||||
ReturnToOrigin // 원래 위치로 복귀 (적대 세력)
|
ReturnToOrigin, // 원래 위치로 복귀 (적대 세력)
|
||||||
|
Dead // 사망 (아무것도 하지 않음)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,8 +4,32 @@ using System.Collections.Generic;
|
|||||||
using Unity.Netcode;
|
using Unity.Netcode;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
public class EnemyPortal : NetworkBehaviour
|
public class EnemyPortal : NetworkBehaviour, IDamageable, ITeamMember
|
||||||
{
|
{
|
||||||
|
[Header("Team Settings")]
|
||||||
|
[Tooltip("포털의 팀 (Hostile 또는 Monster)")]
|
||||||
|
[SerializeField] private TeamType portalTeam = TeamType.Hostile;
|
||||||
|
|
||||||
|
[Header("Health Settings")]
|
||||||
|
[Tooltip("최대 체력")]
|
||||||
|
[SerializeField] private int maxHealth = 500;
|
||||||
|
|
||||||
|
[Header("Visual Effects")]
|
||||||
|
[SerializeField] private GameObject damageEffectPrefab;
|
||||||
|
[SerializeField] private GameObject destroyEffectPrefab;
|
||||||
|
[SerializeField] private Transform effectSpawnPoint;
|
||||||
|
|
||||||
|
private NetworkVariable<int> _currentHealth = new NetworkVariable<int>(
|
||||||
|
0,
|
||||||
|
NetworkVariableReadPermission.Everyone,
|
||||||
|
NetworkVariableWritePermission.Server
|
||||||
|
);
|
||||||
|
|
||||||
|
private NetworkVariable<TeamType> _team = new NetworkVariable<TeamType>(
|
||||||
|
TeamType.Neutral,
|
||||||
|
NetworkVariableReadPermission.Everyone,
|
||||||
|
NetworkVariableWritePermission.Server
|
||||||
|
);
|
||||||
[System.Serializable]
|
[System.Serializable]
|
||||||
public class MonsterEntry
|
public class MonsterEntry
|
||||||
{
|
{
|
||||||
@@ -27,12 +51,33 @@ using UnityEngine;
|
|||||||
public override void OnNetworkSpawn()
|
public override void OnNetworkSpawn()
|
||||||
{
|
{
|
||||||
base.OnNetworkSpawn();
|
base.OnNetworkSpawn();
|
||||||
|
|
||||||
|
if (IsServer)
|
||||||
|
{
|
||||||
|
// 체력 초기화
|
||||||
|
if (_currentHealth.Value == 0)
|
||||||
|
{
|
||||||
|
_currentHealth.Value = maxHealth;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 팀 초기화
|
||||||
|
if (_team.Value == TeamType.Neutral)
|
||||||
|
{
|
||||||
|
_team.Value = portalTeam;
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.Log($"<color=cyan>[EnemyPortal] 포털 스폰됨 (팀: {TeamManager.GetTeamName(_team.Value)}, 체력: {_currentHealth.Value}/{maxHealth})</color>");
|
||||||
|
}
|
||||||
|
|
||||||
GlobalTimer.Instance.OnCycleStart += OnCycleStart;
|
GlobalTimer.Instance.OnCycleStart += OnCycleStart;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void OnNetworkDespawn()
|
public override void OnNetworkDespawn()
|
||||||
{
|
{
|
||||||
GlobalTimer.Instance.OnCycleStart -= OnCycleStart;
|
GlobalTimer.Instance.OnCycleStart -= OnCycleStart;
|
||||||
|
|
||||||
|
// 체력 변경 이벤트 정리 (나중에 추가할 경우 대비)
|
||||||
|
|
||||||
base.OnNetworkDespawn();
|
base.OnNetworkDespawn();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,11 +198,113 @@ using UnityEngine;
|
|||||||
visibility.updateInterval = 0.2f;
|
visibility.updateInterval = 0.2f;
|
||||||
}
|
}
|
||||||
|
|
||||||
enemy.GetComponent<NetworkObject>().SpawnWithOwnership(NetworkManager.Singleton.LocalClientId);
|
var netObj = enemy.GetComponent<NetworkObject>();
|
||||||
|
netObj.Spawn(true);
|
||||||
|
|
||||||
|
Debug.Log($"<color=cyan>[EnemyPortal] {enemy.name} 스폰됨 - OwnerClientId: {netObj.OwnerClientId}, IsServer: {IsServer}</color>");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void IncreaseCost()
|
private void IncreaseCost()
|
||||||
{
|
{
|
||||||
currentCost *= (1f + costIncreaseRate / 100f);
|
currentCost *= (1f + costIncreaseRate / 100f);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region ITeamMember Implementation
|
||||||
|
|
||||||
|
public TeamType GetTeam() => _team.Value;
|
||||||
|
|
||||||
|
public void SetTeam(TeamType team)
|
||||||
|
{
|
||||||
|
if (!IsServer) return;
|
||||||
|
_team.Value = team;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region IDamageable Implementation
|
||||||
|
|
||||||
|
public void TakeDamage(int damage, ulong attackerId)
|
||||||
|
{
|
||||||
|
if (!IsServer) return;
|
||||||
|
|
||||||
|
// 이미 파괴됨
|
||||||
|
if (_currentHealth.Value <= 0) return;
|
||||||
|
|
||||||
|
// 공격자의 팀 확인
|
||||||
|
if (NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(attackerId, out NetworkObject attackerObj))
|
||||||
|
{
|
||||||
|
var attackerTeamMember = attackerObj.GetComponent<ITeamMember>();
|
||||||
|
if (attackerTeamMember != null)
|
||||||
|
{
|
||||||
|
if (!TeamManager.CanAttack(attackerTeamMember, this))
|
||||||
|
{
|
||||||
|
Debug.Log($"<color=yellow>[EnemyPortal] {TeamManager.GetTeamName(attackerTeamMember.GetTeam())} 팀은 {TeamManager.GetTeamName(_team.Value)} 팀 포털을 공격할 수 없습니다.</color>");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 데미지 적용
|
||||||
|
int actualDamage = Mathf.Min(damage, _currentHealth.Value);
|
||||||
|
_currentHealth.Value -= actualDamage;
|
||||||
|
|
||||||
|
Debug.Log($"<color=red>[EnemyPortal] 포털이 {actualDamage} 데미지를 받았습니다. 남은 체력: {_currentHealth.Value}/{maxHealth}</color>");
|
||||||
|
|
||||||
|
// 데미지 이펙트
|
||||||
|
ShowDamageEffectClientRpc();
|
||||||
|
|
||||||
|
// 체력이 0이 되면 파괴
|
||||||
|
if (_currentHealth.Value <= 0)
|
||||||
|
{
|
||||||
|
DestroyPortal(attackerId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DestroyPortal(ulong attackerId)
|
||||||
|
{
|
||||||
|
if (!IsServer) return;
|
||||||
|
|
||||||
|
Debug.Log($"<color=red>[EnemyPortal] 포털이 파괴되었습니다! (공격자: {attackerId})</color>");
|
||||||
|
|
||||||
|
// 파괴 이펙트
|
||||||
|
ShowDestroyEffectClientRpc();
|
||||||
|
|
||||||
|
// 몬스터 스폰 중지 (이벤트 구독 해제)
|
||||||
|
GlobalTimer.Instance.OnCycleStart -= OnCycleStart;
|
||||||
|
|
||||||
|
// 네트워크 오브젝트 파괴 (약간의 딜레이)
|
||||||
|
Invoke(nameof(DespawnPortal), 1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DespawnPortal()
|
||||||
|
{
|
||||||
|
if (IsServer && NetworkObject != null)
|
||||||
|
{
|
||||||
|
NetworkObject.Despawn(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[ClientRpc]
|
||||||
|
private void ShowDamageEffectClientRpc()
|
||||||
|
{
|
||||||
|
if (damageEffectPrefab != null)
|
||||||
|
{
|
||||||
|
Transform spawnPoint = effectSpawnPoint != null ? effectSpawnPoint : transform;
|
||||||
|
GameObject effect = Instantiate(damageEffectPrefab, spawnPoint.position, spawnPoint.rotation);
|
||||||
|
Destroy(effect, 2f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[ClientRpc]
|
||||||
|
private void ShowDestroyEffectClientRpc()
|
||||||
|
{
|
||||||
|
if (destroyEffectPrefab != null)
|
||||||
|
{
|
||||||
|
Transform spawnPoint = effectSpawnPoint != null ? effectSpawnPoint : transform;
|
||||||
|
GameObject effect = Instantiate(destroyEffectPrefab, spawnPoint.position, spawnPoint.rotation);
|
||||||
|
Destroy(effect, 3f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ namespace Northbound
|
|||||||
NetworkVariableWritePermission.Server
|
NetworkVariableWritePermission.Server
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 사망 시 발생하는 이벤트 (매개변수: killerId)
|
||||||
|
/// </summary>
|
||||||
|
public event System.Action<ulong> OnDeath;
|
||||||
|
|
||||||
public override void OnNetworkSpawn()
|
public override void OnNetworkSpawn()
|
||||||
{
|
{
|
||||||
base.OnNetworkSpawn();
|
base.OnNetworkSpawn();
|
||||||
@@ -71,6 +76,8 @@ namespace Northbound
|
|||||||
int actualDamage = Mathf.Min(damage, _currentHealth.Value);
|
int actualDamage = Mathf.Min(damage, _currentHealth.Value);
|
||||||
_currentHealth.Value -= actualDamage;
|
_currentHealth.Value -= actualDamage;
|
||||||
|
|
||||||
|
Debug.Log($"<color=red>[EnemyUnit] 적 유닛이 {actualDamage} 데미지를 받았습니다. 남은 체력: {_currentHealth.Value}/{maxHealth}</color>");
|
||||||
|
|
||||||
// 데미지 이펙트
|
// 데미지 이펙트
|
||||||
ShowDamageEffectClientRpc();
|
ShowDamageEffectClientRpc();
|
||||||
|
|
||||||
@@ -85,11 +92,14 @@ namespace Northbound
|
|||||||
{
|
{
|
||||||
if (!IsServer) return;
|
if (!IsServer) return;
|
||||||
|
|
||||||
|
// 사망 이벤트 발생 (애니메이션 등)
|
||||||
|
OnDeath?.Invoke(attackerId);
|
||||||
|
|
||||||
// 파괴 이펙트
|
// 파괴 이펙트
|
||||||
ShowDestroyEffectClientRpc();
|
ShowDestroyEffectClientRpc();
|
||||||
|
|
||||||
// 네트워크 오브젝트 파괴
|
// 네트워크 오브젝트 파괴
|
||||||
Invoke(nameof(DespawnUnit), 0.5f);
|
Invoke(nameof(DespawnUnit), 3.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DespawnUnit()
|
private void DespawnUnit()
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ namespace Northbound
|
|||||||
[Tooltip("IsMoving bool parameter name in Animator")]
|
[Tooltip("IsMoving bool parameter name in Animator")]
|
||||||
public string isMovingParam = "IsMoving";
|
public string isMovingParam = "IsMoving";
|
||||||
|
|
||||||
|
[Tooltip("Death trigger parameter name in Animator")]
|
||||||
|
public string dieTriggerParam = "Die";
|
||||||
|
|
||||||
[Header("Settings")]
|
[Header("Settings")]
|
||||||
[Tooltip("Auto-load animator controller from MonsterData")]
|
[Tooltip("Auto-load animator controller from MonsterData")]
|
||||||
public bool autoLoadFromMonsterData = true;
|
public bool autoLoadFromMonsterData = true;
|
||||||
@@ -27,6 +30,7 @@ namespace Northbound
|
|||||||
|
|
||||||
private Animator _animator;
|
private Animator _animator;
|
||||||
private EnemyAIController _aiController;
|
private EnemyAIController _aiController;
|
||||||
|
private EnemyUnit _enemyUnit;
|
||||||
private NavMeshAgent _agent;
|
private NavMeshAgent _agent;
|
||||||
|
|
||||||
private NetworkVariable<float> _networkSpeed = new NetworkVariable<float>(
|
private NetworkVariable<float> _networkSpeed = new NetworkVariable<float>(
|
||||||
@@ -47,9 +51,14 @@ namespace Northbound
|
|||||||
|
|
||||||
_animator = GetComponent<Animator>();
|
_animator = GetComponent<Animator>();
|
||||||
_aiController = GetComponent<EnemyAIController>();
|
_aiController = GetComponent<EnemyAIController>();
|
||||||
|
_enemyUnit = GetComponent<EnemyUnit>();
|
||||||
_agent = GetComponent<NavMeshAgent>();
|
_agent = GetComponent<NavMeshAgent>();
|
||||||
|
|
||||||
_aiController.OnAttackPerformed += HandleAttackPerformed;
|
_aiController.OnAttackPerformed += HandleAttackPerformed;
|
||||||
|
if (_enemyUnit != null)
|
||||||
|
{
|
||||||
|
_enemyUnit.OnDeath += HandleDeath;
|
||||||
|
}
|
||||||
|
|
||||||
if (autoLoadFromMonsterData)
|
if (autoLoadFromMonsterData)
|
||||||
{
|
{
|
||||||
@@ -63,6 +72,10 @@ namespace Northbound
|
|||||||
{
|
{
|
||||||
_aiController.OnAttackPerformed -= HandleAttackPerformed;
|
_aiController.OnAttackPerformed -= HandleAttackPerformed;
|
||||||
}
|
}
|
||||||
|
if (_enemyUnit != null)
|
||||||
|
{
|
||||||
|
_enemyUnit.OnDeath -= HandleDeath;
|
||||||
|
}
|
||||||
base.OnNetworkDespawn();
|
base.OnNetworkDespawn();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,6 +87,14 @@ namespace Northbound
|
|||||||
Debug.Log($"[MonsterAnimationController] Triggered attack animation for {target.name}", this);
|
Debug.Log($"[MonsterAnimationController] Triggered attack animation for {target.name}", this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void HandleDeath(ulong killerId)
|
||||||
|
{
|
||||||
|
if (!IsServer) return;
|
||||||
|
TriggerDeathClientRpc();
|
||||||
|
if (debugLogging)
|
||||||
|
Debug.Log($"[MonsterAnimationController] Triggered death animation (killer: {killerId})", this);
|
||||||
|
}
|
||||||
|
|
||||||
private void LoadAnimatorController()
|
private void LoadAnimatorController()
|
||||||
{
|
{
|
||||||
var monsterDataComponent = GetComponent<MonsterDataComponent>();
|
var monsterDataComponent = GetComponent<MonsterDataComponent>();
|
||||||
@@ -111,6 +132,14 @@ namespace Northbound
|
|||||||
|
|
||||||
private void UpdateServerSide()
|
private void UpdateServerSide()
|
||||||
{
|
{
|
||||||
|
// 사망 상태면 이동 애니메이션 중지
|
||||||
|
if (_aiController != null && _aiController.GetCurrentState() == EnemyAIState.Dead)
|
||||||
|
{
|
||||||
|
_networkSpeed.Value = 0f;
|
||||||
|
_networkIsMoving.Value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (_agent == null) return;
|
if (_agent == null) return;
|
||||||
|
|
||||||
float currentSpeed = _agent.velocity.magnitude;
|
float currentSpeed = _agent.velocity.magnitude;
|
||||||
@@ -137,6 +166,15 @@ namespace Northbound
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Rpc(SendTo.ClientsAndHost)]
|
||||||
|
private void TriggerDeathClientRpc()
|
||||||
|
{
|
||||||
|
if (_animator != null)
|
||||||
|
{
|
||||||
|
_animator.SetTrigger(dieTriggerParam);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void ResetAttackTrigger()
|
public void ResetAttackTrigger()
|
||||||
{
|
{
|
||||||
if (_animator != null)
|
if (_animator != null)
|
||||||
|
|||||||
Reference in New Issue
Block a user