Anteriormente, escrevi sobre minha experiência no desenvolvimento de um
jogo de palavras para dispositivos móveis no Android e iOS, que é muito popular, e decidi acelerar o modo multiplayer, quando dois jogadores competem entre si, escrevendo palavras por vez, como a rodada final da transmissão de Sergey Suponev hora ".

Levei um mês e meio para estudar e implementar o multiplayer. No artigo, tentarei descrever o conceito sem exemplos do código-fonte, diminuindo a quantidade de trabalho realizado.
Um pouco de história
O aplicativo foi escrito em C ++ usando o Marmalade SDK. Desde então, o fornecedor parou de oferecer suporte a essa plataforma, vendendo os tipos para os japoneses, e o futuro desse ambiente de desenvolvimento tornou-se muito vago.

Surgiu a questão sobre o que portar projetos atuais para seu apoio adicional.
Por que não cocos2d-x
O Cocos2d-x é um dos mais comuns mecanismos de desenvolvimento de jogos para dispositivos móveis em C ++. Aparentemente, devido ao seu código livre e de código aberto. O mecanismo está mal documentado. A descrição cobre a parte escassa do motor e a maior parte do material está desatualizada há muito tempo.
Com base nos resultados de um período, ainda consegui criar um protótipo do meu aplicativo. Mas as impressões foram muito ruins: parece que o cocos2d-x está montado no joelho. Os níveis de cena de abstração, Sprite, delegado de aplicativos me pareciam muito desconfortáveis, e a necessidade de procurar respostas para perguntas no fórum do coco cada vez mais leva você à ideia de que está fazendo algo errado. Provavelmente minhas mãos estão crescendo fora do lugar errado.
Minha escolha recaiu sobre SDL
O SDL , como o Marmalade SDL, não é um mecanismo, é uma plataforma. Ele fornece uma API de baixo nível, a partir da qual eu construo níveis de abstração convenientes para mim. Tudo isso está escrito em C, o código fonte está aberto.
Em poucas palavras, o SDL é uma biblioteca multiplataforma gratuita para trabalhar com gráficos, som e processamento de mensagens do sistema operacional. É muito conveniente criar um win32-build e depurar a lógica do jogo no Windows, deixando para emuladores móveis e dispositivos físicos apenas depurando funcionalidades específicas do sistema operacional.
Felizmente ou infelizmente, o SDL não fornece ferramentas para uma tarefa tão estreita como o desenvolvimento de um multiplayer para iOS e Android, então tive que me integrar aos serviços correspondentes.
Arquitetura de aplicativos multithread
A lógica do aplicativo e todo o trabalho com gráficos é implementada no encadeamento principal, que é um ciclo de processamento de mensagens e inicia na função principal. Chame este segmento SDL Thread. Por sua vez, outros threads lançam eventos (SDL_PushEvent) para processamento na fila e ele os lê usando SDL_WaitEvent e SDL_PollEvent. Esses são eventos do sistema lançados pelo sistema e suporte já implementados no SDL ou chamadas de retornos de chamada e ouvintes, que já implementamos além da funcionalidade do SDL.

