인스턴스 맵 만들기

# 학습 과정 소개

월드를 제작하다 보면 특정한 유저만 입장할 수 있는 특수 던전 같은 별도의 공간을 만들고 싶을 때가 있습니다. 이때 `RoomService`를 활용할 수 있습니다 다른 유저들과 분리할 수 있는 별도의 공간을 만들고, 유저를 이동시키는 `RoomService`를 알아봅시다. 인스턴스 맵을 활용하기 위해 정적 룸, 정적 맵 개념을 함께 설명합니다.

# 정적 룸

하나의 월드에는 하나의 정적 룸(Static Room)이 있습니다. 정적 룸에는 정적 맵이 생성됩니다. 유저는 정적 맵 간 이동을 자유롭게 할 수 있습니다.

# 정적 맵

정적 맵(Static Map)은 월드 인스턴스 안에 생성됩니다. 월드에서 크리에이터가 제작하는 맵의 기본 값은 정적 맵입니다. 월드가 생성될 때 정적 맵도 함께 생성됩니다.

# 인스턴스 룸

인스턴스 룸(Instance Room)은 인스턴스 맵을 담는 바구니입니다. 크리에이터는 인스턴스 룸에 특정 인스턴스 맵들을 담아 생성할 수 있습니다. 각기 다른 인스턴스 룸이라도 동일한 구성의 인스턴스 맵들로 룸을 꾸릴 수 있습니다. 만약 동일한 인스턴스 맵 구성으로 룸을 구성했다면 key를 사용해 서로 다른 인스턴스 룸으로 구별하기 때문입니다.

또한 각기 다른 인스턴스 룸들을 동시에 생성하는 것도 가능합니다. 이미 생성된 인스턴스 룸이 있다면, 새로운 유저가 정적 룸에 접속했더라도 생성된 인스턴스 룸에게 아무 영향도 끼치지 않습니다. 인스턴스 룸을 생성하고 유저를 인스턴스 룸으로 이동시킨 뒤에는 해당 인스턴스 룸의 정보만 불러오기 때문입니다.

유저를 인스턴스 룸의 인스턴스 맵으로 보내기 위해서는 정적 룸의 정적 맵을 우선 거쳐야만 합니다. 인스턴스 룸에서 벗어나 다시 정적 맵으로 돌아갈 때 돌아갈 정적 맵을 MapName으로 정할 수 있습니다. 돌아갈 정적 맵을 정하지 않았다면 시작 맵으로 돌아가게 됩니다.

[png](167220613027481fbeaab9552405b9d54bf9c2d7e9ed3)

#### Key

동일한 인스턴스 맵으로 구성되었지만 다른 인스턴스 룸을 만들 수 있기 때문에 인스턴스 맵을 구별하기 위해서는 식별 키가 필요합니다. Key는 조작 용도로 사용합니다.

# 인스턴스 맵

맵의 기본값은 정적 맵입니다. 크리에이터가 특정 맵을 인스턴스 맵으로 사용하고자 한다면, Map의 MapComponent의 InstanceMap 프로퍼티가 true여야 합니다.

# RoomService

`RoomService`는 인스턴스 룸을 생성, 파괴하거나 유저를 특정 인스턴스 룸으로 옮길 수 있습니다. 자세한 내용은 [RoomService](/apiReference/Services/RoomService)를 참고하세요.

#### 인스턴스 룸으로 이동

유저는 하나의 인스턴스 룸 안에 생성된 인스턴스 맵 사이를 이동할 수 있습니다. 그러나 인스턴스 룸에서 다른 인스턴스 룸으로는 이동할 수 없습니다. 또한 정적 룸과 인스턴스 룸 사이를 `RoomService`를 통하지 않고는 이동할 수 없습니다. 예를 들어 인스턴스 맵에 정적 맵으로 이동할 수 있는 포탈을 만들었더라도 이동할 수 없는 것이 정상입니다. 아래의 도식을 살펴보겠습니다. 정적 룸에 생성된 Map 01, 02, 03 내에서 유저는 자유롭게 이동이 가능합니다. 만약 정적 맵인 Map01에서 Instance Room A로 특정 유저들을 이동시킬 때는 `RoomService:MoveUsersToInstanceRoom()`을 사용합니다. Instance Room A로 이동한 유저들은 룸에 생성된 인스턴스 맵인 map 04, 05, 06, 07내에서 이동할 수 있습니다. 그러나 Instance Room A에서 곧장 Instance Room B로 이동할 수 없습니다. Instance Room A에서 InstanceRoom B로 이동하고 싶다면 정적 맵 map 01으로 돌아간 뒤 `MoveUsersToInstanceRoom()`을 통해 다른 인스턴스 룸으로 이동해야 합니다. [png](167514939683374cba8eb14d44671bed868c1e16516a3)

#### 인스턴스 룸에서 데이터 접근

