
Al diseñar un juego multijugador, casi el componente más importante es el equilibrio. El trabajo de un diseñador de juegos a este respecto es similar al trabajo de un analista de inteligencia: si trabaja bien, nadie se da cuenta. Vale la pena tropezar, y los jugadores se aprovechan descaradamente del error. Pero lo más interesante sucede cuando, además del diseñador del juego, el programador también se equivoca ...
En este artículo, consideraremos un elemento de la estrategia Cossacks 3 . El juego contiene varios tipos de mosqueteros y otros tiradores de los siglos XVII y XVIII, así como la oportunidad de explorar tecnologías que reducen el tiempo de recarga de los mosquetes. Hay dos mejoras en total, cada una de las cuales aporta + 30% a la velocidad de disparo, según la interfaz del juego.
Pero incluso a simple vista, está claro que algunas unidades de combate, después de investigar las mejoras, disparan no solo al 60%, sino incluso varias veces más a menudo. Al medir la velocidad de disparo directamente con el temporizador de juego incorporado, salen números completamente extraños que no tienen nada que ver con los porcentajes establecidos.
Bajo el capó de los "cosacos"
Afortunadamente, el juego está hecho de una manera muy amigable para los modders, por lo que todos los scripts que necesitamos están disponibles como archivos de texto en la carpeta data / scripts / . A juzgar por la sintaxis, los guiones están escritos en Delphi o en un lenguaje muy similar. Echemos un vistazo a la mecánica de calcular los intervalos entre disparos.
Notas- El análisis se llevó a cabo en el juego "Cossacks 3" versión 2.1.4.
- Todas las secciones del script a continuación contienen un pseudocódigo simplificado.
Cuando comienza el juego, todas las unidades de combate se inicializan. El procedimiento indica los valores de vitalidad, costo y armas para cada tipo. Para armas pequeñas, se pasa un parámetro que indica el intervalo entre disparos en los marcos del juego:
//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 juzgar por los comentarios, la unidad de tiempo "marco del juego" es un atavismo de los primeros "cosacos", cuyo proceso del juego se copió al crear la tercera parte. Sin embargo, los fotogramas se relatan de inmediato en segundos de juego con una proporción de 1:32, y ya no los encontramos:
//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;
Además, al comienzo del juego, se inicializan los datos de las naciones de juego, incluidas las mejoras disponibles. Para cada uno de ellos, la variable de valor se indica y almacena, lo que, al estudiar esta mejora, afecta el recálculo de los parámetros necesarios del juego:
//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, ... );
En nuestro caso, esto significa que los intervalos de las unidades militares después de cada mejora se multiplican por 0.7 y luego ... ¿se redondean?
//lib/player.script procedure _player_ApplyUpgrade() type_attpauseperc : weapon.pause := Round( weapon.pause * (1 + value/100) );
Dado el hecho de que inicialmente los intervalos de los tiradores son números de coma flotante en el rango de 3.125 a 5.0, la decisión de redondear el resultado del recálculo parece bastante extraña, si no importante.
Después de cada disparo, se indica el retraso antes del siguiente disparo. El modificador idividual.attackrate se aplica a las estructuras de la torre y en nuestro caso siempre es 1.
Entonces, además del error matemático en los cálculos, cuyos detalles se pueden leer debajo del spoiler a continuación, hay un redondeo inapropiado de números de punto flotante. Me pregunto qué efecto sobre la mecánica del juego tiene este ligero descuido a primera vista.
Un poco de matemáticaLa velocidad de disparo es inversamente proporcional al tamaño del intervalo entre disparos. Y si es el número de rondas por minuto lo que le importa al jugador, entonces el motor del juego, como regla, usa intervalos para calcular la pausa. El problema aquí es que "reducir el intervalo en un 30%" y "aumentar la velocidad de disparo en un 30%" son cosas completamente diferentes. La relación r entre los intervalos t y el número de disparos n se describe mediante una fórmula simple:
Si, por ejemplo, tomamos un intervalo de 6 segundos (10 rondas por minuto) y lo reducimos en un 30%, entonces no obtendremos 13 rondas por minuto:
Para obtener el valor deseado, debe dividir el intervalo actual entre la relación deseada de la nueva cadencia de tiro a la anterior:
Método de mediciónPara obtener los valores con los que funciona el motor del juego, puedes usar las funciones de registro. Para hacer esto, primero debe habilitar el registro:
//cossacks.ini & editor.ini LogFileEnabled = true LogFileRoot = true
Y luego agregue al final del procedimiento _unit_ApplyAttackPause () una llamada a la función 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;
Ahora puedes jugar con varias flechas y mejoras en el editor de mapas (para habilitar el modo de ataque, presiona Ctrl + W ). El protocolo se escribirá en un archivo de texto en la carpeta / log . Después de cada disparo, se registrará el identificador de la unidad de combate y el valor de su intervalo actual.
Quien es quien
Inicialmente, los guiones del juego distinguen entre 35 tipos de tiradores (sin contar los mercenarios que no se ven afectados por las mejoras). Si los agrupamos a todos por el tamaño del intervalo, podemos distinguir diez categorías. Decidí ordenarlos por aumento relativo en la velocidad de disparo para destacar a los tiradores que más se benefician de las mejoras. Entonces, los resultados del análisis:
| Intervalo de ataque | Disparos / min. | Velocidad de disparo |
Categoría Mejoras | 0 0 | +1 | +2 | 0 0 | +1 | +2 | +1 | +2 |
Yo | 5.00 | 4.0 4.0 | 3.0 | 12,0 | 15 | 20 | + 25% | + 67% |
II | 6.88 | 5,0 | 4.0 4.0 | 8.7 | 12 | 15 | + 38% | + 72% |
III | 5.31 | 4.0 4.0 | 3.0 | 11,3 | 15 | 20 | + 33% | + 77% |
IV | 5.63 | 4.0 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 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 60 | + 56% | + 213% |
En el siguiente diagrama, las columnas corresponden a las categorías I - X, de izquierda a derecha. La última columna discontinua del diagrama corresponde a la tasa de aumento declarada en la interfaz del juego. El grupo izquierdo de columnas muestra un aumento en la velocidad de disparo después de una mejora, la derecha, después de ambas. 
Lista de categorías y unidades.El juego tiene varias naciones: 17 europeas y cuatro únicas (Ucrania, Turquía, Argelia y Escocia). Las facciones europeas son muy similares desde el principio y tienen mosqueteros y dragones de los siglos XVII y XVIII, así como granaderos. Pero a veces las flechas de algunas naciones difieren de la plantilla, o son completamente reemplazadas por un tipo único.
Categoría | Unidades de combate |
---|
Yo | Mosquetero del siglo XVII. (Austria) Szekej (Hungría) Tirador escocés (Inglaterra) Mancomunidad Polaco-Lituana (Polonia) Dragón del siglo XVIII (Países Bajos y Piamonte) |
II | Huntsman (Suiza) Mosquetero real (Francia) |
III | Granadero (Europa excepto Dinamarca y Prusia) Dragón del siglo XVIII (Europa excepto Francia, los Países Bajos y el Piamonte) Caballería ligera (diferentes países) |
IV | Dragón del siglo 17 (Europa) |
V | Mosquetero del siglo XVII. (Holanda) |
VI | Mosquetero del siglo XVII. (España) Mosquetero del siglo XVIII (Baviera y Dinamarca) Granadero (Dinamarca) Voluntario (Portugal) Huntsman (Francia) |
VII | Serdyuk (Ucrania) |
VIII | Mosquetero del siglo XVIII (Sajonia) Granadero (Prusia) |
IX | Mosquetero del siglo XVII. (Europa excepto Austria, Polonia, Países Bajos y España) Pacto Mosquetero (Escocia) Sagitario (Rusia) Janissar (Turquía) Mosquetero del siglo XVIII (Europa excepto Dinamarca, Baviera y Sajonia) Pandur (Austria) Dragón del siglo XVIII (Francia) |
X | Mosquetero del siglo XVII. (Polonia) Hajduk (Hungría) |
Notas:
- Los nombres de las unidades militares se copian de la interfaz rusa del juego.
- Las flechas en el siglo XVIII se muestran en cursiva .
- Las flechas de caballo se resaltan en negrita .
Resulta que el mosquetero polaco del siglo XVII y el secuestro húngaro son los que más ganan con las mejoras en la velocidad de disparo: en lugar del prometido + 60%, disparan más de tres veces más a menudo. Debido al bajo valor inicial del intervalo, finalmente disparan más rápido que todos los otros tiradores dos, tres o incluso cuatro veces.
Entre la caballería, los dragones franceses del siglo XVIII están mejor asentados: reciben una tasa de fuego que se ha más que duplicado. Como resultado, disparan un 50% más de disparos por minuto que sus homólogos de otras naciones europeas.
Naturalmente, el daño de un disparo o el daño por segundo no se tiene en cuenta aquí, pero incluso sin estos datos es obvio que las unidades militares no se comportan como se esperaba.
Como arreglarLa solución más rápida y no invasiva al problema es reescribir la fórmula para aplicar la mejora. Además de rechazar el redondeo, en lugar de multiplicar el intervalo por 0.3, divídalo por 1.3. Para hacer esto, simplemente reemplace la fórmula con el procedimiento de mejora gc_upg_type_attpauseperc con
//lib/player.script Round(weapon.pause*(1+value/100));
en
weapon.pause/(1+(-value)/100);
Dado que las mejoras se aplican de manera consistente, al final, en lugar del + 60% declarado, obtenemos + 69%. Pero sigue siendo mejor que + 213%.
Epílogo
Para identificar de manera confiable los errores de cálculo en el balance en este caso, se deben analizar dos aspectos más de la mecánica del juego: el daño de los tiradores y el valor económico junto con el tiempo requerido para crear una unidad de combate. Sin embargo, el sentido común le dice que espere la próxima actualización primero ...
La idea para el estudio surgió del video " Por qué las tasas de ataque en AoE2 son a menudo incorrectas ", que aborda un problema similar en la estrategia de Age of Empires II .
UPD: error parcialmente corregido
No ha pasado ni una semana desde la publicación del artículo, ya que los desarrolladores de la actualización 2.2.1 corrigieron el error con redondeo. Al mismo tiempo, la fórmula en sí seguía siendo la misma: la velocidad de disparo está creciendo al 43% por actualización. Como el cálculo es incremental, después de examinar ambas mejoras, todas las flechas funcionan un 104% más rápido.
MesaTasa de unidades de fuego en disparos por minuto después de investigar ambas mejoras, en orden creciente:
Unidades de combate | Disparos |
---|
Huntsman (Suiza) Mosquetero real (Francia) | 17,8 |
Mosquetero del siglo XVII. (España) Mosquetero del siglo XVIII (Baviera y Dinamarca) Granadero (Dinamarca) Voluntario (Portugal) Huntsman (Francia) | 20,6 |
Dragón del siglo 17 (Europa) | 21,8 |
Granadero (Europa excepto Dinamarca y Prusia) Dragón del siglo XVIII (Europa excepto Francia, los Países Bajos y el Piamonte) Caballería ligera (diferentes países) | 23,0 |
Mosquetero del siglo XVII. (Austria) Szekej (Hungría) Tirador escocés (Inglaterra) Mancomunidad Polaca- Lituana (Polonia) Dragón del siglo XVIII (Países Bajos y Piamonte) | 24,5 |
Mosquetero del siglo XVII. (Europa excepto Austria, Polonia, Países Bajos y España) Pacto Mosquetero (Escocia) Sagitario (Rusia) Janissar (Turquía) Mosquetero del siglo XVIII (Europa excepto Dinamarca, Baviera y Sajonia) Pandur (Austria) Dragón del siglo XVIII (Francia) | 26,1 |
Mosquetero del siglo XVIII (Sajonia) Granadero (Prusia) | 28,0 |
Serdyuk (Ucrania) | 30,1 |
Mosquetero del siglo XVII. (Holanda) | 32,7 |
Mosquetero del siglo XVII. (Polonia) Hajduk (Hungría) | 39,2 |