월드 인스턴스 통신

# 학습 과정 소개

`WorldInstanceService`와 `WorldInstanceSharedMemory`를 활용해 월드 인스턴스끼리 데이터를 공유하고 이벤트를 보내는 방법에 대해 알아봅시다. 월드 인스턴스와 룸 개념은 [월드 인스턴스](/docs?postId=984), [인스턴스 맵 만들기](/docs/?postId=540) 가이드를 참고하세요.

> Tip. 룸끼리의 통신을 구현하고 싶다면 `RoomService`의 동일한 함수명을 사용해보세요.

# WorldInstanceSharedMemory

월드 인스턴스끼리 직접적으로 통신을 할 수 없기에 서로의 데이터를 공유하기 위해서는 [WorldInstanceSharedMemory](/apiReference/Misc/WorldInstanceSharedMemory)를 사용해야 합니다. WorldInstanceSharedMemory는 공유 변수들의 모임으로, 실시간으로 변화하는 특정 공유 변수의 값을 읽고 쓸 수 있습니다. WorldInstanceSharedMemory의 특징은 휘발성입니다. 사용하는 월드 인스턴스가 없다면 WorldInstanceSharedMemory는 사라집니다.

> Tip. 룸끼리의 통신이 필요한 경우 `RoomService` 와 `RoomSharedMemory`를 활용합니다.

#### 공유 변수

공유 변수는 월드 인스턴스끼리 공유하는 값입니다. WorldInstanceSharedMemory의 어떤 데이터를 변경했다고 하면, 공유 변수의 값을 변경했다는 의미입니다. 아래는 WorldInstanceService와 WorldInstanceSharedMemory를 사용해 공유 변수를 다양하게 활용하는 예제입니다.
    
    
    @Component
    script SharedMemoryDemo extends Component
        @ExecSpace("ServerOnly")
        method void OnBeginPlay()
            -- WorldInstanceSharedMemory를 얻어옵니다.
            local getMemResultCode, mem = _WorldInstanceService:GetSharedMemory("MemoryName")
            
            -- 공유 변수 var1을 생성합니다.
            local createVarResult = mem:CreateVariableAndWait("var1", "value1")
            log (createVarResult.Info.Name)        -- var1
            log (createVarResult.Info.Value)       -- value1
            
            -- 공유 변수 var1을 조회합니다.
            local getVarResult = mem:GetVariableAndWait("var1")
            log (getVarResult.Info.Name)           -- var1
            log (getVarResult.Info.Value)          -- value1
            
            -- 공유 변수 var1의 값을 value2로 수정합니다.
            local keyInfo = SharedVariableKeyInfo("var1", nil)
            local updateVarResult = mem:UpdateVariableAndWait(keyInfo, "value2")
            log (updateVarResult.Info.Value)       -- value2
            
            -- 공유 변수 var1을 삭제합니다.
            local deleteVarResultCode = mem:DeleteVariableAndWait("var1")
            
            -- WorldInstanceSharedMemory를 삭제합니다. 남아 있던 모든 공유 변수도 함께 삭제됩니다.
            local deleteMemResultCode = _WorldInstanceService:DeleteSharedMemoryAndWait("MemoryName")
        end
    end
    

#### 공유 변수 참조

월드 인스턴스가 공유 변수를 생성, 조회, 수정을 하면 해당 공유 변수를 참조한 상태가 됩니다. 한 번 특정 공유 변수를 참조한 월드 인스턴스는 공유 변수가 삭제될 때까지 계속 참조 상태를 유지합니다. 만약 어떤 공유 변수를 여러 월드 인스턴스에서 참조하고 있을 때 특정 월드 인스턴스에서 해당 공유 변수를 삭제해버리면 다른 월드 인스턴스에서 공유 변수를 사용할 수 없게 됩니다. 이처럼 한 곳에서 데이터를 삭제했지만 전체 월드 인스턴스에게 영향을 주는 사고를 방지하기 위해 메이플스토리 월드는 WorldInstanceSharedMemory 공유 변수를 바로 삭제하기 보다 참조를 해제하는 방식을 권장합니다. 모든 월드 인스턴스에서 공유 변수 참조를 해제할 경우 자동으로 해당 공유 변수가 삭제됩니다.
    
    
    @Component
    script SharedResourceReleaser extends Component
        @ExecSpace("ServerOnly")
        method void OnBeginPlay()
            -- 공유 변수 var1의 참조를 해제합니다.
            local releaseVarResult = mem:ReleaseVariableAndWait("var1")
            
            -- WorldInstanceSharedMemory 내 모든 공유 변수의 참조를 해제합니다.
            local releaseMemResultCode = _WorldInstanceService:ReleaseSharedMemoryAndWait("MemoryName")
        end
    end
    

#### WorldInstanceSharedMemory 일괄 요청

