wan의 개발일기
RoomCraft #19. 있는 척했던 기능 만들기 — 1인칭 시점 본문
소스코드 → github.com/Jywan/RoomCraft | 플레이 → jywan.github.io/RoomCraft
들어가며
README를 다시 읽다가 뜨끔했다. "주요 기능" 목록에 "탑뷰 ↔ 3D 자유시점 ↔ 1인칭 뷰"라고 적혀있었는데, 정작 CameraController의 CameraMode enum엔 TopView, FreeView 둘뿐이었다. 1인칭은 있는 척만 하고 실제로는 없었던 것. 이번 편에서 진짜로 만들었다.
시작 위치를 어디로 잡을까
제일 먼저 걸린 문제는 "1인칭으로 전환했을 때 카메라를 어디에 놓을 것인가"였다. 제일 쉬운 방법은 방 꼭짓점들의 평균(중심점)을 쓰는 건데, 이러면 L자형이나 지난번 만든 오목한 커스텀 방에서 그 중심점이 방 밖(파인 부분)에 떨어질 수 있다. 방 안에서 시작해야 할 카메라가 벽 밖에서 시작하는 꼴.
대신 이미 갖고 있던 도구를 재활용했다. 바닥을 만들 때 Ear Clipping으로 삼각분할을 해뒀으니, 그 삼각형 중 아무거나 하나를 골라 세 꼭짓점의 평균을 쓰면 된다. 삼각형의 무게중심은 어떤 모양의 삼각형이든 항상 그 삼각형 내부에 있다 — 그래서 방이 아무리 복잡한 모양이어도 결과값은 항상 바닥 위의 유효한 한 점이 된다.
// RoomBuilder.cs
public Vector3 GetFirstPersonSpawnPoint()
{
if (currentRoomData == null) return Vector3.zero;
List<Vector2> outline = currentRoomData.GetVertices();
List<int> triangles = PolygonUtils.Triangulate(outline);
Vector2 a = outline[triangles[0]];
Vector2 b = outline[triangles[1]];
Vector2 c = outline[triangles[2]];
Vector2 centroid = (a + b + c) / 3f;
return new Vector3(centroid.x, EyeHeight, centroid.y);
}
WASD로 걸어 다니기
기존 탑뷰/자유시점은 둘 다 targetPoint를 중심으로 카메라가 공전하는 방식이라 "이동"이라는 개념 자체가 없었다. 1인칭은 완전히 새로운 조작 방식이 필요했다 — WASD로 걷고, 마우스 드래그로 시점을 돌리는 흔한 1인칭 조작.
private void HandleFirstPersonInput()
{
// 좌클릭 드래그로 시점 회전 (yaw/pitch)
if (fpDragging)
{
Vector3 delta = Input.mousePosition - fpLastMousePosition;
fpYaw += delta.x * lookSpeed * 0.1f;
fpPitch -= delta.y * lookSpeed * 0.1f;
fpPitch = Mathf.Clamp(fpPitch, -80f, 80f);
transform.rotation = Quaternion.Euler(fpPitch, fpYaw, 0f);
}
// WASD 이동, Y는 고정해서 눈높이 유지
Vector3 move = Vector3.zero;
if (Input.GetKey(KeyCode.W)) move += transform.forward;
if (Input.GetKey(KeyCode.S)) move -= transform.forward;
if (Input.GetKey(KeyCode.A)) move -= transform.right;
if (Input.GetKey(KeyCode.D)) move += transform.right;
move.y = 0f;
}
탑뷰 ↔ 자유시점 ↔ 1인칭은 기존에 있던 T키 토글에 그대로 얹었다. 원래 2단 토글이던 ToggleView()를 3단 순환으로 바꿨다.
둘러보기 전용으로
1인칭에서도 가구를 선택하고 옮길 수 있게 할지 고민했는데, 마우스 드래그가 "시점 회전"과 "가구 드래그" 둘 다에 쓰이는 조작이라 겹치면 헷갈릴 게 뻔했다. 1인칭은 배치 결과를 둘러보는 용도로만 쓰고, 편집은 다시 탑뷰/자유시점으로 돌아가서 하는 걸로 정리했다.
// FurnitureInteraction.cs
private void Update()
{
if (cameraController != null && cameraController.GetCurrentMode() == CameraMode.FirstPerson)
return;
HandleSelection();
HandleDrag();
HandleRotation();
HandleDelete();
}
1인칭으로 들어가는 순간 기존에 선택되어 있던 가구도 강제로 선택 해제하도록 했다. 안 그러면 1인칭으로 걸어 다니는 동안에도 툴바에 회전/삭제 버튼이 떠 있는 상태로 남는다.
벽은 막고 가구는 통과 — CharacterController
처음엔 transform.position에 이동값을 그냥 더하는 방식으로 짰다. 당연히 벽을 그대로 뚫고 지나갔다. Unity의 CharacterController 컴포넌트를 카메라에 붙이고, 이동을 characterController.Move()로 바꿨다.
Vector3 delta = move.normalized * (moveSpeed * Time.deltaTime);
if (characterController != null)
characterController.Move(delta);
else
transform.position += delta;
재밌는 건 가구는 여전히 통과된다는 점이다. 가구 콜라이더가 선택/드래그 감지를 위해 isTrigger = true로 되어있는데, CharacterController는 트리거 콜라이더랑 충돌하지 않는다. 벽(일반 콜라이더)만 막고 가구는 통과되는 지금 상태가 결과적으로 딱 원하던 동작이라 그대로 뒀다.
삽질 기록
1인칭에서 영원히 못 빠져나옴
3단 순환으로 바꾸면서 조건 순서를 잘못 짰다.
else if (currentMode == CameraMode.FirstPerson)
SwitchMode(CameraMode.FirstPerson); // 자기 자신으로 전환
SwitchMode() 맨 앞줄이 "같은 모드면 무시"라서, 1인칭 상태에서 이 코드가 실행되면 아무 일도 안 일어난다. 즉 한번 1인칭에 들어가면 T키를 눌러도 계속 1인칭에 갇힌다. FreeView일 때 FirstPerson으로 가야 하는 걸 FirstPerson일 때 FirstPerson으로 가게 잘못 짠 것 — 조건 순서만 바로잡았다.
CharacterController 넣었는데 그래도 벽을 뚫음
코드도 고쳤고 컴파일도 되는데 여전히 벽을 통과했다. 씬 파일을 열어서 확인해보니 CharacterController 컴포넌트 자체가 Main Camera에 없었다 — 코드에 필드만 추가해뒀지, 실제로 Unity 에디터에서 컴포넌트를 추가하고 인스펙터 슬롯에 연결하는 걸 깜빡했던 것. characterController != null 체크가 조용히 폴백 경로(충돌 없는 예전 방식)로 빠지고 있었다.
코드가 컴파일된다고 기능이 다 붙은 게 아니다 — 특히 인스펙터에서 손으로 연결해야 하는 참조는 코드 리뷰만으론 못 잡는다.
Rider가 잡아준 것들
이번엔 라이더의 정적 분석 경고를 하나씩 따라가봤다. Camera.main을 Start()에서 캐싱(FurnitureInteraction에 이미 있던 패턴 재사용), 씬에 항상 존재하는 EventSystem.current의 불필요한 null 체크 제거, move.normalized * moveSpeed * Time.deltaTime처럼 벡터-스칼라 곱셈을 연쇄로 하던 걸 스칼라끼리 먼저 곱하고 벡터엔 한 번만 곱하도록(move.normalized * (moveSpeed * Time.deltaTime)) 순서를 바꿨다. 다만 자주 호출 안 되는 코드(모드 전환 시 한 번뿐인 StartTransition()의 null 체크 등)까지 전부 캐싱하지는 않았다 — 경고가 뜬다고 다 고칠 필요는 없고, 매 프레임 도는 코드인지가 기준이었다.
정리
| 기능 | 핵심 API / 구조 |
|---|---|
| 1인칭 시작 위치 | RoomBuilder.GetFirstPersonSpawnPoint() — 삼각형 무게중심 |
| WASD 이동 + 마우스룩 | CameraController.HandleFirstPersonInput() |
| 3단 시점 순환 | ToggleView() — TopView → FreeView → FirstPerson |
| 가구 상호작용 차단 | FurnitureInteraction.Update() 모드 가드 + DeselectAll() |
| 벽 충돌 | CharacterController.Move() (가구는 트리거라 통과) |
| 다음 단계 | Phase 6-2 모바일 배포 (미정), WebGL 한글 입력 문제 |
'1인 게임개발 활동 > 가구배치 시뮬레이터' 카테고리의 다른 글
| RoomCraft #21. 저장 목록에 미리보기 넣기 - 레이아웃 비교의 첫걸음 (0) | 2026.07.10 |
|---|---|
| RoomCraft #20. 가구도 즐겨찾기가 되나요 - 프리셋 시스템 (0) | 2026.07.10 |
| RoomCraft #18. 첫 배포 — WebGL + GitHub Pages, 그리고 아직 못 고친 한글 버그 (0) | 2026.07.08 |
| RoomCraft #17. 매 프레임 도는 코드 최적화하기 — 그리고 최적화하다 만든 버그 (0) | 2026.07.08 |
| RoomCraft #16. 방 모양 편집을 일반/커스텀 탭으로 - 그리고 버그 네 마리 (0) | 2026.07.07 |