diff --git a/Assets/Scripts/Editor/SlotSetup.cs b/Assets/Scripts/Editor/SlotSetup.cs new file mode 100644 index 0000000..02bd402 --- /dev/null +++ b/Assets/Scripts/Editor/SlotSetup.cs @@ -0,0 +1,65 @@ +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEngine; +using SinoWms.Gameplay; + +namespace SinoWms.EditorTools +{ + /// + /// 적치 슬롯을 창고에 배치한다. 각 슬롯은 바닥 패드 + 트리거 콜라이더 + 기대 품목을 가진다. + /// 메뉴 SinoWMS/Setup Slots. Play 후 박스를 들고 슬롯에서 E키로 적치(정답=초록/오답=빨강). + /// + public static class SlotSetup + { + // 슬롯 위치와 기대 품목 (PickupDemoSetup의 박스 이름과 일치시켜 정답 구성) + private static readonly (Vector3 pos, string expected)[] Slots = + { + (new Vector3(-6f, 0.03f, 8f), "박스 A (생수)"), + (new Vector3(-2f, 0.03f, 8f), "박스 B (우유)"), + (new Vector3(2f, 0.03f, 8f), "박스 C (라면)"), + (new Vector3(6f, 0.03f, 8f), "박스 D (잡화)"), + }; + + [MenuItem("SinoWMS/Setup Slots")] + public static void Setup() + { + var container = GameObject.Find("PutawaySlots") ?? new GameObject("PutawaySlots"); + + for (int i = 0; i < Slots.Length; i++) + { + string slotName = $"Slot_{i + 1}"; + if (container.transform.Find(slotName) != null) continue; + + var slotGo = new GameObject(slotName); + slotGo.transform.SetParent(container.transform, false); + slotGo.transform.localPosition = Slots[i].pos; + + // 바닥 패드 (상태 색 표시) + var pad = GameObject.CreatePrimitive(PrimitiveType.Cube); + pad.name = "Pad"; + Object.DestroyImmediate(pad.GetComponent()); + pad.transform.SetParent(slotGo.transform, false); + pad.transform.localScale = new Vector3(1.2f, 0.05f, 1.2f); + pad.GetComponent().sharedMaterial = + new Material(Shader.Find("Universal Render Pipeline/Lit") ?? Shader.Find("Standard")) + { color = new Color(1f, 0.82f, 0.3f) }; + + // 적치 감지용 트리거 + var trigger = slotGo.AddComponent(); + trigger.isTrigger = true; + trigger.size = new Vector3(1.4f, 1.5f, 1.4f); + trigger.center = new Vector3(0f, 0.75f, 0f); + + var slot = slotGo.AddComponent(); + var so = new SerializedObject(slot); + so.FindProperty("pad").objectReferenceValue = pad.GetComponent(); + so.FindProperty("expectedLabel").stringValue = Slots[i].expected; + so.ApplyModifiedProperties(); + } + + EditorSceneManager.MarkSceneDirty(container.scene); + Selection.activeGameObject = container; + Debug.Log("[SlotSetup] 완료. 박스를 들고 슬롯 위에서 E키로 적치하세요(정답=초록/오답=빨강)."); + } + } +} diff --git a/Assets/Scripts/Editor/SlotSetup.cs.meta b/Assets/Scripts/Editor/SlotSetup.cs.meta new file mode 100644 index 0000000..aaf7b6c --- /dev/null +++ b/Assets/Scripts/Editor/SlotSetup.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: bab24c45943e84dc9bf7ace8f7d0d618 \ No newline at end of file diff --git a/Assets/Scripts/Gameplay/PickupController.cs b/Assets/Scripts/Gameplay/PickupController.cs index 9d2eb85..69a60d4 100644 --- a/Assets/Scripts/Gameplay/PickupController.cs +++ b/Assets/Scripts/Gameplay/PickupController.cs @@ -15,6 +15,7 @@ namespace SinoWms.Player private Carryable _carried; private Carryable _target; // 현재 집을 수 있는 후보(힌트 표시용) + private PutawaySlot _targetSlot; // 들고 있을 때 적치 가능한 빈 슬롯 private GUIStyle _style; private void Awake() @@ -34,7 +35,14 @@ namespace SinoWms.Player if (kb == null) return; if (_carried == null) + { _target = FindNearest(); + _targetSlot = null; + } + else + { + _targetSlot = FindNearestSlot(); + } if (kb.eKey.wasPressedThisFrame) { @@ -58,8 +66,27 @@ namespace SinoWms.Player return best; } + private PutawaySlot FindNearestSlot() + { + var hits = Physics.OverlapSphere(transform.position, pickupRange); + PutawaySlot best = null; + float bestDist = float.MaxValue; + foreach (var h in hits) + { + var s = h.GetComponentInParent(); + if (s == null || s.IsOccupied) continue; + float d = Vector3.Distance(transform.position, s.transform.position); + if (d < bestDist) { bestDist = d; best = s; } + } + return best; + } + private void PickUp(Carryable c) { + // 슬롯에 적치돼 있던 박스라면 슬롯을 비운다 + var fromSlot = c.GetComponentInParent(); + if (fromSlot != null && fromSlot.Occupant == c) fromSlot.Clear(); + _carried = c; _target = null; @@ -75,11 +102,18 @@ namespace SinoWms.Player { var c = _carried; _carried = null; - c.transform.SetParent(null); + + // 근처 빈 슬롯이 있으면 적치(스냅 + 정답 판정) + if (_targetSlot != null && _targetSlot.TryPlace(c)) + { + _targetSlot = null; + return; + } + + // 아니면 그냥 바닥에 내려놓기 c.transform.position = holdPoint.position; c.transform.rotation = holdPoint.rotation; - if (c.TryGetComponent(out var col)) col.enabled = true; if (c.TryGetComponent(out var rb)) rb.isKinematic = false; } @@ -89,8 +123,12 @@ namespace SinoWms.Player _style ??= new GUIStyle(GUI.skin.box) { fontSize = 18, alignment = TextAnchor.MiddleCenter }; string msg = null; - if (_carried != null) msg = $"E: 내려놓기 — {_carried.Label}"; - else if (_target != null) msg = $"E: 집기 — {_target.Label}"; + if (_carried != null) + msg = _targetSlot != null + ? $"E: 적치 — {_targetSlot.Title}" + : $"E: 내려놓기 — {_carried.Label}"; + else if (_target != null) + msg = $"E: 집기 — {_target.Label}"; if (msg == null) return; GUI.Label(new Rect(Screen.width / 2f - 150f, 24f, 300f, 32f), msg, _style); diff --git a/Assets/Scripts/Gameplay/PutawaySlot.cs b/Assets/Scripts/Gameplay/PutawaySlot.cs new file mode 100644 index 0000000..da6809c --- /dev/null +++ b/Assets/Scripts/Gameplay/PutawaySlot.cs @@ -0,0 +1,60 @@ +using UnityEngine; + +namespace SinoWms.Gameplay +{ + /// + /// 적치 슬롯. 캐릭터가 들고 온 Carryable을 받아 스냅 배치하고, + /// 기대 품목(expectedLabel)과 비교해 정답/오답을 바닥 패드 색으로 표시한다. + /// + public class PutawaySlot : MonoBehaviour + { + [Tooltip("이 슬롯에 와야 하는 품목 라벨. 비우면 아무거나 정답 처리.")] + public string expectedLabel; + + [Tooltip("바닥 표시용 패드 렌더러(색으로 상태 표시).")] + [SerializeField] private Renderer pad; + + [Tooltip("박스가 스냅될 패드 위 높이.")] + [SerializeField] private float dropHeight = 0.5f; + + private static readonly Color Empty = new Color(1f, 0.82f, 0.3f); + private static readonly Color Correct = new Color(0.3f, 0.8f, 0.4f); + private static readonly Color Wrong = new Color(0.85f, 0.3f, 0.3f); + + public Carryable Occupant { get; private set; } + public bool IsOccupied => Occupant != null; + public bool IsCorrect => + IsOccupied && (string.IsNullOrEmpty(expectedLabel) || Occupant.Label == expectedLabel); + + public string Title => string.IsNullOrEmpty(expectedLabel) ? name : expectedLabel; + + private void Start() => UpdateMarker(); + + public bool TryPlace(Carryable c) + { + if (IsOccupied) return false; + Occupant = c; + + c.transform.SetParent(transform); + c.transform.localPosition = new Vector3(0f, dropHeight, 0f); + c.transform.localRotation = Quaternion.identity; + if (c.TryGetComponent(out var col)) col.enabled = true; + if (c.TryGetComponent(out var rb)) rb.isKinematic = true; + + UpdateMarker(); + return true; + } + + public void Clear() + { + Occupant = null; + UpdateMarker(); + } + + private void UpdateMarker() + { + if (pad == null) return; + pad.material.color = !IsOccupied ? Empty : (IsCorrect ? Correct : Wrong); + } + } +} diff --git a/Assets/Scripts/Gameplay/PutawaySlot.cs.meta b/Assets/Scripts/Gameplay/PutawaySlot.cs.meta new file mode 100644 index 0000000..810e91f --- /dev/null +++ b/Assets/Scripts/Gameplay/PutawaySlot.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: c9d533b08ac4a487182866db79a8e2e3 \ No newline at end of file