Stupid Quake Bug

Сообщения
67
Реакции
85
Источник: csm.dev
Автор статьи: Дядя Миша

Stupid Quake Bug
Статья из цикла по внутреннему устройству Quake-движков

В своих туториалах я стараюсь подробно освещать именно те моменты, о которых вы не найдете информацию больше нигде в сети. Вполне возможно по той простой причине, что никто об этом ничего не знает, а те кто что-то знает - ничего не напишут. Не исключением стал и Stupid Quake Bug. Если ввести это словосочетание в Гугль, то мы увидим, что оно вполне устойчивое, однако решения проблемы никто не предлагает. В лучшем случае - инвертировать PITCH в проблемном месте. Однако это не решение, а просто заталкивание болезни еще глубже. В этом я убедился на собственном опыте. Найти багу невероятно сложно, потому что сидит она совершенно не там, где можно ожидать. Когда я докопался до сути проблемы, я был очень удивлён. Но тем не менее профиксить её вполне реально, и именно об этом я хочу рассказать в данном туторе. Для начала - немного истории, у нас же всё-таки цикл статей по внутреннему устройству.

Историческое возникновение Stupid Quake Bug

Баги, если они не приводят к вылетам и зависанияим, порой используются игроками в своих корыстных целях. Хороший пример такого рода - распрыжка, позволяющая увеличить максимальную скорость игрока. Stupid Quake Bug, напротив стал невероятной головной болью для программистов, поскольку с каждой инверсией PITCH загонял болезнь всё глубже и глубже, а поделать с этим нельзя было ничего. Но так было далеко не всегда.

Вспомним историю. Кармак вполне справедливо полагал, что создание нового движка начинается с переписывания старого. У данного подхода есть один очень важный момент - на каждом этапе мы имеем работающий движок, по крайней мере те участки кода, которых не коснулись изменения. Я затрудняюсь сказать, есть ли какая-то связь между Dangerous Dave и Wolf3D, а так же между Wolf3D и Doom, но тот факт, что Quake вырос из Doom - это 146%. А что у нас было в первом Doom? А там у нас было вращение исключительно по YAW, как для монстров, так и для игрока. В эту стройную систему не укладывался только один момент - надо было как-то целиться в монстров, находящихся выше или ниже игрока. Но это было успешно решено при помощи системы автоприцеливания, которая затем перекочевала в кваку, а затем и в голдсорс. Quake с углами тоже обходился весьма небрежно. Давайте навскидку вспомним энтити, которые могли использовать более одного угла. misc_teleporttrain - это такое зубчатое ядро от моргенштернымегарулёза, которое летало по последнему уровню и крутилось вокруг собственной оси. info_intermission (обратите внимание, в Quake даже не было поля angles, вместо него было поле angle где можно было задать лишь YAW), который использовал модифицированное поле mangle - чтобы задавать три угла для камеры. Ну и сам игрок. В таких условиях, когда PITCH практически не использовался, баг, подобный Stupid Quake Bug просто обязан был появиться рано или поздно. Он и появился. Но выяснилось это значительно позже, с выходом мода Scourge Of Armagon (HIPNOTIC), там ребята ввели крутящиеся двери и лифты и вдруг с удивлением обнаружили, что их вращение не соответствует физической оболочке.

Для тех кто забыл, я напомню, что, поскольку Quake не поддерживал корректную физику для крутящихся брашевых моделей, в том моде было применено весьма оригинальное решение - колоизация для таких моделей набиралась из невидимых func_wall кубической формы, которые пытались аппроксимировать движение видимой оболочки. Чем больше было таких энтить - тем точнее получалась коллизия. Собственно вот это-то и было обнаружено - видимые оболочки двигались в одну сторону, а реальная коллизия - в обратную. Поскольку аддон был официальным, ребята поддерживали связи с ID и быстренько накатали им письмо с описанием проблемы. Я не поручусь, что всё именно так и было, но ход развития событий это вполне предполагает. Те в ответ чертыхнулись и наговняли по быстрому знаменитый багфикс, с которого Stupid Quake Bug и начал своё победное шествие по планете. Быть может я ошибаюсь и он поселился еще раньше, поскольку projectile-энтити также использовали угол PITCH, но не для вращения, а для задания траектории. Это не влияло на визуализацию, но влияло на вектор направления. Впрочем, если вам важна историческая точность, я полагаю, лучше спросить об этом у самого Кармака

