
Ao projetar um jogo multiplayer, quase o componente mais importante é o equilíbrio. O trabalho de um designer de jogos nesse sentido é semelhante ao trabalho de um analista de inteligência: se ele funciona bem, ninguém percebe. Vale a pena tropeçar, e os jogadores descaradamente aproveitam o erro. Mas a coisa mais interessante acontece quando, além do designer do jogo, o programador também está enganado ...
Neste artigo, consideraremos um elemento da estratégia dos cossacos 3 . O jogo contém vários tipos de mosqueteiros e outros atiradores dos séculos XVII e XVIII, bem como a oportunidade de explorar tecnologias que reduzem o tempo de recarga dos mosquetes. Existem duas melhorias no total, cada uma delas traz + 30% à taxa de tiro - de acordo com a interface do jogo.
Mas mesmo à vista, é claro que algumas unidades de combate, depois de pesquisar as melhorias, disparam não apenas em 60%, mas até várias vezes com mais frequência. Ao medir a taxa de tiro diretamente usando o timer de jogo embutido, surgem números completamente estranhos que não têm nada a ver com as porcentagens declaradas.
Sob o capô dos "cossacos"
Felizmente, o jogo é feito de uma maneira muito amigável para modders, então todos os scripts que precisamos estão disponíveis como arquivos de texto na pasta data / scripts / . A julgar pela sintaxe, os scripts são escritos em Delphi ou em uma linguagem muito semelhante. Vamos dar uma olhada na mecânica de calcular os intervalos entre os disparos.
Anotações- A análise foi realizada no jogo "Cossacks 3" versão 2.1.4.
- Todas as seções de script abaixo contêm um pseudocódigo simplificado.
Quando o jogo começa, todas as unidades de combate são inicializadas. O procedimento indica os valores de vitalidade, custo e armas para cada tipo. Para armas pequenas, é passado um parâmetro que indica o intervalo entre tiros nos quadros do jogo:
//lib/unit.script procedure _unit_InitBase() 'musketeer' : maxhp := 70; SetObjBaseWeapon( x,x,x,x, 150, ... ); SetObjBasePrice( ... ); //lib/unit.script procedure SetObjBaseWeapon( x,x,x,x, pause, ... ) weapon.pause := _misc_FramesToTime( pause );
A julgar pelos comentários, a unidade de tempo "estrutura do jogo" é atavismo dos primeiros "cossacos", cujo processo de jogo foi copiado ao criar a terceira parte. No entanto, os quadros são recontados imediatamente em segundos de jogo, com uma proporção de 1:32, e não os encontramos mais:
//lib/misc.script function _misc_FramesToTime( val ) Result := ( val * gc_frames_to_time ); //dmscript.global gc_frames_to_time := 0.03125; gc_time_to_frames := 32;
Além disso, no início do jogo, os dados das nações de jogos são inicializados, incluindo as melhorias disponíveis. Para cada um deles, a variável value é indicada e armazenada, o que, ao estudar essa melhoria, afeta o recálculo dos parâmetros necessários do jogo:
//lib/country.script procedure _country_Init() _country_AddUpgrade( x,x,x,x, type_attpauseperc, -30, ... ); procedure _country_AddUpgrade( x,x,x,x, upgrade_type, value, ... );
No nosso caso, isso significa que os intervalos das unidades militares após cada aprimoramento são multiplicados por 0,7 e então ... são arredondados?!
//lib/player.script procedure _player_ApplyUpgrade() type_attpauseperc : weapon.pause := Round( weapon.pause * (1 + value/100) );
Dado o fato de que inicialmente os intervalos dos atiradores são números de ponto flutuante na faixa de 3,125 a 5,0, a decisão de arredondar o resultado do recálculo parece estranha, se não importante.
Após cada disparo, é indicado o atraso antes do próximo disparo. O modificador idividual.attackrate é aplicado às estruturas da torre e, no nosso caso, é sempre 1.
Portanto, além do erro matemático nos cálculos, cujos detalhes podem ser lidos no spoiler abaixo, existe um arredondamento inadequado dos números de ponto flutuante. Eu me pergunto que efeito na mecânica do jogo tem essa ligeira supervisão à primeira vista?
Um pouco de matemáticaA taxa de tiro é inversamente proporcional ao tamanho do intervalo entre os disparos. E se é o número de rodadas por minuto que importa para o jogador, o mecanismo do jogo, como regra, usa intervalos para calcular a pausa. O problema aqui é que “reduzir o intervalo em 30%” e “aumentar a taxa de tiro em 30%” são coisas completamente diferentes. A razão r entre os intervalos te número de disparos n é descrita por uma fórmula simples:
Se, por exemplo, tomarmos um intervalo de 6 segundos (10 rodadas por minuto) e o reduzirmos em 30%, não teremos 13 rodadas por minuto:
Para obter o valor desejado, você deve dividir o intervalo atual pela proporção desejada da nova taxa de disparo pela antiga:
Método de mediçãoPara obter os valores com os quais o mecanismo de jogo trabalha, você pode usar as funções de log. Para fazer isso, primeiro você precisa habilitar o log:
//cossacks.ini & editor.ini LogFileEnabled = true LogFileRoot = true
E adicione no final do procedimento _unit_ApplyAttackPause () uma chamada à função Log () :
//data/scripts/lib/unit.script procedure _unit_ApplyAttackPause(const goHnd, weapind : Integer); begin //... if (attpause<>0) then Log(TObjProp(pobjprop).sid+' '+FloatToStr(attpause)); end;
Agora você pode brincar com várias setas e melhorias no editor de mapas (para ativar o modo de ataque, pressione Ctrl + W ). O protocolo será gravado em um arquivo de texto na pasta / log . Após cada tiro, o identificador da unidade de combate e o valor do seu intervalo atual serão registrados.
Quem é quem
Inicialmente, os scripts do jogo distinguem entre 35 tipos de atiradores (sem contar mercenários que não são afetados por melhorias). Se agruparmos todos pelo tamanho do intervalo, poderemos distinguir dez categorias. Decidi classificá-los por aumento relativo na taxa de tiro, a fim de destacar os atiradores que mais se beneficiam com as melhorias. Então, os resultados da análise:
| Intervalo de ataque | Tiros / min | Taxa de incêndio |
Categoria Aprimoramentos | 0 0 | +1 | +2 | 0 0 | +1 | +2 | +1 | +2 |
Eu | 5,00 | 4.0 | 3.0 | 12,0 | 15 | 20 | + 25% | + 67% |
II | 6,88 | 5,0 | 4.0 | 8,7 | 12 | 15 | + 38% | + 72% |
III | 5,31 | 4.0 | 3.0 | 11,3 | 15 | 20 | + 33% | + 77% |
IV | 5,63 | 4.0 | 3.0 | 10,7 | 15 | 20 | + 41% | + 88% |
V | 3,75 | 3.0 | 2.0 | 16,0 | 20 | 30 | + 25% | + 88% |
VI | 5,94 | 4.0 | 3.0 | 10.1 | 15 | 20 | + 48% | + 98% |
VII | 4.06 | 3.0 | 2.0 | 14,8 | 20 | 30 | + 35% | + 103% |
VIII | 4,38 | 3.0 | 2.0 | 13,7 | 20 | 30 | + 46% | + 119% |
IX | 4,69 | 3.0 | 2.0 | 12,8 | 20 | 30 | + 56% | + 134% |
X | 3,13 | 2.0 | 1,0 | 19,2 | 30 | 60 | + 56% | + 213% |
No diagrama abaixo, as colunas correspondem às categorias I - X, da esquerda para a direita. A última coluna tracejada do diagrama corresponde à taxa de aumento declarada na interface do jogo. O grupo esquerdo de colunas mostra um aumento na taxa de tiro após uma melhoria, a direita - depois de ambas. 
Lista de categorias e unidadesO jogo tem várias nações - 17 européias e quatro únicas (Ucrânia, Turquia, Argélia e Escócia). As facções européias são muito parecidas desde o início e possuem mosqueteiros e dragões dos séculos XVII e XVIII, além de granadeiros. Mas, às vezes, as flechas de algumas nações diferem do modelo ou são completamente substituídas por um tipo único.
Categoria | Unidades de combate |
---|
Eu | Mosqueteiro Século XVII (Áustria) Szekej (Hungria) Atirador escocês (Inglaterra) Comunidade Polaco-Lituana (Polónia) Dragão do século XVIII (Países Baixos e Piemonte) |
II | Caçador (Suíça) Mosqueteiro Real (França) |
III | Granadeiro (Europa, exceto Dinamarca e Prússia) Dragão do século XVIII (Europa, exceto França, Holanda e Piemonte) Cavaleiro leve (diferentes países) |
IV | Dragão do século XVII (Europa) |
V | Mosqueteiro Século XVII (Holanda) |
VI | Mosqueteiro Século XVII (Espanha) Mosqueteiro Século XVIII (Baviera e Dinamarca) Granadeiro (Dinamarca) Voluntário (Portugal) Caçador (França) |
VII | Serdyuk (Ucrânia) |
VIII | Mosqueteiro Século XVIII (Saxônia) Granadeiro (Prússia) |
IX | Mosqueteiro Século XVII (Europa, exceto Áustria, Polônia, Países Baixos e Espanha) Convênio com Mosqueteiros (Escócia) Sagitário (Rússia) Janissar (Turquia) Mosqueteiro Século XVIII (Europa, exceto Dinamarca, Baviera e Saxônia) Pandur (Áustria) Dragão do século XVIII (França) |
X | Mosqueteiro Século XVII (Polônia) Hajduk (Hungria) |
Notas:
- Os nomes das unidades militares são copiados da interface russa do jogo.
- As setas em itálico do século XVIII estão em itálico .
- As setas do cavalo são destacadas em negrito .
Acontece que o mosqueteiro polonês do século XVII e o seqüestro húngaro são os que mais ganham com as melhorias na taxa de tiro: em vez dos prometidos + 60%, eles disparam mais de três vezes com mais frequência. Devido ao baixo valor inicial do intervalo, eles disparam mais rapidamente do que todos os outros atiradores duas, três ou até quatro vezes.
Entre a cavalaria, os dragões franceses do século XVIII estão melhor estabelecidos: eles recebem uma taxa de tiro mais do que duplicada. Como resultado, eles disparam 50% mais tiros por minuto do que seus colegas de outras nações européias.
Naturalmente, o dano de um tiro ou dano por segundo não é levado em consideração aqui, mas mesmo sem esses dados, é óbvio que as unidades militares não se comportam como planejadas.
Como consertarA solução mais rápida e não invasiva para o problema é reescrever a fórmula para aplicar a melhoria. Além de rejeitar o arredondamento, em vez de multiplicar o intervalo por 0,3, divida-o por 1,3. Para fazer isso, substitua a fórmula pelo procedimento de aprimoramento gc_upg_type_attpauseperc por
//lib/player.script Round(weapon.pause*(1+value/100));
em
weapon.pause/(1+(-value)/100);
Como as melhorias são aplicadas de maneira consistente, no final, em vez dos declarados + 60%, obtemos + 69%. Mas ainda é melhor que + 213%.
Posfácio
Para identificar de forma confiável erros de cálculo na balança, neste caso, dois outros aspectos da mecânica do jogo devem ser analisados - o dano dos atiradores e o valor econômico, juntamente com o tempo necessário para criar uma unidade de combate. No entanto, o senso comum diz para você aguardar a próxima atualização primeiro ...
Tive a ideia do estudo no vídeo “ Por que as taxas de ataque no AoE2 são frequentemente erradas ”, que aborda um problema semelhante na estratégia do Age of Empires II .
UPD: Erro parcialmente corrigido
Nem uma semana se passou desde a publicação do artigo, pois os desenvolvedores da atualização 2.2.1 corrigiram o erro com arredondamentos. Ao mesmo tempo, a própria fórmula permaneceu a mesma - a taxa de incêndio está crescendo em 43% por atualização. Como o cálculo é incremental, após examinar as duas melhorias, todas as setas funcionam 104% mais rápido.
QuadroTaxa de unidades de tiro em tiros por minuto após pesquisar as duas melhorias, em ordem crescente:
Unidades de combate | Tiros |
---|
Caçador (Suíça) Mosqueteiro Real (França) | 17,8 |
Mosqueteiro Século XVII (Espanha) Mosqueteiro Século XVIII (Baviera e Dinamarca) Granadeiro (Dinamarca) Voluntário (Portugal) Caçador (França) | 20,6 |
Dragão do século XVII (Europa) | 21,8 |
Granadeiro (Europa, exceto Dinamarca e Prússia) Dragão do século XVIII (Europa, exceto França, Holanda e Piemonte) Cavaleiro leve (diferentes países) | 23,0 |
Mosqueteiro Século XVII (Áustria) Szekej (Hungria) Atirador escocês (Inglaterra) Comunidade Polonesa- Lituana (Polônia) Dragão do século XVIII (Países Baixos e Piemonte) | 24,5 |
Mosqueteiro Século XVII (Europa, exceto Áustria, Polônia, Países Baixos e Espanha) Convênio com Mosqueteiros (Escócia) Sagitário (Rússia) Janissar (Turquia) Mosqueteiro Século XVIII (Europa, exceto Dinamarca, Baviera e Saxônia) Pandur (Áustria) Dragão do século XVIII (França) | 26,1 |
Mosqueteiro Século XVIII (Saxônia) Granadeiro (Prússia) | 28,0 |
Serdyuk (Ucrânia) | 30,1 |
Mosqueteiro Século XVII (Holanda) | 32,7 |
Mosqueteiro Século XVII (Polônia) Hajduk (Hungria) | 39,2 |