For the complete documentation index, see llms.txt. This page is also available as Markdown.

멀티플레이어 서버

헤드리스 멀티플레이어 서버로 멀티플레이어 Decentraland 씬을 만드세요.

📔 참고: 멀티플레이어 서버는 이전에 권한 서버. 이름만 변경되었고 기능은 동일합니다. 설치해야 할 SDK 브랜치 이름은 여전히 auth-server, 자세한 내용은 설정.

개요

Decentraland는 플레이어의 기기에서 씬을 로컬로 실행합니다. 기본적으로 플레이어들은 서로를 보고 직접 상호작용할 수 있지만, 각자는 환경과 독립적으로 상호작용합니다. 기본적으로 환경의 변경 사항은 플레이어들 사이에 공유되지 않습니다.

모든 플레이어가 같은 상태의 같은 콘텐츠를 가진 씬을 보게 하는 것은 플레이어들이 더 의미 있는 방식으로 상호작용하기 위해 매우 중요합니다. 이것이 없으면, 한 플레이어가 문을 열고 집 안으로 들어갈 때 다른 플레이어들은 그 문이 아직 닫혀 있는 것으로 보게 되고, 첫 번째 플레이어는 다른 플레이어들에게 닫힌 문을 그대로 통과해 걷는 것처럼 보이게 됩니다.

그 멀티플레이어 서버 는 씬 코드를 실행하고, 상태 변경을 검증하며, 결과를 연결된 모든 플레이어에게 브로드캐스트하는 헤드리스 서버 프로세스입니다. 이는 권한 서버 아키텍처를 따릅니다. 각 클라이언트가 자신의 행동을 보고하도록 신뢰하는 대신, 서버가 단일 진실의 원천 역할을 합니다. 따라서 멀티플레이어 씬 동기화를 위한 권장 방식입니다.

멀티플레이어 서버는 공정성이 게임 메커니즘에서 중요할 때 이상적입니다. 서버 측에서 실행되는 정교한 치트 방지 검증을 구현할 수 있기 때문입니다. 또한 서버에 개인 키와 기타 민감한 정보를 저장하여, 이를 사용자에게 직접 노출할 필요를 없앨 수 있습니다.

멀티플레이어 서버를 두는 것은 실제 문제도 해결해 줍니다. 예를 들어 P2P 설정에서 떠 있는 플랫폼 같은 것을 두 플레이어가 조작하면 상충되는 결과가 나올 수 있습니다. 각 클라이언트가 플랫폼을 서로 다른 높이로 설정하고, 어느 쪽이 맞는지 결정할 권한이 아무에게도 없습니다. 멀티플레이어 서버는 모든 변경을 한 곳에서 해결하므로 모든 클라이언트가 같은 상태로 수렴합니다.

또한 다음을 위한 장소도 제공합니다 세션 간 데이터 유지: 리더보드, 플레이어 진행도, 잠금 해제된 업적, 또는 열린 문이나 배치된 아이템 같은 환경 변경 사항. 플레이어가 돌아오면 세계는 이전에 일어난 일을 반영합니다.

Decentraland가 서버를 호스팅하고 배포해 줍니다. 일반적인 절차로 씬을 게시하면 서버도 별도 단계나 호스팅 비용 없이 원활하게 함께 게시됩니다.

설정

1. auth-server SDK 버전 설치

네이티브 멀티플레이어 서버 API(isServer, registerMessages, 스토리지, EnvVar, 등)는 별도의 SDK 브랜치에서 사용할 수 있습니다. 표준 SDK 브랜치 대신 프로젝트에 설치하려면 다음 명령을 실행하세요:

npm install @dcl/sdk@auth-server
npm install @dcl/js-runtime@auth-server

2. scene.json 구성

멀티플레이어 서버는 다음 플래그로 활성화됩니다 "authoritativeMultiplayer": true 로 scene.json, 루트 수준에 있습니다. 수동으로 추가할 필요는 없습니다. SDK의 auth-server 브랜치를 사용할 때, 씬을 처음 빌드하거나 미리보기할 때 자동으로 추가됩니다. 다만 제거하지 마세요. 이 플래그가 없으면 서버는 절대 실행되지 않고 isServer() 항상 반환합니다 false.

선택적으로, 다음 항목을 scene.json 루트 수준에 추가할 수도 있습니다:

추가 logsPermissions 서버 로그를 볼 수 있는 지갑 주소를 나열하기 위해 console.log() 를 사용합니다. 나열된 사용자는 다음 명령을 실행하여 운영 환경에서 서버 로그를 볼 수 있습니다:

npx sdk-commands sdk-server-logs

