Half-Life и Adrenaline Gamer форум

Всё об игре в Халф-Лайф и АГ
Текущее время: 28 мар 2024, 16:26

Часовой пояс: UTC + 5 часов [ Летнее время ]




Начать новую тему Ответить на тему  [ Сообщений: 3 ] 
Автор Сообщение
СообщениеДобавлено: 22 мар 2012, 07:24 
Не в сети
Аватара пользователя
Зарегистрирован:
06 июн 2010, 16:53
Последнее посещение:
26 мар 2024, 14:36
Сообщения: 1143
Откуда: Владивосток
Работа со строками в Quake-движках

Автор: Дядя Миша
Источник: HLFX.Ru Forum

Работа со строками, как известно, является основной головной болью для программистов на С\С++. В чистом Си на этот счёт вообще не заморачивались, а в С++ Страуструп по каким-то неизвестным причинам не сделал строку одним из базовых типов. То ли побоялся, то ли ему и Си-шных средств вполне хватало. В результате из штатных реализаций нам предлагают только контейнеры STL, а те, кому они не нравятся пишут свои классы строк и извращаются при этом как только могут. Для игровых движков же ситуация была довольно неоднозначная, в своё время. Это сейчас, когда движки пишутся преимущественно на С++, когда STL практически полностью отлажен и его не боятся использовать. Никто в движках даже и не задумывается, как хранить строки и о методах доступа к ним. Но во времена первой, второй, да и третьей кваки всё было далеко не так однозначно. Ку2 и Ку3, как написанные полностью на Си (в ку3 игровой код также написан на чистом Си, но может быть скомпилен в виртуальную машину), использовали стандартные Си-средства для работы со строками. Кармак по этому поводу вообще не строил никаких иллюзий и не парился. Его Сишные строки нисколько не смущали. А вот в первом квейке, и созданном на его базе Half-Life сложилась настолько неоднозначная и я бы даже сказал попросту опасная ситуация, что наши моддеры порою даже не подозревают насколько по тонкому льду они ходят. Именно это обстоятельство и побудило меня написать развернутую статью о методах работы со строками в Half-Life.

1. Немного истории.
Но начать придётся всё же со старого доброго квейка. Как вы помните Half-Life унаследовал основные черты своего родителя, причём главным образом - в механизме взаимодействия с игровой библиотекой. В квейке подгружалась виртуальная машинка, в халфе наместо виртуальной машинки подгружается самая обычная Win32 библиотека. Пикантность ситуации заключается в том, что механизм взаимодействия был переписан ровно настолько, чтобы была возможность заменить виртуальную машинку на обычную библиотеку. Для наглядности примера, представьте себе ваз классику, которому выдернули родной салон и втулили салон от мерседеса или бмв. И вроде бы, если бы смотреть только на парприз - так душа радуется. А если поглядеть по сторонам внимательно - там мешалка обо что-то трётся, тут сидушки криво встали, там кусок парприза дверь мешает закрыть, да и сам он как-то обзор загораживает. Но в целом вроде ездить можно. И посоны во дворе одобряют. Конечно сложно заподозрить Valve в моддинге ради моддинга, я предполагаю, что они держались за кушную виртуальную машину ровно до тех пор пока ихние запросы превысили её скромные возможности. Тогда-то и было принято решение по быстрому переналадить движок на использование обычных библиотек.
А что из этого получилось, мы сейчас с вами и увидим. Как известно, кушная виртуальная машинка работает всего с тремя типами данных - это float, vector и string. Первые два нас мало интерисуют, а вот работу со строками рассмотрим поподробнее. Во первых, хочу заметить, что виртуальные машинки такого типа являются полностью безопасными в силу интерпретируемости. Это значит что пользователь со стороны QC не имеет доступа ни к каким адресам в памяти и боже упаси - указателям. В то же время как любая строка - это массив из символов и указатель указывает на начало строки. Выход из ситуации был крайне простым - строку переконвертировали в смещение от некоего абсолютного значения в памяти, которое статично и никуда не перемещается, даже в принципе не может. В Quake этим абсолютным значением был участок памяти со строками, которые были вкомпилены прямо в progs.dat. Т.е. это были константные строки, написанные программистом в коде. Новые строки добавлялись по такому же принципу. Рассмотрим механизм формирования такой строки во время работы движка:
Код:
string_t iString = ED_NewString (str) - pr_strings;
Здесь как вы видите, мы вычитаем полученный адрес на начало строки из нашего абсолютного значения. Получившееся значение при этом может быть любым, для нас это не играет никакой роли. Для нас важно другое: прибавив это смещение обратно к pr_strings мы вновь получим указатель на начало нашей строки. Таким образом реализуется хранение строк в переменных типа int (string_t это typedef int).
Код:
const char *str = pr_string + iString;
Система, как видите не идеальна. Во первых полученные значения уникальны даже для одинаковых строк. Это значит что сравнивать идентификаторы между собой не имеет смысла. Их придется преобразовать обратно в строки и сравнить обычным Си-способом (чере strcmp). Кроме того система никак не борется с повторяющимися строками (дупликатами). Но в целом возложенную задачу она решает. Тут следует упомнять пожалуй самое главное, то чего не было в Quake и то, что мы получили в Half-Life - принципиальную невозможность создавать новые строки во время исполнения программы. Максимум что было дозволено - объявить константную строку и присвоить её куда-нибудь в entvars_t. Ну например написать:
Код:
self.classname = "monster_xaerox";
Так же строки можно копировать и сравнивать. На этом возможности исчерпываются. Понятное дело что весь этот механизм был полностью возложен на движок и как следствие - избавлен от ошибок скриптёров на куси. Проблемы начались вместе с моддингом под халфу...

