É ingênuo. Super: código e arquitetura de um jogo simples

Vivemos em um mundo complexo e, ao que parece, começamos a esquecer coisas simples. Por exemplo, sobre a lâmina de Occam, cujo princípio é: "O que pode ser feito com base em um número menor não deve ser feito com base em um número maior". Neste artigo, falarei sobre soluções simples e não as mais confiáveis ​​que podem ser usadas no desenvolvimento de jogos simples.



No outono DotNext em Moscou, decidimos desenvolver um jogo. Era uma variação de TI da popular alquimia. Os jogadores tiveram que coletar 128 conceitos relacionados a TI, pizza e Dodo a partir dos 4 elementos disponíveis. E precisávamos entender isso de uma ideia a um jogo de trabalho em pouco mais de um mês.

Em um artigo anterior , escrevi sobre o componente de design do trabalho: planejamento, fakapy e emoções. E este artigo é sobre a parte técnica. Haverá até algum código!

Isenção de responsabilidade: As abordagens, código e arquitetura sobre os quais escrevo abaixo não são complicados, originais ou confiáveis. Pelo contrário, são muito simples, às vezes ingênuos e não projetados para cargas pesadas por design. No entanto, se você nunca criou um jogo ou aplicativo que usa lógica no servidor, este artigo pode servir como um impulso inicial.

Código do cliente


Em poucas palavras, sobre a arquitetura do projeto: tivemos clientes móveis para Android e iOS no Unity e um servidor back-end no ASP.NET com o CosmosDB como armazenamento.

Um cliente no Unity representa apenas interação com a interface do usuário. O jogador clica nos elementos, eles se movem pela tela de maneira fixa. Quando um novo elemento é criado, uma janela aparece com sua descrição.


O

processo do jogo Esse processo pode ser descrito por uma máquina de estado bastante simples. O principal nesta máquina de estados é aguardar a animação da transição para o próximo estado, bloqueando a interface do usuário do player.



Eu usei a biblioteca UnitRx muito legal para escrever o código do Unity em um estilo completamente assíncrono. No começo, tentei usar minha tarefa nativa, mas eles se comportaram instáveis ​​nas compilações para iOS. Mas o UniRx.Async funcionava como um relógio.

Qualquer ação que requer animação é chamada pela classe AnimationRunner:

public static class AnimationRunner
   {
       private const int MinimumIntervalMs = 20;
     public static async UniTask Run(Action<float> action, float durationInSeconds)
       {
           float t = 0;
           var delta = MinimumIntervalMs / durationInSeconds / 1000;
           while (t <= 1)
           {
               action(t);
               t += delta;
               await UniTask.Delay(TimeSpan.FromMilliseconds(MinimumIntervalMs));
           }
       }
   }

Na verdade, isso está substituindo a rotina clássica por UnitTask. Além disso, qualquer chamada que deve bloquear a interface do usuário é chamada por meio de um método de HandleUiOperationclasse global GameManager:

public async UniTask HandleUiOperation(UniTask uiOperation)
       {
           _inputLocked = true;
           await uiOperation;
           _inputLocked = false;
       }

Assim, em todos os controles, o valor de InputLocked é verificado primeiro e, somente se for falso, o controle reage.

Isso facilitou bastante a implementação da máquina de estado descrita acima, incluindo chamadas de rede e E / S, usando a abordagem assíncrona / aguardada com chamadas aninhadas, como em uma boneca russa.

A segunda característica importante do cliente foi que todos os provedores que receberam dados sobre os elementos foram feitos na forma de interfaces. Após a conferência, quando desligamos o back-end, permitiu literalmente em uma noite reescrever o código do cliente para que o jogo ficasse completamente offline. Esta versão pode ser baixada agora no Google Play .

Interação de retorno do cliente


Agora vamos falar sobre as decisões que tomamos ao desenvolver a arquitetura cliente-servidor.



As imagens foram armazenadas no cliente e o servidor foi responsável por toda a lógica. Na inicialização, o servidor leu o arquivo csv com o ID e as descrições de todos os elementos e os armazenou em sua memória. Depois disso, ele estava pronto para ir.

Os métodos de API eram um mínimo necessário - apenas cinco. Eles implementaram toda a lógica do jogo. Tudo é bem simples, mas vou falar sobre alguns pontos interessantes.

Autenticação e Elementos Iniciais


