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

Servers autoritativos

Crie cenas multiplayer do Decentraland com um server autoritativo headless.

Visão geral

Decentraland executa scenes localmente na máquina de um jogador. Por padrão, os jogadores conseguem ver uns aos outros e interagir diretamente, mas cada um interage com o ambiente de forma independente. As mudanças no ambiente não são compartilhadas entre os jogadores por padrão.

Permitir que todos os jogadores vejam uma scene como tendo o mesmo conteúdo no mesmo estado é extremamente importante para que os jogadores interajam de maneiras mais significativas. Sem isso, se um jogador abrir uma porta e entrar em uma casa, os outros jogadores verão essa porta como ainda fechada, e o primeiro jogador parecerá atravessar diretamente a porta fechada para os outros jogadores.

Um servidor autoritativo é um processo de servidor sem interface que executa o código da sua scene, valida alterações de estado e transmite o resultado a todos os jogadores conectados. Em vez de confiar que cada cliente reporte suas próprias ações, o servidor atua como a única fonte da verdade. Isso o torna a abordagem recomendada para sincronizar scenes multiplayer.

Um servidor autoritativo é ideal sempre que a justiça for importante para a mecânica do jogo, pois você pode implementar validações anticheat elaboradas que rodam no lado do servidor. Você também pode armazenar chaves privadas e outras informações sensíveis no servidor, evitando precisar expô-las diretamente ao usuário.

Ter um servidor autoritativo também resolve um problema real: em uma configuração ponto a ponto, dois jogadores controlando algo como uma plataforma flutuante podem produzir resultados conflitantes. Cada cliente define a plataforma para uma altura diferente, e ninguém tem autoridade para decidir qual está correto. Um servidor autoritativo resolve cada alteração em um único lugar, de modo que todos os clientes convergem para o mesmo estado.

Também lhe dá um lugar para persistir dados entre sessões: rankings, progressão de jogador, conquistas desbloqueadas ou mudanças no ambiente, como portas abertas ou itens colocados. Quando os jogadores voltam, o mundo reflete o que aconteceu antes.

Decentraland hospeda e faz o deploy do servidor para você. Publicar sua scene pelo processo normal também publica o servidor sem interrupções, sem etapas extras nem necessidade de pagar por hospedagem.

Configuração

1. Instale a versão do SDK auth-server

As APIs nativas do servidor autoritativo (isServer, registerMessages, Storage, EnvVar, etc.) estão disponíveis em uma branch separada do SDK. Execute os comandos a seguir para instalá-la no seu projeto em vez da branch padrão do SDK:

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

2. Configure scene.json

Opcionalmente, adicione o seguinte ao seu scene.json no nível raiz:

Adicione logsPermissions para listar endereços de wallet que podem ver console.log() do servidor. Os usuários listados podem então ver os logs do servidor em produção executando o seguinte comando:

npx sdk-commands sdk-server-logs

3. Execute a preview

Use o comando padrão de preview, sem etapas extras necessárias. Ao usar a branch auth-server do SDK, a preview inicia automaticamente uma versão local do servidor autoritativo em segundo plano.

A sessão local do servidor não está conectada à de produção, então você pode testar livremente sem afetar os jogadores que estão na sua scene publicada.

Branching de Servidor / Cliente

O código da scene na pasta src do seu projeto executa tanto no servidor quanto no cliente. Use a função isServer() para separar os caminhos de execução:

O servidor executa sua scene sem interface, sem renderização. Ele tem acesso verificado a todas as posições dos jogadores, wearables e outros dados por meio de PlayerIdentityData e é a única autoridade sobre o estado do jogo.

Components sincronizados e validação

Sincronizando Entities com todos os clientes

Use syncEntity para transmitir quaisquer alterações nos components indicados dessa entity:

