🧠 오늘의 핵심 정리
- 아이템 상호작용(IInteractable) + 줍기(ItemObject) 구현
- Interaction: 화면 중앙 Raycast로 상호작용 대상 탐지 + 안내 텍스트 출력
- UIInventory: 스택/사용/드롭/장착 시스템 구현 + 인벤 열면 TimeScale 정지
- EquipTool: 공격 시 스태미나 소모 + 화면 중앙 Ray로 타격 판정
- 적 NPC: NavMesh 기반 AI + 피격/드롭 + 스포너 리스폰
- 점프 패드 구현(점프 부스트) + 맵 이탈 방지 수정
🎒 아이템 상호작용 & 줍기 (IInteractable / ItemObject)
아이템을 “바라보고 E로 줍기” 흐름을 만들기 위해, 상호작용 공통 규격을 인터페이스로 먼저 정의했다.
아이템 오브젝트는 GetInteractPrompt()로 안내 문구를 주고, OnInteract()에서 플레이어에게 아이템 데이터를 넘기고 삭제하도록 구성했다.
public interface IInteractable
{
public string GetInteractPrompt();
public void OnInteract();
}
public class ItemObject : MonoBehaviour, IInteractable
{
public ItemData data;
public string GetInteractPrompt()
{
string str = $"{data.displayName}\n{data.description}";
return str;
}
public void OnInteract()
{
CharacterManager.Instance.Player.itemData = data;
CharacterManager.Instance.Player.addItem?.Invoke();
Destroy(gameObject);
}
}
여기서 포인트는 addItem 이벤트를 쓴 것!
플레이어는 “아이템 데이터를 들고 있음”만 책임지고, 인벤토리가 그 이벤트를 받아 실제 추가 처리를 하게 만들어서 흐름이 깔끔해졌다.
🖥️ Interaction: 화면 중앙 Raycast + 안내 텍스트 출력
2D 때 RaycastHit2D로 상호작용을 했던 것처럼, 3D에서는 카메라 기준 Raycast로 변경했다.
화면 중앙(=조준점) 기준으로 Ray를 쏴서 대상이 있으면 promptText를 켜고, 사라지면 끄는 방식이다.
Ray ray = _camera.ScreenPointToRay(new Vector3(Screen.width / 2, Screen.height / 2, 0));
RaycastHit hit;
if (Physics.Raycast(ray, out hit, maxCheckDistance, layerMask))
{
if (hit.collider.gameObject != curInteractGameObject)
{
curInteractGameObject = hit.collider.gameObject;
curInteractable = hit.collider.GetComponent();
SetPromptText();
}
}
else
{
curInteractGameObject = null;
curInteractable = null;
promptText.gameObject.SetActive(false);
}
입력 처리도 간단하게 “대상이 있으면 실행”으로 했고, 실행 후에는 상태를 리셋해서 UI가 깔끔하게 사라지도록 했다.
public void OnInteractInput(InputAction.CallbackContext context)
{
if (context.started && curInteractable != null)
{
curInteractable.OnInteract();
curInteractGameObject = null;
curInteractable = null;
promptText.gameObject.SetActive(false);
}
}
⏱️ UIInventory: 스택/사용/드롭/장착 + 인벤 열면 시간 정지
인벤토리는 오늘 작업의 핵심이었다.
슬롯에 아이템이 들어가면 아이콘/개수 표시를 하고, 선택한 아이템의 타입에 따라 버튼이 다르게 노출되게 만들었다.
스택 처리는 “같은 아이템이고 maxStackAmount 미만인 슬롯 먼저 채우기”로 구성했다.
ItemSlot GetItemStack(ItemData data)
{
for(int i = 0; i < slots.Length; i++)
{
if (slots[i].item == data && slots[i].quantity < data.maxStackAmount)
{
return slots[i];
}
}
return null;
}
그리고 인벤 열 때 “게임 멈춤”을 확실히 해주려고 Toggle에서 Time.timeScale을 같이 제어했다.
public void Toggle()
{
if (IsOpen())
{
Time.timeScale = 1f;
inventoryWindow.SetActive(false);
}
else
{
Time.timeScale = 0f;
inventoryWindow.SetActive(true);
}
}
🛡️ 장비/공격: 스태미나 소모 + 중앙 Ray 판정 (IDamageable 통합)
무기 사용에 “자원”이 들어가야 생존게임 느낌이 난다… 해서 공격 시 스태미나가 빠지게 만들었다.
PlayerCondition에 UseStamina()를 만들어서, 스태미나가 부족하면 공격 자체를 못 하게 막았다.
public bool UseStamina(float amount)
{
if(stamina.curValue - amount < 0f)
{
return false;
}
stamina.Subtract(amount);
return true;
}
EquipTool에서는 공격 입력 시 먼저 스태미나 체크를 하고, 가능할 때만 공격 애니메이션과 트리거가 실행되도록 했다.
public override void OnAttackInput()
{
if (!attacking)
{
if (CharacterManager.Instance.Player.condition.UseStamina(useStamina))
{
attacking = true;
_animation.Play("Attack");
Invoke(nameof(StopAttackAnimation), ANIMATION_DURATION);
_animator.SetTrigger("Attack");
Invoke("OnCanAttack", attackRate);
}
}
}
타격 판정은 카메라 중앙 Raycast로 처리했고, 기존에는 Resource 전용이었는데 오늘은 IDamageable로 바꿔서 적/자원 모두 통합 처리할 수 있게 했다.
public void OnHit()
{
Ray ray = _camera.ScreenPointToRay(new Vector3(Screen.width/2, Screen.height/2, 0));
RaycastHit hit;
if(Physics.Raycast(ray, out hit, attackDistance))
{
if(doesDealDamage && hit.collider.TryGetComponent(out IDamageable damageable))
{
damageable.TakePhysicalDamage(damage);
}
}
}
👾 적 NPC + 스포너(리스폰)까지
적도 오늘 제대로 붙였다.
NavMesh로 배회/추적/공격 상태를 돌리고, 피격 시 색이 잠깐 변하는 플래시 연출도 넣었다.
그리고 “죽으면 끝”이 아니라, 스포너가 다시 뽑아주는 구조로 연결했다.
NPC가 죽을 때 스포너에게 “죽었음”을 알리고, 스포너가 일정 시간 후 리스폰:
// NPC.cs (Die)
if(spawn != null)
{
spawn.OnNPCDied();
}
Destroy(gameObject);
// NpCSpawn.cs
public void SpawnNPC()
{
currentNPC = Instantiate(npcPrefab, transform.position, transform.rotation);
currentNPC.GetComponent<NPC>().spawn = this;
Animator anim = currentNPC.GetComponentInChildren<Animator>();
if(anim != null)
{
anim.SetTrigger("Spawn");
}
}
public void OnNPCDied()
{
StartCoroutine(RespawnNPC());
}
IEnumerator RespawnNPC()
{
yield return new WaitForSeconds(respawnDelay);
SpawnNPC();
}
이렇게 해두니까 필드가 너무 비지 않고, “생존” 느낌이 확 살아났다.
🪂 점프 패드 구현
JumpPad 태그 구역에 들어가면 onJumpad 플래그를 켜고, 점프 시 jumpBoost를 더해 슈퍼 점프가 되도록 구현했다.
private void OnTriggerEnter(Collider other)
{
if (other.CompareTag("JumpPad"))
{
onJumpad = true;
}
}
private void OnTriggerExit(Collider other)
{
if (other.CompareTag("JumpPad"))
{
onJumpad = false;
}
}
public void OnJump(InputAction.CallbackContext context)
{
if (context.started && IsGround())
{
float finalJumpPower = jumpPower;
if (onJumpad)
{
Debug.Log("슈퍼점프");
finalJumpPower += jumpBoost;
}
_rigidbody.AddForce(Vector2.up * finalJumpPower, ForceMode.Impulse);
_animation.Play("Jump");
}
}
점프 패드는 재밌는데… 항상 맵 이탈 문제가 따라와서 그 부분은 추가로 수정했다.
📌 오늘의 회고
오늘은 기능이 한 번에 너무 많이 붙어서 정신이 없었지만, 그래도 “게임의 뼈대”가 확실히 세워진 날이었다.
인벤토리/장비/스태미나/적/드롭/리스폰까지 있으니까 진짜 생존게임 같은 느낌이 난다.
이제 남은 건… 제출 가능한 상태로 버그 줄이기 + 안정화… 제발!!
'내일배움캠프 본캠프' 카테고리의 다른 글
| [내일배움캠프 34일차 TIL] 3D팀 프로젝트 (0) | 2025.11.14 |
|---|---|
| [내일배움캠프 33일차 TIL] 개인 프로젝트 제출날 (0) | 2025.11.13 |
| [내일배움캠프 31일차] 3D 서바이벌 게임 세팅 (0) | 2025.11.12 |
| [내일배움캠프 30일차 TIL] 3D 게임의 지옥 (0) | 2025.11.10 |
| [내일배움캠프 29일차 TIL] 3D 아이템 (0) | 2025.11.07 |