Суть проблемы

Как это выглядит, видели наверняка все, кто хотя бы раз плотно изучал исходники первого квейка. Вот так:

C++:
gl_rsurf.c (1143)

glPushMatrix ();
e->angles[0] = -e->angles[0];// stupid quake bug
R_RotateForEntity (e);
e->angles[0] = -e->angles[0];// stupid quake bug
Именно наличие исправления в коде отрисовки бмоделей, а не монстров и даёт мне основание сделать такие смелые выводы, что исторически с багом начали борьбу именно во время создания HIPNOTIC. Valve тоже пыталась бороться с багом, поскольку в её моделях появился блендинг анимаций и стало возможным увидеть со стороны куда именно целиться монстр - вверх или вниз. Вы точно так же могли наблюдать в коде StudioModelRenderer строчку:

C++:
StudioModelRenderer.cpp (508)

angles[PITCH] = -angles[PITCH];
О том, что это Stupid Quake Bug вальвовцы предпочли умолчать, видимо из маркетинговых соображений, всё-таки GoldSource это уже движок Valve, многократно исправленный и вдруг тянет баги из первого квейка. Но это был именно он. Если одним глазом заглянуть в декомпил голдсорса, то мы можем убедиться исправление из gl_rsurf.c исчезло, перейдя в код студиомоделей. Перенос строчки из одного места в другое, как вы понимаете, проблему не решил, а просто загнал еще глубже. И кроме того всех окончательно запутал, поскольку возникла стойкая уверенность что корни бага кроются именно в отрисовке.
Я помню наши с Ксером рассуждения на эту тему, вполне логичные, хотя, как показала практика - совершенно ошибочные. Суть их, вкрации сводилась к тому, что корни бага следует искать в неправильной ориентации мира "По Кармаку", ну вот к этому двойному повороту, при котором у нас Z - высота. В коде studiomdl.exe не было никаких правильных трансформаций, был простой свап координат, следовательно версия вполне имела право на существование. К тому же баг успешно перекочевал в Quake2, где его загнали еще глубже.

Оцените:


C++:
gl_rsurf.c (1004)

e->angles[0] = -e->angles[0];// stupid quake bug
e->angles[2] = -e->angles[2];// stupid quake bug
R_RotateForEntity (e);
e->angles[0] = -e->angles[0];// stupid quake bug
e->angles[2] = -e->angles[2];// stupid quake bug
Понять в этой каше вариантов исправления, где именно претоилса коварный баг не представлялось возможным. Тем не менее в Quake3 баг либо отсутствует либо замаскирован. Правда положение осложняется еще и тем, что в самой Quake3 нет крутящихся объектов. Был func_rotating и func_pendulum, чисто для декоративных целей. Поди разбери - исправлен баг или наоборот, загнан еще глубже.
Баги подобного рода весьма устойчивы, если уж они переживают три поколения движков. В самом начале моих исследований, я полагал что баг неисправим по сути, и моя задача сводится к правильному инвертированию PITCH везде, где только нужно по смыслу. Баг, я напомню, вылез, когда я плотно занялся физикой и парент-системой.

Как я боролся с багом