정적 룸과 인스턴스 룸은 별도의 공간이므로 서로의 데이터에 접근할 수 없습니다. 따라서 Service, Logic과 같은 엔트리들은 공간마다 존재합니다. 하나의 월드에서는 하나의 DataStorage를 사용합니다. 따라서 정적 룸과 생성된 인스턴스 룸 모두 동일한 하나의 DataStorage를 사용하는 점을 염두에 둬야 합니다.

# 활용 예시

대기실에 모인 인원 중 Ready 버튼을 누른 3명만 게임이 진행될 맵으로 이동하게 만들어봅시다. [png](16728923078067133fe9f934c494fb88daec6cd7da953) [gif](1672892340962e23f260871484cac83c21fcfefaadb2c)

#### 준비

이 예제에서는 시작 맵을 인스턴스 맵으로 보내는 정적 룸, 돌아오는 정적 룸으로 활용합니다.

  1. 대기 맵과 인스턴스 맵을 만듭니다. 
     * 정적 맵 이름: waitingmap
     * 인스턴스 맵 이름: dungeon [png](1638498213055cf9e5651d3744ef987655fc87ef9add2)
  2. dungeon 맵을 인스턴스 맵으로 사용하기 위해 프로퍼티 에디터 창에서 MapComponent의 InstanceMap 프로퍼티를 [png](16346176407708cb3de01eaaf48a68ab2dd6fe1b1183f)활성화합니다.  
[png](1679381963752d7fcf722dd3543888ed05edabe847044)
  3. [png](163453120840744616a62243642e889159a68a78a56c2) UI 에디터 - 좌측 기본 도구 -  버튼을 활용해 Ready 버튼을 DefaultGroup에 생성합니다. [png](1638498359187950b047e63934e33a1a76ac3dbef58cf)



> Tip RoomService를 테스트하려면 여러 명의 유저가 필요합니다. [png](1635317657654c59c47ffc44d414db579b8d2fc0715a8)[시작]을 누르고, [png](163453813155295c09654c5824a6d825788e3d97c4b7d)멀티 플레이어 추가 버튼을 필요한 유저 수만큼 눌러 추가합니다.

#### 인스턴스 룸 생성

`GetOrCreateInstanceRoom()` 함수를 활용해 인스턴스 룸을 생성합니다.

  1. GameManager 스크립트 컴포넌트를 생성하고, waitingmap에 추가합니다.  
