카테고리 없음

[최종프로젝트] 전체맵 UI - Unity 본캠프 62일차

julesnow 2025. 7. 9. 07:19

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; }

 

언제?
플레이어가 현재 어느 방에 있는 지를 플레이어 쪽에서 기록하고 싶을 때


▫️점검

  • 그리드 형식은 적용 완료
  • 문제!
    방들이 중앙에 겹쳐서 나타남!
    크기 적용 안됨!

▫️문제 상황 요약

  1. 방 아이콘(RoomIcon)이 항상 (0,0)에 겹쳐서 생성됨
    • 방이 중앙에 겹쳐서 여러 개가 1개만 보이는 것처럼 보임
  2. 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 개선 포인트가 남아있다.

  • 실전에서 플레이어가 이동할 때마다 전체맵 동적 리프레시
    성능에 영향을 주지 않는지, 추후 최적화 테스트도 꼭 해보고 싶다.