1 pontos por GN⁺ 2024-06-14 | 1 comentários | Compartilhar no WhatsApp
  • A Serious Engine 1 colocava single-player, multiplayer e reprodução de demo sobre a mesma simulação determinística, registrando e transmitindo ações dos jogadores e blocos do fluxo do jogo em vez do estado completo a cada tick
  • Demo e multiplayer compartilham um estado inicial do jogo e depois aplicam deltas de entrada como CPlayerAction, então a deterministicidade da lógica do jogo e da semente de números aleatórios é a base da sincronização
  • A camada de rede tratava diretamente números de sequência, ACK, retransmissão, limitação de largura de banda e buffers por conexão sobre UDP para manter a jogabilidade mesmo em linhas lentas
  • CNetworkMessage oferece serialização, compressão LZ77/LZRW1 e codificação delta baseada em XOR, mas as mensagens, incluindo chat, não são criptografadas e podem receber apenas compressão
  • O modelo cliente-servidor de Serious Sam economiza banda ao fazer cada cliente manter sua própria simulação, mas exige mecanismos auxiliares como verificação de sincronização, predição, retransmissão e checagem de CRC

Estrutura de simulação comum da Serious Engine

  • O código-fonte da Serious Engine 1 foi aberto em 2016 sob a GNU GPL v2, e a análise se baseia na leitura e depuração desse código publicado
  • Serious Sam foi projetado desde o início como um jogo multiplayer, e até a campanha single-player funciona internamente como um caso especial da estrutura multiplayer
  • Os modos suportados pela engine são os seguintes
    • campanha single-player offline
    • jogo cooperativo online, LAN e local, além de vários modos de jogo
    • tela dividida com vários jogadores em um único cliente
    • gravação e reprodução de demo

Gravação de demo: registrar ações em vez do estado completo

  • Salvar o estado completo do jogo a cada tick deixaria o arquivo grande, então a Serious Engine salva uma vez o estado completo do jogo no início da gravação e depois registra blocos do fluxo do jogo a cada tick
  • Os blocos do fluxo do jogo incluem os seguintes tipos de mensagem
    • MSG_SEQ_ALLACTIONS: ações dos jogadores
    • MSG_SEQ_ADDPLAYER: adicionar jogador
    • MSG_SEQ_REMPLAYER: remover jogador
    • MSG_SEQ_PAUSE: pausar ou retomar
    • MSG_SEQ_CHARACTERCHANGE: alterar atributos do personagem do jogador
  • O elemento central é MSG_SEQ_ALLACTIONS, e a engine desserializa um objeto CPlayerAction para cada jogador ativo e o aplica a CPlayerTarget
  • CPlayerAction contém o estado do jogador
    • velocidade de movimento em coordenadas do mundo pa_vTranslation
    • rotação do personagem em coordenadas do mundo pa_aRotation
    • rotação da visão em coordenadas do mundo pa_aViewRotation
    • botões pressionados no momento pa_ulButtons
    • timestamp em milissegundos baseado em TSC pa_llCreated
  • Na reprodução, o estado inicial do jogo é lido e as ações dos jogadores de cada tick são aplicadas como se fosse uma partida real

Por que a deterministicidade é necessária

  • Essa estrutura parte da premissa de que tudo no jogo é totalmente previsível e que apenas as ações dos jogadores alteram o jogo
  • Até os números aleatórios são tratados com um gerador pseudoaleatório que usa uma semente como parte do estado do jogo
    • CEntity::IRnd() usa CSessionState::Rnd()
    • ses_ulRandomSeed é inicializado durante a desserialização do estado do jogo
  • Se um gerador realmente aleatório ou sementes diferentes forem usados, a reprodução da mesma demo pode produzir resultados diferentes, levando a desincronização