A sintaxe é idêntica à usada pelo recurso Serverless multiplayer , o que torna trivial atualizar uma scene de usar essa arquitetura para o servidor autoritativo. Quando uma scene usa o servidor autoritativo, as atualizações de estado deixam de ser enviadas entre todos os jogadores; em vez disso, todas as atualizações de estado passam agora pelo servidor e são validadas por ele.

Validando alterações

Use validateBeforeChange() para restringir quaisquer atualizações de estado em um component específico de uma entity. Isso permite que você execute uma função de validação personalizada, e as alterações só serão bem-sucedidas quando o teste de validação for satisfeito.

Se a validação retornar o valor true, então a alteração é aceita e propagada para todos os jogadores. Se a validação retornar o valor false, então a alteração é rejeitada. Uma alteração rejeitada não será passada para outros jogadores e será revertida para o jogador que tentou realizá-la.

Validar valores

O caso mais simples é validar que o novo valor sendo gravado está dentro de determinados parâmetros. Por exemplo, aceite apenas alterações em um Transform quando a nova posição Y for maior que 0:

Como validateBeforeChange() só faz sentido no servidor, sempre proteja isso com isServer(). No cliente, a chamada não faz nada útil.

Você pode usar isso para impedir alterações que contrariem a lógica do seu jogo, como mecanismos anticheat.

Validar proximidade ao jogador

Você pode combinar validateBeforeChange() com posições de jogadores verificadas pelo servidor para verificar se um jogador está perto o suficiente de um objeto antes de permitir que ele interaja com isso. Por exemplo, quando um jogador tenta pegar um objeto alterando sua Transform, você pode rejeitar a alteração se o objeto estiver a mais de 5 metros do jogador:

Esse padrão é útil como mecanismo anticheat: ele impede que jogadores alcancem através da scene para pegar objetos com os quais não deveriam conseguir interagir.

Permitir alterações apenas por admins

Você também pode validar com base em quem está enviando a alteração. Todo valor recebido inclui um campo senderAddress com o endereço da wallet do remetente. Use isso para permitir alterações apenas de certos jogadores. Por exemplo, para permitir que apenas admins da scene modifiquem um component VideoPlayer :

Veja Scene Admin para mais contexto sobre como os jogadores se tornam admins em uma scene.

Permitir alterações apenas pelo servidor

O caso mais restrito é aceitar apenas gravações que se originam do próprio servidor, rejeitando qualquer alteração vinda de um cliente. Esse é o padrão ideal para estado que deve ser totalmente autoritativo: pontuações, fase do jogo, entities spawnadas, etc.

Todo valor recebido inclui um campo senderAddress . Quando o remetente é o servidor, esse campo corresponde à constante AUTH_SERVER_PEER_ID, exportada de @dcl/sdk/network/message-bus-sync.

O exemplo abaixo define um pequeno helper protectServerEntity() que aplica essa verificação a um ou mais components em uma entity específica. É uma maneira conveniente de proteger múltiplos components (como Transform e GltfContainer) em uma única chamada:

Components personalizados

Você também pode aplicar validateBeforeChange() em components personalizados definidos pela scene.

Mensagens

Components sincronizados são ótimos para estado que todos os jogadores devem ver continuamente: coisas como posições, pontuações ou fase do jogo. Mas nem tudo se encaixa nesse modelo. Às vezes, um jogador só precisa dizer ao servidor "eu cliquei neste botão" ou "quero entrar no jogo", e o servidor precisa responder com uma resposta única como "a rodada começou" ou "aqui estão suas estatísticas". Isso são eventos, não estado contínuo.

É para isso que servem as mensagens. Use registerMessages() para comunicação tipada e validada por schema entre clientes e o servidor. Mensagens são fire-and-forget: um cliente envia uma para o servidor, o servidor a processa e, opcionalmente, envia uma de volta. Elas não criam nenhum estado persistente por si mesmas.

Definir Mensagens

Defina todas as mensagens em um arquivo compartilhado que tanto o servidor quanto o cliente importam. Assim, ambos os lados sempre concordam sobre quais mensagens existem e quais dados elas carregam. Cada mensagem é um Schemas.Map que descreve seu payload:

Enviar Mensagens

Os clientes só podem enviar mensagens para o servidor. Não existe mensagens diretas de cliente para cliente. O servidor pode transmitir para todos os clientes ou direcionar jogadores específicos por endereço.

Receber Mensagens

No servidor, toda mensagem recebida inclui um objeto context com o endereço de wallet verificado do remetente. Use isso para saber qual jogador enviou a mensagem (nunca confie na identidade auto-relatada no payload).

Aguarde a sincronização do estado antes de enviar

Os clientes devem esperar até que o estado da scene esteja sincronizado antes de enviar sua primeira mensagem, para evitar condições de corrida na entrada:

Tipos de schema disponíveis

Todos os payloads de mensagens e components personalizados usam Schemas para serialização binária. Aqui está uma referência rápida dos tipos disponíveis:

Servidor Lendo Posições dos Jogadores

O servidor pode ler posições verificadas dos jogadores ; os clientes não podem forjá-las. Essa é a base do anticheat baseado em posição:

Armazenamento de Dados

Persista dados entre reinicializações do servidor. Storage é apenas servidor, sempre proteja chamadas com isServer(). O servidor pode tanto gravar quanto ler esses dados.

Os dados podem ser armazenados em dois níveis:

  • World: Use isso para dados relevantes a todos os jogadores, como rankings ou mudanças persistentes no ambiente.

  • Player: Use isso para dados específicos do jogador, como salvar progresso ou preferências desse jogador.

💡 Dica: Storage aceita apenas strings. Use JSON.stringify() / JSON.parse() para objetos e String() / parseInt() para números.

Durante o desenvolvimento local, o storage é gravado em node_modules/@dcl/sdk-commands/.runtime-data/server-storage.json.

Storage de World — Compartilhado Entre Todos os Jogadores

Você também pode gerenciar o storage da scene via linha de comando, usando npx sdk-commands storage scene:

Storage de Player — Por Endereço de Wallet

Você também pode gerenciar o storage de player via linha de comando, usando npx sdk-commands storage player:

Acessar dados armazenados

Você pode ver e editar os dados armazenados ao vivo no seu servidor por meio da UI de storage, inserindo este link:

decentraland.org/storage

Você também pode chegar a esta página pelo Creator Hub. Abra a aba Manage , clique nos três pontos ao lado de um lugar onde você publicou conteúdo e selecione View server data.

Lá você pode ver uma lista de todos os worlds e land onde você pode publicar scenes.

Abra sua scene e depois a aba Scene ou Player tab.

Na aba Scene você verá uma lista de todas as variáveis armazenadas. A partir daí, você pode editar ou remover qualquer uma dessas variáveis clicando no ícone de lápis ou lixeira.

Activate stream

Na aba Player Na aba

Alterando a estrutura dos dados

Os dados armazenados em produção não são apagados quando você publica uma nova versão da sua scene. Isso é ótimo para rankings, progresso do jogador e mudanças persistentes no ambiente que os jogadores esperam que continuem existindo além de pequenas atualizações na sua scene.

O outro lado é que os dados no storage foram gravados por uma versão anterior do seu código. Se o seu novo código espera uma forma diferente, fazer o parse ou ler esses dados antigos pode falhar de maneiras sutis. Um campo que você renomeou estará ausente. Um campo que antes era uma string e agora é um objeto lançará erro quando você tentar acessar uma propriedade nele. Um jogador que não faz login há meses pode carregar dados anteriores a uma estrutura que seu código não sabe mais como tratar.