Batch 접두사가 붙은 함수로 공유 변수의 생성, 조회, 수정 등을 일괄 요청할 수 있습니다. 여러 변수를 처리해야 할 때 사용하면 효율적입니다.
    
    
    @Component
    script BatchVariableCreator extends Component
        @ExecSpace("ServerOnly")
        method void OnBeginPlay()
            local code, mem = _WorldInstanceService:GetSharedMemory("MemoryName")
            
            local nameValues = { ["b_var1"] = "b_value1", ["b_var2"] = "b_value2", ["b_var3"] = "b_value3" }
            local results = mem:BatchCreateVariableAndWait(nameValues)
            
            log (results["b_var1"].Info.Value)        -- b_value1
            log (results["b_var2"].Info.Value)        -- b_value2
            log (results["b_var3"].Info.Value)        -- b_value3
        end
    end
    

#### ETag

ETag는 공유 변수의 데이터가 최신 상태인지 확인하기 위해 필요합니다. ETag는 식별자입니다. 데이터를 생성 또는 수정하거나, 기록을 조회할 때마다 새로운 ETag를 발급합니다. 발급한 ETag는 데이터의 버전을 기록하고, 비교하는데 사용합니다. ETag를 사용하는 이유는 여러 월드 인스턴스에서 하나의 공유 변수를 사용하면 어떤 데이터가 최신 데이터인지, 수정되었는지 알기 어렵기 때문입니다. 또한 여러 월드 인스턴스에서 공유 변수에 접근하므로 예기치 못하게 데이터를 덮어 쓸 수 있기 때문입니다.

공유 변수와 ETag는 함께 사용하는 쌍으로써, 공유 변수의 값을 수정한다면 공유 변수 이름, 값과 더불어 ETag를 함께 요청에 담아 보내야합니다. 이때 요청에 포함된 ETag와 최신 ETag가 서로 다르다면 공유 변수의 값은 수정되지 않습니다. 다만, 크리에이터의 의도에 따라 ETag 확인 과정을 생략할 수 있습니다. 이때는 nil 혹은 빈 문자열을 입력해 무조건적으로 확인 과정을 생략하며 공유 변수 값을 수정합니다.

아래 코드는 ETag를 사용하는 예시입니다.
    
    
    @Component
    script OptimisticVariableUpdater extends Component
        @ExecSpace("ServerOnly")
        method void OnBeginPlay()
            local code, mem = _WorldInstanceService:GetSharedMemory("MemoryName")
            
            -- current.Info.ETag 에는 최신 ETag가 담겨 있습니다.
            local current = mem:GetVariableAndWait("varname")
            
            -- keyInfo에 최신 ETag를 담아 보내야만 공유 변수가 수정됩니다. 다른 값을 담으면 수정되지 않습니다. 단, 빈 문자열이나 nil을 담으면 무조건 공유 변수가 수정됩니다.
            local keyInfo = SharedVariableKeyInfo(current.Info.Name, current.Info.ETag)
            
            -- 공유 변수 수정 성공 시, updated.Info.ETag에는 새롭게 발급받은 최신 ETag가 담겨 있습니다.
            local updated = mem:UpdateVariableAndWait(keyInfo, "new value")
            
            -- ETag 불일치로 인해 공유 변수 수정 실패 시, SharedMemoryResultCode.PreconditionFailed 오류 코드가 반환됩니다.
            if updated.Code == SharedMemoryResultCode.PreconditionFailed then
                error ("ETag Mismatch.")
            end
        end
    end
    

# WorldInstanceService

`WorldInstanceService`의 함수를 활용해 월드 인스턴스끼리 데이터를 공유하거나, 이벤트를 전달할 수 있습니다. WorldInstanceService로 WorldInstanceSharedMemory를 얻을 수 있습니다. 얻은 WorldInstanceSharedMemory로 공유 변수를 생성 또는 수정할 수 있습니다. [png](1679450541490552c575873394cd49aedb25f7cf2f979)

# 월드 인스턴스끼리 이벤트 전송과 수신

`RequestSendEventToAllWorldInstancesAndWait()`, `RequestSendEventToAllWorldInstancesAsync()` 함수를 활용해 다른 모든 월드 인스턴스로 이벤트를 전송할 수 있습니다. 특정 월드 인스턴스에만 이벤트를 전송하고 싶은 경우 `RequestSendEventToWorldInstanceAndWait()` 혹은 `RequestSendEventToWorldInstanceAsync()`를 활용합니다.

#### 이벤트 전송

다른 월드 인스턴스로 이벤트를 보내기 위해서는 월드 인스턴스 Id를 사용해야 합니다. 월드 인스턴스 Id는 `WorldInstanceService.WorldInstanceId`로 알 수 있습니다.
    
    
    @Component
    script WorldEventBroadcaster extends Component
        @ExecSpace("ServerOnly")
        method void OnBeginPlay()
            -- MyWorldInstanceEvent 이벤트를 모든 월드 인스턴스에 전송합니다.
            local evt = MyWorldInstanceEvent()
            local code = _WorldInstanceService:RequestSendEventToAllWorldInstancesAndWait(evt)
            
            if code ~= SendEventRequestResultCode.OK then
                 error ("_WorldInstanceService:RequestSendEventToAllWorldInstancesAndWait failed.")
            end
        end
    end
    