Toda a lógica do jogo é escrita em C ++. O diretório do projeto contém um conjunto de arquivos * .cpp que podem ser divididos em três grupos:
- multiplataforma - os arquivos incluídos na montagem de todas as plataformas (lógica do jogo);
- monoplataforma, isto é, estão incluídos no aplicativo de qualquer plataforma para implementar seus recursos.
Por conseguinte, existem três diretórios separados para o design de cada plataforma:
- proj.win32 - projeto VS2017 Community Edition;
- proj.android - projeto Android usando Gradle;
- proj.ios - projeto Xcode para iOS.
Integração com serviços multiplayer
Agora precisamos colar uma camada separada, que será responsável por funcionalidades como:
- procure um oponente, conexão com o jogo;
- mensagens entre rivais;
- sair da sala de jogos;
- fixação de pontos de jogador nas tabelas de classificação.
As plataformas iOS e Android suportam Multiplayer em tempo real (RTMP). No caso do Android, integramos ao Google Play Services (GPS), no caso do iOS, Game Center. Anteriormente, o Google suportava a integração com o iOS, mas este ano decidiu abandoná-lo.
Neste artigo, não descreverei as ações que você precisa executar no Google Play Console e no AppStoreConnect para configurar o multiplayer, não descreverei a especificação de classes e métodos de integração - tudo isso é descrito em sites de fornecedores.
A seguir, descreverei brevemente quais mudanças precisam ser feitas no projeto para cada uma das plataformas.
Android
Como Eu não disse isso ainda?
O Android NDK é usado para compilar o código C ++. Embora, se você é um desenvolvedor Android, você já sabe.
Instruções gerais para integrar o Google Play Services a um projeto Android são descritas no site para desenvolvedores do Android. No meu projeto, eu uso as seguintes dependências:
implementation 'com.google.android.gms:play-services-games:16.0.0' implementation 'com.google.android.gms:play-services-nearby:16.0.0' implementation 'com.google.android.gms:play-services-auth:16.0.1'
Inicialmente, a idéia era usar uma
API C ++ , que vem na forma de bibliotecas estáticas compiladas sem fontes. Devido ao fato de não haver montagem para a plataforma x86_64 na lista de bibliotecas, decidi que o pessoal do Google realmente não monitora a relevância desse SDK e decidi
inventar sua bicicleta para gravar essa camada em Java, envolvendo-a com os invólucros
JNI . E então, por que preciso de uma dependência extra na forma de bibliotecas sem códigos-fonte que dentro do Java ainda puxam o Java? Além da relevância das classes Java, você também precisará monitorar a relevância dessas bibliotecas.
Como guia, usei um bom exemplo do
Google Samples . Obrigado ao Google por isso. Apple, tome um exemplo do Google!
iOS
Para integrar-se ao Game Center, você deve conectar a estrutura do GameKit. Descrevemos toda a camada de integração com o Game Center em um arquivo * .m e fornecemos a interface através de um arquivo * .h separado. Como o C ++ é um subconjunto da linguagem objetivo-C, não haverá problemas com a montagem dos arquivos * .cpp e * .m em um projeto.
Além da
documentação oficial, ele foi guiado por este projeto:
GameCenterManager . É verdade que algumas coisas do exemplo já estão desatualizadas, o Xcode 10 informará isso e você substituirá a funcionalidade desatualizada pela nova.
O princípio de trabalhar com a camada Multiplayer
Ponto de entrada único
Tendo estudado os recursos do trabalho com multiplayer em ambas as plataformas, criei uma única distração em C ++ para o meu aplicativo e, no momento da compilação, a implementação correspondente o "encaixa" dependendo da plataforma específica. Ou seja, meu aplicativo não conhece nenhum Google Play Services, Game Center e seus recursos. Ele conhece apenas a API do C ++ fornecida, onde, por exemplo, existem métodos como:
SignIn()
Procure um oponente
O jogador pode convidar um amigo da lista de seus contatos ou iniciar o jogo com um oponente aleatório. O jogador que recebeu o convite pode aceitá-lo ou rejeitá-lo. Para todos esses cenários, eu uso a interface padrão do serviço usado. Gostaria de observar que os focinhos do Google parecem muito mais agradáveis no iOS iOS. Talvez um dia minhas mãos cheguem lá e eu escreva minha interface com dominós e jovens senhoras.
Conexão com a sala de jogos
Quando dois jogadores se conectam à sala de jogos virtual, eles recebem os retornos de chamada correspondentes. Agora você precisa escolher quem será o anfitrião.
Seleção de host
Entre os jogadores, você precisa escolher um host, para que ele determine o estado inicial do jogo.
Considere maneiras possíveis de rotear mensagens entre jogadores no caso geral. Observe que, na segunda modalidade, o host também recebe a função de roteador.