3. 미리보기 실행

표준 미리보기 명령을 사용하면 되며, 추가 단계는 필요 없습니다. SDK의 auth-server 브랜치를 사용할 때 미리보기는 자동으로 백그라운드에서 멀티플레이어 서버의 로컬 버전을 시작합니다.

서버의 로컬 세션은 운영 환경의 세션과 연결되어 있지 않으므로, 게시된 씬에 있는 플레이어에게 영향을 주지 않고 자유롭게 테스트할 수 있습니다.

서버 / 클라이언트 분기

프로젝트의 씬 코드는 src 폴더에서 서버와 클라이언트 양쪽에서 실행됩니다. 다음 isServer() 함수를 사용하여 실행 경로를 분리하세요:

서버는 렌더링 없이 헤드리스로 씬을 실행합니다. 서버는 다음을 통해 모든 플레이어 위치, 착용 아이템 및 기타 데이터에 대한 검증된 접근 권한을 가집니다 PlayerIdentityData 그리고 게임 상태에 대한 유일한 권한자입니다.

동기화된 컴포넌트와 검증

모든 클라이언트에 엔티티 동기화하기

사용: syncEntity 를 사용하여 해당 엔티티의 지정된 컴포넌트에 대한 변경 사항을 브로드캐스트합니다:

구문은 다음에서 사용되는 것과 동일합니다 서버리스 멀티플레이어 기능과 동일하여, 이 아키텍처를 사용하는 씬을 멀티플레이어 서버로 업그레이드하기가 매우 쉽습니다. 씬이 멀티플레이어 서버를 사용할 때 상태 업데이트는 더 이상 모든 플레이어 사이에서 전송되지 않고, 대신 모든 상태 업데이트가 서버를 통해 라우팅되고 검증됩니다.

변경 사항 검증

사용: validateBeforeChange() 를 사용하여 엔티티의 특정 컴포넌트에서 상태 업데이트를 제한할 수 있습니다. 사용자 정의 검증 함수를 실행할 수 있으며, 검증 테스트를 통과할 때만 변경이 성공합니다.

검증이 값을 반환하면 true, 그러면 변경이 수락되어 모든 플레이어에게 전파됩니다. 검증이 값을 반환하면 false, 그러면 변경이 거부됩니다. 거부된 변경은 다른 플레이어에게 전달되지 않으며, 이를 시도한 플레이어에게 되돌려집니다.

값 검증

가 새로 값 작성된 값이 특정 매개변수 안에 있는지 검증하는 가장 간단한 경우는 다음과 같습니다. 예를 들어, Transform 에 대한 변경만 허용하며, 새 Y 위치가 0보다 큰 경우:

왜냐하면 validateBeforeChange() 는 서버에서만 의미가 있으므로 항상 다음으로 감싸세요 isServer(). 클라이언트에서는 이 호출이 아무런 유용한 작업도 하지 않습니다.

이를 사용해 치트 방지 메커니즘처럼 게임 로직에 어긋나는 변경을 막을 수 있습니다.

플레이어와의 근접성 검증

다음을 조합할 수 있습니다: validateBeforeChange() 서버가 검증한 플레이어 위치를 사용하여, 플레이어가 상호작용하기 전에 객체에 충분히 가까이 있는지 확인하세요. 예를 들어, 플레이어가 객체의 Transform, 객체가 플레이어로부터 5미터보다 멀다면 변경을 거부할 수 있습니다:

이 패턴은 치트 방지 메커니즘으로 유용합니다. 플레이어가 씬을 가로질러 가면 안 되는 객체를 잡으려고 손을 뻗는 것을 방지합니다.

관리자만 변경 허용

다음을 기준으로 검증할 수도 있습니다 누가 변경을 보내는지. 들어오는 모든 값에는 senderAddress 필드가 있으며, 보낸 사람의 지갑 주소가 들어 있습니다. 이를 사용하여 특정 플레이어의 변경만 허용할 수 있습니다. 예를 들어, 씬 관리자가 다음을 수정할 때만 허용하려면 VideoPlayer 컴포넌트:

참고 씬 관리자 플레이어가 씬에서 관리자 권한을 갖게 되는 방식에 대한 더 자세한 맥락

서버만 변경 허용

가장 엄격한 경우는 서버 자체에서 발생한 쓰기만 허용하고, 클라이언트에서 오는 모든 변경을 거부하는 것입니다. 이것은 완전히 권한을 가져야 하는 상태, 예를 들어 점수, 게임 단계, 생성된 엔티티 등에 적합한 패턴입니다.

