액션 및 인터랙션 시 장비를 착용할 수 있도록 함. 코드 개선 추가

This commit is contained in:
2026-01-28 16:08:12 +09:00
parent 42f5462b54
commit 2539b0f4ba
22 changed files with 323 additions and 206 deletions

View File

@@ -4,7 +4,7 @@ using UnityEngine;
namespace Northbound
{
/// <summary>
/// 액션 - 공격 (팀 시스템 적용)
/// 액션 - 공격 (팀 시스템 + 장비 시스템 적용)
/// </summary>
public class AttackAction : NetworkBehaviour, IAction
{
@@ -16,6 +16,12 @@ namespace Northbound
[Header("Animation")]
public string attackAnimationTrigger = "Attack";
public bool useAnimationEvents = true;
public bool blockDuringAnimation = true;
[Header("Equipment")]
public bool useEquipment = true;
public EquipmentData equipmentData;
[Header("Visual")]
public GameObject attackEffectPrefab;
@@ -24,15 +30,22 @@ namespace Northbound
private float _lastAttackTime;
private Animator _animator;
private ITeamMember _teamMember;
private EquipmentSocket _equipmentSocket;
private bool _isAttacking = false;
private bool _isWeaponEquipped = false;
private void Awake()
{
_animator = GetComponent<Animator>();
_teamMember = GetComponent<ITeamMember>();
_equipmentSocket = GetComponent<EquipmentSocket>();
}
public bool CanExecute(ulong playerId)
{
if (blockDuringAnimation && _isAttacking)
return false;
return Time.time - _lastAttackTime >= attackCooldown;
}
@@ -42,27 +55,43 @@ namespace Northbound
return;
_lastAttackTime = Time.time;
_isAttacking = true;
// 장비 장착 (애니메이션 이벤트 사용 안 할 경우)
if (!useAnimationEvents && useEquipment && !_isWeaponEquipped)
{
if (equipmentData != null && equipmentData.attachOnStart)
{
AttachWeapon();
}
}
// 애니메이션 재생
PlayAttackAnimation();
// 범위 내 적 검색
// 애니메이션이 없으면 즉시 공격 실행
if (_animator == null || string.IsNullOrEmpty(attackAnimationTrigger))
{
PerformAttack();
_isAttacking = false;
}
}
private void PerformAttack()
{
Vector3 attackOrigin = attackPoint != null ? attackPoint.position : transform.position;
Collider[] hits = Physics.OverlapSphere(attackOrigin, attackRange, attackableLayer);
foreach (Collider hit in hits)
{
// 자기 자신은 제외
if (hit.transform.root == transform.root)
continue;
// 대상 확인
var targetDamageable = hit.GetComponent<IDamageable>();
var targetTeamMember = hit.GetComponent<ITeamMember>();
if (targetDamageable != null)
{
// 팀 확인 - 적대 관계인 경우에만 공격
if (_teamMember != null && targetTeamMember != null)
{
if (!TeamManager.CanAttack(_teamMember, targetTeamMember))
@@ -92,13 +121,11 @@ namespace Northbound
private void PlayAttackAnimation()
{
// 애니메이션 트리거
if (_animator != null && !string.IsNullOrEmpty(attackAnimationTrigger))
{
_animator.SetTrigger(attackAnimationTrigger);
}
// 이펙트 생성
if (attackEffectPrefab != null && attackPoint != null)
{
GameObject effect = Instantiate(attackEffectPrefab, attackPoint.position, attackPoint.rotation);
@@ -106,6 +133,108 @@ namespace Northbound
}
}
// ========================================
// Animation Event 함수들
// ========================================
public void OnEquipWeapon()
{
if (!useAnimationEvents || !useEquipment) return;
AttachWeapon();
}
public void OnEquipWeapon(string socketName)
{
if (!useAnimationEvents || !useEquipment) return;
AttachWeapon(socketName);
}
public void OnUnequipWeapon()
{
if (!useAnimationEvents || !useEquipment) return;
DetachWeapon();
}
public void OnUnequipWeapon(string socketName)
{
if (!useAnimationEvents || !useEquipment) return;
DetachWeapon(socketName);
}
public void OnAttackHit()
{
PerformAttack();
}
public void OnAttackComplete()
{
_isAttacking = false;
if (useEquipment && equipmentData != null && equipmentData.detachOnEnd && !equipmentData.keepEquipped)
{
DetachWeapon();
}
Debug.Log("[AttackAction] 공격 완료");
}
// ========================================
// 장비 관리 함수들
// ========================================
private void AttachWeapon(string socketName = null)
{
if (_equipmentSocket == null || equipmentData == null)
{
Debug.LogWarning("[AttackAction] EquipmentSocket 또는 EquipmentData가 없습니다.");
return;
}
if (equipmentData.equipmentPrefab == null)
{
Debug.LogWarning("[AttackAction] 무기 프리팹이 설정되지 않았습니다.");
return;
}
string socket = socketName ?? equipmentData.socketName;
_equipmentSocket.AttachToSocket(socket, equipmentData.equipmentPrefab);
_isWeaponEquipped = true;
Debug.Log($"[AttackAction] 무기 장착: {socket}");
}
private void DetachWeapon(string socketName = null)
{
if (_equipmentSocket == null)
return;
string socket = socketName ?? equipmentData?.socketName;
if (!string.IsNullOrEmpty(socket))
{
_equipmentSocket.DetachFromSocket(socket);
_isWeaponEquipped = false;
Debug.Log($"[AttackAction] 무기 해제: {socket}");
}
}
public void EquipWeapon()
{
if (!_isWeaponEquipped && useEquipment)
{
AttachWeapon();
}
}
public void UnequipWeapon()
{
if (_isWeaponEquipped)
{
DetachWeapon();
}
}
public string GetActionName()
{
return "Attack";
@@ -116,11 +245,29 @@ namespace Northbound
return attackAnimationTrigger;
}
public EquipmentData GetEquipmentData()
{
return equipmentData;
}
private void OnDrawGizmosSelected()
{
Vector3 attackOrigin = attackPoint != null ? attackPoint.position : transform.position;
Gizmos.color = Color.red;
Gizmos.DrawWireSphere(attackOrigin, attackRange);
}
public override void OnDestroy()
{
// 무기 정리
if (_isWeaponEquipped)
{
DetachWeapon();
}
base.OnDestroy();
}
public bool IsAttacking => _isAttacking;
}
}

View File

@@ -35,7 +35,7 @@ namespace Northbound
[Tooltip("건설 시 플레이어가 재생할 애니메이션 트리거 (예: Build, Hammer, Construct)")]
public string constructionAnimationTrigger = "Build";
[Tooltip("건설 시 사용할 도구 (선택사항)")]
public InteractionEquipmentData constructionEquipment;
public EquipmentData constructionEquipment;
[Header("Health Settings")]
[Tooltip("Maximum health of the building")]

View File

@@ -178,7 +178,7 @@ namespace Northbound
return "[E] 건설하기";
float percentage = (_currentProgress.Value / buildingData.requiredWorkAmount) * 100f;
return $"[E] 건설하기 ({percentage:F0}%)";
return $"[E] {buildingData.buildingName} 건설 ({percentage:F0}%)";
}
public string GetInteractionAnimation()
@@ -193,7 +193,7 @@ namespace Northbound
return "";
}
public InteractionEquipmentData GetEquipmentData()
public EquipmentData GetEquipmentData()
{
// BuildingData에 건설 도구가 정의되어 있으면 반환
if (buildingData != null && buildingData.constructionEquipment != null)
@@ -234,12 +234,15 @@ namespace Northbound
// 플레이어의 NetworkObject 찾기
if (NetworkManager.Singleton != null && NetworkManager.Singleton.SpawnManager != null)
{
if (NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(playerId, out NetworkObject playerNetObj))
if (NetworkManager.Singleton.ConnectedClients.TryGetValue(playerId, out var client))
{
var teamMember = playerNetObj.GetComponent<ITeamMember>();
if (teamMember != null)
if (client.PlayerObject != null)
{
return teamMember.GetTeam();
var teamMember = client.PlayerObject.GetComponent<ITeamMember>();
if (teamMember != null)
{
return teamMember.GetTeam();
}
}
}
}

View File

@@ -25,7 +25,7 @@ namespace Northbound
public string interactionAnimationTrigger = "Deposit"; // 플레이어 애니메이션 트리거
[Header("Equipment")]
public InteractionEquipmentData equipmentData = null; // 도구 필요 없음
public EquipmentData equipmentData = null; // 도구 필요 없음
[Header("Visual")]
public GameObject depositEffectPrefab;
@@ -230,11 +230,11 @@ namespace Northbound
{
if (unlimitedStorage)
{
return "자원 보관 (무제한)";
return "[E] 자원 보관 (무제한)";
}
else
{
return $"자원 보관 ({_totalResources.Value}/{maxStorageCapacity})";
return $"[E] 자원 보관 ({_totalResources.Value}/{maxStorageCapacity})";
}
}
@@ -243,7 +243,7 @@ namespace Northbound
return interactionAnimationTrigger;
}
public InteractionEquipmentData GetEquipmentData()
public EquipmentData GetEquipmentData()
{
return equipmentData;
}

View File

@@ -0,0 +1,33 @@
using UnityEngine;
namespace Northbound
{
/// <summary>
/// 액션(상호작용, 공격 등) 실행 시 필요한 장비 정보
/// </summary>
[System.Serializable]
public class EquipmentData
{
[Tooltip("장비를 부착할 소켓 이름 (예: RightHand, LeftHand, Back)")]
public string socketName = "RightHand";
[Tooltip("부착할 장비 프리팹 (예: 곡괭이, 도끼, 검, 활)")]
public GameObject equipmentPrefab;
[Tooltip("액션 시작 시 자동으로 부착")]
public bool attachOnStart = true;
[Tooltip("액션 종료 시 자동으로 제거")]
public bool detachOnEnd = true;
[Header("Advanced Settings")]
[Tooltip("장비를 지속적으로 장착 상태로 유지 (전투 모드 등)")]
public bool keepEquipped = false;
[Tooltip("장비 부착 시 딜레이 (초) - 애니메이션 타이밍 조정용")]
public float attachDelay = 0f;
[Tooltip("장비 제거 시 딜레이 (초) - 애니메이션 타이밍 조정용")]
public float detachDelay = 0f;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 165ad8862ed57f245b545005aa1c2c38

View File

@@ -1,7 +1,7 @@
namespace Northbound
{
/// <summary>
/// 상호작용 대상 없이도 실행 가능한 행동 (공격, 점프 등)
/// 상호작용 대상 없이도 실행 가능한 행동 (공격, 점프, 스킬 등)
/// </summary>
public interface IAction
{
@@ -16,7 +16,7 @@ namespace Northbound
void Execute(ulong playerId);
/// <summary>
/// 액션 이름
/// 액션 이름 (예: "Attack", "Jump", "Skill_Fireball")
/// </summary>
string GetActionName();
@@ -24,5 +24,10 @@ namespace Northbound
/// 플레이어가 재생할 애니메이션 트리거 이름 (없으면 null 또는 빈 문자열)
/// </summary>
string GetActionAnimation();
/// <summary>
/// 액션 실행 시 사용할 장비 정보 (없으면 null)
/// </summary>
EquipmentData GetEquipmentData();
}
}

View File

@@ -30,7 +30,7 @@ namespace Northbound
/// <summary>
/// 상호작용 시 사용할 장비 정보 (없으면 null)
/// </summary>
InteractionEquipmentData GetEquipmentData();
EquipmentData GetEquipmentData();
/// <summary>
/// 상호작용 오브젝트의 Transform

View File

@@ -1,23 +0,0 @@
using UnityEngine;
namespace Northbound
{
/// <summary>
/// 상호작용 시 필요한 장비 정보
/// </summary>
[System.Serializable]
public class InteractionEquipmentData
{
[Tooltip("장비를 부착할 소켓 이름 (예: RightHand, LeftHand)")]
public string socketName = "RightHand";
[Tooltip("부착할 장비 프리팹 (예: 곡괭이, 도끼)")]
public GameObject equipmentPrefab;
[Tooltip("상호작용 시작 시 자동으로 부착")]
public bool attachOnStart = true;
[Tooltip("상호작용 종료 시 자동으로 제거")]
public bool detachOnEnd = true;
}
}

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 883a3042cf05b3b4e9629710b6f4e83f

View File

@@ -101,6 +101,23 @@ public class NetworkPlayerController : NetworkBehaviour, ITeamMember, IDamageabl
// 죽었으면 이동 불가
if (_currentHealth.Value <= 0) return;
// 액션/상호작용 중이면 이동 불가
var attackAction = GetComponent<AttackAction>();
var playerInteraction = GetComponent<PlayerInteraction>();
bool isActionBlocked = (attackAction != null && attackAction.IsAttacking) ||
(playerInteraction != null && playerInteraction.IsInteracting);
if (isActionBlocked)
{
// 이동 불가 시 애니메이션 속도를 0으로
if (_animator != null)
{
_animator.SetFloat("MoveSpeed", 0f);
}
return;
}
_moveInput = _inputActions.Player.Move.ReadValue<Vector2>();
Vector3 move = new Vector3(_moveInput.x, 0, _moveInput.y).normalized;

View File

@@ -34,10 +34,13 @@ namespace Northbound
private Animator _animator;
private EquipmentSocket _equipmentSocket;
private InteractionEquipmentData _pendingEquipmentData;
private EquipmentData _pendingEquipmentData;
private string _currentEquipmentSocket;
private bool _isInteracting = false;
// 다른 컴포넌트가 이동 차단 여부를 확인할 수 있도록 public 프로퍼티 제공
public bool IsInteracting => _isInteracting;
public override void OnNetworkSpawn()
{
if (!IsOwner) return;
@@ -243,14 +246,12 @@ namespace Northbound
GUI.Label(new Rect(Screen.width / 2 - 200, Screen.height - 100, 400, 50), prompt, style);
}
override public void OnDestroy()
public override void OnDestroy()
{
if (_inputActions != null)
{
_inputActions.Dispose();
}
base.OnDestroy();
}
}
}

View File

@@ -22,9 +22,10 @@ namespace Northbound
public string interactionAnimationTrigger = "Mining"; // 플레이어 애니메이션 트리거
[Header("Equipment")]
public InteractionEquipmentData equipmentData = new InteractionEquipmentData
public EquipmentData equipmentData = new EquipmentData
{
socketName = "RightHand",
equipmentPrefab = null, // Inspector에서 곡괭이 프리팹 할당
attachOnStart = true,
detachOnEnd = true
};
@@ -46,7 +47,7 @@ namespace Northbound
{
if (IsServer)
{
_currentResources.Value = 0;
_currentResources.Value = maxResources;
_lastRechargeTime = Time.time;
}
}
@@ -63,10 +64,10 @@ namespace Northbound
{
int rechargeAmountToAdd = Mathf.Min(rechargeAmount, maxResources - _currentResources.Value);
_currentResources.Value += rechargeAmountToAdd;
// Debug.Log($"{resourceName} {rechargeAmountToAdd} 충전됨. 현재: {_currentResources.Value}/{maxResources}");
}
_lastRechargeTime = Time.time;
}
}
@@ -82,7 +83,7 @@ namespace Northbound
return false;
// 플레이어 인벤토리 확인
if (NetworkManager.Singleton != null &&
if (NetworkManager.Singleton != null &&
NetworkManager.Singleton.ConnectedClients.TryGetValue(playerId, out var client))
{
if (client.PlayerObject != null)
@@ -128,7 +129,7 @@ namespace Northbound
// 플레이어가 받을 수 있는 최대량 계산
int playerAvailableSpace = playerInventory.GetAvailableSpace();
// 자원 노드가 줄 수 있는 양과 플레이어가 받을 수 있는 양 중 작은 값 선택
int gatheredAmount = Mathf.Min(
resourcesPerGathering,
@@ -168,7 +169,7 @@ namespace Northbound
{
if (_currentResources.Value <= 0)
return "자원 충전 중...";
return $"[E] {resourceName} 채집 ({_currentResources.Value}/{maxResources})";
}
@@ -177,7 +178,7 @@ namespace Northbound
return interactionAnimationTrigger;
}
public InteractionEquipmentData GetEquipmentData()
public EquipmentData GetEquipmentData()
{
return equipmentData;
}

View File

@@ -16,9 +16,10 @@ namespace Northbound
public string interactionAnimationTrigger = "PickUp"; // 플레이어 애니메이션 트리거
[Header("Equipment")]
public InteractionEquipmentData equipmentData = new InteractionEquipmentData
public EquipmentData equipmentData = new EquipmentData
{
socketName = "",
equipmentPrefab = null,
attachOnStart = false,
detachOnEnd = false
};
@@ -143,7 +144,7 @@ namespace Northbound
return interactionAnimationTrigger;
}
public InteractionEquipmentData GetEquipmentData()
public EquipmentData GetEquipmentData()
{
return equipmentData;
}

View File

@@ -242,7 +242,7 @@ namespace Northbound
/// <summary>
/// 텔레포트 이펙트 재생 (모든 클라이언트)
/// </summary>
[ClientRpc]
[Rpc(SendTo.ClientsAndHost)]
private void PlayTeleportEffectClientRpc(Vector3 position)
{
if (teleportEffectPrefab == null) return;
@@ -254,7 +254,7 @@ namespace Northbound
/// <summary>
/// 사운드 재생 (모든 클라이언트)
/// </summary>
[ClientRpc]
[Rpc(SendTo.ClientsAndHost)]
private void PlaySoundClientRpc(bool isTeleport)
{
if (_audioSource == null) return;
@@ -269,7 +269,7 @@ namespace Northbound
/// <summary>
/// 허용 팀 변경 (서버에서 호출)
/// </summary>
[ServerRpc(RequireOwnership = false)]
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
public void ChangeAllowedTeamServerRpc(TeamType newTeam)
{
allowedTeam = newTeam;
@@ -281,7 +281,7 @@ namespace Northbound
/// <summary>
/// 비주얼 업데이트 (모든 클라이언트)
/// </summary>
[ClientRpc]
[Rpc(SendTo.ClientsAndHost)]
private void UpdateVisualClientRpc(TeamType team)
{
allowedTeam = team;
@@ -376,11 +376,13 @@ namespace Northbound
}
}
private void OnDestroy()
public override void OnDestroy()
{
// 정리
_lastTeleportTime.Clear();
_triggerStates.Clear();
base.OnDestroy();
}
}
}