Abandonamos qualquer sistema de autenticação complicado e geralmente quaisquer senhas. Quando um jogador digita um nome na tela inicial e pressiona o botão "Iniciar", um token aleatório exclusivo (ID) é criado no cliente. No entanto, ele não está conectado ao dispositivo. O nome do jogador e o token são enviados ao servidor. Todas as outras solicitações do cliente para trás contêm esse token.



As desvantagens óbvias desta solução são:

  1. Se o usuário demolir o aplicativo e reinstalá-lo, ele será considerado um novo jogador e todo o seu progresso será perdido.
  2. Você não pode continuar o jogo em outro dispositivo.

Essas foram suposições deliberadas, porque entendemos que as pessoas jogariam no confe apenas com um único dispositivo. E eles não terão tempo para mudar para outro dispositivo.

Assim, no cenário planejado, o cliente chamou o método de servidor AddNewUserapenas uma vez.

Ao carregar a tela do jogo GetBaseElements, um método também foi chamado uma vez que retornou o ID, o nome do sprite e a descrição dos quatro elementos básicos. O cliente encontrou os sprites necessários em seus recursos, criou os objetos dos elementos, escreveu-os localmente e pintou na tela.

Após lançamentos repetidos, o cliente não se registrava mais no servidor e não solicitava elementos de início, mas os retirava do armazenamento local. Como resultado, a tela do jogo foi aberta imediatamente.

Mesclar elementos


Quando um jogador tenta conectar dois elementos, é chamado um método MergeElementsque retorna informações sobre o novo elemento ou relata que esses dois elementos não foram coletados. Se o jogador coletou um novo elemento, as informações sobre isso são registradas no banco de dados.



Aplicamos a solução óbvia: para reduzir a carga no servidor, depois que o player tenta adicionar dois elementos, o resultado é armazenado em cache no cliente (na memória e no csv). Se um jogador tentar re-empilhar itens, o cache será verificado primeiro. E somente se o resultado não estiver lá, a solicitação será enviada ao servidor.

Assim, sabemos que cada jogador pode fazer um número limitado de interações com as costas, e o número de entradas no banco de dados não excede o número de elementos disponíveis (dos quais tivemos 128). Conseguimos evitar os problemas de muitos outros aplicativos de conferência, que depois de um grande e simultâneo fluxo de participantes costumavam fazer backup.

Tabela de pontuação


Os participantes jogaram nossa "Alquimia" não apenas assim, mas também por prêmios. Portanto, precisávamos de uma tabela de registros, que mostramos na tela em nosso estande, e também em uma janela separada em nosso jogo.



Para formar uma tabela de registros, os dois últimos métodos são usados GetCurrentLaddere GetUser.também há uma nuance curiosa. Se um jogador estiver entre os 20 melhores resultados, o nome dele será destacado na tabela. O problema era que as informações desses dois métodos não estavam diretamente relacionadas.

O método GetCurrentLadderacessa a coleção Stats, obtém 20 resultados e o faz rapidamente. O método GetUseracessa a coleção.Userspelo UserId e faz isso muito rápido. A mesclagem dos resultados já está no lado do cliente. Só que não queremos exibir UserId nos resultados, para que eles não estejam lá. A comparação ocorreu pelo nome do jogador e pelo número de pontos marcados. No caso de milhares de jogadores, as colisões seriam inevitavelmente. Mas contamos com o fato de que entre todos os jogadores é improvável que haja jogadores com os mesmos nomes e pontos. No nosso caso, essa abordagem é totalmente justificada.

Fim de jogo


Nossa tarefa inicial era montar um jogo para uma conferência de dois dias em um mês. Todas as decisões que incorporamos na arquitetura e no código se justificaram completamente. O servidor não se deitou, todas as solicitações foram processadas corretamente e os jogadores não se queixaram de bugs no aplicativo.

No próximo Moscow DotNext, é mais provável que instigemos outro jogo, porque agora ele se tornou nossa boa tradição ( CMAN-2018 , IT-Alchemy-2019 ). Escreva nos comentários para que time killer você está pronto para trocar relatórios hardcore de estrelas do desenvolvimento. :)
Para os mesmos ingênuos e interessados, publicamos o código do cliente da alquimia de TI em domínio público .

E observe o canal de telegrama , onde escrevo tudo sobre desenvolvimento, vida, matemática e filosofia.

All Articles