Imagem cortesia da Joy Way

A Joy Way, desenvolvedora do jogo STRIDE, compartilha suas práticas de desenvolvimento multijogador e multiplataforma de RV

Artem Tarasov, líder de sistemas multijogador da Joy Way
Oi, eu me chamo Artem Tarasov e sou líder de sistemas multijogador na Joy Way.

A Joy Way, localizada no Chipre, é uma empresa de desenvolvimento e publicação de jogos de RV. Há cerca de seis anos, temos trabalhado em vários projetos de PC e óculos de RV para SteamVR, Meta Quest, PlayStation VR, Pico e outros.

O desenvolvimento de STRIDE, jogo de parkour e ação em RV, foi um desafio para a equipe, principalmente os modos multijogadores, que são multiplataforma. Tivemos que lidar com várias plataformas, vários dispositivos de entrada, locomoção baseada em física, registro correto de colisões e outras tarefas.

Esta publicação abordará nossas soluções para alguns dos desafios que encontramos ao desenvolver os modos de jogo multijogador para STRIDE.
 

Você pode usar esta publicação como um guia para aproveitar a Unreal Engine no desenvolvimento de jogos multiplataforma e conectá-los a serviços de back-end personalizados. Esta publicação será útil para programadores da Unreal que trabalham com C++Blueprints.

Como escrever um código específico para uma plataforma

Quando se trata de desenvolver um jogo multiplataforma, o primeiro desafio que todo desenvolvedor enfrenta é a necessidade de escrever um código específico para uma plataforma.

O primeiro caso e o mais comum é o uso de plugins e módulos específicos da plataforma. Na Unreal, você pode colocar as plataformas em listas de permissões (whitelist) e proibições (blacklist) para cada módulo:
E para plugins, você pode fazer o mesmo com BlacklistPlatforms, SupportedTargetPlatforms e WhitelistPlatforms (escolha um ou mais parâmetros).
Após colocar as plataformas nas listas de permissões ou de proibições para usar módulos como dependências em C++, você precisará incluir o nome do módulo em PublicDependencyModuleNames e/ou PrivateDependencyModuleNames. A partir daí, as coisas se tornam menos triviais.

Primeiro, você precisa excluir os módulos que não são suportados em algumas plataformas. A maneira mais direta de fazer isso é usar instruções "if/else" em seus módulos de arquivo Build.cs.

Por exemplo:
Depois, você precisa usar flags de compilação específicas da plataforma. Por exemplo:
Você pode ler mais sobre as diretivas de pré-processador C++ aqui.
Um mapa multijogador em STRIDE

Como lidar com múltiplas plataformas rodando em apenas um SO

Ao desenvolver STRIDE, enfrentamos o problema de algumas das plataformas de RV (Pico e Meta Quest) rodarem em Android. Por causa disso, surgiram vários problemas: não poderíamos mais usar as configurações de plataforma da Unreal para diferenciar valores de configuração que o Pico e o Quest tinham em comum. Por padrão, no código, não é possível determinar em qual plataforma se está. Confira as soluções que adotamos.

O problema dos valores de configuração pode ser resolvido com uma injeção de argumentos -ini na Unreal Build Tool. Você pode implementar isso em seus scripts de compilação de CI/CD e inserir as configurações -ini na Unreal Automation Tool para sobrepor as configurações. Use o formato seguinte:
Você pode ler mais sobre as injeções de configuração aqui. Além disso, há uma maneira de modificar a Unreal Automation Tool para aplicar valores de configuração injetados em um jogo compactado. Leia mais aqui.

Com isso feito, você pode inserir um valor de configuração para o UAT e especificar em qual plataforma você está.