Изначально, как я написал, чуть выше, я не ставил себе цели забороть баг окончательно. Я полагал, что мне вполне достаточно правильно инвертировать PITCH в нужных местах, чтобы физические ящики и бочки начали правильно кататься и падать. Я действовал исключительно на сервере, поскольку не без оснований полагал, что если тронуть клиентскую часть, то я опять столкнусь с несовпадением коллизии и видимой оболочки. Собственно, первые опыты в данном направлении тут же это подтвердили. Ну, дурацкое дело нехитрое - я расставил однотипные строчки везде, где это было необходимо по смыслу, а где смысл вступал в противоречие с багом - окружил всякими условиями. Мои тесты не выявили ничего подозрительного, Qwertyus так же ничего не нашёл, и XashXT с якобы исправленным багом был успешно зарелижен под версией 0.62. Вся эта идиллия продолжалась ровно до того момента, пока я не однажды не попытался установить трипмину на наклонную поверхность... Вообщем выяснилось, что трипмины ставятся правильно только на одну половину поверхностей, а на другую - задом наперёд. На поиски и разбирательства ушло несколько дней, прежде чем я смог докопаться до сути проблемы. Я предположил, что баг таится где-то в математических функциях, но не слишком часто используемых, поскольку в таком случае инверсия была бы встроена уже в саму функцию и бага бы никто не заметил. В теории могла быть разница между системами координат на клиенте и на сервере, как известно сервер использует FLU (Forward-Left-Up), а клиент - более привычную FRU (Forward-Right-Up), но от этой версии быстро пришлось отказаться - ведь баг присутствовал по умолчанию во всех энтитях, а не только в тех, кто использовал AngleVectors. Тогда я принял за рабочую гипотезу тот факт, что все клиентские инверсии PITCH - это не причина и даже не следствие бага, а просто неумелые попытки борьбы с ним, которые просто усугубляют ситуацию. Я решил полностью избавиться от всех инверсий в движке и игровых библиотеках и посмотреть, что же из этого получится. Сделать это оказалось достаточно несложно - инверсий было мало. Интерисующися могут поискать в движке ключевое слово ENGINE_COMPENSATE_QUAKE_BUG. После того, как работа была проделана я с удивлением обнаружил, что баг практически исчез отовсюду, вся физика и парент-системы работают абсолютно корректно, как будто ничего и не было. Правда глючили оружия, из которых тоже пришлось вычищать инверсию PITCH. С каждой удалённой строчкой баг проявлял себя всё реже и реже, и наконец исчез везде, кроме монстров. Я понял, что нахожусь на правильном пути и мне лишь осталось найти его истоки, которые привели к возникновению бага, в монстрах, откуда он и пошел, и как с ним пытались бороться совершенно не там. Положение осложнялось еще и тем, что в коде монстров никакой инверсии PITCH попросту не было, а сам код не менялся видимо еще с 1998 года. На тот момент я предположил, что в Source баг давно исправлен. Я взял первого попавшегося монстра, который неправильно себя вёл (им оказался вертолёт Апач) и сравнил код в Source и GoldSource. Даже беглое сравнение выявило несовпадение кода именно в тех, критичных местах, но изюминка была в том, что никакой тупой инверсии угла там попросту не было. Это дало мне повод предположить, что баг не был загнан в код монстров, а напротив - корректно исправлен на всех стадиях.

Приведу пример кода из GoldSource SDK и Source SDK:

C++:
GoldSource, monster_apache:

// pitch forward or back to get to target
if (flDist > 0 && flSpeed < m_flGoalSpeed /* && flSpeed < flDist */ && pev->angles.x + pev->avelocity.x > -40)
{
    // ALERT( at_console, "F " );
    // lean forward
    pev->avelocity.x -= 12.0;
}
else if (flDist < 0 && flSpeed > -50 && pev->angles.x + pev->avelocity.x  < 20)
{
    // ALERT( at_console, "B " );
    // lean backward
    pev->avelocity.x += 12.0;
}
else if (pev->angles.x + pev->avelocity.x > 0)
{
    // ALERT( at_console, "f " );
    pev->avelocity.x -= 4.0;
}
else if (pev->angles.x + pev->avelocity.x < 0)
{
    // ALERT( at_console, "b " );
    pev->avelocity.x += 4.0;
}
А вот Source, hl1_npc_apache.cpp

