Construindo um mundo aberto no navegador, parte 29: Um controlador, qualquer corpo
Por Oleg Sidorkin, CTO e cofundador da Cinevva
Chegou agora? Consulte o guia da série. Ele explica o que é um spike e contém links para todas as partes.
A parte 28 abordou grama e oclusão. Vinte e oito partes construíram um mundo onde se pode estar: terreno que você consegue interpretar, água onde pode nadar, vegetação que se mantém convincente até o horizonte, um servidor que se lembra do que você alterou. Esta parte trata daquilo que se move por tudo isso e é a recompensa de todo o trabalho no motor, porque o objetivo não é um controlador de jogador. É um controlador que não se importa com o corpo que está controlando, de onde vieram suas animações ou se há uma pessoa ou uma IA no comando. O spike 58 constrói o movimento como uma pilha de comportamentos plugáveis sobre um motor de física que não sabe nada sobre locomoção e depois comprova a arquitetura executando três corpos diferentes em instâncias do mesmo motor. O spike 59 conecta um avatar real com animações redirecionadas a esse controlador sem alterar uma única linha dele. O spike 60 conecta um pacote de animações diferente, que não precisa de redirecionamento algum, e extrai a lógica de seleção de clipes para algo que pode realmente ser testado.
Um motor de física que não sabe nada sobre caminhar
Abrir o spike 58 em uma nova aba ↗ · Ver código-fonte
A regra de design é rigorosa, e esse é justamente o ponto: o motor da cápsula não contém nenhuma lógica de locomoção. Nada de caminhada, corrida, salto, atrito, limite de velocidade máxima ou sequer gravidade. Ele integra uma cápsula cinemática contra o terreno e nada mais. Cada comportamento de locomoção — caminhar, deslizar, planar, escalar, nadar, agachar, gerenciar vigor — vive em um controlador independente registrado no motor. A cada quadro, o motor atualiza todos os controladores, pergunta a cada um se deseja assumir o controle e permite que aquele de maior prioridade escreva a velocidade. Caminhar não tem status especial. É apenas o controlador de menor prioridade que sempre responde que sim, portanto é o padrão quando nenhum outro é acionado. Um controlador consiste em seis funções pequenas: um tick, que atualiza seu próprio estado interno a cada quadro, mesmo quando não está ativo; um predicado puro wantsControl, que reivindica o quadro; um applyForces, executado apenas pelo vencedor, que escreve a velocidade e aplica sua própria gravidade, caso queira alguma; além dos opcionais onEnter, onExit e stateName. O controlador de natação retorna ownsCollision: true em applyForces para assumir o tratamento do terreno, porque, caso contrário, sua mola de flutuabilidade entraria em conflito com o ajuste dos pés ao solo feito pelo motor.
Até mesmo os elementos que não são locomoção acabam passando por esse contrato, e foi isso que forçou a ideia seguinte. Um projeto anterior, de canal único, submetia tudo a uma única arbitragem. Isso significava que um monitor de vigor ou uma postura agachada precisava fingir ser locomoção e então recusar o controle com um truque de wantsControl que retornava falso, apenas para executar sua contabilidade interna. A solução divide os controladores em canais nomeados que fazem a arbitragem de forma independente e são aplicados em uma ordem fixa: recurso, depois postura e, por fim, locomoção. O recurso é executado primeiro porque suas escritas, como a redução do vigor, são lidas pelos demais. A postura vem em seguida porque a redução da altura da cápsula ao agachar precisa acontecer antes que o controlador de caminhada a leia para limitar a velocidade máxima. A locomoção é executada por último e controla a escrita da velocidade a cada quadro. Assim, um observador de vigor, um modificador de agachamento e um controlador de natação ativo coexistem de forma limpa, cada um em seu próprio canal, sem que nenhum controlador precise mentir sobre o que é. A demonstração visualiza tudo isso com uma cápsula simples que muda de cor de acordo com o controlador ativo, permitindo observar a arbitragem em ação: o verde da caminhada muda para laranja em uma encosta íngreme quando o deslizamento assume o controle, para ciano no lago quando a natação vence e para creme no ar quando você aciona a planagem.
Um motor, três corpos
O verdadeiro teste de “não conter nenhuma lógica de locomoção” não é o jogador. É verificar se o mesmo motor, sem alterações, consegue controlar algo que nem sequer seja um jogador. createCapsuleEngine é uma fábrica pura, sem estado no nível do módulo, singletons ou efeitos colaterais por instância, então o spike a instancia três vezes. O jogador é uma instância com o conjunto completo de controladores. Um cavalo montável é uma segunda instância que registra apenas um controlador de caminhada, tornando toda a ideia literal: o vocabulário de movimentos de um corpo é apenas o conjunto de controladores registrados. Assim, o cavalo é mais rápido em terreno plano e fisicamente não consegue escalar, nadar ou planar, porque esses controladores nunca foram adicionados. Um NPC autônomo é uma terceira instância, atualizada a cada quadro junto com o jogador e conduzida por um controlador de perambulação que sintetiza sua própria entrada para que o corpo se desloque sozinho, sem ninguém tocando no teclado. O motor nunca fica sabendo que um de seus corpos é um cavalo ou que outro é controlado por IA. Todos são o mesmo integrador de cápsula com listas diferentes de controladores.
A montaria é a única parte que deliberadamente fica fora do motor. Alternar o controle entre o jogador e o cavalo exige coordenar dois motores, e um controlador é executado dentro de um único motor e não consegue enxergar além dessa fronteira. Por isso, a lógica de montaria fica no nível do host: é uma pequena máquina de estados que decide qual motor avança neste quadro, congela o outro, desloca suavemente o cavaleiro até a sela usando um smoothstep ao longo de três quartos de segundo e informa à câmera qual corpo deve acompanhar. A fábrica do motor nunca fica sabendo de nada disso. Essa é a fronteira que a arquitetura traça e mantém. Comportamentos pertencentes a um único corpo são controladores; a coordenação entre corpos é responsabilidade do host. Manter essas responsabilidades separadas é o motivo pelo qual um quarto ou quadragésimo corpo não exigiria nada de novo.
Testado sem navegador
Como o motor não acessa window, document nem Three.js, tudo funciona sem interface gráfica. O spike inclui um executor para Node que simula a interface do terreno, avança o motor quadro a quadro com entradas programadas e faz asserções sobre o estado resultante. Assim, regressões extremamente difíceis de detectar por testes manuais são capturadas por um script: a transição entre saltar e aterrissar, os limiares de entrada e saída da natação, a desativação automática da escalada quando uma superfície fica plana e as taxas de consumo de vigor. Fornecer duas vezes a mesma sequência de entradas e intervalos de tempo e verificar se o estado de saída é idêntico byte a byte garante que o motor seja determinístico, propriedade da qual uma versão em rede acabará dependendo. Vale preservar uma regressão detectada pelo executor: sair de um terreno transitável para uma encosta mais íngreme do que o limite caminhável acionava o controlador de deslizamento e lançava o jogador de volta morro acima. A correção foi uma condição de descida. O deslizamento só começa se a cápsula estiver realmente caindo sobre a encosta. Assim, caminhar horizontalmente contra uma face íngreme agora bloqueia o movimento de forma limpa em vez de provocar um deslizamento, e o teste que afirma que “caminhar contra uma encosta não transitável bloqueia o jogador” impede que o problema retorne.
Conectando um avatar real sem tocar nos controladores
Abrir o spike 59 em uma nova aba ↗ · Ver código-fonte
O spike 59 testa se a camada de controladores está realmente desacoplada do corpo. Ele substitui a cápsula colorida por um personagem real com esqueleto, o rig FBX 3MIKE com clipes da Quaternius Universal Animation Library redirecionados para ele, e os controladores não mudam em nada. A interface entre as camadas é uma única string. Cada controlador já informa o nome de um estado por meio de stateName — idle, walk, run, jump, fall, land, slide, glide, swim, swimIdle —, e a camada do avatar mapeia esse nome para um clipe redirecionado por meio de uma tabela de aliases e faz uma transição suave ao trocar. O controlador de caminhada escolhe dinamicamente seu próprio subestado usando a condição de estar no chão, a velocidade vertical e a velocidade horizontal. Assim, um único controlador de caminhada conduz os estados parado, caminhando, correndo, saltando, caindo e aterrissando, enquanto o avatar apenas segue o nome informado. Como este spike é para um único jogador, o avatar descarta a indireção por inteiros na transmissão e a abstração de múltiplos personagens dos spikes anteriores em rede, mapeando o nome do estado diretamente para um clipe. A prova é que toda a evolução visual da cápsula para um humano com rig não alterou nenhuma linha do código de locomoção, exatamente o benefício que um controlador plugável deve proporcionar.
Um pacote que dispensa redirecionamento e um seletor que você pode testar
Abrir o spike 60 em uma nova aba ↗ · Ver código-fonte
O spike 60 conecta um terceiro corpo, o pacote POLYGON Base Locomotion da Synty, e o carregador é quase inexistente. Cada clipe é fornecido como um FBX independente contendo uma cópia incorporada do mesmo esqueleto da Synty mais uma animação pré-calculada. Como o rig do personagem e todos os clipes usam nomes de ossos idênticos, basta obter o clipe em fbx.animations[0] e reproduzi-lo diretamente no mixer do personagem, sem envolver nenhuma biblioteca de redirecionamento. O Three.js resolve os alvos das trilhas de animação pelo nome do osso, e não pela identidade do objeto, então um pacote no estilo Synty ou Mixamo criado para o rig correspondente simplesmente funciona. Esse é o contraste deliberado com o fluxo da UAL do spike anterior, que exige um redirecionamento pesado porque os clipes de origem e o rig de destino foram criados para esqueletos diferentes. Mesmo controlador, mesma interface baseada no nome do estado, dois pipelines de animação completamente diferentes por trás dela.
A outra metade do spike 60 torna testável a lógica de seleção de clipes. Escolher qual clipe reproduzir envolve muitos limiares subjetivos, e essa lógica estava enterrada dentro da camada do avatar, ao lado do FBXLoader e do DOM, onde não podia ser exercitada. O spike extrai o seletor para funções puras que não acessam nem Three.js nem a janela: elas recebem um registro simples do jogador — velocidade, velocidade horizontal, direção para a qual está voltado, condição de estar no chão, normal do solo e velocidade de impacto — e um substituto de ação de clipe, retornando uma string de alias do clipe. Isso permite que os limiares existam como constantes nomeadas e verificadas por asserções. O salto é dividido em variantes de caminhada, corrida e sprint de acordo com faixas de velocidade alinhadas ao limiar real de sprint do controlador de caminhada; as aterrissagens são divididas em suaves, médias e fortes pela velocidade de impacto; as variantes de clipes de subida e descida são acionadas por um produto escalar de projeção da inclinação, com uma zona morta para clipes planos abaixo de aproximadamente sete graus; uma transição de parado para locomoção sempre é resolvida para a frente, para que uma partida imóvel nunca reproduza um clipe ao contrário; e uma transição de parada é condicionada a um tempo mínimo no loop, porque os clipes de parada por fase do pé da Synty contêm cerca de um segundo de desaceleração criada pelo animador, que parece ridícula quando acrescentada a um passo que o jogador mal chegou a dar. Um pequeno estabilizador de estado adia o estado de queda por alguns quadros, para que uma perda de contato com o chão por um único quadro ao atravessar uma emenda não faça a animação piscar. Extrair tudo isso da camada de renderização significa que as regras podem passar por testes unitários sem uma GPU, aplicando a mesma disciplina de execução sem interface gráfica que o próprio motor recebeu no spike 58. Essa é a diferença entre ajustar a sensação da locomoção por tentativa e erro e poder defini-la com precisão.
Tecnologia mencionada neste capítulo
Um motor de física sem lógica de locomoção. O motor da cápsula integra um corpo cinemático contra o terreno e não contém caminhada, corrida, salto, atrito, limite de velocidade ou gravidade. Cada comportamento é um controlador registrado que expõe tick, wantsControl, applyForces e, opcionalmente, onEnter/onExit/stateName. Caminhar é apenas o comportamento padrão de menor prioridade que sempre responde que sim, e um controlador pode retornar ownsCollision: true para assumir o tratamento do terreno — a natação faz isso para que sua mola de flutuabilidade não entre em conflito com o ajuste dos pés ao solo.
Canais de arbitragem independentes. Os controladores são registrados em canais nomeados — recurso, postura e locomoção — que fazem a arbitragem separadamente e são aplicados em uma ordem fixa. Assim, observadores como vigor e modificadores como agachamento coexistem com a locomoção ativa em vez de fingirem assumir o controle para depois recusá-lo. As escritas do recurso, como o vigor, são lidas pela postura e pela locomoção; as escritas da postura, como a altura da cápsula ao agachar, são lidas pelo limite de velocidade da locomoção; a locomoção é executada por último e controla a escrita da velocidade. Uma fábrica, muitos corpos. createCapsuleEngine é uma fábrica pura, sem singletons, portanto o mesmo motor controla o jogador, um cavalo montável que apenas caminha e um NPC com direção autônoma alimentado por entradas sintéticas. O repertório de movimentos de um corpo corresponde exatamente ao conjunto de controladores registrados; assim, o cavalo não pode escalar nem nadar porque esses controladores nunca foram adicionados. A lógica de montaria fica no nível do host, não em um controlador, pois coordena dois motores que não conseguem acessar um ao outro. Consulte LOD controlado pela GPU.
Testes headless e determinísticos. O motor não acessa window, document nem Three.js, portanto um ambiente de testes Node o controla quadro a quadro e verifica saltos e aterrissagens, limiares de natação, encerramento automático de escaladas e consumo de energia. Reproduzir duas vezes entradas idênticas e verificar se as saídas também são idênticas comprova o determinismo, e um teste de regressão fixa a condição de descida que impede uma caminhada horizontal contra uma superfície íngreme de fazer o jogador deslizar para trás.
Vinculação de avatar independente do corpo, com um seletor testável. Os controladores informam uma string com o nome do estado, e a camada visual a mapeia para um clipe; assim, trocar uma cápsula por um avatar 3MIKE + UAL redirecionado não exige nenhuma alteração no código de locomoção. Os clipes Synty POLYGON compartilham os nomes dos ossos com o rig e são reproduzidos sem redirecionamento (o Three.js vincula as trilhas pelo nome do osso), ao contrário do redirecionamento pesado usado no fluxo UAL. O seletor de clipes é extraído para funções puras, com constantes nomeadas para faixas de velocidade do salto, intensidade da aterrissagem, variantes de projeção em inclinações, uma transição de repouso sempre para a frente, uma proteção de tempo mínimo para a transição de parada e um eliminador de oscilações do estado de queda. Assim, a sensação da locomoção pode ser testada por unidade, em vez de ser ajustada por tentativa e erro.
Parte 29 de 30. Anterior: Parte 28 — Grama até o horizonte e um terreno que se oculta Próxima: Parte 30 — Uma câmera que respeita as paredes Guia da série: /blog/2026-02-25-open-world-browser-series-guide