#### 이벤트 수신

크리에이터가 `WorldInstanceService`에 이벤트 핸들러를 연결해야 이벤트를 수신할 수 있습니다. 이벤트 발생 공간은 서버입니다.
    
    
    @Component
    script WorldInstanceEventHandler extends Component
        @EventSender("Service", "WorldInstanceService")
        handler HandleMyWorldInstanceEvent(any event)
        log ("Receive MyWorldInstanceEvent.")
        end
    end
    

# WorldInstanceService 제한 사항

WorldInstanceService를 사용할 때는 아래의 제한 사항을 염두에 두고 사용해야 합니다.

#### 작명 규칙

WorldInstanceMemory와 공유 변수 이름은 아래 문자로만 작명할 수 있습니다. 최소 1글자 이상의 이름이어야 하며, 최대 100글자 이름일 수 있습니다.

  * 알파벳 대,소문자
  * 숫자
  * 공백
  * _ , -



#### 수량 및 용량 제한

  * 한 월드 당 생성 가능한 WorldInstanceSharedMemory는 최대 100개입니다.
  * 하나의 공유 변수에 저장할 수 있는 최댓값은 80,000 bytes입니다.
  * 공유 메모리 하나의 총 용량은 80MiB를 넘을 수 없습니다.
  * 전송할 이벤트 프로퍼티의 총 용량은 80,000 byte를 넘을 수 없습니다.



#### 사용량 제한

  * 월드의 생성된 모든 월드 인스턴스에서 분당 최대 WorldInstanceSharedMemory 함수 호출 횟수는 1,000회입니다. 
    * Batch 함수 호출 한 번으로 100개의 공유 변수의 값을 변경한다면 함수 호출 횟수는 100회입니다.
    * 10개의 월드 인스턴스에서 동시에 100번 호출하면 호출 횟수는 1,000회입니다.
  * 월드 인스턴스 간 최대 이벤트 전송 횟수는 분당 1,000회입니다.



# 활용 예제

월드 인스턴스끼리 서로의 이벤트를 감지하는 예제입니다.

  1. Workspace - MyDesk - Create Scripts - Create EventType - 새로운 MyUserEnterEvent를 생성합니다.

  2. MyUserEnterEvent에 프로퍼티를 추가합니다.
         
         @Component
         script PlayerContext extends Component
         property string WorldInstanceId = ""
         property string Nickname = ""
         end
         



    
    
    3. Workspace - MyDesk - Create Scripts - Create Logic - 새로운 **MyUserEnterLogic**를 생성합니다.
    4. **MyUserEnterLogic**의 Entity Event Handler에서 [+]를 눌러 **UserEnterEvent**를 추가합니다. 
    [png](16794535826480d4537b7507743b7be90ce6f1e6666b7)
    새로운 유저가 월드 인스턴스에 입장하면 모든 월드 인스턴스에 이벤트를 전달하도록 아래와 같이 작성합니다.
        ```lua
    @Component
    script UserEnterEventPublisher extends Component
        @EventSender("Service", "UserService")
        handler HandleUserEnterEvnet(UserEnterEvent event)
        --------------- Native Event Sender Info ----------------
        -- Sender: UserService
        -- Space: Server
        ---------------------------------------------------------
    
        -- Parameters
        local UserId = event.UserId
        ---------------------------------------------------------
    
        local worldInstId = _WorldInstanceService.WorldInstanceId
        local user = _UserService:GetUserEntityByUserId(UserId)
        local nickname = user.PlayerComponent.Nickname
    
        local evt = MyUserEnterEvent(worldInstId, nickname)
        _WorldInstanceService:RequestSendEventToAllWorldInstancesAndWait(evt)
        end
    end
    

  5. Entity Event Handler에서 [+]를 눌러 MyUserEnterEvent를 추가합니다. self를 service로 변경합니다. [png](1679453622421b14075cdc1894563be692c22e59996f5)
  6. 을 눌러 WorldInstanceService를 지정합니다. [png](16794536057958e520b24e60e46ad8b842e580a652971) 어떤 월드 인스턴스에서 유저가 입장했는지 알 수 있도록 아래와 같이 작성합니다.  

         
         @Component
         script InstanceEntryLogger extends Component
         @EventSender("Service", "WorldInstanceService")
         handler HandleMyUserEnterEvent(any event)
         -- Parameters
         local WorldInstanceId = event.WorldInstanceId
         local Nickname = event.Nickname
         ---------------------------------------------------------
         
         local currWorldInstId = _WorldInstanceService.WorldInstanceId
         
         if currWorldInstId == WorldInstanceId then
             log ("User '" .. Nickname .. "' has entered this world instance.")
         else
             log ("User '" .. Nickname .. "' has entered another world instance.")
         end
         end
         end
         



    
    
    7. 월드를 출시해 서로 다른 월드 인스턴스에서 입장하는지 로그를 확인할 수 있습니다. 메이커에서 확인하면 모두 같은 월드 인스턴스로 입장합니다.