C++:
// pitch forward or back to get to target
if (flDist > 0 && flSpeed < m_flGoalSpeed && GetAbsAngles().x + angVel.x < 40)
{
    // ALERT( at_console, "F " );
    // lean forward
    angVel.x += 12.0;
}
else if (flDist < 0 && flSpeed > -50 && GetAbsAngles().x + angVel.x  > -20)
{
    // ALERT( at_console, "B " );
    // lean backward
    angVel.x -= 12.0;
}
else if (GetAbsAngles().x + angVel.x < 0)
{
    // ALERT( at_console, "f " );
    angVel.x += 4.0;
}
else if (GetAbsAngles().x + angVel.x > 0)
{
    // ALERT( at_console, "b " );
    angVel.x -= 4.0;
}
Даже непосвященному человеку понятно, что это тот же самый код, слегка измененный с учётом архитектуры сорса. И тот факт, что все проверки на PITCH изменились на прямо противоположные, но без тупой инверсии. Те, кто работал с HLSDK, наверняка уже потирают руки - ага, вот он корень зла, функция MakeAimVectors. Ведь PITCH там действительно инвертирован, а значит все беды - из-за нее. Неплохая попытка, но - снова мимо. Ведь в Quake не было никакого MakeAimVectors, а баг - был. Я скажу больше - инверсия PITCH в MakeAimVectors - это просто-напросто ответная часть для клиентского кода в StudioSetupTransform. Т.е. ID профиксила баг, инвертировав брашы, а Valve, первоначально загнала его еще глубже - обратно в модели. Но мы уже подобрались достаточно близко. Тут следует сказать вот еще что. Эта функция не используется, непосредственно вертолётом. Т.е. вертолёт проектировался просто с учётом наличия Stupid Quake Bug. А вот его ракетах - эта функция действительно есть. Ну и чтобы больше вас не мучать я назову её имя. Это...

UTIL_VecToAngles

Да, товарищи. Весь Stupid Quake Bug кроется именно в ней, а все остальное - это лишь безуспешные попытки борьбы с ним. Я имею право это утверждать, как человек, полностью заборовший баг у себя в движке. Но чтобы стало понятно и вам - продолжим наш анализ.

Вот реализация функции в Quake:
C++:
void PF_vectoangles (void)
{
    float    *value1;
    float    forward;
    float    yaw, pitch;
   
    value1 = G_VECTOR(OFS_PARM0);
   
    if (value1[1] == 0 && value1[0] == 0)
    {
        yaw = 0;
        if (value1[2] > 0)
            pitch = 90;
        else
            pitch = 270;
    }
    else
    {
        yaw = (int) (atan2(value1[1], value1[0]) * 180 / M_PI);
        if (yaw < 0)
            yaw += 360;
       
        forward = sqrt (value1[0]*value1[0] + value1[1]*value1[1]);
            pitch = (int) (atan2(value1[2], forward) * 180 / M_PI);
        if (pitch < 0)
            pitch += 360;
    }
   
    G_FLOAT(OFS_RETURN+0) = pitch;
    G_FLOAT(OFS_RETURN+1) = yaw;
    G_FLOAT(OFS_RETURN+2) = 0;
}
Что мы здесь видим? Да ничего особенного, функция как функция, вроде бы правильная. Теперь посмотрим эту же функцию в Quake2, там она перекочевала в игровую библиотеку.

C++:
void vectoangles (vec3_t value1, vec3_t angles)
{
    float    forward;
    float    yaw, pitch;
   
    if (value1[1] == 0 && value1[0] == 0)
    {
        yaw = 0;
        if (value1[2] > 0)
            pitch = 90;
        else
            pitch = 270;
    }
    else
    {
        if (value1[0])
            yaw = (int) (atan2(value1[1], value1[0]) * 180 / M_PI);
        else if (value1[1] > 0)
            yaw = 90;
        else
            yaw = -90;
        if (yaw < 0)
            yaw += 360;
       
        forward = sqrt (value1[0]*value1[0] + value1[1]*value1[1]);
            pitch = (int) (atan2(value1[2], forward) * 180 / M_PI);
        if (pitch < 0)
            pitch += 360;
    }
   
    angles[PITCH] = -pitch;
    angles[YAW] = yaw;
    angles[ROLL] = 0;
}
Отличия видите? Теперь заглянём в Quake3.

C++:
void vectoangles( const vec3_t value1, vec3_t angles ) {
    float    forward;
    float    yaw, pitch;
   
    if ( value1[1] == 0 && value1[0] == 0 ) {
        yaw = 0;
        if ( value1[2] > 0 ) {
            pitch = 90;
        }
        else {
            pitch = 270;
        }
    }
    else {
        if ( value1[0] ) {
            yaw = ( atan2 ( value1[1], value1[0] ) * 180 / M_PI );
        }
        else if ( value1[1] > 0 ) {
            yaw = 90;
        }
        else {
            yaw = 270;
        }
        if ( yaw < 0 ) {
            yaw += 360;
        }
       
        forward = sqrt ( value1[0]*value1[0] + value1[1]*value1[1] );
            pitch = ( atan2(value1[2], forward) * 180 / M_PI );
        if ( pitch < 0 ) {
            pitch += 360;
        }
    }
   
    angles[PITCH] = -pitch;
    angles[YAW] = yaw;
    angles[ROLL] = 0;
}
Упс. И тут такая же беда. Кстати, обратите внимание на инвертированный PITCH в функциях от Quake2 и Quake3. Похоже в ID знали о проблеме, но как настоящие ленивые программисты не торопились переписывать код всех монстров, мотивируя тем, что "и так работает".

