2D 로그라이크 전체맵 UI 레퍼런스 및 구현 방법
📑 목차
⓵ 로그라이크 전체맵 UI 레퍼런스
𝟙 상대적 좌표형 방식
- 세피리아 | 엔터 더 건전
- 아이작의 번제 | Dead Cells
- Hades | Slay the Spire
⓶ 전체맵 구현하기
𝟙 상황 요약 | 수정 방향
𝟚 수정 코드
𝟛 중간 점검
𝟜 원인 진단 | 해결 방법
- 1차
- 2차
𝟝 결과물
𝟞 알게된 점
- 람다식
- FirstOfDefault | Where | Select
- LINQ
𝟟 회고
▪️로그라이크 전체맵 UI 레퍼런스
1. 상대적 좌표형 방식 | 세피리아, 엔터 더 건전
- 플레이어 방이 항상 중앙: 전체맵은 보여주되, 내가 있는 방을 기준으로 다른 방들이 상대적으로 위치
- 방은 전부 동일한 크기(고정 단위)
- ex: 작은방 1x1, 보스/특수방 2x2
- 실제 방 크기/모양 무시, 기호적으로만 표현(UX에 집중)
- 미발견 방은 보이지 않음
- 또는 '뿌연 실루엣'으로 힌트만
전체 방 개수/모양이 미리 드러나지 않음
▫️장점
- 직관적, 플레이어의 "진행 상황" 파악 용이
- 탐험 재미를 유지하면서, 레이아웃 구조는 간단하게 전달
- 맵 자동 스케일링 신경 쓸 필요 없음(그냥 고정 단위 UI로 배치)
▫️단점
- 실제 맵 구조(방 크기/비율)가 다양하면 현실감 부족
- 방 크기 의미가 약화
▫️셸 그리드 방식과의 차이점
- UI에서 방은 "아이콘 단위로만 표현 (방의 실제 크기, 모양, 위치와 상관없이 셀 하나씩만 배치)
- 실제 방 구조: 데이터에서는 월드 좌표/크기/타일맵 구조가 제각각이어도,
맵중앙이 플레이어가 위치한 방이고, 나머지 방들은 그냥 상대 오프셋으로만 배치함
(플레이어방 (0,0), 위방 (0,1), 왼쪽방 (-1,0) 이런 느낌) - 방의 크기(면적, 길이 등) 무시하고,
작은방=100x100, 큰방/보스방=200x200 과 같게 아주 단순화해서 표시 - UI에 보이는 방 개수/위치, 미리보기, 전체구조가 최대한 감춰짐
- 실제 맵(월드)과 UI(전체맵)는 완전히 별개의 단순한 2D 배열 느낌
▫️정리
- 방=아이콘, 배치는 고정셀 방식
- 월드 구조랑 1:1 매핑X, 그냥 맵 탐험 흐름만 UX적으로 직관화
- 탐험 중심의 로그라이크(진행구조 중요, 방 구조 디테일 중요X)에 최적
▫️구현 방법
- RoomIcon 프리팹의 크기 = 고정값(예: 100x100)
- 큰방, 특수방만 2x2 같은 약간의 변형은 가능
- RefreshMap 시 플레이어가 있는 방을 항상 패널 중앙에 배치
- panel 중앙 좌표(0,0) 기준으로 상대좌표 계산
- 나머지 방들은 플레이어 방에서 "상대적 위치만큼" 좌표 오프셋 적용해서 배치
- 아직 방문하지 않은 방(미발견)은 안 그리거나, 흐릿하게 처리
- 전체맵 패널 사이즈는 고정 (1150x860)
2. 셸 그리드 방식 | 아이작의 번제 (Isaac), Dead Cells
- 각 방은 정해진 최소 단위 셀(Grid)로 표현
- 방마다 다양한 크기/모양이지만, UI에서는 '정사각형' or '타일셋'으로만 배치
- 플레이어 위치를 중심으로 화면을 스크롤/클램프하거나, 항상 플레이어 주변만 보여줌
- 맵 전체는 미리 안 보임. 방문 방만 노출
▫️장점
- 직관적으로 각 방의 상대적 위치 파악 용이
- 퍼즐 요소/길찾기에서 구조 이해가 쉬움
▫️단점
- 방이 아주 크거나 특이하게 생긴 경우, 단일 셀로 표현 시 한계
- UI 연동에 GridIndex 같은 시스템 필요
▫️상대적 좌표형 방식과의 차이점
- 방 배치는 실제 데이터상의 Grid/Cell 정보에 맞춰서 배치
- 예시: 방A=(2,4), 방B=(3,4)이면 실제로도 (2,4), (3,4)만큼 띄워서 배치
- 한 셀에 한 방만 (=정사각형 grid로만 표현)
- 방 구조 데이터: 이미 각 방마다 GridIndex/Cell좌표(Vector2Int)가 있음
→ UI에 바로 곱해서 상대좌표로 변환해서 배치
(플레이어방이 중앙에 올 수도, 아닐 수도 있음. 대부분 미니맵에서만 센터에 오도록 오프셋 줌) - 방 크기 무시(전부 1x1 사각형)
- 전체구조는 항상 정해진 Grid 상에서만 표현
▫️정리
- 방=1x1, 정사각 셀
- 월드 구조 = UI 구조 거의 1:1 매핑
- Grid 기반 맵, 구조 퍼즐(경로찾기/분기 등)이 중요한 로그라이크에 최적
▫️구현 방법
- 방 정보를 2D Grid(예: Vector2Int Cell)로 관리
- 방마다 Grid상에서의 x/y만큼 고정간격 곱해서 배치
- ex) anchoredPosition = (room.Cell.x - playerRoom.Cell.x) * cellSize
- 크기는 항상 cellSize(100x100 등) 고정
- 플레이어 방이 중앙(0,0)에 오도록 Offset
▫️상대적 좌표형 vs 셸 그리드 방식
UI 배치 기준 | 플레이어 방 중심, 상대적인 좌표 | 각 방의 GridIndex로 표현, 절대적인 위치 |
방 크기 | 고정셀/아이콘 (특수방만 2x2 등 간단 변형) | 전부 1x1 셀(정사각형) |
월드 구조와 일치도 | 거의 없음 (UX 중심) | 거의 1:1 대응 (Grid 구조) |
미탐색 방 | 보통 숨김, 실루엣 | 숨김/표시 모두 가능 |
중앙 기준 | 플레이어가 위치한 방이 항상 중앙 | UI 설정에 따라 다름 |
3. 아이콘 표시형 | Hades, Slay the Spire, 메이플스토리 아케인 리버
- 방마다 아이콘/심볼만, 경로(선)만 표시
- 방 크기는 완전히 무시하고 "아이콘" 단위
- 경로만 연결(선 그래프 느낌)
- 방 유형(상점, 보스, 이벤트, 보너스 등)별로 아이콘만 다름
- 전체 레이아웃이 덜 중요, '트리형/네트워크형' 시각화
▫️장점
- 정보 최소화, 시각적 혼란 ↓
- 직관적으로 "다음 선택지"에만 집중 가능
▫️단점
- 공간감/방 구조 표현 거의 없음
- 동선 파악/탐험 감각 약화
▫️구현 방법
- 방마다 심볼 아이콘만 배치, 크기는 모두 동일
- 선으로만 연결
- 이 방식은 던전 구조가 단순/비선형일 때(트리형 동선) 더 적합
이 외에도 다음과 같은 메트로메니아 방식이 있지만,
- 진짜 월드 좌표/크기 비율 그대로 Minimap에 맵핑
- 탐색/지도/퍼즐 감각↑
- UI 관리와 스케일링 복잡
로그라이크는 기본적으로 방 수가 많지 않기 때문에
- 세피리아와 같이:
- 플레이어 방 중심 고정, 고정 크기 셀, 전체맵 스케일 신경 안씀, 미발견방 숨김 or 흐림
- 구현/유지/UX 모두 쉽고, 로그라이크와 가장 잘 맞음(세피리아, 건전, 이삭 등)
- 크기 비율 기반 표현은 오픈월드/메트로베니아나 전체 탐색 기반에 적합하고
로그라이크는 정보제한+핵심만 보여주는 게 정답인 듯 하다.
▪️전체맵 구현하기
현재 상황
▫️기존 코드
public void RefreshMap()
{
foreach (var icon in _roomIcons) Destroy(icon);
_roomIcons.Clear();
var state = floorManager.State;
if (state == null) return;
var totalBounds = state.TotalBounds;
Vector2 min = totalBounds.min;
Vector2 mapSize = totalBounds.size;
float scaleX = panel.rect.width / mapSize.x;
float scaleY = panel.rect.height / mapSize.y;
float centerX = panel.rect.width * 0.5f;
float centerY = panel.rect.height * 0.5f;
foreach (var room in state.TotalRooms)
{
if (!room.IsDiscovered) continue;
Vector2 roomPos = (Vector2)room.RoomBounds.center - min;
float uiX = roomPos.x * scaleX;
float uiY = roomPos.y * scaleY;
Vector2 roomSize = room.RoomBounds.size;
float uiW = roomSize.x * scaleX;
float uiH = roomSize.y * scaleY;
var icon = Instantiate(roomIconPrefab, panel);
var rt = icon.GetComponent<RectTransform>();
rt.anchoredPosition = new Vector2(uiX - centerX, uiY - centerY);
rt.sizeDelta = new Vector2(uiW, uiH);
// Button Test
var roomCopy = room;
icon.GetComponent<Button>()?.onClick.AddListener(() => {
Debug.Log("Room Button Clicked!");
OnClickRoom(roomCopy);
});
_roomIcons.Add(icon);
}
}
▪️수정 방향
1. 플레이어가 있는 방을 찾기
- HasPlayer (혹은 Player가 해당 방에 있음을 판별하는 조건)이 있는 방을 찾음
2. 플레이어 방을 기준으로 offset 계산
- 각 room의 Cell - playerRoom.Cell로 UI에서의 상대 좌표를 구함
3. panel 중앙 기준으로 UI 배치
- panel 중심에 플레이어가 오게 하고, 나머지 방들도 상대적으로 배치
▫️플레이어 방 기준 중심으로 배치한 수정 코드
public void RefreshMap()
{
foreach (var icon in _roomIcons) Destroy(icon);
_roomIcons.Clear();
var state = floorManager.State;
if (state == null) return;
// 플레이어가 있는 방 찾기
var playerRoom = state.TotalRooms.FirstOrDefault(room => room.HasPlayer);
if (playerRoom == null)
{
Logger.Log("플레이어 방을 찾을 수 없음!");
return;
}
Vector2Int playerCell = playerRoom.Cell;
// UI 중앙 위치 계산
float centerX = panel.rect.width * 0.5f;
float centerY = panel.rect.height * 0.5f;
// 방 크기(고정셸)
float roomUISize = 50f;
foreach (var room in state.TotalRooms)
{
if (!room.IsDiscovered) continue;
// 상대적 좌표로 배치 (플레이어 기준 offset)
Vector2Int offset = room.Cell - playerCell;
float uiX = offset.x * roomUISize;
float uiY = offset.y * roomUISize;
var icon = Instantiate(roomIconPrefab, panel);
var rt = icon.GetComponent<RectTransform>();
rt.anchoredPosition = new Vector2(uiX, uiY);
rt.sizeDelta = new Vector2(roomUISize, roomUISize);
// Button Test
var roomCopy = room;
icon.GetComponent<Button>()?.onClick.AddListener(() => {
Debug.Log("Room Button Clicked!");
OnClickRoom(roomCopy);
});
_roomIcons.Add(icon);
}
}
- 플레이어가 중앙:
playerCell이 (0,0)이 되고, 나머지는 offset만큼 이동 (즉, 현재 방이 항상 패널 중심) - roomUISize: 모든 방은 같은 크기
(보스방/특수방만 2배, 등은 필요에 따라 sizeDelta만 바꿔주면 됨) - panel 중심 기준: RectTransform이 (0,0)이 패널 중심이 되도록
(pivot/anchor가 0.5, 0.5로 되어 있어야 함) - FirstIrDefault() 사용 위해 Linq 사용 시도
▫️BaseRoom에 HasPlayer 필드 추가
public bool IsDiscovered { get; private set; }
[field: SerializeField] public bool IsCleared { get; private set; }
public bool HasPlayer
{
get
{
var player = FindObjectOfType<PlayerController>();
if (player == null) return false;
return RoomBounds.Contains(player.transform.position);
}
}
// ...
-> 플레이어가 방에 있는지 확인 가능!
▪️중간 점검
- 기존 전체맵에서 월드 위치 기반 → 플레이어 기준 그리드 배치로 바뀜
- 코드의 대부분은 RefreshMap()만 수정하면 됨
추가로 PlayerController에 필드를 추가할 수도 있음
public BaseRoom CurrentRoom { get; set; }
언제?
플레이어가 현재 어느 방에 있는 지를 플레이어 쪽에서 기록하고 싶을 때
▫️점검
- 그리드 형식은 적용 완료
- 문제!
방들이 중앙에 겹쳐서 나타남!
크기 적용 안됨!
▫️문제 상황 요약
- 방 아이콘(RoomIcon)이 항상 (0,0)에 겹쳐서 생성됨
- 방이 중앙에 겹쳐서 여러 개가 1개만 보이는 것처럼 보임
- roomUISize = 50f; 값을 줬는데도, 프리팹 Image가 200x200으로 계속 나옴
- 분명히 코드에서 50f로 sizeDelta를 설정했지만, 인스펙터상으로는 200x200이 됨
▪️원인 진단
1. 방 겹침/중앙 고정 오류
- Cell 정보가 제대로 다르지 않거나, offset 계산이 안 됨
- 혹은 RoomIcon 프리팹의 Anchor/Pivot이 모두 (0.5, 0.5) 중앙정렬이 아닌 상태
- room.Cell 값이 모두 동일(0,0) 이거나, 코드상에서 Cell 좌표를 안 쓰고 있어서 중앙 고정만 됨
2. sizeDelta 적용이 안 됨
- RoomIcon 프리팹 내부에 Image가 여러 개라면, 자식 Image만 크기가 커짐
- 혹은 Instantiate한 후에 rt.sizeDelta를 바꿔도 내부 Image 크기가 프리팹 기본값(200)으로 유지될 수 있음
▫️예상 해결 방법
1. Room의 Cell 값 확인
- BaseRoom의 Cell 값을 반드시 각 방마다 다르게 할당해야 함
→ Procedural 생성/RoomManager에서 생성할 때 Cell값을 방마다 고유하게 설정
2. RoomIcon 프리팹 정리
- Image
- Anchor/Pivot 모두 (0.5,0.5)
- Stretch/Fill 사용하지 말고, TopLeft/Center로 고정
- RectTransform의 Width/Height는 100,100 등 기본값 아무거나
→ 코드에서 icon.GetComponent<RectTransform>().sizeDelta = new Vector2(roomUISize, roomUISize);
로 수정하면 RoomIcon 전체(=Image 포함) 크기가 변경됨
시도했으나 해결 안됨!
▫️문제
- 플레이어가 이동하면 플레이어가 현재 위치한 방을 중앙으로 두고
이전 방은 위치를 옮겨야하는데 업데이트가 안됨
▫️문제 코드
public void RefreshMap()
{
foreach (var icon in _roomIcons) Destroy(icon);
_roomIcons.Clear();
var state = floorManager.State;
if (state == null) return;
// UI 중앙 위치 계산
float centerX = panel.rect.width * 0.5f;
float centerY = panel.rect.height * 0.5f;
// 방 크기(고정셸)
float roomUISize = 50f;
var playerRoom = state.TotalRooms.FirstOrDefault(r => r.HasPlayer);
if (playerRoom == null)
{
Logger.Log("플레이어 방을 찾을 수 없음!");
return;
}
Vector2Int playerCell = playerRoom.Cell;
foreach (var room in state.TotalRooms)
{
if (!room.IsDiscovered) continue;
// 상대적 좌표로 배치 (플레이어 기준 offset)
Vector2Int offset = room.Cell - playerCell;
float uiX = offset.x * roomUISize;
float uiY = offset.y * roomUISize;
var icon = Instantiate(roomIconPrefab, panel);
var rt = icon.GetComponent<RectTransform>();
rt.anchoredPosition = new Vector2(uiX, uiY);
rt.sizeDelta = new Vector2(roomUISize, roomUISize);
// Button Test
var roomCopy = room;
icon.GetComponent<Button>()?.onClick.AddListener(() => {
Debug.Log("Room Button Clicked!");
OnClickRoom(roomCopy);
});
_roomIcons.Add(icon);
}
}
▫️예상 원인
1. panel의 중심(0,0)이 실제로 화면 중심이 아닐 가능성
- anchoredPosition = new Vector2(uiX, uiY);
이렇게만 하면 (0,0) = panel의 Pivot 위치임. - 패널의 Pivot이 (0.5, 0.5)라면 (0,0)이 가운데가 되지만
실질적으로 계산된 위치가 패널의 중앙 기준이 아니라 좌측 상단 기준으로 들어갈 수도 있음
2. 좌표 계산 시 UI 중앙을 더해주지 않음
- 보통 anchoredPosition은 (0,0)이 패널의 Pivot 기준이야.
- 플레이어 기준 offset만큼 계산해도, 중앙에 위치시키려면 UI 중앙 좌표(centerX, centerY)를 더해줘야 함
▫️해결 과정
- offset (0, 0)이 panel의 Pivot 기준 (보통 중앙)이 아닌,
- RectTransform의 (0,0) (Pivot 위치)에 찍혀서
-> 방이 panel 중앙이 아니라 한쪽(좌측 하단 or 좌측 상단 등)으로 몰릴 수 있음
이번에는, 패널 중심 기준으로 좌표를 옮기는 공식을 적용해 보자.
float uiX = offset.x * roomUISize + centerX;
float uiY = offset.y * roomUISize + centerY;
▫️공식 해석
- offset.x/y : 플레이어 방을 기준으로 각 방이 얼마나 떨어져있는지(그리드 단위로)
- 예시 : offset.x = -2 → 플레이어 기준 왼쪽 2칸
- roomUISize : 방 하나의 UI상 픽셀 크기(ex: 50)
- centerX, centerY : 패널 UI 중심 좌표 (중앙 정렬을 위해)
결과적으로,
- uiX : 플레이어가 있는 방이 항상 중앙(centerX)에 오도록
나머지 방들은 플레이어 기준 상대좌표만큼 옆/위/아래에 그려짐
예시 :
- 플레이어방 offset: (0,0) → uiX, uiY = centerX, centerY (중앙)
- 오른쪽 방 offset: (1,0) → uiX = centerX + roomUISize
- 왼쪽 방 offset: (-1,0) → uiX = centerX - roomUISize
시도했으나 이번에는 맵이 중앙에서 벗어나버림
MapRoonIcon Prefab의 RectTransform을 바꾼 후,
public void RefreshMap()
{
foreach (var icon in _roomIcons) Destroy(icon);
_roomIcons.Clear();
var state = floorManager.State;
if (state == null) return;
float roomUISize = 50f;
// 1. 플레이어가 있는 방 찾기
var playerRoom = state.TotalRooms.FirstOrDefault(r => r.HasPlayer);
if (playerRoom == null) return;
Vector2Int playerCell = playerRoom.Cell;
// 2. 패널 중심 좌표
float centerX = panel.rect.width * 0.5f;
float centerY = panel.rect.height * 0.5f;
// 3. 각 방을 플레이어 기준 offset만큼 밀어서 배치
foreach (var room in state.TotalRooms)
{
if (!room.IsDiscovered) continue;
Vector2Int offset = room.Cell - playerCell;
float uiX = offset.x * roomUISize + centerX;
float uiY = offset.y * roomUISize + centerY;
var icon = Instantiate(roomIconPrefab, panel);
var rt = icon.GetComponent<RectTransform>();
rt.anchoredPosition = new Vector2(uiX, uiY);
rt.sizeDelta = new Vector2(roomUISize, roomUISize);
_roomIcons.Add(icon);
}
}
RefreshMap() Method 에서
아래의 부분을 50f -> 105f 로 수정하니 방이 겹쳐 보이는 문제가 해결되었다..
float roomUISize = 105f;
- Unity 내에서 방 크기가 아니라 각 방 사이의 간격으로 인식되고 있었다.
따라서, roomUISize -> roomCellSpacing 으로 변경함
이제 각 방으로 텔레포트를 타도록 구현해보자.
▫️EntiremapUI에 OnClickRoom() 추가
private void OnClickRoom(BaseRoom room)
{
var player = FindObjectOfType<PlayerController>();
if (player == null) return;
player.transform.position = room.RoomBounds.center;
Logger.Log($"텔레포트: {room.name} 방으로 이동!");
// 방 진입 처리(중복 방지)
if (room != player.FloorManager.CurEnteredRoom)
{
room.EnterRoom(player);
}
// 필요시 전체맵 닫기
gameObject.SetActive(false);
}
▫️PlayerController 에 참조 추가
public FloorManager FloorManager { get; private set; }
▫️FloorManager 할당
var player = Instantiate(playerPrefab, playerRoot);
player.transform.position = State.StartRoom.transform.position;
player.FloorManager = this;
했는데 클릭 시 텔레포트 안됨
예상가는 문제: 버튼 클릭했을 때 로그는 뜨기 때문에, 텔레포트 자체 문제
- 플레이어 위치가 이동은 하지만 방에 진입(EnterRoom)이 안 되거나
- RoomBounds.center 값이 이상하거나
- 텔레포트 후에 플레이어가 무언가에 막히는 상황
- 프리팹 경로 문제
▫️수정 코드
public void RefreshMap()
{
foreach (var icon in _roomIcons) Destroy(icon);
_roomIcons.Clear();
var state = floorManager.State;
if (state == null) return;
float roomCellSpacing = 105f;
var playerRoom = state.TotalRooms.FirstOrDefault(r => r.HasPlayer);
if (playerRoom == null) return;
Vector2Int playerCell = playerRoom.Cell;
float centerX = panel.rect.width * 0.5f;
float centerY = panel.rect.height * 0.5f;
foreach (var room in state.TotalRooms)
{
if (!room.IsDiscovered) continue;
Vector2Int offset = room.Cell - playerCell;
float uiX = offset.x * roomCellSpacing + centerX;
float uiY = offset.y * roomCellSpacing + centerY;
var icon = Instantiate(roomIconPrefab, panel);
var rt = icon.GetComponent<RectTransform>();
rt.anchoredPosition = new Vector2(uiX, uiY);
rt.sizeDelta = new Vector2(roomCellSpacing, roomCellSpacing);
var roomCopy = room;
on.GetComponentInChildren<Button>()?.onClick.AddListener(() => { OnClickRoom(roomCopy); });
Logger.Log("CLICK!");
_roomIcons.Add(icon);
}
}
▪️결과물
순간이동이 된다.
▫️기획 추가 조건 반영
- 전투 중 지도 탭/이동 금지
→ 지도/텔레포트 기능 호출 전에 if (isBattle) return; - 클리어 안된 방 이동 금지
→ if (!room.IsCleared) return; 처럼 OnClickRoom 안에 체크 추가
▫️큰방, 작은방 UI 크기 비율 다르게
float cellWidth = room.IsBig ? 210f : 105f; // 2배
float cellHeight = room.IsBig ? 210f : 105f;
rt.sizeDelta = new Vector2(cellWidth, cellHeight);
- 혹은 방 데이터에 실제 크기(예: (26,21), (18,13)) 비율을 미리 정해서 UI상에 가중치 곱해주면 됨
▪️알게된 점
var playerRoom = state.TotalRooms.FirstOrDefault(r => r.HasPlayer);
if (playerRoom == null) return;
Vector2Int playerCell = playerRoom.Cell;
state.TotalRooms.FirstOrDefault(r => r.HasPlayer);
- state.TotalRooms 리스트(혹은 배열)에서
-> HasPlayer가 true인 첫 번째 방을 찾아서 playerRoom 변수에 저장 - 만약 그런 방이 없으면?
→ playerRoom은 null이 된다.
if (playerRoom == null) return;
- 플레이어가 있는 방을 못 찾았으면 함수 종료.
Vector2Int playerCell = playerRoom.Cell;
- playerRoom의 Cell 좌표값(예: (x, y))을 playerCell 변수에 저장
-> 즉, 플레이어가 현재 위치한 방의 '그리드 좌표'를 가져온다는 뜻!
▫️람다식
r => r.HasPlayer
- r: 리스트의 각 요소(여기선 room, 즉 BaseRoom)를 r 로 받아옴
- => : 화살표 함수 | 람다 연산자
- r.HasPlayer : r(방) 객체의 HasPlayer 프로퍼티가 true인지 판별
즉, FirstOrDefault (r => r.HasPlayer)
→ state.TotalRooms에서 HasPlayer가 true인 방을 찾아달라는 뜻
▫️장점
- 가독성: 익명함수(한줄 함수)를 빠르게 전달 가능
- LINQ(컬렉션 쿼리): 리스트에서 조건 검색, 필터링, 정렬 등 편하게 쓸 수 있음
▫️기본 문법
매개변수 => 반환값 (or 실행문)
x => x * x // x의 제곱
room => room.IsCleared // 방이 클리어 상태인지
▫️FirstOrDefault, Where, Select 차이
(1) FirstOrDefault
- 조건에 맞는 첫 번째 값을 찾음.
- 없으면 null(참조형), default(값형) 반환
var room = rooms.FirstOrDefault(r => r.IsCleared);
// rooms 리스트에서 IsCleared==true인 첫 번째 room 반환 (없으면 null)
(2) Where
- 조건에 맞는 모든 값(여러개)을 리스트로 반환
var clearedRooms = rooms.Where(r => r.IsCleared);
// rooms 리스트에서 IsCleared==true인 모든 room만 필터링, 리스트로 반환
- 결과: IEnumerable<BaseRoom>
(3) Select
- 리스트의 모든 원소를 '가공'해서 새 값/리스트로 만듦 (변환)
- 맵핑(map)
var cellList = rooms.Select(r => r.Cell);
// rooms 리스트의 모든 room에서 .Cell만 꺼내서 새 리스트 만듦
- 결과: IEnumerable<Vector2Int> (좌표만 모은 리스트)
▫️리스트/딕셔너리에서 LINQ 직접 사용 예시
예시 List<>
var rooms = new List<BaseRoom> { ... };
(1) FirstOrDefault
var clearedRoom = rooms.FirstOrDefault(r => r.IsCleared);
// 조건 맞는 첫 번째 방 (없으면 null)
(2) Where
var clearedRooms = rooms.Where(r => r.IsCleared).ToList();
// 조건 맞는 모든 방 (리스트로 반환)
(3) Select
var roomCells = rooms.Select(r => r.Cell).ToList();
// 모든 방의 Cell 좌표만 뽑아서 리스트로 만듦
(4) 딕셔너리
var dict = new Dictionary<int, string>
{
{ 1, "Apple" }, { 2, "Banana" }, { 3, "Cat" }
};
var bananas = dict.Where(kv => kv.Value.Contains("Banana")).ToList();
// value에 "Banana"가 포함된 쌍만 리스트로
▪️회고
1. 느낀 점
이번 전체맵 UI 구현 과정은 그저 방 배치만 바꾸는 게 아니라,
게임 UX·탐험 감각을 어떻게 설계하고 보여줄지 고민하는 시간이었다.
- panel 중심 기준(anchoredPosition 계산)을 꼼꼼히 신경 써야 하며,
roomUISize와 cellSpacing 등 변수명 의미도 명확히 분리해야 의도대로 방이 예쁘게 나열된다. - 순간이동/텔레포트 기능에서 클릭 이벤트, 방 탐색 상태, 전투 중 이동 불가 등
추가 예외처리도 중요하다는 걸 체크했다.
- UI/월드 데이터의 분리:
실제 방 크기, 모양, 구조와 UI상의 단순화는
어디까지 추상화해야 유저가 불편하지 않은가? 라는 UX 고민의 연속이었다.
2. LINQ, 람다식 활용의 장점
- LINQ와 람다식을 활용하니 방 탐색(FirstOrDefault, Where, Select 등)이
코드 한 줄로 직관적이고 깔끔하게 처리되었다. - 예전에는 for/foreach문으로 “플레이어가 어디 방에 있는지”
직접 찾았던 것을 state.TotalRooms.FirstOrDefault(r => r.HasPlayer)
로 한 줄에 끝낼 수 있다는 게 인상적이었다. - 특히, 조건에 따라 리스트 필터링/변환/탐색이
대규모 로그라이크처럼 방이 많을 때 실무적으로도 훨씬 빠르고 안전하다는 점을 실감했다.
3. 다음 단계 | 보완하고 싶은 점
- 방 크기/보스방/특수방 등의 UI상 가변 크기 반영은
더 세밀한 예외처리와 테스트가 필요함을 느꼈다. - 전체맵 외에도 미탐색 방 실루엣 처리, 경로 애니메이션, 진입 불가 방 구분 등
추가적인 UX 개선 포인트가 남아있다. - 실전에서 플레이어가 이동할 때마다 전체맵 동적 리프레시가
성능에 영향을 주지 않는지, 추후 최적화 테스트도 꼭 해보고 싶다.