들어오는 모든 값에는 senderAddress 필드가 있습니다. 보낸 사람이 서버일 때 이 필드는 상수 AUTH_SERVER_PEER_ID, 다음에서 내보내진 @dcl/sdk/network/message-bus-sync.

아래 예제는 이 검사를 하나 이상의 컴포넌트에 적용하는 작은 protectServerEntity() 도우미를 정의합니다. 이는 다음과 같은 여러 컴포넌트를 보호하는 편리한 방법입니다 Transform 및 GltfContainer)를 한 번에 처리합니다:

사용자 정의 컴포넌트

또한 다음에 validateBeforeChange() 를 씬에서 정의한 사용자 정의 컴포넌트에도 적용할 수 있습니다.

메시지

동기화된 컴포넌트는 모든 플레이어가 지속적으로 봐야 하는 상태, 예를 들면 위치, 점수, 또는 게임 단계 같은 것에 매우 적합합니다. 하지만 모든 것이 이 모델에 맞는 것은 아닙니다. 때로는 플레이어가 서버에 "이 버튼을 클릭했어요" 또는 "게임에 참여하고 싶어요"라고 말하기만 하면 되고, 서버는 "라운드가 시작되었습니다" 또는 "여기 당신의 통계가 있습니다" 같은 일회성 응답으로 답하면 됩니다. 이것들은 지속적인 상태가 아니라 이벤트입니다.

이때 메시지를 사용합니다. 다음을 사용하세요 registerMessages() 클라이언트와 서버 간의 형식이 지정되고 스키마로 검증된 통신을 위해 사용하세요. 메시지는 일회성 전송입니다. 클라이언트가 서버로 하나를 보내면 서버가 이를 처리하고 선택적으로 다시 하나를 보냅니다. 메시지 자체만으로는 어떤 지속적인 상태도 직접 만들지 않습니다.

메시지 정의

서버와 클라이언트가 모두 가져오는 공유 파일에 모든 메시지를 정의하세요. 이렇게 하면 양쪽이 항상 어떤 메시지가 존재하는지, 그리고 어떤 데이터를 담고 있는지에 대해 일치합니다. 각 메시지는 다음과 같습니다 Schemas.Map 로, 페이로드를 설명합니다:

메시지 보내기

클라이언트는 서버로만 메시지를 보낼 수 있습니다. 클라이언트 간 직접 메시징은 없습니다. 서버는 모든 클라이언트에게 브로드캐스트하거나 주소로 특정 플레이어를 대상으로 할 수 있습니다.

메시지 수신

서버에서는 수신한 모든 메시지에 context 보낸 사람의 검증된 지갑 주소가 담긴 객체가 포함됩니다. 이를 사용하여 어떤 플레이어가 메시지를 보냈는지 확인하세요(페이로드의 자기 보고된 신원에 절대 의존하지 마세요).

보내기 전에 상태 동기화를 기다리기

클라이언트는 접속 시 레이스 컨디션을 피하기 위해 첫 번째 메시지를 보내기 전에 씬 상태가 동기화될 때까지 기다려야 합니다:

💡 팁: isStateSyncronized() 는 클라이언트의 상태가 동기화되었음을 알려줄 뿐이며, 서버 초기화가 끝났음을 보장하지는 않습니다. 더 견고한 준비 상태 확인을 원하면 서버가 "ready" 또는 하트비트 메시지를 브로드캐스트하게 하고, 클라이언트는 게임플레이 메시지를 보내기 전에 그것을 기다리게 하세요.

사용 가능한 스키마 타입

모든 메시지 페이로드와 사용자 정의 컴포넌트는 Schemas 를 사용하여 이진 직렬화를 수행합니다. 사용 가능한 타입의 간단한 참고 목록은 다음과 같습니다:

서버에서 플레이어 위치 읽기

서버는 다음을 읽을 수 있습니다 검증된 플레이어 위치; 클라이언트는 이를 위조할 수 없습니다. 이것이 위치 기반 치트 방지의 기반입니다:

데이터 저장

서버 재시작 간에도 데이터를 유지합니다. 저장소는 서버 전용, 항상 호출을 다음으로 감싸세요 isServer(). 서버는 이 데이터를 쓰고 읽을 수 있습니다.

데이터는 두 수준에서 저장할 수 있습니다:

  • 월드: 리더보드나 지속적인 환경 변경처럼 모든 플레이어와 관련된 데이터에 사용하세요.

  • 플레이어: 해당 플레이어의 진행 상황이나 선호도 저장처럼 플레이어별 데이터에 사용하세요.

