A história do que você não precisa fazer durante o desenvolvimento

Prólogo: Para começar, falarei sobre o projeto para que haja idéias sobre como trabalhamos no projeto e para recriar a dor que sentimos.

Como desenvolvedor, entrei no projeto em 2015-2016, não me lembro exatamente, mas funcionou 2-3 anos antes. O projeto foi muito popular em seu campo, ou seja, servidores de jogos. Quão estranho isso não soou, mas projetos em servidores de jogos estão em andamento até hoje, recentemente vi vagas e trabalhei um pouco na mesma equipe. Como os servidores de jogos são criados em um jogo já criado, portanto, uma linguagem de script é usada para o desenvolvimento incorporado ao mecanismo de jogo.

Estamos desenvolvendo um projeto quase do zero no Mod de Garry (Gmod), é importante notar que, no momento em que escrevi, Harry já estava criando um novo projeto da S & Box no Unreal Engine. Ainda estamos sentados na Fonte.
O que geralmente não é adequado para o nosso tema do servidor.
imagem

"Qual é a sua história assustadora?" - você pergunta.

Temos um tema forte para o servidor de jogos, ou seja, "Stalker" e, mesmo com elementos de RP, a pergunta surge imediatamente: "Como todos podem implementar isso em um servidor?"

Considerando que o mecanismo de origem é antigo (a versão 2013 também é usada no Gmod 32 bits), você não cria grandes cartões, existem pequenas restrições no número de entidades, malhas e muito mais.
Quem trabalhou no motor vai entender.
Acontece que a tarefa geralmente é impossível: criar um perseguidor multijogador limpo com missões, elementos de RPG do próprio original e, de preferência, um pequeno enredo.

Primeiro, a ortografia inicial foi difícil (muitas ações da categoria: arremessar um objeto, erguer um objeto foram escritas do zero), esperando que fosse mais fácil continuar, mas os requisitos aumentaram. A mecânica do jogo estava pronta, restava apenas inteligência, atualização e todo tipo de coisas. Em geral, todos transferidos como podiam.

imagem

Os problemas já começaram durante o trabalho da primeira versão do release, a saber (atrasos, atrasos no servidor).

Parece que um servidor poderoso pode processar solicitações com calma e manter todo o Modo de Jogo.

Descrição simples do modo de jogo
Este é o nome de um conjunto de scripts escritos para descrever a mecânica do próprio servidor
Por exemplo: queremos o tema das agora populares “Royal Battles”, o que significa que o nome também deve corresponder à mecânica do jogo. "A geração de jogadores no avião, você pode pegar as coisas, os jogadores podem se comunicar, você não pode usar mais de um capacete, etc." - tudo isso é descrito pela mecânica do jogo no servidor.

Os atrasos estavam no lado do servidor devido ao grande número de jogadores, pois um jogador consome muita RAM de cerca de 80-120 mb (sem contar itens no inventário, habilidades etc.), e no lado do cliente houve uma forte queda FPS

A energia da CPU não era suficiente para o processamento da física, era necessário usar objetos com propriedades físicas menores.

Além disso, também foram nossos scripts auto-escritos que não foram otimizados.

imagem

Primeiro, é claro, lemos artigos sobre otimização em Lua. Até chegou ao suicídio que eles queriam escrever DLLs em C ++, mas surgiu o problema ao baixar a DLL do servidor para os clientes. Usando o C ++ para uma DLL, você pode escrever um programa que intercepta dados silenciosamente; os desenvolvedores do Gmod adicionaram uma extensão às exceções para download pelos clientes (segurança, embora isso nunca tenha acontecido). Embora fosse conveniente e o Gmod se tornasse mais flexível, mas mais perigoso.

Em seguida, examinamos o criador de perfil (felizmente, as pessoas inteligentes o escreveram) e houve um horror nas funções, percebeu-se que já havia inicialmente funções muito lentas na biblioteca de mecanismos Gmod.

Se você tentou escrever no Gmod, está ciente de que existe uma biblioteca interna chamada matemática.

E as funções mais lentas são, obviamente, math.Clamp e math.Round.

Vasculhando o código das pessoas, percebeu-se que as funções eram lançadas em direções diferentes, sendo usadas em quase todos os lugares, mas incorretamente!

Vamos praticar. Por exemplo, queremos arredondar as coordenadas do vetor de posição para mover a entidade (por exemplo, um jogador).

