Código de rede Age of Empires: 1.500 arqueiros por modem de 28,8 kbps

imagem

Nota do tradutor: este artigo já tem 17 anos e é interessante apenas do ponto de vista histórico. É interessante aprender como os desenvolvedores conseguiram um jogo on-line tranquilo na era dos modems de 28.8k e dos primeiros Pentiums.

Este artigo fala sobre a arquitetura e implementação, bem como algumas lições aprendidas ao criar o código multiusuário (rede) para os jogos Age of Empires 1 e 2 . Ele também descreve as abordagens atuais e futuras da arquitetura de rede usadas pelo Ensemble Studios em seus mecanismos de jogo.

Age of Empires multijogador: requisitos de estrutura


No início do trabalho no código multiplayer Age of Empires, em 1996, estabelecemos objetivos muito específicos, necessários para a implementação da jogabilidade necessária.

  • Batalhas históricas em larga escala e épicas com muitas unidades militares
  • Suporte para até 8 jogadores no modo multiplayer
  • Simulação de jogabilidade suave pela LAN, via conexão direta com o modem e pela Internet
  • Suporte à plataforma de destino: Pentium 90 com 16 MB de RAM e modem de 28,8 kbps
  • O sistema de comunicação deve funcionar com o mecanismo existente (Genie)
  • Estável 15 quadros por segundo em máquinas com configuração mínima

O mecanismo Genie já estava pronto e a jogabilidade no modo de usuário único começou a tomar suas formas. O mecanismo Genie é um mecanismo de loop de jogo unidimensional bidimensional. Sprites são renderizados em 256 cores em um mundo composto por ladrilhos. Os mapas gerados aleatoriamente são preenchidos com milhares de objetos: de árvores que podem ser cortadas a gazelas galopantes. O detalhamento aproximado (após a otimização) do tempo para a execução de tarefas do mecanismo: 30% para renderização gráfica, 30% para IA e localização de caminhos, 30% para a realização de simulações e tarefas oficiais.

Já em um estágio bastante inicial, o mecanismo era relativamente estável e as comunicações para vários usuários tinham que trabalhar com código pronto, sem a necessidade de uma mudança significativa na arquitetura (em funcionamento) existente.

Para complicar a tarefa, o tempo necessário para concluir cada etapa da simulação poderia variar bastante: o tempo de renderização dependia de o usuário estar observando as unidades, rolando ou olhando para a área inexplorada, e os caminhos longos ou o planejamento estratégico da IA ​​afetarem significativamente o tempo de execução do jogo. : as oscilações foram de até 200 ms.

Cálculos breves mostraram que a transferência de até um pequeno conjunto de dados sobre unidades e as tentativas de atualizá-las em tempo real limitam bastante o número de unidades e objetos com os quais o jogador pode interagir. Se você simplesmente transferir as coordenadas X e Y, estado, ação, direção da vista e dano, no jogo não poderá haver mais de 250 unidades móveis.

Queríamos que os jogadores pudessem destruir as cidades gregas com catapultas, arqueiros e guerreiros, e ao mesmo tempo travando cercos com trirremes do mar. Obviamente, precisávamos de outra abordagem.

Simulações simultâneas


Em vez de transmitir o estado de cada unidade do jogo, queríamos realizar simulações absolutamente idênticas em cada máquina, passando a cada um o mesmo conjunto de comandos dados pelos jogadores ao mesmo tempo. Os computadores dos jogadores, em essência, tinham que sincronizar a jogabilidade nas melhores tradições dos filmes de guerra, permitindo aos jogadores emitir comandos e depois executá-los da mesma maneira e ao mesmo tempo, garantindo a identidade dos jogos.

Inicialmente, era difícil implementar uma sincronização tão complicada, mas, como resultado, trouxe vantagens inesperadas em outras áreas.

Aprimoramento básico do modelo