Ponto flutuante e processamento por tick

  • Como a versão de PC de Serious Sam foi lançada originalmente só para Windows, a suposição de mesmo compilador e mesmo runtime ajudava a reduzir problemas de sincronização de ponto flutuante
  • O renderizador fica em uma DLL, e chamadas a APIs OpenGL ou DirectX podem mudar a precisão da FPU, então a Serious Engine usa guardas de precisão como CSetFPUPrecision FPUPrecision(FPT_24BIT)
  • Não foi encontrado um ponto que configure explicitamente o controle de arredondamento, mas há asserts que verificam o estado _RC_NEAR com _controlfp
  • A lógica do jogo é separada do frame rate de renderização
    • a renderização varia conforme hardware e configuração, e aparentemente é limitada internamente a 500 FPS
    • a lógica do jogo fica fixa em 20 ticks por segundo
  • O movimento suave é produzido por interpolação linear entre o tick atual e o anterior, e a interpolação pode ser desativada no console com /net_bLerping=0

Camada própria de pacotes sobre UDP

  • No multiplayer da Serious Engine ainda existem nomes de função como StartPeerToPeer_t, mas o modelo real é cliente-servidor
  • O servidor recebe e processa mensagens dos clientes e repassa as informações relevantes a todos os clientes
  • Cada jogador executa sua própria simulação, de forma parecida com o sistema de demo, e avança o estado ao receber informações sobre as ações dos outros jogadores
  • A rede usa UDP e coloca por cima um protocolo próprio para lidar com pacotes fora de ordem e perdas
  • CPacket gerencia ordem e confiabilidade dos pacotes
    • pa_ulSequence: número de sequência para ordenação e remoção de duplicatas
    • pa_ubReliable: flag de confiabilidade
    • pa_ubRetryNumber: acompanha a contagem de retransmissões
    • pa_tvSendWhen: usado para horário de envio e controle de congestionamento
  • Pacotes confiáveis aguardam ACK e são retransmitidos se o ACK não chegar
    • o número máximo de tentativas é definido por net_iMaxSendRetries, com valor padrão aparentemente 10
    • o intervalo entre retransmissões é definido por net_fSendRetryWait, com valor padrão aparentemente 0.5f
  • Pacotes confiáveis podem formar um fluxo dividido em vários pacotes
    • o primeiro pacote recebe UDP_PACKET_RELIABLE_HEAD
    • o último pacote recebe UDP_PACKET_RELIABLE_TAIL
    • um pacote confiável único carrega as duas flags
  • Pacotes não confiáveis não formam fluxo, porque a perda de um deles pode quebrá-lo

Ciclo de vida da conexão e roteamento de pacotes

  • CCommunicationInterface é responsável pela comunicação da camada de pacotes e tem funções de interface para servidor, cliente e broadcast
  • As interfaces de servidor e cliente assumem que o destino da conexão já é conhecido, e a interface de broadcast é usada para envio e recepção com endereços arbitrários
  • CCommunicationInterface mantém dois buffers mestres
    • cci_pbMasterInput: desserializa pacotes UDP recebidos em CPacket e os armazena
    • cci_pbMasterOutput: serializa CPacket a enviar e o transmite pela API de socket
  • A abstração real de comunicação por cliente fica a cargo de CClientInterface
    • o servidor mantém uma interface para cada jogador no array cm_aciClients
    • o cliente se comunica com o servidor por cm_ciLocalClient
    • tanto cliente quanto servidor usam cm_ciBroadcast para estabelecer conexão
  • adr_uwID de CAddress é usado como identificador único do cliente ou como marca de pacote de broadcast
    • se o valor for '//' ou 0, trata-se de um pacote de broadcast
    • qualquer outro valor é o ID do cliente dentro da sessão