Melhores práticas

  • Sempre faça o parse de forma defensiva. Trate qualquer coisa que saia do storage como entrada não confiável, mesmo que você a tenha gravado. Envolva JSON.parse() em um try/catch, verifique se os campos existem antes de lê-los e tenha um valor padrão sensato pronto quando não existirem:

  • Adiciona campos, não os renomeies nem remova. A alteração de esquema mais segura é uma aditiva: introduz um novo campo com um valor predefinido e deixa os campos existentes como estão. Os dados antigos simplesmente não terão o novo campo, que o teu parsing defensivo já trata. Renomear um campo obriga a que todos os registos antigos deixem de funcionar.

  • Versiona os teus objetos armazenados. Inclui um version campo desde o primeiro dia. Quando leres os dados, faz branching com base na versão e migra as estruturas antigas para a atual antes de as usares. Isto mantém o resto do teu código a funcionar com uma única estrutura atual:

  • Escreve de volta o valor migrado. Assim que atualizares um registo na memória, guarda-o novamente para que a próxima leitura já esteja no novo formato. Com o tempo, isto esvazia o conjunto de registos com estrutura antiga sem precisares de um script de migração único.

  • Para alterações incompatíveis, usa uma nova chave. Se a nova estrutura for realmente incompatível e não valer a pena migrar, escreve para uma nova chave de armazenamento (por exemplo progress_v2) e ignora a antiga. A chave antiga fica inofensivamente no armazenamento e evitas qualquer caminho de leitura que tenha de a interpretar. Podes limpar as chaves antigas mais tarde através da UI de armazenamento ou dos npx sdk-commands storage comandos.

  • Testa com dados reais de produção. Antes de publicar uma alteração estrutural, extrai alguns registos reais da UI de armazenamento e executa o teu novo código de parsing sobre eles. Os casos extremos que causam problemas são normalmente registos que não sabias que existiam.

  • Deixa uma saída de emergência. Tem em mente que podes editar ou eliminar registos individuais a partir da UI de armazenamento ou via npx sdk-commands storage. Se um único jogador ficar preso num estado errado após uma alteração de esquema, podes corrigir o respetivo registo diretamente sem voltar a publicar.

Variáveis de Ambiente

Configura a tua scene sem codificar valores diretamente no código. As variáveis de ambiente são úteis para dados sensíveis e também para flags de funcionalidade ou parâmetros que podem ser alterados facilmente sem voltar a publicar a tua scene.

As variáveis de ambiente são apenas servidor. Protege-as com isServer(). O server pode ler variáveis de ambiente, mas não alterar os seus valores.

EnvVar.get() retorna um Promise<string> e resolve para uma string vazia quando a variável não está definida, por isso fornece sempre um valor de recurso para valores em falta:

Dados sensíveis

As variáveis de ambiente são especialmente úteis para armazenar chaves privadas, códigos de resgate de recompensas e outros dados sensíveis que seria arriscado expor no código compilado da scene pública.

Podes armazenar chaves privadas no storage do server e deixar apenas o server lê-las com isServer(). Dessa forma, os dados sensíveis nunca passam pela máquina do jogador.

Desenvolvimento Local

Para usar variáveis de ambiente enquanto corres o projeto localmente, cria um ficheiro .env na raiz do teu projeto:

Importante: Adiciona .env ao teu .gitignore, para que estes valores potencialmente sensíveis nunca sejam carregados para os content servers públicos.

Alterar variáveis de ambiente

A forma mais simples de alterar os valores das tuas variáveis de ambiente é através da UI de armazenamento.

Podes aceder aos dados armazenados pelo storage da scene introduzindo esta ligação:

decentraland.org/storage

Você também pode chegar a esta página pelo Creator Hub. Abra a aba Manage , clique nos três pontos ao lado de um lugar onde você publicou conteúdo e selecione View server data.

Lá você pode ver uma lista de todos os worlds e land onde você pode publicar scenes.

Abra sua scene e depois a aba Environment tab. Deves ver todas as variáveis de ambiente no projeto.

Activate stream

Nota que não podes ler os valores de nenhuma destas variáveis de ambiente (isso serve para proteger dados sensíveis), mas podes eliminar ou substituir qualquer uma delas. Basta clicar no ícone do lápis ou do caixote do lixo.

Também podes gerir variáveis de ambiente via linha de comandos, usando npx sdk-commands storage env:

Também podes apontar para um ambiente específico com a flag --target :

As variáveis de ambiente publicadas têm precedência sobre os valores .env .

Estrutura de Projeto Recomendada

Separar o código do server, do client e o código partilhado mantém a base de código legível à medida que cresce:

💡 Dica: Mantém todas as registerMessages() chamadas e definições de componentes personalizados em shared/. Tanto o server como o client importam de lá, garantindo que estão sempre de acordo sobre os schemas das mensagens.

Melhores Práticas de Performance

Cada alteração de um component envia o component data inteiro pela rede. Isto é diferente do que o Colyseus faz, que envia apenas diffs. Ao desenhares components personalizados, tem isto em mente. A solução ideal pode ser guardar dados em components separados, com base na frequência de alteração.

❌ Evita components monolíticos

✅ Prefere components atómicos

Regra geral: agrupa campos que mudam em conjunto e com uma frequência semelhante. Separa dados que mudam rapidamente (timers, posições) de dados que mudam lentamente (pontuações, configuração).

Reduz a frequência de mensagens

Evita enviar mensagens em cada frame. Agrupa ou limita a frequência sempre que possível:

Por exemplo, se o server controlar um temporizador de contagem decrescente, não é necessário enviar atualizações a todos os jogadores a cada segundo. O melhor é que cada client calcule a passagem do tempo por sua conta e que o server transmita o seu estado atual a cada 30 segundos, mais ou menos, para garantir consistência.

Erros Comuns

Esquecer a validação no estado apenas do server

Sem validateBeforeChange, os clients podem escrever em qualquer component:

Confiar em valores fornecidos pelo client

Nunca deixes que um client dite os seus próprios valores para dados importantes como saúde, pontuação ou posição:

Enviar mensagens antes da sincronização do estado

Os clients têm de esperar até o estado estar sincronizado antes de interagir:

Aguardar que o server arranque

O server só fica ativo se houver pelo menos um jogador presente na scene. Se não estiver lá ninguém, o server encerra após alguns minutos.

Quando o primeiro jogador entra na scene depois de algum tempo de inatividade, o server demora alguns segundos a arrancar. O código da tua scene deve estar preparado para ter de esperar que o server fique online. Os pedidos iniciais ao server devem ter mecanismos de captura e repetição para proporcionar resiliência.

Exemplo Completo

Um contador multijogador mínimo: clica num botão, o server incrementa uma pontuação sincronizada. O server persiste o contador em Storage para que o valor sobreviva a reinícios do server. Lembra-te de que o server encerra quando não há jogadores na scene, por isso, sem storage, a contagem voltaria a zero sempre que a scene ficasse vazia de jogadores.

Testar Localmente

A preview padrão trata de tudo. Ao usar a branch auth-server do SDK, o server local arranca automaticamente em segundo plano juntamente com a preview do client.

Para testar interações multijogador localmente, abre a preview em duas janelas separadas; cada janela é tratada como um jogador diferente. Liga cada janela com um endereço diferente. Ambos os clients ligam-se à mesma instância local do server.

Usando o Creator Hub, clica no botão Preview uma segunda vez e isso abre uma segunda janela do explorer do Decentraland. Tens de ligar as duas janelas com endereços diferentes. As mesmas sessões permanecem abertas enquanto a scene recarrega.

Como alternativa, podes abrir uma segunda janela do explorer do Decentraland escrevendo o seguinte num URL do browser:

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

Dicas de debugging

  • Prefixa os teus logs com [SERVER] ou [CLIENT] para os conseguires distinguir no terminal:

  • Verifica a sincronização do component no client, registando a contagem de entities:

Debug em Produção

Para veres console.log() a saída do teu server publicado, o endereço da tua wallet tem de estar listado no logsPermissions array em scene.json:

Sem isto, os logs do server ficam ocultos em produção, até mesmo para o dono da scene.

Faz stream dos logs ao vivo do server a partir da linha de comandos, executando isto na pasta do teu projeto

