Главная » Хабрахабр » Играй, но проверяй: как движок обсчитывает дизайнера

Играй, но проверяй: как движок обсчитывает дизайнера

Работа игрового дизайнера в этом плане похожа на работу аналитика спецслужб: если он работает хорошо, никто ничего не замечает. При дизайне многопользовательской игры чуть ли не самой важной составляющей является баланс. Но самое интересное случается, когда помимо гейм-дизайнера ошибается ещё и программист... Стоит оступиться, и игроки бессовестно воспользуются ошибкой.

В игре присутствуют различные виды мушкетёров и иных стрелков 17 и 18 веков, а также возможность исследовать технологии, снижающие время перезарядки мушкетов. В этой статье мы рассмотрим один элемент стратегии «Казаки 3». Всего имеется два улучшения, каждое из которых приносит +30% к скорострельности — если верить интерфейсу игры.

При измерении скорострельности непосредственно с помощью встроенного игрового таймера выходят и вовсе странные числа, которые к заявленным процентам никакого отношения не имеют. Но даже на глаз видно, что некоторые боевые единицы после исследования улучшений стреляют не просто на 60%, а даже в несколько раз чаще.

Судя по синтаксису, скрипты написаны на Делфи или на очень похожем языке. К счастью, игра выполнена в очень дружелюбном для моддеров виде, так что все нужные нам скрипты доступны в виде текстовых файлов в папке data/scripts/. Давайте же взглянем на механику расчёта интервалов между выстрелами.

Примечания

  • Анализ проводился на игре «Казаки 3» версии 2.1.4.
  • Все приведённые ниже отрезки скриптов содержат упрощённый псевдокод.

  1. В процедуре указываются значения жизненной силы, стоимости и вооружения для каждого типа. При старте игры происходит инициализация всех боевых единиц. Для стрелкового оружия передаётся параметр, указывающий интервал между выстрелами в игровых кадрах:

    //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 );

    Впрочем, кадры сразу же пересчитываются в игровые секунды с соотношением 1:32, и больше мы с ними не сталкиваемся: Судя по комментариям, единица времени «игровой кадр» это атавизм из первых «Казаков», игровой процесс которых был скопирован при создании третьей части.

    //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;

  2. Для каждого из них указывается и сохраняется переменная value, которая при исследовании этого улучшения влияет на перерасчёт нужных параметров игры: Также при старте игры инициализируются данные игровых наций, включая доступные улучшения.

    //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, ... );

    В нашем случае это означает, что интервалы боевых единиц после каждого улучшения умножаются на 0,7 а затем… округляются?!

    //lib/player.script
    procedure _player_ApplyUpgrade() type_attpauseperc : weapon.pause := Round( weapon.pause * (1 + value/100) );

    Учитывая то, что изначально интервалы стрелков представляют собой числа с плавающей запятой в диапазоне от 3,125 до 5,0, решение округлять результат перерасчёта выглядит довольно странно, если не сказать бажно.

  3. Модификатор idividual.attackrate применяется к башенным сооружениям и в нашем случае всегда равен 1. После каждого произведённого выстрела указывается задержка перед следующим выстрелом.

    //lib/unit.script
    procedure _unit_ApplyAttackPause() attackdelay := weapon.pause * idividual.attackrate;

Интересно, какой эффект на механику игры имеет эта на первый вздляд незначительная оплошность? Итак, помимо математической ошибки в расчётах, детали которой можно прочитать под спойлером ниже, налицо неуместное округление чисел с плавающей запятой.

Немного математики

И если для игрока важно именно количество выстрелов в минуту, то игровой движок, как правило, использует интервалы для подсчёта паузы. Величина скорострельности обратно пропорциональна величине интервала между выстрелами. Соотношение r между интервалами t и количествами выстрелов n описывается простой формулой: Подвох здесь в том, что «снизить интервал на 30%» и «повысить скорострельность на 30%» это совершенно разные вещи.

$\frac{t_2}=r=\frac{n_2}{n_1}$

Если, например, взять интервал в 6 секунд (10 выстрелов в минуту) и уменьшить его на 30%, то мы не получим 13 выстрелов в минуту:

7=4. $6\space\mathrm{s}\cdot0. 2\space\mathrm{s}}\approx1. 2\space\mathrm{s};\quad\frac{6\space\mathrm{s}}{4. 43\neq\frac{13}{10}$

Чтобы получить нужную величину, следует разделить текущий интервал на желаемое соотношение новой скорострельности к старой:

3}\approx4. $t_2=\frac{t_1}{r}=\frac{6\space\mathrm{s}}{1. 62\space\mathrm{s}$

Метод измерения

Для этого сначало нужно включить запись лога: Чтобы получить значения, с которыми работает движок игры, можно воспользоваться функциями протоколирования.

//cossacks.ini & editor.ini LogFileEnabled = true LogFileRoot = true

А затем добавить в конце процедуры _unit_ApplyAttackPause() вызов функции 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;

Протокол будет записан в текстовый файл в папке /log. Теперь можно поиграться с различными стрелками и улучшениями в редакторе карт (для включения режима нападения следует нажать Ctrl+W). После каждого произведённого выстрела будет записан идентификатор боевой единицы и величина её текущего интервала.

Если сгруппировать их всех по величине интервала, то мы сможем выделить десять категорий. Изначально скрипты игры различают 35 видов стрелков (не считая наёмников, которые не подвержены воздействию улучшений). Итак, результаты анализа: Я решил сортировать их по относительному приросту скорострельности, чтобы выделить тех стрелков, кто больше других выигрывает от улучшений.

Интервал атаки

Выстрелы / мин

Рост скорострельности

Категория$\backslash$Улучшения

0

+1

+2

0

+1

+2

+1

+2

I

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%

Последний штрихованный столбец диаграммы соответствует заявленному в интерфейсе игры приросту скорострельности. В диаграмме ниже столбцы соответствуют категориям I—X, слева направо. Левая группа столбцов показывает прирост скорострельности после одного улучшения, правая — после обоих.

Список категорий и боевых единиц

Европейские фракции изначально очень похожи и имеют мушкетёров и драгун 17—18 вв., а также гренадёров. В игре присутствуют различные нации — 17 европейских и четыре уникальных (Украина, Турция, Алжир и Шотландия). Но иногда стрелки некоторых наций отличаются от шаблонных, или же вовсе заменены уникальным типом.

Категория

Боевые единицы

I

Мушкетер 17в. (Австрия)
Секей (Венгрия)
Шотландский стрелок (Англия)
Посполитое рушение (Польша)
Драгун 18в. (Нидерланды и Пьемонт)

II

Егерь (Швейцария)
Королевский мушкетер (Франция)

III

Гренадер (Европа кроме Дании и Пруссии)
Драгун 18в. (Европа кроме Франции, Нидерландов и Пьемонта)
Легкий кавалерист (разные страны)

IV

Драгун 17в. (Европа)

V

Мушкетер 17в. (Нидерланды)

VI

Мушкетер 17в. (Испания)
Мушкетер 18в. (Бавария и Дания)
Гренадер (Дания)
Доброволец (Португалия)
Егерь (Франция)

VII

Сердюк (Украина)

VIII

Мушкетер 18в. (Саксония)
Гренадер (Пруссия)

IX

Мушкетер 17в. (Европа кроме Австрии, Польши, Нидерландов и Испании)
Мушкетер Ковенанта (Шотландия)
Стрелец (Россия)
Янычар (Турция)
Мушкетер 18в. (Европа кроме Дании, Баварии и Саксонии)
Пандур (Австрия)
Драгун 18в. (Франция)

X

Мушкетер 17в. (Польша)
Гайдук (Венгрия)

Примечания:

  • Наименования боевых единиц скопированы из русского интерфейса игры.
  • Курсивом выделены стрелки 18 века.
  • Жирным шрифтом выделены конные стрелки.

Благодаря низкому изначальному значению интервала они в итоге стреляют быстрее всех остальных стрелков в два, три, а то и четыре раза. Оказывается, больше всех от улучшений скорострельности выигрывают польский мушкетёр 17 века и венгерский гайдук: вместо обещанных +60% они стреляют более чем в три раза чаще.

В итоге они совершают на 50% больше выстрелов в минуту, чем их коллеги из остальных европейских наций. Среди кавалерии лучше всего устроились французские драгуны 18 века: они получают прирост скорострельности более чем в два раза.

Естественно, здесь не учитывается урон выстрела или урон в секунду, но даже и без этих данных очевидно, что боевые единицы ведут себя не так как задумано.

Как исправить

Кроме отказа от округления следует вместо умножения интервала на 0,3 поделить его на 1,3. Самое быстрое и неинвазивное решение проблемы это переписать формулу применения улучшения. Для этого достаточно заменить в процедуре обработки улучшения gc_upg_type_attpauseperc формулу с

//lib/player.script Round(weapon.pause*(1+value/100));

на

weapon.pause/(1+(-value)/100);

Но это всё же лучше чем +213%. Так как улучшения применяются последовательно, в итоге вместо заявленных +60% мы получим +69%.

Однако здравый смысл подсказывает, что сначала следует подождать следующего обновления... Чтобы достоверно выявить просчёты в балансе в данном случае следует проанализировать ещё два аспекта игровой механики — урон стрелков и экономическая стоимость вместе с временем, требуемым на создание боевой единицы.

Идею для исследования я почерпнул из видеоролика «Why Attack Rates in AoE2 Are Often Wrong» (англ.), рассматривающего схожую проблематику в стратегии Age of Empires II.


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

[Перевод] Браузеры отключают звук в вашем WebRTC-приложении. Стоп, что?

Технология WebRTC (голосовые и видеозвонки) хороша тем, что встроена прямо в веб, который, разумеется, прекрасно подходит для WebRTC. Однако иногда веб доставляет немало хлопот, когда нужды WebRTC идут вразрез с общими требованиями к использованию браузеров. Последний пример – автовоспроизведение (далее ...

Создатель игры while True: learn() о программировании в геймдеве, проблемах с VR и симуляции ML

Постоянно выступал, проводил Gamesjam, был частым гостем подкаста Как делают игры. Несколько лет назад мне казалось, что Олег Чумаков (тогда еще из Nival) был самым известным программистом геймдева. Но вы все знаете, с виртуальной реальностью что-то пошло не так, как ...