No nível conceitual mais simples, implementar simulações simultâneas parece muito fácil. Em alguns jogos que usam simulações com um passo fixo (lock-step) e tempos constantes, isso pode até ser bem possível.

Como com essa abordagem, ele deve assumir a responsabilidade de mover simultaneamente centenas ou milhares de objetos, o sistema deve permanecer viável, mesmo com flutuações de atraso de 20 a 1000 milissegundos e conseguir processar alterações durante o processamento de quadros.

Enviar comandos do jogador, confirmar todas as mensagens e depois processá-las antes de passar para a próxima jogada seria um pesadelo do ponto de vista do processo do jogo, com espera constante e uma lenta troca de equipes. Precisávamos de um esquema que pudesse continuar processando o jogo em paralelo com o plano de fundo, aguardando a conclusão do processo de troca de dados.

Mark [Terrano] usou um sistema para marcar comandos que devem ser executados por meio de duas “movimentações de troca de dados” no futuro (as movimentações de troca de dados no AoE foram separadas dos próprios quadros de renderização).

Ou seja, os comandos dados durante o curso 1000 são atribuídos para serem executados durante o curso 1002 (veja a Fig. 1). No decurso de 1001, são executados os comandos dados no decurso de 0999. Isso nos permitiu receber, confirmar e preparar mensagens para processamento, enquanto o jogo continuava desenhando animações e simulações.


Figura 1. Marcação de comandos a serem executados através de dois “movimentos de troca de dados”.

Normalmente, os movimentos levavam 200 ms e as equipes eram enviadas durante esse turno. Após 200 ms, a mudança parou e uma nova mudança começou. Em cada momento do jogo, as equipes eram processadas em uma jogada, recebidas e salvas para a próxima jogada e, em seguida, enviadas para execução duas jogadas depois.

"Controle de velocidade"



Figura 2. Controle de velocidade.

Como as simulações sempre devem ter exatamente a mesma entrada, um jogo não pode correr mais rápido do que a máquina mais lenta consegue processar a troca de dados, executar uma jogada e enviar novos comandos. Chamamos o sistema que altera a duração do golpe para manter animações e jogabilidade suaves sob condições de atraso na troca de dados variáveis ​​e velocidade de processamento "Speed ​​Control".

A jogabilidade pode ser sentida "travando" por duas razões: se a taxa de quadros de uma máquina cair (ou for menor que o resto), outras máquinas processarão seus comandos, renderizarão tudo no tempo previsto e, como resultado, terão que esperar pelo próximo movimento. Nesse caso, qualquer pausa imediatamente se torna perceptível. Além disso, o jogo é retardado por um atraso na troca de dados - os jogadores precisam esperar até que a máquina receba dados suficientes para concluir a jogada.

Cada cliente calculou a taxa de quadros considerada constantemente alcançável, que foi calculada pela média do tempo de processamento de vários quadros. Como esse valor muda durante o jogo, dependendo do escopo, do número de unidades, do tamanho do mapa e de outros fatores, ele foi transmitido em cada mensagem sobre a conclusão da jogada.

Além disso, cada cliente também mediu o "tempo de ping" de si mesmo para outros clientes e vice-versa. Ele também enviou o ping médio ao cliente mais longo em uma mensagem sobre a conclusão da movimentação (no total, 2 bytes foram usados ​​para controlar a velocidade).

Em cada movimento, a máquina designada pelo host analisava as mensagens de conclusão, calculava a taxa de quadros necessária e a correção para o atraso na transmissão de dados pela Internet. Em seguida, o host enviou uma nova taxa de quadros e a duração da troca de dados. As Figuras 3-5 mostram como o fluxo de troca de dados foi dividido em diferentes condições.


Figura 3. Fluxo típico de troca de dados.


Figura 4. Transmissão de dados de alta latência pela Internet na velocidade normal da máquina.


Figura 5. Baixa velocidade da máquina com atraso normal na transferência de dados.