Como sempre tenho apenas dois jogadores no jogo, acontece que tenho um caso especial de conexão ponto a ponto. E, portanto, apenas a definição do estado inicial recai sobre o papel do host, a saber, a escolha da palavra da qual as palavras serão compostas.
Portanto, após os jogadores conectados à sala de jogos, cada um deles conhece a lista de identificadores de participantes do jogo que começou. Vamos chamá-lo de uma lista de
participantID . participantID é um determinado identificador de string exclusivo do participante do jogo, atribuído pelo serviço. Você precisa escolher qual deles será o anfitrião, levar isso para o próprio anfitrião e dizer ao outro que o oponente está selecionado como anfitrião. Como fazer isso?
Seleção de host Android
Não encontrei nenhum conselho sobre como escolher um host no google dock. Eles são silenciosos, partidários. Mas as pessoas boas no
stackoverflow.com criaram um link para o
vídeo , o que explica em detalhes o seguinte princípio:
- cada participante classifica a lista de participantID (ascendente ou descendente - não importa, o principal é que todos façam na mesma ordem);
- cada participante compara seu participantID com o primeiro participantID da lista;
- se eles corresponderem, o jogador atual terá o direito de escolher quem será o anfitrião. Ele
joga uma moeda, puxa aleatoriamente (), escolhendo um host dentre os participantes existentes e diz a todos que são o host.
Seleção de host no iOS
Para iOS, existe um método
chooseBestHostPlayerWithCompletionHandler , que simplifica bastante o cenário de seleção de host em comparação ao que eu descrevi para o Android. Mas, a julgar pelos atrasos visíveis durante a chamada para esse método, ele estima os parâmetros de resposta da rede, mede o ping e, com base nessas estatísticas, decide quem deve ser o host. Isso é mais provável para a arquitetura cliente-servidor acima, onde o host atua como um roteador. Na minha versão de uma conexão ponto a ponto privada, isso não faz sentido e, para economizar tempo, uso um princípio semelhante ao que fiz no Android.
Mensagens entre jogadores
O que é uma mensagem? Uma mensagem é uma matriz de bytes.
- em java, este é um tipo:
byte[]
- no objetivo C, é o seguinte:
NSData *
- em C ++, mapeio todos os itens acima para
std::vector<Uint8>
Existem 2 tipos de envio de mensagens:
- Confiável - entrega garantida através da fila. Usado para entregar mensagens críticas.
- Não confiável - entrega não garantida. Mensagens usadas cujo sucesso na entrega pode ser negligenciado.
Não confiável é normalmente entregue mais rápido que Confiável. Você pode ler mais no site dos fornecedores:
Como vamos usar essa matriz? Muito simples:
- no primeiro byte, escreveremos o tipo de mensagem.
- se a mensagem tiver algum parâmetro, nós os colocaremos nos seguintes bytes. Para cada tipo de mensagem que foi adicionada. parâmetros, implementamos nossa função de serialização e desserialização.
- no final da mensagem, para verificar a integridade, colocaremos uma soma de verificação.
Então, definimos
enum com os tipos de mensagens que os jogadores trocarão entre si durante o jogo:
- Eu sou selecionado pelo host. Eu transmito o estado inicial. Agora é a minha vez (parâmetros: número da versão do protocolo de mensagens, palavra-fonte);
- Você é selecionado pelo host. Estou ansioso para ouvir de você;
- Eu abro (ligo) a palavra. Agora é a sua vez (parâmetro: nomeado palavra);
- Eu desisto Você venceu;
- Não pude dizer uma palavra durante a mudança. Você venceu;
- Eu concordo em me vingar;
- Eu parei o jogo;
- Erro ao analisar a mensagem. Desconectado;
- Sua versão do protocolo de mensagens está desatualizada. Verifique a atualização do aplicativo. Desconectado;
- Minha versão do protocolo de mensagens está desatualizada. Precisa verificar a atualização. Desconectado;
- Ping (mensagem do sistema);
Quando o aplicativo recebe uma mensagem de entrada de um oponente, é chamado o retorno de chamada correspondente, que por sua vez o passa para o Thread SDL principal para processamento.
Monitoramento de conexão
Os serviços de jogos (o do Google e o da Apple) têm funcionalidade de ouvinte que, de uma forma ou de outra, foi projetada para nos notificar sobre uma desconexão de um oponente. Mas notei que, se um dos jogadores é desconectado da Internet, o segundo não reconhece imediatamente que o primeiro foi desconectado e não há ninguém com quem brincar. Os retornos de chamada não são chamados nesses casos ou são chamados após um longo período de tempo. Para que, neste caso, o segundo jogador não espere o apito do câncer na montanha, eu tive que fazer meu próprio monitoramento da conexão, trabalhando com o princípio:
- Cada um dos jogadores envia uma mensagem de ping ao oponente a cada segundo;
- Cada jogador verifica: se não houver mensagem do oponente por mais de 5 segundos, a conexão será perdida, sairemos do jogo.
Resultado
Como resultado do trabalho realizado, consegui um jogo em que me jogo com meus amigos e familiares. Eu jogo tanto no iOS quanto no Android.
É verdade que há uma nuance no iOS - por alguma razão, os óculos não são fixados nas tabelas de classificação, sobre as quais estou atualmente em correspondência com o suporte da Apple.
Espero que este artigo seja útil para os membros da minha equipe e para os interessados em desenvolver aplicativos móveis. Obrigado pela atenção.