wan의 개발일기
RoomCraft #16. 방 모양 편집을 일반/커스텀 탭으로 - 그리고 버그 네 마리 본문
소스코드 → github.com/Jywan/RoomCraft
들어가며
지난 편에서 사각형/L자형 같은 프리셋 방 모양을 골라 쓸 수 있게 됐다. 근데 프리셋만으론 부족하다 — 어차피 사람마다 방 구조는 다 다르게 생겼으니까. 이번 편은 사용자가 원하는 모양을 직접 그리는 기능을 붙였는데, 이번 Phase는 유독 설계를 두 번이나 갈아엎었다. 그 과정이랑 최종적으로 잡은 버그 넷을 정리한다.
그리드 셀이냐, 점 클릭이냐
처음 떠올린 방식은 엑셀 표 만들듯이 N×M 그리드에서 원하는 칸을 켜고 끄는 거였다. 직관적이긴 한데, 만들다 보니 대각선으로만 맞닿은 두 칸을 어떻게 처리할지가 발목을 잡았다. 대각선 인접은 "붙어있다"고 봐야 할지 "떨어져있다"고 봐야 할지 애매하고, 그 경계선을 다각형 외곽선으로 뽑아내는 로직도 따로 필요했다.
그래서 방향을 바꿨다. 셀을 켜고 끄는 대신, 사용자가 꼭짓점을 순서대로 직접 클릭하게 하면 애초에 "외곽선을 추출하는" 단계 자체가 사라진다. 사용자가 찍은 점이 곧 꼭짓점이라 대각선 인접 문제도, 자기교차 방지도 훨씬 단순해진다. 대신 그리드 셀 토글용으로 만들어뒀던 CustomShapeEditorUI.cs, GridOutlineExtractor.cs는 그대로 미사용 코드로 남았다 (정리는 다음 과제).
점을 찍어서 방을 그린다
가로/세로를 입력하면 그 범위 안에 점 그리드가 깔린다. 점을 순서대로 클릭하면 이전 점과 선으로 이어지고, 3개 이상 찍은 다음 시작점 근처를 다시 클릭하면 도형이 닫힌다.
private void OnPointClicked(Vector2 pos)
{
if (isClosed) return;
if (clickedPoints.Count >= 3 && Vector2.Distance(pos, clickedPoints[0]) < snapCloseRadius)
{
DrawLine(clickedPoints[clickedPoints.Count - 1], clickedPoints[0]);
isClosed = true;
return;
}
if (clickedPoints.Count > 0)
DrawLine(clickedPoints[clickedPoints.Count - 1], pos);
clickedPoints.Add(pos);
}
점 간격은 처음엔 고정 픽셀값으로 계산했는데, 방 크기를 크게 입력하면 그리드 전체가 그리는 영역 밖으로 삐져나가는 문제가 있었다. 영역 크기에 맞춰 간격이 알아서 줄어들도록 고쳤다.
// 그리드가 GridArea보다 커지지 않도록 점 간격을 자동으로 축소
effectiveCellPx = cellPixelSize;
if (cols > 1)
effectiveCellPx = Mathf.Min(effectiveCellPx, gridArea.rect.width / (cols - 1));
if (rows > 1)
effectiveCellPx = Mathf.Min(effectiveCellPx, gridArea.rect.height / (rows - 1));
점을 실제 좌표(미터)로 변환하는 쪽도 똑같은 간격값을 써야 한다는 게 포인트였다. 그리드는 동적 간격으로 그려놓고 변환은 고정값을 쓰면 방 모양이 미묘하게 어긋난다 — 그려진 그대로의 비율을 유지하려면 변환식도 같은 기준을 따라가야 한다.
탭으로 통합하기
처음엔 "커스텀 방 만들기"를 메인 화면의 독립 버튼으로, 그리기 패널도 완전히 별개 팝업으로 만들었다. 그런데 애초 요청은 "새 프로젝트" 패널 안에서 일반 / 커스텀 탭으로 전환하는 구조였다 — 만드는 중에 그 방향을 놓쳤다가 다시 확인하고 구조를 되돌렸다.
탭 버튼 2개를 두고, 기존 입력 필드들은 NormalTabContent라는 컨테이너로 묶어서 한 번에 켜고 끌 수 있게 했다. 그리기 패널(CustomRoomPanel)은 별개 오브젝트로 두고 탭 전환 시 둘 중 하나만 보이도록 스크립트로 제어했다.
private void ShowNormalTab()
{
normalTabContent.SetActive(true);
customRoomPanel.ClosePanelExternally();
}
private void ShowCustomTab()
{
normalTabContent.SetActive(false);
customRoomPanel.OpenPanel();
customRoomPanel.transform.SetAsLastSibling();
}
CustomRoomPanel은 원래 자기 완결적인 독립 패널로 설계해서 취소 버튼을 누르면 자기 자신만 닫히게 되어 있었다. 근데 탭 구조로 들어오면서 "커스텀 탭에서 취소를 누르면 새 프로젝트 패널 전체가 닫혀야" 자연스럽다. CustomRoomPanel 내부 로직은 그대로 두고, 버튼 레퍼런스만 밖으로 노출해서 StartSceneUI가 리스너 하나를 더 얹는 방식으로 풀었다.
// CustomRoomPanel.cs — 취소 버튼을 밖에서도 구독할 수 있게 노출
public Button CancelButton => cancelButton;
// StartSceneUI.cs — 새 프로젝트 패널을 통째로 닫는 리스너를 추가로 등록
customRoomPanel.CancelButton.onClick.AddListener(CloseNewProjectPanel);
오목한 모양도 그대로
제일 확인해보고 싶었던 건 오목한(볼록하지 않은) 모양도 제대로 방이 되는지였다. 지난 편의 L자형은 미리 정해둔 6꼭짓점이었지만, 이번엔 사용자가 아무 모양이나 찍을 수 있으니 오목한 도형이 나오는 게 당연하다.
이 좌표를 그대로 EditorScene으로 넘겨서 방을 생성해봤다. Phase 5-1에서 만들어둔 Ear Clipping 삼각분할이 오목한 다각형도 처리하도록 짜여 있어서, 별도 예외 처리 없이 바로 파인 모양 그대로 방이 나왔다. 미리 구조를 다각형 기반으로 잡아둔 보람이 여기서도 나왔다.
삽질 기록
또 그 Layout Group
탭 구조로 바꾸면서 기존 입력 필드들을 NormalTabContent로 옮겼는데, NewProjectPanel에 예전부터 붙어있던 Vertical Layout Group + Content Size Fitter가 새 자식의 선호 크기를 0으로 계산해버렸다. NormalTabContent는 Layout Group이 없는 그냥 빈 컨테이너라 부모한테 보고할 크기가 없었던 것 — 결과적으로 패널 높이가 0으로 찌부러졌다.
이 패턴 벌써 몇 번째인지 모르겠다. Layout Group 자식 구성이 바뀔 때마다 똑같은 이유로 똑같이 터진다. 이번엔 애초에 필요 없어진 두 컴포넌트를 통째로 떼어내고 크기를 수동으로 되돌렸다.
첫 클릭은 무시당한다
커스텀 탭을 처음 열 때, 첫 클릭은 아무 반응이 없고 두 번째 클릭에야 패널이 떴다. 에러도 안 나서 한참 헤맸는데, 원인은 Unity의 Awake() 생명주기였다.
CustomRoomPanel 오브젝트를 씬에 비활성 상태로 저장해뒀었는데, 비활성 오브젝트는 Awake()가 호출되지 않는다. 첫 클릭에서 SetActive(true)를 부르는 순간이 바로 그 오브젝트가 태어나서 처음 활성화되는 시점이라, Unity가 그 자리에서 즉시 Awake()를 실행한다. 근데 그 Awake() 마지막 줄이 하필 panel.SetActive(false) — 방금 켠 걸 자기가 그 자리에서 바로 꺼버리는 거였다. 두 번째 클릭부턴 Awake()가 이미 실행된 뒤라 정상 동작했다.
해결: CustomRoomPanel을 씬에서 기본 활성화 상태로 저장. NewProjectPanel/LoadPanel도 원래 이렇게 활성 상태로 시작해서 런타임에 스스로 숨기는 방식이었는데, 유독 이 오브젝트만 미리 꺼둔 게 문제였다.
뒤에서 비쳐 보이는 메인 화면
커스텀 탭을 열면 뒤에 있던 타이틀 화면이랑 탭 버튼이 은은하게 비쳐 보였다. 그리는 순서(sibling order)와 배경 알파, 두 가지가 겹친 문제였다 — CustomRoomPanel이 형제 오브젝트들보다 먼저 그려지고 있었고, 배경 알파도 완전 불투명이 아니었다.
해결: 탭을 열 때 SetAsLastSibling()으로 맨 앞에 그리게 하고, 배경 알파를 1로 올렸다.
버튼이 패널 밖에 있었다
되돌리기/초기화/확인/취소 버튼을 처음 배치할 때 좌표를 감으로 넣었더니, 패널 높이(600)를 훌쩍 넘는 위치(-520, -560)에 놓여 있었다. 그리드 영역도 패널 경계를 살짝 벗어나 있었고. 결과적으로 버튼 4개가 화면에 아예 안 보이는 상태였다.
해결: 그리드 영역과 버튼들을 패널 안쪽 좌표로 재배치하고, 패널 자체 높이도 내용물에 맞춰 키웠다. 겸사겸사 그리드 영역을 패널 상단에 크게 두고 입력창/버튼을 그 아래로 내려서, 그리는 공간이 잘 보이게 레이아웃도 다시 짰다.
정리
| 기능 | 핵심 API / 구조 |
|---|---|
| 설계 방식 결정 | 그리드 셀 토글 방식 폐기 → 점 클릭 방식 채택 (대각선 인접 문제 회피) |
| 점 클릭으로 다각형 그리기 | CustomRoomPanel.OnPointClicked() + DrawLine() |
| 그리드 오버플로우 방지 | effectiveCellPx — 영역 크기에 맞춰 간격 동적 계산 |
| 일반/커스텀 탭 전환 | StartSceneUI.ShowNormalTab() / ShowCustomTab() |
| 오목한 커스텀 방 생성 | 기존 Ear Clipping 삼각분할 재사용, 별도 처리 불필요 |
| 다음 단계 | 미사용 잔재 코드(그리드 셀 클리커 흔적) 정리, 최적화 작업 |
'1인 게임개발 활동 > 가구배치 시뮬레이터' 카테고리의 다른 글
| RoomCraft #18. 첫 배포 — WebGL + GitHub Pages, 그리고 아직 못 고친 한글 버그 (0) | 2026.07.08 |
|---|---|
| RoomCraft #17. 매 프레임 도는 코드 최적화하기 — 그리고 최적화하다 만든 버그 (0) | 2026.07.08 |
| RoomCraft #15. 방 모양 프리셋 — 사각형 넘어 L자형까지 (0) | 2026.07.05 |
| RoomCraft #14. 방을 다각형으로 - 사각형을 넘어서기 위한 첫걸음 (0) | 2026.07.05 |
| RoomCraft #13. 버그 두 개 잡고 가구 정보 패널 통합하기 (0) | 2026.07.05 |