O "progresso da troca de dados", que era aproximadamente igual ao tempo de ping da viagem de ida e volta para a mensagem, foi dividido pelo número de quadros de simulação que a máquina mais lenta poderia executar em média durante esse tempo.

A duração da troca de dados foi ponderada, para que pudesse aumentar rapidamente de acordo com as mudanças na latência da transmissão de dados pela Internet e diminuir lentamente para a melhor velocidade média que pode ser mantida constantemente. Normalmente, o jogo desacelerava e desacelerava apenas nos momentos dos piores picos - o atraso na transmissão dos comandos aumentava, mas permanecia tranquilo (e aumentava apenas alguns milissegundos por turno), porque o jogo reduzia gradualmente os atrasos à melhor velocidade possível. Isso criou a maior suavidade possível da jogabilidade e, ao mesmo tempo, ajustou as mudanças nas condições.

Entrega garantida


O UDP foi usado na camada de rede e cada cliente estava envolvido na ordenação de comandos, no reconhecimento de perdas e na retransmissão. Cada mensagem usava um par de bytes, indicando o curso para o qual a execução dos comandos foi planejada e o número de série da mensagem. Se uma mensagem foi recebida após a movimentação, ela foi rejeitada e as mensagens recebidas foram salvas para execução. Devido à natureza do UDP, Mark usou o seguinte princípio ao receber mensagens: “Em caso de dúvida, considere a mensagem perdida. Se as mensagens forem recebidas fora de ordem, o destinatário envia imediatamente uma solicitação de retransmissão de mensagens perdidas. Se a confirmação do recebimento for recebida depois do horário previsto, o remetente simplesmente envia a mensagem novamente, sem aguardar um sinal sobre sua perda. "

Benefícios ocultos


Como os resultados calculados pelo jogo dependiam de todos os usuários realizando simulações idênticas, era incrivelmente difícil para um cliente (ou fluxo de dados do cliente) invadir e trapacear. Qualquer simulação realizada de maneira diferente foi marcada como "fora de sincronia" e o jogo parou. Ainda era possível enganar localmente a divulgação de informações, mas esses vazamentos eram relativamente fáceis de corrigir em patches e revisões subsequentes. A segurança se tornou nossa maior vitória.

Problemas ocultos


A princípio, pode parecer que a mesma execução de duas instâncias do mesmo código é fácil de implementar, mas não é. Nos estágios iniciais do projeto, o gerente de produtos da Microsoft, Tim Znamenachek, disse a Mark: “Cada projeto possui um bug persistente que não desiste até o final. Acho que, no nosso caso, estará fora de sincronia. E ele estava certo. As dificuldades de encontrar erros fora de sincronia se multiplicavam a cada pequena alteração. O cervo, cuja posição é ligeiramente diferente ao criar um mapa aleatório, se moverá um pouco diferente e, minutos depois, o caçador se afastará um pouco do caminho ou perderá uma lança, como resultado de voltar para casa sem carne. Portanto, o que às vezes parecia ser apenas uma diferença nas somas de verificação da quantidade de comida tinha motivos muito difíceis de rastrear.

Embora tenhamos verificado o mundo, os objetos, a busca de caminhos, a mira e todos os outros sistemas com somas de verificação, sempre havia algo que não podíamos levar em consideração. Grandes volumes (50 MB cada) de rastreamento de mensagens e despejos de objetos do mundo tornaram o problema ainda mais complicado. Parte das dificuldades era conceitual - os programadores não estavam acostumados a escrever código que usava o mesmo número de chamadas de gerador de números aleatórios em uma simulação (sim, números aleatórios também eram gerados e sincronizados).

imagem

Lições aprendidas


Ao desenvolver a parte da rede do Age of Empires, recebemos várias lições que podem ser aplicadas ao desenvolvimento de qualquer sistema multiusuário de jogos.

Aprenda seu usuário. Estudar o usuário é o passo mais importante para entender suas expectativas em relação à velocidade do multiplayer, aos freios e atrasos percebidos nos comandos de transmissão. Cada gênero é individual e você precisa entender o que combina com seu estilo de jogo e gerenciamento.