Estabelecimento de conexão e mecanismos básicos de segurança

  • Para se conectar ao servidor, o cliente envia um pacote confiável de broadcast com a flag UDP_PACKET_CONNECT_REQUEST
  • Se já houver um cliente conectado com o mesmo endereço e porta, o servidor ignora a solicitação
  • Se for um novo cliente, o servidor procura uma interface de cliente vazia e faz o seguinte
    • gera um identificador único para esse cliente
    • envia o identificador ao cliente em um pacote confiável de broadcast UDP_PACKET_CONNECT_RESPONSE
  • O identificador não usa apenas um índice fixo; ele combina parte de um valor de timer com o índice do cliente
  • Como é preciso acertar o uwID para se passar por outro jogador, a superfície de ataque diminui
  • Se um jogador não conectado enviar um pacote que não seja de broadcast, a Serious Engine pode emitir um aviso no console

Single-player e demo como casos especiais de conexão local

  • Single-player e reprodução de demo também têm internamente servidor e cliente, mas ambos rodam no mesmo processo
  • Como não há necessidade de usar sockets entre componentes no mesmo processo, Client_OpenLocal() conecta diretamente a interface de cliente local e a interface do lado do servidor
  • Os dois CClientInterface conectados movem pacotes do buffer de saída de um lado para o buffer de entrada do outro com ExchangeBuffers
  • O jogo local não precisa passar pelos buffers mestres de entrada e saída nem pelos sockets reais de rede

Camada de mensagens de rede

  • CNetworkMessage é a abstração de mensagem acima dos pacotes e pode ser lido e escrito como um stream
  • As mensagens são serializadas e desserializadas com Read, Write, ReadBits, WriteBits e os operadores << e >>
  • Também podem conter submensagens, e depois de escrever os dados necessários é possível ajustar o tamanho do buffer ao tamanho real dos dados com Shrink
  • O buffer de CNetworkMessage é alocado por AllocMemory, que aparentemente chama malloc internamente
  • CLinearAllocator existe, mas não foi encontrado uso dele, e os buffers de mensagem são alocados e realocados com frequência

Compressão e codificação delta

  • Mensagens podem ser comprimidas com o compressor especificado ou com o compressor padrão para cada tipo de mensagem
  • MESSAGETYPE usa os 6 bits inferiores para o tipo e os 2 bits restantes para o método de compressão
    • LZ77 CzlibCompressor
    • LZRW1 CLZCompressor
    • sem compressão
  • A compressão padrão parece ser LZRW1, e isso pode ser alterado pela variável de shell net_iCompression
  • CPlayerAction não é enviado diretamente; em vez disso, é transmitido um delta obtido por XOR entre a ação atual e a ação anterior
  • No lado receptor, o delta é novamente aplicado por XOR à última ação para restaurar o CPlayerAction original
  • O delta comprime melhor quando as mudanças nos dados são pequenas
    • teclas pressionadas costumam permanecer iguais por vários frames
    • velocidade e rotação da visão também não variam por toda a faixa de ponto flutuante
  • Quando o servidor envia várias ações de jogadores de uma vez com MSG_SEQ_ALLACTIONS, o efeito desse método pode ser ainda maior

Criptografia de mensagens e chat

  • As mensagens da Serious Engine não são criptografadas
  • Se a compressão for desativada com net_iCompression=0, as mensagens de chat do jogo ficam visíveis em texto puro no payload dos pacotes UDP
  • Em situações reais, se a compressão estiver ativada, quem interceptar os pacotes precisa identificar e descomprimir o fluxo LZ, mas os dados necessários estão dentro dos próprios pacotes
  • Na época, muitos jogos não lidavam com criptografia, e implementar mecanismos como autenticação e troca de chaves poderia aumentar a complexidade
  • Na web daquela época, a maioria dos sites também ainda usava HTTP

