오늘의 학습 키워드
1. Bow-Arrow 시스템 구축하며 배운 것
2. 차량 제어 UI 시스템 구현 (Feat. Player 상태 변경)
3. Unity에서 움직이는 플랫폼 구현하며 배운 것들

아이템 장착 구조
- Item_Bow를 인벤토리에서 선택하면, 실제 장착 가능한 Equip_Bow 프리팹이 생성된다.
- 이 프리팹에는 실제 공격 기능이 포함된 Bow.cs 스크립트가 붙어 있다.
- Bow.cs는 UIInventory를 참조하여 화살 수량을 확인하고 소모해야 한다.
프리팹의 한계
- 프리팹은 씬의 오브젝트에 직접 연결할 수 없다.
- Inspector에 UIInventory를 드래그해도 프리팹 상태에선 적용되지 않음
- 해결책은 런타임에 동적으로 연결하는 것
private void Awake()
{
if (inventory == null)
inventory = FindObjectOfType<UIInventory>();
}
화살 발사 처리
- Bow.cs에서 ShootArrowRoutine() 코루틴을 실행해 다음을 처리함:
- 스태미너 소모
- 인벤토리에 화살이 있는지 체크 → 없으면 Debug.Log("화살 없음")
- 발사 방향 계산
- 화살 프리팹 생성 후 Arrow.cs.Launch() 호출
if (!inventory.HasItem(arrowItemData, 1))
{
Debug.Log("화살 없음! 발사 불가");
yield break;
}
inventory.UseItem(arrowItemData, 1);
Inventory 시스템 정비
- UIInventory.HasItem()과 UseItem() 구현 시 ItemData.itemId를 기준으로 비교해야 한다.
- 그렇지 않으면 같은 ScriptableObject라도 인스턴스가 다르면 인식하지 못한다.
if (slots[i].item != null && slots[i].item.itemId == item.itemId)
Arrow 동작 디버깅
- Arrow.cs의 OnCollisionEnter()에서 충돌 로그를 남겨, 어디에 맞았는지 확인함.
- 충돌 후 IDamagable 인터페이스가 있는 오브젝트에만 데미지를 적용
- isLaunched 플래그로 루팅용 화살과 전투용 화살 분리
버그 해결 과정
| 문제 | 해결 |
| inventory == null | Awake()에서 FindObjectOfType<UIInventory>()로 동적 참조 |
| Inspector에서 연결 안 됨 | 프리팹이기 때문에 연결 무효 |
| HasItem()에서 아이템 못 찾음 | ItemData의 참조 대신 itemId로 비교 |
| 화살 무한 발사됨 | 수량 확인 및 UseItem() 로직 추가로 해결 |
최종 결과
- Bow 장착 시 화살 발사가 가능해지고,
- 인벤토리에 화살이 없으면 발사 차단됨
- 발사된 화살은 충돌 시 데미지를 입히고, 이후 루팅 가능한 아이템으로 전환됨
- 유연한 구조로 다른 원거리 무기 시스템에도 확장 가능
TIL 핵심 요약 - Unity Bow/Arrow 시스템 구현
- 프리팹은 Inspector에서 Scene 오브젝트 참조 못함
- Bow는 Equip_Bow 프리팹이라 UIInventory 직접 드래그 불가
- → FindObjectOfType<UIInventory>()로 런타임 연결 처리 필요
- ScriptableObject는 참조 대신 ID로 비교해야 신뢰성 확보
- ItemData끼리 비교할 땐 .itemId로 비교해야 같은 아이템으로 인식됨
- 화살 발사 전에 수량 확인, 발사 후 수량 차감 필수
- HasItem() → UseItem() 순서로 정확하게 처리
- 수량이 없으면 발사 중단
- 지역 변수로 필드 덮어쓰면 Inspector 연결 무시됨
- UIInventory inventory = ... ← 이거 쓰면 필드 연결 무시됨
- inventory = ... ← 이렇게만 써야 연결 유지됨
- Arrow는 충돌 감지 후 IDamagable 감지 + 데미지 적용
- 충돌 후 rb.isKinematic = true, col.isTrigger = true로 루팅 전환 처리
차량 제어 UI 시스템 구현 목표
- E 키로 차량을 조작할 수 있는 UI 오픈
- UI 내부에서 다음 설정 가능:
- 플레이어 이동 속도 조절 (Slider)
- 플레이어 크기 조절 (Slider)
- 무적 모드 설정 (Toggle)
- 설정 적용 버튼
- UI 닫기 버튼
시스템 구성
SportsCar.cs (차량 제어 인터랙션)
- IInteractable을 구현하여 플레이어가 E 키로 상호작용 가능
- UI 오픈 시:
- 커서 활성화
- 현재 플레이어 상태를 UI에 반영
- 조작 잠금(canControl = false)
- UI 닫을 시:
- 커서 잠금
- 조작 가능 상태로 복원
PlayerController.cs
- 새로운 변수 canControl 추가
→ UI가 열렸을 때 이동, 점프 등 모든 조작을 막기 위해 사용 - Move() 및 OnJumpInput()에서 if (!canControl) return;으로 조작 제한
PlayerCondition.cs
- 무적 여부를 외부에서 확인하기 위해 IsInvincible() 메서드 추가
개선한 점
1. 실시간 반영 → 적용 버튼 방식 변경
처음에는 슬라이더나 토글 값이 바로 적용되었지만,
실수로 건드렸을 때 되돌리기 어려운 문제가 있었다.
‘적용’ 버튼을 누를 때만 반영되도록 로직을 리팩토링했다.
2. 상태 반영 UI
UI를 열었을 때 플레이어의 현재 상태 (속도, 크기, 무적 여부) 가
슬라이더와 토글에 자동으로 반영되도록 구현해서 UX를 개선했다.
3. 커서 및 컨트롤 제어
UI가 열리면 커서를 활성화하고 플레이어 입력을 잠그며,
닫으면 다시 원래대로 복원되도록 만들어
게임 흐름을 부드럽게 전환할 수 있게 했다.
결과 : 실패
구현했던 것들 (최종 폐기된 부분)
플레이어가 스포츠카에 탑승하여 직접 자동차처럼 움직이는 구조를 목표로 했음.
실제로 했던 구현:
- 자동차 오브젝트에 E키 인터랙션 구현
- 플레이어를 비활성화하고 자동차를 Player처럼 조작하는 시도
- 카메라와 입력 시스템 전환까지 고려
- 카메라는 원래 Player 내부에 있음
폐기한 이유: 카메라 구조 문제
- 플레이어 오브젝트 안에 CameraContainer가 붙어있는 구조였고,
이 때문에 자동차 탑승 시에는 카메라 위치/대상 전환을 동적으로 바꿔줘야 함 - 하지만 카메라를 옮기거나 교체하려면 다음을 다 바꿔야 했음:
- 카메라의 부모 구조
- PlayerController.cs에서 마우스 입력 처리 (OnLookInput)
- CameraLook()의 위치 갱신 로직
- 기존 플레이어와 새 조작체(자동차)의 컨트롤러 교체
결론:
자동차 탑승은 단순히 플레이어 비활성화 + 자동차 활성화가 아니었고,
카메라 시스템 전체 리팩토링이 필요했기 때문에 너무 부담스러워서 폐기.
그때 고민했던 것들
- "차라리 카메라를 Player 바깥으로 빼면 해결되지 않을까?"
라는 가능성도 떠올렸지만,- 이미 기존 모든 시스템이 cameraContainer에 의존하고 있었고
- PlayerController가 Look과 Move를 통합해서 처리하고 있었기 때문에
Player와 Camera가 분리되는 순간 모든 시점 제어가 무너짐.
그래서 구조를 바꿔서 한 일
- 직접 탑승 구조를 폐기하고, UI를 통해 플레이어 상태를 제어하는 방식으로 선회함.
- PlayerChangeUI.cs에서 다음 UI 기능을 구현:
- 크기 조절 (Slider)
- 이동 속도 조절 (Slider)
- 무적 모드 설정 (Toggle)
- 닫기 버튼 (커서 및 입력 원복)
UI는 성공적으로 작동하며,
플레이어 상태 변화가 잘 반영됨 (속도, 크기, 무적, 조작 제한 등).
느낀 점
- "처음부터 카메라를 플레이어 밖에 두는 구조였으면…" 하는 아쉬움이 있었음
- 카메라와 플레이어를 강하게 엮지 않는 구조가 확장성에 좋다는 걸 체감함
- 실패를 통해 카메라, 입력, 시점 컨트롤은 완전히 독립적으로 관리해야 리팩토링이 쉬움을 배움
TIL 결론 요약
항목상태설명
| 플레이어 → 자동차 탑승 | 실패 | 카메라 위치 및 입력 시스템 전체 리팩토링이 필요했음 |
| 카메라 바깥 구조 시도 | 고민 | 생각은 했지만 실제 시도는 안 함. 리스크 커 보였음 |
| UI 방식 전환 | 성공 | 크기/속도/무적을 UI로 제어, 조작 제한 포함 구현 완료 |
목표
- 시간에 따라 왕복 이동하는 플랫폼(MovingPlatForm)을 구현
- 플랫폼 위에 플레이어가 있을 때 자연스럽게 따라오게 만들기
- 시각적으로도 Collider뿐 아니라 Mesh가 실제로 같이 움직이도록 하기
구현을 위해 고민했던 구조
- pointA, pointB: 이동할 위치를 Transform으로 정의
- Vector3.MoveTowards를 이용해서 플랫폼을 일정한 속도로 왕복하게 함
- OnCollisionEnter/Exit에서 플레이어를 SetParent()하여 자연스럽게 따라가도록 설정
- 플레이어가 UI를 열거나 멈췄을 때는 isFrozen, Rigidbody.isKinematic = true 등을 활용해서 완전히 정지
실수들
1. 자식 오브젝트(MeshRenderer)가 같이 안 움직이는 문제
- 처음엔 Collider는 움직이는데 Mesh는 안 움직여서 "버그인가?" 착각할 수 있음
- 원인: 오브젝트에 Static 체크가 되어 있으면 렌더링이 고정됨
- 해결: Static 체크 해제하면 Mesh도 Collider와 같이 움직인다
Unity에서 Static은 "절대 안 움직임"으로 최적화 처리됨 → 움직이는 오브젝트엔 절대 쓰면 안 됨
2. NullReferenceException 오류
- 에러 예시:
NullReferenceException: Object reference not set to an instance of an object MovingPlatForm.Update () (at Assets/Scripts/MovingPlatForm.cs:XX)
- 원인:
- pointA, pointB를 인스펙터에서 할당 안 한 경우
- GameObject.Find("MovingPlatForm") 로 찾을 때 오타 or 비활성화
- 해결:
- FindObjectOfType<CharacterManager>() 같이 안전한 방식 사용
- transform.parent?.name 처럼 null-safe 코드로 작성
3. 이름만 바꾸고 구조를 바꾸지 않음
- Rock7이라는 프리팹 이름만 MovingPlatForm으로 바꾸고 기능 붙이는 건 OK
- 하지만 내부 구조(MeshRenderer가 자식인지, Static인지 등) 확인 안 하면 Mesh가 안 움직일 수 있음
개선한 점
- 직접 오브젝트에 Debug 로그를 찍어 구조와 부모 관계를 확인함
- Static 문제를 파악하고, Mesh가 움직이지 않는 원인을 분석함
- Rigidbody가 있어도 isKinematic = true로 설정하면 transform.position 이동과 충돌이 공존 가능하다는 점도 확인
최종 구현 코드 핵심
void Update()
{
transform.position = Vector3.MoveTowards(transform.position, target, moveSpeed * Time.deltaTime);
if (Vector3.Distance(transform.position, target) < 0.1f)
{
target = (target == pointA.position) ? pointB.position : pointA.position;
}
}
void OnCollisionEnter(Collision collision)
{
if (collision.gameObject.CompareTag("Player"))
collision.transform.SetParent(transform);
}
void OnCollisionExit(Collision collision)
{
if (collision.gameObject.CompareTag("Player"))
collision.transform.SetParent(null);
}
TIL요약
| 항목 | 내용 |
| Mesh 안 움직임 | Static 체크 때문 → 비활성화 필요 |
| null 에러 | pointA, pointB 미지정, 오브젝트 찾기 실패 |
| 따라가기 구현 | SetParent() 활용해서 자연스러운 움직임 |
| Rigidbody 설정 | isKinematic = true 로 transform 이동 활용 가능 |