Nos estágios iniciais do processo de desenvolvimento, Mark e o designer principal atrasaram a troca de dados com prototipagem (esse protótipo foi revisado várias vezes durante o processo de desenvolvimento). Como eles estavam jogando um jogo para um jogador, era muito fácil simular diferentes níveis de atrasos na transferência de equipes e obter feedback dos jogadores (“o controle parece bom / lento / tremendo / péssimo”).

Para jogos do gênero RTS, os atrasos na transmissão de comandos de 250 milissegundos nem são perceptíveis, a 250-500 ms a jogabilidade é bastante jogável e os freios tornam-se visíveis a 500 ms ou mais. Também é interessante notar que os jogadores estão acostumados ao "ritmo do jogo" e à expectativa mental de um atraso entre os cliques do mouse e as reações da unidade. Uma resposta atrasada constante era melhor do que saltos em atrasos na transmissão de comandos (por exemplo, de 80 a 500 ms) - nesse caso, atrasos constantes de 500 ms eram percebidos como jogáveis, e os mutáveis ​​pareciam "trêmulos" e complicando o jogo.

Isso forçou os programadores a direcionar seus esforços para garantir a suavidade - é melhor escolher uma duração de curso mais longa e ter certeza de que tudo será suave e constante do que realizar operações o mais rápido possível, diante de lentidões regulares. Todas as mudanças na velocidade devem ser graduais e a taxa de crescimento deve ser a menor possível.

Também medimos os requisitos do usuário para o sistema - geralmente eles davam comandos (mover, atacar, cortar árvores) aproximadamente a cada meio e dois segundos, às vezes com picos de 3-4 equipes por segundo durante batalhas ferozes. Como as ações ativas em nosso jogo estão em constante crescimento, os mais altos requisitos para troca de dados surgem no meio e perto do final do jogo.

Se você dedicar algum tempo para estudar o comportamento do usuário, notará outros recursos de como eles jogam, e isso ajudará na configuração do jogo em rede. No AoE, durante os ataques, os usuários clicavam rapidamente no mouse (clique-clique-clique-clique - avançar-avançar-avançar-avançar-avançar!), O que levou a enormes picos no número de comandos emitidos. Além disso, eles enviaram grandes grupos de unidades que precisam abrir o caminho - também altos picos nos requisitos de transmissão de dados pela rede. Um filtro simples, cortando comandos repetidos em um ponto, reduziu significativamente o impacto negativo desse comportamento.

Em geral, os usuários de monitoramento permitem:

  • Descubra as expectativas dos usuários sobre atrasos nos jogos
  • Protótipo de aspectos multiplayer nos estágios iniciais de desenvolvimento
  • Veja um comportamento prejudicial à velocidade do modo multiusuário.

Medição é a coisa mais importante. Se você introduzir métricas nos estágios iniciais do trabalho, aprenderá coisas surpreendentes sobre o seu sistema de troca de dados. Torne as métricas legíveis para os testadores e use-as para entender o que está acontecendo dentro do mecanismo de rede.

Lição: Parte do problema com a troca de dados no AoE surgiu quando Mark deduziu as métricas muito cedo e não verificou novamente os níveis de mensagens (comprimento e frequência) depois de preparar o código final. Coisas inesperadas, como corridas aleatórias entre IAs, caminhos difíceis de calcular e pacotes de comandos mal estruturados podem causar enormes problemas de desempenho, mesmo quando o sistema está funcionando bem.

Faça o sistema notificar testadores e desenvolvedores do que parece ser um excesso de condições de contorno - programadores e testadores verão no processo de desenvolvimento quais tarefas carregam o sistema; isso resolverá problemas nos estágios iniciais de sua ocorrência.

Reserve um tempo para explicar aos testadores como o sistema de troca de dados funciona, mostrar e explicar métricas para eles - você pode se surpreender ao perceber que eles ocorrem quando falhas estranhas ocorrem inevitavelmente no código da rede.