Camada de sessão de jogo

  • CNetworkLibrary, apesar do nome, gerencia a sessão do jogo, incluindo o estado CSessionState
  • CNetworkLibrary herda do CMessageDispatcher mencionado anteriormente
  • Ao iniciar o servidor, a engine executa o seguinte procedimento
    • inicializa a coleta de CRC para preparar a verificação de que os clientes conectados têm os mesmos arquivos do servidor
    • cria um novo CSessionState, serializa esse estado e o salva como estado padrão em ga_pubDefaultState
    • carrega a instância local do mundo
    • inicializa a interface global de comunicação
    • inicializa o estado da sessão local para que, quando clientes se conectarem, seja possível enviar o delta de estado entre o estado padrão e o estado atual do servidor
    • encerra a coleta de CRC e salva o valor em ga_ulCRC
  • A checagem de CRC está mais próxima de detecção precoce de desincronização do que de um sistema antitrapaça
  • O processo de entrada do cliente segue o fluxo abaixo
    • inicializa um estado de sessão local vazio e a interface de comunicação
    • envia MSG_REQ_CONNECTREMOTESESSIONSTATE com versão da build, nome do modo, senha do servidor, número de jogadores locais e CSessionSocketParams
    • recebe MSG_REP_CONNECTREMOTESESSIONSTATE com mensagem, nome do arquivo de mundo, flags de dificuldade e modo de jogo e propriedades da sessão
    • inicializa o estado-base do jogo
    • envia MSG_REQ_STATEDELTA para pedir a diferença em relação ao estado atual do servidor
    • depois de receber MSG_REP_STATEDELTA, reconstrói o stream do estado do jogo com um diff reverso
    • inicializa o estado da sessão local com CSessionState::Read_t()
    • executa a verificação de CRC e encerra a conexão em caso de divergência

Loop principal e retransmissão do fluxo do jogo

  • Os loops principais de cliente e servidor são em grande parte parecidos, mas o servidor executa tarefas adicionais
  • O loop atualiza a interface do cliente local e a interface de broadcast, e o estado da sessão local processa as mensagens de rede recebidas
  • O servidor também cuida da troca de buffers entre interfaces de cliente pareadas, da atualização das interfaces de cliente do lado do servidor, da atualização do GameAgent e do processamento de comandos de shell de administração remota
  • SessionStateLoop() separa o tratamento entre mensagens não confiáveis e confiáveis
    • não confiáveis: MSG_GAMESTREAMBLOCKS, MSG_KEEPALIVE, MSG_INF_PINGS, MSG_CHAT_OUT
    • confiáveis: MSG_INF_DISCONNECTED, MSG_ADMIN_RESPONSE
  • MSG_GAMESTREAMBLOCKS é uma mensagem não confiável, mas a perda dela pode quebrar a sincronização
  • A Serious Engine verifica sequências ausentes durante o processamento do fluxo do jogo e solicita retransmissão
    • se o próximo bloco de sequência esperado existir, ele é processado
    • se o próximo bloco não existir e também não houver blocos mais novos, nada é feito naquele loop
    • se o próximo bloco não existir, mas houver um bloco mais novo, define-se um timeout por possível perda
    • após o timeout, MSG_REQUESTGAMESTREAMRESEND solicita a sequência ausente e a quantidade de blocos
  • O servidor reenviará os blocos do fluxo do jogo que forem solicitados

Processamento de blocos do fluxo do jogo

  • MSG_SEQ_ADDPLAYER é enviado quando um jogador entra na partida e inclui o índice do jogador e um descritor CPlayerCharacter
  • MSG_SEQ_REMPLAYER é enviado quando o jogador se desconecta e contém apenas o índice do jogador
  • MSG_SEQ_CHARACTERCHANGE transmite mudanças de nome, time e aparência do jogador
    • em Serious Sam, o buffer de aparência contém uma estrutura CPlayerSettings
    • ela inclui nome do arquivo do modelo do jogador, política de seleção automática de arma, tipo de mira e várias flags
  • MSG_SEQ_PAUSE transmite pausa ou retomada e imprime no console o nome do jogador que solicitou isso
  • MSG_SEQ_ALLACTIONS contém o tempo do tick atual e as ações de todos os jogadores
    • aplica CPlayerAction a cada CPlayerTarget
    • depois executa timers, eventos, entidades móveis e processamento de física
  • A verificação de sincronização é feita por MakeSynchronisationCheck()
    • CSyncCheck é criado via ChecksumForSync() de entidades, alvos de jogador e outros elementos
    • o cliente envia MSG_SYNCCHECK ao servidor, e a conexão é encerrada se houver divergência em relação ao estado do servidor

