feat: 집기/놓기 상호작용 (게임 핵심 루프)

- Carryable: 집을 수 있는 물건 표시
- PickupController: 범위 내 박스를 E키로 집고/놓기 + 화면 힌트
- PickupDemoSetup: 플레이어에 PickupController 부착 + 입고 박스 4개 스폰

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jklee
2026-06-14 20:05:50 +09:00
parent f5ae70b0aa
commit f8577c5f7d
7 changed files with 196 additions and 0 deletions
+68
View File
@@ -0,0 +1,68 @@
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using SinoWms.Gameplay;
using SinoWms.Player;
namespace SinoWms.EditorTools
{
/// <summary>
/// 집기/놓기 데모 구성: 플레이어에 PickupController 추가 + 입고 박스 스폰.
/// 메뉴 <b>SinoWMS/Setup Pickup Demo</b>. Play 후 박스에 다가가 E키로 집고 놓는다.
/// </summary>
public static class PickupDemoSetup
{
[MenuItem("SinoWMS/Setup Pickup Demo")]
public static void Setup()
{
var player = GameObject.Find("Player");
if (player == null)
{
Debug.LogError("[PickupDemoSetup] Player가 없습니다. 먼저 'Setup Playground Scene'을 실행하세요.");
return;
}
if (player.GetComponent<PickupController>() == null)
player.AddComponent<PickupController>();
var container = GameObject.Find("InboundBoxes") ?? new GameObject("InboundBoxes");
// 입고 대기 박스 4개 (남쪽 통로 근처에 배치)
var colors = new[]
{
new Color(0.75f, 0.55f, 0.35f),
new Color(0.70f, 0.50f, 0.30f),
new Color(0.80f, 0.60f, 0.40f),
new Color(0.65f, 0.45f, 0.28f),
};
string[] names = { "박스 A (생수)", "박스 B (우유)", "박스 C (라면)", "박스 D (잡화)" };
for (int i = 0; i < 4; i++)
{
string objName = $"Box_{i + 1}";
if (container.transform.Find(objName) != null) continue;
var box = GameObject.CreatePrimitive(PrimitiveType.Cube);
box.name = objName;
box.transform.SetParent(container.transform, false);
box.transform.localPosition = new Vector3(-3f + i * 2f, 0.5f, -20f);
box.transform.localScale = Vector3.one;
box.GetComponent<Renderer>().sharedMaterial = MakeMaterial(colors[i]);
var rb = box.AddComponent<Rigidbody>();
rb.mass = 1f;
box.AddComponent<Carryable>().displayName = names[i];
}
EditorSceneManager.MarkSceneDirty(player.scene);
Selection.activeGameObject = player;
Debug.Log("[PickupDemoSetup] 완료. Play 후 박스에 다가가 E키로 집고 놓아 보세요.");
}
private static Material MakeMaterial(Color color)
{
var shader = Shader.Find("Universal Render Pipeline/Lit") ?? Shader.Find("Standard");
return new Material(shader) { color = color };
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 10eba722501bf4dcaa1d33cc90d763b1
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 3ee476866b1bf47f5a10539f8b8dcef5
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+15
View File
@@ -0,0 +1,15 @@
using UnityEngine;
namespace SinoWms.Gameplay
{
/// <summary>플레이어가 집어 옮길 수 있는 물건(박스/팔레트) 표시 컴포넌트.</summary>
[RequireComponent(typeof(Rigidbody))]
[RequireComponent(typeof(Collider))]
public class Carryable : MonoBehaviour
{
[Tooltip("힌트/로그에 표시할 이름. 비우면 오브젝트 이름 사용.")]
public string displayName;
public string Label => string.IsNullOrEmpty(displayName) ? name : displayName;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 315a7e23f649b4187a444b01f14a8e9d
@@ -0,0 +1,99 @@
using UnityEngine;
using UnityEngine.InputSystem;
using SinoWms.Gameplay;
namespace SinoWms.Player
{
/// <summary>
/// 캐릭터의 집기/놓기 상호작용. 범위 내 가장 가까운 Carryable을 E키로 집어
/// 앞쪽 홀드 포인트에 들고 다니고, 다시 E키로 내려놓는다.
/// </summary>
public class PickupController : MonoBehaviour
{
[SerializeField] private float pickupRange = 2.5f;
[SerializeField] private Transform holdPoint;
private Carryable _carried;
private Carryable _target; // 현재 집을 수 있는 후보(힌트 표시용)
private GUIStyle _style;
private void Awake()
{
if (holdPoint == null)
{
var hp = new GameObject("HoldPoint").transform;
hp.SetParent(transform, false);
hp.localPosition = new Vector3(0f, 0.6f, 0.9f);
holdPoint = hp;
}
}
private void Update()
{
var kb = Keyboard.current;
if (kb == null) return;
if (_carried == null)
_target = FindNearest();
if (kb.eKey.wasPressedThisFrame)
{
if (_carried != null) Drop();
else if (_target != null) PickUp(_target);
}
}
private Carryable FindNearest()
{
var hits = Physics.OverlapSphere(transform.position, pickupRange);
Carryable best = null;
float bestDist = float.MaxValue;
foreach (var h in hits)
{
var c = h.GetComponentInParent<Carryable>();
if (c == null || c == _carried) continue;
float d = Vector3.Distance(transform.position, c.transform.position);
if (d < bestDist) { bestDist = d; best = c; }
}
return best;
}
private void PickUp(Carryable c)
{
_carried = c;
_target = null;
if (c.TryGetComponent<Rigidbody>(out var rb)) rb.isKinematic = true;
if (c.TryGetComponent<Collider>(out var col)) col.enabled = false;
c.transform.SetParent(holdPoint);
c.transform.localPosition = Vector3.zero;
c.transform.localRotation = Quaternion.identity;
}
private void Drop()
{
var c = _carried;
_carried = null;
c.transform.SetParent(null);
c.transform.position = holdPoint.position;
c.transform.rotation = holdPoint.rotation;
if (c.TryGetComponent<Collider>(out var col)) col.enabled = true;
if (c.TryGetComponent<Rigidbody>(out var rb)) rb.isKinematic = false;
}
private void OnGUI()
{
_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 (msg == null) return;
GUI.Label(new Rect(Screen.width / 2f - 150f, 24f, 300f, 32f), msg, _style);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 8525e0c0e962c47b78e49a7e3371fe97