Em geral, as métricas devem ter as seguintes propriedades:

  • Seja humano legível e compreensível para os testadores
  • Indicar gargalos, freios e problemas
  • Pouco impacto na execução e constantemente sendo lançado.

Treinamento para desenvolvedores. É muito difícil ensinar aos programadores que estão acostumados a criar aplicativos de usuário único a pensar sobre a separação entre dar, receber e processar um comando. É fácil esquecer que você pode pedir algo que não aconteceu ou o que poderia acontecer alguns segundos após o comando ter sido emitido. Os comandos devem ser verificados quanto à correção no envio e no recebimento.

Em um modelo síncrono, os programadores também precisam considerar que, dentro da simulação, o código não deve depender de nenhum fator local (como disponibilidade de tempo livre, equipamento especial ou configurações diferentes). A execução do código em todas as máquinas deve corresponder. Por exemplo, a presença de sons aleatórios do terreno em uma simulação pode levar a um comportamento diferente do jogo.

Outras lições. Esse deve ser o senso comum comum - mas se você depende de uma rede de terceiros (no nosso caso, é o DirectPlay), escreva um aplicativo de teste independente confirmando que, quando os proprietários reivindicam "entrega garantida", as mensagens realmente recebem essa "ordem de pacote garantida" de fato, existe e que o produto não possui gargalos ocultos ou comportamento estranho ao processar os dados transmitidos no seu jogo.

Prepare-se para criar aplicativos de simulação e simuladores de teste de estresse. No final, criamos três aplicativos de teste mínimos diferentes usados ​​para estudar problemas individuais e importantes: inundações de conexão, problemas com conexões simultâneas na seleção de oponentes e perda de pacotes garantidos.

Teste com modems (e, com sorte, com simuladores de modem) o mais cedo possível; Continue o teste do modem (não importa o quão doloroso possa ser) ao longo do processo de desenvolvimento. ( — , , , , - ?), dialup-, LAN. , LAN.

imagem

Age of Empires 2


Age of Empires 2: The Age of Kings , , The Zone. , DirectPlay , , Age of Empires .

, , «» . -. , , . . , , , .

() The Zone Age of Empires . Age of Kings , . , , , . . The Zone , . - The Zone.

imagem

RTS3:


RTS3 — Ensemble (. .: Age of Mythology) . RTS3 , Age of Empires, .

  • Age of Empires 1 2 . , , , .
  • 3D: RTS3 — .
  • — .
  • TCP/IP: — - TCP/IP 56 /.
  • — , NAT.

RTS3 , Age of Empires 1 2 — — RTS3 . AOE/AOK DirectPlay, RTS3 , .

, . , , . Genie , — BANG! , .

Age of Kings , , . , , .

RTS3



6. - RTS3.

- . RTS3 - (. 6). -, , , .

. . , (, , -). ( Channels, TimeSync ..) , .

. Genie peer-to-peer, «». RTS3 , .

«» ( 7). . Age 1 2 .


7. «» .

Peer-to-peer:

  • «-» «--».
  • — ( ) , .

Peer-to-peer:

  • ( n=0 k-1 (n)), .
  • NAT.

Net.lib. RTS3 , , , , . , , , , , .


8. .

RTS3 BANG! , , , . , BANG! ( ). , , , OSI, (. 8).

Socks, 1

, Socks, API C. . . Socks .

Link, 2

2, Link, . , Link, Listener, NetworkAddress Packet, , (. 9).

  • Packet (): — , / ( ) .
  • Link (): . , . send receive , , void*.
  • Listener (): . .
  • Data stream ( ): , , , .
  • Net Address ( ): , .
  • Ping: . , .

  • 9. Link.

Multiplayer, 3
— , API net.lib. , RTS3 , / — , .

