diff --git a/.gitignore b/.gitignore
index 79e8d45..7c62d28 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,3 +14,4 @@ Assets/_Recovery
Assets/_Recovery.meta
GameData/Backups
.claude/settings.local.json
+AGENTS.md
diff --git a/Assets/Prefabs/Player/Player.prefab b/Assets/Prefabs/Player/Player.prefab
index 08ccf4e..6048f45 100644
--- a/Assets/Prefabs/Player/Player.prefab
+++ b/Assets/Prefabs/Player/Player.prefab
@@ -59,7 +59,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3}
m_Name:
m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.NetworkObject
- GlobalObjectIdHash: 4211758632
+ GlobalObjectIdHash: 1360081626
InScenePlacedSourceGlobalObjectIdHash: 4211758632
DeferredDespawnTick: 0
Ownership: 1
@@ -714,6 +714,18 @@ PrefabInstance:
serializedVersion: 3
m_TransformParent: {fileID: 1710962577221812646}
m_Modifications:
+ - target: {fileID: 1325989084779099817, guid: 274613c415998a647a86a5e09950ce41, type: 3}
+ propertyPath: m_Pivot.x
+ value: 0.5
+ objectReference: {fileID: 0}
+ - target: {fileID: 1325989084779099817, guid: 274613c415998a647a86a5e09950ce41, type: 3}
+ propertyPath: m_Pivot.y
+ value: 0.5
+ objectReference: {fileID: 0}
+ - target: {fileID: 1325989084779099817, guid: 274613c415998a647a86a5e09950ce41, type: 3}
+ propertyPath: m_AnchorMax.x
+ value: 0
+ objectReference: {fileID: 0}
- target: {fileID: 1325989084779099817, guid: 274613c415998a647a86a5e09950ce41, type: 3}
propertyPath: m_AnchorMax.y
value: 0
@@ -724,7 +736,11 @@ PrefabInstance:
objectReference: {fileID: 0}
- target: {fileID: 1325989084779099817, guid: 274613c415998a647a86a5e09950ce41, type: 3}
propertyPath: m_SizeDelta.x
- value: 4
+ value: 500
+ objectReference: {fileID: 0}
+ - target: {fileID: 1325989084779099817, guid: 274613c415998a647a86a5e09950ce41, type: 3}
+ propertyPath: m_SizeDelta.y
+ value: 50
objectReference: {fileID: 0}
- target: {fileID: 1325989084779099817, guid: 274613c415998a647a86a5e09950ce41, type: 3}
propertyPath: m_AnchoredPosition.x
@@ -834,6 +850,10 @@ PrefabInstance:
propertyPath: m_Name
value: ModalPanel
objectReference: {fileID: 0}
+ - target: {fileID: 7839034454177399683, guid: 274613c415998a647a86a5e09950ce41, type: 3}
+ propertyPath: m_fontStyle
+ value: 1
+ objectReference: {fileID: 0}
m_RemovedComponents: []
m_RemovedGameObjects: []
m_AddedGameObjects: []
@@ -878,6 +898,8 @@ MonoBehaviour:
modalPanel: {fileID: 4694931302848755623}
keyText: {fileID: 4193449683656256336}
interactText: {fileID: 8795452566802714605}
+ unavailablePanel: {fileID: 4694931302848755623}
+ unavailableText: {fileID: 8795452566802714605}
defaultKey: E
--- !u!114 &8795452566802714605 stripped
MonoBehaviour:
diff --git a/Assets/Scripts/InteractableModal.cs b/Assets/Scripts/InteractableModal.cs
index de9374f..15a78ac 100644
--- a/Assets/Scripts/InteractableModal.cs
+++ b/Assets/Scripts/InteractableModal.cs
@@ -9,6 +9,8 @@ namespace Northbound
[SerializeField] private GameObject modalPanel;
[SerializeField] private TextMeshProUGUI keyText;
[SerializeField] private TextMeshProUGUI interactText;
+ [SerializeField] private GameObject unavailablePanel;
+ [SerializeField] private TextMeshProUGUI unavailableText;
[Header("Settings")]
[SerializeField] private string defaultKey = "E";
@@ -27,6 +29,11 @@ namespace Northbound
{
modalPanel.SetActive(false);
}
+
+ if (unavailablePanel != null)
+ {
+ unavailablePanel.SetActive(false);
+ }
}
public void ShowModal(IInteractable interactable)
@@ -54,6 +61,11 @@ namespace Northbound
{
modalPanel.SetActive(false);
}
+
+ if (unavailablePanel != null)
+ {
+ unavailablePanel.SetActive(false);
+ }
}
public void UpdateModalContent()
@@ -86,5 +98,48 @@ namespace Northbound
return prompt;
}
+
+ ///
+ /// 상호작용 불가능 메시지를 표시합니다.
+ ///
+ /// 상호작용 불가능한 대상
+ public void ShowUnavailableMessage(IInteractable interactable)
+ {
+ if (interactable == null)
+ {
+ HideModal();
+ return;
+ }
+
+ if (unavailablePanel != null)
+ {
+ // unavailablePanel이 할당된 경우 전용 패널 사용
+ if (modalPanel != null)
+ {
+ modalPanel.SetActive(false);
+ }
+
+ unavailablePanel.SetActive(true);
+
+ if (unavailableText != null)
+ {
+ string prompt = interactable.GetInteractionPrompt();
+ unavailableText.text = ExtractInteractText(prompt);
+ }
+ }
+ else if (modalPanel != null && interactText != null)
+ {
+ // unavailablePanel이 없는 경우 modalPanel을 사용하여 불가능 메시지 표시
+ modalPanel.SetActive(true);
+
+ if (keyText != null)
+ {
+ keyText.text = "";
+ }
+
+ string prompt = interactable.GetInteractionPrompt();
+ interactText.text = ExtractInteractText(prompt);
+ }
+ }
}
}
diff --git a/Assets/Scripts/InteractableModalManager.cs b/Assets/Scripts/InteractableModalManager.cs
index 0629a9d..466daa2 100644
--- a/Assets/Scripts/InteractableModalManager.cs
+++ b/Assets/Scripts/InteractableModalManager.cs
@@ -12,6 +12,7 @@ namespace Northbound
[SerializeField] private PlayerInteraction playerInteraction;
private IInteractable _currentInteractable;
+ private IInteractable _previousUnavailableInteractable;
private bool _isEnabled = false;
private void Awake()
@@ -33,14 +34,13 @@ namespace Northbound
FindModalComponent();
- if (interactableModal != null)
- {
- interactableModal.HideModal();
- }
- else
+ if (interactableModal == null)
{
Debug.LogError("[InteractableModalManager] InteractableModal is still null in Start!");
}
+
+ // Start()에서 HideModal()을 호출하지 않음 - 첫 번째 Update에서 Modal이 표시되지 않는 문제 방지
+ // modalPanel과 unavailablePanel은 InteractableModal.Start()에서 이미 false로 설정됨
}
private void FindModalComponent()
@@ -88,16 +88,26 @@ namespace Northbound
_isEnabled = true;
IInteractable detectedInteractable = GetDetectedInteractable();
+ IInteractable unavailableInteractable = playerInteraction.CurrentUnavailableInteractable;
- if (detectedInteractable != _currentInteractable)
+ bool interactableChanged = detectedInteractable != _currentInteractable;
+ bool unavailableChanged = unavailableInteractable != _previousUnavailableInteractable;
+
+ if (interactableChanged || unavailableChanged)
{
_currentInteractable = detectedInteractable;
+ _previousUnavailableInteractable = unavailableInteractable;
if (_currentInteractable != null)
{
Debug.Log($"[InteractableModalManager] Show modal for interactable");
ShowModal(_currentInteractable);
}
+ else if (unavailableInteractable != null)
+ {
+ Debug.Log($"[InteractableModalManager] Show unavailable message");
+ ShowUnavailableMessageModal(unavailableInteractable);
+ }
else
{
Debug.Log($"[InteractableModalManager] Hide modal");
@@ -149,6 +159,14 @@ namespace Northbound
}
}
+ private void ShowUnavailableMessageModal(IInteractable interactable)
+ {
+ if (interactableModal != null)
+ {
+ interactableModal.ShowUnavailableMessage(interactable);
+ }
+ }
+
private void UpdateModalContent()
{
if (interactableModal != null)
diff --git a/Assets/Scripts/PlayerInteraction.cs b/Assets/Scripts/PlayerInteraction.cs
index d54e1f3..26a64aa 100644
--- a/Assets/Scripts/PlayerInteraction.cs
+++ b/Assets/Scripts/PlayerInteraction.cs
@@ -34,6 +34,7 @@ namespace Northbound
private PlayerInputActions _inputActions;
private IInteractable _currentInteractable;
+ private IInteractable _unavailableInteractable;
private Camera _mainCamera;
private Animator _animator;
private EquipmentSocket _equipmentSocket;
@@ -45,6 +46,7 @@ namespace Northbound
public bool IsInteracting => _isInteracting;
public float WorkPower => workPower;
+ public IInteractable CurrentUnavailableInteractable => _unavailableInteractable;
public override void OnNetworkSpawn()
{
@@ -92,30 +94,42 @@ namespace Northbound
{
Vector3 origin = rayOrigin.position;
Vector3 direction = useForwardDirection ? transform.forward : _mainCamera.transform.forward;
-
+
Ray ray = new Ray(origin, direction);
if (showDebugRay)
{
- Debug.DrawRay(ray.origin, ray.direction * interactionRange,
+ Debug.DrawRay(ray.origin, ray.direction * interactionRange,
_currentInteractable != null ? Color.green : Color.yellow);
}
if (Physics.Raycast(ray, out RaycastHit hit, interactionRange, interactableLayer))
{
IInteractable interactable = hit.collider.GetComponent();
-
+
if (interactable == null)
interactable = hit.collider.GetComponentInParent();
- if (interactable != null && interactable.CanInteract(OwnerClientId))
+ if (interactable != null)
{
- _currentInteractable = interactable;
- return;
+ if (interactable.CanInteract(OwnerClientId))
+ {
+ _currentInteractable = interactable;
+ _unavailableInteractable = null;
+ return;
+ }
+ else
+ {
+ // CanInteract가 false인 경우 추적
+ _currentInteractable = null;
+ _unavailableInteractable = interactable;
+ return;
+ }
}
}
_currentInteractable = null;
+ _unavailableInteractable = null;
}
private void OnInteract(InputAction.CallbackContext context)