💡 팁: 저장소는 문자열만 허용합니다. 객체에는 JSON.stringify() / JSON.parse() 를 사용하고 String() / parseInt() 를 숫자에 사용하세요.

에 기록됩니다 node_modules/@dcl/sdk-commands/.runtime-data/server-storage.json.

체크포인트에서 저장하고, 모든 변경마다 저장하지 마세요

Storage는 서버 재시작과 재배포 후에도 살아남아야 하는 데이터의 영구 저장소입니다. 실시간 데이터 저장소가 아닙니다. 작업 중인 게임 상태는 서버 메모리에 보관하세요. 그게 더 빠르고, 서버에 맞는 올바른 패턴입니다. Storage에는 정말 필요할 때만, 의미 있는 체크포인트에서 쓰세요.

영구 저장하기 좋은 시점:

  • 게임 오버 또는 라운드 종료 시.

  • 플레이어가 나갈 때.

  • 주기적인 디바운스 저장(예: 프레임마다가 아니라 30초에 한 번).

재시작이나 재배포 후에도 살아남아야 하는 데이터만 영구 저장하세요. 나머지는 메모리에 두면 됩니다.

아래 예시는 점수가 올라갈 때마다 저장하는 대신, 점수를 메모리에 보관하고 체크포인트에서만 기록합니다:

동시 플레이어가 많은 scene에서는, 더 견고한 접근 방식으로 아직 저장되지 않은 변경이 있는 키("dirty set")를 추적하고 다음 플러시에서 실패한 쓰기를 다시 시도합니다:

월드 Storage — 모든 플레이어가 공유

또한 명령줄을 사용해 scene storage를 관리할 수 있습니다. npx sdk-commands storage scene:

플레이어 Storage — 지갑 주소별

또한 명령줄을 사용해 플레이어 storage를 관리할 수 있습니다. npx sdk-commands storage player:

저장된 데이터 접근

다음 링크를 입력하면 storage UI를 통해 서버의 실시간 저장 데이터를 확인하고 수정할 수 있습니다:

decentraland.org/storage

Creator Hub을 통해서도 이 페이지에 접근할 수 있습니다. 다음을 여세요. 관리 탭을 열고, 콘텐츠를 게시한 장소 옆의 점 3개를 클릭한 다음 스토리지 보기.

를 선택하세요. 그러면 씬을 게시할 수 있는 모든 월드와 land 목록이 표시됩니다.

씬을 연 다음 씬 또는 플레이어 탭.

다음에서 씬 탭을 보면 저장된 모든 변수 목록이 보입니다. 여기서 연필 또는 휴지통 아이콘을 클릭해 이러한 변수들을 편집하거나 제거할 수 있습니다.

Activate stream

다음에서 플레이어 탭을 보면 서버에 데이터가 저장된 모든 플레이어 목록이 보입니다. 주소나 이름으로 검색한 다음, 관련된 모든 데이터를 확인할 수 있습니다. 연필 또는 휴지통 아이콘을 클릭해 이 데이터를 편집하거나 제거할 수도 있습니다.

데이터 구조 변경

프로덕션의 저장된 데이터는 씬의 새 버전을 게시해도 지워지지 않습니다. 이는 리더보드, 플레이어 진행도, 그리고 작은 업데이트를 넘어 플레이어가 계속 유지되길 기대하는 지속형 환경 변경에 아주 좋습니다.

반대로, storage에 있는 데이터는 이전 버전의 코드로 기록된 것입니다. 새 코드가 다른 형태를 기대한다면, 그 오래된 데이터를 파싱하거나 읽는 과정이 미묘하게 실패할 수 있습니다. 이름을 바꾼 필드는 사라져 있을 것입니다. 예전에는 문자열이었고 지금은 객체인 필드는 그 속성에 접근하려 할 때 예외를 던집니다. 몇 달 동안 로그인하지 않은 플레이어는 코드가 더 이상 처리 방법을 모르는 구조보다 이전의 데이터를 불러올 수 있습니다.