2. Что было сделано в Half-Life.
Виртуальную машину в GoldSrc успешно выбросили на свалку истории. Но не совсем. Во первых, остались те же самые эдикты, осталась структура entvars_t и остались наши любимые строки в виде string_t. Почему остались? Там был довольно большой объем кода, который попросту поленились переписать либо поджимали сроки разработки. Поскольку GoldSrc больше не грузит progs.dat, то pr_strings с указателем на статичные строки из виртуальной машинки был заменен на
Код:
gpGlobals->pStringBase = "";
Следует пояснить особенность компиляции обычных программ, которая заключается в том, что все константные строки, объявленные явным образом являются перманентными, даже при условии что они были объявлены где-то внутри функции. Поэтому такая вот пустая строка никуда не может дется по определению и является отличным абсолютным ориентиром. Но и аналогичные строки в игровой библиотеке точно так же являются перманентными. Перевыделять память для них посчитали излишним. Поэтому была введена функция MAKE_STRING. Беда в том, что многие не то что незнают разницы между ALLOC_STRING и MAKE_STRING, а просто таки лепят в своём коде их по очереди, то так эдак. Частично положение спасается тем фактом, что для наиболее важных мест движок выделяет строки самостоятельно.
Теперь представим себе такой код, когда пользователь выделяет строку самостоятельно, ну например:
Код:
virtual void    KeyValue( KeyValueData *pkvd )
{
    // get support for spirit field too
    if( FStrEq( pkvd->szKeyName, "parent" ) || FStrEq( pkvd->szKeyName, "movewith" ))
    {
        m_iParent = ALLOC_STRING(pkvd->szValue);
        pkvd->fHandled = TRUE;
    }
что будет если в вышеприведенном примере заменить ALLOC_STRING на MAKE_STRING? А вот что! Указатель на строки KeyValueData является временным. Эти строки актуальны лишь во время парсинга очередной энтити. Таким образом через весьма непродолжительное время наша разность указателей, записанная в m_iParent будет указывать на УЖЕ несуществующий адрес локальной переменной. Но пока мы не попытались к ней обратиться (например на карте ничего не скреплено при помощи parent system) то и никакой ошибки, понятное дело не возникнет. Первое же обращение к строке по такому смещению приведет к моментальному вылету и с большей долей вероятность халфа просто схлопнется не дав вам ни малейшего шанса на отладку. Вот почему так важно понимание работы ALLOC_STRING и MAKE_STRING.
Итак, MAKE_STRING можно использовать только в случаях, типа:
Код:
pev->classname = MAKE_STRING( "weapon_9mmhandgun" );
Во всех остальных случаях вы просто обязаны применять ALLOC_STRING. Если вы вообще не уверены что использовать либо ничего не поняли из вышепрочитанного - откажитесь от использования MAKE_STRING совсем, используйте только ALLOC_STRING. Да, это приведет к небольшому перерасходу памяти, но кто её считает в 2012 году.

3. Главная засада.
Если вы полагаете, что я расписал все сюрпризы, которые может вам преподнести халфа в данной области, то вы глубоко заблуждаетесь. Разговор о главной пакости еще только начинается. Главная пакость, опять таки берет своё начало из виртуальной машины первого квейка, где её использование не вызывало никаких проблем.
И привело к чудовищным потенциальным проблемам в GoldSrc о которых большинство даже и не подозревает. Я говорю о функциях, типа PRECACHE_MODEL, PRECACHE_SOUND и им подобных. Скажите, вы никогда не задумывались, что происходит с той строкой, которую вы подсовываете этим функциям в качестве аргумента?
И правильно делали, что не задумывались, потому что истинное положение дел такого рода, что лучше о нём вообще не думать. Итак: движок берет указатель на эту строку из аргумента и сохраняет куда бы вы думали? В свой МАССИВ УКАЗАТЕЛЕЙ на строки. Т.е. гарантия сохранности этих строк целиком полностью возложена на плечи моддера, который вообще не подозревает о таких вещах и уж тем более не задумывается о том, что локальные переменные уничтожаются по завершении работы функции. Другими словами - абсолютно все строки, которые вы кормите функциям, типа PRECACHE_MODEL должны быть либо помещены в память для строк через ALLOC_STRING либо быть просто константными строками. Не дай вам бог завести массив char в классе и кормить имя модели из этого массива - энтить умрёт и движок опять потеряет строку. То же самое относится и к локальной переменной, в которую вы записали имя вашей модели. В обоих рассмотренных случаях half-Life схлопнется без единого звука не оставив вам ни малейшего шанса на отладку. И вы даже не поймете что произошло. В крайних случаях допускается хранить строки в глобальных массивах char - так например сделано в коде changelevel. Сам по себе данный способ не является опасным, однако эти глобальные строки вам придется очищать самостоятельно при необходимости. Ну и напоследок ложка мёда к вышесказанному - в Xash3D все имена моделей копируются в локальный массив имён и поэтому вышеописанная ситуация там невозможна впринципе. Однако то что касается ALLOC_STRING и MAKE_STRING справедливо и для Xash3D (второй раздел статьи).

4. Заключение.
Данная статья конечно малоинформативна в плане принципов устройства Quake-движков, но она наглядно раскрывает всю суть чудовищных граблей на которые рано или поздно наступает любой кодер под GoldSrc, выросший из простого копи-пастинга. И хуже всего того, что грабли, больно ударив по лбу так и не раскрывают причину проблемы. Я сам столкнулся с данным явлением еще во времена импелментации кастомных оружий Xash Weapon System, так и не сумев понять причину загадочных багов. Искренне надеюсь что моя статья прольет свет на это явление и убережет вас от ошибок. А может быть прольет свет на причины непонятных вылетов старого мода, давным-давно заброшенного в разработке, как раз по причине эти необъяснимых вылетов и вы таки найдете в себе силы его доделать. Кто знает...

_________________
Изображение
Vi Veri Veniversum Vivus Vici


Вернуться к началу
 Профиль 
  
СообщениеДобавлено: 22 мар 2012, 07:27 
Не в сети
Аватара пользователя
Зарегистрирован:
06 июн 2010, 16:53
Последнее посещение:
26 мар 2024, 14:36
Сообщения: 1143
Откуда: Владивосток
Отличная статья, очень полезная.

ЗЫ
Мб пора нам под статьи отдельную ветку завести? :)

_________________
Изображение
Vi Veri Veniversum Vivus Vici


Вернуться к началу
 Профиль 
  
СообщениеДобавлено: 22 мар 2012, 13:34 
Не в сети
Site Admin
Зарегистрирован:
01 июн 2010, 01:27
Последнее посещение:
26 мар 2024, 21:42
Сообщения: 6864
Добавлю ещё, что
Код:
// Use this instead of ALLOC_STRING on constant strings
#define STRING(offset)      (const char *)(gpGlobals->pStringBase + (int)offset)
#define MAKE_STRING(str)   ((int)str - (int)STRING(0))
Т.е. MAKE_STRING это просто макрос для вычитания из адреса строки, базового адреса - gpGlobals->pStringBase.


Вернуться к началу
 Профиль 
  
Показать сообщения за:  Поле сортировки  
Начать новую тему Ответить на тему  [ Сообщений: 3 ] 

Часовой пояс: UTC + 5 часов [ Летнее время ]


Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и гости: 3


Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете добавлять вложения

Найти:
Перейти:  
Создано на основе phpBB® Forum Software © phpBB Group
Русская поддержка phpBB