local x = 12.5 local y = 14.9122133 local z = 12.111 LocalPlayer():SetPos( Vector( Math.Round(x), Math.Round(y), Math.Round(z) ) 

3 funções de arredondamento complexas, mas nada sério, a não ser, é claro, em um loop e nem sempre são usadas, mas o Clamp é ainda mais difícil.

O código a seguir é frequentemente usado em projetos e ninguém deseja alterar nada.

 self:setLocalVar("hunger", math.Clamp(current + 1, 0, 100)) 

Por exemplo, auto aponta para o objeto de um jogador e ele possui uma variável local que inventamos, que quando redefinida para o servidor é redefinida, math.Clamp essencialmente como um loop faz uma atribuição suave, eles gostam de criar uma interface suave no Clamp.

Os problemas surgem quando ele funciona em todos os jogadores que visitam o servidor. Um caso raro, mas se 5-15 (depende imediatamente da configuração do servidor) entrar no servidor em um determinado momento e essa função pequena e simples começar a funcionar para todos, o servidor terá bons atrasos na CPU. Pior ainda se math.Clamp estiver em um loop.

A otimização é realmente muito simples, você localiza funções de carga pesada. Parece primitivo, mas em 3 modos de jogo e muitos add-ons eu vi esse código lento.

Se você precisar obter o valor e usá-lo no futuro, não será necessário recuperá-lo novamente, se ele não mudar. Afinal, um jogador que entrar no servidor, em qualquer caso, receberá uma fome igual a 100, portanto esse código é muitas vezes mais rápido.

 local value = math.Clamp(current + 1, 0, 100) self:setLocalVar("hunger", value) 

Tudo está bem, eles começaram a analisar mais detalhadamente como isso funciona. Como resultado, criamos uma mania para otimizar tudo.

Percebemos que o padrão para loop é lento e decidimos criar nossa própria bicicleta, que será mais rápida (não esquecemos o blackjack) e, então, o jogo começou.

imagem

Spoiler
Nós até conseguimos fazer o loop mais rápido no Lua Gmod, mas com a condição de que deveria haver mais de 100 elementos.

A julgar pelo tempo gasto em nosso ciclo e seu uso no código, tentamos em vão fazer isso, porque ele encontrou aplicação apenas no spawn no mapa de anomalias após jogá-las e limpá-las.
E assim para o código. Por exemplo, você precisa encontrar todas as entidades com um nome no início do anom, temos anomalias nesse nome de classe.

Aqui está o script normal no Lua Gmod:

 local anomtable = ents.FindByClass("anom_*") for k, v in pairs(anomtable) do v:Remove() end 

Aqui é para o fumante:

É imediatamente óbvio que esse código g * obviamente será mais lento que o padrão "para pares", mas, como se viu, não.

 local b, key = ents.FindByClass("anom_*"), nil repeat key = next(b, key) b[key]:Remove() until key != nil 


Para analisar completamente essas opções de loop, você precisa convertê-las em um script Lua regular.
Por exemplo, anomtable terá 5 elementos.
A remoção é substituída pela adição usual. O principal é ver a diferença no número de instruções entre as duas opções para implementar o loop for.

Ciclo de baunilha:

 local anomtable = { 1, 2, 3, 4, 5 } for k, v in pairs(anomtable) do v = v + 1 end 

O nosso é ótimo:

 local b, key = { 1, 2, 3, 4, 5 }, nil repeat key = next(b, key) b[key] = b[key] + 1 until key ~= nil 

Vejamos o código do intérprete ( como um montador, não é recomendável procurar um spoiler como um programador de alto nível ).

Apenas no caso, remova os jones das telas. Eu avisei.

Desmontador do ciclo de baunilha
 ; Name: for1.lua ; Defined at line: 0 ; #Upvalues: 0 ; #Parameters: 0 ; Is_vararg: 2 ; Max Stack Size: 7 1 [-]: NEWTABLE R0 5 0 ; R0 := {} 2 [-]: LOADK R1 K0 ; R1 := 1 3 [-]: LOADK R2 K1 ; R2 := 2 4 [-]: LOADK R3 K2 ; R3 := 3 5 [-]: LOADK R4 K3 ; R4 := 4 6 [-]: LOADK R5 K4 ; R5 := 5 7 [-]: SETLIST R0 5 1 ; R0[(1-1)*FPF+i] := R(0+i), 1 <= i <= 5 8 [-]: GETGLOBAL R1 K5 ; R1 := pairs 9 [-]: MOVE R2 R0 ; R2 := R0 10 [-]: CALL R1 2 4 ; R1,R2,R3 := R1(R2) 11 [-]: JMP 13 ; PC := 13 12 [-]: ADD R5 R5 K0 ; R5 := R5 + 1 13 [-]: TFORLOOP R1 2 ; R4,R5 := R1(R2,R3); if R4 ~= nil then begin PC = 12; R3 := R4 end 14 [-]: JMP 12 ; PC := 12 15 [-]: RETURN R0 1 ; return 


Desmontador de bicicletas
 ; Name: for2.lua ; Defined at line: 0 ; #Upvalues: 0 ; #Parameters: 0 ; Is_vararg: 2 ; Max Stack Size: 6 1 [-]: NEWTABLE R0 5 0 ; R0 := {} 2 [-]: LOADK R1 K0 ; R1 := 1 3 [-]: LOADK R2 K1 ; R2 := 2 4 [-]: LOADK R3 K2 ; R3 := 3 5 [-]: LOADK R4 K3 ; R4 := 4 6 [-]: LOADK R5 K4 ; R5 := 5 7 [-]: SETLIST R0 5 1 ; R0[(1-1)*FPF+i] := R(0+i), 1 <= i <= 5 8 [-]: LOADNIL R1 R1 ; R1 := nil 9 [-]: GETGLOBAL R2 K5 ; R2 := next 10 [-]: MOVE R3 R0 ; R3 := R0 11 [-]: MOVE R4 R1 ; R4 := R1 12 [-]: CALL R2 3 2 ; R2 := R2(R3,R4) 13 [-]: MOVE R1 R2 ; R1 := R2 14 [-]: GETTABLE R2 R0 R1 ; R2 := R0[R1] 15 [-]: ADD R2 R2 K0 ; R2 := R2 + 1 16 [-]: SETTABLE R0 R1 R2 ; R0[R1] := R2 17 [-]: EQ 1 R1 K6 ; if R1 == nil then PC := 9 18 [-]: JMP 9 ; PC := 9 19 [-]: RETURN R0 1 ; return 


Uma pessoa inexperiente dirá simplesmente que um ciclo regular é mais rápido porque há menos instruções (15 vs. 19).

Mas não devemos esquecer que cada instrução no intérprete possui ciclos de processador.
A julgar pelo código do desmontador no primeiro ciclo, há uma instrução de forloop escrita previamente para trabalhar com o array, o array é carregado na memória, torna-se global, saltamos sobre os elementos e adicionamos uma constante.

Na segunda variante, o método é diferente, mais baseado na memória, recebe uma tabela, altera um elemento, define uma tabela, verifica nula e chama novamente.
Nosso segundo ciclo é rápido, porque há muitas condições e ações em uma instrução (R4, R5: = R1 (R2, R3); se R4 ~ = zero, então inicia PC = 12; R3: = R4 final) por causa disso come muito , come ciclos de clock da CPU para execução, o passado está novamente mais ligado à memória.

A instrução, com um grande número de elementos, rende-se ao nosso ciclo na velocidade de passagem de todos os elementos. Isso se deve ao fato de que o endereçamento diretamente para o endereço é mais rápido, menor do que os brindes dos pares. (E não temos negação)
Em geral, em segredo, qualquer uso de negação no código diminui a velocidade; isso já foi testado por testes e tempo. A lógica negativa funcionará mais lentamente, pois a ALU do processador possui uma unidade de computação separada “inversor”, para o operando unário (não ,!) Para funcionar, você precisa acessar o inversor e isso levará um tempo adicional.
Conclusão: Tudo o que é padrão nem sempre é melhor, suas bicicletas podem ser úteis, mas novamente em um projeto real, você não deve apresentá-las se a velocidade de liberação for importante para você. Como resultado, concluímos o desenvolvimento completo de 2014 até os dias atuais, uma espécie de mais uma "espera". Embora pareça um servidor de jogo comum, instalado em 1 dia e totalmente configurado para o jogo em 2 dias, você precisa apresentar algo novo.

Esse projeto de longo prazo ainda viu a segunda versão de si mesma, onde há muitas otimizações no código, mas falarei sobre outras otimizações nos artigos a seguir. Suporte com críticas ou comentários, corrija se estou enganado.

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


All Articles