권장 사항

  • 항상 방어적으로 파싱하세요. 직접 저장한 것이라도 storage에서 나오는 모든 것은 신뢰할 수 없는 입력으로 취급하세요. JSON.parse() 을 try/catch로 감싸고, 읽기 전에 필드가 존재하는지 확인하며, 없을 때 사용할 적절한 기본값을 준비하세요:

  • 필드를 추가하세요, 이름을 바꾸거나 제거하지 마세요. 가장 안전한 스키마 변경은 추가 방식입니다: 기본값이 있는 새 필드를 도입하고 기존 필드는 그대로 두세요. 오래된 데이터에는 새 필드가 없을 뿐이며, 이미 방어적 파싱이 이를 처리합니다. 필드 이름을 바꾸면 모든 오래된 레코드가 깨집니다.

  • 저장된 객체에 버전을 넣으세요. version 필드를 처음부터 포함하세요. 데이터를 읽을 때는 버전에 따라 분기하고, 사용하기 전에 오래된 형식을 현재 형식으로 마이그레이션하세요. 이렇게 하면 나머지 코드가 하나의 현재 형식만 다루게 됩니다:

  • 마이그레이션된 값을 다시 저장하세요. 레코드를 메모리에서 업그레이드한 뒤에는 다시 저장해서 다음 읽기에서는 이미 새 형식이 되도록 하세요. 시간이 지나면 일회성 마이그레이션 스크립트 없이도 오래된 형식의 레코드 풀이 줄어듭니다.

  • 파괴적 변경에는 새 키를 사용하세요. 새 구조가 정말 호환되지 않고 마이그레이션할 가치가 없다면, 새 storage 키(예: progress_v2)에 쓰고 기존 키는 무시하세요. 오래된 키는 storage에 무해하게 남아 있고, 이를 해석해야 하는 읽기 경로를 피할 수 있습니다. 나중에 storage UI 또는 npx sdk-commands storage 명령으로 나중에 정리할 수 있습니다.

  • 실제 프로덕션 데이터로 테스트하세요. 구조 변경을 배포하기 전에, storage UI에서 몇 개의 실제 레코드를 가져와 새 파싱 코드를 적용해 보세요. 문제를 일으키는 모서리 사례는 보통 존재하는지도 몰랐던 레코드입니다.

  • 탈출구를 남겨 두세요. storage UI나 npx sdk-commands storage를 통해 개별 레코드를 편집하거나 삭제할 수 있다는 점을 기억하세요. 스키마 변경 후 한 명의 플레이어가 잘못된 상태에 갇히더라도, 재배포 없이 해당 레코드를 직접 수정할 수 있습니다.

환경 변수

코드에 값을 하드코딩하지 않고 scene을 설정하세요. 환경 변수는 민감한 데이터뿐 아니라, 씬을 다시 게시하지 않고도 쉽게 바꿀 수 있는 기능 플래그나 매개변수에도 유용합니다.

환경 변수는 서버 전용입니다 서버 전용. 다음으로 보호하세요 isServer(). 서버는 환경 변수를 읽을 수는 있지만, 값을 변경할 수는 없습니다.

EnvVar.get() 는 Promise<string>을 반환하며 변수가 설정되지 않았을 때는 빈 문자열로 해결되므로, 항상 누락된 값에 대한 대체값을 제공하세요:

민감한 데이터

환경 변수는 공개 scene의 컴파일된 코드에 노출하면 위험한 비밀 키, 보상 청구 코드, 기타 민감한 데이터를 저장하는 데 특히 유용합니다.

비밀 키를 서버의 storage에 저장하고, 서버만 이를 isServer()로 읽게 할 수 있습니다.

로컬 개발

프로젝트를 로컬에서 실행하는 동안 환경 변수를 사용하려면, 프로젝트 루트에 .env 파일을 만드세요:

중요: 다음을 추가하세요 .env 를 .gitignore에 추가하여, 잠재적으로 민감한 값이 공개 콘텐츠 서버에 업로드되지 않게 하세요.

환경 변수 변경

환경 변수 값을 바꾸는 가장 쉬운 방법은 storage UI를 사용하는 것입니다.

다음 링크를 입력하면 scene storage에 저장된 데이터를 접근할 수 있습니다:

decentraland.org/storage

Creator Hub을 통해서도 이 페이지에 접근할 수 있습니다. 다음을 여세요. 관리 탭을 열고, 콘텐츠를 게시한 장소 옆의 점 3개를 클릭한 다음 스토리지 보기.

를 선택하세요. 그러면 씬을 게시할 수 있는 모든 월드와 land 목록이 표시됩니다.

씬을 연 다음 환경 탭. 프로젝트의 모든 환경 변수가 보여야 합니다.

Activate stream

이 환경 변수들의 값은 읽을 수 없다는 점에 유의하세요(민감한 데이터 보호를 위해서입니다). 하지만 어떤 것이든 삭제하거나 덮어쓸 수는 있습니다. 연필 또는 휴지통 아이콘을 클릭하기만 하면 됩니다.

또한 명령줄을 사용해 환경 변수를 관리할 수 있습니다. npx sdk-commands storage env:

또한 다음을 사용해 특정 환경을 대상으로 지정할 수 있습니다 --target 플래그:

배포된 환경 변수는 다음보다 우선합니다 .env 값.

권장 프로젝트 구조

서버, 클라이언트, 공유 코드를 분리하면 코드베이스가 커져도 읽기 쉬워집니다:

💡 팁: 모든 registerMessages() 호출과 커스텀 컴포넌트 정의를 shared/에 두세요. 서버와 클라이언트 모두 거기에서 가져오므로, 메시지 스키마에 항상 합의하게 됩니다.

성능 모범 사례

모든 컴포넌트 변경은 전체 컴포넌트 데이터를 네트워크로 전송합니다. 이는 diff만 보내는 Colyseus와 다릅니다. 커스텀 컴포넌트를 설계할 때 이 점을 염두에 두세요. 최적의 해법은 변경 빈도에 따라 데이터를 별도 컴포넌트에 저장하는 것일 수 있습니다.

❌ 거대한 단일 컴포넌트는 피하세요

✅ 원자적 컴포넌트를 선호하세요

경험칙: 함께 그리고 비슷한 빈도로 변경되는 필드끼리 묶으세요. 빠르게 변하는 데이터(타이머, 위치)와 느리게 변하는 데이터(점수, 설정)를 분리하세요.

빈번한 메시지 제한하기

매 프레임마다 메시지를 보내지 마세요. 가능한 경우 배치하거나 제한하세요:

예를 들어 서버가 카운트다운 타이머를 제어한다면, 매초 모든 플레이어에게 업데이트를 보낼 필요는 없습니다. 각 클라이언트가 스스로 시간 경과를 계산하고, 서버는 대략 30초마다 현재 상태를 브로드캐스트하여 일관성을 유지하는 것이 가장 좋습니다.

서버 자원 제한

멀티플레이어 서버는 각 scene을 샌드박스화된 isolate에서 하드 리소스 상한과 함께 실행합니다. 이 한도를 넘으면 데이터가 조용히 유실되거나 scene의 모든 사람에 대해 서버가 종료될 수 있으므로, scene이 한도 내에서 충분히 여유 있게 동작하도록 설계하세요.

메모리

isolate에는 256MB 의 메모리 상한이 있습니다. 초과하면 isolate가 폐기되고 서버가 연결된 모든 플레이어에 대해 종료됩니다. 작업 상태는 가볍게 유지하고 플레이어가 나가면 플레이어별 데이터를 정리하세요.

CPU

각 실행 턴에는 벽시계 기준 예산이 있습니다:

  • 동기식 실행: 턴당 10초. 이 시간을 넘기는 무한 루프는 isolate를 종료시킵니다.

  • 비동기 턴 정산: 60초. Promise 체인이나 이 await 이 시간보다 오래 걸려 해결되면 isolate가 종료됩니다.

engine.addSystem()의 dt 내 누산기를 사용해 무거운 작업을 여러 틱에 걸쳐 분산하세요. engine.addSystem(). 서버에서 절대 길이 제한 없는 동기 루프를 실행하지 마세요.

수신 메시지 속도

연결된 각 피어는 대략 1,000ms당 300개 메시지까지 보낼 수 있습니다. 초과 데이터 프레임은 버려집니다(대기열에 들어가지 않음). 클라이언트에서 매 프레임마다 메시지를 보내지 마세요. 제한 패턴은 성능 모범 사례 를 참조하세요.

메시지 크기

  • 수신 패킷은 128KB 로 제한됩니다. 너무 큰 패킷은 완전히 버려집니다.

  • scene-to-comms 메시지는 대략 30KB로 제한됩니다. 실제 전송 계층에서는 동기화 메시지를 13KB 보다 훨씬 작게 유지하세요(다음을 참조 메시지 섹션에서 설명합니다).

외부 fetch

동시 signedFetch 호출은 32 개로 제한됩니다. 추가 fetch는 슬롯이 생길 때까지 대기열에 들어갑니다. 각 fetch 시도에는 15초 의 타임아웃이 있습니다. fetch 응답은 10MB로 제한됩니다. WebSocket 연결은 32 개의 동시 소켓 1 MB 과 각 메시지당 최대 메시지 크기

입니다.

에서 설명한 40회 호출 제한은 데이터 저장 Storage를 포함한 모든 호스트 호출에 전역적으로 적용됩니다, signedFetch 및 기타 런타임 API에도 적용됩니다. 초과 호출은 즉시 거부되며 대기열에 들어가지 않습니다.

흔한 함정

서버 전용 상태의 검증을 잊는 것

없이 validateBeforeChange가 없으면, 클라이언트가 어떤 컴포넌트에도 쓸 수 있습니다:

클라이언트가 제공한 값을 그대로 믿는 것

health, score, position 같은 중요한 데이터의 값을 클라이언트가 마음대로 정하게 두지 마세요:

상태 동기화 전에 메시지 보내기

클라이언트는 상호작용하기 전에 상태가 동기화될 때까지 기다려야 합니다:

서버가 시작될 때까지 기다리기

서버는 씬에 최소 한 명의 플레이어가 있을 때만 활성화됩니다. 마지막 플레이어가 떠난 뒤에는 서버가 대략 2분 동안 유지되다가 종료됩니다. 다음 방문에서는 새 인스턴스가 콜드 스타트되며, 이 과정은 대략 프로덕션에서 15초. 로컬 미리보기는 서버를 즉시 시작하므로, 콜드 스타트 문제는 거의 항상 게시한 뒤에야 드러납니다.

씬 코드에서는 서버가 온라인이 될 때까지 기다릴 수 있도록 준비해야 합니다. 서버 시작이 끝나기 전에 보낸 메시지는 조용히 유실됩니다. 기다리는 동안 플레이어에게 "서버가 깨어나는 중" 메시지를 보여주고, 초기 서버 요청에는 재시도 로직을 추가하세요.

팁: 서버 준비 상태를 안정적으로 감지하는 방법은 하트비트입니다: 서버가 Date.now() 를 2초마다 동기화된 컴포넌트 필드에 쓰고, 클라이언트는 값이 마지막으로 변경된 시점을 추적하게 하세요. 6초 안에 변경이 오지 않으면 서버가 오프라인이라고 간주하세요. 이는 isStateSyncronized()보다 더 견고한데, 후자는 전송 연결이 되어 있는지만 확인할 뿐 서버가 실행 중인지까지는 확인하지 못합니다.

전체 예제

가장 간단한 멀티플레이어 카운터입니다: 버튼을 클릭하면 서버가 동기화된 점수를 1 증가시킵니다. 서버는 카운터를 스토리지 에 저장해 서버 재시작 후에도 값이 유지되게 합니다. 서버는 씬에 플레이어가 없으면 종료되므로, 저장소가 없으면 씬에서 플레이어가 모두 떠날 때마다 카운트가 0으로 초기화됩니다.

로컬에서 테스트하기

표준 미리보기가 모든 것을 처리합니다. SDK의 auth-server 브랜치를 사용할 때는 로컬 서버가 클라이언트 미리보기와 함께 백그라운드에서 자동으로 시작됩니다.

로컬에서 멀티플레이어 상호작용을 테스트하려면 미리보기를 두 개의 별도 창으로 여세요. 각 창은 별도의 플레이어로 취급됩니다. 각 창을 서로 다른 주소로 연결하세요. 두 클라이언트는 같은 로컬 서버 인스턴스에 연결됩니다.

Creator Hub에서 미리보기 버튼을 두 번째로 클릭하면 두 번째 Decentraland explorer 창이 열립니다. 두 창 모두 서로 다른 주소로 연결해야 합니다. 같은 세션은 씬이 다시 로드되는 동안 계속 열려 있습니다.

대안으로, 다음을 브라우저 URL에 입력하여 두 번째 Decentraland 탐색기 창을 열 수 있습니다:

decentraland://realm=http://127.0.0.1:8000&local-scene=true&debug=true&multi-instance=true

디버깅 팁

  • 로그에 접두사를 붙이세요 에서 [SERVER] 또는 [CLIENT] 그래야 터미널에서 서로 구분할 수 있습니다:

  • 컴포넌트 동기화 확인 클라이언트에서 엔티티 수를 로그로 찍어 확인하세요:

프로덕션에서 디버깅하기

확인하려면 console.log() 게시한 서버의 출력을 보려면, 지갑 주소가 logsPermissions 배열에 포함되어 있어야 합니다 scene.json:

이것이 없으면, 씬 소유자에게조차 프로덕션에서 서버 로그가 숨겨집니다.

프로젝트 폴더에서 다음을 실행해 실시간 서버 로그를 명령줄로 스트리밍할 수 있습니다

또한 다음처럼 로그의 월드 이름을 수동으로 지정할 수 있습니다:

멀티 씬 월드의 씬 또는 Genesis City의 필지에서 로그를 볼 때는 다음도 전달하세요 위치 좌표용

인증하려면 logsPermissions 에 나열된 지갑 중 하나로 메시지 서명을 요청받게 됩니다. 연결되면 서버 측 console.log() 출력이 실시간으로 표시되어, 재배포 없이 문제를 진단하는 데 유용합니다.