A próxima etapa é adicionar as definições de plataforma referentes aos dispositivos Pico e Quest. Observe a diferença entre GlobalDefinitions do Target Rules e PublicDefinitions do Module Rules. Por padrão, o UAT produz duas versões: uma para o editor (para executar Commandlets durante a compilação) e outra para a plataforma de destino. GlobalDefinitions são definições para todo o alvo. Isso significa que, se você definir as plataformas como Pico/Quest em GlobalDefinitions, a compilação do editor (necessária para executar Commandlets) também terá a definição da plataforma indesejada. Considerando isso, você tem que colocar uma definição de plataforma em PublicDefinitions do Module Rules. Confira como você pode fazer isso:
Agora você pode usar diretivas conhecidas de pré-processador para determinar em qual plataforma você está no código C++ e usar o UBlueprintFunctionLibrary para expor verificações de plataforma aos Blueprints.

Como conectar o jogo com serviços de back-end personalizados

Esta parte descreverá nossa abordagem para a integração de serviços de back-end. Nosso back-end consiste em várias APIs HTTP e uma API WebSocket. A implementação de solicitações HTTP e eventos WebSocket é bastante simples, por isso, quero focar em nossa abordagem para encadear chamadas assíncronas.
Localização do hub multiplataforma em STRIDE
Primeiro, começamos a implementar chamadas de API usando callbacks de função lambda para respostas. No entanto, logo ficamos com muitas chamadas aninhadas, e ficou bem difícil de lidar com o código. Portanto, decidimos tornar cada solicitação uma UBlueprintAsyncActionBase separada.

É bastante simples usar nós de UBlueprintAsyncActionBase gerados automaticamente em Blueprints. Confira o guia de programação.
 
Get Stride Net User Data

No entanto, ainda temos um problema importante para resolver: onde chamamos esses nós? Em alguns casos, uma boa solução pode ser chamar as entidades no jogo. Mas e as chamadas no nível de GameInstance? Nossa solução é usar um UObject estendido para a entidade Worker.

Os Workers são classes derivadas de UObject que controlam o ciclo de vida, e nós usamos o GameInstanceSubsystems. Você pode ler mais sobre subsistemas de programação aqui. O uso do GameInstanceSubsystems não é necessário. Além disso, usar o LocalPlayerSubsystems provavelmente seria uma solução melhor.

Os Workers tornam a manutenção das cadeias de chamadas muito simples.
Agora, vou desmistificar a extensão do UObject e compartilhar algumas dicas não tão óbvias.

Primeiro, há apenas uma coisa que você deve estender. GetWorld para chamar funções globais com o WorldContext. Confira um exemplo de código de sobreposição de Worker GetWorld .
Preste atenção na primeira verificação do CDO. Isso fará sentido mais adiante.

Aqui está o exemplo de código com a criação de um Worker.
Resumindo o que foi dito acima, é preciso herdar do UObject, sobrepor o GetWorld, herdar do Worker em C++ e, por fim, instanciar o objeto correto no núcleo do ciclo de vida do Worker. Confira como obter uma classe de Blueprints para instanciar um objeto correto.
Agora, vamos abordar o CDO. Como você pode ver, atribuímos a variável TSubclassOf na construção do GameInstanceSubsystem. A essa altura, você pode ter problemas se não fizer uma verificação de CDO. Sem essa etapa, nós enfrentávamos falhas no editor e muitos problemas no sistema de recursos. Você pode acabar tendo os mesmos problemas.

Separar cada solicitação e expor as cadeias de solicitações aos gráficos do Blueprint ajudou a resolver o "código espaguete" e facilitou a manutenção do código do back-end. Dessa forma, pudemos fazer iterações mais rápidas e tivemos menos bugs.
Se você estiver interessado em nossos títulos e quiser ter uma visão dos bastidores do desenvolvimento de jogos de RV, siga a Joy Way no Twitter e junte-se ao nosso Servidor no Discord.

    Obtenha a Unreal Engine hoje!

    Obtenha a ferramenta de criação mais aberta e avançada do mundo.
    Com diversos recursos e acesso a códigos-fonte inclusos, a Unreal Engine vem carregada e pronta para usar.