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
+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