스토리지 데이터 보기

다음 링크를 입력하면 scene storage에 저장된 데이터를 접근할 수 있습니다:

decentraland.org/storage

Creator Hub을 통해서도 이 페이지에 접근할 수 있습니다. 다음을 여세요. 관리 탭을 열고, 콘텐츠를 게시한 장소 옆의 점 3개를 클릭한 다음 스토리지 보기.

를 선택하세요. 그러면 씬을 게시할 수 있는 모든 월드와 land 목록이 표시됩니다.

각 항목에 저장된 정보를 보려면 월드 또는 플레이어 데이터를 여세요.

예를 들어 특정 플레이어가 씬을 플레이할 때 문제가 생긴다면, 주소로 이 플레이어를 찾아 어떤 데이터가 저장되어 있는지 확인해 상황을 파악할 수 있습니다. 아마도 서로 모순되는 데이터 상태에 빠진 엣지 케이스를 밟았을 수도 있습니다. 이 페이지에서 그 플레이어의 데이터를 지우거나 수정해서 안정적인 상태로 되돌릴 수도 있습니다.

버전 관리

씬의 게시된 각 버전은 고유한 해시 ID를 가지며, 각 해시는 자체 서버 인스턴스와 짝지어집니다. 즉, 클라이언트 코드와 서버 코드는 항상 함께 움직이며, 오래된 로직을 실행하는 클라이언트가 새 로직을 실행하는 서버와 통신하는(또는 그 반대의) 틈이 생기지 않습니다.

업데이트를 게시하면:

  • 이미 씬에 있는 플레이어들은 떠났다가 다시 돌아올 때까지 계속 이전 버전의 씬을 보게 됩니다. 그들의 클라이언트는 이전 해시에 맞는 서버 인스턴스에 계속 연결된 상태입니다.

  • 새로 들어오는 플레이어들은 업데이트 후 새 씬 버전을 불러오고 새 서버 인스턴스에 연결합니다.

이로 인해 스키마 변경이나 컴포넌트 이름 변경 때문에 클라이언트와 서버 상태가 절대 어긋나지 않습니다. 업데이트가 이미 씬에 있는 플레이어의 세션을 깨뜨리는 일은 절대 없습니다.

대신 트레이드오프가 있는데, 배포 직후 잠시 동안 플레이어가 두 개의 서로 다른 서버 인스턴스에 나뉘어 있게 될 수 있습니다. 이미 있던 플레이어와 방금 들어온 플레이어는 같은 씬에 있더라도 서로를 보지 못하거나 씬을 통한 상호작용을 하지 못할 수 있으며, 이는 이전 플레이어들이 떠나 다시 합류할 때까지 계속됩니다.

💡 팁: 다음을 통해 저장된 데이터는 스토리지 서비스(예: 리더보드, 플레이어 진행 상황, 또는 영구적인 환경 변경)는 아닙니다 버전 간에 초기화됩니다. 스토리지는 위치 수준에서 유지되며 같은 씬을 가리키는 모든 서버 인스턴스 간에 공유되므로, 새 버전은 이전 버전이 끝난 지점에서 그대로 이어받습니다.

Colyseus에서 마이그레이션하기

기존에 Colyseus로 만든 씬이 있다면, 아래 표는 일반적인 Colyseus 패턴을 SDK7의 대응 항목에 매핑합니다:

Colyseus
SDK7 멀티플레이어 서버

room.send(type, data)

room.send(type, data) — 동일한 API

room.onMessage(type, cb)

room.onMessage(type, cb) — 동일한 API

room.state.players (스키마)

syncEntity + 사용자 정의 컴포넌트

JSON 직렬화

이진 직렬화 (다음을 통해 자동으로 Schemas)

별도의 서버 애플리케이션

동일한 코드베이스 — isServer() 분기

커스텀 서버 호스팅

내장: 미리보기가 서버를 자동으로 실행

염두에 둘 주요 차이점:

  • 직렬화: Colyseus는 JSON diff를 보내고, SDK는 변경될 때마다 전체 컴포넌트를 보냅니다. 컴포넌트는 작게 유지하세요(참조 성능 모범 사례).

  • 상태 모델: Colyseus는 자동 diffing이 적용된 변경 가능한 상태 트리를 사용합니다. SDK는 다음을 통해 동기화되는 ECS 컴포넌트를 사용합니다 syncEntity 그리고 다음으로 보호됩니다 validateBeforeChange.

  • 호스팅: 별도의 서버 배포가 없습니다. 멀티플레이어 서버는 씬과 함께 자동으로 배포됩니다.

마지막 업데이트