Однако, это снова не исправление бага, как мы помним в Quake2 он никуда не делся. За неимением исходников GoldSource, приведу функцию из Ксаша. Поскольку в модах под ним всё работает правильно, возьму на себя смелость утверждать, что в халфе она такая же:

C++:
void VectorAngles( const float *forward, float *angles )
{
    float    tmp, yaw, pitch;
   
    if( !forward || !angles )
    {
        if( angles ) VectorClear( angles );
            return;
    }
   
    if( forward[1] == 0 && forward[0] == 0 )
    {
        // fast case
        yaw = 0;
        if( forward[2] > 0 )
            pitch = 90.0f;
        else pitch = 270.0f;
    }
    else
    {
        yaw = ( atan2( forward[1], forward[0] ) * 180 / M_PI );
        if( yaw < 0 ) yaw += 360;
           
        tmp = sqrt( forward[0] * forward[0] + forward[1] * forward[1] );
        pitch = ( atan2( forward[2], tmp ) * 180 / M_PI );
        if( pitch < 0 ) pitch += 360;
    }
   
    VectorSet( angles, pitch, yaw, 0 );
}
Ну и напоследок я вам наконец-таки покажу правильную функцию из HL2 SDK.

C++:
void VectorAngles( const float *forward, float *angles )
{
    Assert( s_bMathlibInitialized );
    float    tmp, yaw, pitch;
   
    if (forward[1] == 0 && forward[0] == 0)
    {
        yaw = 0;
        if (forward[2] > 0)
            pitch = 270;
        else
            pitch = 90;
    }
    else
    {
        yaw = (atan2(forward[1], forward[0]) * 180 / M_PI);
        if (yaw < 0)
            yaw += 360;
       
        tmp = sqrt (forward[0]*forward[0] + forward[1]*forward[1]);
        pitch = (atan2(-forward[2], tmp) * 180 / M_PI);
        if (pitch < 0)
            pitch += 360;
    }
   
    angles[0] = pitch;
    angles[1] = yaw;
    angles[2] = 0;
}
Весьма возможно, что отличия от Q2\Q3 функций - чисто косметические, а результат - идентичный. Впрочем не будем забывать про мешанину диапазонов 0\360 и -180\180, что тоже может оказывать влияние в определённых местах. Однако смена самой функции без переписывания кода монстров и вычищения бага (читай инверсии PITCH) практически изо всех мест ни к чему не приведет. К счастью вам и не придется проделывать эту серъезную работу вслепую. Есть мой референс, в виде XashXT 0.65, где баг был действительно успешно вычищен. Если вы захотите проделать нечто подобное у себя, то теперь уже будете опираться на опыт моих исследований, вместо того чтобы пихать инверсию питча во все доступные места.

Заключение

Заключительную часть слова хотелось бы сделать морализаторской. Вот товарищи, как казалось бы, маленькая и незначительная деталь, отсутствие инверсии в нужном месте приводит к невероятной головной боли и жутким глюкам, с которыми на протяжении многих лет пытаются безуспешно бороться, не в силах докопаться до сути причины или из-за банальной лени. И только когда совсем припекает - садятся переписывать мегабайты кода, ради того чтобы вычистить прочно засевшую пакость. Не доводите свои программы до такого. Даже если предполагается, что вы к ней больше никогда не вернётесь. Не вы, так кто-то другой обязательно вспомнит вас тихим незлым словом.
Ну, на сегодня всё. Пишите, о чём бы вам еще хотелось почитать, в рамках данного цикла статей (связаться с автором статьи можно здесь).

 
Последнее редактирование:

Пользователи, просматривающие эту тему

Сейчас на форуме нет ни одного пользователя.
Сверху Снизу