Predição para reduzir atraso de entrada

  • A predição existe para reduzir a sensação de lentidão em jogos rápidos causada pela latência da internet
  • A predição do jogador local usa as ações enviadas ao servidor
  • A predição de jogadores remotos usa a última ação recebida do servidor
  • Se o cliente avançar diretamente o estado real do jogo sem esperar a resposta do servidor, ele não saberá as ações dos outros jogadores e a sincronização pode quebrar
  • Para não misturar estado real e estado previsto, a Serious Engine usa predictor
    • o predictor é algo próximo de uma cópia “fantasma” ligada à entidade comum
    • o temporary predictor é criado durante a predição e não tem entidade correspondente no estado real do jogo
  • Ao processar ticks de predição, apenas entidades predictor são processadas
  • Quando o cliente recebe ações dos jogadores vindas do servidor, ele destrói os predictors existentes e inicia um novo ciclo de predição
  • Na renderização, a entidade original em predição não é desenhada; o predictor é desenhado no lugar, para dar a impressão de movimento contínuo sem alterar muito o estado real
  • O jogador local só pode prever até a quantidade de ações enviadas ao servidor armazenadas em plt_abPrediction
  • Na predição de jogadores remotos, se cli_bLerpActions estiver desativado, a última ação recebida é repetida
  • Se cli_bLerpActions estiver ativado, a engine faz interpolação linear entre as duas últimas ações, mas o padrão é deixá-lo desativado

Comparação com Doom e Quake

  • A rede de Doom era na prática peer-to-peer, mas os clientes trocavam estruturas parecidas com CPlayerAction e cada um rodava sua própria simulação independente
  • Doom também usava um sistema semelhante para gravação e reprodução de demo
  • Quake adotou outra estrutura, mais próxima de um modelo em que o cliente não executa grande parte da lógica do jogo e recebe atualizações de estado do servidor
  • O modelo de Quake exige menos preocupação com sincronização e também pode facilitar antitrapaça, por exemplo quando o servidor não envia informações de entidades atrás da parede
  • Em Serious Sam, sessões com muito mais inimigos e objetos ativos do que em Quake eram comuns, então enviar o estado de muitos objetos a cada tick poderia impor um custo alto de largura de banda

Portabilidade e limitações estruturais

  • Algumas mensagens de rede serializam estruturas de forma próxima a um reinterpret cast
  • Isso pode funcionar quando se assume um único compilador e uma única plataforma, mas em jogos multiplataforma o layout e o padding das estruturas podem variar
  • Executáveis de 32 bits podem tentar alinhar em fronteiras de 4 bytes, enquanto executáveis de 64 bits podem tentar alinhamento em 8 bytes
  • Também há questões de endianness
    • PCs x86 usam little-endian
    • o PS3 usa big-endian
  • A estrutura da Serious Engine é elegante por abstrair, para a lógica do jogo, a diferença entre meios de transporte como rede e arquivo, mas o modelo em que todos os clientes mantêm uma cópia do estado do jogo abre espaço para trapaças
  • Por exemplo, um cliente modificado poderia destacar o contorno de outros jogadores atrás de paredes em partidas de deathmatch

