wan의 개발일기
RoomCraft #20. 가구도 즐겨찾기가 되나요 - 프리셋 시스템 본문
소스코드 → github.com/Jywan/RoomCraft
플레이 → jywan.github.io/RoomCraft
들어가며
지난 편에서 README에 적혀만 있고 실제론 없던 1인칭 뷰를 발견해서 만들었는데, 같은 방식으로 다시 훑어보니 하나 더 나왔다. "저장/관리: 여러 레이아웃 비교, 가구 목록 재사용"이라고 적혀있는데, 프로젝트 저장/불러오기는 되지만 "레이아웃 비교"도 "가구 목록 재사용"도 실제로는 없었다. 레이아웃 비교는 여러 방을 동시에 띄우거나 스크린샷을 나란히 보여줘야 해서 작업이 커서 보류하고, 가구 재사용 쪽부터 만들었다.
프로젝트와 무관한 저장소
기존 저장 시스템(ProjectManager)은 방 하나 + 그 안의 가구들을 한 세트로 묶어서 저장한다. 근데 가구 프리셋은 성격이 다르다 — "이 침대 디자인이 마음에 들어서 다른 방 만들 때도 쓰고 싶다"는 요구라, 특정 프로젝트에 종속되면 안 된다. 그래서 완전히 별도의 전역 저장 파일을 하나 뒀다.
// FurniturePresetManager.cs
public class FurniturePresetManager : MonoBehaviour
{
private string presetFilePath;
private void Awake()
{
presetFilePath = Path.Combine(Application.persistentDataPath, "FurniturePresets.json");
}
public void SavePreset(FurnitureData data)
{
FurniturePresetList list = LoadAll();
list.presets.Add(data);
File.WriteAllText(presetFilePath, JsonUtility.ToJson(list, true));
}
}
JsonUtility는 최상위 List를 바로 직렬화하지 못해서, FurnitureData 리스트 하나를 감싸는 FurniturePresetList 래퍼 클래스를 따로 만들었다. 이미 ProjectManager가 ProjectData를 이런 식으로 감싸서 저장하고 있어서, 그대로 패턴만 재사용했다.
생성 폼 재활용하기
가구 생성 팝업(FurnitureCreatorUI)은 원래 카테고리/이름/치수/색상을 입력받아서 FurnitureData를 만들고 바로 방에 배치하는 용도였다. 이 폼을 그대로 재활용해서, "생성"이랑 "프리셋으로 저장" 두 버튼이 같은 입력값을 서로 다른 곳으로 보내게 만들었다.
// 기존엔 OnCreateClicked() 안에 있던 파싱 로직을 헬퍼로 분리
private FurnitureData BuildFurnitureDataFromForm()
{
// 이름/치수/카테고리/색상 파싱 (기존 로직 그대로)
...
return data;
}
private void OnCreateClicked()
{
FurnitureData data = BuildFurnitureDataFromForm();
furnitureInteraction.CreateFurniture(data);
panel.SetActive(false);
}
private void OnSavePresetClicked()
{
FurnitureData data = BuildFurnitureDataFromForm();
presetManager.SavePreset(data);
RefreshPresetDropdown();
}
불러오는 쪽은 드롭다운 하나 추가했다. 프리셋을 고르면 그 값들이 폼 필드에 그대로 채워지고, 이후 "생성"을 누르면 그 값 그대로 가구가 만들어진다.
저장은 됐는데 티가 안 남
처음 만들고 테스트해보니, "프리셋으로 저장" 버튼을 눌러도 실제로는 잘 저장되는데 화면상 아무 반응이 없었다. 드롭다운을 직접 열어봐야만 저장된 걸 확인할 수 있는 상태 — 사용자 입장에선 버튼이 눌렸는지조차 알 수 없다.
버튼 텍스트를 저장 직후 잠깐 바꿔서 눈에 보이는 피드백을 줬다.
private void OnSavePresetClicked()
{
FurnitureData data = BuildFurnitureDataFromForm();
presetManager.SavePreset(data);
RefreshPresetDropdown();
presetDropdown.SetValueWithoutNotify(currentPresets.Count); // 방금 저장한 항목을 선택 표시
savePresetButtonLabel.text = "저장됨!";
Invoke(nameof(ResetSavePresetButtonLabel), 1f);
}
버튼 텍스트가 "저장됨!"으로 잠깐 바뀌었다가 1초 뒤 원래대로 돌아오고, 드롭다운도 방금 저장한 항목을 자동으로 선택한 상태로 바뀐다. 둘 다 딱히 무겁지 않은 처리라 Invoke 하나로 충분했다.
삽질 기록
라이더 경고, 다 고칠 필요는 없더라
이번 편을 진행하면서 라이더가 계속 "'null'과의 비교는 비용이 높습니다"를 띄웠는데, 하나씩 따라가다 보니 두 부류로 나뉘었다.
진짜 고쳐야 하는 것
:
CameraController.HandleFirstPersonInput()의
characterController != null,
FurnitureInteraction.Update()의
cameraController != null — 둘 다 가드 없이 매 프레임 그대로 실행되는 코드였다. hasCharacterController/hasCameraController bool로 캐싱했다.
안 고쳐도 되는 것
:
HandleSelection() 안의 clicked == null, StartTransition()의 roomBuilder != null 같은 것들. 전부 Input.GetMouseButtonDown이나 모드 전환처럼 "가끔만 실행되는" 코드 안에 있어서, 캐싱해도 체감 이득이 없었다. clicked처럼 애초에 클릭마다 값이 달라지는 것들은 캐싱 자체가 불가능하기도 했다.
기준을 하나로 정리했다: 라이더는 코드가 실제로 매 프레임 도는지 모르고 정적으로만 판단해서 경고를 띄운다. "이 코드가 가드 없이 Update()에서 매 프레임 무조건 실행되는가"만 확인하면 고칠 가치가 있는지 바로 판단된다.
내가 낸 버그 하나 더
예전에 GridSnap을 매 프레임 FindAnyObjectByType로 다시 찾던 걸 캐싱하자고 제안했었는데, 이번에 다시 보니 Start()에 대입문(gridSnap = FindAnyObjectByType<GridSnap>();)만 있고 정작 private GridSnap gridSnap; 필드 선언이 없어서 컴파일 에러가 나 있었다. HandleDrag()도 여전히 예전 방식(매번 새로 찾기) 그대로였고. 필드 선언 추가하고 HandleDrag()가 캐싱된 필드를 쓰도록 마무리했다.
정리
| 기능 | 핵심 API / 구조 |
|---|---|
| 전역 가구 프리셋 저장 | FurniturePresetManager + FurniturePresetList (JsonUtility 래퍼) |
| 생성 폼 재사용 | BuildFurnitureDataFromForm() 공통 헬퍼 |
| 저장 피드백 | 버튼 텍스트 일시 변경 + 드롭다운 자동 선택 |
| Rider 캐싱 완료 | hasCharacterController, hasCameraController, GridSnap 캐싱 마무리 |
| 다음 단계 | 레이아웃 비교 뷰(보류), Phase 6-2 모바일 배포(미정) |
'1인 게임개발 활동 > 가구배치 시뮬레이터' 카테고리의 다른 글
| RoomCraft #22. Undo/Redo, 복제, 벽 스냅 - 세 기능 한 번에 넣고 본 버그들 (0) | 2026.07.11 |
|---|---|
| RoomCraft #21. 저장 목록에 미리보기 넣기 - 레이아웃 비교의 첫걸음 (0) | 2026.07.10 |
| RoomCraft #19. 있는 척했던 기능 만들기 — 1인칭 시점 (0) | 2026.07.09 |
| RoomCraft #18. 첫 배포 — WebGL + GitHub Pages, 그리고 아직 못 고친 한글 버그 (0) | 2026.07.08 |
| RoomCraft #17. 매 프레임 도는 코드 최적화하기 — 그리고 최적화하다 만든 버그 (0) | 2026.07.08 |