Também podes especificar manualmente o nome do world nos logs com:

Ao veres logs de uma scene num world com várias scenes ou de parcels em Genesis City, passa também uma posição para as coordenadas:

Ser-te-á pedido que assines uma mensagem com uma das wallets listadas em logsPermissions para autenticar. Assim que estiveres ligado, verás a saída do server console.log() em tempo real, o que é útil para diagnosticar problemas sem precisares de voltar a publicar.

Ver dados de storage

Podes aceder aos dados armazenados pelo storage da scene introduzindo esta ligação:

decentraland.org/storage

Você também pode chegar a esta página pelo Creator Hub. Abra a aba Manage , clique nos três pontos ao lado de um lugar onde você publicou conteúdo e selecione View server data.

Lá você pode ver uma lista de todos os worlds e land onde você pode publicar scenes.

Abre o world ou os dados do jogador para veres a informação armazenada para cada um.

Por exemplo, se um determinado jogador tiver um problema ao jogar a tua scene, podes procurá-lo pela address e ver que dados estão armazenados para ele, para entender a situação. Talvez tenha encontrado um caso extremo em que acabou com dados contraditórios. Podes até limpar ou editar os dados desse jogador nesta página, para o restaurar para um estado estável.

Controlo de Versões

Cada versão publicada da tua scene recebe o seu próprio hash ID exclusivo, e cada hash é emparelhado com a sua própria instância do server. Isto significa que o código do client e o código do server avançam sempre em conjunto; não existe uma janela em que um client a correr lógica antiga fale com um server a correr lógica nova (ou vice-versa).

Quando publicas uma atualização:

  • Os jogadores já presentes na scene continuam a ver a versão antiga da scene até saírem e voltarem a entrar. Os seus clients mantêm-se ligados à instância do server que corresponde ao hash antigo.

  • Os novos jogadores que chegam depois da atualização carregam a nova versão da scene e ligam-se à nova instância do server.

Isto garante que o estado do client e do server nunca fica desincronizado por causa de uma alteração de schema ou de um component renomeado. Uma atualização nunca pode quebrar a sessão de um jogador que já está na tua scene.

A contrapartida é que, durante uma pequena janela logo após um deploy, os jogadores podem ficar divididos por duas instâncias de server diferentes. Um jogador que já lá estava e um jogador que acabou de chegar podem não se ver nem conseguir interagir através da scene, mesmo estando na mesma scene, até os jogadores antigos saírem e voltarem a entrar.

💡 Dica: Os dados armazenados através do Storage service (como tabelas de classificação, progresso do jogador ou alterações persistentes no ambiente) não são apagados entre versões. O storage é persistido ao nível da localização e partilhado entre todas as instâncias de server que apontam para a mesma scene, por isso as novas versões retomam exatamente de onde a anterior ficou.

Migrando do Colyseus

Se tens uma scene existente construída em Colyseus, a tabela abaixo mapeia padrões comuns do Colyseus para os equivalentes no SDK7:

Colyseus
SDK7 Authoritative Server

room.send(type, data)

room.send(type, data) — mesma API

room.onMessage(type, cb)

room.onMessage(type, cb) — mesma API

room.state.players (schema)

syncEntity + components personalizados

Serialização JSON

Serialização binária (automática via Schemas)

Aplicação de server separada

Mesma base de código — isServer() branching

Hosting de server personalizado

Integrado: a preview corre o server automaticamente

Principais diferenças a ter em mente:

  • Serialização: o Colyseus envia diffs JSON; o SDK envia o component completo em cada alteração. Mantém os components pequenos (ver Melhores Práticas de Performance).

  • Modelo de estado: o Colyseus usa uma árvore de estado mutável com diffing automático. O SDK usa components ECS sincronizados via syncEntity e protegidos com validateBeforeChange.

  • Hosting: Não há deployment de server separado. O authoritative server é implantado automaticamente em conjunto com a scene.

Atualizado