1 comentários

 
GN⁺ 2024-06-14
Comentários do Hacker News
  • Fui um dos desenvolvedores responsáveis pela implementação do código de rede de Serious Sam
    Eu frequentemente dormia debaixo de uma mesa no escritório da Croteam e ficava fuçando a Usenet, especialmente textos que explicavam o sistema de predição de QuakeWorld, que me inspiraram
    Naquela noite, enquanto meu colega Dan testava usando uma velha máquina Unix 486 como roteador para simular latência, eu programei uma implementação mínima e simples
    Isso foi muito antes de o jogo de verdade ser construído em cima dela

    • Fico me perguntando por que fizeram aqueles homens-bomba sem cabeça correrem gritando, com o som ficando mais alto à medida que se aproximavam
      Ainda ouço aquele som
    • Adoro a atmosfera de alguém aparecer casualmente nos comentários de um texto sobre como um jogo lendário foi feito e dizer algo como “ah é, fui eu que fiz aquilo, foi divertido”
    • Eu gostava muito daquele jogo
      Sempre gostei muito de jogos cooperativos e de tiro, mas meus amigos só queriam jogar Counter-Strike
      Graças a Serious Sam, às vezes eu conseguia convencê-los a jogar algo de que eu gostava também
    • Serious Sam era um dos jogos favoritos do meu tio, junto com Duke Nukem 3D
      Ele foi uma pessoa muito importante na minha vida, e jogar os jogos de que ele gostava é uma boa forma de manter viva a memória dele
      Um ótimo jogo, um ótimo multiplayer e lembranças muito boas
    • O modo tela dividida foi algo pelo qual sou realmente grato
  • Serious Sam sempre foi um forte jogo para LAN party
    Não porque fosse o título mais vistoso da época, nem porque alguém tivesse planejado isso com antecedência
    Quando outros jogos morriam por causa de problemas de driver, aquecimento, atualizações e afins, você abria Serious Sam e ele simplesmente funcionava, então dominava as LAN parties
    Isso continuou nas continuações, e mesmo que o PC de alguém estivesse completamente ferrado, ele ainda oferecia suporte estável a tela dividida e lidava bem com os dispositivos de entrada
    A parte sistêmica do jogo era realmente excelente em termos de confiabilidade

    • Serious Sam rodava rápido até em hardware horrível e ainda parecia bem legal
      De forma parecida, Counter-Strike não tinha gráficos incríveis, mas rodava bem até em PCs torradeira, e por isso ficou popular por tanto tempo
    • O som de “aaaaaaaaaaaaah” vindo de vários alto-falantes era divertido
    • No fim dos anos 90, eu administrava o site de suporte técnico da EA, e a equipe de suporte/QA jogava Serious Sam em massa depois do expediente
      Era o único jogo de tiro em primeira pessoa que rodava de forma consistente nos PCs de trabalho, e era muito divertido
      Naquela época, QA e suporte técnico se sobrepunham bastante na EA; no verão, o pessoal do suporte trabalhava como beta testers internos para os lançamentos de fim de ano, e no inverno, quando as ligações aumentavam perto do Natal, voltavam ao suporte técnico
  • Quando implementei multiplayer no port de Game Boy Color de Vigilante 8, usei jogabilidade determinística
    O cabo link do GBC enviava e recebia 1 byte simultaneamente em duas direções, funcionando como um par de registradores de deslocamento que se preenchiam mutuamente através do cabo
    O jogo era fixado à taxa de quadros do GBC e, na prática, muita atualização de tela precisava acontecer a cada V-Blank; se isso fosse perdido, a rolagem suave quebrava
    No início do multiplayer, trocávamos as seeds, e a execução funcionava assim: no quadro A, lia-se a entrada e ela era comprimida em 1 byte e colocada no buffer de transmissão. Enquanto o quadro B era renderizado, a transmissão acontecia. No começo do quadro C, você então tinha a entrada local enviada no quadro A e a entrada do oponente recebida no quadro B
    Essas entradas eram aplicadas ao estado do jogo e o quadro C era renderizado, então tanto a entrada local quanto a remota eram aplicadas com 1 quadro de atraso
    O jogo local não tinha atraso de entrada, então, se você perdeu no multiplayer, pode culpar a latência e, se quiser, pode me culpar especificamente

    • Comprei esse cartucho algumas semanas atrás e, como gosto de cartuchos antigos de rumble do GBC, fiquei impressionado que ele tivesse multiplayer por cabo link
      É um jogo muito bom, e a explicação técnica da implementação do multiplayer também é excelente
  • A Croteam é realmente uma equipe de desenvolvimento de jogos muito talentosa
    Eu gostei muito de The Talos Principle 1 e 2, e no primeiro eles foram um dos pioneiros a criar cedo um motor de jogo Vulkan totalmente customizado

    • Fiquei bem triste por terem abandonado o próprio motor e usado a Unreal Engine em The Talos Principle 2
    • Acabei de descobrir que o DLC de Talos 2 sai nesta sexta-feira no Steam
  • Fico me perguntando se é a mesma ideia de “1500 arqueiros em 28.8K” de Age of Empires
    https://www.gamedeveloper.com/programming/1500-archers-on-a-...

    • Sim. Ambos usam um sistema de lockstep determinístico
      Muitos jogos usaram esse sistema por muito tempo, mas hoje em dia ele parece menos comum do que antes por vários motivos
    • Esse número mostra como é estranho Tempest Rising ter limite de unidades
      Basta haver recurso ou não; não faz sentido impor limite só por impor
  • Jogos com 10 vezes mais largura de banda também têm dificuldade para suportar essa quantidade de inimigos
    Só agora percebi que o aumento dos recursos tecnológicos talvez tenha um efeito colateral ruim sobre a eficiência e a criatividade na ciência da computação
    Quanto mais largura de banda, armazenamento, memória e poder de processamento temos, mais o software reage ficando mais lento, mais inchado e mais incompetente por unidade de recurso
    Dá para chamar isso de efeito Benjamin Button no design de software

    • Fico curioso para saber quantos seriam “essa quantidade de inimigos” em números
      Se o texto traz esse número, eu não encontrei
      Mesmo entre jogos modernos, há vários multiplayer com contagens de inimigos suficientemente “massivas”, e, se o importante for número de jogadores, também existem jogos que suportam grandes quantidades de jogadores
    • Normalmente isso é mais conhecido como bloat de software
    • Sim, isso é conhecido como lei de Wirth
  • Eu diria que foi um jogo em que passei mais tempo me movendo para trás do que indo para a frente

    • Existe até um jogo sobre isso, chamado “I Hate Running Backwards”
      Está no Steam e faz parte do universo de Serious Sam, embora eu não saiba se é da mesma desenvolvedora
    • Você e Netrisca estavam juntos, mas aqueles milhares de inimigos estavam sozinhos
    • Também havia armas que pareciam disparar frações de segundo antes de você apertar o botão do mouse
    • Também lembro de tentar desesperadamente pegar a munição que surgia enquanto eu recuava daquele jeito
  • A estrutura de Factorio também é parecida, transmitindo quase só eventos de entrada e dependendo de um núcleo de simulação lockstep
    Há algumas exceções visíveis, como a ferramenta de planejamento ferroviário

    • Um dia eu gostaria de trabalhar com uma estrutura lockstep dessas
      Parece uma restrição de projeto satisfatória e fácil de testar
  • Lembro de jogar Serious Sam em um demo da PC Gamer quando era criança
    Mesmo naquela época, ele já era visto como um jogo retrô que remetia aos velhos tempos de DOOM e Quake
    Agora se passaram literalmente 20 anos, e ele próprio virou um clássico

  • Starsiege: Tribes era grandioso e absurdamente divertido até em uma conexão 56K

    • Os desenvolvedores de Tribes escreveram um white paper sobre conceitos parecidos de código de rede
      https://www.gamedevs.org/uploads/tribes-networking-model.pdf
    • Talvez tenha sido meu jogo favorito da infância, especialmente Tribes 2
      Na verdade, baixei Tribes 2 há pouco tempo e joguei contra bots alguns meses atrás
      É um jogo antigo, mas ainda foi divertido, e eu penso com frequência em tentar recriá-lo com algo como Unity
      Talvez um dia eu faça isso
    • Tribes era excelente
      Em 1999, ver terreno gerado proceduralmente, descer por ele como se estivesse esquiando, ver outras pessoas subirem do mesmo jeito, e ainda ter mapas grandes e muitos jogadores, era impressionante