BANG! , . API , , .

  • Client (): . () ( ). , .
  • Session (): , , , . . host() join(), , , . / , .
  • Channel Ordered Channel: . . TimeSync, .
  • Shared Data: . , , .
  • Time Sync: .

Game Communications, 4

RTS3. , , . , , .

imagem


Sistema de sincronização aprimorado. Nenhuma equipe de desenvolvimento do Age of Empires poderia dizer que não precisamos de melhores ferramentas de sincronização. Como em qualquer projeto, ao analisar o processo de desenvolvimento post-mortem, verifica-se que a maior parte do tempo foi gasta em algumas áreas, mas poderia ser muito menor se os abordássemos com antecedência. No início do desenvolvimento do RTS3, no topo da lista dessas áreas estava a sincronização de depuração.

O sistema de rastreamento de sincronização RTS3 visa principalmente reconhecer rapidamente erros de sincronização. Outras prioridades foram a simplificação do uso, a capacidade de processar arbitrariamente grandes quantidades de dados sincronizados transmitidos pelo sistema, a capacidade de compilar completamente o código de sincronização na compilação do release e, finalmente, a capacidade de alterar completamente a configuração do teste alterando variáveis ​​em vez de recompilar completamente.

A verificação de sincronização no RTS3 é realizada usando dois conjuntos de macros:

#define syncRandCode(userinfo)
gSync->addCodeSync(cRandSync, userinfo, __FILE__, __LINE__)


#define syncRandData(userinfo,
v) gSync->addDataSync(cRandSync, v, userinfo, __FILE__, __LINE__)


Ambas as macros recebem o parâmetro de cadeia userinfo, que é o nome ou a indicação de um elemento sincronizado específico. Por exemplo, uma chamada de sincronização pode ter esta aparência:

syncRandCode("syncing the random seed", seed);

Comandos síncronos do console e variáveis ​​de configuração. Como qualquer desenvolvedor de mod do Quake pode confirmar, os comandos do console e as variáveis ​​de configuração são muito importantes para o processo de desenvolvimento. Os comandos do console são simples chamadas de função feitas usando o arquivo de configuração de inicialização, o console do jogo ou a interface do usuário, que invocam a funcionalidade arbitrária do jogo. As variáveis ​​de configuração são denominadas tipos de dados fornecidos pelas funções simples get, set, define e toggle, que usamos para todos os tipos de teste e definição de parâmetros de configuração.

Paul criou versões compatíveis com vários jogadores de nossos sistemas de comando do console e configurações variáveis. Com a ajuda deles, podemos transformar convenientemente uma variável de configuração regular (por exemplo, enableCheating) em uma variável de configuração multijogador, adicionando um sinalizador à definição da variável de configuração. Se esse sinalizador estiver ativado, a variável de configuração é transferida para o jogo multiplayer e as decisões sincronizadas no jogo (por exemplo, na permissibilidade da transferência gratuita de recursos) podem ser baseadas em seu valor. Os comandos do console do multiplayer têm um princípio semelhante - as chamadas para os comandos do console do multiplayer são transmitidas pela rede e executadas de forma síncrona em todas as máquinas clientes.

Ao usar essas duas ferramentas, os desenvolvedores podem usar o sistema multiplayer sem escrever código. Eles podem adicionar rapidamente novas ferramentas de teste e configuração e integrá-las facilmente em um ambiente de rede.

Resumir


A simulação sincronizada e o modelo ponto a ponto foram usados ​​com sucesso na série de jogos Age of Empires. Apesar da importância crítica de investir tempo na criação de ferramentas e tecnologias para solucionar os principais problemas dessa abordagem (como sincronização e métricas de rede), a viabilidade dessa arquitetura no gênero de estratégias em tempo real foi comprovada pela experiência. As melhorias subsequentes que fizemos no RTS3 levaram ao fato de que a jogabilidade multiplayer é quase indistinguível da de usuário único, mesmo nas condições mais terríveis das conexões de rede.

Source: https://habr.com/ru/post/pt417703/


All Articles