[png](1638498169847ea785d529504411b94811514d2fe0c42)
  2. GameManager 스크립트 컴포넌트에 인스턴스 룸을 만드는 스크립트를 아래와 같이 작성합니다. 
         
         @Component
         script InstanceRoomGenerator extends Component
         property integer roomIdx = 0
         
         method any GetOrCreateInstanceRoom()
         -- RoomService를 이용해 Instance Room을 생성하고 roomIdx를 1 올려줍니다.
         -- roomIdx를 Instance Room을 만드는 키로 사용해 매번 새로운 InstanceRoom이 만들어지게 합니다.
         local instanceRoom = _RoomService:GetOrCreateInstanceRoom("DungeonMap"..tostring(self.roomIdx))
         self.roomIdx = self.roomIdx + 1
         return instanceRoom
         end
         end
         



    
    
    #### 특정 인스턴스 룸으로 이동시키기 
    게임이 진행될 인스턴스 룸에 입장할 수 있는 유저를 정하는 조건을 만들어 유저를 이동시켜봅시다. 조건은 크리에이터의 기획에 따라 달라질 수 있습니다.
    예시는 **'Ready' 버튼을 3명 이상의 유저가 누르면** 새로운 인스턴스 룸으로 이동시킵니다.
    
    1. **GameManager 스크립트 컴포넌트**에 주기적으로 유저의 Ready 상태를 확인해, 인스턴스 룸으로 이동시키는 스크립트를 작성합니다. 
        ```lua
    @Component
    script MatchmakingManager extends Component
        property number checkReadyPlayerTimer = 0
        property integer playerNumPerGame = 0
    
        @ExecSpace("ServerOnly")
        method void OnUpdate(number delta)
        self.checkReadyPlayerTimer = self.checkReadyPlayerTimer + delta
        -- 일정한 주기로 Ready 상태가 된 플레이어를 체크합니다.
        if self.checkReadyPlayerTimer >= 5.0 then
            self.checkReadyPlayerTimer = 0.0
    
            -- Ready 상태인 플레이어 수와 이름을 가져옵니다.
            local readyPlayerNum = 0
            local readyPlayers = {}
                for k, v in pairs(_UserService.UserEntities) do
                if v.Player.isReady == true then
                    readyPlayerNum = readyPlayerNum + 1
                    readyPlayers[#readyPlayers+1] = v.Name
                end
            end
    
            -- Ready 상태인 플레이어 수가 미리 설정한 값 이상일 경우 해당 플레이어들을 새로운 Instance Room으로 보내주는 로직을 실행합니다.
            if readyPlayerNum >= self.playerNumPerGame then
    
                -- 만들어야 할 Instance Room 개수만큼 반복합니다.
                for i = 1, math.floor(readyPlayerNum / self.playerNumPerGame) do
    
                    -- Instance Room을 생성합니다.
                    local instanceRoom = self:GetOrCreateInstanceRoom()
                    local toSendPlayers = {}
    
                    -- 각 Instance Room에 배치해 줄 플레이어들을 가져옵니다.
                    for j = 1, self.playerNumPerGame do
                        local idx = (i-1) * self.playerNumPerGame + j
                        toSendPlayers[#toSendPlayers+1] = readyPlayers[idx]
                    end
    
                    -- 가져온 플레이어들의 정보를 이용해 해당하는 Instance Room으로 보내줍니다.
                    _RoomService:MoveUsersToInstanceRoom(instanceRoom.InstanceKey, toSendPlayers)
                end
            end
        end
        end
    end
    

  2. waitingmap의 프로퍼티 에디터 창에서 GameManager의 playerNumPerGame에 3을 입력합니다. [png](1638779042401ec936b5ae88046b085ded53f0bae7132)

  3. ReadyButton 스크립트 컴포넌트를 생성하고 Ready UI 버튼에 추가합니다.  
유저가 준비 상태가 되면 서버에 해당 유저가 준비되었다는 정보를 보내줍니다.
         
         @Component
         script ReadyToggleButton extends Component
         property boolean isReady = false
         
         @EventSender("Self")
         handler HandleButtonClickEvent(ButtonClickEvent event)
         -- Parameters
         local Entity = event.Entity
         --------------------------------------------------------
         
         -- 버튼이 눌렸을 때 해당 플레이어가 Ready 상태일 경우 Ready 상태를 취소하고, UI의 Text를 수정한 뒤 서버에 알립니다.
         if self.isReady  == true then
             self.isReady = false
             self.Entity.TextComponent.Text = "Ready"
             _UserService.LocalPlayer.Player:OnCancelReady()
         -- 버튼이 눌렸을 때 해당 플레이어가 Ready 상태가 아닐 경우, Ready 상태로 만들어주고, UI의 Text를 수정한 뒤 서버에 알립니다.
         else
             self.isReady = true
             self.Entity.TextComponent.Text = "Cancel"
             _UserService.LocalPlayer.Player:OnReady()
         end
         end
         end
         



    
    
    5. **Player 스크립트 컴포넌트**를 생성하고 [png](16346008379922a9b756c3912461db7195808cb554abd) DefaultPlayer에 추가합니다.  
     플레이어의 ready 정보를 서버로 동기화시켜 유저의 상태를 확인합니다.
        ```lua
    @Component
    script InstanceReadyController extends Component
        property boolean isReady = false
        property number roomIdx = 0
        property any readyButton = nil
    
        @ExecSpace("ClientOnly")
        method void OnBeginPlay()
        wait(1)
        self.readyButton = _EntityService:GetEntity("Entity ID") -- readyButton의 Entity ID를 입력합니다.
        -- Instance Room으로 이동하면 Ready 버튼이 보이지 않도록 합니다.
        if _UserService.LocalPlayer.CurrentMapName == "dungeon" then
            self.readyButton.Enable = false
        end  
        end
    
        @ExecSpace("ServerOnly")
        method void OnReady()
        self.isReady = true
        end
    
        @ExecSpace("ServerOnly")
        method void OnCancelReady()
        self.isReady = false
        end
    end
    
    
    
    

#### 정적 룸으로 이동시키기

게임이 끝난 맵의 유저는 `MoveUsersToStaticRoom()` 함수를 활용해 정적 룸으로 돌아가게 만듭니다. 돌아갈 정적 맵을 지정하지 않으면 시작 맵으로 돌아갑니다.

  * InstanceGameManager 스크립트 컴포넌트를 생성, 작성한 뒤 dungeon 맵에 스크립트 컴포넌트를 추가합니다. 
        
        @Component
        script TimedRoomClearer extends Component
        @ExecSpace("ServerOnly")
        method void OnBeginPlay()
        local playFunc = function()
            local users = {}
            -- 현재 룸에 존재하는 플레이어들을 가져옵니다.
            for k, v in pairs(_UserService.UserEntities) do
                users[#users+1] = v.Name
            end
            -- 해당 플레이어들을 정적 룸으로 돌려보냅니다.
            _RoomService:MoveUsersToStaticRoom(users)
        end
        
        -- 게임이 시작되고 10초가 지난 후 playFunc 함수가 실행되도록 합니다.
        _TimerService:SetTimerOnce(playFunc, 10.0)
        end
        end
        



    
    
    ##### 참고 가이드
    * [유저 엔티티를 찾아주는 UserService](/docs?postId=60)
    * [엔티티를 탐색하는 EntityService](/docs?postId=201)
    * [월드 인스턴스](/docs?postId=984)