/
Tags: вычислительная техника микропроцессоры
ISBN: 5—88579-006—X
Text
Станьте высококлясным программистом жяпишите шру
типа DOOM! Советы профессионала, все необходимое ия дискетах:
DOOM изнутри, системы программирования 3D, драйвера
для создания звука и видеовстявок, файлы с готовыми
монорями. Как зобизься насюяшею стереоизображения.
ББК 32.97
Х35
А. Хонич
X 35 Как самому создать трехмерную игру.—М.: фирма
МИКРОАРТ, 1996. - 290 с.: ил.
ISBN 5—88579-006—X
Книга предназначена для тех, кто хочет познакомиться с
устройством трехмерной игры типа DOOM и попробовать свои
силы в создании увлекательных игр. В книге шаг за шагом
изложены структура программы, все процедуры и их взаимосвязь,
что будет интересно программистам пишущим объемистые
программы. Также изложены интересные алгоритмы и рассмотрены
тонкие места.
Для програмистов владеющих язком "С”, а также для всех тех,
кто хочет повысить свою квалификацию в программировании.
х 2404020000-006 ^^
37Г (03)—96
ISBN 5—88579—006—X
© фирма МИКРОАРТ
ОГЛАВЛЕНИЕ
ОГЛАВЛЕНИЕ......................................................3
ПРЕДИСЛОВИЕ.....................................................9
ГЛАВА 1
ВЫБОР ИНСТРУМЕНТАРИЯ...................................... 14
DOS Extender DOS4GW и виртуальная память..................... 19
ГЛАВА2
СТРУКТУРА ИГРЫ.............................................21
2.1. Методология создания видеоигр............................21
2.2. Как разбить игру на секции...............................22
ГЛАВА 3
ИНИЦИАЛИЗАЦИЯ..............................................24
3.1. Распределение памяти для экранов (V_AllocMem())............24
3.2. Загрузка стандартной конфигурации (LoadDefaults()).......25
3.3. Инициализация зоны памяти (Zonelnit())...................26
3 .4. Инициализация файлов ресурсов (InitWadFiles())........29
3.5. Инициализация графических данных. (RefreshlnitO).........32
3.5.1 Инициализация текстур...................................32
3.5.1.1. Инициализация текстур для стен (WallTextures)........32
3 .5.1.2. Инициализация текстур для полов/потолков(Е1аПехШге5)..40
3.5.2 . Инициализация спрайтов (функция Spriteslnfo)..........40
3.5.3 . Инициализация таблицы цветов (LoadColorMap)...........41
3.6. Инициализация анимации (InitPlayLoop)....................42
3.6.1. Инициализация таблицы текстур для переключателей (InitSwitchTable)... 42
3.6.2. Инициализация анимации текстур (функция InitPicAnims)..44
3.6.3. Инициализация анимации спрайтов (InitSprites)..........46
3.7. Установка статуса машины (SetupMachineState).............48
3.7.1. Настройка DPMI (LockRegion)............................48
3.7.2. Инициализация “Мыши” (StartupMouse)....................49
3.7.3. Инициализация Джойстика (StartupJoystick)..............50
3.7.4. Инициализация клавиатуры (StartupKeyboard).............51
3.7.5. Инициализация звука (StartupSound).....................51
3.7.6. Инициализация таймера (StartupTimer)...................53
3 .8. Установка звука (SetupSound)...........................54
3.9. Загрузка основного шрифта (HU Init)......................57
3.10. Инициализация статус панели (ST Init)...................58
ГЛАВА 4
ОСНОВНОЙ ЦИКЛ ИГРЫ (PLAYLOOP)..............................59
4.1. Многостраничный режим VGA 320x200x256 и как с ним работать.60
4.2. Многорежимность и многофункциональность..................67
ГЛАВА 5
СИНХРОНИЗАЦИЯ СКОРОСТИ ПО ТАЙМЕРУ...........................69
ГЛАВА 6
КОНТРОЛЬ СО СТОРОНЫ ИГРАЮЩЕГО...............................71
6.1. CHEAT коды. Что это такое и как с ними работать?.........79
6.2. Работа с меню............................................82
ГЛАВА 7
НАЧАЛО ИГРЫ (ЗАГРУЗКА ДАННЫХ)..............................89
7.1. Ресурс THINGS. (Первый объект)...........................94
7.1.1. Типы объектов..........................................94
7.1.2. Атрибуты объектов......................................95
7.1.3. Структура “sprites”.................................. 97
7.1.4. “Оживление” объектов (анимация)........................99
7.1.5. Загрузка объекта THINGS...............................103
7.2. Ресурс LINEDEFS. (2-ойобъект)...........................111
7.2.1. LINEDEFS атрибуты.....................................112
7.2.2. LINEDEFS тапы.........................................113
7.2.3. Загрузка ресурса LINEDEFS.............................114
7.3. Ресурс SIDEDEFS. (Третий объект)........................117
7.3.1. Загрузка ресурса SIDEDEFS.............................118
7.4. Ресурс VERTEXES. (Четвертыйобъект)......................119
7.4.1. Загрузка ресурса VERTEXES.............................119
7.5. Ресурс SEGS. (Пятый объект).............................120
7.5.1. Загрузка ресурса SEGS.................................121
7.6. Ресурс SSECTORS. (Шестой объект)........................122
7.6.1. Загрузка ресурса SSECTORS.............................122
7.7. Ресурс NODES. (Седьмой объект)..........................123
7.7.1. Загрузка ресурса NODES................................129
7.8. Ресурс SECTORS. (Восьмойобъект).........................131
7.8.1 Типы секторов..........................................132
7.8.2 Загрузка и обработка ресурса SECTORS...................134
7.9. Ресурс REJECT. (Девятый объект).........................138
7.10. Ресурс BLOCKMAP. (Десятый объект)......................140
7.10.1. Загрузка ресурса BLOCKMAlP...........................141
7.11. Функция GroupLines.....................................142
7.11. Функция StartSectors...................................147
7.12. Функция RefreshTextures................................149
ГЛАВА 8
ОСНОВНЫЕ ВИРТУАЛЬНЫЕ ПРОЦЕССЫ В 3D ИГРАХ...................150
Функции GeneralControl. ..................................150
8.1. Структура DEMO файла........................................................153
Функция RunDemo...............................................................153
8.2. Обработка объектов..........................................................154
8.2.1. Обработка данных игрока...................................................154
Функция ProcessingPlayer. ................................................... 154
Функция PlayerDead............................................................156
Функция ContPlayerToMoves.....................................................157
Функция ControlPlayerToKorZ. .................................................158
Функция PlayerlnSpecialSector.................................................159
Функция Shooting................................................................................. 161
Функция PressUse..............................................................163
Функции CheckWay Run..........................................................163
Функции CheckTouchLine и CheckTouchThing......................................167
Функция MatchDistanceOflectors......................................................... 169
Функция CheckPozit Line.......................................................170
Функция PressUse................................................................................. 171
Функция SetDataCeilFloor......................................................172
Функция SpecLinePushButton............................................................. 172
Функции SetOppositeTexture....................................................174
Функция StartButton...........................................................175
8.2.2. Обработка объектов. Классы объектов........................................176
Функция ProcessingThings......................................................176
Функция обработки класса: SpriteProcessing....................................177
Функция обработки кчасса дверей: OpenDoor.....................................177
Функция обработки кчасса потолков: ProcessCeil............................. 178
Функция обработки кчасса полов: ProcessFloor..................................179
Функция обработки кчасса платформ: MovePlats. ................................181
Функция обработки кчасса: LightBlinks. .......................................181
Функция обработки кчасса: RandLightBlinks.....................................182
Функция обработки кчасса: LightOscill.........................................183
8.2.2.1 Класс объектов THING......................................................184
Функции SpriteProcessing......................................................184
Функция горизонтальных перемещений Move Horizontal............................185
Функция SlideTraverse.........................................................187
Функция EndFlyMissile.........................................................188
Функция вертикальных перемещений Movel ertical. ..............................188
Функция анимациии спрайтов RunSprite............................................ 190
Функции обработки кадров......................................................190
Состояние покоя (пассивное состояние).............................................192
Функция RestEnemy.............................................................193
Функции CheckVisualOfThings и CheckLocationEnemy..............................194
Как самому создать трехмерную игру
Состояние преследования (активное состояние)......................196
Функция ActiveEnemy...........................................197
Функция ChangeTrend. .........................................199
Функция NewChaseDir...........................................199
Функция OneStep Thing.........................................201
Функция RandChaseDir..........................................201
Функция SpawnStepThing........................................202
Функция Check Near Attack. ...................................203
Функция CheckAttack...........................................204
Ближняя и дальняя атака...........................................205
Функция SetAngEneniy. ........................................205
Функция Shoot BaronOfHell.....................................205
Функция SpawnMissile..........................................206
Функция EndFlyMissile.........................................207
8.2.22 Класс объектов DOOR.......................................207
Инициализация класса DOOR - функции InitKeyDoors и
InitDoorsFromTrigger..............................................207
Функция GetNumCorrespondSector................................208
Обработка класса DOOR - функция Open Door.....................209
Функция GetCurrentPhase.......................................210
8.2.2. 3 Класс объектов LIGHT.....................................211
Инициализация класса LIGHT - функция InitLightBlinks. ........211
Функция GetMinLightFroni......................................212
Обработка класса LIGHT - функция LightBlinks..................213
8.2. 3 Обработка оружия...........................................213
Функция InitWeapon............................................214
Функция Init GetWeapon........................................215
Функции Spawn WeaponFrame.....................................217
Функция InitWeapon............................................217
Функция GetWeapon.............................................218
Функция NormalWeapon..........................................218
Функция ShotFromWeapon........................................219
Функция ShotFromShotgun.......................................221
Функция MatchHeightOJTarget...................................221
Функция AutoSetHeigh ToShoot..................................222
Функции ShotWithBullet........................................223
Функция ShotToMaxDistance.....................................223
Функция HitToTarget...........................................224
Функция HitToTarget.......................................... 226
Функции SpawnBlood............................................226
Функция CheckToNextFire.......................................227
Функция ShotWithBullet....................................227
Функция RemoveWeapon......................................228
8.2. 4. Управление анимацией.................................229
Функция ProcessingAnimated................................229
ГЛАВА 9
ОСНОВНЫЕ ПРИНЦИПЫ РАБОТЫ С ГРАФИКОЙ....................... 231
9 .1. Особенности организации графики в игре DOOM..........232
9.2. Трехмерность............................................233
9.3. Принцип разработки графических функций..................234
9.4. Основная функция обработки графики в игре DOOM..........235
Функция Init ValTab forPlayer.............................240
Функция InitTable WallPosX................................242
Функция Init DrawSegs. ...................................243
Функция InitTableSprite...................................244
Функция ProcessTree.......................................244
Функции Draw Subsector....................................245
Функция ProJectSpritesInSector............................248
Функция DrawSegment.......................................248
Функция DrawPlanes. ......................................252
Функции ProjectCurColumn forPlane.........................258
(функция DrawSprites......................................260
Функция DrawWeapon........................................260
ГЛАВА 10
СОЗДАНИЕ СОБСТВЕННОЙ ГРАФИКИ..............................262
10.1. Введение...............................................262
10.2. Основные этапы создания графики........................263
10.2. Создание видео.........................................267
ГЛАВА 11
ПОГОВОРИМ О “СТЕРЕО”......................................268
11.1 Введение................................................268
11.2 . Итак, задело..........................................269
Функция инициализации.....................................270
Обработчик прерывания.....................................271
Функция деструктора.......................................271
ПРИЛОЖЕНИЕ
КАК СДЕЛАТЬ, ЧТОБЫ ВАШИ ПРОГРАММЫ РАБОТАЛИ БЫСТРЕЕ .... 273
1. Использование механизма предварительных вычислений.....273
2. Использование табличных данных.........................273
3. Желательно, чтобы количество передаваемых параметров при
вызове функции не превышало 4 (3)............................275
4. При работе на ассемблере используйте неявные операции
умножения и деления на константу............................275
5. Старайтесь не пользоваться универсальными функциями и
прерываниями, а применяйте свои собственные, узко специализированные,
быстродействующие функции..................................275
6. Оптимизируйте работу с циклами.......................276
7. Оптимальная работа с видеобуфером....................276
8. При работе на ассемблере используйте 32-разрядные регистры
даже там, где это не обязательно...........................279
9. Все, критичные по времени, функции и циклы после отладки
желательно переписать на ассемблер..........................279
10. Избегайте пользоваться прерываниями реального режима.279
11. Выключите менеджеры expanded памяти..................280
ПРЕДИСЛОВИЕ
В последнее время компьютер все прочнее и стремительней входит в наш
быт, становясь такой же привычной вещью, как телевизор или магнитофон,
хотя совсем недавно приобрести его могли лишь состоятельные люди или
организации. Причиной этой компьютерной революции является резкое
снижение цен на комплектующие, и, как следствие, на сами компьютеры.
С появление систем Мультимедиа все больше людей покупают компьютер
для домашнего пользования. Мультимедиа - это волшебный мир занимательных
игр, огромных энциклопедий, путеводителей с отличной графикой, видео и
звуком. В мультимедийные игры могут играть не только дети, но и взрослые.
В настоящее время стали появляться очень красивые и реалистичные
игры, которые с некоторой натяжкой можно отнести к виртуальной
реальности, (хотя до настоящей реальности еще очень далеко). Примером
одной из таких игр является выпущенная в 1993 году фирмой ID Software
игра “DOOM”, что в переводе означает “Рок”, “Судьба”. За очень
короткий срок эта игра создала себе такую репутацию, как ни одна другая
игра за всю историю развития компьютерных игр. Что же так выгодно
отличает игру DOOM от других игр?
• Первое, что бросается в глаза - это потрясающий реализм. Движения
игрока в трехмерном лабиринте осуществляются необычайно плавно.
Однако, это само по себе не является новым. Подобное мы уже видели,
например, в игре “Wolf 3D” (тоже фирмы 1D). В отличие от других
трехмерных игр, в DOOM’e использована новая система потения трехмерных
лабиринтов, использована новая технология 3D ENGINE, которая
практически по всем параметрам превосходит все предшествующие системы
и, имея необычайную гибкость, позволяет строить всевозможные лабиринты
почти без ограничений. Рассмотрим основные атрибуты данной системы
построения, которые отсутствуют в Wolf 3D:
1. Полы и потолки в различных секторах могут иметь различную высоту.
2. Использование совершенного алгоритма масштабируемого
текстурирования стен и полов (потолков) с использованием системы
перспективного затенения.
3. Стены по отношению друг к другу могут располагаться под различными
углами (не только под прямыми).
В дополнение можно сказать, что использована гибкая система управления
освещенностью, лестницы, лифты, прессы и многое другое.
В связи с вышеперечисленным нетрудно догадаться, что данная система
далеко продвинулась по сравнению с “коридорным” стилем игры “Wolf 3D”.
Используя все ее возможности, можно создавать уникальные строения с
катакомбами, дворцами, балконами, огромными залами и узкими коридорами.
• Отличный звук еще более усиливает эффект присутствия в и без того
реальной атмосфере.
• Богатое разнообразие. Имеется около 100 текстур для закраски стен и
полов, огромное количество объектов (как движущихся, так и
неподвижных). Это значительно обогащяет игру, внося в нее элементы
неожиданностей. Например, в DOOM-1 встреча со Spider Demon’oM происходит
только на самом последнем уровне.
• Возможность играть по сети по протоколу IPX до четырех человек. (Также
имеется режим DEATHMATCH - состязание в смерти).
• Открытая архитектура. Этот пункт можно было бы поставить на первое
место, т.к., на мой взгляд, именно возможность редактирования, изменения и
создания новых лабиринтов явилось причиной столь высокого рейтинга игры
DOOM. Но менять можно не только лабиринты, но и график,; звук и многое
другое, т.е. фактически можно создать новую игру (в качестве примера можно
привести “Alien DOOM”). В настоящее время для DOOM’a существует около
1000(!) уровней, созданных независимыми лицами. Существуют отдельные CD
с новыми уровнями и всевозможными программами, утилитами для DOOM’a.
На этих же CD имеются неплохие редакторы уровней.
Игра DOOM открыла новое направление в индустрии компьютерных
развлечений. Другие фирмы стали выпускать трехмерные игры “от первого
лица”, и сейчас их выпущено немало. Наиболее яркие представители:
“HERETIC”, “HEXEN”, “DESCENT”. “ROTT”, “DARK FORCES” и др. Но
практически ни одна из них не дотягивает до первоисточника. У некоторых игр
сложное управление, у некоторых графика слаба, у некоторых - 3D технология
несовершенная, и др. Обычно после 3-часового (максимум недельного)
состязания в одну из этих игр мнения о ней обычно сводятся к одной фразе: “А
все-таки DOOM лучше!”. Единственным исключением может быть (хотя мнения
в этом вопросе расходятся) игра “HERETIC” от Raven Software. Но это и
неудивительно. Дело в том, что в HERETIC’e используется та же технология 3D
ENGINE, что и в DOOM’e. Пожалуй, единственным новшеством в HERETIC’e в
техническом плане (на мой взгляд, отрицательным) является система VGA с
одной видеостраницей, что для медленных видеокарт неприемлемо. (В
дальнейшем все, что будет говориться о DOOM’e, также будет относиться к
DOOM II и к HERETIC. Несовпадающие пункты будут оговорены отдельно).
В основе построения игры DOOM лежит система 3D ENGINE. Эта система
базируется на двумерном BSP (Binary' Space Partition). Не подумайте, что тут
опечатка. Именно двумерном, а не трехмерном. В DOOM’e плоская карта, и в
один и тот же момент времени два сектора не могут находиться друг под другом.
Поэтому' DOOM было бы справедливо назвать не трехмерной игрой, а
псевдотрехмерной, но псевдотрехмерность не бросается в глаза и игра выглядит
понастоящему трехмерной, если не догадываешься о принципах ее построения.
Многие критики ругают DOOM за чрезмерную жестокость и пропаганду
насилия. Я вынужден согласиться с этим. Действительно, в игре много крови и
ужаса. Подвешенные к потолку монстры, наколотые на колья, размазанные по
стенам - это чересчур.
Однако по технологии 3D ENGINE можно создавать не только кровавые
стрелялки, но и красивые безобидные увлекательные игры. Но это дело
сценаристов и художников.
Некоторые думают, что написание игр, подобных DOOM - дело сложное и
под силу лишь профессионалам. Так вот, целью данной книги является описание
принципов построения игр, подобных DOOM, и доказательство с пранедлявости
пословицы “не так страшен черт, как его малюют”. Мы докажем вам, что игра
DOOM не сложнее любой другой игры, а по некоторым параметрам даже проще
Самым сложным, пожалуй, является ядро - построение изображения в реальном
времени. А все остальное практически не отличается по сложности от других
игр.
К сожалению, объем книги не позволяет детально описать данную
технологию с формулами, иллюстрациями и графиками. Поэтому для
заинтересовавшихся можно рекомендовать приобрести ее исходники у фирмы
MICROART(cm. рекламу в конце книги). В данной книге будут описаны общие
принципы системы 3D ENGINE на уровне объектов, их взаимодействие,
форматы данных, структуры и др. Хотя здесь описываются алгоритмы и
использованы исходники, взятые из игры DOOM 1, тем не менее я
категорически не рекомендую вам ограничиться простой переделкой игры
DOOM. Используя материал данной книги и прибавив к нему комбинацию
графики и звукового сопровождения, можно написать весьма интересную игру.
Попытайтесь написать свой совершенно потрясающий сценарий. Я надеюсь,
что эта книга откроет вам путь к созданию собственных игр.
Для понимания большинства материала, представленого в этой книге, вы
должны хорошо владеть языком программирования Си. Также вы должны
неплохо ориентироваться в 386-м ассемблере (ассемблер защищенного
режима), т.к. в данной книге будут рассматриваться подпрограммы на
ассемблере и макросы, хотя основной акцент сделан на Си.
Встречающаяся лтература по программированию игр носит, как правило,
общий характер. При этом уделяется большое внимание программированию
периферийных устройств, со всевозможными математическими алгоритмами,
реально не использующихся в играх типа “DOOM”. Тем не менее, эти издания
дают общую подготовку; значительно облегчающее чтение данной книги.
В качестве такой обзорной книги я предлагаю, (особенно программистам с
недостаточным стажем) ознакомиться с работой А. Ла Мот “Секреты
программирования игр”
А теперь несколько слов о структуре книги.
Эта книга посвящена технике написания игр, подобных DOOM. В
построении книги заметна определенная система, направленная на
последовательное, логичное написание игры; начиная от выбора
инструментария и кончая созданием собственной графики.
В первой главе, “Выбор инструментария”, вы ознакомитесь с основными
принципами выбора того или иного языка программирования в зависимости
от сложности выполняемой задачи.
Во второй главе, “Структура игры”, описаны основные этапы разработки
игровой программы в общем виде, т.е. то, что характерно для любой игровой
программы.
Третья глава, “Инициализация”, является одной из самых больших в
данной книге. В ней шаг за шагом рассматриваются функции инициализации
основных устройств , участвующих в игре, заполнение виртуальных таблиц
и загрузка данных. Вы получите первый урок по работе с объектами и
некоторыми структурами данных.
В четвертой главе, “Основной цикл игры”, вас ожидает краткий курс
обучения для работы с многостраничным режимом VGA 320*200*256. Также
будет кратко рассмотрен цикл, в котором крутится вся игра (какие функции
входят в этот цикл); кроме того, вам предстоит краткий обзор двух переменных,
существенно влияющих на ход игры, т.е. фактически управляющих игрой.
Пятая глава, “Синхронизация по таймеру”, посвящена аспектам
быстродействия вашей программы. В ней описаны способы синхронизации
скорости программы по таймеру; чтобы скорость работы программы не
зависела от аппаратной платформы. Здесь я вам расскажу, что можно делать
(какие функции вызывать), пока программа ожидает, находясь в функции
синхронизации по таймеру, чтобы максимально загрузить центральный
процессор.
В шестой главе, “Контроль со стороны играющего” обсуждается построение
интерфейса между' игроками и такими устройствами, как клавиатура, джойстик
и мышь.
В седьмой главе, “Начало игры (загрузка данных)” описаны основные типы
данных в игре DOOM, объекты, типы объектов, их взаимодействие, а также
впервые будет обсуждаться технология двумерного BSP (технология отсечения
скрытых поверхностей), и ее реализация в игре DOOM.
В восьмой главе, “Основные виртуальные процессы в 3D играх”, будет
описана терминология виртуальных процессов и будут рассмотрены каждый
из этих процессов в отдельности.
В девятой главе, “Основные принципы работы с графикой”, описаны
основные функции по обработке виртуальных объектов и вывода их на экран с
использованием функций масштабирования и техники перспективного
затенения.
В десятой главе, “Создание собственной графики”, я поделюсь с вами
мыслями по поводу создания спрайтов и текстур без художников, собственными
силами с использованием бытовой видеокамеры,
В идиннадпатой главе, “Добро пожаловать в объемные игры”, вас ждет
нечто необычное. Вы узнаете, что такое “настоящий” объем и ознакомитесь с
принципами его получения “подручными средствами”.
В Приложении я познакомлю вас с техническими приемами повышения
производительности. Ваша игра будет работать быстрее и движения персонажей
будут более плавными, если вы последуете приведенным в приложении советам.
Глава 1
Выбор инструментария
Скорость работы 3-мерных игр зависит от многих факторов, основным из
которых является тип используемого процессора. Если для игры DOOM
написано, что для ее работы требуется процессор не ниже 386SX, то это не
означает, что в нее можно играть на компьютерах с использованием данного
процессора. Через некоторое время вам это надоест, и более того, сложится
ложное представление об игре, как о неинтересной и нудной. Практика
показывает, что для нормальной, быстрой работы данной программы
необходим процессор не хуже 486DX2-66 да еще и с быстрой видеокартой с
шиной VLB или PCI?
Немаловажную роль в скорости работы любой программы играет
оптимальный алгоритм, максимальное использование регистровых
переменных, отказ от вычислений с плавающей точкой, по возможности
сведение всех сложных вычислений в таблицу, использование совершенной
системы распределения памяти.
Конечно же, исходя из второго пункта, можно попытаться написать DOOM
на ассемблере. Однако, мало того, что это занятие не для слабонервных, к концу
написания может оказаться, что данная программа уже устарела года этак на
три, и вы напрасно потеряли столько драгоценного времени, получив разве что
моральное удовлетворение.
Третий пункт также может быть не совсем понятен. Каким это образом
такую сложную технологию можно реализовать без операций вычисления с
плавающей точкой? Однако здесь можно внести поправку. Отказ от плавающей
арифметики вовсе не предполагает использование целочисленной арифметики.
Имеется нечто среднее между ними - фиксированная точка. Число с
фиксированной точкой состоит из 32 бит (4-х байт). При этом младшие 16 бит
несут информацию о дробной части, следующие 15 бит - информацию о целой
части, а последний 32-й бит - информацию о знаке числа. Операции над числами
с фиксированной точкой (в дальнейшем буду называть фиксированными
числами) несущественно отличаются от целочисленной арифметики и не
предполагают наличие арифметического сопроцессора. Например, сложение
и вычитание фиксированных чисел не отличается от целых чисел, для
преобразования фиксированного числа в целое необходимо сдвинуть его
вправо на 16, для обратного преобразования - влево на 16, при умножении
фиксированных чисел одно число умножается на другое и получившийся 64-
битный результат сдвигается вправо на 16. Например, если одно число находится
в регистре ЕАХ, а второе в EDX, то их произведение будет выглядеть так:
imul edx
shrd eax,edx,16
Результат возвращается в регистр Е АХ. Все операции над фиксированными
числами сводятся в файл “fixed, h”.
Последний пункт также может наводить на размышления, тем более, что
под совершенной системой распределения памяти понимается использование
участков памяти больше 64К. Здесь подразумевается использование плоской,
несегментированной модели памяти, т е. использование защищенного режима.
Кроме того, защищенный режим предоставляет программисту огромный
простор для творчества, т.к. сам код программы, так же как и данные,
располагается за пределами первого мегабайта и не ограничен размером 640К
(это теоретически, а практически - гораздо меньше).
Исходя из последнего пункта, ничего не остается, как предложить вашем}'
вниманию мощный инструмент для программиста - язык программирования
WATCOM C/C++ V10.0. На нем создаются программы для различных
операционных систем (DOS, WINDOWS, WINDOWS NT, OS/2 и др.), но нас в
настоящий момент интересует только DOS, поэтому не пугайтесь, когда увидите
его дистрибутив из 61 дискеты, собираясь оплакивать свободное место на своем
винчестере. Установка Watcom для DOS’a займет меньше, чем BORLAND C++
V4.5, а именно около 28 Мбайт.
Кстати, о BORLAND’e. Для тех, кто до этого писал программы на нем, будет
крайне досадно узнать, что у Watcom’a нет нормальной среды разработки для
написания программ в DOS’e Однако я настоятельно рекомендую использовать
в качестве среды текстовый редактор Multi Edit v7.0. Он настолько подходит для
этой роли, что его можно вклинить в стандартную поставку Watcom, и проблема
со средой отпадет сама собой. Кроме того, в самом синтаксисе языка WATCOM
имеются незначительные отличия от BORLAND’а, но об этом поговорим чуть
позже. А сейчас несколько слов о настройке WATCOM’a, его основных утилитах
и возможностях.
WATCOM позволяет компилировать программы для работы в защищенном
режиме под DOS extender: DOS4GW. DOS extender - это расширитель DOS’a,
предназначенный для установки защищенного режима, обеспечения
интерфейса DPMI (DOS Protect Mode Interface), эмуляции прерываний
реального режима и обеспечения работы с виртуальной памятью. Поскольку
DOS4GW имеет свой собственный интерфейс DPMI, он не нуждается в 386-х
менеджерах памяти типа QEMM,EMM386,МАХ386 и др.; кроме того, без
последних он работает быстрее и эффективнее, поэтому тем, кто решил
посвятить свое свободное время написанию каких-либо программ на
WATCOM’е, придется отказаться от услуг QEMM’a и ему подобных и
использовать HIMEM, хотя последний не в состоянии предоставить в ваше
распоряжение 634К свободной conventional памяти. Однако здесь вам придется
привыкнуть к тому, что вам теоретически не нужна больше нижняя память.
Гораздо важнее объем памяти, “лежащий” за пределами первого мегабайта,
т.е. extended memory'. Как это ни странно, но для запуска программ, написанных
на WATCOM’e (независимо от размера самой программы), порой достаточно
меньше 200К нижней памяти. Однако это далеко не всегда так. То, что код и
данные программы не используют нижнюю память, совершенно не означает,
что последняя пустует. Это было бы расточительством - не использовать лишние
512К памяти, поэтому’ программисты чаще всего используют эту память для
своих нужд: например, буфера экранов, буфер DMA для оцифрованного звука,
всевозможные таблицы и др., т.е. как данные. Что касается вообще работы с
памятью, то, если вы работаете в однозадачном режиме, вам больше не нужны
сегменты. Вы о них как бы забыли, хотя теоретически они есть. В этом случае,
например при обращении к графическому экрану, который обычно имеет адрес
0ха000:0, вы будете использовать физический адрес экрана ОхаОООО. Как же,
спросите вы, пользоваться прерываниями, которые требуют в качестве
аргумента адрес: сегмент плюс смещение? Здесь имеются два варианта:
I. Использовать прерывание защищенного режима (int386). Для некоторых
прерываний в качестве адреса можно передавать физический адрес памяти без
сегмента (подробности смотрите в help’e).
2. Использовать эмуляцию прерываний реального режима с
использованием функции DPMI ЗООЬ. При этом все параметры передаются
гак, как будто вы работаете в реальном режиме и записываются в структуру
RMI (Real Mode Info), а адрес этой структуры передается в регистре EDI при
вызове прерывания DPMI (для тех, кто забыл, напомню, что номер DPMI
прерывания: 0x31).
Если мы уже заговорили о памяти, то немного слов о функциях
распределения памяти. Для использования памяти, находящейся выше
мегабайта, служат функции:
malloc(int size) - для выделения памяти необходимого размера;
free(void *point) - для освобождения памяти. Для использования памяти
ниже мегабайта используется функция DPMI 100Н.
А теперь рассмотрим наиболее существенные отличия WATCOM C/C++
от BORLAND C++.
Во-первых, тип данных - Int в WATCOM’e для 32-битного кода имеет размер
32 бита. В общем, это не имеет значения, если только вы не используете какие-
либо внешние структуры, в которых важны номера смещений.
Во-вторых, в WATCOM’e, в отличие от BORLAND’a, очень сложный
встроенный ассемблер. Наиболее простой выход здесь - написать часто
используемые макросы на ассемблере, используя директиву компилятора
“pragma aux” и использовать этот макрос как функцию. Например, для быстрой
пересылки в памяти макрос “move” может выглядеть так:
//pragma aux move parm [edi] [esi] [ecx] = \
“push edi” \
“moveax,ecx”\
“shrecx,2”\
“rep movsd” \
“movcl,al”\
“andcl,3”\
“rep movsb” \
“pop edi” \
modify [eax esi ecx];
При использовании этого макроса вы можете просто написать:
move(dst,src,len); что означает: переслать данные из src по адресу dst длиной
1еп. Конечно, для этой же цели можно воспользоваться функцией
memcpy(dst,src,Ien). но это будет несколько дольше, кроме того, не следует
забывать, что move используется именно как макрос, т.е. компилятор просто
подставляет его код без использования call, что в раде случаев существенно. Я
рекомендую написать макросы и собрать их в один файл, например, “inline.h”.
В-третьих, в WATCOM’e есть некоторые операторы, которые имеются и в
BORLAND’е, но ключевые слова и синтаксис не совпадают. В этом случае
необходимо заглянуть в HELP или примеры.
А теперь несколько слов о подготовке компилятора к работе. Во-первых,
необходимо установить пути и создать переменные окружения. Хотя
инсталлятор делает это автоматически, я считаю, что знать об этом необходимо.
Если WATCOM установлен у вас на диске С: в директории “WATCOM”, то в
autoexec.bat у вас должно быть:
PATH c:\watcom\bin;c:\watcom\binb;...
SET include=c:\watcom\h
SET watcom=c:\watcom
После этого, если вы выбрали для себя в качестве среды редактор Multi Edit,
то прежде всего настройте в нем клавиши так, как вы к этому привыкли.
Например, если до этого вы работали в BORLAND’е, то для компиляции можно
использовать клавишу F9, Zoom - F5 и т.д., но это дело вкуса. Для настройки
клавиш в ME войдите в основное меню “Other” и там выберите пункт “Installa-
tion and setup”, где войдите в “Key mapping”. Для компиляции в WATCOM
используются программы wcc.exe, wcc386.exe, wpp.exe, wpp386.exe и wasm.exe.
Первые две предназначены для компиляции “C’’-программ, вторые - “СРР”-
программ, а последняя - ассемблер. Однако максимальные удобства
предоставляют утилиты wcl.exe и wcl386.exe, которые позволяют компилировать
и линковать любые программы, будь то С, СРР или ASM, причем делают это
автоматически. Следовательно, можно использовать только утилиту vvcI386.exe.
Но на данном этапе вам необходимо определиться, сколько файлов у вас
будет в проекте. Если количество файлов превышает 5, то линкер лучше
запускать отдельно, после компиляции.
При вызове компиляции в Multi Edit создайте, используя клавишу “In-
sert”, три новых пункта:
1. с именем “Watcom Compile” и командной строкой.
wcI386 /с Zs /Зг /d 1 /otiamlm <FILE>.<EXT>.
Этот пункт используется для компиляции программы.
<FILE>.<EXT> говорит Multi Edify о том, что должен использоваться
файл активного окна.
/с - только компиляция, без линкования;
/s - отключение проверки переполнения стека (для скорости);
/Зг - передача параметров в функции с использованием 386-х регистров;
Zdl - использовать отладочную информацию;
/о - после нее идут команды оптимизации. Вышеперечисленные команды
являются наиболее оптимальными для скорости выполнения программы, хотя
вам никто не мешает поэкспериментировать;
2. с именем “Watcom Link” и командной строкой:
wlink @link.
Этот пункт используется для линкования без использования отладочной
информации. Второй параметр - это указатель на файл linklnk (заметьте,
что расширение Ink указывать необязательно) - файл директив линковщика.
Файл link. Ink может иметь такой вид:
Option stack=OxefUO
System dos4g
Name example
Первой строкой стоит указатель на файл objfiles.Ink, в котором вы через
запятую указываете имена obj файлов, входящих в проект. Например, если ваш
проект состоит из трех файлов: ship.obj, shipasm.obj и memory.obj, то файл
objfiles.Ink будет выглядеть так:
File ship,shipasm,memory
Порядок расстановки файлов значения не имеет.
3. с именем “Watcom Link Debug” и командной строкой
wlink @linkd.
Этот пункт используется для линкования с использованием отладочной
информации. Отличается от второго пункта только наличием в файле
linkd.lnk строки “Debug line,static”:
@objfiles
Debug line,static
dosseg
Option stack=0xefD0
System dos4g
Name example
При создании каждого из этих пунктов желательно в опциях установить
флажок (обозначается [х]) “Save all files”, что означает запись всех файлов
перед началом компиляции. Это необходимо, чтобы оградить вас от
случайностей, связанных с компиляцией ваших программ, результатом которых
может стать зависание компьютера, и, следовательно, потеря последних
исправленных текстов исходников.
Теперь несколько слов об отладке программ. Для этих целей служит
программа vvd.exe (Watcom Debugger). Этой программой можно отлаживать
программы, написанные под расширитель DOS’a DOS4GW. При этом
удобно создать такой ВАТ файл (например с именем vv.bat):
vvd /tr=rsi /swap %1 %2 %3 %4 %5 %6 %7 %8
/tr=rsi - отладка Rational Systems программы, т.е. написанной под
расширитель DOS4GW.
/swap - отладка программы, работающей в графическом режиме.
%1 - имя программы, а остальные параметры - возможные аргументы.
Используя файл wd.dbg, можно установить все клавиши в отладчике по
аналогии с BORLAND’obckhm Turbo Debugger’oM и изменить цвета.
DOS Extender DOS4GW и
виртуальная память
Настало время подробней остановиться на DOS расширителе DOS4GW
и его возможностях. В стандартной поставке WATCOM’a дается DOS4GW
версии 1.95, однако имеется также версия 1.97, которая незначительно отличается
от первой в основном по работе с виртуальной памятью, а также имеется
DOS4GW Professional (gwpro.exe), которая выгодно отличается от первых двух
возможностью “пристегивания” к программе, для работы которой требуется
наличие DOS extender’a. Разумеется, что при этом не требуется наличие
внешнего расширителя, что придает программе более законченный вид Вместе
с программой 4gwpro.exe поставляется программа 4gwbind.exe, которая
собственно и выполняет операцию присоединения (а также отсоединения).
В расширителях DOS4GW имеется встроенный менеджер виртуальной
памяти. Для его использования в расширителе 4gwpro достаточно при
подключении указать опцию -f, что означает постоянное использование
виртуальной памяти. В качестве примера можно привести игру Descent. Для
использования виртуальной памяти с DOS4GW можно создать такой ВАТ файл
(например, с именем runvirt.bat):
set DOS4GVM = @new4g.vmc
prog.exe
где prog.exe - имя запускаемой программы. Файл “new4g.vmc” может
выглядеть так:
MINMEM = 512
МАХМЕМ = 4096
VIRTUALSIZE = 16384
DELETESWAP
MINMEM и МАХМЕМ - минимальный и максимальный размеры
оперативной памяти для менеджмента.
VIRTUALSIZE - размер swap файла вместе с оперативной памятью.
DELETESWAP - указывает на то, что swap файл при выходе из
программы должен быть удален
Для установки всех параметров виртуальной памяти по умолчанию
достаточно в файле runvirt.bat написать всего одну строку:
set DOS4GVM = 1.
Глава 2
Структура игры
2.1. Методология создания видеоигр.
Компьютерная игра, как н любой другой программный продукт, должна
создаваться по определенной методике. Вот основные фазы создания
видеоигр:
1 .Нужна идея, или сценарий. Если игра будет развиваться на нескольких,
непохожих друг на друга, уровнях, то надо описать каждый из них. В
сценарий игры необходимо внести необычные или неожиданные
моменты, встреча с которыми, как правило, происходит в конце уровня.
Необходимо заинтересовать игрока, чтобы у него появился азарт,
заставляющий его проходить уровень за уровнем вашу игру. От удачного
сценария зависит 50% успеха всей игры.
2 .Необходимо разработать структуру игры. Определитесь, какую
технологию вы будете использовать. Если у вас уже имеются наработки
по некоторым технологиям (исходники), то лучше писать игру с
использованием этих наработок. Поверьте мне, писать сложную игру с
“нуля” - дело непростое. Да и времени это занимает столько, что вашего
энтузиазма до конца написания игры может не хватить. Так что
хорошенько подумайте над этим пунктом.
После этого заострите свое внимание на специфике игры:
3 .Выберите, в каком видеорежнме у вас будет работать игра.
Большинство игр в настоящее время работают в стандартном разрешении
VGA 320*200*256, т.к. это разрешение обеспечивает самый быстрый
вывод. Однако, если вы ориентируетесь на более мощные компьютеры
(Pentium системы), то можете использовать более высокие разрешения
(VESA режимы 640*400*256,640*480*256,800*600*256 и др.) В данном
случае я не советую использовать количество цветов больше 256, т.к это
резко затормозит игру и, кроме того, наложит значительные ограничения
на объем оперативной памяти.
4 . Подумайте, насколько сложной будет графика, продумайте алгоритмы
детализации, масштабирования и поворотов, если это необходимо.
Определите тип используемой графики (bitmap, векторная или их
комбинации). Уже на данном этапе необходимо оценить
производительность игры и разработать оптимальные (быстрые)
алгоритмы работы с графикой.
5 .Создание виртуального мира. (Взаимодействие виртуальных объектов
с окружающей средой).
В дальнейшем вам могут понадобиться инструментальные средства. Вот
их минимальный набор:
1 .Программа для рисования битовых образов;
2 . Программа для анимации битовых образов;
3 .Исходные тексты на Си или Ассемблере для быстрого рисования
графических примитивов, перемещения битовых образов (бит-блиттинг),
масштабирования;
4 .Алгоритмы “искусственного интеллекта” для персонажей игры;
5 .Средства для работы со звуком;
б .Си-код для работы с устройствами ввода;
7 .Инструменты для рисования уровней и сохранения их на диске;
8 .Наборы звуковых файлов для музыки и звуковых эффектов (обычно
для музыки - в формате MIDI, для эффектов - в формате WAV);
2.2. Как разбить игру на секции.
Все 3D игры для программистов являются чем-то сложным и
непостижимым. Но, несмотря на кажущуюся сложность, нх внутренняя
структура по сравнению с друтими играми не претерпела серьезных изменений.
Не вдаваясь в подробности, в данной структуре можно выделить всего семь
пунктов, но нс надо забывать, что каждый нз этих семи пунктов весьма сложен
и разнообразен, настолько сложен, что одному ему можно было бы посвятить
целую книгу; а разнообразен постольку; поскольку зависит от сюжета и жанра
игровой программы. Все этн семь пунктов, связанные в одно целое,
представляют собой законченную игру. Каждый из них выполняет вполне
самостоятельные функции, но тем не менее все они тесно связаны между собой
какими-то переменными, структурами и функциями и один без другого
су шествовать не могут. Итак, вот эти семь пункгов:
• Инициализация игры
• Основной цикл игры.
• Начало игры (Загрузка данных).
• Контроль со стороны играющего (играющих).
• Оценка игровой ситуации.
• Работа с графикой.
• Звуковое сопровождение.
А теперь подробнее остановимся на каждом из этих пунктов и в качестве
примера будем приводить технический принцип реализации той или иной
задачи в игре DOOM. Как вы сами понимаете, объем книги не позволит во всех
деталях описать работу сложной программы. Но нашей задачей является научить
вас писать (или хотя бы понимать) подобные, на первый взгляд кажущиеся
сложными, программы. А что касается всевозможных “наворотов”, то они как
раз и занимают большую часть объема игры и необходимы для ее разнообразия,
чтобы играющему не было скучно, но для нас они - задача вторичная и,
разумеется, все их перечислять мы не будем, но основные, в качестве примера,
все же приведем. Итак, за работу...
Глава 3
Инициализация
Инициализация - это процесс подготовки игры к работе. Для
большинства игр этот пункт можно было бы опустить, т.к. либо она в них
вообще отсутствует, либо по объему и значимости настолько мала, что о
ней и не стоит говорить.
Что касается больших, серьезных программ, то в них инициализация имеет
особо важное значение, и от того, насколько эффективно она построена, в той
или иной мере зависит скорость и эффективность работы самой игры. По
времени инициализация может занимать от долей секунд до нескольких минут.
С визуальной точки зрения инициализация может проходить либо в текстовом,
либо в графическом режиме на фоне какой-либо заставки. Очень часто
встречается, когда начальная стадия инициализации проходит в текстовом
режиме, а затем осуществляется переход в графический режим. В DOOM’e вся
инициализация осуществляется в текстовом режиме, но в настоящее время
большинство производителей компьютерных игр предпочитают использовать
графический режим, т.к. возможности визуального восприятия и разнообразия
у последнего несомненно выше.
А теперь на примере игры DOOM рассмотрим, в какой последовательности
может быть построена инициализация, и более или менее подробно попытаемся
разобраться в каждом их этих пунктов. Тут следует подчеркнуть - все, о чем
будет говориться ниже, нельзя принимать за эталон. В каждой программе
инициализация устроена по-разному. Она зависит от принципов технической
реализации самой программы, а не только от ее жанра. Итак...
3.1. Распределение памяти для экранов
(V_AllocMem() )
В игре DOOM используется многостраничный режим VGA 320x200x256.
Экран с таким разрешением занимает 320*200=64000 байт в памяти. В своей
работе DOOM использует для различных целей четыре таких буфера. Не
подумайте, что каждый из этих бу феров соответствует одной из четырех страниц
VGA. К четырехстраничному режиму' VGA эти четыре буфера не имеют
никакого отношения. А роль каждого из них мы обсудим ниже при описании
принципов и особенностей работы с графикой. Функция V AllocMemO выделяет
256000 байт в нижней памяти и может выглядеть так:
void VAllocMem( void)
{
char *adscreens = alloc_lo\\(64000*4);
inti;
for (i=0; i<4; i++ )
{
AdrScreens| i | = adscreens;
adscreens+= 64000;
? }
AdrScrecns - это массив из четырех указателей на буфера экранов.
3.2. Загрузка стандартной конфигурации
( LoadDefaults() ).
В файле конфигурации содержатся жизненно важные параметры для
работы программы, такие как номера звуковых Devic’oB и их данные. Есть
параметры, которые просто облегчают вам жизнь, такие как установленные
коды клавиш управления, размер экрана, громкость звука и множество
других параметров, которые на работоспособность программы не влияют,
но придают работе с игрой приятный комфорт. Файл конфигурации можно
изменить двумя способами:
- запустить программу “setup.exe” и установить необходимые параметры
(после чего не забыв записаться).
- самому в текстовом редакторе изменить в файле “default.cfg”
интересующие данные, но в данном случае вы должны знать (или хотя бы
догадываться), что вы делаете. В противном случае, нет гарантии того, что игра
после данного вмешательства будет работать корректно.
При выходе из программы конфигурация автоматически записывается.
Файл конфигурации представляет собой текстовый скрипт файл, в левой
части которого содержится название параметра, а в правой части - его значение
Для чтения из файла имени и значения параметра используется команда:
fscanf (file, “%79s %[Л\п]\п”, str_nam, strarg);
Эта команда в нормальном случае возвращает число 2, т.е. два параметра,
первый из которых (название параметра) находится в str nam, а второй (значение
параметра) - в str arg. Структура, с которой работают функции LoadDefaultsQ
и SaveDefaults() имеет вид:
struct config
{
char *name; // название параметра (например, sfx_volume)
int * val; И адрес переменной, куда поместить значение параметра
int default; // значение параметра по умолчанию
int type; // тип параметра
} setupfMAXI.INES];
Третий элемент структуры - default необходим для того случая, когда
данная строка отсутствует в файле конфигурации, либо отсутствует сам
файл. В этом случае все переменные принимают значения по умолчанию.
Надо только не забыть в начале функции LoadDefaultsQ изобразить
конструкцию вида:
for (1=0; i<MAXLINES; i++ )
setup[i].val[O] = setup(i).default;
}
Последний элемент структуры - type в конкретной реализации может
принимать различные значения. Ведь параметры в конфигурации могут быть
разных типов. Это может быть целое число, строка, число с плавающей точкой
и др. В DOOM’e этот элемент принимает значения 0 - для целых чисел и 1 - для
кодов клавиш. Дело в том, что в нем используется свой низкоуровневый
обработчик кодов клавиатуры и ASCII коды некоторых клавиш не соответствуют
его внутренним кодам. Поэтому те коды, которые заданы в конфигурации,
табличным методом преобразуются во внутренние коды программы, а при
записи конфигурации осуществляется обратное преобразование. В заключении
хотелось бы отметить, что в DOOM’e можно назначить имя для альтернативного
файла конфигурации, используя ключ “-config”. Например:
doom.exe -config other, cfg
3.3. Инициализация зоны памяти
(Zonelnit())
Настало время подробнее остановиться на организации памяти в игре
DOOM и преимуществах данного подхода. В процессе работы DOOM использует
большое количество небольших участков памяти для виртуальных объектов,
графических данных, звука и др. Если бы для выделения памяти при этом
использовалась функция malloc, то это сильно тормозило бы работу'. Но это
еще не все... Суть нового подхода заключается в том, что функцией malloc
выделяется вся имеющаяся в распоряжении DPMI память, которая называется
зоной, и в пределах этой зоны работают свои собственные функции
распределения памяти, которые в силу своей специфичности работают
несомненно быстрее и эффективнее, чем универсальные, аналогичные функции
типа malloc или free. Вызов функции выделения памяти внутри зоны zmalloc
выглядит так:
void *zmalloc(int len, void **adr_cache, int tag)
Здесь мы видим, что, кроме параметра 1еп (размер выделяемого участка),
передаются еще два параметра, отсутствующие в функции malloc.
adr cache - указатель на данные внутри таблицы кэширования (На
кэшировании мы остановимся чуть ниже, где будем обсуждать структуру файла
ресурсов). Если д анные, для которых вьщеляется участок памяти в кэшировании
не нуждаются, то данный параметр должен быть нулем.
tag - номер категории данных. Этот параметр определяет группу
данных, для которых вы выделяете участки памяти. Пользоваться этим очень
удобно. Например, если вы для временных целей выделяете много раз
участки памяти с номером 1:
Adr[i] = zmalloc(len, NULL, 1);
то после того, как данная память вам больше не нужна, вам необязательно
столько же раз вызывать функцию zfree, сколько раз вы вызывали zmalloc. В
этом случае можно воспользоваться функцией
FreeTags(int mintag, int maxtag),
где mintag и maxtag акиветствснно минимальный и максимальный номера
категории данных для освобождаемых участков. В нашем конкретном случае
это будет выглядеть так:
FreeTags(l,l);
Если значение tag превышает 100, то это говорит о том, что данный участок
памяти является кэшируемым, т.е. непостоянным, и программа zmalloc может
использовать этот участок для других данных, если для них не найдется
свободного места внутри зоны. В случае, когда tag больше 100, наличие второго
параметра при вызове функции zmalloc обязательно.
Теперь разберемся, что делает функция Zonelnit().
Во-первых, осуществляется инициализация DPMI памяти:
struct memhead *DPMI_MemoryInit (register int *memsiz)
{
struct memhead ♦ buffer,
unsigned memi[12J;
union REGS regs;
struct SREGS sregs;
stos( &sregs, sizeof(sregs));
sregs.es = FP_SEG( &memi);
regs.x.edi = FP_OFF( &memi);
regs.x.eax=0x500;
int386x( 0x31, ®s, ®s, &sregs);
printff “ DPMI memory present: %i bytes. “, *memi);
do
{
*memi -=0x10000;
if ( *memi 0x800000 )
*memi = 0x800000;
buffer = malloc(*memi);
} while ( ’buffer );
printf (“(%i bytes allocated for zone).\n”, *memi);
if( *memi< 3000000)
ExitWithMsg (“Insufficient DPMI memory!”);
*memsiz = *memi;
return buffer;
}
Здесь мы видим, что для получения информации о свободной памяти
используется функция DPMI 500h. При выделении памяти для зоны
осуществляется ограничение сверху на размер зоны в пределах около 8 Мбайт.
Это сделано, во-первых, потому, что больше восьми мегабайт памяти не
требуется, а во-вторых, если у вас памяти около 4 Мбайт и вы используете
работу с виртуальной памятью, то на протяжении всей игры вынуждены будете
наблюдать негаснущий светодиод винчестера н бесконечные Зиар’инги.
Если свободной памяти меньше 3 Мбайт, то производится выход из
программы с критическим сообщением: “Insufficient DPMI memoiyl”. Функция
DPMIMemoiylnit возвращает начальный адрес зоны, как указатель на
структуру memhead:
struct memhead
{
int siz; // размер зоны
struct memblk
{
int len; // длина участка
int zan; // флаг занятости участка или указатель на адрес кэша
int tag; // номер категории данных
int flag; // идентификатор зоны: ZONEID
struct memblk *memnext; И указатель на следующий участок
struct memblk *memprev; // указатель на предыдущий участок
} memfirst;
struct memblk *memend; // указатель на последний занятый участок
struct memblk memcur; И header первого занятого блока
};
Идентификатор зоныZONEID в DOOM’e имеет всегда значение Ох 1 d4a 11 и
предназначен для идентификации участков памяти при работе функций
распределения памяти внутри зоны. Второе, что делает функция ZonelnitQ -
заполнение структуры memhead начальными данными. Сама функция ZonelnitQ
выглядит так:
void Zonelnit (void)
{
int mernlen;
struct memhead *mm;
mm = DPMI_MemoryInit(&memlen);
AdrZoneMem = mm;
mm-siz = memlen;
mm-memfirst.zan = 1;
mm-memfirst.blks = 1;
mm-memfirst.memnext = mm-memfirst.memprev = mm-memend =&mm-memcur,
mm-memcur.len = memlen - 32;
mm-memcur.zan = 0;
AdrZoneMem-memcur.memnext = mm-memcur.memprev = &mm-memfirst;
}
AdrZoneMem - начальный адрес зоны.
3.4. Инициализация файлов ресурсов
(InitWadFilesO ).
Для начала я хочу пояснить все преимущества использования одного
файла ресурсов перед использованием отдельных файлов вообще н для
DOOM’a в частности.
Во-первых, экономия места на диске. Известно, что файлы на винчестере
всегда занимают ровное количество кластеров. Поэтому, если размер кластера
у вас на винчестере 16К и у вас записано 10 файлов по 1 К, то у вас зря пропадает
10*15К=150К (!) полезного пространства. Таким образом, чем больше размер
кластера и чем больше на винчестере мелких файлов, тем неэкономней
расходуется место на вашем винчестере. Во-вторых, скорость доступа. Файлы
на винчестере всегда расположены в произвольном порядке, при этом в ряде
случаев файлы, которые вы чаще всего используете, расположены не в соседних
кластерах, а порой даже в разных концах диска. При обращении к файлу'
винчестер выполняет операцию позиционирования (SEEK) на файл, а эта
операция, как известно, одна из самых медленных и зависит от типа винчестера.
Если вы оптимально спроектировали файл ресурсов таким образом, что часто
используемые данные (например, текстуры) собраны в одну группу, то скорость
доступа можно свести теоретически к нулю (!), учитывая тот факт, что конец
одного файла и начало следующего находятся в одном кластере. Желательно,
конечно, чтобы файл ресурсов не был фрагментирован.
Что касается DOOM’a, то здесь можно назвать еще одно, пожалуй, самое
важное преимущество файла ресурсов - возможность кэширования
графических данных (текстур, спрайтов н объектов). Дело в том, что графика в
любой игровой программе занимает львиную долю памяти. При этом, если
программа рассчитана для работы всего с четырьмя мегабайтами, то с
распределением памяти мотуг возникнуть серьезные проблемы. Поэтому, дабы
не наблюдать иа мониторе нескончаемые ошибки, в переводе примерно
означающие: “что-то с памятью моей стало”, разработчики фирмы id
предложили очевидный путь решения данной проблемы: кэширование. В
нескольких словах принцип кэширования можно объяснить так: в памяти
находятся только часто используемые данные. Если при загрузке
следующих данных оказывается, что их в памяти нет, программа выделяет
для них память, куда, собственно, данные и грузятся. Если при выделении
памяти для новых данных оказывается, что памяти недостаточно, то
освобождаются участки памяти, которые давно не использовались, до тех пор,
пока не образуется “дырка” нужного размера (где можно разместить новые
данные). Конкретизируя сказанное, мне остается добавить, что для кэширования
важны не имена файлов, а их порядковые номера в каталоге, что, собственно,
файл ресурсов вам и предоставляет. Автоматически распределением памяти
для кэшируемых данных занимается функция zmalloc(), при этом третий
параметр - tag должен быть больше 100.
А теперь рассмотрим структуру файла ресурсов, который в DOOM’e имеет
расширение “WAD”. Вся его структура состоит из двух частей:
1 .Заголовок файла, или Header.
2 . Ката лог файлов. Здесь я оговорюсь: чтобы в дальнейшем не возникало
путаницы с терминами, файлы внутри файла ресурсов я условно буду называть
блоками. Заголовок файла ресурсов представлен структурой:
typedef struct
char id[4J;
int +num_lumps;
int seek;
} WadHead;
- id[4] - сигнатура, состоящая из четырех символов ASCII. Сигнатура может
иметь два значения: IWAD - для оригинального, коммерческого файла WAD;
PWAD - для некоммерческого пользования, т.е. данный файл был создан
сторонними лицами.
- num lumps - количество блоков в файле ресурсов;
- seek - местоположение каталога в файле ресурсов.
Один блок представлен структурой:
ty pedef struct
{
int seek;
int size;
charname[8],
} Lumpinfo;
- size - размер блока;
- name - имя блока, которое состоит из восьми заглавных букв.
Инициализация файла ресурсов представляет собой загрузку' каталога файла
WAD в буфер AdrLumps и выделение памяти для таблицы кэширования
AdrCache:
AdrCache = (void “)malloc(num_lumps * sizeof( void ♦•)).
Чтение блоков представлено двумя функциями:
1 .ReadLumpForName(char *name,int tag) - чтение блока с именем name;
2 .ReadLumpForNumber(int num,int tag) - чтение блока с номером num.
И та и другая функции имеют функцию кэширования. Функция
ReadLumpForNumber имеет вид:
void *ReadLumpForNumber(int num, int tag)
{
void ‘adres;
register struct memblk *cur,
if«( num = num lumps) // номер загружаемого блока превышает
//общее количество' блоков
ExitWithMsg (“W_CacheLumpNum: %i = NumLumps”, num);
if (!AdrCache[num]) 11 кэш для данного блока пустой,
{ И осуществляется загрузка.
adres = zmalloc( lumplen(num), &AdrCache[num], tag); память для блока
ReadLump (AdrCache[num], num); II загрузить блок
}
else
{ И данный блок уже находится в памяти
cur = (struct memblk *XAdrCache[nutn]) - 1;
if (cur-flag != ZONEID) 11 проверка наличия индентификатора зоны
ExitWithMsg(“Z_CT at wwad.c: %i”, 302);
ChangeTag(AdrCache[num], tag); //произвести изменение номера категории
// данных для данного блока в соответствии с новым номером
}
return AdrCache[numJ;
}
Функция ReadLumpForName имеет вид:
void ‘ReadLumpForName (char ‘name, int tag)
{
return ReadLumpForNumber(getnum(name), tag);
}
Функция getnum(char ‘name) возвращает порядковый номер блока в каталоге
файла ресурсов для имени name. Подводя итоги, хотелось бы подчеркнуть, что
гораздо выгоднее использовать функцию чтения блока по номеру, чем по имени,
т.к в последнем случае осуществляется поиск блока в каталоге, а если блоков в
каталоге около тысячи и искомый блок далеко не первый, то данный процесс
может затянуться надолго. Именно поэтому для блоков, к которым
предъявляются жесткие требования по скорости доступа, необходимо
предварительно составить таблиц}- порядковых номеров в файле ресурсов.
3.5. Инициализация графических
данных. ( Refreshlnit() )
Инициализация графических данных осуществляется в несколько этапов.
По быстродействию данный процесс самый медленный, т.к. почти все
графические объекты загружаются с диска. А теперь обо всем по порядку.
3.5.1 Инициализация текстур
Текстура - основной вид графических данных в любой 3D игре. Это те
узоры и орнаменты, которые накладываются на полы, потолки н стены От
количества, разнообразия и реалистичности текстур в большой степени
зависит и интерес к игре.
Так как обращение к текстурам, как к графическим данным, производится
наиболее часто, то от эффективности и правильно построенного алгоритма их
инициализации зависит вся дальнейшая скорость и эффективность работы
самой 3D игры. Текстуры в DOOM’e кэшируются. Это подразумевает, что на
протяжении всей работы программы они могут по мере необходимости
подгружаться с диска (особенно, когда мало памяти). В связи с данным
обстоятельством д ля увеличения быстродействия на диске текстуры содержатся
в распакованном формате. Кроме того, данный формат несколько специфичен,
что связано с особенностями чстырехстраничного режима VGA и также
необходимо для максимальной скорости вывода на экран. Тот же специальный
формат характерен для спрайтов (монстры, предметы и др.) различных
изображений, меню, статус бара н других графических данных, коих в файле
ресурсов большинство. Несущественные отличия имеются в формате текстур
для полов и потолков. Подробно о графическом формате для различных
объектов смотрите в главе 10, а сейчас мы рассмотрим принципы подготовки
текстур к использованию отдельно для стен (WallTextures) и полов-потолков
(FlatTextures).
3.5.1.1. Инициализация текстур для стен
(WallTextures)
Перед тем как переходить к описанию инициализации, необходимо
пояснить принцип описания стенных структур в DOOM’e.
Список этих текстур находится в блоках TEXTURE 1 и TEXTURE2 файла
ресурсов. В них содержатся имена стенных текстур для различных секций
объекта S1DEDEFS (описание объектов для определения уровней смотрите в
главе 7), используемых при редактировании уровня. TEXTURE2 содержит
информацию о текстурах только для коммерческой версии игры. Первые четыре
байта в ресурсах TEXTURE - количество описателей текстур N (число типа int).
Далее следует таблица из N указателей на структуры текстур (на описатели).
TextNam е W Id th Н Ie д h t TextP ath О ffs X О ffs Y
A A S TIN К ¥ 2 4 72 WALL00 3 0 0
W A L L 0 0 _ 3 I 2 -6
В IG D О О R I 128 96 W 131 0 0
W I 3 _ I 0 2 4
D О О R 2 _ 1 I 7 0
W 1 3 _ 1 I I 3 0
W I 3 _ I 1 1 3 2 5 *
Указатель представляет собой смещение относительно начала блока TEXTURE
на начало описателя текстуры. Сразу за таблицей смещений расположены
собственно сами описатели текстур, которые можно изобразить так:
struct texture
{
char name[8]; // имя текстуры, используемой в SIDEDEFS, например
// “STARTAN3”
int NoUsedl; И всегда О
short Totalwidth; // общая ширина текстуры
short TotalHeight; // общая высота текстуры
int NoUsed2; И всегда О
short NumTexDesc; // количество описаний (struct txmor)
struct txmor
{
short Xoffset; // начальная координата по X относительно
//начала текстуры
short Yoffset; И начальная координата по Y относительно
//начала текстуры
short NumPnam, И номер картинки в блоке PNAMES
int NoUsed;
} more[MAXMORETEX];
};
Поля TotalWidth и TotalHeight определяют общее пространство,
занимаемое полной текстурой (обычно 128 на 128 или 64 на 72 и т.д.), которое
индивидуально для конкретной текстуры. Каждая текстура строится из
отдельных картинок (как мозаика), количество которых может достигать 64. При
этом достигается максимальная гибкость н экономия, т.к., имея в своем
распоряжении всего около десятка соответствующих картинок, можно,
комбинируя их, “слепить” любое количество (в разумных пределах) текстур.
При этом не следует забывать, что полная ширина текстуры не должна
превышать 256, а высота -128. Суммарный размер всех картинок, из которых
составляется одна текстура, не должен превышать 64 кбайта.
Поле NumTexDesc определяет количество картинок, из которых
составляется текстура. Поэтому количество структур txmor в одном
описании - величина переменная и в зависимости от конкретной текстуры
колеблется от 1 до 64.
В блоке PNAMES содержатся имена картинок, из которых
складываются текстуры:
struct pnames
{
int numPnam; // общее количество картинок в блоке PNAMES
struct nampic
{
char nam[8]; // имя картинки
}nam[NUMPICS];
};
Графические данные картинок, имена которых содержатся в блоке PNAMES,
находятся в файле ресурсов между входами PSTART и P END. Именно поэтому
при создании собственного файла ресурсов PWAD вы не должны своим данным
давать имена P START и P END. Подводя итоги и конкретизируя все сказанное,
хотелось бы привести для лучшего понимания пример скрипт файла, который
в текстовом виде соответствует описателю структур в блоках TEXTURE 1 и
TEXTURE2:
Здесь в качестве примера приведены описания двух текстур AASTINKY
и BIGDOOR1, первая из которых состоит из двух картинок, а вторая - из
пяти (см. рис.).
W13 1
- Width и Height - соответственно полная ширина и высота текстуры;
- TexPath - имя картинки;
- OffsX и OffsY - координаты картинки соответственно по X и Y внутри
текстуры.
Отрицательная координата означает, что часть картинки будет отрезана.
Например, если координата по Y равна -6, то это означает, что первые шесть
строк картинки для текстуры будут потеряны.
А теперь, когда мы разобрались, как строятся стенные текстуры, можно
приступить к описанию системы инициализации Весь алгоритм инициализации
- это заполнение таблиц и структур. Для простоты изложения рассмотрим
каждую из этих таблиц отдельно и расскажем, зачем она нужна. Но
предварительно определим переменную N как общее количество текстур,
которое является суммой количества текстур в блоках TEXTURE 1 н TEXTURE2
(как говорилось выше, количество текстур в блоке TEXTURE определяют первые
четыре байга, как величина int).
struct Textureslnfo - определяется как:
struct text_info
{
struct txdefFirst
{
char name[8]; // имя текстуры в SIDEDEFS
short width; // ширина текстуры
short height; // высота текстуры
short numMore; // количество структур txdefMore
} first;
struct txdefMore
{int Xoflset; 11 координата по X
int Yoffset; 11 координата no Y
int NumTexture; // номер текстуры в файле ресурсов
}more[MAXMORETEX];
} TexturesInfofN];
Эта структура почти аналогична той, что рассматривалась нами, как
описание текстур в блоках TEXTURE, но является внутренней структурой
в DOOM’e. В процессе инициализации в нее переписываются соответствующие
параметры из структуры texture. Единственным новым параметром является
NumTexture в структуре txdefMore”, которая определяет порядковый номер
текстуры в каталоге файла ресурсов.
А сейчас, как мы обещали, рассмотрим таблицы, используемые для стенных
текстур (напомним, что N - это общее количество текстур).
NumTexturesLump - двумерный массив на тип short.
Память для него выделяется:
NumTexturesLump=(short**)zmalloc(N*sizeof(short**), NULL, 1 );
NumTexturesLump - массив из N указателей на тип short. Один из таких
указателей указывает на номера картинок (которые мы рассматривали в
PNAMES) в файле ресурсов в зависимости от координаты X внутри текстуры,
т.е. в зависимости от номера столбца тексту ры. Эта таблица напрямую связана
с методом вывода текстуры на экран: развертка не по горизонтали, а по
вертикали. Для текстуры п можно записать:
NumTexturesLump[n]=zmalloc(TexturesInfo[n].first.width *sizeof(short),NULL,l),
Если окажется, что один и тот же столбец принадлежит больше чем
одной текстуре, то в таблицу NumTexturesLump по адресу данного столбца
записывается -1. Например, если в текстуре B1GDOOR1 (смотрите выше)
картинки W131 начинаются с нулевой координаты по X (смещение по Y
значения не имеет), то в таблицу' NumTexturesLump по смещению 0 для данной
текстуры записывается -1.
А теперь попытаемся объяснить, зачем используется такой подход. Как
уже говорилось выше, текстуры в DOOM’e выводятся вертикально, столбец за
столбцом. Каждый раз при выводе очередного столбца программа проверяет
таблицу NumTexturesLump по текущей координате X. Если значение больше О,
то теку щий столбец состоит из одной текстуры и данное значение указывает на
номер данной текстуры в каталоге файла ресурсов. В этом случае текстура
просто загружается с диска. Если же значение меньше 0, то это говорит о том,
что данный столбец состоит из нескольких текстур. Прн этом вызывается
функция, которая во временной памяти создает образ данного столбца,
состоящего из нескольких текстур, путем загрузки всех текстур, определенных
в txdefMore по текущей координате X. После обработки текущего столбца
временная память за ненадобностью освобождается.
- OffsetTextureCohimn - двумерный массив на тип short. Память для него
выделяется:
OffsetTextureColurrm=(short**)zmalloc(N*sizeof(short **), NULL, 1 );
Как и в предыдущем случае, OffsetTextureColumn - это массив из N
указателей на тип short. Один из таких указателей указывает на смещения
относительно начала текстуры, указывающие на начало графических
данных для конкретного столбца. Эта таблица используется для быстрой
установки указателя на адрес графических данных при выводе текстуры.
Например, если мы хотим вывести текстуру начиная с координат X и Y, то адрес
данной текстуры будет рассчитываться как:
AdrTexture + OffsetTextureColumn[NumText][X] + Y,
где NumText - номер текстуры;
- AdrTexture - начало текстуры (адрес текстуры, загруженной с диска):
AdrTexture=ReadLumpI'orNum(NumTexturesLuinp[NumText][X],l01),
Если вы помните, второй параметр (tag) определяет категорию данных, и в
данном случае указывает на то, иго данные кэшируются (tag >100).
OffsetTextureColumn[NumText][X] указывает на начало данных в текстуре
NumText для столбца X. Здесь координаты X и Y не следует путать с
координатами на экране Это координаты внутри текстуры. В заключение можно
отметить, что заполнение данной таблицы упрощается в связи с
особенностями формата графических данных в игре DOOM. Об этом вы
можете прочесть в главе 9.
- AdLoadingTexture - двумерный массив на тип char:
AdLoadingTexture = (char **)zmalloc(N*sizeof(char**), NULL, 1 );
Данная таблица представляет собой таблицу кэша для комбинированных
текстур, о котроых говорилось при описании таблицы NumTexturesLump. Как
вы помните, комбинированная текстура (состоящая из нескольких картинок)
создается во временной памяти путем загрузки всех картинок, определенных в
txdefMore. Если при выводе очередного столбца каждый раз будет производиться
данная операция, то это сильно затормозит игру. Очевидным выходом является
метод кэширования. Т.е. при первом выводе комбинированной текстуры для
нее выделяется память:
zmalloc(SizeOfUolumns[NumText],&AdLoadmgTexture[NumText], 101);
где и создается образ комбинированной текстуры. При выводе очередного
столбца вероятнее всего вторично не надо создавать образ, т.к. он уже есть и на
него указываетAdLoadingTexture[NumTextJ. Мы сказали “вероятнее всего”,
потому' что данный участок памяти имеет свойство кэширования. Если при
очередном вызове zmalloc окажется, что последнему не хватает памяти, он
может для себя освободить данный участок, записав в
AdLoadingTexture[NumText] нуль
- SizeOfColumns - одномерный массив на тип int:
SizeOfColumns = zmalloc(N*sizeof(int ♦), NULL, 1);
Этот массив очень прост. Для конкретной текстуры элемент вычисляется
как сумма высот общей текстуры только для комбинированных столбцов:
if (CountTexhiColumn[NumText][[i] > 1 )
SizeOfC'olumns|NumText]+=TexturesInfo[NumText]first.height,
Здесь CountTexInColumn[NumText][[i] определяет количество картинок для
столбца i в текстуре NumText.
Служит этот массив, как мы видели выше, для выделения памяти для
комбинированной текстуры.
- HeightTexture - одномерный массив из N чисел типа FixedPoint:
HeightTexture = zmalloc(N*sizeof(int *), NULL, 1);
FixedPoint - это фиксированная точка, которая от int практически не
отличается и определяется:
typedef FixedPoint int;
Каждый элемент в массиве HeightTexture для конкретной текстуры
представляет собой общую высоту текстуры (размер по Y), преобразованную
в фиксированную точку:
HeightTexture[NumText]=INT_TO_FIXED(TexturesInfo[NumText].first.height);
Как уже говорилось выше, преобразование числа из целого в
фиксированную точку' производится путем сдвига первого на 16:
^define INT_TO_FIXED(f) ((f) < 16)
Таблица HeightTexture используется в функции RenderWallRange
(наложение текстуры на стены), о чем речь пойдет в 9 главе.
- MaskOfText - одномерный массив из чисел типа int:
MaskOfText = zmalloc(N*sizeof(int *), NULL, 1 );
Каждый элемент данной таблицы является маской для выравнивания по
ширине текстуры:
{
flag = 1;
while(flag*2<=TexturesInfo[NumText].first. width)
{
flag *= 2;
}
MaskOfTextfNumText] = flag-1;
}
Например, если общая ширина текстуры равна 72, то ближайшая маска для
нее будет 63 (64-1). Данная таблица используется при выводе текстуры, маскируя
координату X, чтобы исключить выход последней за пределы. В связи с данным
обстоятельством ширина текстуры всегда должна быть выравнена в двоичном
эквиваленте. Например: 32,64,128,256 идр. значения, не превышающие 256.
Если данного выравнивания не будет, то часть текстуры путем маскирования
будет теряться.
Вот и все, что хотелось сказать об инициализации стенных текстур.
Надеюсь, что полученная информация поможет вам написать
самостоятельно функции инициализации текстур Хотелось бы только
отметить: все, что здесь говорилось о выводе текстур на экран, касается
лишь подготовки текстуры для вывода (загрузка текстуры в память). Что
же касается самого вывода, то эта операция производится с
масштабированием, и выполняет ее функция, написанная на ассемблере.
Кроме того, это тема отдельного разговора, и ей посвящена глава 9.
В заключение можно привести функции, которые вам будут полезны при
загрузке текстур.
Функция TextureNumForName возвращает номер текстуры по счету (тот
самый номер, который необходим при работе с вышеописанными таблицами
- NumText):
int TextureNumForName (char *namtext)
{
int num;
if ((num = SearTexture (namtext)) ==-!)// если текстуры с именем namtext нет,
И то выдается сообщение об ошибке
ExitWithMsg(“R_TextureNumForName:%snotfound”,namtext);
return num
}
Функция SearTexture осуществляет поиск текстуры в структуре Textureslnfo
и, если она нс найдена, то возвращает -1, если первым символом в имени
текстуры стоит то возвращается 0, в противном случае возвращается номер
текстуры по счету:
int SearTexture (char *namtext)
{
register inti;
if (namtext[0] ==“-’)
return 0; (
for (i=0; i<NumTexes; i++)
{
if (! stmicmp( namtext,Textureslnfo[i].first.name,8) )retum i;
}
return-1;
}
3.5.1.2. Инициализация текстур для
полов/потолков (FlatTextures)
В дальнейшем для простоты эти текстуры будем называть плоскими
текстурами. Плоские текстуры принципиально отличаются от стенных текстур
тем, что имеют всегда фиксированный размер 64x64. Кроме того, не бывает
комбиниро ванных плоских текстур. Такие упрощения оправданы в связи с двумя
обтоятельствами:
Во-первых, для полов и потолков не нужно такого разнообразия, как при
закраске стен, на них обычно не обращаешь много внимания;
Во-вторых, данный подход в значительной степени увеличивает
быстродействие программы в целом. Инициализация плоских текстур очень
проста и сводится к заполнению однбго одномерного массива:
void InitFlatTextures (void)
<
register int i;
fstart = getnum(“F_START”) + 1;
fend = getnum(“F_END”) -1;
nftex = fend - fstart + ];
adftex = zmalloc ((nftex+1 )*sizeof(int *), NULL, 1 );
for (i=0; i<nftex; i++)
{
adftex[i] = i;
}
}
В каталоге файла ресурсов плоские текстуры находятся между именами
“F START” и “F END”. Как видно, переменные fstart и fend явдякггся номерами
для соответственно первой и последней плоской текстуры в каталоге файла
ресурсов, nftex - определяет количество плоских текстур, используемых в
программе. О функции getnum(name) шла речь в пункте 3.4. Массив adftex, как
видно выше, заполняется порядковыми номерами плоских текстур, начиная с
нуля. На первый взгляд, использование данной таблицы совершенно
бессмысленно. Однако это было бы так, если бы этот массив на протяжении
всей игры ни разу не модифицировался. Но этот массив модифицируется в
функции ProcessingAnimated, которая отвечает за анимацию текстур.
3.5.2. Инициализация спрайтов
(функция Sprites/nfo)
К спрайтам в игре DOOM относятся все предметы и объекты, монстры,
как движущиеся, так и неподвижные. Формат данных для спрайтов
подробно описывается в 7 главе, а о принципах анимации спрайтов
рассказано в пункте 3.6.3. Здесь пойдет речь о заполнении некоторых таблиц
и инициализации переменных:
Исходный текст функции Spriteslnfo:
void Spriteslnfo (void)
{
register short *ad_spr,
register int i;
sstart = getnum(“S_START”) + 1;
send = getnum(“S_END”) - 1;
NumSpnteLumps = send - sstart + 1;
WidthSprite=zmalloc(NumSpriteLumps*sizeof(int),NULL, 1);
LeftOffset_Sprite=zmalloc(NumSpriteLumps*sizeof(int), NULL, 1 );
TopOffset_Sprite=zmalloc(NumSpriteLumps*sizeof(int), NULL, 1 );
for (i=0; i<NumSpriteLumps, i++)
{
adspr = ReadLumpForNum(sstart+i, 101);
WidthSprite[i] = INT_TO_FKED(ad_spr[0]),
LeftOffset Spntefi] = INT TOFIXEDfadspr[2]);
TopOffset_Sprite[i] = iNT_TO_FIXED(ad_spr[3]);
}
}
Все имена спрайтов в каталоге файла ресурсов заключены между метками
“S START” и “S END”. Переменные sstart и send указывают соответственно
на первый и последний спрайты в каталоге файла ресурсов.
NumSpriteLump - общее количество файлов для спрайтов. Не следует путать
с количеством спрайтов, т.к. один спрайт может состоять более чем из одного
файла (несколько кадров, каждый из которых в восьми или менее ракурсах).
Таблица WidthSprite - содержит информацию о размерах спрайтов по
горизонтали, те. об их ширине. Таблица LeftOffsetSprite - служит для коррекции
положения спрайта по координате X при проектировании спрайтов
(используется функцией ProjectSprite).
Таблица TopOffset Sprite - аналогична таблице LeftOffset Sprite, ио для
координаты Y.
3.5.3. Инициализация таблицы цветов
(LoadColorMap)
Эту таблицу' было бы правомерно назвать картой яркости. Она используется
для перспективного затенения и в некоторых других случаях, о чем подробно
можно прочитать в главе 9. Функция LoadColorMap осуществляет загрузку
объекта “COLORMAP”:
void LoadColorMapfvoid)
{
register int numcol,
register char *adcol,
numcol = getnum(“COLORMAP”);
adcol = zmalloc (lumplcn(nuincol)+255, NULL, 1);
adcolor = (char *) (((int)(adcol )+255) & OxflTHlOO);
readlump( adcolor, numcol);
}
При выделении памяти для карты цветов осуществляется выравнивание
указателя памяти по границе 256. Эго необходимо д ля оптимального алгоритма,
используемого в функциях вывода текстур и спрайтов, написанных на
ассемблере. Функция lumplen возвращает размер блока, а функция readlump
осуществляет загрузку' блока по адресу adcolor.
3.6. Инициализация анимации
(InitPlayLoop)
Под анимацией в данном случае понимается не только анимация
спрайтов, но и текстур. Этот процесс инициализации состоит из трех
пунктов, каждый из которых мы отдельно рассмотрим.
3.6.1. Инициализация таблицы текстур для
переключателей (InitSwitch Table).
В игре DOOM для приведения в действие каких-либо механизмов
(открывания дверей, построения лестниц, привода лифтов и др.) используются
различные переключатели. Все эти переключатели плоские и представляют из
себя текстуры. Т.к. переключатель обычно имеет два состояния (включено и
выключено), то к одному переключателю относятся две противоположные
текстуры. В процессе игры, при нажатии на выключатель (по клавише “SPACE”),
программа меняет текстуру в соответствующем месте на противоположную,
что визуально выглядит как переключение рубильника.
Функция InitSwitchTable заполняет таблицу текстур для переключателей:
void InitSwitchTable (void)
i
i
register int comver;
register int i, j;
comver = 1;
if( ! Shareware Vers)
comver = 2;
j = i = 0,
while ( swobjji]. ver)
f
I
if( swobjfi] ver <= comver )
{
swtext[j++J = TextureNumForName(swobj[i].namel);
swtext[j++j = TextureNumForName(swobj[i] name2);
}
swtext[j] = -1;
numswitch = j/2;
}
Здесь видно, что в таблице swtext содержатся номера взаимно
противоположных текстур по принципу чегный/нечетный. Переменная
Shareware Vers равна 0, если вы используете коммерческую версию игры. В
этом случае досту пно больше объектов и текстур, объекты swobj представлены
структурой switch, которую для игры DOOM1 можно изобразить:
static struct switch
{
char name 1 [9];
char name2[9];
hort ver;
} swobjf] =
{
{“SW1BRCOM”,”SW2BRCOM”,1},
{“SW1BRN1 ”,”SW2BRN1", 1},
{“SW1 BRN2”,”SW2BRN2”,1},
{“SW1BRNGN”,”SW2BRNGN”,1},
{“SW1 BROWN”,”SW2BROWN”,1},
{“SW1COMM”,”SW2COMM”,1},
{“SW1COMP”,”SW2COMP”,1},
{“SW1DIRT”,”SW2DIRT”,]},
{“SWlEXrr”,”SW2EXIT”,l},
{“SW 1 GRAY”,”S W2GRAY”, 1},
{“SW1GRAY1”,”SW2GRAY1",1},
{“SW1 METAL”, ”S W2METAL”, 1},
{“SW1PIPE”,”SW2PIPE”,1},
{“SW 1 SLAD”,”SW2SLAD”, 1},
{“SW1STARG”,”SW2STARG”,1},
{“SW1 STON1”,”SW2STON1 ",1},
{“SW1 STON2”,”SW2STON2”, 1},
{“S W1 STONE”,’’SW2STONE”, 1},
{“SW1STRTN” ,”SW2STRTN”,1},
{“SW 1 BLUE”,”SW2BLUE”,2},
{“SW1CMT”, ”SW2CMT”,2},
{“SW1 GARG”,”SW2GARG”,2},
{“S W1 GSTON”,”SW2GSTON”,2},
{“SW 1 HOT”,”SW2HOT”,2},
{“SW1LION”,”SW2LION”,2},
{“SW1 SATYR” ,”SW2SATYR”,2},
{“SW1 SKIN” ,”SW2SKIN”,2},
{“SW1 VINE” ,”SW2VINE”,2},
{“SW] WOOD”,”SW2WOOD”,2},
{NULL,NULL,0}
};
Параметр ver в структуре switch определяет доступность данного
переключателя. Если ver = 1, то переключатель может использоваться только в
условно бесплатной версии игры. Если ver = 2, то переключатель может
использоваться в любой версии.
3.6.2. Инициализация анимации текстур
(функция InitPicAnims)
Под анимацией текстур здесь понимается движение стен и полов
Например, вулканическая лава или вода, токсичная жидкость должны быть
в движении, чтобы приблизить атмосферу к реальности. На стенах также
могут меняться всевозможные узоры. И вес это зависит только от фантазии
и профессионализма художников.
Как и в предыдущем случае, анимация достигается путем замены текстуры
Но, в отличие от предыдущего случая, для одной анимации текстур здесь не
две, а больше. С каждым циклом игры в соответствующем месте меняется
номер текстуры. Теперь рассмотрим функцию, которая осуществляет
инициализацию структуры picarr для анимации текстур. Поняв ее суть, вы
поймете, как осуществляется анимация текстур в игре DOOM.
void InitPicAnims (void)
!
register int i;
i = 0;
while ( picanim[i].type != -] )
{
if (picanim[i].type )
{
if( Searrexture(picanim[i].nameFirst) != -1)
{
picarr[i].TextFirst=TextureNumFc»Name(picannn[i].nameFirst);
pican[ij.TextLast=TextureNumF<xNaiiie(picaniin[i].nameLast);
}
else
{
i++;
continue:
}
}
else
{
if (SearLump(picanim[i].name2) !=-l )
{
picarr[i].TextFirst=FlatNumForNaine(picanim[i].First),
picarr[i].TextLast=FlatNumForName(picanim[i].Last);
}
else
* i++;
continue:
}
}
picarr[i].type = picanim[i].type;
pican[il.NumAnnns=picarr[i].TextLast-picarr[i].TextFirst+l;
if ( picarr[i].NumAnims < 2 )
ExitWithMsg(“P_InitPicAnims: bad cycle from %s to
%s”,picanim|i].nameFirst,picanim[i].nameLast);
picarr[i] delay = picanim[i] delay;
i++;
}
AdrAnimText = picarr[ i];
}
Исходная структура picanim для игры DOOM 1 выглядит так:
static struct animtext
{
int type;
char nameLast[9];
char nameFirst[9];
int delay;
} picanimf] =
{
{O,”NUKAGE3",”NUKAGE1 ",8},
{0,”FWATER4",”F WATER 1 ",8},
{O,”SWATER4",”SWATER1 ”.8},
{O,”LAVA4",”LAVA1 ”,8},
{0,”BLOOD3",’’BLOOD 1 ",8},
{1,”BLODGR4",”BLODGR1 ",8},
{1 ,”SLADRIP3",”SLADRIP 1 ”,8},
{1 .’’BLODRIP4 ".’’BLODRIP1 ",8},
{1 ,’’FIREWALL”,’’FIREWALA”,8},
{1 ,”GSTFONT3",”GSTFONT1 ”,8},
{1 ,”FIRELAVA”, ”FIRELAV3",8},
{1 ,”FIREMAG3",’’FIREMAG 1 n,8},
{] ,”FIREBLU2",”FIREBLU1 ",8J,
{1 ,’’ROCKRED3",’’ROCKRED1 ",8},
{-1,NULL,NULL,0}
};
Как вы уже, наверное, догадались, параметр type определяет тип текстуры:
0 - текстура для полов/потолков, а 1 - для стен. nameFirst и hameLast
определяют соответственно первое и последнее имена для анимации.
Параметр delay представляет собой задержку; которая необходима при
любой анимации.
При выполнении функции InitPicAnims получаем массив структур picarr:
struct anim
{
mt type; // тип текстуры (0 - для полов/потолков, 1 - для стен)
int TextFirst; И номер первой текстуры для анимации
int TextLast; // номер последней текстуры для анимации
int NumAniins; // количество текстур для анимации
int delay, И задержка анимации
} picarr[16];
3.6.3. Инициализация анимации спрайтов
(InitSprites)
Прежде чем приступить к описанию алгоритма инициализации, хотелось
бы подробней рассказать об особенностях спрайтовой системы в игре DOOM.
Как известно, в большинстве игр движущийся объект состоит из нескольких
фаз движения. От того, насколько много таких фаз для одного объекта, зависит
плавность его движения. Одна из таких фаз представлена одним рисунком,
который и называется спрайтом. Такая же спрайтовая организация сохранилась
и в DOOM’e. Но DOOM - игра трехмерная. Эго подразумевает, что одну фазу
движения можно наблюдать с разных сторон, разных ракурсов. Те. один объект
можно охватить в диапазоне от 0 до 360 градусов. Для того, чтобы изобразить
один из спрайтов в данном диапазоне с дискретностью в один градус,
понадобится 360 картинок на одну' фазу' движения. Но это очень неэкономично
и, следовательно, неприемлемо. В DOOM’e использована дискретность 45
градусов, что составляет максимум 8 картинок на одну' фазу. Слово максимум
здесь стоит неслучайно, т.к. таких картинок может быть и меньше восьми.
И с этим связана еще одна особенность спрайтовой системы игры DOOM, о
чем речь пойдет ниже. Имя любого спрайтового объекта (не путайте с именем
спрайта, т.к. один спрайтовый объект состоит из нескольких спрайтов)
состоит из четырех заглавных букв. После этих четырех букв следует
символ, который определяет номер фазы, или номер спрайта для данного
объекта. Эти символы представлены латинскими заглавными буквами от А до
Z (А для нулевого спрайта, В -для первого и т.д). После номера фазы обязательно
следует цифра, которая определяет ракурс, с которого наблюдается объект.
Цифры представлены в диапазоне от 0 до 8. Если в качестве цифры стоит 0, то
это означает, что данный спрайт со всех ракурсов (сторон) выглядит одинаково.
Например для имени ВРАКАО можно сказать, что для спрайта ВРАК (первые
четыре символа в имени) фаза 0 (символ А после имени) со всех сторон будет
выглядеть одинаково (цифра 0). Теперь установим соответствие между цифрами
ракурса и собственно самим ракурсом. Напомним, что таких цифр 8 (от 1 до 8),
что соответствует дискретности поворота 45 градусов (360 / 45 = 8):
1 - вид спереди;
2 - вид спереди/справа;
3 - вид справа;
4 - вид сзади/справа;
5 - вид сзади,
6 - вид сзади/слева;
7 - вцд слева;
8 - вид спереди/слева.
Как видно, данная нумерация соответствует повороту объекта по часовой
стрелке с дискретностью 45 градусов. Например, картинка с именем BOSSF3
представляет собой вид справа для пятого спрайта (буква А соответствует
нулевому спрайту) объекта BOSS.
Но это еще не все. В DOOM’e используется еще один подход, который
заключается в совмещении нескольких спрайтов или раку рсов в одной картинке,
что позволяет экономить место как в памяти, так и на диске. Дело в том. что
подавляющее большинство спрайтов являются симметричными. Т.е. вид справа
не отличается от вида слева. Для того чтобы из одного вида симметрично
получить другой, достаточно осуществить зеркальное отображение по оси
ординат. Именно такой подход используется в DOOM’e. Для зеркального
отображения в имени файла ставится еще одна последовательность из буквы и
цифры, которая рассматривалась выше. Например, имя BOSSB4B6 для первого
спрайта объекта BOSS соответствует виду сзади/справа и сзади/слева, причем
для вывода последнего картинка должна быть зеркально отображена.
Зеркальное отображение осуществляется очень просто. Для этого достаточно
вывод спрайта начинать не с первого столбца, а с последнего. Используя
алгоритм зеркального отображения, можно добиться уникальных
результатов. Рассмотрим спрайт с именем SKELC1F1. Это скелет, который
замахнулся своей костлявой рукой, приготовившись нанести
сокрушительный удар. Как видно из имени - это вид спереди для спрайта с
номером 2. Так же данное имя принадлежит пятому спрайту, но в зеркальном
отображении, что визуально соответствует замаху' второй рукой. Таким
образом, мы имеем два разных спрайта, которым принадлежит один файл.
А теперь расскажем, что делает функция инициализации. Она осуществляет
заполнение структуры sprinfo:
struct memsp
{
int numfram, // количество спрайтов в объекте
struct framinfo
{
int flag; // если 0, то спрайт не вращается, I - вращается.
short numsp[8]; И порядковый номер спрайта в каталоге
//файла ресурсов, начиная с sstart (смотрите выше).
char typesp[8J; И если 1, то спрайт должен быть зеркально отображен
} rotinfo[26];
} sprinfo[NUM_OBJECTSJ;
Количество структур fraininfo для различных объектов может быть разным,
но их максимальное число не должно превышать 26, что соответствует диапазону
латинских букв от А до Z. В одной структуре framinfo, как видно содержится
информация о вращении спрайта. Например, numsp[0] - это иомер блока в
каталоге файла ресурсов для вида спереди, a numsp[2] - вада справа. В функцию
InitSprites передается адрес на таблицу' имен для объектов:
InitSprites(&adsprites). Таких имен в DOOM1 - 105 (NUM OBJECTS). Вот
начало и конец данной таблицы:
char *adsprites[] =
{
“TROO”,”SHTG”,”PUNG”,”PISG”,”PISF”,”SHTF”,...,”SMGT”,
”SMRT”,NULL
};
Получив необходимую информацию, я надеюсь, вы осилите
самостоятельно функцию InitSprites, которую мы здесь не приводим из
соображений экономии места.
3.7. Установка статуса машины
(SetupMachineState)
Под данным выражением тут понимается работа со всем, что имеет
отношение к конкретному “железу" (Hardware). Например, установка таймера,
клавиатуры, джойстика и др. Рассмотрим каждый пункт отдельно.
3.7.1. Настройка DPMI (LockRegion)
Данный пункт предназначен для закрытия региона памяти от перемещения.
Поясню, в чем дело. При работе программы под многозадачной операционной
системой (Windows, OS/2) или просто в режиме с виртуальной памятью
некоторые участки памяти не привязаны к одному месту в физической памяти.
В какой-то момент времени они могут свапироваться на диск, после чего,
по мере необходимости, загружаться опять в память, но не на свое прежнее
место. Все решения в данном случае принимает менеджер виртуальной
памяти, и программист контролировать этого не может. Все было бы ничего,
если бы виртуальная память не затрагивала участок кода программы.
Однако для кода менеджер виртуальной памяти исключения не делает. Для
него вся память одинаковая. Для нас очень важно, чтобы код программы не
перемещался, т.к. это чревато неприятными последствиями, учитывая тот факт,
что игра должна работать в реальном времени. Для этого случая имеется в
“арсенале” DPMI функций функция “Закрыть регион памяти” (Lock memory
Region). После ее выполнения закрытый участок памяти исключается из
компетенции менеджера виртуальной памяти, что нам и требовалось. Но перед
нами автоматически встает другая проблема: каким образом определить начало
кода программы и конец ее данных. Это очень просто. На начало текста
программы всегда указывает глобальная переменная______begtext, а конец данных
практически совпадает с адресом переменной____________Argc. Поэтому вызов
функции LockRegion можно оформить так:
extern int Argc;
extern int begtext;
LockRegion(& begtext, (mt )&_Argc-(int)& begtext);
Нетрудно догадаться, что в функцию LockRegion передаются два параметра.
1 Адрес начала кода программы.
2 . Длина региона для закрытия.
Сама функция LockRegion выглядит так:
void LockRegion (int adrcode, int sizecode)
regs.x.eax = 0x600,11 DPMI функция: LockRegion
regs.w.cx = adrcode & Oxfiff; //ex - младшая часть адреса
regs, w.bx = adrcode > 16; I I bx - старшая часто адреса
regs. w. di = sizecode & Oxfiff; // di - младшая часто длины кода
regs.vv.si = sizecode > 16; // si - старшая часть длины кода
int386 (0x31, ®s, ®s);
}
3.7.2. Инициализация “Мыши"(StartupMouse)
Эта функция очень проста и сводится к вызову нулевой функции 3311
прерывания. Если функция в регистре ах возвращает -1, то драйвер мыши не
загружен.
void StartupMouse (void)
<
i
union REGS regs,
struct SREGS sregs;
mouspres = 0;
if ( !CheckArg(“-nomouse”) && use mous )
!
regs.w.ax = 0;
int386 (0x33, ®s, ®s);
if(regs.w.ax !=-l )
printf (“ (mouse not present).\n”);
else
{
print! C‘ (mouse found).\n”);
mouspres = 1;
InitCybei Мап(),
} }
}
Если драйвер мыши загружен, то в переменную mouspres записывается 1,
в противном случае 0. Переменная use mous соответствует конкретному пункту
в файле конфигурации. Функция InitCyberMan() осуществляет инициализацию
3D мыши Cyber Мап, и здесь мы ее рассматривать не будем.
3.7.3. Инициализация Джойстика
(Startupjoystick)
Для работы с IBM джойстиком используется порт с адресом 0x201. Здесь
мы ие будем приводить алгоритмы и примеры работы с джойстиком, тем более
что они написаны на ассемблере. Кроме того, для этого существует масса
другой литературы. Единственное, что хотелось бы подчеркнуть - это то, что
одного определения наличия джойстика недостаточно. Если джойстик
обнаружен, то осуществляется его калибровка. Что это такое и зачем она нужна?
Дело в том, что подавляющее большинство джойстиков - аналоговые. Это
означает, что на элементарное перемещение ручки они выдают не какое-то
двоичное состояние типа включено/выключено, а определенную величину,
которая является числовым аналогом глубины перемещения ручки Этих
величин, как вы уже догадались - две: по оси X (горизонтальное перемещение)
и по оси Y (вертикальное перемещение). При включении питания компьютера
эти два значения в джойстике устанавливаются случайно. Они в большой
степени зависят от типа джойстика и практически не зависят от положения
ручки последнего в момент включения питания. Если запустить программу
без калибровки джойстика, то с управлением у вас могут возникнуть проблемы,
связанные с тем, что вы можете самопроизвольно перемещаться в любом
непредсказуемом направлении без вашего участия
Калибровка проводится для привязки программы к случайно
установленным координатам вашего манипулятора. Для этого выполняются
три пункта:
1. Ручка джойстика устанавливается в центр и нажимается кнопка. По
нажатию кнопки программа запоминает координаты джойстика в двух
переменных:
JkrX = joyX;
JkrY = joyY;
JoyX и JoyY - текущие координаты джойстика
2. Ручка перемещается в левый верхний угол и закже нажимается кнопка
джойстика. При этом записываются две граничные координаты для
отрицательного смещения:
jkrxmin = (JkrX + joyX) > 1;
jkrymin = (JkrY + joyY) > 1;
3. Ручка перемещается в правый нижний угол и также нажимается кнопка
джойстика. При этом записываются две граничные координаты для
положительного смещения:
jkrxmax = (JkrX + joyX) > 1;
jkrymax = (JkrY + joyY) > 1;
После данной калибровки, если при опросе джойстика оказывается,
например, что величина joyX меньше jkrxmin, то это означает, что вы
переместили ручку джойстика влево. Если joyX больше jkrxmax, то вы
переместили ручку вправо. То же самое относится и к вертикальным
перемещениям.
3.7.4. Инициализация клавиатуры
(StartupKeyboard)
Функция StartupKeyboard осущесвляет перехват вектора прерывания
клавиатуры 9 и устанавливает новый вектор на адрес newkeyb.
void (_interrupt far *kbOld!nterX);
void StartupKeyboard (void)
{
kbOldlnter = dos^etvect (9);
_dos_setvect(9, newkeyb);
}
Как осуществляется обработка клавиазуры, мы расскажем в 6 главе. При
выходе из программы прерывание клавиатуры обязательно должно
восстанавливаться:
_dos_setvect(9, kbOldlnter);
3.7.5. Инициализация звука (StartupSound)
Данная функция осуществлет автоматическую проверку наличия
звуковой платы и соответствие ее параметров тем, что установлены в
конфигурации. Мы не будем подробно останавливаться на этом вопросе по
крайней мере по двум причинам.
Во-первых, как правило, используются готовые библиотеки для работы со
звуком. При этом программист, подключив эти библиотеки к своему проекту,
пользуется готовыми функциями, не вникая в подробности. Он может и ие
знать, что делают эти функции.
Во-вторых, из-за наличия огромного количества различных звуковых
карт описать процесс инициализации, как и все остальные процессы на
низком уровне, не представляется возможным.
Одной из самых неплохих библиотек, поддерживающих работу практически
с любой звуковой платой (а также со SPE АК’ером), является библиотека фирмы
Human Machine Interfaces. Эта библиотека в данном случае очень
привлекательна тем, что имеются ее версии для работы в плоской модели
памяти, т.е. в защищенном режиме. Одной из положительных ее особенностей
является то, что она работает с внешними звуковыми драйверами:
- hmidet.386 - драйвера для детектирования и определения параметров
звуковых плат;
- hmidrv.386 - драйвера для работы с оцифрованным звуком на уровне
DMA;
- hmimdrv.386 - MIDI драйвера для работы с синтезатором (наборы
микросхем OPL, волновой синтез и др.).
В чем же преимущество внешних драйверов перед внутренними? А в
том, что внутренние драйвера всегда находятся в памяти, т.к. они
подключены к коду программы. Это очень неэкономно, особенно если учесть,
что они поддерживают больше десятка звуковых плат и для каждой из этих
плат используется своя программа обработки. Эти драйвера будут лишним
грузом в памяти даже в том случае, если вы звук не используете. С внешними
драйверами все обстоит совершенно иначе. В этом случае в память грузится
только та часть кода, которую использует конкретная установленная звуковая
плата.
В DOOM’e используются внутренние звуковые драйвера. Цифровой
звук в DOOM’e, как и в большинстве современных игр, многоканальный
(до восьми каналов). О том, что это такое и о принципах его реализации мы
расскажем в пункте 3.8.
Примером внутренних звуковых драйверов может служить звуковая
библиотека MID-KIT, которая поддерживает все звуковые платы типа
Soundblaster, включая SB 16 и одновременное воспроизведение до 32 голосов
оцифрованного звука (библиотека скомпилирована и скомпонована на
Watcom’e).
3.7.6. Инициализация таймера (StartupTimer)
Таймер нужен в любой игре в основном для синхронизации по скорости,
те. чтобы скорость игры ие зависела от быстродействия компьютера. Также
таймер может выполнять и другие функции. Например, осуществлять выборку’
данныхдля синтезатора при проигрывании музыки и др. В DOOM’e прерывания
таймера возникают с частотой 140 Гц. Функцию StartupTimer можно представить
так, хотя в DOOM’e она несколько сложнее.
void StartupTimer (freq)
{
tmOldlnter = _dos_getvect (8); // 3ai юмнить старый адрес прерывания
_dos setvect(8, newtimer); // установить новый вектор
SetTimer (1193180/ freq); // изменить частоту таймера
TimerOk =1;
}
void Set Timer (short val)
{
outp (0x43,0x36);
outp (0x40,val & 255);
outp (0x40,(val > 8) & 255);
}
При работе нового обработчика таймерных прерываний необходимо
позаботиться о том, чтобы системное время не прекращало свой ход. Для этого
из нового обработчика прерывания надо вызывать функцию tmOld!nter(). Все
бы ничего, но частота первого намного выше (стандартная частота таймера
18.2 Гц). Как это делается, показано ниже на простом примере:
void__interrupt_far new_timer()
{
timuser++;
CounSec++;
if ( CounSec == 140 )
{
CounSec = 0;
Secs++; 11 прибавление сукунд
if (Secs == 60 )
{
Secs = 0;
Mins++; // прибавление минут
if (Mins == 60 )
{
Mins = 0;
Hours++; // прибавление часов
if ( Hours == 24 )
Hours = 0;
}
}
}
if (!(timuser % 15))
tmOldInter();
outp(0x20,0x20);
}
В этом примере обработчик new timer осуществляет подсчет времени в
часах, минутах и секундах. В DOOM’e используется более сложный алгоритм,
связанный с музыкальными делами. Именно поэтому мы здесь его не приводим.
3.8. Установка звука (SetupSound)
Выше уже говорилось, что цифровой звук в DOOM’e многоканальный.
Функция SetupSound осуществляет инициализацию структур для
микширования звуковых каналов,, а также устанавливает громкость для
оцифрованного звука и для музыки.
А теперь рассмотрим, что же такое оцифрованный звук и как его
микшировать. Для простоты будем считать, что у нас 8-битный звук, т.е. одна
выборка представляет собой один байт. Для начала вспомним из курса физики,
что такое звук. Реальный звуковой тон - это синусоидальная волна, амплитуда
которой зависит от громкости звука, а частота - от высоты тона. В любой звуковой
плате цифровой звук воспроизводится через ЦДЛ (цифро-аналоговый преобра-
зователь). На выходе ЦАП мы всегда имеем аналоговый эквивалент двоичного
кода на его входе. Эти двоичные коды приходят на вход ЦАП с определенной
частотой, которая называется частотой выборки (или частотой дискретизации).
Частота выборки в соответствии с теоремой Котельникова, всегда должна быть
как минимум в два раза выше частоты воспроизводимых звуковых колебаний.
Например, невозможно воспроизвести звуковой тон частотой 12 Кгц, если
частота выборки у вас 11 Кгц. Чтобы достигнуть качества воспроизведения
компакт дисков, используется частота выборки 44.1 Кгц. С качеством
воспроизведения звука связан еще один параметр - разрядность ЦАП. Обычно
ЦАП имеет 8 или 16 разрядов. Чем больше разрядов, тем большим количеством
цифровых кодов (и, следовательно, аналоговых уровней) можно представить
весь динамический диапазон звуковой волны, и тем чище будет звук. Для 8-ми
разрядных карт максимальное шличестве кодов - 256 (2Л8 = 256), для 16-разрядных
-65536 (2Л16 = 65536). Нетрудно догадаться, что файл 16-битного звука в два раза
больше по размеру такого же файла, ио для 8-битного звука. Те же соотношения
справедливы и по отношению к частоте выборки: файл с частотой выборки 44
Кгц в два раза больше файла с часто! ой выборки 22 Кгц. Следовательно, если
мы выигрываем в объеме, то проигрываем в качестве, и наоборот. В этом
случае приходится идти на компромисс. Обычно в играх ие требуется
высокого качества звукового сопровождения (относительно, конечно). В
большинстве игр используется оцифрованный 8-ми разрядный звук с
частотой выборки 11025 Гц, что вполне приемлемо. Нетрудно подсчитать, что
секунда воспроизведения такого звука соответствует размеру файла - 11025
байт. Тут следует отметить, что частота выборки должна быть одна для всех
звуковых файлов. Иначе возникнут проблемы с микшированием.
Вот мы и вернулись к вопросу микширования. Микширование может
осуществляется двумя способами: масштабируемое сложение выборок и
умножение. Первый метод лучше использовать для смешения звуковых каналов,
а второй - для регулировки громкости. Оба этих метода должны осуществляться
без искажения средней точки. Средняя точка - это ось, через которую
проходит синусоида, иными словами - это половина динамического
диапазона звуковой волны. Для 8 битного звука - это код 128 или 0x80
(256/2 = 128). Для метода микширования со сложением программа расчета
одной выборки выгладит так:
char MathMixAdd (char sei 1 ,char sel2)
<
i
int mix;
mix = sell -0x80+sel2;
if(mix<0)
mix = 0; // ограничение волны снизу
else
if(mix>255)
mix = 255; 11 ограничение волны сверху
return (char)mix;
}
Для регулировки громкости звука программа расчета одной выборки
выглядит так:
char MathMixMul (char sei,char vol)
{
int mix;
vol *- 4;
mix = (sel-0x80)*vol+0x80;
if ( mix < 0)
mix = 0; 11 ограничение волны снизу
else
if (mix > 255 )
mix = 255; 11 ограничение волны сверху
return (char)mix;
}
В первой функции входные параметры sei 1 и sel2 являются выборками
сигнала для первого и второго источника. Во второй функции параметр sei
определяет выборку' результирующего смикшированного звукового канала,
а параметр vol - громкость звука Максимальное значение громкости для
умножения должно быть 128. Следовательно, если мы использовали
умножение значения громкости на 4, то градаций регулировки громкости у
нас 32 (32*4 = 128). Если бы у нас было 16 значений громкости, то надо было бы
использовать умножение на 8. Обе функции возвращают результирующую
смешанную выборку. Для ускорения работы их лучше всего переписать на
ассемблере. Для примера приведем макрос на ассемблере для регулировки
громкости звука:
//pragma aux asmvol pami [edi] [esi] [ecx] [edx] = \
“push ebp” \
“leaebp,[edx*4|” \
“mov ebx,0x80” \
“jecxz dal” \
“dal3: lodsb” \
“inovzx eax,al” \
“sub eax,ebx” \
“imul ebp” \
“idivebx” \
“add eax.ebx” \
“text eax,eax” \
“jgedall” \
“xoreax,eax” \
“dal 1: cmp eax,255” \
“jledal2” \
“mov al,255” \
“dal2: stosb” \
“loopdal3” \
“dal: pop ebp” \
fy [eax esi edi ebx ecx edx];
В отличие от предыдущего примера этот макрос обрабатывает не одну
выборку, а буфер. Его вызов выглядит так: asmvol (dst,src,size,vol); где dst -
указатель на адрес, куда поместить результат; src - указатель на исходный буфер;
size - размер буфера; vol - значение громкости звука.
Теперь в двух словах попытаемся объяснить, как организована работа с
цифровым звуком иа высоком уровне в самой игре. Если вам на определенном
этапе игры необходимо воспроизвести какой-либо звук (например, при
открытии двери, либо при выстреле и др.), то вызывается функция FXSound с
двумя параметрами, первый из которых является указателем на структуру
данных того объекта, которому принадлежит звук, а второй - порядковый номер
звука. Данные объекта необходимы для того, чтобы определить направление
источника звука относительно игрока для стерео режима и его удаленность
(чем дальше источник звука от игрока, тем меньше его громкость). Функция
FXSound первым делом проверяет удаленность источника от игрока. В том
случае, если источник очень далеко, то никаких действий не производится и
осуществляется выход из функции. В противном случае ищется незанятый
звуковой канал, для звука выделяется память соответствующего размера,
куда загружается звук. После этого заполняется структура звукового
канала, в которой может быть любое количество параметров, но для
простоты ее можно описать так:
struct chanjnfo
{
SFX *SoundInfo;
THING *obj;
char *posDMA;
} SFXChanels[8];
В этой структуре поле Soundinfo - это блок данных для конкретного звука
(его номер передается в функцию FXSound вторым параметром), в котором
определены такие данные, как имя файла для звука, указатель на буфер звука
(который выделяется перед загрузкой звукового файла с диска),
продолжительность звучания в тиках таймера, относительная громкость и
другие параметры, количество которых зависит от конкретного алгоритма.
Поле obj - это блок данных объекта, которому' принадлежит источник звука.
posDMA - текущая позиция буфера DMA. Указывает на адрес, куда необходимо
поместить данный звук ддя воспроизведения. Функция FXSound только
заполняет данную структуру для конкретного канала и больше ничего не делает.
А все остальные действия выполняет функция SendSFX, которая находится
в основном цикле игры перед функцией работы с графикой. Эта функция в
цикле просматривает каждый канал и осуществляет по каждому из занятых
каналов изменение громкости, если это необходимо (если параметр obj в
структуре chan info ие равен нулю, то громкость объекта зависит от
удаленности данного объекта от игрока). После этого осуществляется смешение
(микширование) всех занятых каналов и общая регулировка громкости, после
чего результат пересылается в буфер DMA. Также функция SendSFX проверяет:
не закончилось ли воспроизведение какого-либо канала и, если это так,
освобождает данный канал путем освобождения буферов и записью в поле
Soundinfo нуля.
3.9. Загрузка основного шрифта (HU_lnit)
Все основные надписи в игре DOOM выполняются шрифтом, каждый
символ которого представлен отдельным блоком в файле ресурсов. Шрифт
начинается с символа ‘Всего символов 64. Имя символа состоит из названия
STCFN и трех цифр, начиная с 033. Таким образом, функция HUInit выглядит
так:
void HU Init (void)
{
char str[9J;
J ’
for (i=0; i!=63; i++ )
sprintf (str, “STCFN%.3d” j++);
hdisp[i] = ReadLumpForNamc (str, 1);
}
}
Работа co шрифтами в различных программах организована по-разному, и
в DOOM’e использован не самый экономный метод, но зато самый простой.
3.10. Инициализация статус панели
(STJnit)
Стазу с панель - это часть экрана, на которой отображается состояние игрока,
его самочувствие, текущий счет, выбранное оружие и масса других параметров,
число которых варьируется в зависимости от уровня сложности самой игры.
Обычно статус панель располагается в нижней части экрана, ио бывает, что ее
располагают сверху и даже сбоку; Статус панель состоит из множества
элементов. Например, разноцветные ключи, внешнее изображение игрока в
зависимости от самочувствия и др. Функция ST Init загружает все элементы
статус панели в память и заносит адреса загруженных блоков в соответствующие
таблицы, т.е. разбивает все данные статус панели на группы. Мы не будем
приводить здесь функцию ST Init по той причине, что, если вы будете писать
свою игру, у вас наверняка будет своя статус панель.
Вот мы и закончили описание функций инициализации. Еще раз повторюсь:
все, о чем говорилось выше - это всего лишь пример, как делаются подобные
программы. В вашей собственной игре, возможно, все будет несколько по-
другому и с технической точки зрения, и с дизайнерской. Поэтому не
принимайте все сказанное за эталон.
А сейчас мы вплотную подошли к самой главной теме - основному циклу'
игры. Здесь вы узнаете, как осу нкствляегся управление в игре, как производится
обработка объектов и многое другое.
Глава 4
Основной цикл игры (PlayLoop)
Любая компьютерная игра постоянно крутится в одном небольшом (или
большом) цикле, который называется основным циклом игры. Этот цикл может
быть безусловным или условным. Ои может быть любого размера и иметь
различное количество функций. Выход из этого цикла может осуществляться
только при окончании игры, а может вообще никогда не осуществляться (в
этом случае выход из игры производится в одной из функций данного цикла).
Все это разнообразие связано лишь с алгоритмом построения самой игры По
размеру основного цикла нельзя судить о сложности игры, в чем вы скоро
сами убедитесь. Каждая функция, вызов которой осуществляется из основного
цикла, может быть настолько сложной и разветвленной, что по объему может
занимать добрую половину' игры, а то и больше. Мы не будем здесь приводить
различные примеры из личного опыта, чтобы не навязывать свою точку зрения.
Напомним еще раз, что в каждой игре основной цикл строится по-разному. Но
в силу того, что наша книга посвящена игре DOOM, все же приведем пример из
этой игры, опустив некоторые детали (эти детали связаны с режимом “timedemo”
и к данному повествованию отношения не имеют):
void PlavGame (void)
{
InitGraph();
while (1 )
{
TryRunTicsQ;
SendSFX(chuv[player].PlayerThing);
GraphicsQ;
}
}
Здесь мы оговоримся. Игра DOOM - сетевая игра, которая поддерживает
одновременную работу в сети четырех человек. Но в этой книге в процессе
изложения мы будем опускать те пункты, которые связаны с работой в сети для
того, чтобы не загромождать и без того довольно объемный материал.
Как видно выше, бесконечный цикл организован с помощью оператора
while(l). Условие в скобках никогда не сбудется (1 - это “истина”), поэтому цикл
бесконечный. Конечно, в данном случае можно прибегнуть к помощи
оператора goto, но Керниган и Ричи предупреждали, что это признак плохого
тона. Бесконечный цикл также можно организовать например с помощью
конструкции:
for (;;)
{
тело цикла
}
В функции PlayGame перед переходом к основному цикла' мы видим вызов
функции InitGraph. А теперь вспомним, что вся инициализация в DOOM’e с
выводом ее результатов проводилась в текстовом режиме. Значит только сейчас,
при входе в функцию PlayGame устанавливается графический режим.
Остановимся на этом пункте подробнее...
4.1. Многостраничный режим VGA
320x200x256 и как с ним работать.
Освещение данного важного вопроса начнем с функции установки
графического режима InitGraph. Эта функция первоначально устанавливает
стандартный режим VGA (0x13), после чего, путем манипуляции с портами
устанавливает четырехстраничный режим:
void InitGraph (void)
VgaSet = \J! признак графического режима
regs, w.ax = 0x13;
int3 86 (Ox 10, ®s, ®s); // установить VGA режим (0x13)
AdVga = (char *)0xa4000;
bout(0x3c4,4); // последовательный доступ к памяти
bout(0x3c5,(bin(0x3c5) & 0xf714)); // в битовой плоскости
bout(0x3ce,5);// каждая плоскость может быть перепис. в
bout(0x3cf,(bin(0x3cf) & Oxfd)); // другую через per. защелки.
wout(0x3c4,0xfD2); И разр. доступ ко всем плоскостям
stos(OxaOOOO, 65536); // очистка видео-памяти
bout(0x3d4,0x!4); И
bout(0x3d5,(bin(0x3d5) & Oxbf));
bout(0x3d4,0x 17);
bout(0x3d5,(bin(0x3d5) 10x40)); // режим адресации байтов
bout(0x3ce,4); //регистр выбора карты чтения
SetPal (ReadLumpForName(“PLAYPAL”,101));
GraphDisk();
}
Вы, вероятно, увидели здесь очень много непонятных функций: bout.bin,
vvout, stos. Нетрудно догадаться, что это все макросы, написанные на
ассемблере, которые заменяют стандартные библиотечные функции. Это
сделано потом}’, что вызов библиотечной функции всегда сопровождается
командой call, даже если эта функция очень простая. Например, outp - послать
байт в порт. Поэтому лучше такие функции переписать на ассемблере, как
макросы, и использовать их, подставляя вместо стандартных функций. Как это
делается, мы рассказывали выше, но конкретно для данного случая стоит
повториться и привести примеры используемых нами макросов. Запомните их
хорошенько, а еще лучше запишите, т.к. в процессе изложения мы не раз будем
к ним возвращаться. Итак:
bout - послать байт в порт (1 параметр - адрес порта, 2 - значение):
#pragma aux bout parm [dx] [al] = \
“outdx,al”;
wout - послать слово в порт (1 параметр - адрес порта, 2 - значение):
flpragma aux wout parm [dx] [ax] = \
“outdx,ax”;
bin - прочитать байт из порта (параметр - адрес порта):
flpragma aux bin parm [dx] = \
“xoreax,eax”\
“inal,dx”\
value [eax];
stos - заполнить буфер нулями (1 параметр - адрес буфера, 2 - длина):
flpragma aux stos parm [edi] [edx] = \
“xoreax,eax”\
“mov ecx,edx” \
“shrecx,2”\
“repne stosd” \
“movcl,dl”\
“andcl,3”\
“repne stosb” \
modify [edi eax ecx];
Раз мы уже говорим о макросах, то я приведу еще несколько, чтобы в
дальнейшем не возвращаться к данном}' вопрос}':
move - пересылка буфера(1 параметр - адрес пункта назначения, 2 параметр
- адрес исходного буфера,3 параметр - длина);
flpragma aux move parm [edi] [esi] [eax] = \
“movecx,eax”\
“shr ecx,2” \
“repne movsd” \
“mov cl,al” \
“andcl,3”\
“repne rnovsb” \
modify [esi edi ecx];
getlen - вычислить длину строки (параметр - адрес строки):
flpragma aux getlen parm [edi] = \
“sub ecx,ecx” \
“dec ecx”\
“xoreax,eax”\
“repne scasb” \
“notecx”\
“dec ecx”\
modify [edi eax] \
value [ecx];
movstr - переслать строку (1 параметр - адрес строки назначения, 2 параметр
- адрес исходной строки):
flpragma aux movzer parm [edi] [esi] = \
“mvz: lodsb” \
“stosb” \
“test al, al ”\
“jne mvz” \
modify [eax edi esi];
На этом этапе мы позволим себе некоторое замечание: во всех
вышеуказанных макросах параметр modify указывает на те регистры,
которые изменялись внутри макроса. В принципе можно обойтись без modify,
используя сохранение данных регистров на входе макроса (оператор push)
и восстаиовлеиие на выходе (оператор pop). Одиако делать этого ие
рекомендуется, т.к. компилятор в процессе генерации кода сам таким
образом перераспределяет регистры в соответствии с используемым
макросом, что в большинстве случаев никакого сохранения регистров и ие
требуется, и получается более оптимальный код. По возможности реже
используйте команды push и pop, т.к. известно, что категория команд работы
со стеком одна из самых медленных.
Но мы немного отвлеклись от темы (надеемся, что читатель простит нам
то, что мы совершили небольшой экскурс в мир ассемблера). Переменная
VgaSet указывает на то, что установлен графический режим. Она используется
в процессе игры, о чем мы расскажем чуть ниже, и при восстановлении
текстового режима. Функция SetPal устанавливает цветовую палитру:
void SetPal (char * adp)
{
register int i.prtpal;
register char ch;
WaitVrt(l);
bout (0x3c8,0);
prtpal = 0x3c9;
for (i=0; i<256*3; i++)
ch= tabpalet[use_gamma][adp[i]J;
CurPalettefi] = ch;
bout (prtpal, ch>2);
}
1
Что такое палитра, мы здесь объяснять не будем. Но тем не менее на
некоторых деталях вышеописанной функции остановимся. Первое, что
бросается в глаза - это функция WaitVrt. Эта функция ждет обратного хода
кадровой развертки. Во время обратного хода и устанавливается палитра,
чтобы не было мерцания. Единственным параметром функции WaitVrt
является счетчик кадров (сколько кадров ждать), в связи с чем данную
функцию можно использовать для организации задержек: void WaitVrt (int
loop)
{
register iht adrp;
adrp = 0x3da;
while (loop-)
{
while ( !(bin(lopj) & 8) ) {}
while (bin(lopj) & 8) {}
}
}
Еще одной особенностью функции SetPal является то, что данные палитры
пересчитываются с использованием таблицы tabpalet. Что это за таблица? Дело
в том, что в DOOM’e используется метод коррекции гаммы. Популярно это
можно объяснить программной регулировкой яркости. Сделано это очень
кстати, т.к. без этих мер играть в игру было бы “слишком темно”, хотя, как
утверждают разработчики, коррекция гаммы необходима только для некоторых
“темных” мониторов. Номер гаммы определяется переменной usegamma и
изменяется в DOOM’e от 0 до 4 Нулевое значение соответствует состоянию:
коррекция гаммы выключена. В данном случае значения на выходе таблицы
соответствуют входным значениям, те. мы имеем линейную зависимость. По
мере увеличения переменной use gamma зависимость все более приобретает
ярко выраженную логарифмическую зависимость. По причинам экономии
места мы не будем приводить здесь данную таблицу; тем более что по
полученной информации вы без труда сами можете ее воспроизвести методом
интерполяции.
Еще одна функция, вызов которой осуществляется из InitGraph - GraphDisk.
Эта функция загружает объект, который визуально представлен маленькой
дискетой. Эта дискета появляется в правом нижнем углу’ экрана в то время,
когда идет загрузка данных. Зачем это нужно, я думаю, объяснять излишне. Как
это осущест вляется? В функции загрузки данных с диска readlump первым делом
вызывается функция OutDisk, которая первоначально проверяет переменную
VgaSet. И если она не равна нулю, осуществляет вывод графического диска в
правый нижний угол, предварительно запомнив под ним информацию Перед
выходом из функции readlump осу ществлястся вызов функции ClearDisk. которая
после вышеупомянутой проверки переменной VgaSet производит
восстановление информации, на которую накладывался диск. Тут следует
отметить, что все операции по выводу данного значка должны быть очень
быстрыми, чтобы не тормозить обмен с винчестером, по возможности без
циклов и без использования стандартных универсальных функций вывода
графики. Вместо изображения диска можно использовать все, что угодно,
например, песочные часы. И необязательно в углу' экрана; можно и в середине.
В данном случае все зависит только от вашего воображения
А теперь несколько слов посвятим работе с многостраничным режимом
VGA. Зачем нужен многостраничный режим? Почему не пользоваться одной
страницей? Ведь это намного проще. А все дело в том, что запись в видеопамять
осуществляется в десятки раз медленнее, чем в обычную память. В результате
при выводе картинки на экран видна прорисовка. Особенно это заметно, когда
предыдущая картинка сильно отличается от текущей, выводимой. При этом
наблюдается эффект как бы расслоения экрана. Все сказанное характерно
прежде всего для медленных видеоадаптеров (Trident, Realtek и др.), но бывает
и на быстрых акселераторах. И в данном случае это никак от программиста не
зависит, не зависит от того, насколько оптимальный алгоритм вывода он
использует (чаще всего используется команда: rep movsd). Это все аппаратные
издержки. Чтобы такого не произошло, используется многостраничный режим,
который в двух словах можно охарактеризовать так: изображение сначала
выводится в невидимую часть экрана, а после того, как формирование
изображения закончено, производится переключение страницы, и невидимое
становится видимым. Затем цикл повторяется сначала, те. изображение
готовится в противоположной странице и затем снова - переключение страницы.
Переключение страницы происходит настолько быстро (ведь это обычная
запись в порт), что глаз ие в состоянии это заметить, и вы никогда не увидите
перерисовки на экране, каким бы медленным ни был ваш компьютер. Функция
FlipPage, осуществляющая переключение активной страницы, выглядит так:
void FlipPage (void)
{
wout(0x3d4, ((int)(AdVga) & OxfIUO) + 12);
AdVga+= 0x4000,
if ( AdVga == OxacOOO )
AdVga = (char *)0xa0000,
}
Переменная AdVga (если вы помните, мы ее установили в функции
InitGraph) всегда указывает на адрес следующей невидимой страницы. Мы
сказали: следующей, т.к. всего этих страниц - 3. и необходимо три раза пройти
функцию FlipPage, чтобы вернуться к тому значению переменной AdVga, с
которого начинали. Четвертая страница используется для служебных целей.
Откровенно говоря, использовать три страницы там, где можно использовать
две - это излишество, ио разработчики DOOM’a пошли таким путем. Что же...
Это их право. Для использования двухстраничного режима достаточно лишь
проинвертировать 14 бит в переменной AdVga( AdVga Л= 0x4000; ) вместо
прибавления к нему значения 0x4000 и затем еще проверки условия с целью
ограничения.
Безусловно, многостраничный режим имеет свои преимущества, ио у него
есть и недостатки, причем очень существенные. За все приходится платить. А
цена за многостраничность такая, что многие фирмы отказываются от нее в
пользу одной страницы (например, в игре HERETIC). В чем же дело?! А дело в
том, что организация экрана в многостраничном режиме нелинейная, как в
одиостраничном режиме. Линейный экран - это когда соседняя справа точка
на экране представлена соседним байтом в видео-памяти. Другими словами,
можно переслать изображение на экран с использованием стандартных
функций пересылки (например, прибегнув к описанному выше макросу' move)
и при этом сразу увидеть изображение. Нелинейный экран может сильно
затормозить игру, если не принять специальных мер по быстродействию И в
DOOM’e эти меры были приняты. Но - обо всем по-порядку. Сначала мы
расскажем, как устроен многостраничный режим VGA и на небольших примерах
покажем, как с ним работать, а затем расскажем, как в DOOM’e решена
проблема быстродействия.
Режим, о котором идет речь, практически полностью идентичен
чегырехплоскостиому режиму' EGA. В связи с этим мы не будем подробно
останавливаться на его принципах. Можно лишь напомнить, что соседняя по
горизонтали точка находится в соседней плоскости. Это означает, что
фактическая организация одной плоскости представлена разрешением не
320x200, а 80x200. При выводе очередной точки на экран необходимо
переключить номер плоскости. Это делается записью в регистр маски (ОхЗс5/
2) при записи в видеопамять и записью в регистр выбора карты чтения (Ox3cf/
4) прн чтении из видеопамяти. Таким образом, при выводе одной строки
изображения (а их всего 200) мы должны 320 раз производить запись в порт.
Но это очень иеоптимально. Поэтому чаще всего осуществляется вывод
изображения сначала в одну' плоскость, затем в другую и т.д., до четырех. В
этом случае запись в порт осуществляется всего четыре раза.
Если плоскостей - четыре, то размер одной плоскости составляет 16000 байт
(16000 ♦ 4 = 64000 = 320 ♦ 200). Физические адреса страниц для 0,1,2 и 3 страниц
осхугзстствснно равны ОхаОООО, 0ха4000,0ха8000 и ОхасООО. А теперь на примере
двух функций приведем пример работы с данным режимом: Функция UpdateBox
осуществляет вывод на экран из буфера AdrScreensfO] (буфера экрана мы
определяли в функции VAllocMem. смотрите пункт 3.1.) участка с
координатами kx 1 и кх2 шириной widt и высотой heig.
void UpdateBox (register intkxl, register intkyl, register int widt, register intheig)
register int i,xsrc,xdst, widtdst,adsrc,addstjJ<;
register char *src;
register short *dst; // проверка на границы
if(kxl || kyl || widt<=01| heig<=01| kxl+widt>3201| kyl+heig>200)
ExitWithMsg(“Bad lUpdateBox (%i, %i, %i, %i)”, kxl, kyl, widt, heig);
i = kxl» 3; // выравнивание
widtdst = ((widt+kxl )»3)-i+l, // размер по x для вывода
xsrc = kyl *3204*8; // нач. координата по x для исходного буфера
i = 0; // счетчик плоскостей
adsrc = 320 - widtdst*8; И приращение для адреса исходного буфера
bout(0x3c4,2); // доступ к регистру маски
addst = 40-vtidtdst; // приращение для адреса экрана
xdst = xsrc»2; // нач. координата по х для экрана
do
{
bout(0x3c5, l«i); //установить плоскость
sic = AdrScreens(O]+xsrc+i; // адрес исходного буфера
dst = (short *)(AdVga+xdst); // адрес экрана
for (j=0, j <heig; j++) // цикл no Y
{
k = widtdst;
while (k-) // цикл по X
*dst++ = (short )((src[4]«8)+src[0]); И запись
//двух точек на экран
adl += 8;
}
sre += adsrc;
dst += addst;
k
} while (i<4 );
}
Для ускорения эта функция осуществляет вывод на экран ие по одному
байту, а сразу по два, что уменьшает количество итераций по х. Функция,
которую мы привели в качестве примера, осуществляет запись на экран. А
сейчас мы покажем вам на примере функции CopyScreenTo, как можно читать
из экрана:
void CopyScreenTo (register char * dst)
{
register int i,j;
register char *dst 1;
bout(0x3ce,4); // доступ к “регистру выбора карты чтения”
i = 0; // счетчик плоскостей
do
{
bout(Ox3cf,i), // запись в “регистр выбора карты” чтения номера плоскости
J = 0;
dstl = dst+i; И адрес буфера
do
{
*dstl = AdVga(j++];
dstl += 4,
} while (j<16000 );
i++,
} while (i <4 );
}
Эта функциях копирует содержимое экрана в буфер dst и в комментариях
не нуждается.
В заключении данного раздела можно привести функцию, которая
восстанавливает текстовый режим экрана. Вызов'ее осуществляется при выходе
из игровой программы:
void RestoreScreen (void)
{
char *ad;
ad = (char *)0x449; 11 текущий режим экрана
if ( ad(0] ==0x13)// если установлен 13 режим (VGA), то установить
{ // текстовый режим.
regs.w.ax = 3;
int386(0xl 0, ®s, ®s);
}
}
4.2. Многорежимность и
многофункциональность.
Вот мы после столь длительного вступления подошли к сути: к основным
функциям, из которых складывается игра, которые принимают
непосредственное участие на той или иной стадии игры. Прежде чем приступить,
несколько слов об организации игры и ее режимах. DOOM - игра сложная,
миогорежимная и многофункциональная. Мы не можем рассматривать ее
работу шаг за шагом, т.к. на определенных этапах ее работы некоторые
функциии принимают активное участие, а на других этапах совсем нс
принимают. Поэтому разумнее всего было бы весь материал поделить на
основные этапы работы игры, например, начало игры (загрузка данных у ровня),
процесс игры (его можно поделить на несколько частей), конец уровня,
восстановление записанной игры и др ), которые по отдельности и изложить.
Для начала поясним основные переменные, которые управляют всей игрой:
- GameMode - это основная переменная, управляющая игрой Она может
принимать четыре значения:
О - реальная игра (GameMode равна нулю, так же в режиме демонстрации);
1 - конец уровня;
2 - конец игры (финальная заставка);
3 - заставка.
- Переменная ChangeMode - определяет какие-либо изменения в ходе
игры. Ее нормальное состояние - 0, но при необходимости какого-либо
изменения, например, начало нового уровня, ее состояние меняется, на что
реагирует функция GeneralProcessing, где осуществляется обработка
дайной переменной. Переменная ChangeMode имеет 9 основных значений,
не считая 0:
1 - восстановление состояния после “убийства” игрока;
2 - начало игры (загрузка данных уровня) (New Level);
3 - восстановление записанной игры (Load Game);
4 - запись игры (Save Game);
5 - инициализация режима демонстрации;
6 - конец уровня;
7 - конец эпизода;
8 - переход на следующий уровень;
9 - запись копии текущего экрана на диск (Screen Capture). После обработки
переменной в функции GeneralProcessing она сбрасывается в 0, что указывает
на то, что далее никаких изменений производить ие надо.
В игре используется еще множество других переменных, управляющих
программой на том или ином этапе:
- MapMode - режим карты в процессе игры;
- MenuMode - режим меню, и т.д. О иих мы подробнее расскажем в
процессе изложения.
Выше мы говорили о том, что конкретные функции принимают участие
лишь на определенных этапах работы игры. Но имеются функции, работа
которых никогда не прекращается. Они находятся в основном цикле, и никакими
условиями их область действия не ограничена. Речь идет о функциях
управления, контроля со стороны играющего. С них мы и начнем.
Глава 5
Синхронизация скорости по
таймеру
Здесь сразу у вас может возникнуть справедливый вопрос. А где же находятся
вызовы функций внешнего контроля и обработки объектов? Ведь в основном
цикле игры их ие было. А вызов их осуществляется в функции TryRunTics
(функция синхронизации по таймеру). И это правильно. Посудите сами.
Большую часть времени программа “проводит” именно в функции TryRunTics.
Зачем же ей бессмысленно тратить время на ожидание, когда можно заняться,
например, обработкой данных управления, т.е. внешнего* воздействия со
стороны играющего. Это резонно. Именно так, с пользой для дела надо
проводить свободное время, так сказать, “сочетать полезное с приятным”.
Пример функции TryRunTics может выглядеть так:
void TryRunTics( void)
!
register int tm_start, tmend;
tm_start = timuser;
tm_end = starttic+2;
if (DebFil) II файл для отладки
fprintf(DebFil, “Frame: %i, Tics to frame: %i\n”, GameFrame, timuser-starttic);
if (tm_end < tm_start) // программа не успевает
tm^start -= tm_end;
while (tm start- )
{
Control(); // заполнение структур управления
ProcessingControl(); // обработка структур управления
if (ChangeDemoMode)
SwitchDemoMode(); И переключение DEMO режима
FlashMenuCursor(), И мигание курсора в меню
GameControl(&PlayerControl); // управление игроком
GeneralControl(); // основные процессы юры
GameTics++;
}
}
else // программа успевает (ожидание)
{
tm_start-;
while (tmend >= timuser)
{
if (tmstart != timuser )
{
tmstart = timuser;
ControlQ; // заполнение структур управления
ProcessingControl();// обработка структур управления
if ( ChangeDemoMode )
Switch DemoModef), И переключение DEMO режима
FlashMenuCursoi(), И мигание курсора в меню
GameControl(&PlayerControl); И управление игроком
GeneralControl(); И основные процессы игры
GameTics++,
}
}
}
starttic = timuser;
Мы сказали - пример, т.к. в DOOM’e эта функция выглядит совершенно
иначе. Она намного сложнее, что прежде всего связано с синхронизацией по
сети. А теперь попытаемся объяснить, как работает данная функция. Основное
ее предназначение - ожидание. Если бы ие было этого ожидания, то иа
компьютерах с процессором типа 486DX2-66 и выше вы бы двигались, как будто
у вас в арсенале имеются сапоги-скороходы (а об оптимальной тактике ведения
боя и говорить не приходится). Если компьютер используется ие слишком
быстрый, то программа без ожидания проходит основные функции ровно
столько тактов, на сколько опаздывает программа. Иначе она ожидает каждый
такт таймера, и по каждому такту, как по бою часов, проходят те же функции.
Финальный такт определяет переменная tm end. О переменной timuser мы
говорили раньше, ио можно повториться. Оиа изменяется в программе
обработки прерываний от таймера с каждым тактом таймера, т.е. с частотой
140 Гц.
А теперь по порядку рассмотрим функции, которые используются в
TryRunTics (рассматриваются в главе 6).
Глава 6
Контроль со стороны играющего
Этот раздел, как мы видели выше, состоит из трех функций.
1. Control - обработка клавиатуры и других устройств ввода (мышь,
джойстик) и заполнение специальных структур управления.
2. ProcessingControl - обработка структур управления, т.е. выполнение
соответствующих действий в зависимости от внешних воздействий и в
зависимости от текущих режимов работы программы.
3. GameControl - эта функция принимает активное участие в самом процессе
игры и отвечает за перемещение игрока в трехмерном пространстве и другие
его действия, которые приводят в трепет его врагов.
Прежде чем приступить к описанию работы вышеперечисленных функций,
необходимо пояснить, что такое структуры управления, и почему мы говорим
о них во множественном числе. Поясню сначала второй пункт на примере
обработки клавиатуры. Допустим, что в тот момент, когда вы усердно нажимаете
на клавиши, надеясь уничтожить очередного обнаглевшего монстра, программа
занята не менее важными делами, например, формированием изображения
лабиринта на экране (точнее, в невидимой его части). За то время, пока она
формирует изображение, теоретически возникает несколько прерываний от
клавиатуры, и их количество напрямую связано с быстродействием вашего
компьютера и, конечно же, с параметрами настройки клавиатуры. Казалось
бы, волноваться не о чем. У любой клавиатуры есть очередь размером в 15
знаков. Однако есть два “но”. Во-первых, нельзя иметь доступ к любому символу
в стандартной очереди команд клавиатуры: они считываются последовательно.
А во-вторых, в некоторых случаях имеется вероятность переполнения
внутреннего буфера клавиатуры. В связи с этим в функции обработки
прерываний от клавиатуры мы заполняем свой буфер клавиатуры (своя очередь)
размером в 32 знака:
Исходный текст обработчика клавиатуры newkeyb:
void__interrupt_far newkeyb ()
1
chare;
c = bin(0x60); // чтение кода клавиши
keycode[LastCountKey++ & Oxlf]=c; // занести код в очередь
curkey = с; // текущий код
bout(0x20,0x20);
}
Но это еще не все. Известно, что в АТ клавиатуре нередко одно нажатие
(или отжатие) клавиши сопровождается серией кодов. Например, при нажатии
на клавишу “Pause” возникает 5 кодов В связи с этим несколько последовательных
кодов можно расценивать, как одну команду. Одна такая команда заносится в
одну' структуру управления. Всего таких структур 64, и оии тоже представляют
собой очередь команд Данные структуры заполняются командами не только
от клавиатуры, но и от других устройств ввода (джойстик, мышь), те. они
универсальны:
typedef struct
{
char type, И тип ус тройства ввода
char code; // код клавиши
char movex; // перемещение по оси х
char move_y; // перемещение по оси у
} CONTROL;
11араметры move x и move_y используются только при управлении джойстиком или
мышкой. Параметр type определяет тип управляющего воздействия (устройства
ввода):
//define CPRESSKEY 0 //клавиша нажата
//define C_RELEASEKEY 1 // клавиша отжата
//define C MOUSE 2 // управление мышкой
//define CJOYSTICK 3 //управление джойстиком.
А теперь рассмотрим функцию Control, которая занимается заполнением
вышеописанных структур, проверяя состояние мышки, джойстика, и анализируя
буфер клавиатуры (наш буфер):
void Control (void)
{
CONTROL pnn;
register int kb 1 ,kb2;
register char ch;
UpdateMouseControl(); // заполнение структуры для мышки
UpdateJoyControlO; // заполнение структуры для джойстика
while ( CurCountKev < LastCountKey )
kbl = (int)keycode[CurCountKey++ & 0x1 f]; // текущий код
kb2 = kbl & 6x7f;
ch = keycode[(CurCountKey-2) & Ox 1 f]; // предыдущий код
if(kb2 == 0x2a|| kb2 == 0x36 )//Pressed ‘Shift’
{
if (ch == OxeO )
continue;
kbl &= 0x80;
kbl |=0x36;
}
il ( kbl == OxeO || ch == Oxel)
continue;
if( kbl == 0xc5 && ch == 0x9d)
pnn. type = 0; // нажатие “Pause”
prm code= PAUSE,
}
else
{
pnn.type = (kbl & 0x80) != 0; И нажатие или отжатие
switch ((kbl & 0x7f))
{
case 0x48: pnn.code = FORWARD;
break;
case 0x50: pnn.code = BACKWARD;
break;
case 0x4b: pnn.code = LEFT;
break,
case 0x4d: prm.code = RIGF1T;
break;
default: pnn.code = tab_char(kbl & 0x7f);
}
UpdateControlI)ata(pnn);
}
}
Первым делом программа заполняет структуру управления для мышки и
для джойстика. Но на них мы останавливаться не будем, т.к. в них ничего
экстраординарного нет. Они стандартны.
Функция Control обрабатывает клавиши последовательно, код за кодом,
начиная с позиции CurCountKey и до позиции LastCountKey. Каждая команда
записывается функцией UpdateControlData в структуру управления:
void UpdateControlData (register CONTROL *dat)
{
move(&ControlData[CountControlData++], dat, sizeof(CON'l ROL));
CountControlData &= 0x3f;
}
Таблица tab char, осуществляет перекодировку кода клавиши в код ASCII и
в некоторые другие специфичные коды. Эта таблица выглядит так:
char tabcharf] =
! 0,0x1b,0x31,0x32,0x33,0x34,0x35,0x36,
0x37,0x38,0x39,0x30,0x2d,0x3d,0x7f,9,
0x71,0x77,0x65,0x72,0x74,0x79,0x75,0x69,
0x6f,0x70,Ox5b,Ox5d,OxOd,Ox9d,0x61,0x73,
0x64,0x66,0x67,0x68,0x6a,0x6b,0x6c,0x3b,
0x27,0x60,0xle,0x5c,0x7a,0x78,0x63,0x76,
0x62,0x6e,0x6d,0x2c,0x2e,0x2f,0xb6,OxeO,
0xb8,0x20,0,0xbb,0xbc,0xbd,0xbe,0xbf,
0xc0,0xcl ,0xc2,0xc3,0xc4,0,0,0xc7,
0xad,0xc9,0x2d,0xac,0x35,0xae,0x2b,0xcf,
0xaf,0xdl ,0xd2,0xd3,0,0,0,0xd7,
0xd8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
о,о,о,о,о,о,о,о
В соответствии с кодировкой через данную таблицу, некоторые клавиши
можно определить так:
//define ТАВ 9
# define CTRL 0x9d
//define ALT 0xb8
//define SHIFT 0xb6
//define PRNTSCR OxeO
//define LEFT Oxac
//define FORWARD Oxad
//define RIGHT Oxae
//define BACKWARD Oxaf
//define ENTER Oxd
//define ESCAPE Oxll?
//define SPACE 0x20
//define END Oxcf
//define HOME 0xc7
//define PGUP 0xc9
//define PGDOWN Oxdl
//define INSERT 0xd2
//define DEL Oxd3
//define DELETE 0x7f
//define Fl Oxbb
//define F2 Oxbc
//define F3 Oxbd
//define F4 Oxbe
//define F5 Oxbf
//define F6 OxcO
//define F70xcl
//define F8 0xc2
//define F9 ОхсЗ
//define Fl 00xc4
//define Fl 1 0xd7
Z/defineF12 0xd8
//define PAUSE Oxff
Обработка заполненных в функции Control структур производится в
функции ProcessingControl:
void ProcessingControl (void)
1
register int m;
register CONT ROL *ad;
m = CurOffsetContr;
while ( m != CountControlData )
{
ad = &ControlData[ni++];
if ( IFuncControl(ad))
OtherControl(ad);
m &= Ox3f;
}
CurOffsetContr = m;
}
Данная функция осуществляет обработку всех структур управления,
начиная с позиции CurOffsetContr до позиции CountControlData Она состоит
из двух частей.
1. FuncControl - это очень обширная функция, которая занимается
обработкой функциональных клавиш и управлением меню. Если
передаваемая в данную функцию структура каким то образом повлияла на
нее, т е передаваемый в функцию код находится в пределах “ее
компетенции”, то функция возвращает единицу. В этом случае функция
OtherControl игнорируется для данной структуры
2. OtherControl - эта функция занимается всем, что не вошло в функцию
FuncControl и по своему объему немного уступает последней.
int OtherControl (register CONTROL *ad)
{
register int m, 1;
register int *i;
if ( 'ChangeMode && (PlayDemo || GameMode == 3))
{ if ( ad->type == CPRESSKEY ||
(ad->type == C_MOUSE && ad->code) ||
(ad->tvpe == C_JOYSTICK && ad->code) )
{
InitMainMenu(); // инициализация основного меню
return 1;
return 0;
}
if( ! GameMode)
(
if ( ControlCheatCodes(ad)) // проверка CHEA! кодов
return 1;
if ( ControlInMapMode(ad)) // управление в режиме карты
return 1;
}
switch ( ad->type )
case C_PRESSKEY: if (ad->code == 255) // нажатие клавший
PressPause = 1;
else
KeyPressed(ad->code] = 1;
break;
case C_RELEASEKEY. KeyPressed[ad->code] = 0; // отжатие клавший
return 0;
case C_MOUSE: i = AdButControlMous; // данные мышки
1(0] = ad->code & 1;
i 1 ] = ad->code & 2;
i[2] = ad->code & 4;
MousDirect=ad-move_x*(mouse_sen+5) /10;
MousMove=ad-move_y*(mouse_sen+5) / 10;
break;
case С JOYSTICK, i = AdBufControlJoystick; И данные джойстика
i[0 =ad->code& 1;
i[l =ad->code&2;
i[2 =ad->code & 4;
i[3]=ad->code & 8;
JoyDirect=ad->mo vex; // HanpaBj tei me
JoyMove=ad->move_y; // перемещение
}
return 1;
Внутри оператора switch данная функция формирует данные для функции
GameControl, вызов которой осуществляется из функции TtyRunTics (смотрите
главу 5). В таблице KeyPressed соответствующий байт равен единице, пока
нажата соответствующая клавиша.
Функция GameControl призвана подготовить данные, необходимые для
управления в самом процессе игры: перемещение в пространстве, стрельба,
смеиа оружия и др. Эти данные представляют собой структуру:
typedef struct
{
signed char Move; // данные перемещения
signed char Strafe; // данные продольного перемещения
short DirControl, // данные направления
unsigned char PressedKevs; II нажатые клавиши
} GAME_CONTROL;
Функция GameControl, анализируя данные, подготовленные в функции
OtherControl для клавииатуры, мышки и джойстика, формирует данную
структуру, адрес которой в нее передается:
void GameControl (register GAME CONTROL *ad)
{
register int straf, speed, moves, dir_rang, strf, o;
stos(ad, sizeof(GAME_CONTROL));
if ( KeyPressed[key_straf] ||
AdBufControlMous[mou_stra] ||
AdBufControlJoystickfjoy straj )
straf = 1; // признак продольного перемещения
else
straf = 0;
if ( KeyPressed[keyspeed] ||
AdBufControlJoystick[joy_spee] )
speed = 1; // признак ускорения
else
speed = 0;
mov = strf = 0;
if ( JoyDirect < 0 || JoyDirect > 0 ||
KeyPressed[key_rig| || KeyPressed[key_lef| )
SpeedDir++, И ускорение поворота
else
SpeedDir = 0;
if ( SpeedDir < 6 )
dir_rang = 2, // малая скорость
else
dir_rang = speed;
if(straf)
if ( KeyPressed[key_rig]) // клавиша вправо
strf += SpeedStraf] speed];
if ( KeyPressed[key_lef] ) // клавиша влево
strf -= SpeedStrafJspeed];
if ( JoyDirect > 0 ) // джойстик вправо
strf+= SpeedStraf]speed];
if ( JoyDirect < 0 ) // джойстик влево
strf -= SpeedStraf]speed];
I
else
{
if ( KeyPressedfkeyrig]) // клавиша вправо
ad->DirControl -= SpeedDir(dir_rang];
if ( KeyPressedfkeylef] ) // клавиша влево
ad->Diitontrol += SpeedDir] dir_rang];
if ( JoyDirect < 0 ) // джойстик вправо
ad->DirControl -= SpeedDir[dir_rang];
if ( JoyDirect > 0 ) // джойстик влево
ad->DirControl += SpeedDir[dir_rang];
}
if ( Key Pressedfkeyup]) // клавиша вперед
moves += SpeedMove] speed];
if ( Key Pressedfkeydown] ) // клавиша назад
moves -= SpeedMove]speed];
if ( JoyMove > 0 ) // джойстик вперед
moves += SpeedMovefspeed];
if ( JoyMove < 0 ) // джойстик назад
moves -= SpeedMove(speed];
if ( KeyPressed[key_strafr] ) // клавиша продольного
strf+= SpeedStraf]speed]; // перемещения вправо
if ( KeyPressed(key_strafl]) // Ю1авиша продольного
strf -= SpeedStraf]speed]; II перемещения влево
if ( KeyPressedfkeyfire] ||
AdBufControlMous(mou_fir] ||
AdBufControlJoystick[joy_fir] )
ad->PressedKeys |= 1; // нажатие Fire (огонь)
if ( KeyPressedfkeyuse] ||
AdBufControlJoystick[joy_us] )
ad->PressedKeys |= 2; // нажатие Use (использовать)
for ( o=0; o<7; o++ ) // выбор оружия
{ // (цифровые клавиши)
if ( KeyPressed[o+0x311 )
{
ad->PressedKeys |= 4; И оружие выбрано
ad->PressedKeys |= o<3; // номер оружия
break;
}
i
।
о = AdBufControlMousJmouforwj; // кнопка мышки вперед
if ( о )
moves += SpeedMove[speed|;
if ( о != PrevPressForw && DelayMous > 1) // также данная кнопка используется
{ // как Use (двойное нажатие)
PrevPressForw = о;
if(o)
CountPressMous++; И счетчик нажатий
if ( CountPressMous == 2 )
{
ad->PressedKeys |= 2; // признак Use
CountPressMous = 0;
}
else
DelayMous = 0; // задержка между щелчками кнопки
}
else
{
DelayMous++;
if ( DelayMous > 20 )
{
CountPressMous = 0; // время между нажатиями вышло
PrevPressForw = 0;
}
if ( AdBufControlMous[mou_stra] || AdBufControlJoystickfjoystra] )
o=l; // кнопки джойстика и мышки “продольное
else // перемещение используются также для
о=0; // открывания дверей (двойное нажатие)
if ( о != PrevPressStraf && DelayJoy > 1 )
{
PrevPressStraf = о; // предыдущее состояние кнопки
if(o)
CountPressJoy++; И счетчик нажатий
if ( CountPressJoy == 2 )
ad->PressedKeys |= 2; // признак Use
CountlYessJoy = 0; // сбросить счетчик нажатий в 0
}
else
DelayJoy = 0; И задержка между нажатиями
else
{
DelayJoy++;
if ( DelayJoy > 20 ) // время вышло
CountPressStraf=0; // счетчик нажатий сбросить в О
PrevPressStraf = 0;
}
moves += MousMove; // перемещение мышки
if ( straf)
strf += MousDirect*2, И продольное перемещение мышки
else
ad->DirControl -= MousDirect«3; // направление (угол)
MousMove = 0;
MousDirect = 0;
if ( moves > 50 ) // ограничение на перемещение сверху
moves = 50;
else
if ( moves < -50 ) // ограничение на перемещение снизу
moves = -50,
if ( strf > 50 ) // ограничение на продольное перемещение
strf = 50; // сверху
else
if ( strf < -50 ) И ограничение на продольное перемещение
strf = -50; // снизу
ad->Move += moves;
ad->Strafe += strf;
if ( I’ressl’ause )
ad-I’ressedKeys = 0x81; // нажатое клавиши “Pause”
PressPause = 0;
}
if ( UseSaveGame )
UseSaveGame = 0; // запись игры (Save Game)
ad-PressedKeys = (CurNumSave « 2) | 0x82;
}
}
Данную функцию мы привели в качестве примера, чтобы было понятно,
как можно сочетать управляющее воздействие от нескольких устройств ввода
одновременно. Функция комментирована, и здесь остается лишь пояснить
некоторые используемые в ней таблицы:
-SpeedStraf - данные приращения скорости для продольного перемещения:
static int SpeedStraf]| = {0x18,0x28};
-SpeedMove - данные приращения скорости для перемещения:
static int SpeedMovef] = {0x19, 0x32};
-SpeedDir - данные приращения скорости для направления (изменение угла):
static int SpeedDirf] = {640,1280,320};
А теперь, прежде чем закончить данный раздел, хотелось бы поговорить о
методе обработке CHEAT кодов и организации работы меню.
6.1. CHEAT коды. Что это такое и как с
ними работать?
Cheat коды (коды обмана) предназначены для облегчения жизни играющего.
Один код может сделать вас “Бессмертным”, другой - дать все имеющееся в
арсенале оружие, третий - дать все ключи, ну и так далее. Сомнительно, что
разработчики игр вводят возможность использования CHEAT кодов в
отладочных целях Так же маловероятно, что эти коды с момента
приобретения коммерческой лицензионной версии игры общедоступны, т.к.
о них в документации ничего не сказано (исключением является лишь игра
Descent. В файле ReadMe для данной игры описаны CHEAT коды) На
данную мысль наводит также то обстоятельство, что в самой игре эти коды
находятся не в явном виде (ASCII коды) - они закодированы. Каким же образом
они кодируются и как осуществляется их проверка? Это очень просто, и мы
покажем, как это делается, на примере проверки кода “IDDQD” - кода бессмертия
в DOOM’e. Как было показано, проверкой CHEAT кодов занимается функция
ControlCheatCodes, вызов которой осуществляется из функции OtherControl. В
данную функцию передается указатель на текущую структуру CONTROL,
которая несет информацию об очередной нажатой клавише. В данной функции
нам нужен только код этой нажатой клавиши. Мы не будем здесь приводить
пример функции ControlCheatCodes. Можно лишь сказать, что в ней коды
проверяются лишь в том случае, если игра не в сети, и если текущий уровень
сложности (Skill Lex el) у вас не четвертый (Nightmare). Проверяет конкретный
код функция CheckCheatCode, в которую передаются два параметра:
1. Адрес на данные проверяемого кода.
2. Текущий код нажатой клавиши. Функция CheckCheatCode возвращает
единицу в том случае, если пароль совпал. Если возвращается 0, то это может
означать, что пароль не совпал, или еще не все символы пароля были
введены, т.к. для того, чтобы пароль совпал, вы должны ввести
последовательно все символы этого кода (в нашем случае их 5). Причем
никакого тайм-аута здесь нет: вы можете один символ пароля ввести сейчас,
а следующий через час, и он будет воспринят. Важно лишь, чтобы между'
этими двумя кодами не прошел еще один посторонний код от клавиатуры
(заметьте, что только от клавиатуры, т.к. другие устройства ввода в паролях
участия не принимают).
Для нашего случая вызов функции CheckCheatCode будет выглядеть так:
if ( CheckCheatCode (&P_IDDQD, ad->code) )
{
... // действия, связанные с данным паролем
}
где ad - теюзцая структура управления; &P IDDQD - указатель на структуру:
typedef struct
char *OffsetCheat, // указатель на адрес пароля
char *CurCheat; //текущая позиция пароля
} CHEAT;
static CHEAT Р IDDQD = {&IDDQD, NULL};
&IDDQD - это указатель на закодированный пароль. Мы здесь его приведем,
а чуть ниже объясним принцип кодирования в DOOM’e:
static char IDDQD[] = {0xb2,0x26,0x26,0xaa,0x26,255},
Код 255 (Oxfi) является терминатором (признак конца пароля). Метод
кодирования пароля заключается в обмене местами битов в байте. Например,
0-ой бит будет 3-им, 1-ый бит будет 2-ым, и т.д. При этом составляется таблица
(256 байт), в которой по соответствующему смещению (например, 1) находится
байт, который является секретным кодом нашего смещения. Те. если выше мы
говорили, что 0-ой бит будет 3-им, то по смещению 1 в данной таблице будет
записано 8 (в двоичном: 00001 000).
А теперь посмотрим, как работает функция CheckCheatCode.
int CheckCheatCode (register CHEAT *ad, register char b)
{
register int g, val, val 1;
vail = 0; // возвращаемое значение
if( CheatFirst)
{
CheatFirst = 0,1I первый вход в функцию
for ( g=0; g<256; g++ ) II сформировать таблицу
{
val = ((g&l )«7)+((g&2)«5); // 0-ой бит будет 7-ым
val += g & 4; // 1 -ый бит - 6-ым, 2-ой - не меняется
val += (g & 8)«1, // 3-ий - будет 4-ым
val += (g & Ox 10)>> 1; // 4-ый - будет 3-ий
val += g & 0x20; // 5-ый - не меняется
val += (g & 0x40 )»5; // 6-ой - будет 1-ым
val += (g & 0х80)»7; // 7-ой - будет 0-ым
CheatSvmbol[g-H-] = val; И занести в таблицу
}
}
if( !ad-CurCheat)
ad-CurCheat = ad-OffsetCheat, И текущую позицию на начало
if ( !ad-CurCheat[O]) И ввод символа без кодирования
{ // используется, например, при вводе
ad-CurCheat[O] = b; // номера уровня
ad-CurCheat++;
}
else
{
if ( CheatSymbol[b| == ad-CurCheat|O]) 11 проверка кода
ad-CurCheat++; И код совпал, указатель увеличить
else
ad-CurCheat = ad-OfTsetCheat, // текущую позицию на начало
}
if ( ad-CurCheat(O] == 1 )
ad-CurCheat[O]++.
else
if( ad-CurCheat(O] == 255 ) // проверка терминатора
ad-CurCheat = ad-OffsetCheat; // ввод пароля завершен
val 1 = 1; // доступ открыт
}
return val 1;
)
Как видим, принцип действия очень прост. Каждый раз при вводе
правильного символа пароля указатель CurCheat увеличивается на единицу и
проверяется на код 255. Если достигнут этот код , то все символы были набраны
правильно. Если же очередной символ пароля не совпал, то указатель
сбрасывается на начало пароля.
6.2. Работа с меню
Заранее оговорюсь: все, о чем здесь будет говориться, будет несколько
отличаться от того принципа, который заложен в игре DOOM. Наш метод проще,
но тем не менее по гибкости и своим возможностям является достойной
альтернативой для технологии, используемой разработчиками игры DOOM.
Вызов меню начинается с функции InitMenu. В нее передается адрес структуры
для конкретного меню. Для примера рассмотрим вызов основного меню
программы - MainMcnu:
InitMenu (&MainMenu);
Функция InitMenu выглядит так:
void InitMenu (register MENU *mn)
{
if ( mn != CurMenu )
{
MenuMode = 1; // режим меню
CurMenu = mn; // указатель на адрес уст ановленного меню
}
}
А теперь рассмотрим основные структуры, которые необходимы для
организации работы с меню. Их две:
1 .Структура меню:
typedef struct
{
short Nums; // количество активных пунктов
MISC ♦msc; // указатель на данные пунктов
void (*GraphFuncXvoid); // функция графического оформления
short ColActive; // цвет активного пункта
short PosX; // нач. координата меню по X
short PosY; // нач. координата меню по Y
short CurPosit; // текущая активная позиция
short CurFlash; // счетчик анимации курсора
} MENU;
-Nums - количество позиций меню. Если меню представляет собой просто
вопрос (например: “Вы уверены, что хотите выйти из программы? Нажмите
Y или N”) , т е. в нем отсутствует перемещение курсора, то Nums равен 0;
-msc - указатель на структуры MISC. Этих структур ровно столько, чем)’
равен Nums;
-GraphFunc - функция, которая отвечает за дизайн меню. Эта функция
вызывается первой при выводе меню на экран. В ней можно, например, красиво
вывести заголовок меню или использовать какую-либо анимацию.
-Col Active - цвет активной позиции, т.е. той позиции, на которой находится
курсор. Этот параметр представляет собой два байта, младший - индекс в палитре,
который стандартно используется для вывода строки меню; старший - новый
индекс в палитре, который используется вместо вышеописанного младшего
байта. Например, если вывод строки осуществляется 8-ым цветом, а активная
позиция должна быть выведена 43 (в шестнадцатеричном исчислении) цветом,
то ColActive равен 0x4308;
-PosX и PosY - координаты, с которых начинается вывод заголовка меню;
-CurPosit - текущая позиция курсора в меню;
-CurFlash - используется для анимации курсора.
2 .Структура для позиций меню:
typedef struct
I
short posX; // координата X
short posY, // координата Y
short numfont, 11 номер шрифта
short ColFirst; 11 цвет горячей клавиши
char *string; // строка
void (*funcXint), // указатель на функцию исполнения
short key, // горячая клавиша
} MISC;
- posX и posY - начальные координаты позиции;
- numfont - номер шрифта, которым выводится строка;
- ColFirst - цвет горячей клавиши Горячая клавиша позволяет быстро, без
предварительного перемещения курсора, исполнить нужный пункт меню.
Принцип установки цвета пояснялся выше;
- string - строка, соответствующая данной позиции;
- fiinc - указатель на функцию исполнения. Вызов данной функции для
соответствующей позиции осуществляется при нажатии на Enter нли на
конкретную горячую клавишу;
- key - горячая клавиша Состоит из двух байт: младший - ASCII код горячей
клавиши; старший - номер позиции в строке. Например, если ваша строка:
"New Game” и вы хотите, чтобы горячей клавишей была буква “w”. то key будет
равен: ‘w’+0x200.
А теперь поясним основные отличия данной системы меню от той системы,
которая используется в DOOM’e. Дело в том, что в DOOM’e вывод позиций
меню осуществляется не как вывод строки, а как вывод изображения, т е. каждая
строка меню представляет собой растровое изображение. Этот подход имеет
несомненные преимущества, т.к. имеет наибольшею гибкость в смысле дизайна.
Нашу систему меню без труда можно превратить в ту; что используется в
DOOM’e, если вместо string (указатель на строку) использовать например namfil
(имя файла) и исключить за ненужностью все определители цветов. Но для нас
не это главное. Вы для себя сами определите, какую из систем использовать
предпочтительней. Главное, чтобы вы уяснили суть.
В качестве примера, для лучшего понимания, определим данные этих двух
структур дая MainMcnu:
static MISC MainMenuSel[] =
{
{160,80,1,0x1330,’’NewMission”, &F_NewMis, ‘n’},
{160,105,1,0x1330,’’Load Mission”, &F_LoadMis, TJ,
{160,130,1,0x1330,’’Save Mission”, &F_SaveMis, ‘s’},
{160,155,1,0x1330,’’Quit Mission”, &F_Quit, ‘q’},
{160,180,1,0x1330,’’Sound Volume:”, &F_Palet, ‘ v’+0x600}
{160,215,1,Ox 1330,’’Messages:”, &F_Messag, ‘m’},
{160,240,1,0x1330,’’Graphic detail:”, &F_GraphDet, ‘g’},
{160,265,1,0x1330,’’Palette level:”, &F_Palet, ‘p’}
MENUMainMenu={8,&MainMenuSel,&F_MainMenu,0x5030,180,44,0,0};
Здесь, мы надеемся, вам сразу стало все ясно, поэтому комментарии
излишни.
Теперь рассмотрим, как осуществляется работа с меню. Обработка меню
включает в себя две функции: ControlMenu - управление в меню и
GraphMenu - вывод меню на экран. Первая из этих функций, о чем мы
рассказывали выше, вызывается из функции FuncControl, в которую
передается текущая структура CONTROL. Вызов функции ControlMenu
осуществляется таким образом:
if ( MenuMode )
ControlMenu(ad);
где ad - указатель на структуру CONTROL. А теперь рассмотрим, как
осуществляется управление в функции ControlMenu:
mt ControlMenu (register CONTROL *ad)
{
register int typ, moves, 1;
register unsigned int m;
register MENU *mn,
register MISC *ms;
typ = ad->type, И тип управляющего воздействия
m = -1; // код клавиши
if (typ == CJOYSTICK && timuser > DelayJoyMenu ) //joystick
{ 11 эмуляция клавиатуры джойстиком
moves = ad->move_y;
if ( moves == -1 )
in = FORWARD; // джойстик вверх
DelayJoyMenu = timuser+5; // задержка автоповтора
}
else
if ( moves == 1 )
{
m = BACKWARD; // джойстик вниз
DelayJoyMenu = timuser+5; // задержка автоповтора
}
}
moves = ad-move_x;
if ( moves == -1 )
{
m = LEFT; // джойстик влево
DelayJoyMenu = timuser+2; // задержка автоповтора
else
{
if ( moves == 1 )
{
m = RIGIГГ; // джойстик вправо
DelayJoyMenu = timuser+2; // задержка автоповтора
}
}
if ( ad->code & 1 )
{
DelayJoyMenu = timuser+5; // задержка автоповтора
tn = ENTER; // кнопка 1 джойстика
}
if ( ad->code & 2 )
{
DelayJoyMenu = timuser+5; // задержка автоповтора
m = DELETE; 11 кнопка 2 джойстика
}
else
if ( tvp == C_MOUSE && timuser>DelayMousMenu ) П mouse
{
1 = MenuMousCurPosY + ad->move_y;
MenuMousCurPosY = 1; // текущая позиция мышки no Y
if ( Menu_MousTargPosY-30 > 1)
{
DelayMousMenu = timuser+5; // задержка автоповтора
Menu_MousTargPosY -= 30; // целевая позиция мышки по Y
Menu_MousCurPosY = Menu_MousTargPosY;
in = BACKWARD; // мышь вниз
}
else
{
if ( Menu MousTargPosY+30 < 1 )
DelayMousMenu = timuser+5; И задержка автоповтора
m = FORWARD; // мышь вверх
MenuMousTargPosY += 30; // целевая iюзиция мышки no Y
Menu_MousCurPosY = Menu_MousTargPosY;
}
}
1 = MenuMousCurPosX + ad->move_x;
MenuMousCurPosX = 1;
if ( Mcnu_MousTargPosX-30 > 1 )
Delay MousMenu = timuser+5; // задержка автоповтора
MenuMousTargPosX -= 30; // целевая позиция мышки по X
MenuMousCurPosX = Menu_MousTargPosX;
m = LEFT; // мышка влево
}
else
if ( Menu_MousTargPosX+30 < 1)
{
DelayMousMenu = timuser+5; И задержка автоповтора
Menu_MousTargPosX += 30; // целевая i юзиция мышки по X
Menu_MousCurPosX = Menu_MousTargPosX;
m = RIGHT; // мышка вправо
}
}
if ( ad->code & 1 ) // правая кнопка мышки
DelayMousMenu = timuser+15; // задержка автоповтора
m = ENTER; // эмуляция Enter
}
if ( ad->code & 2 ) // левая кнопка мышки
{
DelayMousMenu = timuser+15; // задержка автоповтора
m = DELETE; // эмуляция DELETE
}
else
if (typ == CPRESSKEY ) // клавиша нажата
m = ad->code; // код клавиши
if ( m != -1 ) // код воспринят
rrm = CurMenu,
ms = mn->msc;
if ( !mn->Nums ) // если Nums равно 0, то ожидается
{ И нажатие клавиши
if ( m != SPACE && m != ‘n’ && m 1= ‘y’ && m != ESCAPE )
return 0;
if (ms->func)
ms->func(m); // вызов функции исполнения
MenuMode = 0;
FXSoun(NULL, SFX_SWITCH);
return 1;
switch ( m)
case BACKWARD: if ( mn->CurPosit == mn->Nums-l ) И вниз mn->CurPosit = 0; else mn->CurPosit++; // переместить курсор FXSoundfNULL, SFXJLIFTSTOPPING); return 1;
case FORWARD: if ( mn->CurPosit == 0 ) // вверх mn->CurPosit = mn-Nums-1; else mn->CurPosit-; // курсор вверх FXSoundfNULL, SFX_LIFT_STOPPING); return 1,
case LEFT: mn-CurPosit = 0; // курсор в начало FXSoundfNULL, SFX_LIFT_STOPPING); return 1;
case RIGHT: mn->CurPosit = tnn->Nums-l; // курсор в конец FXSoundfNULL, SFX LII'T STOPPING), return 1;
case ENTER: MenuMode = 0; if( ms[mn->CurPozitj.func ) ms[mn->C urPozit]. func( mn->C urPozi t); FXSoundfNULL, SFX_PISTOL_FIRE), return 1;
case ESCAPE: MenuMode = 0; И отмена меню C urMenu->C urPozit = CurPozition; FXSoundfNULL, SFX_SW1TCII); return 1;
case DELETE: InitMenu (&MainMenu); FXSounfNULL, SFX_SWITCH_CLOSING);
} return 1;
for (1=0; l<mn->Nums; 1++ ) // обработка горячих клавиш
if ( m == (ms[l].key & Oxfl))
{
mn->CurPosit = 1;
MenuMode = 0;
if (ms[l].func)
ms[l].func(l);
return 1;
}
}
}
return 0;
}
Что касается функции GraphMenu, too ее работе мы расскажем в главе 10,
где будут обсуждаться функции работы с графикой. В заключение несколько
слов об анимации курсора. Что это такое? Под анимацией понимается
последовательная смена растровых изображений курсора. При этом
наблюдается какой-либо эффект: то ли это плавное изменение тона объекта, то
ли мигание глаз какого-нибудь монстра, или вращение объекта и т.д В структуре
MENU номером текущей картинки является CurFlash. Старший бит в CurFlash
определяет направление анимации. О-в сторону увеличения; 1 - в сторону’
уменьшения. Мы видели, что анимацией курсора занимается функция
FlashMenuCursor, вызов которой осуществляется из функции TryRunTics. Эта
функция очень простая:
void FlashMenuCursor (void)
register int i;
if ( MenuMode )
{
i = CurMenu->CurFlash;
i f (i < 0 ) // старший би г установлен
{ // анимация в обратную cropotry
1 &= 0х7ПГ;
if’(i<0)
i = 0;
else
i |=0x8000;
}
else // старший бит сброшен
{
i++;
if (i == 8 ) // конечный номер изображения
i = 0x8007; // для анимации
}
CurMenu->CurFlash = i;
} }
Этот пример мы привели из того расчета, что всего растровых изображений
для анимации у вас 8. Надеюсь, не нужно объяснять, что для вывода изображения
с этим номером старший бит номера должен быть замаскирован В некоторых
случаях, например, при вращении объекта, обратная анимация (реверс) нс
нужна. В этом случае старший бит ие используется, и в данной функции при
достижении границы (в нашем случае - 8) счетчик CurFlash должен
сбрасываться в 0.
А теперь предположим, что вы в нашем меню выбрали пункт: New Mis-
sion, решив начать игру. При этом, после выбора эпизода и уровня сложности,
в переменную ChangeMode записывается 2, что указывает на начало новой
игры.
С чего начинается игра?...
Глава 7
Начало игры (Загрузка данных)
Игра начинается вызовом функции loadlev:
loadlev(InitCurSkill, InitCurEpisode, InitCurMap);
HE
-InitCurSkill - уровень сложности, начиная с 0;
-InitCurEpisode - выбранный эпизод игры, начиная с 1 (в DOOM’e их 3);
-InitCurMap - уровень в эпизоде. Когда начинается новая игра, InitCurMap
должен быть равен 1.
Любая игра начинается с инициализации (установки в начальное состояние)
данных и загрузке объектов. Начиная с данного раздела мы не будем обобщать
и развивать наш материал на все игры, т.к. все, о чем сейчас будет говориться,
узко специфично только для DOOM’a и ему' подобных игр. И если мы будем
сравнивать тот или иной подход, используемый в DOOM’e, с другими играми,
то эта книга по своему объему’ вполне сможет составить конкуренцию “Войне
и миру” Толстого. Кроме того, это вас только запутает. Итак, теперь разговор
пойдет только о технологии DOOM’a.
Функция loadlev проверяет корректность входных параметров,
инициализирует некоторые переменные и вызывает функцию LoadCurLev:
void loadlev (int skil, int epis, int map)
inti;
if (pause ) 11 если была нажата Пауза,
{ //то паузу сбросить и включить музыку
pause = 0;
MusicOnQ;
}
if (skil < 0) // минимальный уровень сложности: 0
skil = 0,
else
if ( skil > 4) // максимальный уровень сложности: 4
skil = 4;
if( epis < 1 )// минимальный эпизод: 1
epis= 1;
if (epis > 3) // максимальный эпизод: 3
epis = 3;
if (epis > 1 && Shareware Vers ) // в условно бесплатной
epis = 1; // версии игры только один эпизод
if ( map < 1 ) // минимальный уровень- 1
map = 1;
if ( тар > 9 ) // максимальный уровень: 9
тар = 9;
InitRandomQ; // инициализация счетчиков случ. чисел
CurSkill = skil, // текущий уровень
CurEpisode = epis; // текущий эпизод
CurMap = тар; // текущая карта
chuv->PlayerStatus = PS_START_AFTER_DEAD; // статус игрока
SaveRecord = 0; // отсутствует запись демо
PlayDemo = 0; // отсутствует воспроизведение демо
MapMode = 0; // режим карты выключен
PlayMode = 1; // режим игры включен
ChangeSky(epis); // выбор изображения неба в зависимости от эпизода
LoadCurLev(); // загрузка данных
}
Здесь стоит пояснить назначение некоторых функций: InitRandom - эта
функция сбрасывает счетчики случайных чисел в 0. Что это такое и зачем они
нужны? Наверное, излишне объяснять, что в любой игре должен присутстаовать
элемент случайности. Если бы можно было предсказать поведение монстров
или какие-либо другие процессы, то в игру неинтересно было бы играть.
Генерация псевдослучайных чисел может осуществляться по-разному. В
DOOM’e используется табличный метод. Таким образом, имеется таблица, в
которой последовательность байтов своим хаосом напоминает белый шум. У
данного метода есть преимущества и недостатки. Основное преимущество -
это быстродействие, т. к. генерация псевдослучайного числа представляет собой
просто выборку из таблицы. Есть еще одно преимущество. Вспомним, что в
DOOM’e имеется возможность записи и проигрывания DEMO. Если бы
генерация случайных чисел была непредсказуема, то это было бы невозможно,
т.к. при записи и воспроизведении DEMO монстры вели бы себя по-разному: А
в данном случае достаточно перед записью DEMO сбросить счетчик генерации
случайных чисел в 0. И то же самое проделать перед воспроизведением DEMO.
Тогда псевдослучайные числа при воспроизведении будут идти в той же
последовательности, в какой они шли при записи. А теперь приведем
простейшую функцию, которая занимается генерацией псевдослучайных
последовательностей:
int Random 1 (void)
{
CountRandom 1 = (CountRandoml+1 )&0xff; //инкремент счетчика
return (int)ranm[CountRandoml ]; // выборка из таблицы
}
Это первый генератор. Всего их в DOOM’e используется 2. И оба они
используют одну таблицу случайных последовательностей ranm. Разные у них
только счетчики (указатели позициии в таблице гашп). Второй генератор
используется в экранных операциях, при окончании уровня, при работе со
звуком Первый определяет поведение монстров, используется при стрельбе и
в некоторых других случаях.
Инициализация счетчиков очень проста:
void InitRandom (void)
{
CountRandoml = 0, // сброс первого счетчика
CountRandom2 = 0; И сброс второго счетчика
}
Еще одна функциия, нуждающаяся в пояснении - функция ChangeSky. Эта
функция устанавливает изображение неба, характерное для конкретного
эпизода. Небо представляет собой растровое изображение размером 256 на
128. В DOOM’e небо используется без анимации, но можно значительно усилить
эффект, если использовать для неба анимацию. Эта анимация может быть
случайной (например, гроза, молнии на хмуром фоне неба) или
последовательной (например, плавно плывущие по небу облака). Анимация
предполагает использование нескольких растровых картинок для одного неба.
Функция ChangeSky предполагает использование анимации, вследствие чего
она отличается от принципа, заложенного в DOOM’e, в котором нет анимации
дтянеба.
voidChangeSkv (register int lev)
{
register int i, 1;
char nam[9]:
lev-;
movzer(nam, sky[lev].name); // имя файла для неба в nam
1 = scasb(nam); //длина строки
nam[l+l] = 0;
for (i=0; i<sky[lev].nums; i++ )
{ // к имени файла в конец добавляются цифры
nam[l] = i+’O'; // для конкретного номера аним. изображения
NumSkys[i] = TextureNumForName( nam);
!
NumSkys[i] = -1; // таблица номеров заканчивается -1
NumSky = NumSkysfO]; И текущий номер изображения неба
TypeAnimSky = sky|lev]type; II тип анимации
DelaySky = skyflev].delay; // задержка анимации
!
После обработки данной функции в таблице NumSkys содержатся номера
растровых изображений в каталоге файла ресурсов. При работе программы по
определенному закону эти номера из таблицы заносятся в переменную NumSky;
которая определяет текущее изображение неба. Закон, по которому эти номера
извлекаются из таблицы, определяется переменной TypeAnimSky:
0 - случайный закон;
1 - последовательный закон;
DelaySky определяет задержку, те. через какое количество циклов игры
обновлять изображение неба. Этот параметр участвует только в случае
последовательной смены изображений. При случайном законе и задержка, и
номер картинки случайны. Структуру, которая используется в функции
ChangeSky для игры DOOM, можно определить так:
struct sky
{
short nums, // количество изображений анимации
short type; // тип анимации
short delay; // задержка анимации
char ’name; II ключевое имя для аним. изображений
} sky[] =
f { 3,0,0, “SKY1” },
{ 5, 1,2, “SKY2”},
{ 1,0,0,“SKY3”}
Если параметр nutns равен 1, то это означает, что анимация отсутствует.
Таким образом, для первого эпизода используется случайная анимация неба
(гроза) с тремя картинками, для второго эпизода - последовательная анимация
(плывущие облака) с 5 картинками, а для третьего эпизода анимация отсутствует.
А теперь мы пришли к функции LoadCurLev. Эта функция вызывает
функцию загрузки объектов уровня и сбрасывает все буфера управления:
void LoadCurLev (void)
{
register mt i;
GameMode = 0;
LoadObjLev (CurEpisode,CurMap);
StartLevelTimerTic = timuser;
ChangeMode = 0;
CheckHeapO;
stos(KeyPressed, 256);
JoyMove = 0;
JoyDirect = 0;
MousMove = 0;
MousDirect = 0;
pause = 0;
UseSaveGame = 0;
PressPause = 0;
stos(AdBufControlMous, 4),
stosfAdBufControlJoystick, 4);
}
// режим игры
// загрузка объектов
// начальное сост. таймера
// больше никаких изменений
//проверка системы распред, памяти
// очистить таблицу нажатых клавиш
// сбросить перемещ. джойстика
// сбросил, перемещ. мышки
// выкл. режим паузы
Здесь вам должно быть все понятно, т.к обо всех переменных, исполыуемых
в этой функции, мы уже рассказывали. Поэтому сразу перейдем к функции
LoadObjLev. Сразу оговорюсь: вам наверняка в этой функции не все сразу
будет ясно, т.к. в ней есть ссылки на объекты, о которых мы еще ие рассказывали.
Но нельзя обо всем рассказать сразу. В дальнейшем в процессе изложения вам
все станет ясно. Здесь я поясню, что структура chuv является данными игрока
(chuv от слова “чувак”). В DOOM’e используется 4 таких структуры, потому
что, если вы не забыли, в DOOM можно играть по сети вчетвером. Однако мы
уже говорили о том, что все сетевые “примочки” будем пропускать. О данной
структуре подробней мы расскажем позже.
В первом DOOM’e первый уровень начинается с имени Е1М1, где первая
цифра определяет номер эпизода, а вторая - номер карты в этом эпизоде.
Последний уровень начинается с имени ЕЗМ9. После названия уровня
последовательно идут 10 имен блоков данных (ресурсов), определяющих уровень.
Их последовательность в каталоге всегда постоянна. Вот они: THINGS, L1NEDEFS.
SIDEDEFS, VERTEXES, SEGS, SSECTORS, NODES, SECTORS, REJECT,
BLOCKMAP.
Загрузка этих объектов в DOOM’e идет не в той последовательности, в
которой они расположены в каталоге. Разговор же о них мы поведем в
последовательности наилучшего понимания, чтобы разговор о неизученных
(неизвестных) объектах свести к минимуму.
void LoadObjLev(int epis,int map)
char strl| 5];
register int nmlev;
inti;
THING ♦ t;
Secret = 0;
Items = 0;
LivingThings = 0;
chuv->st. items = 0;
chuv->st. secrets = 0;
chuv->st.kills = 0;
chuv->LevelOf! lead = 1;
И количество секретов всего
И количество предметов всего
// количество “живых” объектов всего
// сброс статистики для игрока
// количество найденных предметов
// количество обнаруженных секретов
// количество убитых за игру
NewMusic ();
FreeZone(50, 99);
InitThings ();
strl
strl
strl
strl
strl
[0
1
2
3j
41
= ‘Е’;
= epis + ‘О’;
= ‘М’;
= тар + ‘О’;
= 0;
// Включить музыку для данного уровня
И очистшъ память с 50 по 99 регион
// Инициализация таблицы объектов
// формирование имени для уровня
// например, Е1М4
nmlev = getnum (strl);
LBlockMap (nmlev+10);
LVerTexes (nmlev+4),
L_Sectors (nmlev+8);
L_SideDefs (nmlev+3);
LLineDefs (nmlev+2);
LSSectors (nmlev+6);
L_Nodes (nmlev+7),
L_Segs(nmlev+5);
// порядковый номер в файле ресурсов
И загрузка BLOCKMAP
И загрузка VERTEXES
// загрузка SECTORS
И загрузка SIDEDEFS
// загрузка LINEDEFS
И загрузка SSECTORS
И загрузка NODES
// загрузка SEGS
areject= ReadLumpForNum(nmlev+9,50); И загрузка REJECT
GroupLines ();
L_Things (nmlev+1),
StartSectors();
if (Refreshlext)
RefreshTextures();
// сгруппировать все линии
// загрузка THINGS
//инициализация спец. секторов
// загрузка всех текстур
7.1. Ресурс THINGS. ( Первый объект )
Этим ресурсом представлены все предметы в игре с различными
свойствами: “живые” и “неживые”, движущиеся и неподвижные, летающие и
нелетающие и т.д. К ним относятся разные монстры, светильники, оружие,
экипировка и многое другое.
Каждый объект в ресурсе THINGS состоит из десяти байт, поделенных на 5
значений типа short:
(1) - координата X объекта;
(2) - координата Y объекта;
(3) - угол поворота объекта. В автоматической карте 0 соответствует
направлению на восток, 90 - на север, 180 - на запад и 270 - на юг. Это
значение используется только для монстров, игроков и пунктов прибытия
через телепортацию. Для остальных предметов утол значения не имеет. Они
со всех сторон одинаковы.
(4) - тип объекта (смотрите раздел 7.1.1);
(5) - атрибуты объекта (смотрите раздел 7.1.2).
7.1.1. Типы объектов
Тип объекта, который в ресурсе THING представлен 6,7 байтом,
является индивидуальным номером для каждого предмета или объекта. В
DOOM’e их больше сотни Поэтому мы ие имеем возможности здесь всех их
Дес. Ц1 ести . С п р а й т П о с л е д . С в О писание
1 0 0 0 1 PLAY + Игрок 1
5 0 0 0 5 В К Е Y а b S Синий ключ
6 0 0 0 6 Y К Е Y а b S Ж елтый ключ
7 0 0 0 7 S Р I D * • SPIDER DEMON: Гигантские ходжчие мозги
1 4 О 0 0 е М аркировка позиции, куда или откуда игрок или моистр перемет аю тса при помощи телепортера
4 9 0 0 3 1 G О R 1 в b с b • Подает а н и а я к потолку жертва
5 5 0 0 3 7 S М ВТ abed ♦ К оротка синив свеча
2 0 14 0 7 d е В О N 1 a b с d с b ! П р и б а в к а к силе как прем ня
перечислить, да это и не к чему. В качестве примера приведем несколько
объектов с различными свойствами:
Список сокращений:
Послед. - (Последовательность):
“+” -означает сложное спрайтовое устройство, характерное для игроков и
монстров, которое мы обсуждали в главе 3. Имеется в виду много спрантов
и различные виды в зависимости от угла зрения.
- означает отсутствие визуального вида;
“а” и следующие за ним символы латинского алфавита обозначают
последовательность анимации:
- если ab - мнгание объекта (2 кадра);
- если abed - повторяющаяся анимация (при этом после четвертого
кадра анимация начинается с первого);
- если abeb - повторяющаяся анимация (при этом после четвертого
кадра анимация идет в обратную сторону, начиная с третьего) и т.д.
Св. -(Свойства)
- Препятствие. Тип объекта, через который нельзя перешагнуть, или
пройти сквозь него;
“S” -Формальный предмет, который может “взять” игрок;
ИЛ” -Подвешенный на потолке, или летающий (если монстр);
“!” -Артифакты, подсчет которых ведется и выдается в качестве статистики
при окончании уровня.
7.1.2. Атрибуты объектов
Атрибуты объекта в ресурсе THING представлены последними двумя
байтами. Вот они, в соответствии с установленным битом в данном поле ресурса
THING.
- бит 0 объект присутствует на уровнях сложности 1 и 2;
- бит 1 объект присутствует на уровне сложности 3;
- бит 2 объект присутствует на уровнях сложности 4 и 5;
- бит 3 Указывает на “глухого охранника”.
- бит 4 объект присутствует только в сетевой игре.
- биты 5-15 значения не имеют.
Ранжирование объектов по уровню сложности игры (Skill) характерно
только для монстров. Если установлен атрибут 07/0f - это означает, что
данный монстр присутствует на всех уровнях сложности; 06/0е - на 3,4 и 5
уровнях; 04/0с - только на 4 и 5 уровнях сложности.
Атрибут “глухого охранника” имеет значение только для монстров.
Если установлен этот атрибут, то монстр будет в состоянии покоя, пока не
увидит игрока, или пока игрок сзади не всадит в него пулю или изрядный заряд
картечи. Если атрибут “глухого охранника” сброшен, то монстр может
“активизироваться” при шуме выстрела. Звук не может проникать через плотные
стены. Такие стены имеют специальный атрибут. Данный атрибут используется
для проверки активизации монстра от шума.
Те свойства, которые мы обсуждали, являются далеко не полным их списком
для объектов. В DOOM’e имеется внутренняя структура, которая отражает все
параметры и особенности объекта Об этой структуре мы поговорим чуть
ниже. А сейчас несколько слов о параметрах объектов, о которых мы еще нс
говорили. В качестве примера мы приведем таблицу, в которой отражены эти
параметры доя некоторых объектов (Для всех объектов данные параметры
Номер о б ъ е кт £ Радиус Вы сота Скорость Энергия Масса К атегория оСь актов
1 6 3 6 1 0 0 1 0 0 ИГРОК
7 1 2 8 1 0 0 1 2 3 0 0 0 10 0 0 Д.мок-паук
9 2 0 5 6 8 3 0 1 0 0 Б и । ш * сер ж в я т
1 6 4 0 1 1 0 1 6 4 0 0 0 J 0 0 0 С уЬ ег-Д ем о к
J 8 3 0 5 6 8 1 3 0 4 0 0 П р я зр *i, я ев к д н м ы й демон
3 0 0 1 2 0 5 6 8 б 0 1 0 0 коричневых о гм е к и ы х шаров
3 0 0 2 3 0 3 6 8 1 3 0 4 0 0 Демок
3 0 0 3 2 4 6 4 8 10 0 0 10 0 0 Барон Ад»
3 0 0 4 2 0 3 6 8 2 0 1 0 0 Бывши* р »до во а
3 0 0 3 3 1 S « 8 4 0 0 4 00 С всо d ещ ев (краевая летающая одно гл а 1в я голова)
3 0 0 6 1 6 3 6 8 1 0 0 5 0 Lost юи 1,
2 0 3 5 1 0 4 2 2 0 1 00 Бочка с отколам к
2 0 1 6 Все предметы которые можно взять
11 1 б Болью к и с т в о
5 4 3 2 1 « Б о л ь ш о е коричневое дерево
привести невозможно из-за их огромного количества ). В этой таблице
использованы следу тощие параметры:
Радиус - радиус объекта. Этот параметр необходим для контроля
столкновений и входа в сектор. Если размер сектора по горизонтали меньше
или равен двойному радиусу объекта, то данный объект не может войти в этот
сектор (например, очень узкий коридор). РАД определяет ширину объекта.
Высота - высота объекта. Этот параметр используется для проверки
сближения с движущимися разрушающими потолками (вертикально
перемещаемые платформы), контроля столкновений и проверки входа в сектор.
Если высота сектора меньше высоты объекта (монстра или игрока), то последний
не может войти в этот сектор.
Скорость - Скорость монстра;
Энергия - Сила объекта. Определяет, насколько сложно убить этого монстра
или игрока, или насколько сложно разрушить какой-то объект. Например, у
“сержанта” сила меньше, чем у CYBER демона. Поэтому и “убить” первого
гораздо легче, чем второго.
Масса - Масса объекта. Это параметр определяет, на какое расстояние
отшвырнет объект при попадании в него снаряда с определенной убойной
силой.
7.1.3. Структура "sprites”
А теперь, как мы обещали, представляем вам структуру; которая отражает
все свойства объекта:
struct
{
sprites
int ExtThing; // внешний номер объекта (четвертое поле в THING ресурсе)
int ofThinQuiet; II состояние покоя для монстра
int power, И сила объекта
int ofThinActiv;
int S_InitSpr;
int DelayAgr;
//активное состояние (ходьба, преследование)
И звук инициализации (для монстров)
// задержка агрессивности
int SAttackWithoutFire; И звук ближнего боя (без огнестрельного оружия)
int ofHitlnThing;
int SensibilityOfHit;
intS HitlnThing;
int oTNearAttack;
int ofFarAttack;
int olDeadThing;
// попадание в этот объект снаряда (пули)
// вероятность попадания снаряда
// звуковая реакция объекта на попадание
И ближний бой (без использования огнестрельного оружия
// дальний бой (с использованием огнестрельного оружия
И смерть монстра или игрока
int offixplodeThing;
int SEndThing;
int Speed,
int Dimension;
int Height;
II взрыв объекта
И предсмертный вопль (или звук взрыва)
// скорость монстра
// радиус объекта
И высота объекта
int MassOfThing,
int StrikeHealth;
int S_TalkOfThing;
int Options;
//вес объекта
И убойная сила снаряда (только для снарядов.)
И звук разговора монстра
// атрибуты объекта (опции)
В этой структуре те поля, которые начинаются символами “S
обозначают номер SFX звука в конкретной ситуации. Поля, начинающиеся
символами “of’ обозначают стартовый номер структуры thin, которая с
данной позиции определяет поведение объектов и их анимацию. Ниже мы
покажем, как можно “оживить” монстров с использованием структуры thin
на конкретных примерах, а сейчас расскажем о последнем поле в структуре
sprites - Options. Оно определяет основные аспекты поведения монстров на
том или ином этапе работы игры, визуальное восприятие объектов и многое
другое. Эти атрибуты являются внутренними в игре. Они могут меняться
(устанавливаться или сбрасываться) в зависимости от той или иной ситуации.
Вот эти атрибуты (ТА - означает: Thing Attribute):
Adeline TAGETTABLE 1 И предмет можно взять
Adefine TA_NO_JUMP_OVER 2 // через объект нельзя перешагнуть
Adeline TA LIVE 4 И “Живой” объект (можно убить или взорвать)
Adeline TA_DEAF_GUARD 0x20 II “глухой” охранник
Adeline TA_ATTACK_GUARD 0x40 // Признак: монстр атакует
Adeline TA_HIT_TO_THING 0x80 // Признак, попадание в объект
Adeline TA_HANGING 0x100 // Подвешенный к потолку объект
Adeline TA_NO_FALL 0x200 И объект не падает (характерно для снарядов и
//летающих монстров)
Adeline TA_CAN_JUMP_OVER_WALL 0x400 // Для всех движущихся
// объектов (монстры, игроки, снаряды) означает, что для
// объекта не является препятствием пропасть больше 24
// высотой. Для монстра этот атрибут сброшен, пока его не
// убили (когда его убьют, он может свалиться в эту пропасть).
Adeline TA_CAN_TAKE_THING 0x800 // это объект, который
// может брать предметы (только игрок)
Adeline TANOCLIPPING 0x1000 И объект может проходить
// сквозь стены и препятствия
Adeline TA_FLYING 0x4000 И летающий монстр
Adefine TA_CAN_CLIMB_ON_HIGH_PLAT 0x8000 // Для всех
И движущихся объектов (монстры, игроки, снаряды)
// означает, что, для объекта не является препятствием
// платформа, которая больше 24 выше текущей координаты
// объекта по Z. В DOOM’e этот атрибут проверяется, но
// нигде не ус танавливается
Adeline TA M1SSILE 0x10000 // объект является снарядом
Adefine TA_AFTER_DEAD_ENEMY 0x20000 И это тот предмет,
// который оставляет монстр после своей гибели
// например, “Сержант” оставляет дробовик
Adefine TA FUZZ 0x40000 // невидимый монстр (“Призрак”)
Adefine TANOBLOOD 0x80000 // если данный атрибут
// установлен, то попадание в этот объект дроби аналогично
// попаданию в стену (без крови). В противном случае
// попадание влечет за собой мелкие “капельки крови”
Adefine TA_MONSTER 0x400000 II объект является монстром
Adefine ТАГТЕМ 0x800000 // объект является артифактом
Adefine TA_COLLISION_SOUL 0x1000000 // этот атрибут
// присутствует только для объекта “Lost Soul” (потерян-
// ная душа) и его установка говорит о том, что во время
И атаки “Lost Soul” наткнулся на препятствие (стену или
// другой объект
Adefine TASMOKE 0x10000000 // Прозрачный объект, объект
// визуально выглядит, как туман или дым, через который
// все просвечивает. В DOOM’e не используется данный
И атрибут. Но в игре HERETIC подобные эффекты есть.
7.1.4. “Оживление” объектов (анимация)
А теперь можно подвести итоги и для разминки попытаться оживить
пару монстров, положить какой нибудь предмет (например, синий ключ),
изобразить горение свечи и имитировать полет снаряда.
Для этих пяти пунктов определим структуру sprites, где, как вы помните,
отражены все параметры и аспекты поведения объекта:
struct sprites Thlnfof] =
{
{ //5 SPECTRE “SARG” (невидимый демон)
0x3A,SF_SARG,150,SF_SARG+2,SFX_E£MON_SroT_PLAYER,8,
SFX_DEMON_CLAWING_PLAYER,SF_SARG+13,180,
SFX_DEMON_PAIN,SF_SARG+10,0,SF.SARG+15,0,
SFXDEMONDYING, 10,INT_TO _]TXED(30),
INT_TO_FIXED(56),400,0,SFX_DEMON_NEARBY,
TA_NO_JUMPjOVER | TALIVE | TAMONSTER | TA FUZZ
{ //6 CACODEMON “HEAD” (красная одноглазая летающая голова)
0xBBD,SF_HEAD,400,SF_HEAD+1,
SFX_CACODEMON_SPOT PLAYER,8,0,SF_HEAEH5,128,
SFX_DEMON_PAIN,0,SF_flEAD+2,SF_HEAD+8,0,
SFX_CACODEMON_DYTNG,8,INT_TO_FIXED(31),
INT_TO_FIXED(56),400,0,SFX_DEMON_NEARBY,
TA_NO_JUMP_OVER | TA LIVE | TA_MONSTER | TA_NO_FALL
| TA_FLYING
}.
{ //27 Синий ключ “BKEY”
5, SFBKEY,1000,0,0,8,0,0,0,0,0,0,0,0,0,0,
INT_TO_FIXED(20),INT_TO_FIXED( 16), 100,0,0,
TAGETTABLE
},
{ //70 Короткая синяя свеча “SMBT”
0x37, SFSMBT, 1000,0,0,8,0,0,0,0,0,0,0,0,0,0,
INT_TO_FIXED( 16),INT_TO_FIXED( 16), 100,0,0,
TA_NO_JUMP_OVER
{ //15 Rocket missile “MISL”( 102)
-1 ,SF_MISL,1000,0,SFX_ROCKET FIRE,8,0,0,
o,o,o,o,sf_mise,o,sfx_barrel_‘Exploding,
INT_TO_FIXED(20),INT TO_FEXED( 11 ),INT TO FIXED(8),
100,20,0,TA_AT10 | TA_tTO_FALL |
TA_CAN_JUMP OVER WALL | TA MISSILE
}
};
Все, что мы определили в этой структуре для конкретных объектов, не
является вымыслом. Это все “подлинное”, т.е. взято из игры DOOM1 “один в
один”.
Как видно выше, скорость снаряда представлена числом с фиксированной
точкой, а скорость других объектов целым числом. Это прежде всего связано
с различиями в методах их обработки.
Здесь значения, начинающиеся символами “SFX ” определяют номер
SFX, звука. А те значения, которые начинаются с “SF_” (Sprite Frame),
определяют стартовый номер некой структуры thin в соответствии с
конкретными действиями, которая определяет анимацию. Одна структура
thin несет информацию об одном кадре, а их совокупность позволяет
“оживить” объект. Структура thin представлена таким образом:
struct thin
{
unsigned int sprite; // номер спрайта
int frame; // номер кадра
int count; // задержка данного кадра
union func fn; // функция обработки и контроля
int next; // следующий кадр (эточюмер следующей струк туры thin для анимации)
int pozX; // эти координаты используются в процессе
// игры для отражения текущих координат кадра
int pozY;
};
union func
{
void (*fnWeap)(MAN *, struct weapon *); // функция обработки
// для оружия, которое находится в руках игрока
void (*fnSpr)(THING *); // функция обработки всех других объектов
};
Прежде всего, чтобы придать законченность нашим действиям, определим
данные этой структуры для вышеописанных пяти объектов:
struct thin th[] =
{
{0,0,-1,NULL,SFNONE,0,0},
{29,0,10,&RestEnemy,SF_SARG+l ,0,0},
{29,1,10,&RestEnemy,SF_SARG,0,0},
{29,0,2,&ActiveEnemy,SF_SARG+3,0,0},
{29,0,2,&ActiveEnemy,SF_SARG+4,0,0},
{29,1,2,&ActiveEnemy,SF_SARG+5,0,0},
{29,1,2,&ActiveEnemy,SF_SARG+6,0,0},
{29,2,2,&ActiveEnemy,SF_SARG+7,0,0},
{29,2,2,&ActiveEnemy,SF_SARG+8,0,0},
{29,3,2,&ActiveEnemy,SF_SARG+9,0,0},
{29,3,2,&ActiveEnemy,SF_SARG+2,0,0},
{29,4,8,&SetAngEnemy,SF_SARG+l 1,0,0},
{29,5,8,&SetAngEnemy,SF_SARG+12,0,0},
{29,6,8,&Shoot_Demon,SF_SARG+2,0,0},
{29,7,2,NULL,SF_SARG+14,0,0},
//Demon SF SARG
// состояние покоя (0)
// активное состояние (2)
// ближняя атака (10)
// попадание в демона (13)
{29,7,2,&SoundHitInThing,SF_SARG+2,0,0},
{29,8,8,NULL,SF_SARG+16,0,0}, // умирание демона (15)
{29,9,8,&SoundDeadEnemy,SF_SARG+17,0,0},
{29,10,4,NULL,SF_SARG+l 8,0,0},
{29,11,4 ,&CanJumpOver Thing,SF_SARG+19,0,0},
{29,12,4,NULL,SF_SARG+20,0,0},
{29,13,-1 .NULL, SF_NGNE,0,0},
//Fly head SFHEAD
{30,0,10,&RestEnemy,SF_HEAD,0,0}, // состояние покоя (0)
{30,0,3,&ActiveEnemy,SF_HEAD+l ,0,0}, // активное состояние (1)
{3O,l,5,&SetAngEnemy,SF HEALH-3,0,0}, //атака(2). Огн. шары
{30,2,5,&SetAngEnemy,SFFIEAD+4,0,0},
{30,0x8003,5,&Shoot_Cacodemon,SF_HEAD+l ,0,0},
{30,4,3,NULL,SF HEAD+6,0,0}, //гюладанисвмонстра(5)
{30,4,3,&SoundHitInThing,SF HEADt-7,0,0},
{30,5,6,NULL,SF_HEAD+l ,0,0},
{30,6,8,NULL,SF_HEAD+9,0,0}, // умирание
{30,7,8,&SoundDeadEnemy,SF_HEALHl 0,0,0},
{30,8,8,NULL,SF_HEAD+l1,0,0},
{30,9,8,NULL,SFHEALh-12,0,0},
{30,10,8,&CanJumpOverThing,SF_HEAD+13,0,0},
{30,11,-1,NULL, SFNONE,0,0},
// Blue keycard SFBKEY
{42,0,10,NULL,SF_BKEY+1,0,0}, // мигание ключа
{42,0x8001,10,NULL,SFBKEY,0,0},
// Short blue firestick SF_SMBT
{102,0x8000,4,NULL,SF_SMBT+1,0,0}, //горение свечи
{102,0x8001,4,NULL,SF_SMBT+2,0,0},
{102,0x8002,4,NULL,SF_SMBT+3,0,0},
{102,0x8003,4,NULL,SF_SMBT,0,0},
// Rocket missile SFJvflSL
{22,0x8000,1,NULL, SF JvflSL, 0,0},
};
Как видно выше, все объекты, кроме снаряда, со временем изменяются. У
ннх сменяются кадры. Летящий снаряд же до тех пор, пока не встретит
препятствие, изменений не претерпевает.
У некоторых из объектов в поле frame установлен старший биг. Это означает,
что для данного кадра используется максимальная яркость. Если бит сброшен,
то яркость для объекта устанавливается в соответствии с окружающей
освещенностью и по правилам перспективного затенения (т.е. в зависимости
от удаления от игрока).
А теперь, как бы подводя черту, покажем, как определить смещения на
структуру thin для конкретных объектов, которые мы использовали в структуре
sprites:
Adeline SF_NONE 0
Adeline SF_SARG SF_NONE+1
Adeline SF_HEAD SF_SARG+21
Adeline SFBKEY SF_HEAD+14
Adeline SF_SMBT SFBKEY+2
Adeline SFJvflSL SF_SMBT+4
Как видите, все очень просто. Смещение для следующего объекта
вычисляется как сумма номера предыдущего объекта и количество структур
thin, которыми этот объект определен.
При определении анимации объектов в структуре thin мы использовали
указатели на некоторые функции. Об этих функциях конкретно и подробно мы
поговорим, когда речь пойдет об обработке объектов. А сейчас мы объясним
весь принцип работы данной системы. Дело в том, что вы получили сразу
слишком много информации, и переварить всю эту информацию без
подкрепления какими-то конкретными примерами очень сложно. Здесь мы не
будем приводить примеры конкретных функций. Как мы уже сказали,
конкретный разговор об этом пойдет при обработке объектов. Здесь мы лишь
объясним алгоритм. Этот алгоритм для всех объектов одинаковый. И в этом
заключается основное преимущество технологии DOOM’a - практически
абсолютная универсальность обработки объектов. В этом вы могли убедиться
сами, когда видели, что в структуре sprites для двух разных монстров на
определенных этапах поведения вызываются одни и те же функции. Но не
будем забегать вперед. Итак..
В самом начале монстр находится в состоянии покоя (ofThinQuiet) При
этом он может находиться либо без движения (как летающая голова), либо
просто переставляет ноги (как демон). Но неизменно одно - монстр при этом
не перемещается в пространстве. Как мы видели выше, в состоянии покоя для
всех демонов вызывается функция RestEnemy. Эта функция в основном
необходима, чтобы определить фактор, который бы вывел монстра из состояния
покоя.
Этих факторов несколько:
- шум выстрела (если монстр не глухой);
- вид игрока: если монстр увидит игрока, то выходит из состояния покоя
(разумеется, сзади глаз у монстров нет, и увидеть игрока он может только
находясь к нему лицом. Сам же игрок необязательно находится к монстру
лицом);
- и еще один фактор - монстр активизируется, если в него попадет снаряд
или пуля (при этом неважно, кто в него выстрелил, свой или чужой).
Последний фактор в ряде случаев приводит монстра ие в активное
состояние, а в состояние смерти, но об этом позже. Когда монстр активизируется
(ofThinActiv), он издает восклицание, или вопль (кому как больше нравится),
которое напрямую связано с полем S InitSpr в структуре sprites. В активном
состоянии у монстра в голове присутствует только одна мысль: как бы
“замочить” игрока. И в этом ему помогает функция ActiveEnemy, которая
постоянно вызывается в активном состоянии. Эта функция определяет уровень
интеллекта монстра, если так можно выразиться. В активном состоянии монстр
при помощи функции ActiveEnemy постоянно ищет игрока, преследует его и,
если последний обнаружен, то атакует его. Для разных монстров методы атаки
различны. Некоторые используют только ближний бой (ofNearAttack), как
Демон, некоторые только дальний бой (ofFarAttack), как летающая голова
(которая плюется огненными шарами), некоторые используют и то и другое в
зависимости от расстояния до игрока, как “Барон ада”. В начальный момент
атаки вызывается функция Set AngEnemy, которая направляет оружие монстра
на игрока (устанавливает угол), т.е. позволяет монстру точно прицелиться.
На завершающей стадии атаки вызывается функция, которая индивидуальна
для каждого монстра и зависит от методов его борьбы. Если в монстра попадает
снаряд, но этого недостаточно, чтобы убить его (ofHitlnThing), то он корчится
от боли и издает вопль (S HitlnThing). После попадания в монстра снаряда он
немедленно переходит к атаке, что вполне логично. Если исход поединка
разрешается в пользу игрока, то монстр с предсмертным стоном, который
определен полем S EndThing в структуре sprites, погибает (ofDeadThing). У
некоторых из монстров в DOOM’e смерть представлена довольно живописно.
Это выглядит, как разлетание его на кусочки (ofExplodeThing). Эта незавидная
участь постигает монстра или игрока в том случае, если убойная сила попавшего
в него снаряда более чем в,, два раза превосходит его собственную энергию
(силу').
После убийства монстра чаще всего последний не исчезает, а остается
лежать там, где его настигла шальная пуля. При этом на последней стадии
умирания вызывается функция CanJumpOverThing, которая у этого объекта
сбрасывает атрибут TA NO JUMP OVER, что означает, что этот объект больше
не является препятствием и через него можно перешагивать (а ведь при жизни
через монстра нелья было перешагнуть или пройти сквозь него).
7.1.5. Загрузка объекта THINGS
А сейчас мы, собственно, пришли к тому, с чего начали - к функции
L Things, которая загружает объекты:
struct WadThingFile
{
short PosX; // координата X объекта
short PosY; И координата Y объекта
unsigned short angl; // угол поворота объекта
unsigned short type; // тип объекта
unsigned short attr, // атрибуты объекта
};
void L_Things (int num)
{
register struct WadThingFile *thin;
register int iliums;
thin = (struct WadThingFile *)ReadLumpForNum(num, 1);
//чтение
// ресурса THINGS
// количество объектов
nums = lumplen(num)/ sizeof( struct WadThingFile);
for ( i=0; i < nuins; i++ )
{
SpawnMapThing (&thin[i]); // создать объект
}
zfree (thin); // освободить память
}
Функция L Things в цикле вызывает функцию SpawnMapThing, которая
создает карту объектов. Прежде чем приступить к обсуждению функции
SpawnMapThing, необходимо разобраться, что из себя представляет карта
объектов (MapThings). Карта объектов должна быть построена таким образом,
чтобы в любое время можно было быстро добавить объект или уничтожить
его. В этом смысле эта структура напоминает систему распределения памяти.
Карта объектов представляет собой цепочку, одним звеном которой является
структура THBIND:
typedef struct thing_bind TH BIND;
struct thing bind
{
TH BIND * prev; // указатель на предыдущий объект
TH BIND * next; // указатель на следующий объект
};
Как видите, все очень просто. Началом цепочки объектов является
переменная ChainOfThings (структура TH BIND). В этом случае инициализация
цепочки объектов будет выглядеть так (вызов этой функции осуществлялся
перед загрузкой объектов в функцйи LoadObjLev):
TH BIND ChainOfThings;
void InitThings (void)
ChainOfThings.prev= &ChainOfThings;
ChainOfThings.next = &ChainOfThings;
}
Чтобы добавить объект в таблицу, вызывается функция AddThing:
void AddThing (TH BIND * adr)
{
ChainOfThings.prev->next = adr,
adr->next= &ChainOfThings;
adr->prev = ChainOfThings.prev;
ChainOfThings.prev= adr,
};
Здесь первой строкой полю next предыдущего объекта присваивается адрес
текущего объекта. Второй строкой для текущего объекта полю next
присваивается адрес начала цепочки объектов. Таким образом, круг замкнулся.
Каждый предыдущий объект указывает на следующий до тех пор, пока не
встретится начало цепочки объектов (ChainOfThings). Две следующие
строки необходимы для того, чтобы объект можно было удалить из таблицы.
Они представляют собой связь с предыдущим объектом. При удалении
объекта из таблицы объектов первый также удаляется из памяти (Удаление
объекта конкретно мы рассмотрим при обсуждении функции обработки
объектов).
А теперь наконец рассмотрим, что из себя представляет объект (после столь
томительного ожидания). Это структура, в которой отражены все свойства,
параметры, атрибуты, положение в пространстве и другие данные объекта, его
связь со спрайтовой системой:
typedef struct thing
{
TH_BIND t; // связь всех объектов
void (*func)(struct thing *); // функция обработки объекта
int korX; // координата X объекта
int kor Y; // координата Y объекта
int korZ; // координата Z объекта
THING * NextTh; // эти два значения необходимы для
THING * PrevTh; // организации карты объектов в секторе
unsigned int Ang; // угол поворота объекта
unsigned int sprite; // номер спрайта для объекта
int frame; // номер текущего кадра в спрайте
THING * MapNextTh; // эти два значения необходимы для
THING * MapPrevTh; // организации карты объектов в BLOCKMAP
SUB_SECTOR * ssect; // суб-сектор, в котором находиится объект
int floorZ; // координата Z пола под объектом
int ceilZ; // координата Z потолка над объектом
int dimens; // радиус объекта
int height; // высота объекта
int incX; // приращение координаты по X для объекта
int inc Y; // приращение координаты по Y для объекта
int incZ; // приращение координаты по Z для объекта
unsigned int type; //тип объекта (его номер)
struct sprites * SprDat; // указатель на спрайтовые данные
int count; // текущий счетчик анимации (задержка)
struct thin * spr, // текущий указатель на данные анимации
int attr; // атрибуты объекта
int power; // энергия объекта
int ChaseDir, // направление перемещения (для монстров)
int CounRespawn; // время движения монстра в одном направлении
THING * enemy, // указатель на цель (кого атаковать)
int DelayAgr, // задержка восстановления после шока
int AgrShoot; // агрессивность
MAN * player; // указатель на структуру игрока
int NumPlayer; // номер игрока, которого монстр пытается обнаружить в
//первую очередь
short kx_r; // остальные данные необходимы для
THING short ky_r; // воскрешения монстров (Respawn)
short Angr;
short Type_r;
short Attr r;
} THING;
Некоторые вещи, указанные в этой структуре (например, спрайтовые
данные и указатели на структуры sprites и thin), мы уже обсуждали. А то,
что не обсуждали, надеемся, вам скоро станет ясно.
Функция SpawnMapThing формирует карту объектов:
void SpawnMapThing (register struct WadThingFile *adr)
{
register unsigned short t,u;
int j;
register THING * thi;
t = adr->type; // тип объекта
if (t <= 4 ) // игроки (максимально 4 игрока)
{
movlO (&TempPlayerInfo[t-l][0],adr); //информация об игроке
CreateThingForPlayer (adr); // создать объект для игрока
return;
}
switch (CurSkill)
{
caseO: u=l; break;
case 4: u=4; break;
default: u= l«(CurSkill-l);
}
if( adr->attr& u )
{
for (j=0; j<NUM_THINGS; j++ )
{ // поиск блока данных объекта
if ( adr->type == ThInfo[j].ExtThing )
break;
}
if (j == NUM_THINGS )
ExitWithMsg(“P_SpawnMapThing: Unknown type %i at (%i,%i)”,
adr->type,adr->PosX,adr->PosY);
if (Inomonsters || !(ThInfo[j].Options & TA MONSTER) )
{ // Создать объект
thi = CreateThing (INT_TO_FIXED(adr->PosX),
INT_TO_FIXED(adr->PosY),
((Thlnfofj],Options & TA HANGING) == 0) + 0х7(1ШП, j);
if (thi->count > 0 )
thi->count = Random 1() % thi->count+l;
if (thi->attr & TA MONSTER )
{ // информация для “воскрешения”
movlO (&thi->kxr, adr);
LivingThings++; //количество монстров
}
if (thi->attr & ТА_ГГЕМ )
Items++; // количество артифакгов
thi->Ang = ((int)adr->angl/45)«29; //пересчет утла
if (adr->attr & 8) // глухой охранник
thi->attr |= TA DEAF GUARD;
}
}
}
Эта функция ищет в Thlnfo с определенным номером (ExtThing) и, если
последний найден, создает новый объект с использованием функции
CreateThing, в которую передается четыре параметра:
- 1 и 2 - координаты X и Y объекта;
- 3 - координата Z объекта. Если этот параметр равен максимальному
положительному7 целому значению (0x7 ЙНСГ), то объект подвешен к потолку;
если параметр равен максимальному отрицательному целому значению
(0x80000000), то координатой Z объекта является координата пола (объект
находится на земле).
- 4 - порядковый номер объекта (всего их в DOOM’e -103).
Исходный текст функции CreateThing:
THING * CreateThing (int x, int y, int z,’int typ)
{
register THING * adr,
adr = (THING *)zmalloc ( sizeof(THING), NULL, 50);
stos (adr, sizeof(THING));
adr->type = j; // тип объекта
adr->korX = x; // координата X
adr->kor Y = у; // координата Y
adr->SprDat = &ThInfo[j]; // спрайтовые данные объекта
adr->dimens = Thlnfo[j]. Dimension; //размер объекта
adr->height = Thlnfofj |. Height; // высота объекта
adr->attr = ThInfo[j].Options; // атрибуты объекта
adr->power = Thlnfojjj power, // энергия объекта
if(CurSkill !=4)
adr->DelayAgr = ThInfo[j].DelayAgr;
adr->spr = &th[ThInfo[j].ofThmQuiet]; // состояние спокойств.
adr->count = adr->spr->count; // счетчик для тек. кадра
adr->sprite = adr->spr->sprite; // номер тек. спрайта
adr->frame = adr->spr->frame; // номер текущего кадра
AddThing Ssec Map (adr); // прибавить объект в карту
adr->floorZ = adr->ssect->SectSeg->floorZ; // координата пола
adr->ceilZ = adr->ssect->SectSeg->ceilZ; // координата потол.
if (z == 0x80000000 ) // объект на земле
adr->korZ = adr->floorZ;
else
if (z == 0x7ffHnT) // объект подвешен к потолку
adr->korZ = adr->ceilZ - adr-SprDat-Height;
else
adr->korZ = z; 11 координата Z объекта
adr->func = &SpriteProcessing; // функция обработки объекта
AddThing (adr->t); // занести объект в таблицу
return adr,
}
Здесь мы говорили об объектах, как о предметах или монстрах, различных
артефактах и т. д. Но для игрока, который принимает непосредственное участие
в игре, тоже создается структура THING. Единственным ее отличительным
признаком является поле player в структуре THING (для остальных
объектов это поле равно нулю). Поле player указывает на структуру данных
игрока. Рассмотрим ее:
typedef struct man
{
THING * PlayerThing; // указатель на объект для игрока
int PlayerStatus; // статус игрока
GAME CONTROL Control; // структура управления для игрока
int LevelOfHead; // уровень головы игрока относ, пола
int height; // высота игрока
int InertJumpZ; // инерционность прыжка по Z
int PositionWeapon; // позиция оружия
int energy; // энергия игрока
int Armor; // текущие доспехи игрока
int ArmorRang» // уровень защиты
int spec[6]; // специальные артифакты
int keys[6]; // ключи
int PickFullAmmo; // полная экипировка
int CurrentAmmo; // текущее оружие
int ChangeAmmo; // сменить на это оружие
int PresentAmmo[8]; // доступное оружие
int ammo[8]; // патроны для оружия
int Fire; // нажатие на спусковой крючок (огонь)
int Use; // игрок взял предмет (изм. палитры)
int cheat; // CHEAT код
int CountUseFire; // счетчик считает, поканаж. Fire
struct statist // статистика
int kills; // сколько монстров убито
int items; // сколько артифактов найдено
int secrets; // сколько секретов обнаружено
} st;
char * string; // локальное сообщение (в верхи, части)
int CountPalBloodEyes; // Время красной палитры (при попадай.)
int CountPalUseObj; // Время серой палитры (предм. подобран)
THING * Enemy; // указатель на “врага”
int StepShooting, //Фаза стрельбы
int palitra; // текущая палитра (глаза)
struct weapon AmmoData[2]; // структуры для оружия
int SecretMis; // признак секретной миссии
} MAN;
В этой структуре в поле spec указываются специальные виды защиты, экипировки и
др:
Adeline SP_INVULNERABILITY 0 // неуязвимость
Adeline SP_BERSERK 1 //колоссальная физическая сила
Adeline SPPARTIAL 2 // невидимость
Adeline SP_RAD_SHIELD 3 //радиационная защита
Adeline SP_COMP_MAP 4 // компьютерная карта
Adeline SP LIGHT 5 // максимальная освещенность
Для проверки наличия какого-либо параметра, например, колоссальной
физической силы, можно проверить на ненулевое значение соответствующее
смещение в массиве spec:
if (chuv->spec[SP_BERSERK])
{
выполняемые в связи с этим действия
}
В DOOM ’е, как мы уже сообщали, на данные игроков указывает переменная
chuv: MAN chuvfMAXPLAYERS]; где MAXPLAYERS - максимальное количество
игроков в сети (в DOOM’e MAXPLAYERS равно четырем).
В функции SpawnMapThing для объекта, номер которого находится в
диапазоне от 1 до 4 (Player), вызывается функция CreateThingForPlayer. Эта
функция инициализирует структуру MAN для конкретного игрока и создает
для него объект (функция CreateThing), адрес которого присваивает полю
PlayerThing структуры MAN:
void CreateThingForPlayer (struct WadThingFile *adr)
register int j;
THING * t;
j = adr->type - 1;
if (playersfj] )
if ( chuv[j].PlayerStatus == PS START AFTER DEAD )
RestPlayer (j); 7/ восстановить игрока после “смерти”
t = CreateThing (INT_TO_FIXED(adr->PosX),
INT_TO_FIXED(adr->PosY),
0x80000000, TTPLAYER);
t->player = &chuv[j];
t->Ang = ((int)adr->angl / 45)« 29; // угол поворота игрока
t->power = chuvfj].energy; // энергия игрока
chuvfj
chuvfj
chuv j
chuvfj
chuvfj
chuvfj
chuvfj
chuvfj
chuvfj
.PlayerStatus = PS_NORMAL; // нормальное состояние
.CountUseFire = 0;
.string = NULL; // сообщения нет
.CountPalBloodEyes = 0;
.CountPalUseObj = 0;
.StepShooting = 0;
.palitra = O;
.height = P I1EIGHT; // высота игрока
.PlayerThing = t;
InitWeapon (&chuv[j]); // инициализация оружия
if(j== player)
InitStatusPanel (); // инициализ. статус панели
InitMessages (); // инициализ. системы сообщений
Теперь несколько слов об угле (Ang). Если вы внимательно следите за
ходом изложения, то уже дважды должны были заметить (один раз - в функции
SpawnMapThing, а другой - в CreateThingForPlayer), что угол
преобразуется во внутренний формат. Что это за преобразованйе и зачем
оно нужно? Дело в том, что в игре трудно оперировать с углами, выраженными
в градусах. Поэтому в DOOM’e приняты следующие соответствия:
Оград. -0;
90 град -0x40000000;
180 град - Ох800000(ХУ,
270 град - ОхсООООООО;
Кроме того, следует отметить, что углы в игре фактически имеют только 12
старших значащих разрядов. Остальные 20 разрядов значения практически не
имеют. Нетрудно подсчитать, что всего при этом будет 4096 градаций углов.
Почему, спросите вы, нельзя использовать целиком 32-разрядное число или
нельзя ли хотя бы увеличить число градаций? На первый вопрос ответ очевиден.
Дело в том, что при перемещении всех объектов в пространстве активно
используются две таблицы: Cosine[4096] и Sine[4096], которые представляют
собой, соответственно, косинус и синус утла поворота. Значения в этих таблицах
представлены в формате с фиксированной точкой в диапазоне от -1 до 1. Всего
таких значений в каждой таблице - 4096. Те. на один градус поворота приходится
чуть больше 11 значений в каждой таблице (4096/360= 11.3(7)). Практика показала,
что этого достаточно (это ответ на второй вопрос). В DOOM’e направление на
восток (угол равен 0) соответствует перемещению по координате X в положи-
тельном направлении. Следовательно, значение в таблице Cosine по нулевому
смещению должно быть близко к 1 (Cosine[0] = OxflH), а значение в Sine по
этому смещению близко к нулю (Sine[0] = 0x19). Таким образом, эти таблицы
необходимы для преобразования внешнего воздействия во внутренние
координаты в зависимости от угла поворота игрока. Поясним... Когда вы
нажимаете клавишу вперед или назад, то для вас это всегда перемещение вдоль
координаты Y. На самом деле это перемещение зависит от угла поворота в
плоскости. При этом реальные, фактические приращения координат объекта
рассчитываются через данные таблицы в зависимости от управляющего
воздействия и угла поворота:
THING *t;
int 1J;
1 = chuv(0].Control.Move «11;
t = chuv[O].PlayerThing;
j = t->Ang » 19;
t->incX +=_fixmul(l, Cosine[j]),
t->incY += _fixmul(l, Sine[j ]),
Это пример того, как осуществляется перемещение игрока. incX и incY
являются приращениями координат объекта. Текущие координаты просто
прибавляются к данным приращениям:
t->korX += t->incX;
t->korY += t->incY;
Параметры incX и incY определяют и инерционность объекта, т.к. после
управляющего воздействия они не сразу сбрасываются в 0, а умножаются на
коэффицииснт, меньший 1:
t->incX = _fixmul(0xe800, t->incX);
t->inc Y = _fixmul(0xe800, t->incY);
Поэтом}' после пропадания управляющего воздействия объект некоторое
время движется по инерции. Однако при встрече препятствия оба приращения
чаще всего сбрасываются в 0. Но не всегда. В том случае, если объект наткнулся
на стену не под прямым углом, он скользит вдоль стены в направлении и со
скоростью в соответствии со знаком отклонения угла от величины 90 градусов
н с величиной данного отклонения. Этим “скольжением” занимается функция
SlideTraverse.
Вот и все, что мы хотели сказать о загрузке объектов. Некоторым из
читателей может показаться, что тут много избыточной, ненужной
информации. Возможно, это так, но эти “лишние” данные позволят вам
сориентироваться и найти наиболее оптимальные решения в конкретной
ситуации для вашей игры.
7.2. Ресурс LINEDEFS. (2-ой объект)
Каждый linedef представляет линию от одной VERTEXES (вершины) до
другой. Он состоит из 14 байт, поделенных на 7 значений типа short:
(1) начиная от VERTEX с этим номером (первый VERTEX - 0);
(2) до VERTEX с этим номером (например, 24 VERTEX - 23);
(3) атрибуты, смотрите п. 7.2.1;
(4) тип, смотрите п. 7.2.2;
(5) это номер переключателя, защелки, который запускает различные
эффекты, зависящие от типа линии. Эта защелка связана со всеми секторами,
имеющими тот же номер переключателя (в последнем поле ресурса
SECTORS).
(6) индексируемый номер “правого” SIDEDEF;
(7) индексируемый номер “левого” SIDEDEF, если эта линия является
границей двух секторов. В противном случае, если линия примыкает только к
одному сектору, то данное значение равно -1 (Oxfflf).
“Правый” и “Левый” SIDEDEF зависят от направления линии
(LINEDEF). Направление определяется полями (1) и (2), т.е.
“от VERTEX до VERTEX”:
левая сторона правая сторона
от--------------->до<---------------от
правая сторона левая сторона
Важно понимать, что все линии должны иметь правую сторон}'. Если линия
односторонняя, то она имеет собственное направление и является частью только
одного сектора, который она ограничивает.
7.2.1. UNEDEFS атрибуты
Третье поле каждой линии - это значение типа short, которое определяет
свойства этой линии, атрибуты линии. Каждый атрибут представляет собой
бит:
БИТ Эф ф *кт, связанны й с установленным битом (бит"1)
О Линни непроходимая Игроки и монстры ие могут пройти через эту границу. Снаряды взрываются прн столкновении с этой линией.
1 Блокировка для монстров. М онстры не могут пересечь эту границу.
2 Линия двухсторонняя. Установленный бит означает, что данная ливни ограничивает два сектора, которыенмеются с обеих сторон л н н и н .
3 Верхняя текстура ’не закреплена". "Закрепленная" текстура может перемещаться вииз/вверх, если сектор, который ограничивает данная линия, также перемещается вниз/вверх. Для примеря: если потолок опускается вниз, то "з а к р е п л е и и а я " текстура на правой н левой стороне платформы также будет перемещаться вниз. При этом самотеком создается новый материал (текстура), который до этого момента был невидим. Дверн н лифты всегда являются "закреплении мн".
4 Н ижняя текстура "не закреплена"
5 Секретная дверь В автоматической карте данная лииня выглядит как обычная сплошная стена, которая ннчем не отличается отдругнх стен Таким образом скрывается данный секрет, пока на эту стену ие "нажать". Этот бнт не имеет отношения к секретным секторам (номер 9), н в конце у р о в н ястати ст и к а по обнаруженным секретным дверям не выдается.
6 Блокировка звука. Обычно звук не может проникнуть сквозь сплошную стену, у которой установлен данный бнт. Звук может "распространятся" нз одного сектора в другой, насколько позволяют относительные высоты полов/потолков. Н апример: звук ие может проникать нз сектора с высотой п о л о в/о т о л к о в 0/64 в другой сектор с высотой 72/128. Но он может пройти нз сектора с высотой 0/6 4 в сектор с высотой 56/1 28.
7 Нет на карте. Если данный бнт установлен, то данную линию не видно на автоматической карте (даже в том случае, если у вас в наличии имеется компьютерная карта лабнрннта - computer allmap).
8 Всегда на карте При у ст а н о в л е и н о м бите данная линия всегда будет вндна на автоматической карте, даже в том случае, если линия обнаружена игроком еще не была Если бит сброшен, то линия появляется на карте лишь в том случае, если она была “увидена” игроком.
9-15 Н с используются н могут иметь любые значения (0 нлн 1)
7.2.2. LINEDEFS типы
Значение в четвертом поле ресурса LINEDEFS определяет тип линии,
т.е. специальный эффект, связанный с данной линией. Чаще всего
активизируются сектора при пересечении линии с совпадающим с данными
секторами номером защелки (trigger).
Исключением являются “двери”, переключатели окончания уровня. Для
достижения этих эффектов имеется связь между' линиями, при помощи которых
они приводятся в действие, и секторами, с которыми производятся данные
эффекты. Этим связующим элементом является “ярлычок” (определенный
номер), который имеется в LINEDEFS и соответственно тот же номер во
всех “подопытных” секторах (этих секторов может быть несколько.
Например, пересечение одной линии может привести к опусканию 4-х
платформ. Таким образом, здесь участвуют 4 сектора).
Все функции исполняются только с правой стороны линии. Таким
образом, переключателй и двери можно активизировать только с правой
стороны. Телепортеры так же активизируются только при пересечении их с
правой стороны. А сейчас для примера приведем таблицу с некоторыми
специальными типами линий.
Тип А т р и буты Эф ф «кт
0 Нет эффекта
1 S R Дверь. Сектор на другой стороне данной линии поднимается до уровня, на 8 меньше уровня первого соседнего потолка. Затем опускается спустя 6 секунд.
1 0 W 1 П ол опускается. Затем через 3 секунды поднимается.
3 3 S 1 Дверь. Требуется желтый ключ. Остается открытой.
3 9 W 1 Телепортатор в другой сектор. Только один сектор должен иметь соответствующий номер “ярлычка" (телепортироваться одновременно в несколько секторов нсвозмож но).
4 0 W 1 Потолок повышается до уровня соседнего потолка.
4 6 G 1 Открыть дверь. (Дверь остается открытой).
4 8 А ннмаимя: горизонтальное скролнрованне стены .
5 2 W - Коней уровня. Перейти на следующий у р о в с н ь .
8 0 W R Яркость устанавливается в соответствии с максимальной яркостью в соседних секторах.
8 7 W R Старт двигающейся платформы (пол двигается вверх/в низ с периодичностью 5 секунд).
8 9 W R Прекратить синдром движения платформы, стартуемой по 87 линии.
Все типы линий, используемые в DOOM’e, мы здесь приводить не будем,
потому что, во-первых, этих типов слишком много, а во-вторых - это не нужно
(в своей игре вы вряд ли будете копировать из DOOM ’а все эти эффекты).
Для начала объясним, что означают символы в колонке “Атрибуты”:
W - функция активизируется при пересечении игроком данной линии;
S - функция активизируется при нажатии (с использованием SPACEBAR)
на данну ю стену (переключатели или двери);
G - функция активизируется при выстреле в эту стену из ружья или
пистолета;
1 - функция срабатывает только один-единственный раз;
R - действие данной линии может повторяться сколько угодно раз.
Некоторые функции могут повторяться сколько угодно раз. Например,
при “открывании” двери последняя через некоторое время закрывается,
после чего ее снова можно открыть. А может быть так, что дверь после
открытия не закрывается. В данном случае функция срабатывает только
один раз. Все сказанное также относится и к различным переключателям,
которые могут срабатывать либо один, либо бесконечное количество раз.
7.2.3. Загрузка ресурса LINEDEFS
А теперь рассмотриим функцию LLinedefs, которая загружает ресурс
LINEDEFS и создает внутреннюю структуру LINE:
void L_LineDefs (int num)
{
struct WadLineFile
{
short Vertl; // начиная от этой вершины
short Vert2; // заканчивая этой вершиной
short attr; // атрибуты линии
short type; //типлинии
short trig; //номер переключателя
short RigSid; // номер правой стороны
short LefSid; // номер левой стороны
} *lin;
register inti;
register VERTEX ♦ from;
register VERTEX * to;
// количество линий
NumLines = lumplen(num) / sizeof (struct WadLineFile);
alin = zmalloc (NumLines * sizeof(LINE), NULL, 50 ), // адрес
stos (alin, NumLines * sizeof(LINE)); // линий
lin = ReadLumpForNum(num,l); // чтение данных о линиях
for ( i=0; i<NumLines; i++ )
{
alinfiJ.Attr = lin[i].attr; // атрибуты линии
alin[i].Type = lin[i] type; // тип линии
alin i].Trig = lin[i] trig; // номер переключателя
alin i].FromVert=&avert[lin[i].Vertl];// указатель на вершину “от”
alin[i].ToVert=&avert[lin[i].Vert2]]; // указатель на вершину “до”
from = alin[i].FromVert;
to = alin[i].ToVert;
alin[i].lenX = to->krX - from->krX; // длина линии по X
alin[i].lenY = to->krY - from->krY; // длина линии no Y
if ( alin[i].lenX == 0 )
alin[i].TypeSlope = 1; // линия параллельно оси Y
else
if ( alin[i].lenY == 0 ) // линия параллельно оси X
alin[i].TypeSlope = О,
else
if ( _fixdiv(alin[i].lenX, alin[i].lenY) 0)
S.TypeSlope = 2; // линия под углом от 0 до 90
' градусов к оси X
alin[i].TypeSlope = 3; И линия под углом от 90 до
if ( from->krX < to->krX ) // 180 градусов к оси X
{
alin[i].lesX - from->krX, // минимальная координата по X
alin[i].bigX = to->krX; // максимальная координата по X
}
else
{
alin[i]. lesX = to->krX; // минимальная координата по X
alin[i].bigX = from->krX; // максимальная координата по X
if ( from->krY < to->krY )
{
alin[i].lesY = from->krY; // минимальная координата no Y
alin[i].bigY = to->krY; // максимальная координата no Y
}
else
{
alin[i].lesY = to->krY; // минимальная координата no Y
alin[i].bigY = from->krY; // максимальная координата no Y
}
alin[i].RLSid[O] = lin[i].RigSid; // номер правого SIDEDEF
alin[i 1.RLSid[ 1 ] = lin[i].LefSid; // номер левого SIDEDEF
if( alm[i].RLSid[0] !=-l)
alinfi], Srig = asid[alin[i].RLSid[O]].SecSide; // сектор
else alin[i]. Srig = NULL; // справа
if( alin[i].RLSid[l] !=-l)
alin[i].Slef=asid[alin[i].RLSid[l]].SecSide;//сектор
else alin[i].Slef = NULL; // слева
}
zfree (lin); // освободить буфер
}
Поле TypeSlope в структуре LINE определяет направление линии:
Typeslope = О
Typeslope = 1
Typeslope = 2
Typesfope = 3
Рис. Зависимость значения Typeslope от направления линии
Функция LLineDefs создает структуру LINES, которая активно
используется в игре, в частности, при выводе “Автоматической карты” или для
получения информации о секторах, которые данные линии ограничивают. Вот
эта структура:
typedef struct line
{
VERTEX * FromVert; // указатель на VERTEX (от)
VERTEX * To Vert; // указатель на VERTEX (до)
int lenX; // длина линии по X
int lenY; //длина линии по Y
short Attr; // атрибуты линии (см. пункт 6.2.1)
unsigned short Type; // тип линии (см. пункт 6.2.2)
short Trig; // номер переключателя
short RLSid[2]; // номера правого и левого SIDEDEF
int bigY; // максимальная юхц'динага по Y
int lesY; // минимальная координата по Y
int lesX; // минимальная координата по X
int bigX; // максимальная координата по X
int TypeSlope; // направление линии
SECTOR * Srig; // сектор по правую сторону линии
SECTOR * Slef; // сектор по левую сторону линии
int CountUseLine; // уникальное значение, которое
} LINE;
// изменяется конкретным образом каждый раз, когда
// обрабатывается эта линия. Используется для того, чтобы
// не делать дважды одну работу
Вот и все, что мы пока хотели сказать о линиях. В дальнейшем мы
покажем, как использовать структуру LINES на конкретных примерах.
7.3. Ресурс SIDEDEFS. ( Третий объект )
Ресурс SIDEDEF определяет текстуры, которые “накладываются” на
стены. Эти текстуры рисуются вдоль линий, о которых мы говорили в
разделе 7.2. Группа текстур (SIDEDEF) определяет сектор.
Один SIDEDEF какой-либо линии ограничивает только один сектор. Игрок
не может видеть сквозь текстурированную стену.
Ресурс SIDEDEF состоит из 30 байт, разбитых на 2 значения типа short, три
строки по восемь символов char, и последнее значение типа short:
(1) смещение X для вставки соответствующей “стенной” текстуры в
“пространство” стены. Положительное смещение передвигает текстуру таким
образом, что часть левой стороны оказывается “откусанной” (количество
столбцов в левой части текстуры и есть положительное смещение).
Отрицательное смещение перемещает текстуру дальше вправо в
“пространстве” стены.
(2) смещение Y аналогично смещению X, но для вертикали.
(3) имя “верхней” текстуры. Используется, когда высоты потолков соседних
секторов не совпадают. “Верхняя” текстура - это стенная текстура, которая
соединяет потолок одного сектора с потолком соседнего сектора.
(4) имя “нижней” текстуры. Используется, когда высоты полов соседних
секторов не совпадают. “Нижняя” текстура - это стенная текстура, которая
соединяет пол одного сектора с полом соседнего сектора.
(5) имя “полной” текстуры. Это постоянная часть стены.
(6) номер сектора, которому принадлежит данный SIDEDEF.
Имена текстур находятся в ресурсах TEXTURE 1/2.0 них мы уже говорили
в п. 3.5.1. Имя текстуры состоит из восьми или менее символов ASCII.
Простой SIDEDEF не имеет “верхней” и “нижней” текстуры. В этом случае
для данных текстур первым символом (и единственным) в имени стоит символ
Двухсторонние стены могут быть невидимыми (прозрачными), если в имени
текстуры поставить символ Этот знак является индикатором прозрачности
(т.е. нет текстуры).
Если стена, на которую накладывается текстура, шире, чем размер текстуры
по горизонтали, то текстуры пристыковываются друг к другу по горизонтали
как черепица на крыше, или как кафельная плитка. Так, текстура шириной 64
пикселя будет “наклады «ться” на стену с координатами 0,64,128,192 и т.д. То же
самое: если стена выше, чем размер текстуры по вертикали, то текстуры
пристыковываются друг к другу по вертикали. Причем каждая новая
пристыковка идет с кратностью 128, т.е. вертикальные координаты
пристыковываемых текстур будут 0,128,256,384 и т.д. Таким образом,
вертикальный размер текстуры должен быть кратен 128. Если он меньше
128, то часть стены будет заполняться неизвестными данными, н выглядеть
это будет ужасно.
7.3 .1. Загрузка ресурса SIDEDEFS
А теперь рассмотрим функцию LSideDefs, которая загружает ресурс
SideDefs и создает внутреннюю структуру SIDE:
void LSideDefs (int num)
{
struct WadSideFile
// загрузка ресурса SIDEDEFS
{
short offsX;
short offsY;
char UpTex[8];
charLowTex[8];
charFullTex[8];
short Sect;
// смещение по X для текстуры
// смещение по Y для текстуры
// имя для верхней текстуры
// имя для нижней текстуры
/1 имя для полной текстуры
//номер сектора
} *sid;
register int i;
// количество сторон (SIDE)
numside = lumplen(num)/ sizeof (struct WadSideFile);
asid = zmalloc (numside * sizeof(SIDE), NULL, 50 );
stos(asid, numside * sizeof(SIDE));
sid = ReadLumpForNum(num, 1); // загрузка ресурса SIDEDEFS
for (i=0; i<numside; i++)
}
asid[i].krX = INT_TO_FIXED(sid[i]. offsX); //смещениеХ
asid[i].krY = INT_TO_FIXED(sid[i].ofrsY); //смещение Y
// номер “верхней” текстуры (если отсутствует, то 0)
asid[i].UppTex = TextureNumForName (sid[i].UpTex);
// номер “нижней” текстуры (если отсутствует, то 0)
asid[i].LowTex = TextureNumForName (sid[i].LowTex);
// номер “полной” текстуры (если отсутствует, то 0)
asid[i].FulTex = TextureNumForName (sid[i].FullTex);
asid[i].SecSide = &asec[sid[i].Sect]; // номер сектора
}
zfree (sid); // освободить буфер
Внутренняя структура SIDE почти не отличается от внешней структуры
WadSideFile. Единственным ее отличием является то, что в ней использованы
не имена текстур, а их номера. По этим номерам можно ссылаться на
конкретную текстуру, используя информацию Textureinfo, о которой мы
говорили в пункте 3.5.1.1. (для текстуры с номером N это будут данные:
TextureInfo[N]). Структура SIDE определяется так:
typedef struct side
{
int krX;
intkrY;
short UppTex;
// смещение X для текстуры
// смещение Y для текстуры
// номер “верхней” текстуры
short LowTex; И номер “нижней” текстуры
short FulTex; // номер “полной” текстуры
SECTOR * SecSide; // номер сектора
} SIDE;
Структура SIDE используется в функциях “рендеринга” стен й при
изменении текстур “на ходу” (в случае переключения рубильников и нажатия
кнопок). К ней мы еще вернемся.
7.4. Ресурс VERTEXES. (Четвертый
объект)
Этот объект, пожалуй, самый простой. Он определяет координаты вершин
д ля ресурсов LINEDEFS и SEGS. Он состоит из четырех байт, поделенных на два
значения типа short:
(1) координатах
(2) координата Y
В автоматической карте в игре с использованием сетки (нажатие “G” - grid)
один квадрат сетки имеет размер 128x128 (шестнадцатеричное 80h х 80h).
7.4.1. Загрузка ресурса VERTEXES
Функция L VerTexes загружает объект VERTEXES и создает
внутреннюю структуру VERTEX:
void L_VerTexes (register int num)
{
struct WadVertexFile // 4 байта
{
short x; // позиция X
short у; // позиция Y
} *ver,
register inti;
numvertex = 1 umplen(numysizeof( struct WadVertexFile);
avert = zmalloc (num vertex*sizeof( VERTEX), NULL, 50);
ver = ReadLumpForNum(num,l); // загрузка ресурса VERTEXES
for (i=0; i<numvertex i++)
{
avert[i].krX = INT_TO_FIXED(ver[i].x); // вершина X
avertfij.krY = INT_TO_FIXED(ver[i].y); // вершина Y
}
zfree (ver); // освободить буфер
};
Структура VERTEX, как неотъемлемая часть структур LINE и SEGS,
широко используется в игре, в частности при прорисовке автоматической
карты, при анализе границ (столкновений), при прорисовке лабиринта и
т.д.
typedef struct vertexes
int krX; 11 вершина X
int krY; 11 вершина Y
} VERTEX;
7.5. Ресурс SEGS. (Пятый объект)
Сегменты (SEGS) - это прямые линии, замкнутая последовательность которых
образует субсектор (смотрите ниже пункт 7.6). Они являются частью узлов
(NODES) рекурсивного дерева. Каждый сегмент состоит из 12 байт, поделенных
на 6 значений типа short:
(1) - начиная с этой вершины (указатель на VERTEX);
(2) - заканчивая этой вершиной (указатель на VERTEX);
(3) - угол: 0 - восток; 16384-север; 32768-запад; 49152-юг.
Данное числовое представление углов в диапазоне от 0 до 360 градусов мы
будем называть двоичным угловым измерением. (Binary' Angle Measurement
или BAMS).
(4) -иомер LINEDEF, соответствующего этому сегменту:
(5) 0 - этот сегмент находится на правой стороне LINEDEF;
1 - этот сегмент находится на правой стороне LINEDEF;
Это значение определяет направление сегмента (SEGS):
0 - соответствует тому же направлению, что у LINEDEF;
1 - соответствует противоположному направлению.
(6) - смещение внутри LINEDEF к началу' этого сегмента, т.е. расстояние
от начала LINEDEF до вершины сегмента, определенном в первом значении
ресурса SEGS. Это смещение соответствует тому же направлению (углу),
что и направление сегмента (3 значение ресурса SEGS). Если пятое значение
(предыдущее) равно нулю, то это смещение соответствует расстоянию между
конечной вершиной LINEDEF и начальной вершиной SEGS (1 значение
ресурса SEGS); если пятое значение равно 1, то смещение соответствует
расстоянию между начальной вершиной LINEDEF и начальной вершиной
SEGS. Таким образом, если сегмент начинается с одной нз конечных точек
LINEDEF, то смещение будет равно нулю.
Более подробно о назначении ресурса SEGS вы узнаете из пункта 7.7,
где мы попытаемся разобраться с узлами рекурсивного дерева.
Для диагональных сегментов смещение может быть получено по формуле:
смещение = ^/(х2 - xl)2 + (у2 - j4)2
7.5 .1. Загрузка ресурса SEGS
Функция LSegs загружает ресурс SEGS и создает внутреннюю
структуру SEGS:
void L_Segs (int num) // загрузка и обработка ресурса SEGS
{
struct WadSegsFile //12 байт
{
short vertex I ; //начиная с этой вершины
short vertex2, // заканчивая этой вершинной
unsigned short angle; 11 угол. В двоичном угловом измерении:
// 0 = восток, 16384 = север,
И 32768 = запад, 49152 = юг
short linedef; И номер linedef
short normal; И1 означает: от vertex 1 до vertex2
И 0 означает: от vertex2 до vertex 1
int distance; И расстояние от вершины линии до начала сегмента
}*seg;
register int ijc;
register LENE * m;
numsegs = lumplen(num)/sizeof( struct WadSegsFile);
asegs = zmalloc (numsegs * sizeof( SEGS), NULL, 50);
stos (asegs, numsegs * sizeof(SEGS)); // очистить память
seg = ReadLumpForNum(num,l); // загрузить ресурс SSECTORS
for (i=0; i<numsegs; i++)
asegs[i].FrVert = &avert[seg[i]. vertex!]; //от вершины
asegs[i].ToVert = &avert[seg[i].vertex21; // до вершины
asegsp] .Angle = INT_TO_FIXED(seg[i].angle); II угол
asegsfi .Offset = INT_TO FIXED(seg[i].distance);//расстояние
m = &alin[seg[i],linedef]; /7номер линии для сегмента
asegs[i].LinSeg=m;
k = seg[i].normal; // номер стороны (0 или 1)
asegs(ij.SidSeg = &asid|m->RLSid[k] I; //сторона сегмента
asegs[i].SecSeg=asegs[i].SidSeg->SecSioe;//секторнапервойстороне сегмента
if ( m->Attr & LA_TWO_SIDED )
asegs[i].OppSecSeg = asid[m->RLSid[kA 1 ]]. SecSide; // сектор
else asegs[i].OppSecSeg = NULL; // на второй стороне сегмента
zfree (seg); // освободить память
Функция L Segs формирует внутреннюю структуру SEGS:
typedef struct segs
{
VERTEX * FrVert; // от вершины
VERTEX * To Vert; 11 до вершины
int Offset;// расстояние от вершиины линии до начала сегм.
int Angle; И угол
SIDE * SidSeg; И сторона сегмента (указатель на SIDE)
LINE * LinSeg; // линия сегмента (указатель на LINE)
SECTOR * SecSeg; // сектор на стороне сегмента
SECTOR * OppSecSeg; II сектор на противоположной стороне сегмента
JSEGS;
Эта структура используется в игре при прорисовке трехмерного лабиринта
и для определения визуального восприятия для игрока и монстров. Например,
если игрок уввдел какую-либо стену (независимо от удаления этой стены от
игрока), то эта стена будет отображена на автоматической карте именно как
линия).
7.6. Ресурс SSECTORS. (Шестой объект)
SSECTORS - это субсектора. Оии делят сектора на выпуклые
многоугольники. Субсектора являются частью узлов рекурсивного дерева (см.
пункт 6.7). Количество субсекторов равно числу узлов плюс 1. Каждый субсектор
состоит из четырех байт, поделенных на 2 значения типа short:
(1) - сколько сегментов (SEGS) находится в этом субсекгоре
(2) - начиная с этого номера сегмента.
7.6 .1. Загрузка ресурса SSECTORS
Функция LSSectors загружает ресурс SSECTORS и создает внутреннюю
структуру SUB SECTOR:
void L SSectors (int num) // загрузка и обработка ресурса SSECTORS
(
struct WadSSectorsFile //4 байта
{
short num; // количество segs в субсекторе
short segs; // начиная с этого seg
}*sec;
register inti;
numssec = lumplen(num)/sizeof(struct WadSSectorsFile);
assec = zmalloc (numssec * sizeof(SUB_SECTOR), NULL, 50);
stos (assec, numssec * sizeof(SUB SECTOR)); // очистить память
sec = ReadLumpForNum(num,l);7/ загрузить ресурс SSECTORS
for (i=0; i<numssec; i++)
{
assec[i].num = sec[i].num; // количество segs в секторе
assec[i].segs = sec[i].segs; // начиная с этого segs
}
zfree (sec); // освободишь память
Внутренняя структура SUB SECTOR почти не отличается от внешней
структуры ресурса SSECTOR. Единственным отличием является поле
SectSeg. Один субсектор не может относится к двум и более секторам.
typedef struct ssec
{
SECTOR * SectSeg; // сектор, относящийся к этому субсектору
short num; // количество сегментов в субсекторе
short segs; И начиная с этого сегмента
}SUB_SECTOR;
7.7. Ресурс NODES. (Седьмой объект)
NODES - это узлы рекурсивного дерева.
Ресурс NODES (узлы) состоит из структур по 28 байт каждая, поделенных
иа 14 значений типа short:
(1) - начальная координата X линии раздела;
(2) - начальная координата Y линии раздела;
(3) - смещение по X между началом и концом разделительной линии;
(4) - смещение по Y между’ началом и концом разделительной линии;
Для примера значения 64 128 -64 -64 в охивстстиии с вышеперечисленными
параметрами соответствуют линии раздела от (64,128) до(0,64);
(5) - верхняя граница по Y для правой разделительной области;
(6) - нижняя граница по Y для правой разделительной области;
(7) - нижняя граница по X для правой разделительной области;
(8) - верхняя граница по X для правой разделительной области;
И все сегменты на правой ветке узла должны быть в
И пределах этой области
(9) - верхняя граница по Y для левой разделительной области;
(10) - нижняя граница по Y для левой разделительной области;
(11) - нижняя граница по X для левой разделительной области;
(12) - верхняя граница по X для левой разделительной области;
И все сегменты на левой ветке узла должны быть в
И пределах этой области
(13) - номер узла или субсектора для правой ветви. Если 15 бит установлен,
то этот номер (замаскированный с 0x701) представляет субсектор. Если 15
бит сброшен, то это номер следующего узла в соответствии с рекурсивным
деревом;
(14) - номер узла или субсектора для левой ветви.
Из всех ресурсов WAD файла в DOOM’e ресурс узлов самый запутанный и
трудный в понимании. Ои необходим для верного отображения трехмерного
лабиринта с учетом скрытых (от глаз) поверхностей и стен. Ресурсы
SSECTORS и SEGS также участвуют в этом процессе и вместе с ресурсом
NODES составляют как бы неделимое целое. Этим неделимым целым
являются узлы рекурсивного дерева.
В этом пункте я попытаюсь популярно объяснить, что из себя
представляет ресурс NODES и как с его помощью строится трехмерный
лабиринт. Я не буду приводить принцип формирования узлов по следующим
причинам: во-первых - это очень сложный алгоритм, и тяжело
прокомментированный код был бы трудно читаемым. Во-вторых - имеется
очень много программ, которые это делают: программа “bsp.exe” формирует
ресурсы NODES,SEGS и SSECTORS из набора LINEDEFS, SIDEDEFS и
VERTEXES. (Параметром программы “bsp.exe” является wad файл со
сформированной картой уровня. Карту уровня можно сформировать в
любом редакторе уровня для DOOM’a, например, в DOOMCAD или в DCK.
Выходом программы “bsp.exe” является новый файл “temp.wad” с
вычисленными структурами NODES, SEGS и SSECTORS). Отличный редактор
уровней для DOOM’a “waded.exe” умеет сам формировать ресурсы узлов.
Узлы представляют собой отделения в двоичном делении пространства (bi-
naty space partition, или BSP), которые делят уровень; они необходимы для
определения, какие стены находятся на переднем плане, а какие являются
невидимыми. Этот процесс известен как удаление скрытых поверхностей.
Субсектора и сегменты - необходимые части структуры узлов. BSP дерево
обычно используется в трехмерном пространстве, но в DOOM’e используется
упрощенная, 2-мерная версия схемы.
Основной принцип состоит в том, чтобы делить карту уровня иа малые
части до тех пор, пока каждая из этих малых частей не будет содержать только
сегменты стены, которые не могут служить преградой (блокировать вид других
стен) в этом месте. Эти малые неделимые части называются субсекторами.
Каждый сегмент стены, относящийся к конкретному' субсектору, представляет
собой LINEDEF или ее часть. Этот сегмент относится к ресурсу SEGS. Это
деление используется в алгоритме двоичного дерева в DOOM’e для быстрой
прорисовки трехмерного лабиринта (рисование видимых поверхностей).
Деление на субсектора производится в пределах сектора. Одни субсектор
не может принадлежать к двум и более секторам. Части уровня, которые
находятся за пределами секторов, игнорируются.
Сегментами могут быть только LINEDEF или ее часть. Стороны созданного
субсектора, которые не яалякпся частью linedef, сегментами не будут (мнимые
или воображаемые линии в пределах сектора).
Некоторые сектора ие требуют никакого деления. Для примера рассмотрим
квадратный сектор, все стены которого строго ортогональны к полу. Таким
образом, в любой точке зрения данного квадрата ни одна из четырех стен ие
может блокировать вид любой другой стены. Таким образом, этот квадратный
сектор целиком является субсектором.
А теперь рассмотрим сектор, имеющий такую форму:
Суб-сектор 1 Продолжение ^Суб-сектор 2
Рис Деление сектора произвольной формы на 2 субсекгора
На данном рисунке стена 4 и часть 3 стены закрыты диагональной сплошной
стеной 5. Следовательно, данный сектор требует раздела.
Предположим, что диагональная стена 4 имеет продолжение, как показано
на рисунке пунктиром.
Таким образом, мы получили два результирующих субсекгора. Обратите
внимание, что в пределах каждого из созданных субсекторов ни одна стена ие
закрывает обзор любой другой стены.
Вообще говоря, субсектор всегда является выпуклым многоугольником.
Слово “выпуклый” означает, что линия, соединяющая любые две точки внутри
данного многоугольника, всегда целиком находится в этом многоугольнике.
Все треугольники и прямоугольники выпуклы (но ие все четырехугольники). В
простом евклидовом пространстве DOOM’a выпуклость означает, что все углы
внутренних многоугольников меньше или равны 180 градусов.
Дополнительное осложнение теперь возникает из-за двухсторонних линий
(LINEDEFS). Ее две стороны находятся в различных секторах. Таким образом,
каждая двухсторонняя линия (LINEDEF) становится двумя (или более - если
сегмент захватывает не всю линию) сегментами, или можно сказать, что каждая
сторона становится сегментом. Создание сегментов из сторон (SIDEDEFS) -
неплохая идея, т.к. в этом случае сегмент может быть связан с сектором.
Всякий раз при начальном делении уровня на субсектора выбирается
несколько произвольно (обычно в середине карты) сегмент (SEG), который, как
нож, делит оставшееся пространство на два (двоичное деление). Этот сегмент
становится линией раздела узла, а оставшееся место на любой стороне линии
раздела станет ПРАВЫМИ и ЛЕВЫМИ ветвями узла. Все линии раздела имеют:
направление (или угол);
пространство на “правой” стороне раздела: правая ветвь узла;
пространство на “левой” стороне раздела: левая ветвь узла.
Обратите вниимание, что если в оставшемся месте невозможно более найти
сегмент, который может служить линией раздела, то дальнейшее деление ие
требуется, и это оставшееся место является субсектором ( или “листом” на
рекурсивном дереве).
Если линия раздела пересекает любую линию (LINEDEF). не через
вершины этой линии, то она делит эту линию на два сегмента (или на четыре,
если линия двухсторонняя).
После того, как выбраны линии раздела и созданы узлы, необходимо их
строго упорядочить, т.е. присвоить им строго определенные номера. “Дерево”
создается от верхушки вниз таким образом:
- после первого деления осуществляется деление левой ветви, затем ее левой
ветви и так далее до тех пор, пока самая последняя левая ветвь в делении
ну уедаться не будет (она является субсекгором). Потом необход имо подняться
на одну ветвь “вверх” и поделить правую ветвь, затем его левую до тех пор, пока
не останется ни одной неподеленной ветвн. Таким образом, всегда в первую
очередь обрабатываются левые ветвн дерева, а затем правые.
Заранее никогда нельзя знать, сколько получится узлов и субсекгоров. Их
подсчет ведется в процессе деления, после чего “верхушке” дерева дается
максимальный номер. Таким образом, в процессе “роста” дерева узлы и суб-
секгора считаются от 0 и выше, а после того, как все создано (дерево достигло
своего максимума), эти номера реверсируются. Например, если получиилось
725 узлов, то 725 узел станет 0-ым, а 0-ой станет 725.
А теперь, как бы подводя итог, рассмотрим на простом примере, как можно
создать рекурсивное дерево:
Рис. Формирование рекурсивного дерева.
1. Создаем сегменты (SEGS) из всех линий (LINEDEFS). Здесь мы имеем
5 двухсторонних линий.
2. Выбираем точку 0 и идем на запад (налево). Это первая линия раздела.
Обратите внимание - пунктирной линией обозначено продолжение линии
раздела (мнимая линия).
3. Если вы не забыли, сначала должно делиться пространство с левой
стороны линии раздела. Поэтому слева выбираем точку 1 и идем вверх.
4. Далее слева от линии раздела 1 выбираем точку 2 и ведем вверх.
5. Слева и справа от линии раздела 2 получились две неделимые области
(два прямоугольника), которые становятся субсекгорами а и Ь.
6. Правая область от линии 1 также не нуждается в делении и становится
субсектором с.
7. Далее точно так же делятся правые области от линии раздела 0.
8. После деления всего уровня (у иас получилось 5 узлов и 6 субсекторов)
номера в рекурсивном дереве реверсируются (обратная последовательность),
т.е. 0 узел станет 4, а 4 узел станет 0, и точно также с субсекторами. Для номеров
субсекгоров устанавливается 15 бит, а для номеров узлов 15 бит сброшен.
Например, субсекгор 1 фактически будет иметь номер в шестнадцатеричном
исчислении: 0x8001.
Для автоматической генерации рекурсивного дерева необходимо создать
алгоритм, способный автоматически оптимально выбирать линии раздела.
Основной принцип здесь состоит в том, чтобы свести к минимуму' количество
узлов и субсекгоров, ну и, желательно, дополнительных вершин (которые
получаются при мнимом продолжении линий раздела). Большинство
генераторов BSP дерева обычно делят карту' пополам, и выбирают первую
линию раздела где-то в середине карты.
Существуют два метода оптимизации BSP дерева, на мой взгляд,
взаимоисключающих:
1. Получение сбалансированного дерева. У сбалансированного дерева
количества узлов и ветвей с левой и с правой сторон приблизительно должны
совпадать.
2. Свести к минимуму количество расколов линий и дополнительных вершин.
Таким образом, уменьшается общее количество сегментов (SEGS) Однако
обойтись совсем без расколов в подавляющем большинстве случаев не удастся.
Но нам это и не нужно. Нам нужно, чтобы их было как можно меньше. Как это
можно оптимизировать? Тут самое главное - оптимальный выбор линий раздела.
Рассмотрим простой пример, изображенный на рисунке.
Если линиями раздела выбрать а и Ь, то мы получим четыре дополнительные
вершины, 5 суб-секторов и 16 сегментов.
Если в качестве линий раздела выбрать А и В, то дополнительных вершин не
требуется.
Рис. 7.4. Методы выбора линий раскола.
При этом получилось три субсектора и 12 сегментов.
На мой взгляд, наиболее важным направлением оптимизации является
минимизация расколов, т.к. чем меньше количество сегментов, тем быстрее
работает программа. Основное время в программе занимает прорисовка стен
и подготовка к этому процессу. Подготовка включает в себя достаточно сложные
вычисления и проверки, связанные с масштабированием стеи и
аксонометрическими преобразованиями. Поэтому в идеальном случае надо
стремиться к тому, чтобы один сегмент являлся одной линией (LINEDEF). Но
конечно, при возможности надо пытаться оптимизировать балансировку7 дерева,
хотя на быстродействии это сильно не скажется (во всяком случае, для глаза это
будет незаметно).
Все, что я здесь говорил, имеет только одну цель: необходимо, чтобы вы
уяснили суть. Если вы это поймете, то без проблем сможете создавать свои 3D-
игры. Это ВБРдерево является основной изюминкой, главным секретом всех
трехмерных игр. Все 3 D-игры используют этот подход (в играх DESCENT и
QUAKE используется 3-х мерный BSP), и альтернативы пока ие придумано.
Вам не обязательно изобретать что-то свое. На это могут уйти годы, а лучше
вряд ли получится. Как я уже говорил, существуют программы, которые
довольно оптимально создают необходимые структуры рекурсивного дерева
(BSP.EXE). Вы можете использовать эти программы с учетом того, что входные
параметры должны иметь формат DOOM’a Если же у вас свой формат данных,
то напишите несложные программы-утилиты, которые будут преобразовывать
ваш формат в формат данных DOOM’a и наоборот
7.7.1. Загрузка ресурса NODES
Функция LNodes загружает ресурс NODES и создает внутреннюю
структуру NODE:
void L_Nodes (int num)
struct rect // вершины прямоугольников
{
short maxy; // максимальная координата no Y
short miny; // минимальная координата no Y
short minx; // минимальная координата по X
short maxx, // максимальная координата по X
};
struct WadNodesF ile // 28 байт
{
short x; // позиция X
short у; // позиция Y
short dx; // смещение от конца узловой линии по X
short dy; И смещение от конца узловой линии по Y
struct rect area[2], // вершины первого и второго прямоугольников
short ssector[2]; II номер первого и второго субсектора если 15 бит
//установлен, то этот номер (маскированный с 0х7(П) представляет субсектор.
//Иначе (если 15 бит сброшен) этот номер является номером следующего
// узла (одной из веток рекурсивного дерева)
} *nod;
register int ijc;
numnodes = lumplen( num )/sizeof( struct WadNodesFile);
anodes = zmalloc (numnodes * sizeof(NODE), NULL, 50 );
stos (anodes, numnodes * sizeof(NODE)); // очистить память
nod = ReadLumpForNum(num,l); // загрузить ресурс NODES
for (i=0; icnumnodes; i++ )
anodes[i].krX = INT_TO_FIXED(nod[i].x); //позициях
anodes ij.krY = INT TO_FEXED(nod[il.y); //позиция Y
anodes i] dx = INT_TO_FIXED(nod[i].dx); // расстояние по X
anodes ij.dy = INT_TO_FIXED(nod[i].dy); //расстояние no Y
for ( k=0, k<2; k++ )
anodes[i
anodes[i
anodes[i
anodes i
anodes! i
.ssectlk
. local [k
.local k
.local k
locatlk
= nod[i].ssector[k]; II субсектор
. Yupper=INT TO_FIXED(nod[i}.area
. Ylower=INT2TO_FIXED(nod[i].area
.Xlower=ENT_TO_FIXED(nod
Xupper=INT_TO_FIXED(nod
area
.area
k|.maxy);
kj.miny);
kj.minx);
k].maxx);
zfree (nod); // освободишь память
Внутренняя структура NODE практически не отличается от структуры
WadNodesFile. В нее ничего нового не добавлено:
typedef struct node
int krX; int krY; intdx; int dy; // позиция X // позиция Y // смещение от конца узловой линии по X И смещение от конца узловой линии по Y
BOUND locat[2]; // координаты правой и левой площадей
unsigned short ssect[2]; // субсектора или узлы правой и левой ветвей
(NODE;
А теперь, как я и обещал, забегу немного вперед и объясню вам, как работать
с рекурсивным деревом, как использовать структуру NODE для прорисовки
лабиринта. Это довольно просто, и я покажу вам это на примере из DOOM’a.
Для прорисовки стен вызывается функция ProcessTree таким образом:
ProcessTree (numnodes-1);,
где numnodes - общее количество узлов.
Рассмотри функцию ProcessTree:
void ProcessTree (register int nd)
{
register int m;
register NODE *n;
while (!(nd & 0x8000))
{
n = &anodes[nd]; // текущий узел
m = GetNextBranch_ofTree(PozX_ofPlayer, PozY_ofPlayer, n);
ProcessTree((unsigned int)n->ssect[m]); // обработать следующий узел
m Л= 1; И следующая ветвь
if (!CheckVisual_OppArea(&n->locat[m])) //проверка видимости
//противоположной области
return;
nd = n->ssect[m] & Oxfiff;
Draw_Subsector(nd & OxfflUfiT); // прорисовка субсектора
}
Рассмотрим работу этой программы на простом примере, изображенном
на рисунке :
(О)
Рис. Пример прорисовки лабиринта.
На этой схеме SO и S1 - субсектора. Они имеют номера соответственно
0x8000 и 0x8001. Так как у нас в данном случае только один узел, то в
функцию ProcessTree передается 0 (numnodes-1 == 1-1 = 0). В функцию
GetNextBranch ofTree передаются три параметра PozX ofPlayer и
PozYofPlayer - координаты игрока по X и Y соответственно и п - указатель
на текущий узел. Эта функция проверяет, с какой стороны от линии раздела
находится игрок и возвращает:
О - если с левой стороны,
1 - если с правой стороны.
В нашем случае эта функция возвращает 0, т.к. игрок находится с левой
стороны от линии раздела. На левой стороне находится субсектор S1 с номером
0x8001. Таким образом вызывается далее функция ProcessTree с номером 0x8001.
Т.к. 15 бит установлен, то вызывается функция Draw Subsector, которая
осуществляет прорисовку сегментов стен этого субсектора в видимой области.
После этого происходит возврат обратно в первоначальную функцию
ProcessTree и управление передается функции CheckVisual ОррАгеа. Это
достаточно сложная функция. Она проверяет путем аксонометрических
преобразований, не захватывает ли обзор игрока пространство, относящееся к
противоположной ветви (в нашем случае - это субсектор S0). И если не
захватывает, то возвращает 0. В нашем случае эта функция возвращает 1, т.к.
игрок видит часть субсектора S0. В этом случае прорисовывается субсектор с
номером 0x8000.
Таким образом, вы видите - какова цепочка узлов (сколько звеньев в этой
цепочке), такова вложенность функции ProcessTree.
Подробнее о функциях GetNextBranch ofTree и CheckVisual OppArea мы
расскажем позже.
7.8. Ресурс SECTORS. (Восьмой объект)
SECTOR - это горизонтальная область на карте, где определяются высоты
пола и потолка, освещенность, а также текстуры для полов/потолков Сектора
могуч быть различных форм, причудливых очертаний. Они могуч быть
большими и маленькими. В одном помещении может быть определено
несколько секторов. В помещении - любое изменение высот пола и потолка или
яркости - это новый сектор (и следовательно, отдельный LINEDEFS и SIDEDEFS).
Каждый “сектор” состоит нз 26 байт, включая два значения типа short, две
строки по 8 символов типа char и затем 3 значения типа short:
(1) высота пола для этого сектора;
(2) высота потолка для этого сектора;
Разница в 24 между высотами полов двух соседних секторов является
проходимой для игроков и монстров (при условии, что они поднимаются
вверх), ио уже 25 является непреодолимым препятствием для них (слишком
высоко). Для игрока при движении вниз (если высота сектора, на котором
он находится, больше, чем высота сектора, на который ои ступает)
препятствий нет Это просто обрыв. Но при любой высоте обрыва игрок не
разбивается. Для монстров же обрыв высотой 25 является препятствием. И
на другой сектор они попасть сами не могут, если только им ие помочь.
(При убийстве монстра атрибут “боязни высоты” у них сбрасывается и,
следовательно, монстр может улететь в пропасть.
(3) имя текстуры для пола из директории файла ресурсов;
(4) имя текстуры для потолка из директории файла ресурсов;
Все имена в файле ресурсов между F START и F END определяют имя
картинки для полов/потолков. Имя F SKY1 является условным индикатором
прозрачности, где надо изобразить небо. Размер текстуры дтя полов/потолков
всегда фиксирован (кроме текстуры для неба) и имеет разрешение 64x64. Для
неба размер текстуры всегда равен 256x128.
(5) яркость сектора. О - минимальная освещенность (темно, ничего не видно),
255 - максимальная освещенность (да будет свет);
(6) специальный сектор (смотрите далее пункт 7.8.1);
(7) номер переключателя (триггера), соответствующий определенному
номеру переключателя в конкретном LINEDEF. Когда пересекается данная линия,
то с этим сектором что-нибудь происходит (ои либо опускается, либо
поднимается, или меняется освещенность).
7.8 .1 Типы секторов
Этот номер управляет способом изменения освещения сектора и тем,
насколько данный сектор губителен дтя здоровья игрока при нахождении
последнего в д анном секторе. Значение -10/20% означает, что у игрока с каждой
секундой, пока он находится в этом секторе, отнимается 10 процентов энергии
на первом уровне сложности и 20 процентов - на остальных уровнях. Если
игрок имеет броню (armor), то энергии отнимается меньше в зависимости от
экипировки, но экипиировка со временем (пока ои находится в этом опасном
секторе) также убывает.
Все эффекты освещенности означают то, что уровень яркости дтя этого
сектора непостоянен (ие всегда имеет значение, соответствующее 5-ому полю
ресурса SECTORS). Уровень яркости может меняться от минимального
освещения одного из соседних секторов до значения, определенного дтя данного
сектора (в 5 поле). Если нет соседнего сектора, у которого было бы освещение
ниже, чем в данном секторе, то эффекта освещенности не будет. “Колебание
света” означает, что уровень освещенности плавно меняется с максимального
значения до минимального и наоборот. Например, через 2 секунды свет
проходит от максимального значения (255) до минимального (0) и идет назад.
Номера специальных секторов:
0 - нормальный сектор, эффект отсутствует;
1 - свет мерцает со случайным периодом;
2 - свет мерцает с периодом в 0.5 секунд (после начала уровня мерцание
начинается через случайное время в пределах 0.5 секунд);
3 - свет мерцает с периодом в 1 секу ндт,- (после начала уровня мерцание
начинается через случайное время в пределах 0.5 секунд);
4 - -10/20% здоровья и свет мерцает с периодом 0.5 секунд;
5 - -5/10% здоровья;
7 - -2/5% здоровья (пол с кислой средой);
8 - колебание света;
9 - “секретный” сектор. Игрок должен переместиться на этот сектор, чтобы
найденный секрет был засчитан. Процентное соотношение найденных секретов
выдается в конце уровня после его прохождения;
10 - потолок опускается через 30 секунд после начала уровня;
11 -10/20% здоровья. Если энергии остается меньше 10%, то уровень
заканчивается. Если это 8 уровень, то заканчивается эпизод;
12 - свет мерцает с периодом в 1 секунду (после начала уровня мерцание
начинается немедленно);
13 - свет мерцает с периодом в 0.5 сшвнд (после начала уровня мерцание
начинается немедленно);
14 - потолок опускается через 300 секунд после начала уровня;
16 -10/20% здоровья.
Остальные значения типов секторов в DOOM 1 недопустимы. В соответствии
с перечисленными номерами можно определить имена специальных секторов
для упрощения использования их в игре:
// special sectors
//define SS_RANDOM_LIG_BLNK 1
«define SS_LIG_BLNK_05S 2
«define SS_LIG_BLNK_10S 3
«define SS_HEALTH10_LIG_BLNK_05S 4
«define SS_HEALTH5 5
«define SS_HEALTH2 7
«define SS_LIG_OSCIL 8
«define SS_SECRET_SEC 9
«define SS_CEILING_30S_DOWN 10
«define SS_HEALTH10_END 11
«define SS_LIG_BLNK_10S 12
«define SS_LIG_BLNK_05S 13
«define SS_CEILING 300S 14
«define SS_HEALTH 1016
Здесь первые два символа SS означают Special Sector.
7.8.2 Загрузка и обработка ресурса SECTORS
Функция L Sectors загружает ресурс SECTORS и формирует внутреннюю
структуру SECTOR:
void L_Sectors (register int num) // загрузка ресурса SECTORS
!
register int i;
struct WadSectorFile // структура ресурса SECTORS
short heig floor, // высота пола
short heig ceil, // высота потолка
char name_floor[8]; // имя текстуры для пола
char name_ceil[ 8], // имя текстуры для потолка
short bright; // освещенность
short spec; // специальный сектор
short trig; // номер триггера
} *sec;
NumSectors = lumplen(num) / sizeof(struct WadSectorFile);
asec = (SECTOR *)zmalloc(NumSectors*sizeof(SECTOR), NULL, 50);
stos (asec, NumSectors * sizeof(SECTOR));
sec = ReadLumpForNum(num, 1);
for (i=0; i<NumSectors; i++)
asec i
asec i
asec i
asec i
asec i
asec i
asec[i
asecfi
.floorZ = INT_TO_FIXED(sec[i].heig_floor);
.ceilZ = INT_TO_FIXED(sec[i].heig ceil),
NumFloor = FlatNumForName (sec[T].name_floor);
NumCeil = FlatNumForName (sec[i].name_ceil);
light = sec|i].bright;
.special = sec|i].spec;
plat = sec[i], trig;
.ThingsInSector= NULL;
zfree (sec); // освободить буфер
Внутренняя структура SECTOR весьма внушительна. Я тут не буду
перечислять, где в игре она используется, т.к. она используется практически
везде, и мы еще не раз будем к ней возвращаться Итак, вот оиа:
typedef struct sectors
{
int floorZ; // высота пола
int ceilZ; // высота потолка
short NumFloor; // номер текстуры для пола
short NumCeil; // номер текстуры для потолка
short light; // уровень освещенности
short special; // номер специального сектора (см. пункт 7.8.1)
short plat; // номер триггера
int DinInSector, // шум в секторе при стрельбе используется для актива «и ши монстров
THING * ThingDin; // объект, который произвел шум (игрок)
int MaxYinMap, // максимальная координата Y сектора на карте
int MinY inMap, // минимальная координата Y сектора на карте
int Minx_inMap; // минимальная координата X сектора на карте
int MaxX_inMap; И максимальная координата X сектора на карте
И далее начальная часть структуры THING
И ее адрес передается в функцию
// обработки звука для стереоэффекта
THBIND t;
void (*funcXstruct thing *);
int korX;
int korY;
int korZ,
int CoimtUseSector; // уникальное значение, которое изменяется конкретным
//образом каждый раз, когда обрабятыi ается этот сектор.
//Используется для того,чтобы не делать дважды одну работу
THING *ThingsInSector, И список объектов, которые присутствуют в этом секторе
struct door * Active; И активизируемая дверь в секторе
int NumLines; И количество линий, ограничивающих сектор
LINE * * LinesInSector, // указатель на список линий, ограничивающих сектор
short ThunderLight; // освещение от грозы (в ори потальном DOOM’e отсутствует)
} SECTOR;
В структуре SECTOR имеются, на наш взгляд, данные, нуждающиеся в
пояснении. Как видно, не все поля в этой структуре затрагиваются в функции
L Sectors. Эти данные заполняются позже в функции GroupLines, до которой
еще дойдет очередь.
-DinlnSector - это параметр, ненулевое значение которого означает, что в
секторе был произведен шум от звука выстрела и произошел переполох среди
монстров. При этом шум необязательно может производиться в этом секторе.
Если между секторами нет препятствий, поглощающих звук, то последний может
распространяться довольно далеко через десятки секторов. После того, как был
произведен шум, активизируемые монстры направляются в сторону эпицентра
шума и ищут тот объект (игрока), который нарушил их спокойствие.
-ThingDin - это объект (указатель на THING), который произвел шум в
секторе. Это его преследуют “разбуженные” монстры.
-Max Y in Map, MaxX InMap, Min Y InMap, MinX InMap - это, выражаясь
математическим языком, координаты вершин прямоугольника, в который
вписан данный сектор, т.е. крайние координаты по осям X и Y для данного
сектора:
XI . Y2 Х2, Y2
X1.Y1 Х2. Y1
Эти координаты используются в DOOM’e для контроля соприкосновения
объектов с опускающимися потолками, которые опускаются с целью
раздавить все, что под ними находится.
-1, func, korX, korY, korZ - это начальная часть структуры THING (смотрите
пункт 7.1.5.). Зачем это нужно? Почему' бы, в таком случае, ие воспроизвести
здесь целиком структуру THING? Отвечаю последовательно на оба вопроса:
во-первых, адрес этой части передается в функцию FXSound. Формат вызова
этой функции такой:
FXSound (THING *obj, int fx_num).
Зачем тут нужен указатель на объект obj? Это очень просто, obj - это
источник звука, у которого имеются координаты X и Y. (координата Z нам не
нужна). По этим координатам можно определить положение этого источника
относительно игрока (справа, слева). В звуковой функции это положение
используется для обеспечения стереоэффекта. Например, если объект
находится правее игрока, то звук будет исходить из правого динамика. При
этом глубина стереоэффекта зависит от угла зрения игрока на объект.
Максимальная глубина (разнос между каналами) обеспечивается при углах 90
градусов (для правого уха) и 180 градусов (для левого уха). Но координаты
объекта используются не только для стереоэффекта. Пожалуй, основное их
предназначение - определение расстояния до игрока. Чем дальше находится
объект от игрока, тем меньше громкость звучания для этого игрока. При этом,
если объект слишком далеко, то редукция звука настолько велика, что его не
слышно. Как вы поняли, для функции FXSound в структуре THING важны только
два параметра: korX и korY.
Ответ на второй вопрос непосредственно вытекает из ответа на первый
вопрос. Если в структуре THING нас интересуют только два параметра, которые
находятся почти в самом начале структуры, то зачем городить целиком структуру
THING внутри структуры SECTORS? Это было бы неоправданным
расточительством памяти. Поэтому' мы используем только часть структуры
THING.
У вас наверняка “вертится на языке” вопрос: а зачем нам вообще тут нужен
звук, да еще и с обеспечением стереоэффекта и регулировкой громкости в
зависимости от дальности. Объясняю: это необходимо для озвучивания
подвижных секторов, к которым относятся двери, платформы, лифты,
лестницы и другое. Ведь известно, что, например в DOOM’e все двери
являются секторами. У этих узких секторов может опускаться или
подниматься потолок. В этом и заключается эффект закрывания и
открывания дверей. То же самое относится и к другим перечисленным
объектам. Для каждого из секторов вычисляются параметры korX и korY
как середина этого сектора. При этом, например, озвучание открывания
двери для сектора sec (сектор двери) будет выглядеть так:
IXSoimd( (THING *)&sec->t, SFXJDOOR_OPEN).
Координаты korX и korY вычисляются в функции GroupLines, о которой
речь пойдет ниже.
-CountUseSector - это значение является индикатором того, что данный
сектор уже обрабатывался. Оно используется, в частности, при проектировании
спрайтов, которые находятся внутри сектора. При этом перед началом
обработки секторов инкрементируется некое значение Counter. А при вызове
функции проектирования объектов, находящихся внутри конкретного сектора,
проверяется, обрабатывался ли уже данный сектор путем сравнения значения
Counter со значением CountUseSector, относящимся к конкретному сектору. И
если окажется, что их значения совпали, значит, этот сектор уже обрабатывался
и повторно обрабатывать его не надо. Вы, конечно, можете возразить: почему
бы последовательность обработки не построить таким образом, чтобы дважды
один объект не попадался. А в этом кроется специфика системы 3D-Engine, где
обработка субсекторов и секторов организована в виде двоичного
рекурсивного дерева, а не последовательной обрабатки каждого сектора.
-ThingsInSector - это указатель на первый объект, подлежащий обработке,
находящийся в этом секторе. Обработка объектов, находящихся в конкретном
секторе, строится по такой схеме:
THING *t;
SECTOR *se;
t = se-> ThingsInSector,
while (t)
II обработка объекта t
t = t->NextTh
Обработка объектов ведется до тех пор, пока объект t не станет равным
нулю, т.е. все объекты исчерпаны в этом секторе.
-Active - этот параметр используется при активизации подвижных
секторов типа дверей, лифтов и др. Это указатель на специальный класс
объекта. Его ненулевое значение говорит о том, что сектор активизирован.
Например, вы открыли дверь, и она должна через определенное время
закрыться. При этом в нормальном состоянии двери (закрытом) для данного
сектора значение Active будет равно нулю. При открытии двери ненулевое
значение Active будет указывать на объект “дверь” (структура door). И в
этом состоянии оно будет оставаться до тех пор, пока дверь не закроется
При закрывании двери объект door ликвидируется (zfree), и параметру Active
присваивается значение нуль.
-NumLines, LinesInSector - эти два параметра определяют форму сектора.
Это, соответственно, количество и список линий, ограничивающих сектор
Сектор может быть любой формы и любого размера. Он может состоять из
любого количества линий. Для замкнутого пространства количество линий
должно быть ие меньше трех. Поэтому для одного сектора минимальное
количество линий - три. Ограничений сверху практически ист, а теоретически -
65535. Этого количества больше чем достаточно. Список линий для секторов
заполняется в функции GroupLines.
-ThunderLight - этот параметр в DOOM’e ие используется. Но я его
использовал в своей игре для имитации вспышки молнии. Мы уже говорили об
анимации для неба. Так вот, когда небо имеет вид грозовых туч, иа нем иногда
вспыхивают молнии (случайным образом) Но ие было бы эффекта реальности,
если бы данная вспышка не освещала все вокруг. Для этого и введен параметр
ThunderLight. При вспышке молнии в ThunderLight пересылается значение light
этого сектора. После чего в light засылается.максимальное значение яркости
для сектора (255). При исчезновении вспышки молнии происходит обратная
пересылка: из ThunderLight в light, и сектор обретает свою нормальную яркость.
Таким образом, ThunderLight необходим лишь дтя сохранения параметра light.
Эффект вспышки молнии будет наблюдаться толью в том случае, если данный
сектор в нормальном состоянии не максимально освещен. Если же сектор имеет
довольно низкую освещенность, то вспышки молнии, которые спустя
определенное время сопровождаются раскатами грома, впечатляют.
7.9. Ресурс REJECT. (Девятый объект )
REJECT - это необязательный ресурс. Его целью является ускорение
обнаружения игрока монстром, когда они находятся в разных секторах. REJECT
может также использоваться дтя некоторых специальных эффектов. Это битовый
массив, где конкретная единица на пересечении определенной строки и столбца
определяет способ обнаружения игрока монстром в этом секторе (номер
столбца), когда игрок находится в другом секторе (номер строки). Размер REJECT
в байтах определяется как:
округленное в большую сторону (где SECTORS - общее
SECTORS2 ’
---------- количество секторов).
Вот пример данной таблицы из пяти
секторов:
Здесь единица означает, что монстр не
может активизироваться и начать атаку' на
игрока, пока не заметит его. О означает, что
ограничений нет. Все “неглухие” монстры
также активизируются при звуке выстрела
(включая удар кулака).
Смтар, где ивхадитея игрок
О i 1 2 3 4
0 1 0 0 °
>, ГД« К 1 1 0 1 1 о
2 0 1 0 1 0
X 9 0 1 1 1 0
э 4 0 0 1 0 0
'Активизированный” монстр будет спокойно преследовать игрока, но он не
будет его атаковать, если на пересечении столбца (сектор, где находится этот
монстр) и строки (сектор, где находится игрок) будет стоять " 1 ”. Таким образом,
все единицы в реусурсе REJECT определяют компанию миролюбивых
монстров, которые с угрожающим видом всюду преследуют игрока, но
фактически в бой с ним не вступают.
Каким образом этот битовый массив “запихнуть” в ресурс REJECT ? При
чтении слева направо и сверху вниз
первый бит относится к нулевому разряду 0-го байта;
второй бит относится к первому разряду 0-го байта;
девятый бит относится к нулевому разряду 1-го байта.
Таким образом, для нашего вышеупомянутого случая эта таблица будет
выглядеть так (надеюсь, что вы не забыли, что нулевой разряд находится справа):
101000100010100101000111 хххххххО (в шестнадцатеричном: А2 294700).
Таким образом, ресурс REJECT - это длинная строка битов: от самого младшего
значимого бига, до самого старшего (хотя старший может быть и незначимым,
если квадрат количества секторов не кратен восьми). Обычно, если монстр в
секторе А не может обнаружить игрока в секторе В, то справедливо и обратное:
игрок в секторе В тоже ие может увидеть монстра в секторе А. Таким образом,
если 0/1 установлено, то 1/0 так же должно быть установлено. На пересечении
одинаковых секторов (например, 0/0 или 3/3) в подавляющем большинстве
случаев биг сброшен. Однако дтя очень больших секторов бывают исключения.
Битовый массив REJECT используется в DOOM’e для ускорения обнаружения
игроков монстрами. В этом случае в начале функции CheckVisualOfThings, в
которую передаются два параметра - указателя на THING, проверяется бит на
пересечении сектора, в котором находится монстр (первый объект), и сектора,
в котором находится игрок (второй объект). Если это единица, то проверки
видимости объектов не производится, а осуществляется выход из функции
CheckVisualOfThings с возвратом “0”, что означает: игрок не обнаружен В
противном случае возвращается результат функции CheckLookThings,
которая проверяет, “видит” ли один объект другого. Если бы не было
массива REJECT, то это могло бы значительно затормозить игру, т.к.
функцию CheckLookThings пришлось бы вызывать для каждого монстра,
независимо от его местоположения относительно игрока. И тут кстати
заметить, что функция CheckLookThings одна из самых непростых и
запутанных, и, следовательно, она выполняется достаточно долго
Это один из примеров использования техники предварительных
вычислений для ускорения работы игры. Таких примеров в DOOM’e десятки.
Без данной техники DOOM просто невозможно было бы создать. В данном
конкретном примере предварительные вычисления осуществляются в редакторе
уровней игры. Но предрасчетныс массивы и структуры также формируются и
в процессе работы игры: при загрузке данных уровня, при проектировании 3D
лабиринта и во многих других случаях.
7.10. Ресурс BLOCKMAP. (Десятый
объект)
BLOCKMAP - предрасчетная структура, используемая в игре для
упрощения обнаружения границ стен и объектов. Если бы нс было
BLOCKMAP, то можно было бы свободно проходить сквозь стены и объекты.
Каждый уровень поделен на блоки. Всего их может быть на одном уровне
- 128. Один блок представляет собой квадрат (эти квадраты можно увидеть в
режиме автоматической карты, если нажать “G” - Grid или “сетка”).
Первые два числа BLOCKMAP: XORIGIN и YORIGIN-это координаты левого
нижнего угла карты уровня. Эти два числа обычно иа 8 значений меньше
минимальных координат X и Y, которых могут достигать вершины в LINEDEFS.
Далее следуют два числа - количество столбцов (COLUMNS) и количество
строк (ROWS), которые соответственно определяют, сколько блоков
используется на уровне в направлении X и сколько в направлении Y Этих
блоков должно быть столько, чтобы они захватывали все вершины в ресурсах
LINEDEFS. Если имеются вершины, которые не охватывают BLOCKMAP, то в
данном районе карты можно будет свободно перемещаться сквозь стены и
объекты, т.е. границы учитываться не будут. При этом могут возникнуть
проблемы, которые визуально могут отображаться как зеркальные эффекты.
Далее следуют (ROWS * COLUMNS) указатели на список границ для
конкретного блока. “Направление” этих указателей: направо (восток) и затем
вверх (север). Так, первый указатель соответствует строке 0, столбцу 0; второй
указатель - строке 0, столбцу' 1, а когда дойдет до последнего столбца, то далее
начнется со строки 1, столбца 0 и т.д. Например, если на уровне 45 столбцов, то
46 столбец начнется со строки 1, столбца 0.
После всех этих указателей вдет список границ (блоков). Каждый из данных
описателей содержит список номеров LINEDEFS, т е. номеров линий,
которые частично или полностью присутствуют в этом блоке и которые
определены в ресурсах LINEDEFS (о ресурсах LINEDEFS речь шла в пункте 7.2).
Для примера, если координаты первого блока для вашего уровня в направлении
X изменяются от 0 до 127 включительно, а следующего - от 128 до 255
включительно, и имеется вертикальная линия с координатой по X 128, то эта
линия будет принадлежать второму блоку, хотя кажется, что оиа находится на
границе блоков. Все сказанное также относится к координате Y.
“Пустой” блок состоит из двух чисел 0 и -1. Количество чисел (номеров
линий) в “непустом” блоке определяется как количество линий, которые
частично или полностью включает этот блок, плюс два. Эти “два” включают: (0)
- в начале списка и (-1) - в конце списка. Например, список 0,234,235,329,-1
означает, что данный блок включает линии 234,235 и329. Если блок включает 0-
ую линию, то тут никаких особенностей нет и список будет выглядеть точно так
же: 0,0,... ,-1.
Подводя итоги, можно обобщить все сказанное в таком описании ресурса
BLOCKMAP:
Координата начала сетки блока по X;
Координата начала сетки блока по Y;
Количество столбцов (количество блоков в направлении X)
Количество строк (количество блоков в направлении Y)
Смещение Блока 0 от начала BLOCKMAP, тип short
Смещение Блока N-l (N = число столбцов * число строк)
Список линий для блока 0:
0, номер каждого LINEDEF в блоке 0, -1 (Oxfiff)
Список линий для блока N-1:
0, номер каждого LINEDEF в блоке N-l, -1 (Oxfiff)
Z10.1. Загрузка ресурса BLOCKMAP
Функция L BlockMap осуществляет загрузку ресурса BLOCKMAP:
void L_BlockMap (int num) // Загрузка ресурса BLOCKMAP.
int i;
dat blkmap = ReadLumpForNum(num,50); // загрузка BLOCKMAP
blkmap = (short *\dat_blkmap + 4); // адрес указателей на линии
МарХ = INT_TO_FIXED(dat_blkmap[0]); // нач. координата по X
MapY = INT_TO_FIXED(dat_blkmap[l]); // нач. координата по Y
i=dat_blkmap[2] * dat_blkmap[3] * sizeof(THENG *);
BlocksX = dat_blkmap[2]; // количество столбцов
BlocksY = dat_blkmap[3]; // количество строк
ThingsInBlock = (THING **)zmalloc (i, NULL, 50);
stos (ThingsInBlock, i); // очистка списка движ. объектов
}
Переменная ThingsInBlock является списком движущихся объектов для
каждого блока. При каждом движении объекта этот объект стирается из
предыдущего блока (в соответствии с его предыдущими координатами) и
заносится в новый блок (в соответствии с его новыми координатами). Это
необходимо для контроля столкновения с движущимися объекгамй.
7.11. Функция GroupLines
Функция GroupLines вызывается после загрузки данных DOOM’a. Она
предназначена для осуществления некоторых предварительных вычислений,
заполнения некоторых полей в структурах, которые мы рассматривали выше.
Рассмотрим шаг за шагом те действия, которые выполняет функциия GroupLines:
1. Заполнение поля SectSeg в структуре SSECTOR Т.к один субсекгор всегда
находится внутри только одного сектора, то вполне разумно в структуре
SSECTOR иметь номер этого сектора. С учетом того, что одна сторона может
принадлежать только одному' сектору' и в структуре SIDE имеется поле SecSide,
определяющее номер сектора для данной стороны, то номер сектора,
принадлежащего стороне любого сегмента, ограничивающего конкретный
субсекгор, и будет SectSeg.
2. Заполнение данных для структуры SECTOR Сначала в цикле по NumLines
(общее число линий) мы подсчитываем количество линий, ограничивающих
сектор с учетом того факта, что линии бывают односторонними и
двухсторонними. Далее в цикле по NumSectors (количество секторов) мы
заполняем в структуре SECTORS поле LincsInSector, которое представляет собой
список линий, ограничивающих данный сектор. Теперь мы подробней
остановимся на функциях PdLin и GranOb, которые используются в данном
цикле. Обе эти функции оперируют со структурой BOUND, которая определяет
координаты вершин прямоугольника:
typedef struct bound
XIower.Yupper
XIower.Ylower
Xupper.Yupper
X lipper,Ylower
{
mt Yupper, // максимальная координата по Y
int Ylower; // минимальная координата по Y
int Xlower; // минимальная координата по X
int Xupper, // максимальная координата по X
} BOUND;
Таким образом, структура BOUND - это прямоугольная область с
вышеперечисленными вершинами:
Функция PdLin является инициализирующей. Она у стана вливает граничные
начальные координаты для прямоугольника, т.е. максимальными координатами
по X и Y будет минимальное значениие int (0x80000000), а минимальными -
будет максимальное значение int (0x7tfflilf):
«define MINJNT 0x80000000
«define MAXJNT 0x7fHW
void PdLin (BOUND *buf)
buf-Yupper = buf->Xupper = MIN_INT,
buf-Ylower = buf->Xlower = MAXINT;
}
Функциия GranOb, на мой взгляд, в комментариях не нуждается:
void GranOb (BOUND *buf, int x, int y)
if (x < buf->Xlower)
buf->Xlower = x; // x меньше значения Xlower
else
if (x > buf->Xupper)
buf->Xupper = xjlx больше значения Xupper
if ( у < buf-> Ylower)
buf-> Ylower = у; // у меньше значения Ylower
else
if ( у > buf-Yupper )
buf-> Yupper = у; // у больше значения Yupper
А теперь мы объясним, зачем здесь используются эти функции. Они
необходимы для определения координат прямоугольной площади, в которую
40,60
вписан текущий сектор. Каким образом это
работает? Рассмотрим это на примере.
Допустим, у нас имеется треугольный сектор:
Буквами а, b и с здесь обозначены линии в
той последовательности, в которой они
обрабатываются в цикле, а цифрами -
координаты вершин.
30,20 b 50,20
После обработки линии “а” у нас получился следующий прямоугольник:
После обработки линии “Ь” площадь прямоугольника
увеличилась:
После обработки линии "с”
вершины прямоугольника не
изменились, т.к. эта линия уже
находится внутри прямоу-
гольника. Таким образом,
сектор, который ограничивают
линии “а”, “Ь” и "с”, вписан в
прямоугольник с координатами:
Xlower: 30;
40,60
30,20 b 50,20
Хиррсг: 50;
Ylowcr: 20;
Yupper: 60;
Функция типа GranOb имеет широкое распространение практически во
всех компьютерных играх. Ее используют, например, при выводе графики на
экран, когда экран не требуется обновлять постоянно. Представьте себе, что
вам необходимо вывести на экран 3 простых спрайта:
Обычно во всех играх по отдельности спрайты на экран не выводятся. Они
предварительно выводятся в
буфер размером с экран
(теневой экран), и только
после того, как информация
в буфере подготовлена
полностью, весь буфер (при
постоянном обновлении
экрана) или его часть
выводятся на экран (видимая
часть). Но каким образом
можно узнать, какую часть
буфера выводить на экран?
Ведь спрайтов и образов,
выводимых в буфер, могут
быть десятки и даже сотни.
Для этих целей и используются функции PdLin и GranOb. Первым делом перед
подготовкой изображения в теневом экране вызывается инициализирующая
функция PdLin. После этого перед выводом очередного спрайта в буфер (или
после его вывода - это не важно) дважды вызывается функция GranOb, как это
сделано в функции GroupLines Причем порядок передаваемых параметров и в
первом и во втором вызове не имеет никакого значения. Например, можно
использовать в первом вызове минимальные координаты спрайта (MinX, MinY),
а во втором - максимальные (MaxX, MaxY):
GroupI .ines(&area, MinX, MinY);
GroupLines(&area, MaxX, MaxY);
Здесь area - координаты вершин обновляемой на экране области. Само
собой, что в функцию PdLin так же передается параметр area.
Функции GroupLincs можно вызывать и так, хоть это и не так логично и
красиво:
GroupLines(&area, MinX, MaxY);
GroupL.ines(&area, MaxX, MinY);
Единственное, чего при этом необходимо придерживаться - это чтобы в
этих двух вызовах не повторялись координаты. Например, вот неверная
запись:
GroupLines(&area, MinX, MinY);
GroupLines(&area, MinX, MaxY);
При этом необходимо помнить, что первой координатой идет позиция по
X, а второй - по Y А теперь вернемся к нашему примеру с тремя спрайтами.
После вывода этих спрайтов в буфер мы имеем область area с координатами:
Xlower: 2;
Xupper: 14;
Ylowcr: 0;
Yupper: 6,
Эта область и копируется из буфера на экран. Данный подход используется
в DOOM’e для прорисовки статуе-панели, в экранных эффектах (“оплывание
или стекание изображения вниз”) и в некоторых других случаях.
Существуют и более оптимальные методы обновления экрана. Посмотрите
повнимательней на наш пример. Не правда ли, что оптимальнее было бы
копировать две области:
Первая область с координатами:
Xlower: 2;
Xupper: 8;
Ylower: 0;
Yupper: 5;
Вторая область с координатами:
Xlower: 10;
Xupper: 14;
Ylower: 2;
Yupper: 6;
Как вы поняли, в этом методе спрайты относятся к одной области только в
том случае, если они хотя бы немного пересекаются. В этом случае
используются более сложные алгоритмы проверки на пересечение областей
вывода. Данный подход используется, например, в играх: “Prince of Persia”,
“Aladdin” и некоторых других. Но мы эти методы рассматривать ие будем,
т.к. к DOOM’у они не относятся.
Вернемся к функции GroupLines. Зачем нам нужны координаты
прямоугольной области, в которую вписан сектор? Они нужны, чтобы
приблизительно определить центр сектора. Координаты центра сектора,
как мы говорили выше, используются при вызове звуковых функций для
озвучивания Скрипа дверей, гидравлических лифтов и др.
3. Последним пунктом в функции GroupLines в структуру SECTORS
записываются минимальные и максимальные координаты сектора в
автоматической карте (смотрите пункт 6.10). При этом используются данные
прямоугольной области, вычисленные ранее.
Исходный текст функции GroupLines:
void GroupLines (void) // группы линий
BOUND stak;
int i jJc
register LINE ** adr;
register SECTOR *se; //1. Заполнение поля SectSeg в структуре SSECTOR
for (i=0; i<numssec; i++)
assec[i].SectSeg=asegs(assec[i].segs].SidSeg->SecSide;
} // подсчет числа линий, orpaj (ичиьающих сектора
j = 0;
for (i=0; i<NurnLines; i++)
{
j++;
alin[i].Srig->NumLmes++;
if(alin[i].Slef&&alin[i].Slef != alinfi].Srig )
{
j++;
alinf i] .Slef->NumLines++;
}
} // выделить память для списка линий
adr = zmalloc(i*sizeof(LINE **), NULL, 50);
for ( i=0; i<NumSectors; i++ )
{
se = &asec[i],
PdLin (&stak),
se->LinesInSector = adr; // список линий в секторе
for ( j=0; j<N umLines; j++ )
{
if( alin[j] Srig == se || alin[j].Slef== se )
{
adr[0] = &alin[j];
GranOb (&stak,alin[j].FromVert->krX,
alin[j] .From Vert->krY);
GranOb (&stak,alin[j].ToVert->krX,
alin[j].ToVert->krY);
adr-н-,
}
if ( se->NumLines!=((intyadr)-se-LinesInSector) )
ExitWithMsg (“PGroupLines: miscounted”);
se->CenterSectorX = (stak.Xupper + stak.Xlower)» 1,
se->CenterSectorY = (stak.Yupper + stak.Ylower)» 1;
k = (stak. Yupper - MapY + 0x200000)» 23;
if (k >= BlocksY)
k = Blocks Y-l;
se->MaxY_inMap = k, II максимальный номер блока но Y на карте
k = (stak.Ylower - MapY - 0x200000)» 23;
if( к < 0 )
к = 0,
se->MinY_inMap = к; // минимальный номер блока по Y на карте
k = (stak.Xupper - МарХ + 0x200000) » 23;
if ( к >= BlocksX)
k=BlocksX-l;
se->MaxX_inMap = к; II максимальный номер блока по X на карте
k = (stak.Xlower - МарХ - 0x200000)» 23;
if( к < 0 )
к = 0;
se->Minx_inMap = к; // минимальный номер блока по X на карте
} }
7.11. Функция StartSectors
Функция StartSectors осуществляет начальную инициализацию
специальных секторов и записывает начальные значения для анимации стен и
полов, для движущихся платформ. Подробнее о некоторых из функций
инициализации мы поговорим при рассмотрении классов объектов, а сейчас,
дабы вас нс запутать, я себе позволю лить предварительные комментарии, и
введу вас в курс дела. Для того, чтобы специальный сектор действовал
(например, при мигании света), необходимо его обрабатывать каждый цикл
игры (подсчитывать счетчик, изменять величину яркости и др ). Для этих
целей в DOOM’e создастся особый класс объекта (класс LIGHT), и
обрабатывается этот объект в том же цикле, что и все остальные объекты в
DOOM’e (например, монстры). Функции инициализации специальных
секторов создают данные виртуальные объекты.
void Start Sectors (void) //предварительная обработка специальных секторов и анимации
register SECTOR * se;
register int i;
for (i=0; i<NumSectors; i++)
{
se = &asec[i];
if (se->special)
{
switch (se->special)
{ II инициалirvu ля случайно мерцающего света
caseSSRANDOMLIGBLNK:
InitRandLightBlinks(se);
break;
// инициализация света с периодом в 0.5 секунд
case SS_LIG_BLNK_05S: InitLightBlinks (se, 15,0);
break;
11 инициализация света с периодом в 1 секунду
case SS_LIG_BLNK_10S: InitLightBlinks (se, 35,0);
break;
// инициализация света с периодом в 0.5 секунд
И агрессивная среда
case SS_HEALTH10_LIG_BLNK_05S:
InitLightBlinks (se, 15,0);
se-special = SS_HEALTH10_LIG_BLNK_05S;
break;
// инициализация мерцающего света
case SS_LIG_OSCIL: InitLightOscil (se);
break;
11 секретный сектор
case SS_SECRET_SEC: Secret++;
break;
11 потолок опускается через 30 секунд после
//начала уровня
case SS_CEILING_30S_DOWN: ActivateDoorAfter30S (se);
break;
II инициализация света с периодом в 1 секунду
case SS_LIG_BLNK_10S: InitLightBlinks (se, 35,1);
break;
// инициализация света с периодом в 0.5 секунд
case SS_LIG_BLNK_05S: InitLightBlinks (se, 15,1);
break;
// потолок опускается через 300 секунд после
//начала уровня
case SS_CEILING_300S: ActivateDoorAfter300S (se, i);
}
}
}
NumAnimWalls = 0;
for ( i=0; i<Numl.ines; i++ )
{
if ( alin[i].Type == LT ANIMAIED )
AnimWalls[Num_AnimWalls++] = &alin[i];
// список анимационных стен
)
stosd ((int *)&ActiveCeils[O], О, MAXNUMCEILS);// активные потолки
stosd ((int *)&ActivePlats[O], 0, MAXNUMPLATS);// активные
//платформы
for ( i=0; i<MAXNUM_ACTIVEBUTTONS; i++ )
{
stos (&ActiveButtons[i], sizeof(struct button));
И активные кнопки
}
} }
Активными платформами, потолками или кнопками являются те
объекты, которые нуждаются в обработке. Оии обрабатываются каждый
цикл игры до тех пор, пока не отпадет в них надобность. Например, при
открытии двери, которая должна через 10 секунд закрыться, все время,
начиная с момента открытия, виртуальный объект “дверь” будет активным
и будет обрабатываться до тех пор, пока дверь не закроется. После закрытия
двери объект уничтожается, и данная дверь становится не больше, чем
обычной стеной, пока ее кто-нибудь опять не откроет.
7.12. Функция RefreshTextures
Мы не будем подробно останавливаться на этой функции из-за се
чрезвычайной простоты. Основное ее предназначение - увеличение скорости
работы программы. Каким образом это достигается? Как мы уже говорили
выше, все графические данные в DOOM’e кэшируются. На этом и основан
принцип действия функции RefreshTextures. Она просто загружает все
используемые на данном уровне текстуры и спрайты в память. Нетрудно
догад аться, что, если в машине установлено много оперативной памяти (больше
8 мегабайт), то ни раз}' за время прохождения уровня не будет обращения к
диск}', и скорость существенно повысится. Если же оперативной памяти
установлено мало (около 4 мегабайт), то будет довольно частое обращение к
диск}', особенно если на уровне довольно большое разнообразие текстур и
спрайтов. Функция RefreshTextures не является обязательной. Но она
значительно влияет иа скорость работы игры. Из всех функций загрузки
объектов DOOM’a эта функция самая медленная, т.к. текстур и объектов в
игре используется очень много. Быстродействие данной функции в
значительной степени зависит от фрагментации файла doom, wad
Вызов функции RefreshTextures осуществляется только в том случае, если
переменная RefreshText отлична от 0. Эта переменная равна нулю только в
том случае, если этот уровень уже загружался в предыдущий раз. Например,
после гибели игрока при нажатии “Space”, а также при проигрывании Demo
файла.
Глава 8
Основные виртуальные процессы
в 3D играх.
В большинстве случаев весь цикл игры можно условно поделить на три
этапа:
1. Управление в игре;
2. Обработка данных (основные процессы);
3. Работа с графикой (вывод на экран);
Первый пункт мы уже рассмотрели в главе 6. Данную главу мы посвятим
BTopoNty пункту7.
Что такое виртуальные процессы игры? К ним я отношу всю обработку7
данных, которая не имеет видимого отношения к графике. Это создание
виртуальных таблиц, создание и обработка внутренних структур данных,
обработка объектов, алгоритмы искусственного интеллекта, это воплощение
сценария игры, всей ее схемы. Я не отношу7 сюда работу7 с меню, диалоги,
системы сообщений. Это тема отдельного разговора. В большинстве
современных игр виртуальные процессы занимают львиную долю программы.
Возьмем, например, шахматы. Здесь большая часть программы сосредоточена
на просчете возможных комбинаций, “обдумывании” хода, и лишь малая часть
кода (в сравнении с общим кодом программы) отвечает за вывод результата на
экран.
DOOM не является исключением. Хотя вывод 3-х мерного лабиринта на
экран - задача не из простых, тем не менее функций отображения несоизмеримо
меньше, чем функций виртуальной обработки. Это можно объяснить. Дело в
ТОм, что во всех играх существует огромное количество объектов, типов
объектов, классов объектов. Каждый тип или класс объектов имеет свои функции
обработки, свои алгоритмы обработки. Вывод же на экран чаще всего,
независимо от типа объекта, осуществляется одной общей функцией вывода
спрайтов. Это, конечно, если не считать специальных эффектов, например,
вывод прозрачного объекта.
Все изменения структур иа втором этапе игры воплощаются в графику7 на
третьем этапе игры. Но об этом мы поговорим позже (в главе 9).
Функции GeneralControl.
В DOOM’e все виртуальные процессы сосредоточены в функции
GeneralControl.
Функция GeneralControl обрабатывает переменные ChangeMode и
GameMode, назначение которых мы рассматривали в пункте 4.2 Здесь
переменная ChangeMode изменяет нормальный ход игры (если ее значение
отлично от 0), после чего принимает значение 0.
Переменная GameMode определяет один из четырех режимов обработки.
Исходный текст функции GeneralControl:
void GeneralControl (void) И основные процессы в игре
{
register GAME CONTROL *up;
register char ch;
if ( chuv->PlayerStatus == PS_START_AFTER_DEAD )
InitRespawnPlayer(); И инициализация восстановления
И игрока после смерти
while ( ChangeMode)
{
switch ( Changelylode)
{
case 1: // восстановление состояния после “убийства” игрока
LoadCurLev();
break;
case 2: // начало игры (загрузка данных уровня) (New Level)
loadlev(InitCurSkill, InitCurEpisode,
InitCurMap);
ChangeMode = 0;
break;
case 3:1I восстановление записанной игры (Load Game)
PlaySav();
break;
case 4:1I запись игры (Save Game)
SaveGame();
break;
case 5:1I инициализация режима демонстрации
RunDemoQ;
break;
case 6: // конец уровня
EndLevel();
break;
case 7: // конец эпизода
EndEpisode();
break,
case 8: // переход на следующий уровень
CurMap = StatEnd.next+1;
GameMode = 0;
LoadCurLevQ;
ChangeMode = 0;
InitDrawBorder = 1;
break;
case 9:1I запись копии текущего экрана на диск (Screen Capture).
GifSave();
ChangeMode = 0;
}
}
up = &chuv->Control;
inove(up, PlayerControl, sizeof(GAME_CONTROL));
if ( PlayDemo ) // проигрывается DEMO файл
! if ( (ch = Bufr| ()])== 0x80 )
EndRecord(); // конец DEMO файла
else
up->Move = ch;
up->Strafe = Bufr[l];
up->DirControl = (short)Bufr[2] « 8;
up->PressedKeys = Bufr[3];
Bufr += 4,
}
if (SaveRecord )
if( KeyPressedf'q’])
EndRecordQ; 11 закончить запись DEMO файла
Bufr[0] = up->Move;
Bufr 1 = up->Strafe;
Bufr 2 = (charXup->DirControl» 8);
Bufr[3] = up->PressedKeys;
UpdateRecordFrame(up); // записать один фрагмент DEMO
ch = chuv->Control.PressedKeys;
if ( ch & PRJPAUSE)
{
switch ( ch & 3 ).
case 1: if (pause Л= 1) // нажата клавиша ‘PAUSE’
MusicOff();
break;
case 2: if( !NamSave[0])
mov9(NamSave, “NET GAME”);
ChangeMode = 4; // инициализация
//записи игры под именем ‘NET GAME’
CurNutnSave = (ch & 0x1 c)» 2;
}
switch (GameMode)
{ // нормальная игра
case 0: ProcessingObjects(); // обработка объектов
ProcessingStatus(); II обработка статуса
ProcessingMap(); И обработка автоматической карты
ProcessingStringO;// обработка строк в верхней
//части экрана
break;
// Конец уровня
case 1: ProcessingEndLevel(); И функции обработки конца
// уровня (статистика)
break;
//Конец игры
case 2: EndGame(); И функции обработки конца
// игры (финальной заставки)
break,
// Стартовая заставка
case 3: DelayTitlePicQ, // функция задержки стартовой
// заставки
} 1
8.1. Структура DEMO файла
В игре DOOM возможна запись и воспроизведение DEMO файлов. Это
означает, что при записи DEMO файла все ваши действия в игре запоминаются,
с тем чтобы в будущем можно было бы просмотреть вашу' игру. Эго очень
интересно, учитывая тот факт, что просмотреть ваш DEMO файл и оценить
уровень вашего мастерства могут также и посторонние люди.
Структура DEMO файла очень проста, и мы рассмотрим ее на примере
воспроизведения DEMO файла.
Функция RunDemo
Воспроизведение начинается с функции RunDemo:
void RunDemo (void)
{
ChangeMode = 0; //режим игры не менять
BufRecord = ReadLumpForName(NamDerno, 1); // чтение DEMO
// файла с именем NamDemo
Bufr = &BufRecord[3]; // текущий указатель на буфер DEMO
RefreshText = 0; II выключить режим предварит, загрузки графики
loadlev(BufRecord[0], BufRecordf 1], Bufl<ecord[2|),
PlayMode = 0; И указывает, что режим записи игры невозможен
RefreshText = 1; 7/ включить режим пред-арит. загрузки графики
PlayDemo = 1; // режим воспроизведения DEMO файла
}
Т.к. мы не учитываем режим сетевой игры, то DEMO файл немного
упростили (исключили из заголовка 4 байта, в которых единицы означали
присутствие конкретного игрока в игре). Как видно, первые три байта DEMO
файла соответственно определяют уровень сложности (Skill), Эпизод (Episode)
и Карту (Мар). Далее следует N структур GAME CONTROL, где N - количество
циклов игры в DEMO файле. Говоря нормальным языком, N определяет
продолжительность проигрывания DEMO файла. Символ 128
(шестнадцатеричное 0x80) является терминатором (концом) DEMO файла.
8.2. Обработка объектов
Пришло время подробнее поговорить об объектах. Мы уже говорили о
физических объектах, их структурах данных, об анимации. В этой главе мы
расскажем о классах объектов и о методах их обработки. Общая функция
обработки объектов выглядит так:
void ProcessingObjects (void) // об'дтбггка объектов
{
if (ipause)
if (!MenuInPlayMode || PlayDemo || chuv->LevelOfHead==l)
ProcessingPlayer(chuv); II обработка данных игрока
ProcessingThings(); // обработка объектов
ProcessingAnimated(); 7/управление анимацией
GameCount++, И счетчик циклов игры
}
}
Как видно, полная обработка объектов сводится к трем функциям:
ProcessingPlayer, ProcessingThings и Processing Animated.
8.2.1. Обработка данных игрока
Функция ProcessingPlayer.
Функция ProcessingPlayer отвечает за перемещение игрока, выбор
оружия и управляет некоторыми атрибутами игрока.
Исходный текст функции ProcessingPlayer:
void ProcessingPlayer (register MAN *m) // обработка данных игрока.
{
register GAME_CONTROL *up;
register THING *t;
register char ch;
register int I, j;
t = m->PlayerThing;
if ( m->cheat & CH_IDSPISPOPD) // CHEAT код: прохождение сквозь стены
t->attr |= TA_NO_CLIPPING;
t->attr &= ~TA_NO_CLIPPING;
up = &m->Control;
if (t->attr & TA SHOOTING ) // объект стреляет
up->DirControl = 0;
up->Move = 0x64,
up->Strafe = 0;
t->attr &= ~TA_SHOOTING,
}
if ( m->PlayerStatus == PSDEAD )
PlayerDead(m);// контроль нажатия “SPACE” для “оживления” игрока
return;
}
if ( t->DelayAgr ) // игрок парализован (без движения)
t->DelayAgr-;
else
ContPlayerToMoves(m);//nepeMemeHHe игрока в прос транстве (по X и Y)
ControlPlayerToKorZ(m);// контроль игрока по координате Z
if (t->ssect->SectSeg->special) // игрок в специальном секторе
PlayerlnS pecialSector(in);
if ( up->PressedKeys & PR_PAUSE )
up->PressedKeys = 0;
ch = up->PressedKeys;
if ( ch & PRDIGIT ) II выбор оружия
{
l = ((int)ch&Qx38)»3;
if (!1 && m->PresentWeapon[7] &&
(m->CurrentWeapon!=WP_CHAINSAW ||
!m->spec[SP_BERSERK]))
1 = WP_CHAINSAW;
if (m->PresentWeapon[l] && l!=m->CurrentWeapon)
{
if (1 == WPJPLASMAGUN || 1 == WP_BFG9000 )
if (! Share ware Vers)
m->Change Weapon = 1; 11 коммерческая версия
}
else
m->ChangeWeapon = 1;
}
if (ch & PR_SPACE )
{
if( !m->Use)
{
PressUse(m); // нажатие “SPACE” (откры ть дверь или
И переключить рубильник)
m->Use = 1;
}
}
else
m->Use = 0;
ProcessingWeapons(m); И работа с оружием
if (m->spec[SP_BERSERK]) И сила
m->spec[SP_BERSERK]++;
if ( m->spec[SP_INVULNERABILITY]) И неуязвимость
m->spec[SP_IN VULNERABILITY]-;
if ( m->spec[SP_PARTIAL]) // невидимость
{
if (. i.Gra->syec(SP _PAKUAL\\ \
m->PlayerThing-attr &= ~TA_FUZZ;
}
if ( m->spec[SP_LIGHT]) // освещенность
m->spec[SP_LIGHT]-;
if ( m->spec[SP_RAD_SHIELD] ) // радиационная защита
m->spec[SP_RAD_SHIELD]-;
if (m->CountPalBloodEyes) // счетчик палитры
in->CountPalBloodEyes-, // красных глаз
if( m->CountPalUseObj) // счетчик палитры
m->CountPalUseObj-; // предметов
1 = m->spec[SP_INVULNERABILITY];
if(l)
{
if( 1> 128 ||(1 &8))
m->palitra = 32;
else
m->palitra = 0;
return;
}
j = m->spec[SP_LIGHT];
if(j)
{
if(j> 128ЦО &8))
m->palitra= 1;
else
m->palitra = 1;
}
А теперь вкратце остановимся на каждой из используемых здесь функций.
Функция PlayerDead.
Давайте теперь разберемся, что могло бы произойти в реальном случае,
если бы игрока убили (ие дай бог, конечно). При падении игрока уровень его
глаз понижается относительно линии горизонта. С технической точки зрения
реализовать это очень просто. Необходимо лишь уменьшать каждый цикл игры
переменную height (высота) в структуре MAN:
if ( m->height > INT_TO_FIXED(6))
m->height -= INT_TO_FIXED(1);
if ( m->height < INT_TO_FIXED(6))
m->height = INT_TO_F1XED(6);
Оружие у игрока также постепенно убывает. Управляет всеми процессами
с оружием функция Processingweapons, о которой мы поговорим в разделе
8.2.3.
В DOOM’e реализован еще один весьма оригинальный эффект. Если игрока
постигла смерть от чей-либо руки (например, его убил сержант), то в момент
умирания он поворачивается к нему лицом, чтобы увидеть своего убийцу в
последний раз. Реализуется это так:
register THING *t, *tl;
register MAN *m;
register unsigned int 1, j;
t = m->PlayerThing,
tl = m->Enemy; 11 структуры данных врага
if(tl &&tl !=t)
{
II вычисление направление между двумя объектами
1 = Direction(t->korX, t->korY, tl->korX, tl->korY);
j = t
1 -= t->Ang;
if (1<INT_TO_FIXED(910) || 1>INT_TO_FIXED(-910))
t->Ang=j;
else
if ( 1 < MINJNT ) // MINJNT 0x80000000
t->Ang += INT_TO_FIXED(910); // изменение направления
else 11 игрока
t->Ang -= INT_TO_FIXED(910);
}
Функция ContPlayerToMoves.
Эта функция отвечает за перемещение игрока в пространстве. Используя
поля Move и Strafe в структуре GAME CONTROL, она модифицирует
приращения координат объекта по X и У (поля incX и inc Y в структуре THING):
void ContPlayerToMoves (register MAN *m)// перемещение игрока в пространстве
{
register GAME CONTROL *up;
register THING *t;
register unsigned int j;
register inti;
up = &m->Control; // указатель на структуру управления
t = m->Playerfhing; И указатель на структуру объекта
t->Ang += up->DirControl« 16; II направление движения
StandOnFloor = (t->korZ<=t-floorZ); И игрок твердо стоит на земле (не падает)
if ( up->Move && StandOnFloor )
{
1 = up->Move, // перемещение вперед/назад
1«= 11;
j = (unsigned int)t->Ang » 19;
t->incX += _fixmul(l, Cosinejj]); II приращение по X
t->incY += _fixmul(l, Sinejj ]); // приращение no Y
}
if( up->Strafe && StandOnFloor)
1 = up->Strafe; И перемещение влево/вправо
1«= 11;
j = (unsigned intXt->Ang - 0x40000000)» 19;
t->incX += fixmul(l, Cosine[j]); //приращение по X
t->incY += fixmul(l, Sine[j]); // приращение no Y
if( (up->Move || up->Strafe) &&
t->spr == &thlSF_PLAY] )
RunSprite(t,SF_PLAY+l); // при наблюдении со стороны игрок при любом
//перемещении передвигает ногами
}
Здесь мы впервые использовали очень важную функциию RunSprite. Эта
функция запускает анимацию спрайта с позиции, определенной вторым
параметром (в нашем случае это SF_PLAY +1). Подробно о методах анимации
мы рассказывали в пункте 7.1.4.
Функция ControlPlayerToKorZ.
Эта функция отвечает за все, что связано с вертикальными перемещениями
игрока: падение, положение глаз относительно горизонта, для большего
реализма игрок при ходьбе раскачивается. Также раскачивается оружие в его
руках.
void ControlPlayerToKorZ (register MAN *in) // контроль игрока по координате Z
{
register THING *t;
register int 1;
t = m->PlayerThing;
in->PozitionWeapon = (_fixmul(t-incX, t-incX) +_fixmul(t-incY, t-incY))» 2;
// Чем больше скорость игрока, тем больше значение
// PozitionWeapon в структуре MAN, которое определяет
// амплитуду’ раскалывания оружия в руках игрока.
if ( m->PozitionWeapon >INT_TO_FIXED(16)) // ограничение
m-PozitionWeapon = INT_TO_FIXED(16); // амплитуды
if (!StandOnFloor)
{
m->LevelOfHead = t-korZ+P_HEIGHT; // позиция глаз игрока
// P_HEIGHT определяет высоту игрока
// ( define P HEIGHT INT_TO_FIXED(41))
1 = t->ceilZ-INT_TO_FIXED(4); I I t-ceilZ: высота потолка
//над игроком
// 4 определяет расстояние от макушки головы игрока
// до уровня его глаз
if (1 < m-LevelOfHead)
m->LevelOfHead = 1; // потолок давит на игрока
// игрок в согнутом состоянии
else
m->LevelOfHead=m-height+t-korZ; // нормальное положение
// глаз игрока, определенное его высотой
// игрок в полный рост
}
else
!
1 = _fixmul(m-PozitionWeapon/2,
Sine[(GameCount * 0x199) & OxlfH]);
// вычисление коэффициента, определяющего раскачивание
// игрока при движении
if ( m->PlayerStatus == PS_NORMAL )
{
m->height += m->InertJumpZ; II инерционная коррекция
// положения глаз игрока
И InertJumpZ определяет ускорение
if (m->height>P HEIGHT)
{
m->height=P_HEIGHT; // нормализация положения глаз
m->InertJumpZ = О,
if ( rn->height < INT_TO_FIXED(32))
{
m->height=INT_TO_FIXED(32); // максимальное
//согнутое состояние при поднятии на ступеньку, после чего
И игрок начинает выпрямление
if ( m->InertJumpZ <= 0 )
m-> InertJumpZ = 1;
}
if (m->InertJumpZ)
{
m->InertJumpZ += 0x4000; 11 ускорение выпрямления
jif ( !m->InertJumpZ )
m->InertJumpZ = 1;
}
}
m->LevelOIHead = m->height+t->korZ+l; II окончательное
И вычисление позиции глаз игрока
1 = t->ceilZ - INT_TO_FIXED(4);
if (1 < m-LevelOfHead) // потолок давит на игрока
m->LevelOfHead = 1; // игрок в согнутом состоянии
}
}
Функция PlayerlnSpecialSector.
Эта функция вызывается, если игрок попадает в специальный сектор, О
специальных секторах мы говорили в пункте 7.8.1. Чаще всего пребывание в
специальном секторе не сулит игроку ничего хорошего. В большинстве случаев
это ведет к убыванию энергии.
Исходный текст функции PlayerlnSpecialSector:
void PlayerlnSpecialSector (register MAN *m) II игрок в специальном секторе
register THING *t;
register SECTOR *s;
t = m->PlayerThmg,
s = t->ssect->SectSeg;
if (t->korZ == s->floorZ ) II игрок стоит на земле
{
switch (s->special)
{
case SS HEALTH5: // -5/10% здоровья
if( !m->spec[SP_RAD_SHIELD] &&
!(GameCount & Ox If) )
Shooting(t, NULL, NULL, 10);
break,
case SSHEALTH2: // -2/5% здоровья. Пол с кислой средой,
if ( !m->spec[SP_RAD_SHELD] &&
!(GameCount & 0x1 f))
Shooting(t, NULL, NULL, 5);
break;
case SS HEALTH 10:11 -10/20% здоровья
case SSHEALTH10_LIG_BLNK_05S:
if ((!m->spec[SP_RAD_SHIELD] ||
Randoml () < 5) &&
1 (GameCount & Ox 1 f))
Shooting(t, NULL,, NULL, 20);
break;
case SS_SECRET_SEC: 11 “секретный” сектор
m->st.secrets++;
s->special = 0;
break;
case SS_I LEALTH10END:
// -10/20% здоровья. Если энергии остается меньше 10%,
// то уровень заканчивается. Если это 8 уровень, то
// заканчивается эпизод.
m->cheat &= ~CH_IDDQD;
if (!(GameCount & 0x1 f))
Shooting/t, NULL, NULL, 5);
iff m->energy< 10)
InitEndLevelf); 11 инициализация конца уровня
break;
default: ExitWithMsg(“P_PlayerLnSpecialSector: unknown
special %i”,s->special);
}
}
}
В пояснении здесь, пожалуй, нуждается только функция Shooting. Эта
функция используется в игре достаточно часто. Она вызывается при поражении
объекта. Эта ([дикция универсальна. Она используется для игроков, для монстров
и даже для бочек, которые можно взорвать.
- Первым параметром в эту' функцию передается указатель на объект
поражения;
- Второй параметр - у казатель на поражающий объект;
- Третий параметр - источник поражения,
- Четвертый параметр - это сила поражения (поражающий фактор).
Чаще всего второй и третий параметр совпадают, т.е. поражающий объект
и источник поражения - одно и то же. Но представим такую ситуацию. Игрок
стреляет в монстра из гранатомета или плазмомета. В этом случае поражающим
объектом будет снаряд, а источником поражения будет игрок. Тут вы можете
спросить, а зачем вообще нужен источник поражения. А нужен он для того,
чтобы объект поражения знал, кто покушается на его жизнь При этом жертва
бросает все свои дела и немедленно начинает атаковать наглеца,
осмелившегося на него напасть. Все вполне логично.
Функция Shooting.
Функция Shooting в сил}’ своей универсальности достаточно громоздка,
но сложной ее не назовешь. Я привожу ее здесь с некоторыми сокращениями:
void Shooting (register THING *t, register THING *tl,
register THING *t2, register int pow) // поражение
register unsigned int as, pw;
register MAN *m;
if ((t->attr & TA_LIVE) && t-power > 0)
{
if (t->attr & TA_COLLISION_SOUL’) // для “п «Теркиных душ”
//приостанавливаются все передвижения
t->incX = t->incY = t->incZ = 0;
m = t->player,
if (m && ICurSkill)
pow »= 1;
if ( 11 ) // если есть источник поражения, то далее
{ И рассчитывается, на какое расстояние отбросить
И жертву в зависимости от ее массы и поражающего фактора
if ( !t2 || !t2->player ||
t2->player->Current Weapon != WPCHAINSAW )
{
as = Direction(tl->korX,tl->korY,t->korX,t->korY);
// направление между поражающим объектом и
И объектом поражения
pw = ((pow*25)«15) I t->SprDat->MassOffhing,
// расчет коэффициента
if ( pow < 40 && pow > t->power &&
(t->korZ-t 1 ->korZ)>INT_TO_FIXED(64) &&/ Randoml () & 1))
{
pw *= 4;
as+= 0x80000000;
}
as »= 19;
t->incX += _fixmul(pw, Cosine(asJ); // приращение по X
t->incY += _fixmul(pw, Sine[as]); // приращение no Y
}
}
if ( m) // жертвой является игрок
if( t->ssect->SectSeg->special==SS_HEALTH10_END &&
pow = t->power )
pow >= t->power-l;
if( pow< 1000)
{
if ((m->cheat & CH IDDQD) || m->spec
[SPIN VULNERABILITY])
11 игрок неуязвим
return;
6—1481
if ( m->ArmorRang ) // степень защиты (0,1 или 2)
{
if ( m->ArmorRang == 1 )
as = pow/3,
else
as = pow » 1;
if ( as >= m->Armor )
{
as = m-> Armor;
tn->ArmorRang = 0; // защита на нуле (нет защиты)
}
m->Armor -= as; // уменьшение защиты
pow -= as; // уменьшение степени поражения
}
m->energy -= pow, // уменьшение энергии игрока
if(m->energy<P)
m->energy = 0;
m->Enemy = t2; // враг игрока
m->CountPalBloodEyes += pow, // красные глаза
if (m->CountPalBloodEyes > 100 )
m->CountPalBloodEyes = 100;
as = (pow< 100? pow 100);
}
t->power -= pow, // уменьшение энергии объекта
if (t->power <= 0 )
Dead (t2, t); //конец объекта
else
if ( Randoml()<SprDar>SensibilityOfHit &&
!(t->attr & TA_COLL1SION_SOUL) )
{ t->attr |= TA_ATTACK_GUARD;
RunSprite(t, t->SprDat->ofHitInThing);
// запуск анимации при попадании в объект
}
t->DelayAgr = 0; // задержка агрессивности
if(t2)
{
t->enemy = t2;
if ( &th[t->SprDat->ofThinQuiet] == t->spr &&
t->SprDat->offhinActiv )
И если объект находится в состоянии покоя, то
//перевести его в активное состояние
RunSprite(t, t->SprDat->ofThinActiv);
// запуск анимации объекта в активном состоянии
}
}
Функция PressUse.
Эта функция вызывается при нажатии клавиши “SPACE”, что может
означать открытие двери, активизация лифта или нажатие кнопки. Вызов этой
функции происходит всякий раз, когда нажимается “SPACE”, но к активным
действиям это приводит далеко не всегда. Например, если вы находитесь на
середине огромного плато и нажимаете “SPACE”, то вызывается функция
PressUse, которая проверяет некоторые условия (например, достаточно ли
близко вы находитесь от объекта) и при их несоблюдении выходит, не произведя
никаких действий.
Функция PressUse не сложна н выглядит так:
void PressUse (register MAN *m)
{
register THING *t;
register int 1;
t = m->PlayerThing;
1 = t->Ang » )9;
ThingMan = t;
CheckWay_Run(t->korX, t->korY,
(Cosine[l]«6)+t->korX,
(Sine[l]«6)+t->korY,
1, &_PressUse);
Функции CheckWay_Run.
Мы сказали, что функция PressUse несложна, но это не совсем так. Вся
сложность и вся гениальность заключается в функции CheckWay_Run.
Смысл этой функции заключен в ее названии: “Проверка пути и запуск”. В
чем же ее гениальность? Дело в том, что эта функция настолько
универсальна, что придумать что-либо более универсальное и в то же время
простое практически невозможно. Для того, чтобы это стало понятней, я
попытаюсь объяснить, что она делает.
Первые два параметра определяют координаты соответственно по X и Y
начала пути, а вторые два параметра определяют координаты конца пути. Т.е.
фактически первые четыре параметра определяют линию (путь).
Пятый параметр определяет флаг, 0-й бит которого говорит о том, что
необходимо анализировать столкновение с стенами (LINES), а первый бит -
столкновение с объектами (THING). В данном случае нас интересуют только
стены, поэтому флаг равен 1.
Последний параметр (шестой) определяет функцию запуска при
соблюдении условий столкновения на заданном пути.
В этом-то и заключается универсальность данного подхода. Функцию
CheckWayRun можно использовать при стрельбе на большие расстояния Только
флаг при этом будет равен трем, ну и, конечно, функция исполнения (6-ой
параметр) будет другой.
Для анализа границ стен функция CheckWay Run использует структуру
BlockMap, которую мы описали в главе 7.10. В этом случае анализируется
каждый блок, через который проходит путь (линия).
Хорошенько запомните такой подход! Он очень может вам пригодиться
при написании ваших собственных игр.
Может возникнуть вопрос: зачем данный алгоритм использовать в функции
PressUse? Ну при стрельбе - понятно. Там необходимо анализировать большие
пространст ва Но при нажатии
SPACE эффект достигается
лишь при приближении
вплотную к стене. Но не тут-то
было! Не обязательно
приближаться вплотную к
стене для того, чтобы нажатие
SPACE возымело действие.
Имеется определенное
максимально допустимое
расстояние от стены. И каким
бы малым ни было это
расстояние, нет гарантии того,
что получившаяся линия (путь) находится в одном блоке. Чаще всего как раз
это не так. Другой вопрос - это то, что нз функции PressUse в большинстве
случаев функция CheckWay Run работает гораздо быстрее, нежели при
стрельбе, т.к. количество анализируемых блоков в первом случае, как правило,
меньше.
Итак, приводим исходник функции CheckWay Run:
void CheckWay _Run (int kxl ,int kyl ,int kx2,int ky2,
int fl, int (*vXSTEP ♦))
{
register int pxl, px2,
pyl, py2,1, ml, diffix, difBy,
WayX, WayY, difWx, difWy
Counter++;
Stepsindex = &Steps; // начальный индекс структур STEP
if (!((kxl - MapX) & 0x7fffiI) )
kx 1 += Ox10000; II крайняя граница по X
if (!((ky 1 - MapY) & 0х7ЙПГ))
kyl += 0x10000; // крайняя граница no Y
MoveRange.pozX = kxl; // MoveRange - определяет
MoveRange. pozY = ky 1; // анализируемое пространство
MoveRange. difX = kx2 - kxl;
MoveRange.difY = ky2 - kyl;
kxl -= MapX; // координаты относительно начала
kx2 -= MapX; // карты блоков
kyl -= MapY;
ky2 -= MapY;
pxl = kxl » 23; // пересчет реальных координат в
pyl = kyl » 23; // номера блоков для BLOCKMAP
рх2 = kx2 » 23;
ру2 = ky2 » 23;
1 = ку2 - kyl;
ml = (kxl » 7) & Oxfflf; И дробная часть координаты X
// блока в начале пути
if (рх2 > pxl )// X должен иметь положительное приращение
{
difBx = 1;// положительное значение приращения блоков по X
difWy = fixdiy(l,abs(kx2 - kxl)); // значение приращения
// координаты Y линии
1 = INT_TO_FIXED( 1) - m 1;// коэффициент коррекции для
} // расчета координаты Y стартовой линии
else
if (рх2 < pxl) // X должен иметь отрицательное приращение
difBx = -1; // значение приращения блоков по X
difWy = _fixdiv(l, abs(kx2 - kxl)); И значение приращения
// координаты ' линии
1 = ml; // коэффициент коррекции для расчета
} И кт*. рдиняты Y стартовой линии
else
{ // линия вертикальная
difBx = 0; И приращение по X отсутствует
1 = INT_TO_FIXED(1); //эти два значения выбраны с тем
difWy = INT_TO_F1XED(256);// расчетом, чтобы никогда не
// соблюдалось условие: ( FIXED_TO_INT(WayY) == pyl )
}
ml = kyl » 7;
WayY = m 1 + fixmul( 1, difWy); // начал ьная координата
И линии по Y с фиксированной точкой
ml &= Oxfflf;
l = kx2-kxl;
if ( py2>py 1 ) // Y должен иметь положительное приращение
difBy = 1;// положительное значение приршцсния блоков по Y
difWx = _fixdiv(l, abs(ky2 - ky 1)); //значение приращения
// координаты X линии
1 = INT_TO_FIXED(1) - ml;// коэффициент коррекции для
} // расчета координаты X стартовой линии
else
if (ру2 <ру 1 ) // Y должен иметь отрицательное приращение
difBy = -1;//отрицательное значение приращения блоков по Y
difWx = _fixdiv(l, abs(ky2 - ky 1)); П значение приращения
// координаты X линии
1 = ml; // коэффициент коррекции для расчета
} // координаты X стартовой линии
else
{ // линия горизонтальная
difBy = 0; // приращение по X отсутствует
difWx = INT_TO_FIXED(256);// эти два значения выбраны с тем
1 = INT TO_FIXED( 1); // расчетом, чтобы никогда не
7/ соблюдалось условие: ( FIXED_TO_INT(WayX) == pxl )
WayX = (kxl » 7) + _fixmul(l, difWx); // начальная
// координата линии по X с фиксированной точкой
1 = 0;
do //начало цикла
{
if (fl & 1 )
{ // проверка столкновениия с линиями
if(!CheckTouchLine(pxl, pyl, &MakeStep_Lipe))
return;
}
if (fl&2 )
{ //проверка столкновениия с объектами
if (!CheckTouchThing(pxl, pyl, &MakeStep_Thing))
return;
}
if (pxl == px2 && pyl == py2 ) // выход из цикла, если
break; // все блоки проанализированы
if ( FIXED_TO_INT(WayY) == pyl )
{
WayY += difWy; // приращение линии no Y
pxl += diffix; // приращение блоков по X
}
else
if ( FIXEDJTO_INT(WayX) == pxl )
{
WayX += difWx; // приращение линии по X
pyl += difBy; // приращение блоков no Y
} while (1<64 ); // максимальное количество анализируемых блоков: 64
ProcessingSteps(v, INT_TO_FIXED(1)); //исполнение функции,
//заданной 6-ым параметром
}
Надеюсь, с переменными difBx и difBy вам здесь все ясно. Они определяют
направления приращения блоков pxl и pyl соответственно по осям X и У в
зависимости от направления линии. В комментариях здесь нуждаются значения
WayX, difWx и WayY, difWy. По сути, WayX и WayY - это те же номера блоков,
что pxl и pyl, только в формате с фиксированной точкой. Зачем иам тут
понадобилось представление с фиксированной точкой? Да потому, что линии
могут быть расположены под любыми углами относительно координат. В
фиксированной точке отпала бы необходимость, если бы углы были только
прямые.
DifWx и difWy представляют собой проекцию линии на оси координат
соответственно X и Y при перемещении на один блок. Если линия -
вертикальная, то перемещение будет производиться только по оси Y и difWx
будет равен 0.
Функции CheckTouchLine и CheckTouchThing
Функции CheckTouchLine и CheckTouchThing соответственно
проверяют столкновение, (те. нахождение в данном блоке) с линиями и
объектами. Если столкновение имело место, то заполняется структура STEP
по указателю Stepsindex, после чего этот указатель инкрементируется. Таким
образом, после анализа всех блоков мы в результате имеем N структур
STEP, где N - количество встреченных препятствий на нашем пути.
Структура STEP представлена следующим образом:
typedef struct step
{
int dist; // расстояние между началом вектора пути и
// пересекаемой линией или объектом
int type; // тип: 0 - объект, 1 - линия
union stl
{
LINE *li; И указатель на линию
THING *th, // указатель на объект
} obj ;
} STEP;
Поле type в структуре STEP определяет тип объекта в данной структуре:
0 - THING (объект);
1 - LINE (линия).
Эти функции почти идентичны, поэтому в качестве примера рассмотрим
одну из них: CheckTouchLine:
int CheckTouchLine (int x, int y, int (*prgXLINE *))
{
register short *bmap;
register LINE *li’
if (x >= 0 && у >= 0 && x < BlocksX && у < BlocksY )
{
bmap = &dat_blkmap[blkmap[y*BlocksX+x]];
while ( bmap(0] != -1 ) // проверить все линии в данном блоке
{
li = &alin[bmap(O]J; И указатель на текущую линию
if ( Counter != li->CountUseLine ) II данная линия
{ // еще не анализировалась на данном этапе
li->CountUseLine = Counter;
if (!prg(li))// функция проверки линии и заполнения
return 0; // структуры STEP
}
bmap++;
}
}
return 1;
Алгоритм функции CheckTouchLine очень прост. Вшар указывает на
текущую линию до тех пор, пока не встретится -1. Перед анализом очередной
линии проверяется, не использовалась ли данная линия в текущем цикле (в
нашем конкретном случае в функции CheckWay Run) путем сравнения if (
Counter != li->CountUseLine ). И если условие истинно, то линия не
использовалась. Данная проверка осуществляется для того, чтобы не делать
одну и ту же работу несколько раз. Ведь часто возможна такая ситуация,
что одна линия может принадлежать нескольким смежным блокам. В этом
случае при анализе очередного блока данная линия опять подвернется под
РУКУ
Если линия еще не использовалась, то запускается функция анализа линии
и заполнения структуры STEP. В нашем случае - это функция MakeStep Line.
Эта функция проверяет; пересекается ли вектор пути с текущей линией, указатель
которой передается в функцию MakeStep_Line. Проверка осуществляется в два
этапа:
1. Проверяется, с какой стороны от вектора пути находятся обе вершины
линии. Если обе находятся с правой стороны, или с левой, то линии пересечься
не могут. Это условие является достаточным. Если же вершины наход ятся по
разным сторонам от вектора, то линии могут пересекаться. Эго условие является
необходимым, ио не достаточным. Реализация данной проверки выглядит так:
if (Checkside Vector_ofPoint(li->FromVert->krX, li->FromVert->krY, &MoveRange) ==
CheckSide Vector_ofPoint(li->ToVert->krX, li->ToVert->krY, &MoveRange)
return 1; // линии пересечься не могут
Функция CheckSideVector ofPoint возвращает 0, если координата, заданная
в первых двух параметрах, находится справа от вектора, указатель на который
передается третьим параметром. Если координата находится слева от вектора,
то функция возвращает 1. В реализации данная функция несложна, и я надеюсь,
что вы осилите ее самостоятельно.
2. По специальной формуле вычисляется расстояние между началом вектора
пути и точкой пересечения двух векторов. Тут вы можете спросить, откуда же
взялся второй вектор? А второй вектор - это наша проверяемая линия.
Как самому создать трехмерную игрую
Функция MatchDistanceOfVectors.
Дистанцию вычисляет функция MatchDistanceOfVectors, в которую
передан • гсл два указателя на вектора:
m = MatchDistanceOfVectors(&MoveRange, &v);
Формула, о которой сейчас пойдет речь, достаточно эффективна, но тем не
менее без первой проверки (смотрите выше) ее использовать нельзя.
Если вектора параллельны, то функция MatchDistanceufMxtors возвращает 0.
В ряде случаев, если вектора не пересекаются, то формула возвращает
отрицательное значение. Формула выглядит так:
(у2.х- vl.x)* v2.dy + (vl.y - v2.y)* v2.dx
& v2.dy*v\.dx-v2.dx*v\.dy
Здесь vl и v2 - это соответственно первый и второй вектора. Значения dx и
dy в данных векторах определяют разницу координат соответственно по X и Y:
v.dx=v.xl-v.x,
где v.xl - конечная координата вектора по X.
Если вектора параллельны, то знаменатель формулы будет равен нулю.
Поэтому, дабы не возникла ошибка деления на ноль, знаменатель вычисляется
сначала, и в случае его равенства нулю возвращается 0.
Реализация данной формулы на языке С выгладит так:
int MatchDistanceOfVectors (VECTOR *vl, VECTOR *v2)
{
register int m;
m = _fixmul(v2->difY » 8, vl->difX) -// вычисление
_fixmul( v2->difX » 8, v 1 ->difY); II знаменателя
if (!m)
return 0;
return fixdiv(_fixmul((v2->pozX-vl->pozX)»8,
v2->difY)+
_fixmul(( v 1 ->poz Y-v2->poz Y)»8,
v2->difX),m);
}
Исходный текст функции MakeStep Line .
int MakeStep_Line (register LINE *li)
{
VECTOR vec,
register int dist;
if ( MoveRange.difX > INT_TO_FIXED(16) ||
MoveRange. difY >INT_TO_FIXED(16) ||
MoveRange.difX < INT_TO_FIXED(-16) ||
MoveRange.difY < INT_TO_FIXED(-16))
if ( CheckSideVector_ofPoint(li->FromVert->krX,
li->FromVert->krY,
&Ma\eiiingie)==CheckSideVector_ofPoint(li->ToVert->krX,li->To\feit->krY,
&MoveRange) )
return 1; // линии пересечься не могут
)
else
{ //проверка на ближней дистанции
if(CheckPozit_Line(MoveRange.pozX,
MoveRange.pozY, li) ==
CheckPozit_Line(MoveRange.pozX+MoveRange.difX,
MoveRange.pozY+MoveRange.difYJi))
return 1;
vec.pozX = li->FromVert->krX; // определение вектора vec;
vec.pozY = li->FromVert->krY;
vec.difX = li->lenX;
vec.difY = li->IenY;
dist = MatchDistanceofVectors(&MoveRange, &vec);
// вычисление дистанции между векторами
if ( dist < 0 )
return 1; // дистанция меньше 0. Вектора не пересекаются
Stepslndex->dist = dist; // расстояние.
Stepslndex->type = 1; // тип: линия.
Stepslndex->obj.li = li; // указатель на линию.
Stepslndex++; И инкремент указателя,
return 1;
}
Функция CheckPozitJJne.
Она практический аналогична функции CheckSideVectorofPoint за
одним исключением. Вектором туг является анализируемая линия. Поэтому’
третьим параметром в иее передается указатель на линию. Данная линия
является вектором, т.к. поля 1епХ и lenY могут иметь знак. Следовательно,
линия имеет направление. Таким образом, вектор для линии “li” задается
четырьмя параметрами:
li->FromVert->krX - начальная координата вектора X;
li->FromVert->krY - начальная координата вектора Y;
li->lenX - длина вектора по X (знаковое значение);
li->lenY - длина вектора по Y (знаковое значение).
А теперь, разобравшись с алгоритмом “сортировки” линий, вернемся
наконец к функции PressUse.
Функция PressUse.
Когда данные о всех линиях собраны, вызывается функция обработки
структур STEP: ProcessingSteps. Данная функция одну за другой проверяет все
структуры STEP и в порядке возрастания дистанции (поле dist в структуре STEP)
до достижения значения, передаваемого в нее вторым параметром, вызывает
функцию, которая передается в нее первым параметром (в нашем случае
это функция PressUse).
Функция PressUse является последним звеном в длинной цепочке
функций, которые мы описали выше. Она собственно и выполняет те действия,
которых мы добивались с самого начала. Какие же это действия? Если данная
линия является обычной стеной (тип линии равен 0), то просто издается звук,
похожий на то, что человек пытается сдвинуть непосильную ношу. Если же
линия имеет тип, то это означает, что с ней можно производить какие-либо
действия. В этом случае проверяется с какой стороны от линии находится игрок
(если вы помните из пункта 7.2.2, специальные линии имеют эффект только с
правой стороны). Если игрок находится от линии с правой стороны, то
вызывается функция SpecLine PushButton, которая и выполняет определенные
действия в соответствии с типом линии.
Исходный текст функции PressUse:
int PressUse (register STEP *pl)
{
if ( !pl->obj.li->Type ) // простая линия без типа
SetDataCeilFloor(pl->obj.li); И заполнение гл обильных
II переменных относительно разницы вертикальных
И координат двух смежных секторов, ограниченных
// данной линией
if (MinHeight > 0) // MinHeight всегда больше 0, если
return 1, // линия двухсторонняя
FXSoun(ThingMan, SFX_NOWAY); II звук: непреодолимое
} II препятствие
else
{ // проверка, с какой стороны от линии находится игрок
И если 0, то с правой стороны
if( CheckPozit_Line(ThingMan->korX,ThingMan->korY,pl->obj.li) != 1)
И если игрок с правой стороны, то выполнить действия.
SpecLine_PushButton(ThingMan, pl->obj. li);
return 0, // возвращаем 0, т.е. дело сделано, дальше обработка не требуется
}
Как самому создать трехмерную игру
Функция SetDataCeilFloor.
Здесь мы впервые встретили вызов функции SetDataCeilFloor. Эта
функция очень часто используется в DOOM’e. Она устанавливает
следующие глобальные переменные:
MinCeilZ - минимальная высота потолка двух смежных секторов;
MaxFloorZ - максимальная высота пола двух смежных секторов;
MinFloorZ - минимальная высота пола двух смежных секторов;
MinHeight - разница высот между двумя секторами. Это разница
высот между минимальной высотой потолка и максимальной высотой пола.
Таким образом, MinHeight фактически определяет высоту окна (или двери).
Если линия односторонняя, то MinHeight равен 0. Односторонняя линия
означает стену. Двухсторонняя линия означает повышение или понижение
пола и/или потолка (соседний сектор).
Исходный текст функции SetDataCeilFloor:
void SetDataCeilFloor (register LINE *li)
{
register int a;
register SECTOR *r, *1;
if(li->RLSid[l]==-l )
a=0;
else
{
r=li->Sng; //указатель на правый сектор
l=li->S lef; // указатель на левый сектор
MinCeilZ=min(r->ceilZ,l->ceilZ); // минимальная высота потолка
if ( r->floorZ > l->floorZ )
{
MaxFloorZ = r->floorZ; // максимальная высота пола
MinFloorZ = l->floorZ; // минимальная высота пола
}
else
{
MaxFloorZ = l->floorZ; // максимальная высота пола
MinFloorZ = r->floorZ; // минимальная высота пола
}
a=MinCeilZ-MaxFloorZ; //разница высот между секторами
}
MinHeight = а;
}
Функция SpecLine_PushButton.
Эта функция по размерам весьма внушительна. Ее размер практически
линейно зависит от числа типов линий. Исходный текст этой функции мы
приведем здесь частично:
int SpecLine_PushButton (THING *t, LINE *li)
{
if (!t->player) 11 если не игрок, то монстр
{
if ( li->Attr & LA_SECRET )
return 0; // монстры не могут “пользоваться” секретными линиями
switch (li->Type )
{ // список дверей, которые могут открывать монстры
case LT_MN_DOOR_OpenWait6sClose:
case LT_Sl_DOOR_OpenStay_BLUE:
case LTSl_DOOR_OpenStay_ YELLOW:
case LT_Sl_DOOR_OpenStayJRED:
gotogl;
}
return 0;
}
switch (li->Type ) // далее идет обработка линий,
{ // которые может использовать только игрок
case LT_SR_DOOR_OpenWait6sClose_BLUE:
case LT_SR_DOOR_OpenWait6sC lose_ YELLOW:
case LT_SR_DOOR_OpenWait6sClose_RED:
case LT_MN_DOOR_OpenStay:
case LT_Sl_DOOR_OpenStay_BLUE:
case LT_Sl_DOOR_OpenStay_ YELLOW:
case LT_Sl_DOOR_OpenStay RED:
case LT_MN_DOOR_OpenWaTt6sClose:
gl:
ProcessDoor(li, t), // инициализация открывания/закрывания дверей
break;
case LT_S1_STAIRS_BU1LD:
if (InitStairs(li)) // инициализация построения лестницы
SetOppositeTexture(li, 0); И изменить текстуру на противоположную
break;
case LT_S1_FLOOR_CHANGE_DONUT:
// инициализация: пол данного сектора понижается,
// соседнего сектора повышается, пока их высоты не
// сравняются
if (FloorLower_NeighborRise(li))
SetOppositeTexture(li, 0); II изменить текстуру
// на противоположную
break;
case LT S END LEVEL: // конец уровня
playvid(); // проигрывание видеофайла
// (в DOOM’e отсутствует)
lnitEndLevel(); // инициализация конца уровня
SetOppositeTexture(li, 0); // изменить текстуру
// на противоположную
break;
. // еще всякая всячина
}
}
Здесь мы видим частый вызов функции SetOppositeTexture.
Как самому создать трехмерную игру
Функции SetOppositeTexture.
Эта функция меняет текстуру переключателя, рубильника или кнопки
на противоположную. Например, если кнопка выключена, то она
подсвечена зеленым светом (или вообще не подсвечена). При нажатии кнопки
(с использованием SPACE) текстура выключенной кнопки меняется на
другую текстуру: кнопка подсвечена красным светом. Вторым параметром
в функцию SetOppositeTexture передается флаг, единичное значение
которого определяет, что данный переключатель через одну секунду' должен
перейти в исходное состояние (изменить текстуру на первоначальную).
Список номеров текстур для переключателей, как вы помните из раздела
3.6, находится в массиве swtext.
Исходный текст функции SetOppositeTexture:
void SetOppositeTexture (LINE *li, int v)
register SIDE *si;
register int sf, lowt, fult, upt, i;
if ( v == 0)
li->Type = 0;
si = &asid[li->RLSid[O]];
sf = SFX SWITCH CLOSING;
fult = si->FulTex;
if (li->Type == LT_S_END_LEVEL )
// тип линии становится О
И правая сторона линии
И номер звука
// номер полной текстуры
sf = SFXSWITCH;
upt = si->UppTex;
lowt = si->LowTex;
for (i=0; i<(numswitch*2); i++)
if ( upt == swtextfi])
И номер звука при окончании уровня
// номер верхней текстуры
// номер нижней текстуры
// numswitch - это число
И пар переключателей
// сравнение номера
// верхней текстуры
FXSoun(ActiveButtons->PlayerThing, sf); // звук
si->UppTex=swtext[iAl]; // номер противоположной текстуры
if(v)
StartButton(li, 0, swtext[i], 35); И инициализация
// структуры для возврата текстуры в исходное
//состояние через одну секунду
return;
}
if ( fult == swtext[i] ) 11 сравнение номера
{ И полной текстуры
FXSoun(ActiveButtons->PlayerThing, sf); И звук
si->FulTex=swtext[iAl], II номер противоположной текстуры
if( v)
StartButton(li, 1, swtext[i], 35); // инициализация
И структуры для возврата текстуры в исходное
//состояние через одну секунду
return;
}
if (lowt == swtext[i] )
II сравнение номера
// нижней текстуры
FXSoun(ActiveButtons->PlayerThing, st); // звук
si->LowTex=swtext[iAl]; И номер противоположной текстуры
if(v)
StartButton(li, 2, swtext[i], 35); // инициализация
// структуры для возврата текстуры в исходное
//состояние через одну секунду
return;
Функция StartButton.
Она ищет свободную структуру ActiveButtons (список активных
переключателей) и заполняет ее данными. Данные структуры обрабатываются
в функции ProcessingAnimated, о которой речь пойдет ниже.
Структура определена следующим образом:
^define MAXNUM_ACTIVEBUTTONS 16
struct button
{
LINE Hine; // указатель на линию
unsigned int TypeText; II тип текстуры: 0 - верхняя; 1 - полная; 2 - нижняя
int NumTexture; II номер первоначальной структуры
int Delay; // задержка возврата в исходное состояние
THING *PlayerThing; // указатель на объект
}.
struct button ActiveButtons[MAXNUM_ACTIVEBUTTONS];
Исходный текст функции StartButton:
void StartButton (LINE *li,int typ.int txt,int delay)
{
register int i;
for ( i=0; i<MAXNUM_ACTIVEBUTTONS; i++ )
{
if ( !ActiveButtons[i].Delay )
// свободная структура
ActiveButtons i
ActiveButtonsfi
ActiveButtons i
ActiveButtons[i
ActiveButtons i
. line = li; // указатель на линию
TypeText = typ; //тип
.NumTexture = txt; // номер текстуры
Delay = delay; // задержка
PlayersThing=(THING *)&li->Srig->t;
return;
}
} II свободных не найдено (такое произойти вряд ли может)
ExitWithMsg(“P_StartButton: no button slots left!”);
На этом мы закончим описание функции ProcessingPlayer. Ей мы посвятили
даже больше места, чем требовалось. Однако мы здесь фактически ни слова не
сказали о перемещении игрока на плоскости, т.е. по координатам X и Y. Для
этого необходимо было изменить координаты игрока в структуре THING (korX
и korY). Мы только изменили приращения координат. А все дело в том, что
перемещение игрока в пространстве подчиняется общим законам перемещения
объектов, и этим занимается функция обработки объектов, о которой речь
пойдет в следующем разделе.
8.2.2. Обработка объектов. Классы объектов.
Функция ProcessingThings.
Обработкой всех объектов в DOOM’e занимается функция
ProcessingThings.
void ProcessingThings (void)
{
register THING *t;
register THBIND *tl, *t2;
t=(THING *)ChainOIThings.next; // первый объект в списке
while (t != (THING *)&ChainOfThings) // об; ;•.&; i ль все объекты
{
if (t->func==-l ) // если функция обработки равна -1
{ //то объект подлежит удалению из списка и из памяти
tl = t->bind.next; //удаление объекта из списка
t2 = t->bind.pfev;
tl->prev = t2;
t2->next = tl;
zfree(t); // удаление объекта из памяти
}
else
{
if (t->func)
t->func(t); // запуск функции обработки объекта
}
t = (THING *)t->bind.next; // следующий объект в списке
}
}
Обработка объектов здесь осуществляется по классическому- методу. Во
многих играх используется подобный алгоритм. И это немудрено. Данный метод
прост и в то же время универсален. Именно об универсальности мы сейчас и
поговорим. До сих пор мы говорили об объектах, как о структуре THING. В
основном эти объекты являются спрайтамн или неподвижными предметами.
Но в DOOM’e существуют и другие объекты, некоторые из которых в общем
случае объектами не назовешь. В связи с этим мы введем понятие классов
объектов. Классы объектов отличаются друг от друга методами обработки.
Таким образом, функции обработки у них различны. Объединяет их два первых
поля структуры, которые мы видели в структуре THING:
TH_BIND bind; И связь всех объектов
void (*func)(struct thing *); // функция обработки объекта
У каждого класса структура всегда начинается с этих двух полей.
Таким образом, все классы можно обрабатывать в функции
ProcessingThings.
А теперь перечислим классы объектов, используемые в DOOM’e, и в
качестве примера рассмотрим обработку некоторых из этих классов.
Функция обработки класса: SpriteProcessing.
1. Класс спрайтов. Этот класс самый обширный. Он определен структурой
THING, о которой мы много говорили.
Функция обработки класса дверей: Open Door.
2. Двери. Это класс обработки дверей. Ои определен структурой door:
typedef struct door
{
THBIND bind;
void (*func)(struct door *);
int type;
SECTOR * sector;
} DOOR;
int MinCeil;
intIncrease;
int phase;
int RepeatDelay;
int delay;
// функция обработки
//тип двери
И сектор двери
И минимальное значение потолка
// определяет скорость открывания/закрывания двери
И текущая фаза
И значение задержки
// текущий счетчик
Двери бывают четырех типов (значение от 0 до 3 в поле type).
- 0: Принудительно открывается - спустя 6 секунд автоматически
закрывается;
- 1: Закрывается спустя 30 секунд.
- 2: Дверь закрывается н остается закрытой.
- 3: Дверь открывается и остается открытой.
Поле MinCeil определяет, до какой высоты должна подниматься дверь.
Значение Increase представляет собой вертикальное приращение
координаты и определяет таким образом скорость открывания/ закрывания
двери. В DOOM’e это значение, как правило, равно 2.
Значение phase определяет текущую фазу' двери:
- 1: дверь закрывается.
0: отсчет времени (дверь в это время ие открывается и не закрывается).
1: дверь открывается.
Значение RepeatDelay определяет задержку. Например, через какое
время должна закрыться дверь после ее открывания.
Значение delay представляет собой текущий счетчик задержки. При
инициализации двери в нее переносится значение нз RepeatDelay.
Функция обработки класса потолков: ProcessCeil.
3. Потолки. Это класс обработки потолков. Он определен структурой ceil:
struct ceil
{
TH BIND bind;
void(*funcXstruct ceil *); // функция обработки
int Туре; SECTOR * sector; // тип (принцип действия) // сектор
int MinCeil; // минимальная высота потолка
int MaxCeil; // максимальная высота потолка
int Increase; // определяет скорость поднятия/опускания потолка
int Crush; // разрушительное действие
int Phase; // текущая фаза
int Trig; // номер триггера (переключателя)
int PrevPhase; } CEIL; // предыдущая фаза
Поле Type в данной структуре определяет принцип действия (алгоритм
работы функции ProcessCeil):
0: Потолок опускается, пока не сравняется с полом.
1: Потолок поднимается до максимальной высоты потолка одного из
соседних секторов.
2: Потолок опускается, пока не дойдет немного до пола.
3: Синдром медленного опускания - поднимания потолка с
разрушительной силой.
4: Синдром быстрого опускания - поднимания потолка с разрушительной
силой.
3 и 4 типы имеют триггерный эффект. Например, если при движении
потолка нажать на одну кнопку, потолок остановится. При нажатни иа
другую кнопку движение продолжится.
MinCeil и MaxCeil определяют диапазон или пределы, в которых должен
двигаться потолок. При этом MinCeil определяет минимальное значение, а
MaxCeil - максимальное значение.
Значение Increase аналогично описанному выше одноименному
значению в структуре door.
Поле Crush определяет, губительно или нет для игрока и монстров
опускание потолка. Если Crush равно 1, то данный потолок при опускании
может раздавить. Следовательно, он опасен.
Значение Phase определяет текущую фазу потолка:
-1: потолок опускается.
1: потолок поднимается.
Поле Trig используется только для 3 и 4-го типа потолков. Это значение
используется для поиска в массиве структур ActiveCeil нужной структуры в
соответствии с определенной линией при остановке/запуске потолков. Для этого
используются функции:
ResumeMovingCeil - продолжить движение потолка.
StopMovingCeil - остановить движение потолка.
Значение PrevPhase является промежуточным. В функции StopMovingCeil
в него копируется значение из phase. А в функции ResumeMovingCeil значение
phase восстанавливается из PrevPhase. Таким образом, при возобновлении
движения потолка он продолжает движение в том направлении, какое было в
тот момент, когда его остановили.
Функция обработки класса полов: ProcessFloor.
4. Полы. Это класс обработки полов. Он определен структурой floor:
typedef struct floor
{
TH BIND bind;
void (*func)( struct floor *);
int Type;
int Crush;
SECTOR * sector;
int Direction;
int SpecialSec;
short NumFloor,
int EndFloorZ;
int Increase;
} FLOOR;
// функция обработки
// тип (принцип действия)
//разрушительное действие
// сектор
// направление
// номер специального сектора
// номер текстуры для полов
// конечная координата для пола
// скорость
Значение Туре, как и в предыдущем случае, определяет алгоритм функции
обработки ProcessFloor и влияет на инициализацию параметров в структуре
floor в момент ее создания (активизация):
0: Пол медленно опускается до максимального уровня пола одного из
соседних секторов;
1: Пол медленно опускается до минимального уровня пола одного из
соседних секторов;
2: Пол быстро опускается до максимального уровня пола одного из
соседних секторов + значение 8;
3: Пол медленно поднимается до уровня минимального потолка одного
из соседних секторов. При этом минимальный уровень потолка соседнего
сектора не должен превышать уровень потолка данного сектора (в программе
имеется ограничение).
4: Пол медленно поднимается до уровня максимального пола одного из
соседних секторов.
5: Пол медленно поднимается до уровня минимальной высоты текстуры
одной из линий, ограничивающих данный сектор (учитываются обе стороны
линии).
6: Пол медленно опускается до минимального уровня пола одного из
соседних секторов. После опускания тип и номер текстуры пола данного сектора
изменяется в соответствии с тем сектором, до которого ои опустился;
7: Пол медленно поднимается на 24 относительно текущего положения;
8: Пол медленно поднимается на 24 относительно текущего положения.
Тип и номер текстуры пола изменяется в соответствии с тем сектором, на
котором находится игрок в момент активизации пола;
9: То же самое, что и 3-ий тип. Только имеет разруа ительную силу.
Значение Direction определяет направление движения пола:
-1: пол опускается;
1: пол поднимается;
Значения SpecialSec и NumFloor записываются в момент инициализации.
Они соответственно определяют номер специального сектора и иомер
текстуры, которые надо присвоить активному сектору (полу) после опускания/
поднимания.
Значение EndFloorZ определяет вертикальную координату; до которой надо
поднять/опустить пол.
Значение Increase имеет то же назначение, что в структуре ceil.
Функция обработки класса платформ: MovePlats.
5. Платформы. Это класс обработки движущихся платформ. Платформы
в DOOM’e двигаются только по вертикали. Они представляют собой лифты,
на которых можно перемещаться вверх или вниз. Данный класс определен
структурой plat:
typedef struct plat
{ TH_BIND bind; void (*func)(struct plat *); // функция обработки
SECTOR * sector, И сектор
int Increase; И скорость
int MinFloor, И минимальная координата для пола
intMaxFloor, // максимальная координата для пола
int Delay; И задержка
int Counter, // текущий счетчик
int phase; // текущая фаза движения
intPrevPhase; И предыдущая фаза движения
int Crush; // разрушительное действие
int Trig; // номер триггера
int type; //тип
} PLAT;
Значение type определяет тип платформы:
0: Платформа медленно поднимается/опускается. Стоит 3 секунды. Затем
медленно опускается/поднимается.
1: Платформа опускается со средней скоростью. Стоит 3 секунды. Затем
поднимается с той же скоростью.
2: Платформа быстро поднимается на определенное значение (в DOOM’e
используются значения 24 или 32).
3: Платформа быстро поднимается до уровня максимального пола
одного из соседних секторов. Текстура пола текущего сектора модифицируется.
Остальные поля в данной структуре аналогичны одноименным полям в
структурах вышеописанных классов, и вопросов вызывать не должны.
Функция обработки класса: LightBlinks.
6. Закономерно мигающий свет. Это класс обработки света.
Не правда ли, странно? Свет - нематериален, и к объектам его в прямом
смысле не отнесешь. Но в данный момент это не имеет значения. Главное то,
что в структуре SECTOR имеется поле Light, изменяя которое по различным
законам, можно добиться различных световых эффектов. Конечно, можно
не городить для этого отдельный класс объектов, а обрабатывать световые
эффекты где-нибудь в другом месте, в другой функции Но это было бы
просчетом. С объектами очень просто работать: их можно добавлять, можно
уничтожать, их обработка проста и осуществляется централизованно. Что
может быть еще лучше? На мой взгляд, все, что в игре имеет непостоянный
характер (изменяется, уничтожается, добавляется), должно быть
реализовано, как класс объектов. В DOOM’e так и сделано. Итак, данный
класс определен структурой light:
typedef struct light
{
TH BIND bind;
void (*func)(struct light *); И функция обработки
SECTOR ♦ sector, // сектор
int DelayBlink; // задержка перед включением светового эффекта
int FrLight; // минимальный свет
intToLight; //максимальный свет
int TimeOff; // время минимального света
intTime_On; //время максимального света
} LIGHT;
Данная структура очень проста.
Поле DelayBlink определяет время, через которое с момента инициалкгации
должен включиться световой эффект. Зачем это нужно? Представьте себе,
допустим, 5 смежных одинаковых секторов. Для каждого из этих секторов
инициализируется в начале игры класс LIGHT (смотрите раздел 7.11), но у
каждого соседнего сектора значение DelayBlink будет отличаться на 1. В
результате мы получим эффект световой волны, распространяющейся по этим
пяти секторам (бегущий свет).
Значения FrLight и ToLight определяют, соответственно, максимальный и
минимальный свет д ля заданного сектора. Обратите внимание, что свет не плавно
меняется от минимального до максимального, а переключается мгновенно
(синдром включения/выключения).
Поля TimeOff и TimeOn, соответственно, определяют время минимального
света и время максимального света. Обычно время минимального света всегда
равно одной секунде, а время максимального света - величина переменная и
зависит от инициализации.
Функция обработки класса: RandLightBlinks.
7. Случайно мигающий свет. Этот класс очень похож на предыдущий, за
одним исключением. Свет мигает не периодически, а случайно. При этом
создается иллюзия неисправной лампы дневного света (когда барахлит стартер).
Этот класс представлен той же структурой light, поэтому' повторяться не
будем.
Функция обработки класса: LightOscill.
8. Плавно меняющийся свет. В отличие от предыдущих двух классов здесь
свет ие мгновенно переключается, а плавно меняется с максимального до
минимального и наоборот. Класс представлен структурой OscLight:
typedef struct OscLight
{
TH BIND bind;
void (*func)(struct OscLight ♦); // функция обработки
SECTOR * sector, // сектор
int MinLight; // минимальный свет
int MaxLight; U максимальный свет
intlncDecLig; // скорость изменения света
} OSC_LIGHT;
Здесь поля MinLight и MaxLight определяют, соответственно, уровни
минимальной н максимальной освещенности.
Значение IncDecLig представляет собой приращение света, т.е. определяет
скорость изменения освещенности.
Вот мы и описали вкратце те классы объектов, которые используются в
DOOM’e. Этих классов не так много, и этот список несомненно можно
пополнять, вводя новые экстравагантные объекты с потрясающими свойствами.
А теперь мы обсудим общие принципы работы с объектами независимо от их
класса.
С самого начала объект необходимо инициализировать. У каждого класса
объектов - своя функция инициализации, но каждая из них подчиняется общим
законам;
1. Выделить память под объект (структуру объекта);
2. Занести в поля структуры начальные данные. Эти начальные д анные
зависят от типа объекта. Типы для каждого класса мы рассматривали выше.
3. Занести объект в таблицу объектов (функция AddThing).
4. Затем идет обработка объектов, которой руководит функция
ProcessingThings.
5. Когда объект больше не нужен, ои удаляется. Для удаления объекта
вызывается функция DisableThing, которая состоит всего из одной строки;
void DisableThing (THING * t)
{
t->func = (void *)-l;
)
После выполнения данной функции объект будет удален из памяти в
функции ProcessingThings.
Для первого класса объектов (THING) мы уже рассматривали функцию
инициализации (CreateThing) в разделе 7.1.5.
А сейчас мы кратко рассмотрим в качестве примера работу с тремя
классами объектов: THING, DOOR и LIGHT.
8.2.2.1 Класс объектов THING
Как мы уже говорили, это самый распространенный класс в DOOM’e.
К нему' относятся все предметы, “живые” объекты (монстры, игроки),
артифакты, подвижные и неподвижные объекты. Одним словом - все то, что
не относится к стенам, потолкам, полам, дверям и платформам.
Неудивительно, что функция обработки данного класса (SpriteProcessing) - самая
сложная в DOOM’e. Перед описанием данной функции давайте попробуем
логически разобраться, что она должна делать.
Функции SpriteProcessing.
Во-первых, раз объектам и в основном являются спрайты, то данная функция
должна уметь их перемещать. Те. она модифицирует координаты korX, korY и
korZ в структуре THING. Прй перемещении объекта она анализирует
столкновения со стенами и с другими объектами. Также она контролирует
пересечение объекта со специальными линиями, и при этом производится
соответствующий эффект.
Во-вторых, данная функция должна управлять анимацией спрайта.
В DOOM’e, кроме этих двух пунктов, данная функция занимается также
“воскрешением” демонов, которое задействуется на четвертом уровне
сложности.
Исходник функции SpriteProcessing с небольшими изменениями:
void SpriteProcessing (register THING *t)
if (t->incX || t->incY)
{ // если приращения координат по X и Y не равны нулю,
MoveHorizontal(t); И то функция перемещения объекта на плоскости
if (t->func == -1 ) И если объект удален,
return; И то выход
}
if (t->korZ != t->floorZ || t->incZ )
{ //еаш объект не стоит ногами на земле или имеется
И приращение вертикальной координаты,
MoveVertical(t); И то функция перемещения объекта по вертикали
if (t->func == -1 ) //если объект удален,
return; //то выход
}
if( t->count != -1 )
t->count-; // задержка спрайта
while ( !t->count)
{
if (!RunSprite(t, t->spr->next)) // запуск функции
break; //анимации объекта
}
return;
if ((t->attr & TA_MONSTER) && RespawnEnable )
t->CounRespawn++; // счетчик (12 секунд)
if (t->CoimRespawn=12*35 && ’(GameCount & Ox 1 f) &&Random 1 () < 4 )
Respa wnDemons(t); //“воскрешение” демона
}
Данная функция представляет собой как бы скелет обработки объектов.
Здесь присутствует вызов всего того, о чем мы писали выше.
А теперь вкратце рассмотрим каждый элемент обработки в лице функций:
MoveHorizontal, MoveVertical и RunSprite. Функцию RespawnDemons
рассматривать мы не будем, т.к. она очень проста и, кроме того, специфична.
Функция горизонтальных перемещений MoveHorizontal.
Данная функция вызывается в том случае, если приращения координат
объекта incX и incY не равны 0.
void MoveHorizontal (register THING *t)
register MAN *m;
register int xl,yl;
if (!t->incX && !(y 1 =t->incY))
{
if (t->attr & TA COLLISION SOUL ) // проверка атрибута
{ И столкновения с препятствием доя потерянной души
t->incY = у 1; И все приращения по нулям
t->incX = yl;
t->incZ = yl;
t->attr &= ~TA_COLLISION^SOUL; II сбросить атрибут
RunSpriteft, t->SprDat->ofThmQuiet); II перевести
} И пот ерянную душу в состояние покоя
}
else
{
m = t->player,
if (t->incX > INT_TO_FEXED(30))
t->incX = INTTO_FIXED(30);
else
if (t->incX < INT_TO_FIXED(-3()))
t->incX = INT_TO_FIXED(-30);
if (t->incY > INT_TO_FIXED(30))
t->incY = INT_TO_FIXED(30);
else
if (t->incY < INT_TO_FIXED(-30))
t->incY = INT_TO_FIXED(-30);
xl = t->incX + t->korX;
yl = t->incY + t->korY,
if ( !MoveThingTo(t, xl, yl))
// новая координата объекта X
И новая координата объекта Y
// переместил, объект
// на новые координаты х1 и у1
// функция MoveThingTo возвращает 0, если объект
// встретил какое-либо препятствие
if (m) // если объект - игрок,
SlideTraverse(t); И то скольжение вдоль стены
else
if (t-attr & TA MISSILE ) И если объект - снаряд
if (TouchLine && TouchLine->Slef &&
TouchLine->Slef->NumCeil == numfskyl )
{ // если снаряд улетает в небо, то он
//просто удаляется
RemoveTlung_Ssec_Map(t); // удалить объект из
// списка объектов субсекторов.
RemoveSndThing(t); И удалить звук объекта
DisableThing(t); // заблокировать объект
return;
EndFlyMissile(t); И в противном случае взорвать
} // снаряд
else
{ //если объект не игрок и не
//снаряд, то остановить его движение
t-incX = 0;
t-incY = 0;
}
if (!(t-attr & (TA_COLLISION_SOUL | IA_MISSILE)) &&
t-korZ <= t-floorZ)
{ //если малые приращения
if (t->incX>-0xl000 && t->incX<0xl000 &&
t->incY>-0xl000 && t->incY<0xl000)
{
if (!m || (!m->Control.Move && !m-Control.Strafe))
{ if(m)
{ II если это игрок
if ((((int)m->PlayerThing->spr-(int)&th[O])/
sizeof( sinUfhune)-(SF_PLAY+1 ))<4)
RunSprite(m->PlayerThing, SF_PLAY);//iirpoK
} // осталааличается
t->incX = 0; // приращения координат по X и Y
t->incY = 0; И сбрасываются в 0
return;
}
}
t->incX=_fixmul(t->incX, 0хе800); И коррекция
приращения
t->incY=_fixmul(t->incY, 0хе800); // координат
} // для инерции
}
}
Здесь вам должно быть все ясно. Основным “гвоздем” здесь является
функция MoveThingTo, которая, собственно, и перемещает объект на новые
координаты. Эта функция не сложная, но громоздкая. Поэтому' исходный текст
ее приводить здесь ие будем, но тем не менее объясним, что она делает.
Первым делом вызывается функция, которая, анализируя путь, заполняет
структуры STEP аналогично тому, как мы это разобрали при описании
функции PressUse. Затем, в соответствии с накопленной информацией о линиях,
составляется таблица специальных линий, которые пересек объект.
Одновременно проверяются линии или объекты, которые могли бы служить
препятствием на пути следования объекта. И, если таковые имеются, то
устанавливается глобальная переменная MoveThing в 1, а в глобальную
переменную TouchLine записывается указатель на линию, которая
послужила препятствием (или в переменную TouchThing записывается
указатель иа объект, если препятствием стал объект). В самом конце
функции, если препятствий обнаружено не было, проверяется массив
специальных линий (если таковые встретились на пути следования объекта),
и для каждой линии вызывается функция SpecLine WalkAccrossLine,
которая производит определенные действия в соответствии с типом линии.
Если было обнаружено препятствие, то функция MoveThingTo возвращает
0. В противном случае она возвращает 1.
Функция SlideTraverse.
А теперь поговорим о том, зачем нужна функция SlideTraverse. Замечали
ли вы при игре в DOOM, что, если вы упираетесь в какую-либо стену' не под
прямым углом, то скользите вдоль этой стены. Наверняка замечали, но не
придавали этому значения, т.к это кажется естественным. Именно этим
скольжением и занимается функция SlideTraverse. Она вызывается только для
игроков. Если же монстры встретятся со стеной (независимо от угла), то они
меняют направление перемещения. Как работает функция SlideTraverse? Если
вектор перемещения игрока пересекается со стеной под прямым углом, то
перемещения ие будет и игрок останется стоять там, где он встретил препятствие.
Если же угол между' вектором перемещения и линией препятствия не прямой,
то объект скользит вдоль линии препятствия. При этом направление скольжения
зависит от знака разности угла пересечения и утла 90 градусов. А скорость
скольжения прямо пропорциональна абсолютному значению этой разности.
Функция EndFlyMissile.
Эта функция вызывается, когда снаряд врезается в какое-либо
препятствие. Снаряд при этом взрывается или разбивается о стену, произведя
определенный шум.
Исходный текст функции EndFlyMissile:
void EndFlyMissile (register THING *t)
{
register int i;
t->incZ = 0; // все приращения координат сбросить в 0
t->incX = 0;
t->incY = 0;
RunSpnteft, ThInfo[t->type].ofDeadThing); // запуск
И анимации конца снаряда
t->count -= Randoml() & 3;
t->attr &= ~TA_MISSILE; И сбросить атрибут снаряда
if( t->SprDat->S_EndThing)
FxSoun(t, t->SprDat->S_EndThing); И звук конца снаряда
Функция вертикальных перемещений MoveVertical.
Данная функция вызывается только в том случае, если объект ие “стоит
ногами” на земле или приращение координаты Z (incZ) не равно 0.
void MoveVertical (register THING *t) 11 вертикальные перемещения
{
register MAN *m;
register THING *tl;
register intn, 1;
m = t->player,
if ( m && t->korZ<t-floorZ ) II координата Z игрока меньше
// координаты пола могут означать, что игрок
// шагнул на какую-то ступеньку или упал с
//высоты. Это может означать, что его ноги
//увязли в почве под действием тяжести
{
m->height-=t->floorZ-t->korZ; И уменьшение высоты игрока
// скорость возврата в исходное состояние
m->InertJumpZ = (INT_TO_FIXED(64)-m->height)» 3;
t->korZ += t->incZ; И приращение координаты Z
if ((t->attr & TA_FLYING) && (tl=t->enemy) &&
!(t->attr & TA_COLLISION_SOUL) &&
!(t->attr & TA_MOVE_VERTICAL))
{ И расчет коррекции no Z для летающего монстра, когда
И он преследует цель
И расчет горизонтальной дистанции до объекта
1 = MatchDistance(t->korX-tl->korX, t->korY-tl->korY);
n = (t->height>iyt-tl->korZ-t->korZ,
if ( п < 0 && 1 <-(п*3))
t->korZ-=INT_TO_FIXED(4); // отрицательное приращение
else // (спускаться вниз)
if(n>0&&l>n*3 )
t->korZ+=INT_TO_FIXED(4); II положительное приращение
} //(подниматься вверх)
if ( t->korZ<=t->floorZ ) И объект соприкасается с полом
{
if (t->incZ < 0 ) И отрицательное приращение, т.е.
{ //объект падал
if ( m && t->incZ <INT_TO_FIXED(-8)) //если значение
{ И приращения велико, то игрок падает с большой высоты
m->InertJumpZ = t->incZ » 3;
FXSoun(t, SFX_PLAYER_H1TTING_GROUND);
} И звук падения на землю
t->incZ = 0; И приращение равно 0
}
t->korZ = t->floorZ; // координата Z объекта равна координате пола
if (t->attr & TA COLLISION SOULi)
t->incZ=-t-incZ; И доя “потерянной души” приращение
//меняется на противоположное. Таким
И образом, объект как бы отскакивает (как мячик)
if (t->attr & TA_MISSILE) И если это снаряд,
{
EndFlyMissile(t); И то взорвать его
return;
} }
else И если объект не соприкасается с полом
{
if ( !(t->attr&TA_NO_FALL)) //если объект должен падать
{
if (t->incZ == 0 ) // если приращение равно 0
t->incZ = -0x20000; // задать начальную скорость падения
else // в противном случае
t->incZ -= Ох10000; // увеличить скорость падения
} И (имитация ускорения свободного
} //падения)
if ((t->korZ+t->height)>t-ceilZ )// если верхняя точка
{ И объекта выше потолка (объект упирается в потолок)
if (t->incZ>0 ) И если имеется положительное приращение,
t->incZ = 0; //то сбросить его в 0.
t->korZ=t->ceilZ-t->height; И коррекция координаты Z
if (t->attr & TA_COLLISION_SOUL ) // для “потерянной
t->incZ = -t->incZ; //души”, изменение направления
//вертикального перемещения
if (t->attr & TA_MISSILE ) И для снаряда
EndFlyMissile(t); И конец его существования
Данный исходник хорошо комментирован и, посему вопросов вызывать
ие должен.
Функция анимациии спрайтов RunSprite.
Мы уже не раз использовали вызов данной функции в наших
исходниках, и сейчас подробно расскажем, что она делает. В общих словах,
эта функция управляет анимацией спрайтов. Для того, чтобы лучше понять
все, о чем будет идти речь, освежите в памяти раздел 7.1.4 настоящей книги,
где описываются основы анимации.
Функция RunSprite сменяет кадр спрайта. Номер кадра передается в
функцию вторым параметром. Текущие параметры кадра записываются в
структуру THING. Если номер кадра равен 0, то данный объект
уничтожается.
Исходный текст функции RunSprite:
int RunSprite (register THING * t, register int v)
register struct frame *fr;
if( !v)
{
t->spr=NULL;
RemoveThing_Ssec_Map(t);
RemoveSndThing(t);
DisableThing(t);
return 0;
}
fr = &th[v];
t->spr = fr;
t->count = fr->count;
t->sprite = fr->sprite;
t->frame = fr->frame;
if (fr->fn.friSpr)
fr->fn.fnSpr(t);
return 1;
// если номер кадра равен 0,
// спрайт для данного объекта отсутствует
// удалить объект из списка
// объектов субсекторов
// удалить звуковую информацию объекта
// заблокировать объект
// указатель на кадр
// счетчик (задержка) кадра
//номер спрайта
И номер кадра в спрайте
// если есть функция обработки,
И то запустить ее
Функции обработки кадров.
Теперь я сдержу' свое слово, которое дал в разделе 7.1.4, и расскажу' немного
о функциях обработки кадров. Разумеется, абсолютно все я охватить не
смогу', а расскажу лишь о конкретном примере для анимации одного из
монстров, например - “барона ада”. Как вы помните из раздела 7.1.3, все
свойства объекта отражены в структуре sprites. Поэтому для начала определим
данные этой структуры для “барона ада”:
struct sprites
int Ext Thing;
// внешний номер объекта (четвертое поле в THING ресурсе)
int ofTh inQuiet; int power; int ofThinActiv; int SInitSpr, int DelayAgr; И состояние покоя для монстра //сила объекта И активное состояние (ходьба, преследование) // звук инициализации (для монстров) И задержка агрессивности
int SAttackWithoutF ire; // звук ближнего боя (без
— // огнестрельного оружия)
int ofHitlnThing; // попадание в этот объект снаряда (пули)
int SensibilityOfHit; int SHitlnThing; int ofNearAttack; // вероятность попадания снаряда // звуковая реакция объекта на попадание И ближний бой (без использования
int ofFarAttack; И огнестрельного оружия // дальний бой (с использованием
int ofDeadThing; // огнестрельного оружия) // смерть монстра или игрока
int ofExplodeThing; // взрыв объекта
intSEndThing; int Speed; int Dimension; int Height; int MassOfThing; int StrikeHealth; int STalkOfThing; // предсмертный вопль (илц звук взрыва) И скорость монстра // радиус объекта // высота объекта //вес объекта // убойная сила снаряда (только для снар.) // звук разговора монстра
int Options; }; { OxBBB, SF BOSS, 1000, SF BOSS+2, // атрибуты объекта (опции) // BARON OF HELL “BOSS” (“барон ада”) И внешний номер объекта // состояние покоя для “барона ада” // энергия “барона ада” // активное состояние “барона ада”
SFX_DEMON_LORD SPOT_PLAYER, //звук ннтиаъпаци и “барона ада”
8, 0, SF BOSS+13, 50, 7/ задержка агрессивности “барона ада” // звука ближнего боя нет // попадание в “барона ада” снаряда/пули // вероятность попадания снаряда/пули
SFX_DEMON_PAIN, И звуковая реакция “барона ада”на попадание
SF_BOSS+10, SF BOSS+IO, SF BOSS+15, 0, //ближний бой И Д.ГП.ЕИЙ бой // смерть “барона ада” // взрыва “барона ада” нет
SFX_DEMON_LORD DYING, И предсмертный вопль “барона ада”
8, Tt скорость передвижения “барона ада”
INT TO_FIXED(24), // радиус “барона ада”
INT_TO_FIXED(64), И высота “барона ада”
1000, 0, //вес “барона ада” И т.к это не снаряд, то убойной силы нет
SFX_DEMON_NEARBY, И звук разговора “барона ада”
TANOJUMPOVER | TALIVE 1TAMONSTER // атрибуты “барона ада”
А теперь, после того, как мы определили все данные “барона ада”,
изобразим для него структуру frame, которая отражает анимацию и обработку
каждого кадра объекта:
{31,0,10,&RestEnemy,SF_BOSS+1,0,0},
{31,1,10,&RestEnemy,SF_BOSS,0,0},
{31,0,3,&Acti veEnemy,SF_BOSS+3,0,0},
{31,0,3,&ActiveEnemy,SF_BOSS+4,0,0},
{31,1,3,&Acti veEnemy,SF_BOSS+5,0,0},
{31,1,3,&Acti veEnemy,SF_BOSS+6,0,0},
{31,2,3,&ActiveEnemy,SF_BOSS+7,0,0},
{31,2,3,&ActiveEnemy,SF_BOSS+8,0,0},
{31,3,3 ,&Acti veEnemy,SF_BOSS+9,0,0},
{31,3,3,&ActiveEnemy,SF_BOSS+2,0,0},
{31,4,8,&SetAngEnemy,SF_BOSS+11,0,0},
{31,5,8,&SetAngEnemy,SF_BOSS+l 2,0,0},
// “Барон ада” SF_BOSS
// состояние покоя (0)
// активное состояние (2)
// ближняя и дальняя атака (10)
{31,6,8,&Shoot_BaronOfHell,SFBOSS+2,0,0},
{31,7,2,NULL,SF_BOSS+14,0,0}, // попадание в монстра
{31,7,2,&SoundHitlnThing,SF_BOSS+2,0,0}, //(13)
{31,8,8,NULL,SF_BOSS+16,0,0}, // умирание монстра
{31,9,8,&SoundDeadEnemy,SF_BOSS+17,0,0},//(15)
{31,10,8,NULL,SF_BOSS+18,0,0},
{31,11,8,&CanJumpOverThing,SF_BOSS+19,0,0},
{31,12,8,NULL,SF_BOSS+20,0,0},
{31,13,8,NULL,SF_BOSS+21,0,0},
{31,14,-1 ,&InitEnd_WhileDeadEnemy,SF_NONE,0,0},
Состояние покоя (пассивное состояние).
При создании объекта (например, в начале игры) он находится в
состоянии покоя (смотрите функцию CreateThing в разделе 7.1.5).
Как видно выше, состояние покоя состоит из двух кадров: 0 и 1. В данном
случае, он просто переставляет ноги, но при этом не перемещается в
пространстве. Зачем же понадобилось переставлять ноги, если нет движения.
На мой взгляд, это сделано для того, чтобы монстр не был похож на скульптуру,
мумию или чучело. Другими словами, чтобы было видно, что он - “живой”. В
таком случае, возникает очевидный вопрос, а почему бы, если он переставляет
ноги, не перемещать его в пространстве. На данный вопрос имеется не столь
очевидный ответ. Представьте себе, допустим, что на одном уровне игры
присутствует в общей сложности около 500 (!) монстров. И все они двигаются.
Для каждого монстра при движении д олжны обрабатываться границы объектов
(ведь монстр не может проходить сквозь стены). А как мы видели выше - это
задача не из простых. Поэтому такое хаотичное, да и никому не нужное движение
монстров сильно бы тормозило игру; Разработчики DOOM’a это прекрасно
понимали. И они придумали следующий алгоритм. В состоянии покоя монстр
не перемещается в пространстве. При этом вызывается функция RestEnemy.
Эта функция проверяет все факторы, которые могут вывести монстра из
состояния покоя. К этим факторам относятся:
- шум в секторе от звука выстрела;
- монстр увидел игрока;
- игрок попал в монстра (например, из пистолета).
При совпадении хотя бы одного из трех вышеперечисленных факторов
монстр переходит из состояния покоя в активное состояние. В этом случае он
перемещается в пространстве и преследует того, кто потревожил его покой -
игрока. Если игрок исчезает из поля зрения монстра, то последний еще некоторое
время ищет игрока, и в конце концов, отчаявшись его найти, опять переходит
в состояние покоя, которое, как известно, много процессорного времени не
отнимает.
Таким образом, в активном состоянии одновременно в игре находится
лишь незначительная часть монстров, которые находятся в
непосредственной близости от игрока. Такой подход оправдан с точки
зрения быстродействия. Но все-таки, на мой взгляд, было бы лучше, если
бы монстр в активном состоянии никогда не оставлял надежд}' найти игрока
и преследовал бы его, как бы далеко друг от друга онн не находились.
Функция RestEnemy.
А теперь поближе познакомимся с функцией RestEnemy, о назначении
которой мы уже говорили:
void RestEnemy (register THING *t) II состояние покоя
{
register THING *tl;
register unsigned int 1, m;
t->AgrShoot = 0;
tl = t->ssect->SectSeg->ThingDin; // игрок, который произвел шум от выстрела
if (tl &&(tl->attr&TA_LIVE)) И если данный “наглец”
{ // не вымысел, и если он еще живой (ТА LIVE),
t->enemy = tl; // то он становится врагом этого монстра,
if ((t->attr & TA_DEAF_GUARD) // если монстр - глухой
!CheckVisualOffhings(t, tl) && И если объекты друг друга не видят
!CheckLocationEnemy(t, 0)) // если монстр не видит ни одного игрока,
return; И то так и остаться в пассивном состоянии
}
else//иначе
{
if (!CheckLocationEnemy(t, 0)) // если монстр не видит ни одного игрока,
return; //то так и остаться в пассивном состоянии
} // далее - монстр переходит в активное состояние
1 = t->SprDat->S_InitSpr, //звук инициализации
ш=1;
if( 1)
{
switch (1)
case SFX SOLDIER SPOT PLAYER l .
case SFX_SOLDIER_SPOT_PLAYER_2:
case SFX_SOLDIER_SPOT_PLAYER_3:
// один из трех случайных звуков для солдата и сержанта
m = RandomlQ % 3 + SFX SOLDIER_SPOTPLAYERl;
break;
case SFX_IMP_SPOT_PLAYER_1:
case SFX JMP_SPOT_PLAYER_2:
// один из трех случайных звуков для IMP
m = RandomlQ % 2 + SFX_IMP_SPOT_PLAYER_1,
1 = t->type; // тип монстра
if (1==TT_SPIDER DEMON || 1==TT_CYBER_DEMON )
FXSoun(NULL, m); // звук в оба уха дця двух самых крутых монстров
else // иначе
FXSoun(t, m); И звук в стерео-режиме (в зависимости
// от местонахождения монстра относительно игрока)
}
RunSprite(t, t->SprDat->ofThinActiv); // запуск фазы активного монстра
}
Функции CheckVisualOfThings и CheckLocationEnemy.
Здесь мы впервые встретили вызов двух функций:
CheckVisualOfThings - проверяет видимость двух объектов, указатели на
которые передаются в нее первым и вторым параметрами. Если объекты “видят”
друг друга, то функция возвращает 1. В противном случае, она возвращает 0.
CheckLocationEnemy - проверяет видимость каждого из игроков монстром.
Почему мы говорим об игроках во множественном числе? Если вы ие забыли,
DOOM - игра сетевая, и в нее могут играть до четырех человек одновременно.
Исходный текст функции CheckVisualOfThings:
int CheckVisualOfThings (THING *tl, THING *t2)
{
register int ks, 1, m;
И проверка в ресурсе REJECT бита на пересечении
// двух секторов, в которых находятся объекты
ks = ((int)tl->ssect->SectSeg - (int)asec)/
sizeof(SECTOR),
ks = ks*NumSectors+((int)t2->ssect->SectSeg -
(int)asec)/sizeof( SECT OR);
m = 1 «(ks & 7);
if (arej ect[ks»3 j & m ) // если это единица, то
return 0; II они не могут видеть друт друта
CenterOfThing = tl->korZ+tl->height-(tl->height»2);
MaxHeighVisual = t2->korZ+t2->height-CentcrOfThing;
MinHeigh Visual = t2->korZ-CenterOfThing;
// проверка видимости двух объектов
return _CheckVisThing(tl->korX, tl->korY,
t2->korX, t2->korY);
}
Как видно, для ускорения обнаружения тут используется ресурс
REJECT, о котором мы говорили в разделе 7.9. Если иа пересечении строки
сектора первого объекта и столбца сектора второго объекта находится 1,
то объекты никогда не “увидят” друг друга, находясь в данных секторах
Если это 0, то вызывается функция CheckVisThing, которая очень похожа
на функцию CheckWay Run. Она также собирает информацию о всех линиях
на пути вектора просмотра и затем, анализируя информацию о всех линиях
в порядке увеличения дистанции (расстояния от начала вектора), принимает
решение о встретившихся препятствиях. Если препятствия в виде стен имели
место, то функция возвращает 0. Если препятствий не было, то считается,
что объекты “видят” друг друга, и возвращается 1. При анализе препятствий
учитывается также вертикальное расстояние между объектами,
определенное границами MinHeighVisual и MaxHeighVisual. Обратите
внимание, что препятствием тут могут являться только стены (линии), а
объекты таковыми не являются, и пересечение их вектором просмотра не
анализируется. Я не буду здесь приводить исходный текст функции
CheckVisThing, т.к., повторюсь, она аналогична функции CheckWay Run.
Только, в отличие от последней, немного специфична и, как следствие, несколько
быстрее работает. Так что действуйте “по образу и подобию” функции
CheckWayRun.
А теперь немного поговорим о функции CheckLocationEnemy. Как мы
видели, вызов этой функции осуществляется в двух случаях:
- если “неглухой” монстр не смог обнаружить источник шума
(выстрелившего игрока);
- если шума ие было, то монстр просматривает местность с целью
обнаружить одного из игроков (целью для одного объекта может быть только
один объект. За “двумя зайцами” они ие гоняются.
Исходный текст функции CheckLocationEnemy:
int CheckLocationEnemy (register THING *t, register int v)
{
register int l,m;
register unsigned int a;
register MAN *ma;
register THING *tl;
1 = -1;
m = 0; 11 счетчик количества обнаруживаемых игроков
st:
if( players[t->NumPlayer] ) //если такой игрок есть
if (1 == t->NumPlayer ||
m++ == 2 )
return 0;
1 = t->NumPlayer,
// если пошли по второму кругу
// если два игрока уже проверены
// возврат О
// если не проверялся ни один
// игрок, то 1 - номер первого
ma = &chuv[t->NumPlayer]; // указатель на данные игрока
//указатель на данные объекта
tl = ma->PlayerThing;
if (ma->energy>0 &&
t!=tl &&
Check VisualOfThingsft, tl))
// если игрок - живой
// если это - два разных объекта
// если между ними нет
// препятствий угол между векторами зрения объектов
а = Direction(t->korX,t->korY,tl->korX,tl->korY)-t-Ang;
if(v||
a<=unsigned)0x40000000|| a>=0xc0000000 |l
MatchDistance(t 1 ->korX-t->korX,t 1 ->kor Y4->korY)
<=INT_7;O_FIXED(64))
t->enemy = tl;
return 1;
И монстр нашел свою цель
// возврат 1
t->NumPlayer = (t->NumPlayer + 1) & 3,
goto st;
// следующий игрок
Алгоритм очень примитивен. Монстр за один раз ищет не более двух
игроков, начиная со значения NumPlayer. Если игрок в игре присутствует
(players[t->NiunPlayer] равен 1, где t->NumPlayer - номер игрока), если его энергия
больше 0 и между ним и монстром препятствий в виде стен нет
(CheckVisualOfThings(t, tl) не равен 0), то далее проверяются условия
обнаружения монстром игрока. Если передаваемый в функцию
CheckLocationEnemy второй параметр не равен нулю, то условия не
анализируются и сразу считается, что монстр игрока обнаружил. Этих условий
два:
- угол зрения между объектами должен лежать в диапазоне от -90
градусов (360-90=270 градусов) до 90 градусов. Например, если объекты стоят
лицом друг к другу, то угол равен 0 градусов;
- если первое условие ие соблюдается, то проверяется дистанция между
объектами, т.е. длина прямой, соединяющей объекты. Достаточной для
обнаружения является дистанция - 64.
Состояние преследования (активное состояние).
Каким образом монстр переходит из пассивного в активное состояние - мы
уже знаем. Теперь разберемся, чем занимается монстр в состоянии
бодрствования.
Как самому создать трехмерную игру
Функция ActiveEnemy.
Как видно, каждый раз в активном состоянии вызывается функция
ActiveEnemy. Эта функция намного сложнее функции RestEnemy. И это не
удивительно. Ведь в функцию ActiveEnemy практически заложены элементы
искусственного интеллекта. Монстр сам “принимает решения” о направлении
движения, о том, когда надо стрелять, а когда не надо. Все это выглядит вполне
естественно. Также тут используются элементы случайности и
непредсказуемости, что еще более усиливает эффект реальности.
Исходный текст функции ActiveEnemy:
void ActiveEnemy (register THING *t) // активное состояние
register int Г,
register THING *tl;
if ( t->DelayAgr )
t->DelayAgr-;
if (t->AgrShoot) -•
t->AgrShoot-;
И декремент задержки агрессивности
ChangeTrend(t);
if( !(tl = t->enemv) ||
!(tl ->attr & TA_LIVE))
// декремент времени преследования
И если цель отсутствует
И или если цель - не живая
{
if (!CheckLocationEnemy(t, 1)) // поиск цели
RunSprite(t,t->SprDat->of FhinQuiet); // если цель не
return; И обнаружена, то перейти в состояние покоя
if (t->attr & TA_SHOOTING ) И если монстр стреляет
{
t->attr &= -ТА SHOOTING;
if( CurSkill !=4)
NewChaseDir(t);
return,
}
if (t->SprDat->ofNearAttack &&
Check_NearAttack(t))
// и если текущий уровень сложнос ти
И не последний, то изменить направление
//движения монстра
// если у монстра есть ближний бой
И и если ближний бой возможен
{
if (1 = t->SprDat->S_AttackWithoutFire )
FXSoun(t, 1); И звук ближнего боя
RunSprite(t,t->SprDat->ofNearAttack); И запуск ближнего
return; //боя
}
if (t->SprDat->o£FarAttack)
{
if (CurSkill >= 4 ||
!t->CounRespa\\n )
{И направлении истекло
if (CheckAttack(t))
// если у монстра есть дальний
//бой
// если последний уровень сложнос ти
// или время движения в одном
// проверка на возможность атаки
{ И если атака разрешена, то запуск дальнего боя
RunSprite(t, t->SprDat->ofFarAttack);
t->attr |= TA SHOOTING; //монстр атакует
}
return;
if (t->AgrShoot || // если время преследования не истекло
CheckVisualOfThings(t,t->enemy) 11 И если между врагами
И нет препятствий
ICheckLocationEnemyft, 1)) И если цель не обнаружена
if ( —t->CounRespawn<0 || И если пора менять направление
! SpawnStepThing(t)) // или дальше дороги нет
NewChaseDir(t); // новое направление движения
if (t->SprDat->S_TalkOfThing && Randoml() < 3)
FXSoun(t, t->SprDat->S_TalkOfThing); //монстр сопит
Прежде чем продолжить, мне хотелось бы объяснить назначение поля
ChaseDir в структуре THING. Это значение определяет текущее
направление движения монстра. Оно может принимать значения от 0 до 8.
При этом значения от 0 до 7 соответствуют новому направлению движения,
а значение 8 определяет, что менять направление не стоит. Поле ChaseDir
изменяется в функции NewChaseDir, которая “принимает решение” о новом
направлении движения монстра исходя из принципов логичного
преследования им игрока. Функция NewChaseDir вызывается в трех случаях:
- после выстрела монстр меняет направление движения (если уровень
сложности не последний);
- если “счетчик движения в одном направлении” (CounRespawn) истек;
- если монстр встретил препятствие при движении.
Новое значение ChaseDir преобразуется в значение угла Ang в функции
ChangeTrend. Имеются следующие соотвстстния между' значением ChaseDir и
углом Ang:
ChaseDir Ang & ОхеООООООО
О О
1
2
3
4
5
6
7
0x20000000
0х40000и00
0x60000000
0x80000000
ОхаООООООО
ОхеООООООО
ОхеООООООО
Функция ChangeTrend.
Данная функция построена таким образом, что монстр не может
мгновенно изменить направление движения на противоположное. Он это
делает постепенно с дискретностью в 45 градусов.
Например, если монстр идет на север, а затем моментально поворачивает
на юг - это выглядело бы нереально.
Исходный текст функции ChangeTrend:
void ChangeTrend(register THING *t)
{
register int 1;
if (t->ChaseDir < 8 )
{
t->Ang &= ОхеООООООО;
1 = t->Ang - (t->ChaseDir « 29);
if(l>0)
l->Ang -= 0x20000000; /
else
if (1 < 0)
t->Ang += 0x20000000;
}
}
Функция NewChaseDir.
II надо ли менять направление
И маскирование текущего утла
/ поворот на 45 градусов левее
И поворот на 45 градусов правее
На первый взгляд, эта функция несколько сложна. И это неудивительно.
Ведь это именно она определяет “разумное” поведение монстра, т.к. руководит
направлением его движения.
Исходный текст функции NewChaseDir:
//противоположные направления движения (ча 180 градусов)
static int Left 180Degrees[] = {4,5,6,7,0,1,2,3,8};
И одно из диагональных направлений движения:
// 3: северо-запад; 1: северо-восток; 5: юго-запад;
// 7: юго-восток
static int DiagonalsDir[] = {3,1,5,7};
void NewChaseDir (register THING *t)
{
register THING *tl;
register int kxl, kyl, kx2, ky2,1, m;
if ( !(t 1 =t->enemy) ) // если нет цели, то кого преследовать
ExitWithMsg(“P_NewChaseDir. called witch no target”);
m = t->ChaseDir; // текущее направление
1 = Left 180Degrees[m]; //1 = противоположное направление
kxl = tl->korX - t->korX; //разница координат по X
kvl = tl->korY - t->korY; //разница координат no Y
if ( kxl > INT_TO_FIXED(10))
kx2 = 0; // направление на восток (X+)
else
if ( kxl < INT TO_FIXE1X-10) )
kx2 = 4; // направление на запад (Х-)
else
kx2 = 8; // не менять направление по X
if ( kyl < IN'f_TO_FIXED(-10))
ky2 = 6; //направление наюг(У-)
else
if ( kyl > INT_TO_FIXED(10))
ky2 = 2; //направление на север (Y+)
else
ку2 = 8; //не менять направление по Y
if ( кх2 1= 8 && ку2 != 8 ) // необходимо корректировать
{ // координаты и но X и по Y одновременно
// следующее диагональное направление
t->ChaseDir = DiagonalsDir[(kyl<0)*2+(kxl>0)];
// сделали один шаг в этом направлении
if ( 1 != t->ChaseDir && QpeStep Thing(t) )
return;
}
if (Randoml () > 2001| abs(kyl )>abs(kxl)) // определение
{ // приоритетного направления обработки (что
сначала)
kyl = ky2; // поменять местами направления
ку2 = кх2;
кх2 = kyl;
}
if (1 == ку2 ) // резко менять направление на 180 градусов
ку2 = 8; //не стоит
if( 1 = кх2)
кх2 = 8;
if ( кх2 != 8 ) // если можно изменить направление в сторону
{ // сокращения дистанции по X
t->ChaseDir = кх2; // то меняем направление
if (OneStepThing(t)) // и делаем один шаг в данном
return; //направлении
if ( ку2 != 8 ) // если можно изменить направление в сторону
{ // сокращения дистанции по Y
t->ChaseDir = ку2; // то меняем направление
if (OneStep Ihing(t)) //и делаем один шаг в данном
return; // направлении
i
if ( m != 8 ) // иначе, если текущее направление не заблокировано
t->ChaseDir = m; // то оставляем текущее направление
if (OneStepTh in g( t)) //и делаем один шаг в данном
return; // направлении
)
if (RandChaseDir(t, 1)) // если и тут не вышло, то берем
return; //случайное направление
if (1 != 8 ) // иначе в крайнем случае использовать
{ // направление 180 градусов от текущего
t->ChaseDir = 1;
if (OneStep_Thing( t)) // делаем один шаг в данном направлении
return;
)
t->ChaseDir = 8; // заблокировать изменение направления движения
}
Функция OneStep_Thing.
Алгоритм функции NewChaseDir несложен. Единственной ее целью
является сближение монстра со своей жертвой (это необязательно игрок).
Сокращение дистанции одновременно по X и Y осуществляется путем установки
одного из четырех диагональных перемещений, при котором сокращается
дистанция и по X и по Y. После установки нового направления монстр пытается
сделать один шаг в этом направлении. При этом вызывается функция
OneStep Thing, которая возвращает 1 в случае, если шаг сделан успешно и
препятствий на пути не обнаружено. В данном примере случайным образом в
диапазоне от 0 до 15 устанавливается переменная CounRespawn, которая
определяет время непрерывного движения в выбранном направлении. Если
на данном шаге обнаружено было препятствие (стена или объект), то
функция OneStep Thing возвращает 0.
Исходный текст функции OneStepThing:
int OneStep Thing (register THING *t) // один шаг для монстра
if (!SpawnStepThmg(t)) // сделать один шаг
return 0; И если наткнулся на npei ытствие возвратить 0
t->CounRespawn=Randoml() & Oxf,// количество шагов без изменения направления
return 1;
}
Функция RandChaseDir.
Если функция NewChaseDir не смогла найти ни одного логичного
направления в сторону' сближения с врагом, то она пытается найти другое
направление вопреки законам логики (т.е. в сторону удаления от объекта). При
этом вызывается функция RandChaseDir. Вторым параметром в эту функцию
передается направление, которое необходимо игнорировать (т.е. не
обрабатывать).
Исходный текст функции RandChaseDir:
int RandChaseDir (register THING *t, register int 1)
{
register int m;
if (Random 1()&1) // с вероятностью 50% идет проверка
{ // направлений в сторону увеличения направления
for ( m=0; m<=7; m++ )
{
if ( m != 1)
{
t->ChaseDir = m; // взять направление
if ( OneStep_Thing(t) ) // делаем один шаг в данном
return]; //направлении
}
}
}
else // в противном случае - в сторон}' декремента направлений
{
for ( m=7; m>=0; m- )
{
if (m != 1)
{
t->ChaseDir = m; // взять направление
if ( OneStep_Thing(t)) // делаем один шаг в данном
return 1; // направлении
} }
}
return 0; //ни одно из направлений не подошло
}
Функция SpawnStepThing.
Вот мы и просмотрели те функции, которые меняют направления движения
монстра. Туг вы просто не могли не заметить, что после смены направления
обязательно делается один проверочный шаг в выбранном направлении, чтобы
убедиться, что направление выбрано верно и препятствий нет. Шаг осуществляет
функция OneStepThing, о принципе работы которой было сказано выше.
Данная функция в свою очередь вызывает функцию SpawnStepThing, которая
собственно н осуществляет шаг. При перемещении монстра учитывается его
скорость. Перемещение осуществляется как по горизонтали, так и по вертикали
(для летающих монстров).
Исходный текст функции SpawnStepThing:
// коэффициент приращения по X для монстров в зависимости
// от направления движения
static int CoefAngX[]= {0x10000,0xb798,0,0x11(14868,
ОхПГГОООО,Ох11Т14868,0,ОхЬ798};
// коэффициент приращения по У для монстров в зависимости
// от нш г явления движения
static int CoefAngY[]= {0,0xb798,0x10000,0xb798,0,
0xfflT4868,0xfflTO000,0xffff4868);
int SpawnStepThing (register THING *t)
{
register int kx, ky, m;
if ((m = t->ChaseDir) == 8 ) // шаг делать не будем
return 0;
// новая координата для монстра по X
kx = t->korX + t->SprDat->Speed*CoefAngX[m];
// новая координата для монстра no Y
ky = t->korY + t->SprDat-Speed*CoefAngY[m];
if ( !MoveThingTo(t, kx, ку) ) И если встречено препятствие
if ((t->attr & TAFLYING) && MoveThing ) //если монстр
{ // летает
if (t->korZ < FloorZ )
t->korZ += INT_TO_FIXED(4); // монстр взлетает
else
t->korZ -= INT_TO_FIXED(4); // монстр снижается
t->attr |= TA_MOVE_VERTICAL; // движение по вертикали
return 1;
}
if (INumSpecLines)
return 0;
t->ChaseDir = 8;
// если не обнаружено спец-линий
//то выход
// далее не двигаться
m = 0;
while (NumSpecLines-)
{ //обработка специальных линий
if( SpecLine_PushButton(t, SpecLine[NumSpecLines]))
m = I;
}
return m;
}
t->attr &= ~TA_MOVE_VERTICAL;
// рожденный ползать летать не может
if ( !(t->attr & TA FLYING))
t->korZ = t->floorZ; //ноги на твердой земле
return 1;
Если летающий монстр встречает препятствие (функция MoveThingTo
возвращает 0), то он спускается ближе к земле на уровень игрока. Монстры
могут воздействовать на некоторые специальные линии, если коснутся их. К
ним относятся, например, простые двери.
Функция Check_NearAttack.
А теперь поговорим о том, как монстр готовится к атаке. Вы уже знаете,
что существует ближний бой (рукопашная схватка) и дальний бой с
использованием огнестрельного или метательного оружия.
Перед началом ближнего боя должна осуществляться проверка: достаточно
ли близко находится цель, чтобы схватиться с ней в рукопашную. Эту проверку
проводит функция CheckNear Attack.
Исходный текст функции Check Near Attack:
int Check_NearAttack (register THING *t)
{
register THING *tl;
if ( !(t 1 = t->enemy)) // если цель отсутствует,
return 0; // ближний бой не разрешен
// если расстояние между врагами больше 64,
if (MatchDгstanсе(11 ->korX-t->korX,
11 ->korY-t->korY)>=INT_TO_FIXED(64) )
return 0; //ближний бой не разрешен
// если между врагами нет препятс гвий,
if( CheckVisualOfThings(t,tl))
return 1; // ближний бой разрешен
return 0; //ближний бой не разрешен
}
Как видите, рукопашный бой разрешается лишь в том случае, если
соблюдаются три условия:
- есть, с кем драться;
- расстояние между’ врагами меньше 64;
- между врагами отсутствуют препятствия (стены).
Функция 'CheckAttack.
Что касается дальнего боя, то алгоритм остается тем же. Т е. сначала
разрешение на возможность дальней атаки, и, если дано добро, то можно
атаковать. Такое разрешение здесь дает функция CheckAttack.
Исходный текст функции CheckAttack:
int CheckAttack (register THING *t)
// проверка на возможность дальней атаки
{
register THING *tl;
register int], m;
t] = t->enemy; // это его цель
if ( t->player && tl->player ) // если оба враждующих - игроки,
return 0; // то в своих стрелять не надо
// если между объектами имеется препятствие(я)
if( (Check VisualOffhings(t, tl))
return 0; //тоатака невозможна
if (t->attr & TA_ATTACK GUARD ) // если в этого монстра
{ //кто-то попал (но не убил), то он обязан мстить
t->attr &= ~TA_ATTACK_GUARD; // после чего он добреет
return 1;
}
if( t->DelayAgr) // состояние шока (не может атаковать)
return 0;
// далее производится расчет вероятности атаки
//(значение 1)
// вероятность прямо пропорциональна дистанции
1 = MatchDistance(t->korX-tl ->korX, t->korY-tl->korY)-
INT_TO_FIXED(64);
if ( !t->SprDat->ofNearAttack)
1 -= INT_TO_FIXED(128);
1 = FIXED_TO_INT(1);
if ( (m=t-type)==TT_CYBER_DEMON ||
m==TT_SPIDER DEMON ||
m==TT_LOST_SOUL )
1 »= 1; // для вышеперечисленных трех монстров
// вероятность атаки в два раза меньше
if (1 > 200 ) // ограничение сверху для всех
1 = 200;
if (m == TT CYB ER DEMON )
{
if( 1> 160 ) // ограничение сверху для Cyber-демона
1= 160; ) if(Randoml()<l) И принятие решения
return 0; // атака не разрешена
return 1; // атака разрешена
Ближняя и дальняя атака.
Функция SetAngEnemy.
После того, как “дано разрешение” на атаку, монстр не заставляет
себя долго упрашивать и начинает атаку'. Первым делом он наводит оружие
на цель (таким образом прицеливается). За точность наведения отвечает
функция SetAngEnemy.
Исходный текст функции SetAngEnemy:
void SetAngEnemy (register THING *t) // прицеливание
{
register THING *tl;
if (tl =t->enemy ) // если есть цель
{
t->attr &= ~TAJDEAF_GUARD; //вычисление утла поворота
t->Ang = DirectTon(t->korX,t->korY,tl->korX,tl->korY);
if (tl->attr & TAFUZZ ) 11 если цель - прозрачная,
t->Ang += (Random 1()-Randoml ())« 21; //то наведение
} И не совсем точное
}
Функция Shoot_BaronOfHell.
После наведения оружия следует выстрел. Барон Ада в случае ближней
атаки дерется когтями, а в случае дальней атаки метает какую-то зеленую массу'
(весьма болезненную). В данном случае вызывается функция
ShootBaronOfHell.
Исходный текст функции Shoot BaronOfHell:
void ShootJBaronOfHell (register THING *t)
{
if (t->enemy)
if( Check_NearAttack(t))
// если есть, в кого стрелять
// проверка на ближнюю атаку
{
FXSoun(t, SFX_DEMON_HITTING_PLAYER); // звук борьбы
// его врагу явно не повезло
Shooting(t->enemy, t, t, (Random 1()%8+1 )*10);
)
else // если дальняя атака, то послать “зеленую массу”
SpawnMissile(t, t->enemy, TT_FIRE_BALL7);
I
}
Функция SpawnMissile.
Данная функция создает объект (снаряд), придает ему постоянную
скорость. Тип снаряда задается третьим параметром.
Исходный текст функции SpawnMissile:
void SpawnMissile (THING *tl, THING *t2, int v)
{
register THING *t3;
register unsigned int 1;
register int m;
// создать новый объект (снаряд)
t3 = CreateThing(tl->korX, tl->korY,
11 ->korZ+INT TO_FIXED(32), v);
if (1 = t3->SprDat->S_InitSpr ) //звук инициализации
FXSoun(t3,1);
t3->enemy = tl;
// направление снаряда
1 = Directional->korX, tl->kprY, t2->korX, t2->korY);
if (t2->attr & TA_FUZZ ) // если цель невидимая
1 += (Random 1 ()-Random 1 ())«20; // то направление неточное
t3->Ang = 1;
1»= 19;
// приращение координаты снаряда X
t3->incX = _fixmul(t3->SprDat->Speed, Cosinefl]);
// приращение координаты снаряда Y
t3->incY = fixmul(t3->SprDat->Speed, Sinefl]);
m = MatchDistance(t2->korX-tl->korX,t2->korY-tl->korY)/
t3->SprDat->Speed;
if(m< 1 )
m= 1;
// приращение координаты снаряда Z
t3->incZ = (t2->korZ-tl->korZ) I m;
t3->count -= Randoml() & 3;
// новая координата X
t3->korX += t3->incX»l;
// новая координата Y
t3->korY += t3->incY»l;
// новая координата Z
t3->korZ += t3->incZ»l;
if (!MoveThingTo(t3,t3->korX,t3->korY)) // если встречено препятствие,
EndFlyMissile(t3); // то конец снаряд}'
}
Функция EndFlyMissile.
Если снаряд во время свободного полета встречает препятствие, то он
взрывается или разбивается о стену или какой-либо объект. При этом вызывается
функция EndFlyMissile, в которую передается указатель на снаряд. Эту функцию
мы рассматривали выше.
Мы рассмотрели с вами три фазы анимации монстра:
- состояние покоя;
- активное состояние,
- состояние ближней и дальней атаки
Остальные две фазы (“попадание в монстра” и “смерть монстра”) мы
рассматривать не будем, т.к. они почти не включают в себя функций обработки.
Это просто чистая анимация без особых наворотов. Следовательно, для нас
интереса они не представляют.
8.2.2.2 Класс объектов DOOR.
После класса THING данный класс, как ни странно, является самым
многочисленным в DOOM’e. Дело в том, что в DOOM’e очень много различных
дверей. Некоторые двери открываются только при помощи одного из трех
разноцветных ключей, некоторые открываются и не закрываются, некоторые
двери работают только на закрывание, ну и так далее.
Инициализация класса DOOR - функции InitKeyDoors и
InitDoorsFromTrigger.
Активизация “механизма” дверей начинается с инициализации. В
DOOM’e инициализацию дверей осуществляют две функции.
Первая (InitKeyDoors) вызывается при попытке открыть дверь, для которой
требуется ключ.
Вторая (InitDoorsFromTrigger) вызывается во всех остальных случаях.
Здесь мы рассмотрим функцию InitDoorsFromTrigger, т.к. она более
универсальна.
Исходный текст функции InitDoorsFromTrigger.
int InitDoorsFromTrigger (register LINE *Ii, register int v)
I
register int rt, s, i;
register DOOR *sp;
register SECTOR *se;
rt = 0;
s = -1,
while ( (s = GetNumCorrespondSector(li, s)) >= 0 )
{
se = &asec[m]; // указатель на сектор
if ( ! se->Active ) // если сектор - неактивный
rt = 1; // создать объект DOOR
sp = (DOOR *)zmalloc(sizeof(DOOR), NULL, 51);
AddThing(&sp->bind); // внести его в список объектов
sp->sector = se;
se->Active = sp;
sp->func = &OpenDoor,
switch ( v )
// указатель на сектор
// указатель на дверь
// функция обработки
//тип двери
{
// Дверь закрывается и остается закрытой
case 2: sp->MinCeil = GetMinCeilZ(se) -
INT_TO_FDCED(4);
sp->phqse = -1; // дверь закрывается
// звук закрывания двери
FXSoun((THING*)&se->t, SFX_DOOR_CLOSED);
break; // Закрывается спустя 30 секунд
case 1: sp->MinCeil = se->ceilZ;
sp->phase = -1; // дверь закрывается
// звук закрывания двери
FXSoun((THING *)&se->t, SFX_DOOR_CLOSED);
break;
И Принудительно открывается - спустя 6 секунд
// автоматически закрывается;
case 0:
}
}
return rt;
// Дверь открывается и остается открытой
case 3; sp->MinCeil = GetMinCeilZ(se) -
INT_TO_FIXED(4);
sp->phase = 1; // дверь открывается
if ( sp->MinCeil != se->ceilZ )
// звук открывания двери
FXSoun((THING *)&se->t, SFX_DOOR_OPEN);
}
sp->Increase = INT_TO_FIXED(2); // скорость
sp->RepeatDelay = 150; //время задержки
sp->type = v; //тип двери
Функция GetNumCorrespondSector.
Здесь функция GetNumCorrespondSector возвращает номер очередного
сектора, который соответствует номеру' триггера данной линии. Почему' -
очередного? Да потому, что одна линия может одновременно активизировать
несколько секторов (дверей). Вторым параметром в функцию передается номер
сектора, который уже обрабатывался. Если функция GetNumCorrespondSector
возвращает значение -1, то это означает, что больше секторов, соответствующих
данной линии, нет. Указатель на линию передается первым параметром.
Исходный текст функции GetNumCorrespondSector:
int GetNumCorrespondSector (register LINE *li, register int i)
{
while (i<NumSectors) II поиск осуществляется от текущего
{ И значения до NumSectors.
i++,
if ( li->Trig==asec[i].plat ) // совпадают “ярлычки” ?
return i;
}
return -1; // сектор не найден
}
Обработка класса DOOR - функция OpenDoor.
Обработкой дверей, как было сказано выше, занимается функция OpenDoor.
Эта функция, в зависимости от текущей фазы и типа двери, осуществляет четыре
основных действия:
открывание двери;
закрывание двери;
ожидание (промежуточная фаза);
удаление класса двери из памяти и из списка объектов.
Исходный текст функции OpenDoor.
void OpenDoor (register DOOR *dor)
register int i;
switch ( dor->phase)
// текущая фаза двери
case 0: if (-dor->delay)
return;
if ( dor->type == 0 )
11 фаза 0 (ожидание)
И декремент счетчика задержки
// возврат, если время не истекло
// если тип двери: О,
dor->phase = -1;
// то дверь закрывается
FXSoun ((THING *)&dor->sector->t,
SFX_DOOR_CLOSED);
else
//игиче
if( dor->type == 1 ) //если тип двери: I,
dor->phase = 1; //то дверь открывается
FXSoun ((THING *)&dor->sector->t, SFX_DOOR_OPEN);
)
return, II фаза -1 (закрывание двери)
// взять текущую фазу двери
case -1: i=GetCurrentPhase(dor->sector,dor->Increase,
dor->sector->floorZ, 0,
dor->phase);
if (i==2 ) // дверь уже опустилась.
{
switch ( dor->type )
{
case 1: dor->phase = 0; // далее ожидать
dor->delay = 30*35; // 30 секунд
caseO:
case 2: break,
}
}
else
if ( i==l && dor->type 1= 2) // если дверь при
{ // закрывании наткнулась на препятствие,
dor->phase = 1; // то она открывается
FXSoun ((THING *)&dor->sector->t,
SFX_DOOR_OPEN);
)
return;
// фаза 1 (открывание двери)
// взять текущую фазу двери
case 1: i=GetCurrentPhase(dor->sector, dor->Increase,
dor->MinCeil, 0,
dor->phase);
if (i==2 ) // дверь уже поднялась.
if ( !dor->type )
{
dor->phase = 0; // ожидать определенное время
dor->delay = dor->RepeatDelay;
return;
}
}
dor->sector-> Active = NULL; //данный объект больше не нужен
DisableThing ((THING *)dor);
}
}
Функция GetCurrentPhase.
Основные действия по обработке платформ выполняет функция
GetCurrentPhase. Эта функция осуществляет один шаг (поднятие или опускание
платформы) и возвращает статус. Статус может принимать три значения:
0: платформа еще движется;
1: платформа в процессе движения встретила препятствие;
2: платформа достигла максимального предела.
В функцию GetCurrentPhase передаются 5 параметров:
- первый параметр - указатель на сектор (активная платформа);
- второй параметр - значение приращения платформы (скорость
опускания/поднимания);
- третий параметр - максимальный предел, до которого должна
опускаться/подниматься платформа;
- четвертый параметр определяет степень опасности для того, кто попадает
под двигающуюся платформу. Если он равен 0, то опасности нет н платформа
никого раздавить не может.
- пятый параметр определяет фазу движения:
-1 - движение вниз;
1 - движение вверх.
Исходный текст функции GetCurrentPhase мы приводить не будем, т.к. она
несложна, но в то же время весьма громоздка.
8.2.2.3 Класс объектов LIGHT.
Данный класс, как мы видели выше, управляет периодическими (т.е. не
случайными) световыми эффектами. В структуре LIGHT для этого имеются
все данные. Периодичность (частоту' и скважность) мигания определяют
параметры Time Off и Time On.
Инициализация класса LIGHT - функция InitLightBlinks.
Световые эффекты в DOOM’e в основном включаются в начале игры. При
этом, как мы видели в разделе 7.11, вызывается функция InitLightBlinks, в
которую передаются три параметра:
- первый параметр - указатель на сектор, в котором осуществляется
световой эффект;
- второй параметр - задержка выключения света (задержка включения
всегда равна около 0.2 секунды);
- третий параметр - если равен нулю, то задержка включения светового
эффекта - величина случайная.
Исходный текст функции InitLightBlinks:
void InitLightBlinks (SECTOR *se, int dl, int fl)
{
register struct light * t; // создать объект LIGHT
t = (LIGHT *)zmalloc(sizeof (LIGHT), NULL, 51);
AddThing (&t->bind); // Добавить его в список объектов
t->Time_On = 5; t->Time_Off= dl; t->func = &LightBlinks; t->sector = se; // сектор t->ToLight = se->light; // время включения // время выключения // функция обработки // макс 1й свет // минимальный свет
t->FrLight = GetMinLightFrom(se, se->light),
if (t->FrLight == t->ToLight)
t->FrLight = 0;
se->special = 0; // сектор уже не специальный
if (!fl)
t->DelayBlink = (Random 1 () & 7)+1; // случайное начало
el se t->DelayBlink = 1; // иначе - немед ленно начать
}
Функция GetMinLightFrom.
Эта функция проверяет все сектора, находящиеся по другую сторону
линий, ограничивающих данный сектор. Иначе, говоря нормальными
словами, она проверяет все соседние сектора и берет минимальную
освещенность, начиная с освещенности, передаваемой в функцию вторым
параметром. Например, если в текущем секторе освещенность равна 128, а
в одном из соседних секторов равна 64, то функция возвращает 64. Если же
в соседних секторах минимальная освещенность 244, то функция возвратит
128.
Исходный текст функции GetMinLightFrom:
int GetMinLightFrom (register SECTOR *se, register int v)
{
register inti;
register LINE ** li;
register SECTOR *j;
register LINE *p;
li = se->LinesInSector; // список линий, щраничюйющих сектор
for (i=0; i<se->NumLines; i++) II проверить все линии
{
P = h[i];
if (!(p->Attr & LA TWO SIDED)) // если линия односторонняя,
j = NULL; // то на другой стороне сектора нет
else
{
if ( se == p->Srig ) // если данный сектор находится на
И правой стороне линии,
j = p->Slef; // то взять сектор на левой стороне
else
j = p->Srig; // иначе взять сектор на правой стороне
1
if (j && j->light<v) И если в соседнем секторе освещенность меньше,
v = j->light; // то присвоить ее текущему значению
1
return v; // возвратить значение минимальной освещенности
}
Обработка класса LIGHT - функция LightBlinks.
Обработкой объектов класса LIGHT занимается функция LightBlinks.
Это очень простая функция. Она просто меняет значение в поле light
структуры SECTOR с определенной периодичностью.
Исходный текст функции LightBlinks:
void I .ightBlinks (register struct light * t)
{
if (!(-t->DelayBlink)) // текущее значение задержки
if ( t->FrLight == t->sector->light )
{
t->sector->light = t->ToLight; // максимальный свет
t->De!avBlink = t->Time_On,
)
else
{
t->sector-^light = t->FrLight, II минимальный свет
t->DelayBlink = t->'fime_Off;
}
}
}
Bee!!! Я больше не буду говорить об объектах и классах объектов. Данного
материала с огромным количеством исходников вам вполне должно хватить,
чтобы создать около миллиона совершенно уникальных объектов и их мутантов
(классов), если, конечно, вы этого захотите.
А сейчас мы посвятим отдельную тему обработке оружия.
8.2.3 Обработка оружия.
Оружие в DOOM’e не относится к объектам. Следовательно, и обработка
его несколько отличается от того принципа, который мы описали в разделе
8.2.2.
Раз уж речь пошла об оружии, то просто необходимо рассказать о структуре
weapon, т.к. функции обработки оружия работают с этой структурой. Вот
она:
tvpedef struct weapon
(
struct frame *CurSprite; // указатель на текущий кадр
int DelayFrame; И текущий счетчик задержки кадра
int pozX; // координата X для оружия
int pozY; // координата Y для оружия
} WEAPON;
Как мы видим, для оружия используются те же структуры frame, что и
для спрайтов. Поэтому определим такую структуру, например, для
дробовика:
// Дробовик: SFSHTG
{1,0,1 ,&NormalWeapon,SF_SHTG,0,0}, //нормальное состояние
// (дробовик в руке)
{1,0,1 ,&RemoveWeapon,SF_SHTG+1,0,0}, И убрать дробовик
{1,0,1 ,&GetWeapon,SF_SHTG+2,0,0}, // взять оружие
{1,0,3,NULL,SF_SHTG+4,0,0}, // начало стрельбы
{1,0,7,&ShotFromShotgun,SF_SHTG+5,0,0}, // выстрел
{1,1,5,NULL,SF_SHTG+6,0,0},
{1,2,5,NULL,SF_SHTG+7,0,0},
{1,3,4, NULL, SF_SHTG+8,0,0},
{1,2,5,NULL,SFSHTG+9,0,0},
{1,1,5,NULL,SF_SHTG+10,0,0},
{1,0,3,NULL,SF_SHTG+11,0,0},
{1,0,7,&CheckToNexlFire,SF_SHTG,0,0},
// Дробовик стреляет: SF_SHTF
{5,0x8000,4,&Lightl_WhileFire,SF_SHTF+1,0,0}, // пламя из ствола
{5,0x8001,3,&Light2_WhileFire,SF_SHOT_END, 0,0},
И Конец выстрела SF_SHOT_END
{1,4,0,&EndShoot,SF_NGNE,0,0},
Функция InitWeapon.
Итак, начнем с самого начала игра, когда у игрока нет оружия. Как вы
помните, в функции CreateThingForpiayer вызывается функция InitWeapon. Она
инициализирует структуры WEAPON (заполняет начальными данными) для
игрока, и записывает указатель на получившуюся структуру в поле WeaponData
структуры игрока MAN.
Исходный текст функции InitWeapon:
void InitWeapon (register MAN * m)
m->WeaponData[0].CurSprite = NULL;
m->WeaponData[l].CurSprite = NULL;
m->ChangeWeapon = m->CurrentWeapon;
Init_GetWeapon (m);
}
Очевидно, тут необходимо кое-что пояснить. Значение Currentweapon в
структуре MAN определяет номер оружия, которое носит игрок. Значение
ChangeWeapon определяет номер оружия, которое в данный момент нужно
“взять” игроку. Всего в первом DOOM’e используется 8 типов оружия, номера
которых определены таким образом:
Adeline WP HAND 0 // кулак
Adefine WPPISTOL 1 // пистолет
Adefine WP SHOTGUN 2 // дробовик
Adeline WPCHAINGUN 3 // пулемет
Adefine WP_ROCKET_LAUN 4 // гранатомет
Adefine WP_PLASMAGUN 5 И плазмомет
Adefine WP_BFG9000 6 // BFG9000 (очень круто)
Adefine WP_CHAINSAW 7 // бензопила
Adefine WP_NOCHANGE 9 // не изменять
Функция InitjGetWeapon.
Когда игрок просто носит оружие (нормальное состояние), в поле
ChangeWeapon записано значение WPNOCHANGE, что говорит о том, что
оружие менять не надо Если необходимо сменить оружие, то в поле
ChangeWeapon необходимо записать номер нового оружия. При этом, когда в
функции NormalWeapon обнаружится, что ChangeWeapon не равен
WP NOCHANGE, игрок меняет оружие. Смена оружия происходит в два этапа.
Сначала игрок убирает оружие, определенное полем Current Weapon, затем
берет оружие, определенное полем ChangeWeapon. При инициализации взятия
нового оружия в поле ChangeWeapon записывается значение WPNOCHANGE.
Инициализацией взятия оружия управляет функция Init GetWeapon:
void InitGetWeapon (register MAN * m)
I
register int wp;
if (m->ChangeWeapon = WP NOCHANGE )
m->ChangeWeapon = m->CurrentWeapon; // взять это оружие
if ( ni->ChangeWeapon == WP_CHAINSAW ) // если это пила,
FXSoun (m-PlayerThing, SFX_CHAINSAW_START); // то звук включения пилы
wp = m->ChangeWeapon; И номер оружия, которое надо взять
m->ChangeWeapon=WP_NOCHANGE; // нормальное состояние оружия
m->WeaponData->pozY=INT_TO_FIXED(128); И начальное значение
// координаты Y
SpawnWeaponFrame (m, 0, Frame_Weapon[wp].ofGet); // запустить процесс
//взятия оружия
Прежде чем продолжить, я хотел бы юе-что сказать о структуре данных
FrameWeapon.
Эта структура в чем-то аналогична структуре sprite для спрайтов, о которой
шла речь в разделе 7.1.3. Толью та структура отражала свойства исключительно
спрайтов, а эта структура несет информацию об оружии и при этом намного
меньше первой.
struct weapfram
{
intNumBox;
int ofGet;
int ofRemove;
int ofNormal;
int ofShot;
11 номер коробки боеприпасов
// номер спрайта: взять оружие
// номер спрайта: положить оружие
// номер спрайта: носить оружие
// номер спрайта: выстрел
int ofFire; // номер спрайта: пламя из ствола
} Frame_Weapon[] =
{
// кунак
{5,SF_PUNG+2,SF_PUNG+1;SF1PUNG,SF_PUNG+3,SF_NONE}>
И пистолет
{0,SF_PISG+2,SF_PISG+1 ,SF_PISG,SF_PISG+3,SF_PISF},
И дробовик
{1 ,SF_SHTG+2,SF_SHTG+1 ,SF_SHTG,SF_SHTG+3,SF_SHTF},
И пулемет
{0,SF_CHGG+2,SF_CHGG+l ,SF_CHGG,SF_CHGG+3,SF_CHGF),
//гранатомет
{3,SF_MISG+2,SF_MISG+1,SF_MISG,SF_MISG+3,SF_MISF},
//плазмомет
{2,SF_PLSG+2,SF_PLSG+1 ,SF_PLSG,SF_PLSG+3,SF PLSF},
//BFG9000
{2,SF_BFGG+2,SF_BFGG+1,SF_BFGG,SF_BFGG+3,SF_BFGF},
//бензопила
{5,SF_SAWG+3,SF_SAWG+2,SF_SAWG,SF_SAWG+4,SF_NONE}
};
Поле NumBox в структуре weaponfram определяет номер коробки, откуда
брать боеприпасы. Например, пистолет и пулемет используют одну коробку
(нулевую). Коробка номер 5 неисчерпаема. Таким образом, кулаком и
бензопилой можно пользоваться сколь угодно долго.
Я уверен, вы не раз уже задавались вопросом: а зачем нам нужны две
структуры weapon для одного игрока (смотрите структуру MAN)? Если вы
задавали этот вопрос, то это хорошо. Значит, вы пытаетесь осмыслить
прочитанное. Но тем не менее, как бы вы не старались найти положительный
ответ на данный вопрос, вам вряд ли это удастся. Но ничего, я вам помогу'.
Представьте себе, что вы стреляете из пистолета (не наяву; боже упаси, а в
игре). При этом в момент выстрела из ствола пистолета вырывается пламя.
Именно это пламя и является второй структурой weapon. Конечно, можно
было бы обойтись одной структурой weapon, нарисовав все кадры пистолета,
включая пламя. Но это было бы расточительно с точки зрения размера спрайтов
и неэффективно с точки зрения алгоритма программы. А так все очень просто.
Кота надо выстрелить, игрок вытягивает руку и нажимает на спусковой крюток
В момент выстрела мы просто формируем вторую структуру weapon для
пламени (пламя пристыковывается к концу ствола), что визуально выглядит,
будто огонь вырывается из ствола.
Функции Spawn Weapon Frame.
А теперь мы вплотную подошли к функции SpawnWeaponFrame. Эта
функция запускает различные фазы использования оружия, такие как: “Взять
оружие” (ofGet), “Положить оружие” (ofRemove), “Выстрел из оружия” (ofShot)
и др. Первым параметром в функцию передается указатель на данные игрока
(структура MAN). Вторым параметром передается номер структуры weapon
для игрока. Третий параметр определяет номер кадра (или номер структуры
frame в списке th).
Исходный текст функции Spawn WeaponFrame:
void SpawnWeaponFrame (MAN *m, int i, int fr)
{
register struct weapon *ad; ad = &m->WeaponData[i]; // указатель на используемую И структуру weapon для игрока
while (fr != 0) { ad->CurSprite = &th[fr]; ad->DelayFrame = th[fr].count; if (th(fr].pozX ) { // указатель на текущий кадр // текущая задержка
ad->pozX=th(fr].pozX « 16; // координата X дтя оружия
ad->pozY=th[fr].pozY « } ' 16; // координата Y для оружия
if (th[fr].fn.fnWeap) // если имеется функция обработки
th[fr].fh.fnWeap(m, ad); II то запустить ее
if( !ad->CurSprite || ad->DelayFrame)
return;
fr= ad->CurSprite->next;
)
ad->CurSprite = NULL;
Функция InitWeapon.
В разделе 8.2.1 я обещал вам рассказать о функции Processing Weapons.
Именно это я сейчас и сделаю, тем более что для этого уже подошло время.
Если функция InitWeapon инициализировала оружие в начале игры, то
ProcessingWeapons контролирует оружие и занимается его анимацией в
процессе игры.
void ProcessingWeapons (register MAN *m)
I
register struct weapon *1;
register int i;
for (i=0; i<2; i++ ) II проанализировать обе структуры
{ // weapon
1 = &m->WeaponData[i]; // указатель на текущую структуру
if (l->CurSprite && !->DeIayFrame != -1)
{ И если для данной структуры имеются спрайты и если
// счетчик задержки кадра не равен -1
!->DelayFrame-; // декремент счетчика задержки
if (! l->DelayFrame ) // если счетчик равен 0,
// то запустили следующий кадр
Spawn WeaponFrame(m, i, !->CurSprite->next);
}
}
m->WeaponData[l].pozX = m->WeaponData[0].pozX;
m->WeaponData[l].pozY = tn->WeaponData[0].pozY;
}
А сейчас по порядку рассмотрим функции, которые присутствуют в
структуре frame и поэтому вызываются (илн не вызываются) в зависимости
от конкретного состояния определенного оружия. Ну и начнем мы, конечно,
с того момента, когда игрок берет оружие в начале игры...
Функция GetWeapon.
Эта функция очень проста, она всего лишь изменяет координату' Y оружия
до тех пор, пока не достигнут предел. После чего запускает следующую фазу
оружия ofNormal. Изменение координаты Y оружия визуально выглядит, как
будто игрок его поднимает.
Исходный текст функции GetWeapon:
void GetWeapon (register MAN *m, register WEAPON *wp)
{
register int y;
у = wp->poz.Y-INT_TOFIXED(6); И немного “поднять” оружие
wp->pozY = у; // новая координата оружия Y
if ( У <= INT_TO_FIXED(32)) // если оружие поднято до конца
{
wp->poz.Y = INT_TO_FIXED(32); //ограничение высоты оружия
// запустить кадр оружия ofNormal (игрок носиг оружие)
SpawnWeaponFrame(m,0,
Frame_Weapon[m->CurrentWeapon].ofNormal);
}
}
Функция Normalweapon.
Все то время, пока игрок просто носит оружие, происходит вызов именно
этой функции. Эта функция контролирует смену оружия, определяет, когда
необходимо произвести фазу выстрела, а также она вычисляет физические
координаты оружия в текущий момент (когда игрок ходит, то оружие
раскачивается).
Исходный текст функции NormalWeapon:
void NormalWeapon (register MAN *m, register WEAPON *\vp)
{
register struct frame *t;
register int 1;
t = m->PlayerThing->spr;
// если игрок стреляет, то перевести его в нормальное состояние
if (t == &th[SF_PLAY+5] || t == &th[SF_PLAY+6])
RunSprite(t, SF_PLAY);
1 = m->CurrentWeapon; И текущее оружие у игрока
И если оружие - бензопила, то ее звук
if ( 1 == WP CHAINSAW && wp->CurSprite==&th[SF_SAWG])
FXSoun(m->PlayerThing, SFX_CHAINSAW_IDLE); И звук бензопилы
if ( m->ChangeWeapon != WP_NOCHANGE || !m->energy)
{ // если необходимо сменить оружие или у игрока энергия
И на нуле, то запустить фазу “убрать оружие”
Spawn WeaponFrame(m, 0, Frame_Weapon[l].ofR emove),
return;
if ( m->Control.PressedKeys & PR_CTRL ) // если игрок стреляет
{ // (нажал клавишу “CTRL”)
if (!m->Fire || (1 != WP_ROCKET_LAUN && 1 != WP BFG9000))
{ И Для гранатомета и BFG9000 одно нажатие CTRL соответствует
//одному выстрелу (для них m->Fire должен быть 0)
m->Fire = 1; // нажат спусковой крючок.
ShotFromWeapon(m); //выстрел
return;
}
}
else
m->Fire = 0;
// если “CTRL” не нажат, то m->Fire равен 0,
И что означает, что спусковой крючок отпущен
1 = GameCount «7; // счетчик цикла игры
l&=Oxlfff;
// далее следует расчет текущих координат для оружия
wp->pozX = _fixmul(m->PozitionWeapon, Cosine[l|)+ INT_TO_FEXED( 1);
wp->pozY = _Tixmul(m->Pozition Weapon, Sinefl & Oxffl])4-LNT_TO_FIXED(32);
}
Здесь следует пояснить фазу выстрела. Если нажата клавиша CTRL, то игрок
производит выстрел. Фазу выстрела исполняет функция ShotFromWeapon.
Функция ShotFromWeapon.
Эта функция первым делом проверяет, имеются ли в коробке патроны для
данного оружия. Для этого вызывается функция CheckAmmo WhileFire. Если
патронов нет, то данная функция сменяет оружие и возвращает 0. Если патроны
есть, то запускается фаза оружия ofShot (выстрел) и запускается функция
имитации шума от выстрела MakeDin, которая будит монстров, находящихся в
данном секторе и прилегающих секторах.
Исходный текст функции ShotFromWeapon:
void ShotFromWeapon (register MAN *m)
{
if( CheckAmmo_WhileFire(m))
{
RunSprite(m->PIayerThing,SF_PLAY+5); // игрок нацеливает оружие
// запустить фазу стрельбы
SpawnWeaponFrame(m,0,
Frame_Weapon[m-X}urrentWeapon]. ofShot);
MakeDin(m->Player'l'hing);//BOcnpoH3BecTH шум от выстрела
}
Теперь посмотрим, как производится выстрел из дробовика* (да и не
только из него). При нажатии на спусковой крючок из ствола вылетают с
огромной скоростью около 8 дробинок (запускается фаза ofShot для первой
структуры weapon). Одновременно из ствола вырывается пламя, которое на
короткое время освещает все вокруг (запускается фаза ofFire для второй
структуры weapon). Фаза освещения быстро проходит (всего два кадра). А
фаза выстрела продолжается еще некоторое время, т.к. после выстрела
создается имитация перезарядки игроком дробовика. После перезарядки
оружие возвращается в нормальное состояние (фаза ofNormal). Ifo перед
этим (конец фазы перезарядки) вызывается функция CheckToNextFire,
которая проверяет, нажат ли до сих пор спусковой крючок. Зачем она нужна?
Как говаривал Шерлок Холмс, элементарно. Представьте себе, что вы
несколько раз подряд, практически не отпуская спусковой крючок,
стреляете из пистолета. Вероятность того, что при очередном выстреле вы
попадете точно в десятку, как и в предыдущий раз, очень мала (конечно,
если вы не чемпион по стрельбе из пистолета). Однако, если вы после
очередного выстрела сделали паузу, чтобы как следует прицелиться, то
очень вероятно, что вы опять попадете в десятку.
Объяснение настолько простое, что я не знаю, стоит ли его приводить. Но
так и быть. После отдачи, которая следует после выстрела из огнестрельного
оружия (и от которой рука дергается), очень сложно в кратчайший срок
точно прицелиться.
Именно это хотели показать разработчики игры DOOM. Если после
перезарядки спусковой крючок все еще нажат, то производится очередной
выстрел, но направление (угол) прицеливания немного изменяется
случайным образом. В этом и заключается назначение функции
CheckT oNextF ire.
Функция ShotFromShotgun.
Эта функция вызывается в начале фазы ofShot. Собственно, здесь и
производится выстрел из дробовика. При этом воспроизводится звук выстрела.
Запускается анимация игрока в момент выстрела, уменьшается на 1 количество
патронов, осуществляется прицеливание и выпускается 7 дробинок.
Исходный текст функции ShotFromShotgun:
void ShotFromShotgun (register MAN *m, register WEAPON *wp)
{
register THING *t;
register int i;
t = m->PlayerThing;
FXSoun(t, SFX_SHOTGUN_FIRE); // звук выстрела из дробовика
RunSprite(t, SFJPLAY+6); // игрок стреляет
// уменьшить количество патронов в коробке
m->ammo[Frame_Weapon[m->CurrentWeapon].NumBox]-;
// запустить фазу огня для второй структуры weapon
SpawnWeaponFrame(m, 1 J'rame_Weapon[m->CurrentWeapon].ofFire);
И прицелиться
MatchHeightOfTarget(t);
for (i=0; i<7; i++) // выпустить 7 дробинок
{
ShotWithBullet(t, i); // выпустить одну дробинку
}
Функция MatchHeightOfTarget.
Теперь, я думаю, стоит пояснить, что означает слово “прицелиться”. Вы не
раз, наверное, замечали при игре в DOOM, что вертикальное прицеливание
здесь производится автоматически. Вот вам и ответ на поставленный вопрос.
Игроку' достаточно лишь навести оружие на цель (оружие поворачивается вместе
с ним). О том, ниже игрока находится цель или выше его, ему задумываться и
наводить оружие выше/ниже не надо. За него “думает” программа. За
работоспособность системы “автоматического наведения” отвечает функция
MatchHeightOfTarget. Эта функция призвана установить глобальную
переменную HeightOfTarget, которая определяет уровень вертикального
прицеливания.
Исходный текст функции MatchHeightOfTaiget:
void MatchHeightOfTarget (register THING *t)
{
register inti;
1 = t->Ang; // текущее направление оружия
//искать цель
HeightOfTaiget = AutoSetHeigh_ToShoot(t, 1,
INT TO_FIXED(0x400));
if (’TaigetReach) // если цель не достигнута
{
1 += INT_TO_FIXED(0x400); //оружие немного левее
// искать цель
HeightOfTarget = AutoSetHeigh_ToShoot(t, 1,
INT TO_FIXED(0x400));
if (iTargetReach) 11 если цель не достигнута
{
1 -= INT_TO_FIXED(Ox800); // оружие правее
// искать цель
HeightOfTaiget=AutoSetHeigh ToShoot(t, 1 JNT_TO_FIXED(0x400));
}
}
Как видим, эта функция пытается найти цель в трех направлениях: реальное
направление, немного левее и немного правее.
Функция AutoSetHeigh_ToShoot.
Поиском цели занимается функция AutoSetHeighToShoot. Вторым
параметром в эту’ функцию передается угол вектора обзора. Третий параметр
- дистанция (расстояние) обзора или длина вектора обзора. Начальные
н> «рдинаты вектора обзора - текущая позиция игрока. Если цель не обнаружена,
то функция AutoSetHeigh ToShoot возвращает 0. Если цель обнаружена выше
игрока, то она возвращает положительное значение, которое призвано в
будущем корректировать вертикальное наведение. Если цель находится ниже
игрока, то возвращается отрицательное значение.
Исходный текст функции AutoSetHeigh ToShoot:
int AutoSetHeighToShoot (THING *t, unsigned int ang,
int dist)
register int x,y;
MaxDistance = dist;
dist = INT_TO_FIXEE)(dist);
ang»= 19;
x = Cosine[l] ♦ dist;
у = Sine[l] * dist;
FireThing=t;
TargetReach = O;
LevelHands = (t->height»l)+t-:
MinHeighVisual = -OxaOOO;
MaxHeighVisual = OxaOOO;
11 вычисление длины вектора по X
И вычисление длины вектора по Y
// указател» на объект, от
//которого идет вектор
// указатель на цель
И уровень оружия по Z
TOFIXED(8);
// минимальная )-.изуал’.нтя высота
И максимальная аил шп лая высота
//запустить процесс анализа сп типутис
// функцией MathHeightToShoot.
CheckWay Run(t->korX, t->korY, t->korX+x, t->korY+y,3,&MathHeightToShoot),
if (TargetReach) 11 если цель засечена,
return DiflHeighShoot; //товззиратип.прцмнкниевысоты,
return 0; // иначе возвратить 0
Значения MinHeighVisual и MaxHeighVisual определяют вертикальные
границы визуального восприятия цели. Например, если обнаружена цель,
которая находится относительно текущего положения оружия (LevelHands)
выше значения MaxHeigh Visual, то данная цель засечена не будет. По данному’
принципу’ “сортировку” целей осуществляет функция MathHeightToShoot.
Исходный текст данной функции я приводить здесь не буду; На мой взгляд, я
и так несколько увлекся с исходниками. Надо же оставить вам некоторый
простор для творчества. Подумайте. Функция MathHeightToShoot очень
несложна. Оиа анализирует всю накопленную информацию об объектах
(помните структуру STEP?) и “сортирует” объекты по известному' нам
принцип}'. Если объект подходит, то указатель на него записывается в глобальную
переменную TargetReach, а коррекция высоты - в переменную DiffHeighShoot.
Функции ShotWithBullet.
А теперь немного поговорим о функции ShotWithBullet. Эта функция
выпускает одну' пулю в предполагаемую цель. Перед ее вызовом уже должна
быть вычислена переменная HeightOfTarget. Функция ShotWithBullet вводит
небольшую случайную поправку по координате X, если пуля не первая. Также
непостоянна сила выстрела (убойная сила пули).
Исходный текст функции ShotWithBullet:
void ShotWithBullet (register THING *t, register int v)
register int pow, ang;
pow = ((Random 1() % 3)+l )*5; // убойная сила одной пули
ang = t->Ang; // утол
if ( !v )
11 случайная коррекция утла (неточное прицеливание)
ang+= (Randoml()-Randoml())« 18;
И выстрел на максимальную дистанцию
ShotToMaxDistance(t, ang, INT_TOJ?IXED(0x800),
HeightOfTarget, pow);
1
Функция ShotToMaxDistance.
Для выстрела из всех видов огнестрельного оружия (не считая
метательного оружия типа гранатомета или плазмомета) в DOOM
используется функция ShotToMaxDistance.
Первым параметром в функцию ShotToMaxDistance передается
указатель на объект, который производит выстрел.
Второй параметр - угол вектора выстрела (направление полета пули).
Третий параметр - максимальная дистанция выстрела (длина вектора).
Четвертый параметр - коррекция полета пули по вертикали.
Пятый параметр - убойная сила выстрела.
Если третьим параметром установить очень малую дистанцию, то это можно
охарактеризовать как ближняя атака. Именно такой прием используется в
DOOM при атаке с бензопилой.
Исходный текст функции ShotToMaxDistance:
void ShotToMaxDistance (THING *t, unsigned int ang, int dist,
int heig, int pow )
{
register int x, y;
ang»= 19;
MaxDistance = dist; // максимальная дистанция выстрела
FireThing = t; PowerShoot = pow, И объект, производящий выстрел // убойная сила
dist = INTTOFIXED(dist); //дистанцияв формате c
х = Cosine[ang] * dist; у = Sinefang] * dist; DiflHeighShoot = heig; // фиксированной точкой И вычисление длины вектора по X // вычисление длины вектора по Y // коррекция по Z // уровень оружия по Z
LevelHands = (t-height>l>t-korZ+INT_TO>IXED(8);
// процесс сканировавши пути полета пули
CheckWay_Run(t-korX, t-korY, t-korX+x, t-korY+y,
3, &HitToTarget);
}
И опять здесь присутствует неизменная функция CheckWay Run.
Функция HitToTarget.
Эта функция занимается здесь поражением целей.
Исходный текст функции HitToTarget:
int HitToTarget (register STEP *pl)
register LINE *li;
register THING *t;
register SECTOR *se;
register int 1, xl, у 1, z 1;
if ( pl->type ) // если объект - линия
{
li = pl->obj.li;
if (li->Type ) // если линия - специальная
// запустить функцию включения спецэффектов при
// noi гадании в линию из пистолета/дробовика
SpecLine_ShootTheLine(FireThing, li);
if (li->Attr & LA TWO SIDED )
{
SetDataCeilFloor(li); // установить глобальные переменные
// для полов/потолков двух смежных
// секторов
1 = _fixmul(MaxDistance, pl->dist);
if (li->Srig-> floorZ == li->Slef->floorZ ||
_fixdiv(MaxFloorZ-LevelHands, 1) <= DiflHeighShoot)
{
if( li->Srig->ceilZ == li->Slef->ceilZ ||
_fixdiv(MinCeilZ-LevelHands, 1)>= DiflHeighShoot)
11 если пуля не задела стену, то возвратить единицу
return 1;
}
// коррекция расстояния до линии
1 = pl->dist - _fixdiv(INT_TO_FIXED(4), MaxDistance);
// координата X в точке попадания в линию
xl = MoveRange.pozX + _fixmul(MoveRange.difX, 1);
// координата Y в точке попадания в линию
у 1 = MoveRange.pozY + _fixmul(MoveRange.difY, 1),
И координата Z в точке попадания в линию
zl = LevelHands + _fixmul(DifIHeighShoot,
_fixmul(MaxDistance, 1));
se= li->Srig;
// если пуля улетела в небо, то закончить обработку
if( se->NumCeil == nuinfskyl )
(
if(zl>se-ceilZ ||( (se=li-Slef)&&(int)se->NumCeil==numfskyl))
return 0;
// попадание в стену (дымок)
WallBeat(xl,yl,zl);
else // если объект - THING
{ // если это тот же объект, который производил выстрел
if ((t = pl->obj.th) == FireThing )
// то возврат 1
return 1;
И если объект нечувствителен к попаданию
if ( !(t->attr & TA_LIVE) )
И то возврат 1
return 1;
1 = _fixmul(MaxDistance, pl->dist),
// если пуля пролетела выше "головы” объекта,
if (_fixdiv(t->korZ+t->height-LevelIlands,!)<DiflHeighShoot)
// то возвратить 1
return 1;
// если пуля пролетела ниже объекта,
if (_fixdiv(t->korZ-LevelHands,l)>DifiHeighShoot)
И то возвратить 1
return 1;
// коррекция расстояния до объекта
1 = pl->dist - _fixdiv(INT_TO_FIXED( 10), MaxDistance);
// координата X в точке попадания в объект
xl = MoveRange.pozX + _Fixmul(MoveRange.difX, 1);
// координата Y в точке попадания в объект
yl = MoveRange.pozY + _fixmul(MoveRange.difY, 1);
// координата Z в точке попадания в объект
zl = LevelHands + _fixmul(DifiHeighShoot,
_fixmul(l, MaxDistance));
if (t->attr & TANOBLOOD) // если в объекте нет крови
WallBeat(xl,у 1, zl); //то высекаются искры
else
SpacvnBlood(xl ,yl,zl,PowerShoot), //иначе капает кровь
if ( Pow erShoot ) И если имеется убойная сила пули
// то пусть объект t (цель) это почувствует
Shootmg(t, FireThing, FireThing, PowerShoot);
return О,
// дальнейшая обработка не имеет смысла
Функция HitToTarget
Она анализирует как попадание в стену (линию), так н в объект. При
попаданию в стену из последней высекаются искры. Управляет этим высечением
функция WallBeat. При попадании в объект, если у него установлен атрибут
ТА NO BLOOD (нет крови), то из него также высекаются искры. Примером
такого объекта может служить бочка с токсичными отходами. Если же
вышеназванный атрибут сброшен, то объект (монстр или игрок) истекает
кровью. В этом случае вызывается функция SpawnBlood.
Исходный текст функции WallBeat:
void WallBeat (int x, int y, int z)
register THING *t;
t= CreateThing(x,y,z + ((Randoml()-Randoml())« 10),
TT_WALL_PUFF), // создать объект TT_WALL_PUFF
t->incZ=INT_TO_FIXED( 1); // дымок медленно поднимается вверх
t->count -= Random 1()&3; // случайная задержка первого кадра
// максимальная дистанция равна 64 при использовании кулака
if( MaxDistance == INT_TO_FIXED(64) )
RunSprite(t, SF_PUFF+2), // в этом случае искр нет
!
Функции SpawnBlood.
void SpawnBlood (int x, int y, int z, int pow)
register THING *t;
// создать объект TT_DROP_OF_BLOOD
t = CreateThing(x, y, z + ((Randoml()-Randoml())« 10),
TT_DROP_OF BLOOD);
t->incZ=IN T_TO_FIXED(2);
t->count -= Random 1()&3;
if ( pow <= 12 && pow >= 9 )
RunSprite(t, SF_BLUI>H);
else
if ( pow < 9 )
RunSprite(t, SF_BLUD+2);
II приращение координаты no Z
И кровь капает вниз
// случайная задержка первого кадра
// запуск при среднем воздействии
// запуск при слабом воздействии
Функция CheckToNextFire.
О назначении этой функции мы говорили немного выше. А сейчас мы
приведем ее исходник:
void CheckToNextFire (register MAN *m, register WEAPON *ad)
if ((m->Control.PressedKeys & PR_CTRL) &&
m->ChangeWeapon == WP NOCHANGE && m->energy )
{
m->Count(JseF ire++:
ShotFrom Weapon/in);
}
else
{
m->CountUseFire = 0;
CheckAmmo_WhileFire(m);
} }
11 количество выпущенных пуль за
И время одного нажатия спускового крючка
И выстрел из оружия
// иначе (если отпущен курок)
// сбросить CountUseFire
// проверить патроны для данного
// оружия
Функция ShotWithBullet
Зачем нам нужно значение CountUseFire? Оно используется при выстреле
из пистолета и пулемета. Если, например, при выстреле из пулемета стрелять не
отпуская курок, то пули будут все время попадать в разные места (а не в одну'
точку). При этом, как обычно, используется функция ShotWithBullet. Вызов ее
в данном случае будет выглядеть таким образом:
ShotWithBullet(t, m->CountUseFire == 0);
Здесь второй параметр не будет равен нулю, если выстрел не первый. Это
говорит функции ShotWithBullet, что необходимо вводить случайную
коррекцию угла прицеливания.
В функции CheckToNextFire мы вторично столкнулись с вызовом функции
CheckAmmo WhileFire. В данном случае, если после очередного выстрела
патронов больше не осталось, она сменяет оружие на более подходящее в
данной ситуации. Исходный текст данной функции мы приводить здесь не
будем. Дело в том, что она очень проста и, кроме того, принцип смены
оружия можно организовать как угодно, и это скажется только на удобстве
игры, но на ее работоспособности - нисколько. Так что “флаг вам в руки”.
Функция Removeweapon.
Данная функция вызывается, когда необходимо убрать оружие. Это может
произойти в двух случаях: при смене оружия и в случае смерти игрока.
void RemoveWeapon (MAN *m, struct weapon *ad)
{
ad->pozY += TNTTO_F1XED(6);
if ( ad->pozY>=INT_TO_FIXED(128) )
{
if ( m->PlayerStatus == PS DEAD )
// немного опустить оружие
// если оружие опущено
//до предела
// если игрок уже в раю
{
ad->pozY = INT_TO_FIXED(128). И то оружие опущено
return,
}
if ( !ni->energy ) // если игрока сбили
{
Space и WeaponFramc(ni, 0, SF_NONE)y/ запустить холостую фазу
return;
}
m->CurrentWeapon = m->ChangeWeapon, И заменить оружие
Init GetWeapon(m); И и поднять его
} 1
Здесь мы впервые встретили определение “холостая фаза”. Ее данные
определяются так (структура frame):
{0,0,-1 ,ЬП JLL,SF_NONE,0,0},
Мы видим, что здесь в поле count стоит -1. Это означает, что для данного
объекта анимация далее будет пропускаться (смотрите в разделе 8.2.2.1 функцию
SpriteProcessing).
Вот и все, что мы хотели сказать об использовании оружия. Данного примера
вам должно хватить для того, чтобы произвести на свет какое-нибудь
оригинальное оружие типа навороченного лазера (бластера) или какого-нибудь
экстравагантного метательного оружия.
Можно также сделать разрушение стен при попадании в них изрядного
заряда картечи или ракеты.
А сейчас, если вы обратитесь к началу' раздела 8.2., а именно заглянете в
функцию Processingobjects, то увидите, что мы с вами еще не рассмотрели
функцию ProcessingAnimated.
8.2.4. Управление анимацией.
Функция ProcessingAnimated
Эта функция управляет анимацией текстур для полов/потолков и стен,
занимается скроллированием текстур для стен и осуществляет обработку
активных (нажатых) “кнопок”.
Анимация текстур для полов/потолков и стен - это просто периодическая
последовательная смена номера соответствующей текстуры.
Анимация для полов синей текстуры ассоциируется с рябью на воде Если
зеленая текстура - то это токсичная жидкость, красная - лава.
Скроллирование текстур для стен представляет собой инкремент координаты
X для стенной текстуры на единицу', что выглядит как движение текстуры
вправо или влево.
Исходный текст функции ProcessingAnimated:
void ProcessingAnimated (void)
register int i, 1, m;
register struct anim *an;
register LENE *li;
1 = GameCount;
an = &picarr[0];
while ( an < AdrAnimText)
// Анимация текстур
// указатель на начало списка анимации
// “огласить” весь список
for(i=an->TextFirst;i<(an->TextFirst+an->TextLast);i++)
{
// расчет очередного номера текстуры
m = (1 / an->delay + i) % an->NumAnims + an->TextFirst;
if ( an->type )
NumTextures[i] = m; // изменение текстуры для стены
else
adftexti] = m;
}
текстур
for (i=0; i<Num_AnimWalls; i++ )
// изменение текстуры для
// пола/потолка
// Скроллирование
// обработать все стены
li = AnimWalls[i];
if ( li->Type == LTANIMATED ) // стена скроллируемая
asid[li->RLSid[O]].krX += INT_TO_FIXED( 1); //инкремент
И координаты X
}
for ( i=0; i<MAXNUM_ACTTVEBUTTONS; i++ ) // обработать
{ И все нажатые кнопки
if ( ActiveButtons[i],Delay ) // если время не истекло
{
if( !(-ActiveButtons[i], Delay) ) //декремент задержки
{ // если время истекло
switch ( ActiveButtons[i].TypeText) // тип текстуры
{(верхняя, полная, нижняя)
// восстановить верхнюю текстуру
case 0: asid[ActiveButtons[i].line->RLSid[O]].UppTex =
Acti veButtonsf ij.N umTexture;
break;
// восстановить полную текстуру
case 1: asid[ActiveButtais[i].line->RLSid[O]] FulTex=
Acti veButtons[ i ] .NumTexture;
break;
// восстановить нижнюю текстуру
case 2: asid[ActiveButtons[i].line->RLSid[O]] LowTex=
Acti veB uttonsf i ]. N umTexture;
}
// зв\к отжатия кнопки
FXSoun(ActiveButtons[i].PlayersThing,
SFXSWITCHCLOSING);
// данная кнопка больше не активная
stos(&ActiveButtons[i], sizeof(struct button));
Глава 9
Основные принципы работы с
графикой
В этой главе мы рассмотрим все особенности организации быстрой графики
в трехмерных играх.
Если задуматься о типах создаваемых компьютерных игр, то станет очевидно,
что основной упор нам надо делать на графику. В большинстве игр вывод
графики по времени занимает более 80% от полного времени цикла игры. Звучит
это несколько парадоксально. Ведь еще недавно я говорил о том, что виртуальные
функции по объему занимают львиную долю программы. Однако объем
программы и время ее исполнения не всегда находятся в прямой
пропорциональной зависимости. Здесь не следует забывать о циклах. Ведь вывод
абсолютно всех графических объектов осуществляется в цикле. Причем
количество итераций цикла вывода объекта может превышать десятки тысяч.
Например, для того, чтобы вывести объект размером с экран для графического
режима 13h (разрешение 320x200), необходим цикл из 64000 итераций. В
простейшем случае, если не требуется объект масштабировать, крутить или
обрезать, иными словами, когда имеется готовый графический образ размерами
320x200 (например, стартовая заставка), то можно выводить объект не байтами,
а словами илн двойными словами, что значительно увеличит быстродействие
(соответственно в два или в четыре раза) Для нашего примера вывод стартовой
заставки на экран на ассемблере можно организовать таким образом:
mov esi,StartScr; указатель на графический образ
mov edi,0xa0000; начальный адрес экрана
mov ecx,16000; счетчик 16000*4=64000
repe movsd ; переслать образ на экран
Однако, мы не упомянули еще об одном обстоятельстве, из-за которого
вывод графики осуществляется достаточно медленно. Это время доступа к
видеопамяти. В '‘шустрых” вцдеоакселераторах (например, Stealth 64 VLB/PCI)
обычно запись одного байта в видеопамять требует 3-4 тактов ожидания
центрального процессора. Для медленных видеокарт этот показатель обычно
превышает значение 10. Для эксперимента вы можете запустить DOOM на
компьютере с процессором 486dx2 или выше с хорошим акселератором, после
чего поставить видеокарту типа Realtek или Trident-9000 и “погонять” DOOM с
данной конфигурацией. Разница в скорости будет видна на глаз. Поэтому в ряде
случаев производители компьютерных игр чаще всего готовят изображение в
теневом экране (область обычной памяти размером с экран), после чего
пересылают часть или полное изображение в видеопамять. Однако, это выгодно
лишь в том случае, если не используется многостраничный режим экрана,
либо в одном цикле игры вывод многократно осуществляется в одно и то же
место экрана.
Поясню на примере, в каких случаях это может произойти. Представьте
себе, что вам необходимо вывести два спрайта в трехмерном пространстве.
При этом один спрайт находится от плоскости просмотра (экрана) дальше
другого. В данном случае тот объект, что ближе к экрану, частично заслоняет
собой дальний объект. Очевидным решением тут было бы вывести на экран
дальний объект, а затем ближний. Для этого перед выводом всех объектов на
экран сначала вызывается функция, которая, сортируя объекты в порядке от
дальнего к ближнему, составляет таблицу объектов, предназначенных д ля вывода
на экран. В эту' таблицу сводятся все параметры, необходимые для быстрого
вывода объекта: физические координаты на экране, коэффициент
масштабирования, угол зрения объекта (в какую сторону’ повернут объект
относительно вектора нормали), его яркость и другие параметры. После этого
подпрограмма прорисовки объектов, “просматривая” данную таблицу, выводит
объекты в теневой экран. А теперь представьте, что будет, если вместо теневого
экрана используется физический экран. Тогда в местах, где объекты
перекрываются, осуществляется многократная запись в видеопамять, что
неэкономно с точки зрения быстродействия. Вы, конечно, можете мне легко
возразить. Ведь в первом случае приходится делать двойную работу': сначала
подготовить теневой экран, а затем перекопировать его в физическиий экран
(видеопамять). Не окажется ли это медленней? Однозначно ответить на этот
вопрос нельзя. Если объектов очень много, то быстрее получится в первом
слу'чае. Если объектов мало, то быстрее - второй метод. Но, в любом случае,
используя теневой экран, вы ничем не рискуете, т.к. копирование теневого
экрана в видеопамять в режиме 13h осуществляется очень быстро. А если
используете неполное обновление видеопамяти, то сам бог велел вам работать
с теневым экраном.
9.1. Особенности организации графики
в игре DOOM
Создатели компьютерных нгр при выводе графики обычно используют
одни из двух подходов:
- частичное обновление игрового пространства;
- полное обновление игрового пространства.
Первый метод используется в большинстве старых игр типа “Prince of Persia”.
В данном случае обычно имеется статический или слабо движущийся задний
план, на фоне которого выводятся спрайты. Т.к. игровое пространство
обновляется не полностью, то требуется запоминание информации под
спрайтами для того, чтобы затем восстановить картинку под ними.
Второй метод стал применяться относительно недавно. Его используют
очень многие современные игры, написанные на языке WATCOM C/C++:
“Mortal Combat”, “Rebel Assault 1/2”, “Nascar Racing” и др. DOOM тоже
относится к этой категории. При этом в DOOM’e не используется теневой
экран, а изображение готовится в невидимой области видеопамяти. А как
же все то, о чем мы только что говорили относительно многократного вывода
байта в одно и то же место видеопамяти! Да в том-то и дело, что в DOOM’e
вывод трехмерного лабиринта и объектов настолько четко оптимизирован,
что ни разу не получается многократного вывода в одно и то же место на
экране. При этом используются всевозможные виртуальные таблицы, в
которых содержится информация относительно уже выведенных участков
экрана. А ведь верно, скорость работы гра'фики в игре DOOM достойна
восхищения. Насколько быстро и плавно можно передвигаться в трехмерном
лабиринте с использованием клавиши “Shift”!
9.2. Трехмерность.
Последние достижения в области программирования наконец-то
приблизили наши фантазии о трехмерных играх к осязаемой реальности. DOOM
воочию показал, что сегодня можно создать трехмерные игры для недорогого
ПК, всего несколько лет назад казавшиеся недоступными. Я считаю, что
произошла революция в технологии развития трехмерных игр. Вслед за
DOOM’om стали выходить трехмерные игры по аналогичной или другой
технологии 3D-ENGINE. Их стали условно называть клонами DOOM’a. В
настоящее время, на мой взгляд, лишь одна игра имеет улучшенную систему
3D-ENGINE. Я имею ввиду DESCENT, использующую трехмерную схему BSP.
Говоря по правде, раньше также существовало множество трехмерных
имитаторов полетов и пространственных игр. Но они все были очень слабы
графикой (все объекты были слабо детализированы и, кроме того, представлены
в векторном формате). Разработчики издавна неоднократно пытались создать
улучшенную пространственную игру, используя нетрадиционные методы
трехмерного моделирования, но по-настоящему более-менее это удалось
фирме ID Software в 1990 году, которая выпустила игру Wolfenstein 3D. Самое
удивительное было то, что данная игра могла работать на процессоре 80286.
Однако останавливаться на достигнутом фирма ID Software не собиралась, и
уже в 1993 вышла ставшая легендой игра DOOM. Эта игра работала на
процессорах не ниже 80386 и использовала плоскую модель памяти. Что будет
дальше... А дальше, по-видимому, опять слово за фирмой ID Software,
готовящейся выпустить к июню 1996 года игру Quake. Судя по рекламе, это
будет что-то потрясающее. Но, как говорится, “поживем - увидим”.
В этом разделе мы кратко рассмотрим основные принципы трехмерного
моделирования.
Говоря математическим языком, любая точка в трехмерном
пространстве описывается с помощью уникального набора трех координат
X, Y и Z. При этом экран представляется плоскостью с координатами X и Y,
а координата Z перпендикулярна экрану. Но каким образом можно
преобразовать трехмерные объекты в двух измерениях? Очень просто... Мы
отображаем их на плоскость. И позволит это сделать проекция. При этом
трехмерная модель выглядит так, что у игрока возникает полное ощущение
объемности.
Существуют два вида проекций:
- параллельная проекция;
- перспективная проекция.
Параллельная проекция очень проста. Мы всего лишь отбрасываем Z-
координату и рисуем каждую точку объекта по двум координатам (X,Y).
Перспективная проекция немногим отличается от параллельной. Мы просто
используем Z-компонент для масштабирования координат (X,Y) с целью
отдаления точки от плана просмотра и для придания объекту более
реалистичного вида.
Во всех пространственных играх (DOOM не является исключением) за центр
системы координат принимается центр игрового пространства (объема
просмотра). Например, если игровое пространство равно размеру экрана, то
центр системы координат будет находиться в точке экрана с координатами
(160,100).
В DOOM’e координаты X и Y для объекта определяют физические
координаты экрана, пересчитанные в соответствии с законами перспективной
проекции. Координата Z определяет удаление объекта от плоскости просмотра
и определяет коэффициент масштабирования объекта, а также уровень его
освещенности.
По законам перспективной проекции дорога, уходящая за горизонт всегда
стремится слиться с небом. И линия данного слияния, если дорога ровная и
прямая, всегда находится на уровне глаз (точка посредине между глазами
является центром системы координат).
9.3. Принцип разработки графических
функций.
При разработке компьютерных динамичных игр, пожалуй, основная
трудность заключается в разработке оптимальных функций обработки и вывода
графики. В большинстве случаев эти функции пишутся на ассемблере (в DOOM’e
все графические процедуры вывода трехмерного лабиринта и спрайтов
написаны на ассемблере).
Обычно на этапе разработки игры данные функции пишутся на “С” (так
как это быстрее и проще в отладке), для того чтобы проверить
работоспособность алгоритма. Затем, на этапе оптимизации, часть функции
или полностью функция перепиоываются на ассемблер. Большинство
вычислений, производимых внутри цикла вывода, сводятся в таблицы.
Этап оптимизации порой занимает больше времени, чем этап разработки.
Особенно тщательно стоит оптимизировать такие трудноопгимизируемые и
медленные функции, как масштабирование и вращение объектов. По-
возможности, необходимо больше пользоваться регистровыми переменными,
меньше использовать операции умножения и деления и не использовать
универсальные функции (на все случаи жизни), так как универсальность, как
правило, избыточна, что не сулит дополнительного увеличения быстродействия.
Не надо экономить память (в разумных пределах). Главный критерий, к которому
надо стремиться - это скорость.
9.4. Основная функция обработки
графики в игре DOOM.
Обработкой и выводом графики в игре DOOM управляет функция Graphics.
Вызов этой функции, как мы видели в главе 4, осуществляется из основного
цикла игры PlayGame.
Исходный текст функции Graphics:
void Graphics (void) И обработка графики
{
register int m, n, 1;
m = 0;
if (ChangeParScr)
//ненулевое значение ChangeParScr указы><ает
И на то, что изменились параметры экрана и необходимо
И пересчитать виртуальные таблицы и некоторые глобальные
//переменные
ChangeGraphicsMode();
// пересчитать таблицы иглобальные переменные
ChangePal = -1; // необходимо изменить палитру
CountDrawBorder = 3,
// необходимо три раза перерисовывать бордер (текстуры в
// виде кирпичиков) вокруг игрового пространства
if( GraphMode != ChangeDrawEffect)
И необходимо использовать графический эффект: стекание
{
n = 1; // признак использования эффектов графики
CopyScreen2();
// копирование текущего экрана во временный буфер 2
}
else
п = 0; // эффект графики не использовать
switch ( GraphMode)
{
case 0: if (GameTics)
И хотя бы один цикл игры уже прошел
{
if ( MapMode ) И режим карты
DrawingMap(); И нарисовать карту
DrawingStatus((SizeScreenY == 200));
// вывести статус в нижнюю часть экрана
}
break;
case 1: DrawEndLevel();
И нарисовать статус конца уровня
break;
case 2: EndMapPics();
// нарисовать финальную заставку
break;
case 3: DrawPath(0,0,0,
(short *)LumpForName(NameTitlePic, 101));
UpdateChangeBlock();
// вывести измененные части на экран
if ( (GraphMode && (MapMode && GameTics )
DrawingPlane(&chuv[player]);
// вымести трехмерный лабиринт и спрайты на экран
if ((GraphMode && GameTics)
DrawPlayerString(); И вывод сообщений
if (GraphMode != ChangePal)
SetPal ((char *)LumpForName(“PLAYPAL”, 101));
// установить палитру
if( (GraphMode && ChangePal)
{
OfiDrawBorder = 0;
DrawBorder();
// вывести текстуры бордера в буфер экрана
if ((GraphMode && (MapMode && SizeScreenX != 320 )
{
if ( MenuInPlayMode || OldMenuInPlayMode ||
(OfiDrawBorder)
CountDrawBorder = 3; // три раза выводить бордер
if ( CountDrawBorder)
{
OutBorder lbScreen();
// вывести бордер на экран из буфера
CountDrawBorder-; И декремент счетчика
}
}
OldMenuInPlayMode = MenuInPlayMode;
OfiDrawBorder = InitDrawBorder;
ChangeDrawEffect = ChangePal = GraphMode;
if (pause) // если нажата пауза
{
if ( MapMode)
m = 4; // координата Y в режиме “PAUSE”
else
m = StartPozYtoScreen + 4;
// вывести сообщение в верхней части экрана “PAUSE”
DrawPathDirect(((SizeScreeriX-68)»l) +
StartPozX_toScreen,m,0,
(short *)LumpForName(“M_PAUSE”,101));
DrawMenu(); // вывод меню
if( !n)
SwitchVGAPageQ; // переключить видимую страницу
else
{
CopyScreen3(0,0, 320,200);
II копирование текущего экрана во временный буфер 3
m = GetCurrentTic() -1; И взять текущее время (тик)
do
{
1 = GetCurrentTic();
if(n = (l-m))
{
m = 1; И предыдущее время равно текущему
n = SpawnDrawEffect(l, 0,0,320,200, п);
// использовать эффект экрана
UpdateChangeBlock();
// вывести на экран измененные участки
DrawMenu(); И вывод меню
Switch VGAPageQ;
//переключить видимую страницу
}
} while (In);
}
}
Как видно, данная функция лишь управляет выводом графики и
осуществляет вывод конкретных графических функций в зависимости от
режимов работы игры.
При первом входе в функцию Graphics или при любом изменении размеров
плана просмотра переменная ChangeParScr равна 1, что влечет за собой вызов
функции ChangeGraphicsMode, которая осуществляет заполнение некоторых
виртуальных таблиц и глобальных переменных, часто используемых в игре.
Исходный текст функции ChangeGraphicsMode:
char *MapColor[16][48J;
// таблица перспективного затенения
void ChangeGraphicsMode (void)
{
register int m, i, p, n, o, q, r;
ChangeParScr=0;
// далее вызов функции необязателен
m = blkscr;
if ( m == 11 )
И размер экрана (если 11, то весь экран)
SizeScreenX = 320; И размер по горизонтали
SizeScreenY = 200; // размер по вертикали
}
else
// иначе расчет по формулам соответственно по X и Y
{
SizeScreenX = m « 5; И для X: NewX = Х*32
SizeScreenY = (m * 168 /10) & 0хй1Ш18;
}
DetailLow = detail; //графическая детализация
DetailSizeScreenX = SizeScreenX » DetailLow;
И горизонта.! ЬШцit размер по X с учетом детализации
HalfSizeScreenX = DetailSizeScreenX/2;
// центр трехмерной системы координат
HallSizeScreenY = SizeScreenY/2;
И центр зренияY
// то же самое для Y с фиксированной точкой
fixHal (SizeScreenY = INT_TO_FIXED(Hal (SizeScreenY);
И то же самое для X с фиксированной точкой
fix_HalfSizeScreenX = INT_TO_FIXED(HaliSizeScreenX);
if (IDetailLow )
// адреса функций для высокой детализации
{
FnDrawTexture = &Asm_DrawWallHigh;
FnDrawCurrent = &Asm_DrawWallfligh;
FnDrawPlane = &Asm_DrawPlaneHigh;
}
else // адреса функций для низкой детализации
{
FnDrawTexture = &Asm_DrawWallLow,
FnDrawCurrent = &Asm_DrawWallLow,
FnDrawPlane = &Asm_DrawPlaneLow,
}
ScreenAdjust(SizeScreenX, SizeScreenY);
//Вычисление физиических параметров экрана
И (начальные координаты и др. параметры)
CreateTableProject();// таблицы проекций
И коэффициент масштабирования для вывода спрайтов
//оружия
ScaleAmmoX = INT_TO_FIXED(DetailSizeScreenX)/ 320;
// обратный коэффициент масштабирования для вывода
//спрайтов оружия
Neg^ScaleAmmoX = INTTO_FIXED(320) /
DetailSizcScreenX;
for( i=0; i<DetailSizeScrcenX; i++)
// вычисление таблицы
{
EndPosY_ofX[i] = (short )SizeScreenY;
}
for (i=0; i<SizeScreenY; t++ )
// вычисление таблицы масштабирования no Y для плана
{
m = abs(INT_TO_FIXED(i - SizeScreenY/2) + 0x8000);
TabScale_ofY_Plane[i] =_fixdiv((INT_TO_FIXED(DetailSizeScreenX «
DetailLow)/2), m);
for (i=0; i<Detai!SizeScrcenX; r++)
// вычисление виртуальной таблицы дистанций по X
//для плана
{
Distance_ofXjPlane[i] = _fixdiv(INT_TO_FIXED(l),
abs(Cosme(VisAngle_ofX[i]» 19]));
}
i = 0;//количество итераций no i: 16
m = 960;
n = DetailSizeScreenX « DetailLow,
do
// далее цикл для вычисления таблицы затенений
{
о = m /16,
q = 0; // количество итераций по q: 48
// (число градаций затенения по вертикали)
г = 0;
do
{
p = o- r/ n/2;
if(p<0)
р = 0;
if(p = 32)
Р=31;
// максимальный уровень освещенности
MapColor[i][q] = adcolor + (р < 8);
// таблица перспективного затенения
г+=320;
q++;
} while ( q != 48 );
i++;
m -= 64;
} while (i != 16 );
}
Одной из самых основополагающих здесь является функция
CreateTableProject. Эта функция создает две таблицы:
PerspPosX ofAngle] n] - представляет собой зависимость физических
координат X на экране от утла обзора. Эта таблица используется для вычисления
начальных и конечных координат сегментов стен при их текстурировании (п -
угол, изменяется от 0 до 4095).
VisAngle_ofX[n] - таблица, обратная таблице PerspPosXofAngle.
Представляет собой значения угла обзора в зависимости от физической
координаты экрана X. (п - физическая координата. Изменяется от 0 до 319)
Также функция CreateTableProject инициализирует одну глобальную
переменную с именем LimitVisualAngle, которая называется граничным углом
просмотра и используется для анализа границ при прорисовке сегментов стен.
Нетрудно догадаться, что
LimitVisualAngle = VisAngleofXfO];
А сейчас мы, пожалуй, подошли к самому' главному. Мы коротко опишем
чрезвычайно сложную и главную функцию в системе 3D-ENGINE, которая
осуществляет вывод трехмерного лабиринта с использованием законов
перспективной проекции и перспективного затенения. Такая нелегкая задача
поручена функции DrawingPlane, которая вызывается в режиме графики 0.
(GraphMode = 0). Достаточно полно я опишу' данную систему в инструкции по
пользованию системой 3D-ENGINE, которая будет поставляться в комплекте с
библиотеками функций данной системы, на дискетах приложения (см. рекламу
в конце книги).
Функция DrawingPlane первоначально инициализирует некоторые таблицы
и глобальные переменные, которые активно используются в дальнейшем, затем
рисуются стены, потолки и полы,.и в последнюю очередь вызывается функция,
которая выводит изображения спрайтов, объектов и оружия.
Исходный текст функции DrawingPlane:
void DrawingPlane (MAN *m)
{
// инициализация таблиц и глобальных переменных для игрока
Init_ValTab_forPlayer(m);
// инициализация таблицы, используемой для маскирования
И части стены, перекрываемой стеной на переднем плане
InitTable_WallPosX();
И инициализация таблицы, используемой для прорисовки
//стен (сегментов)
Init_DrawSegs();
// инициализация таблицы, используемой для прорисовки
// полов/потолков
lnitVal_Plane(X
И инициалиизация таблицы, используемой для прорисовки
// объектов
InitTableSprite();
// прорисовка стен
ProcessTree(numnodes -1);
И прорисовка полов/потолков
DrawPlanes();
// прорисовка двумерных объектов (спрайтов)
DrawSprites();
}
Функция lnit_ValTab_forPlayer.
Функция Init_ValTab_forPlayer устанавливает некоторые глобальные
переменные, которые имеют непосредственное отношение к игроку. Это прежде
всего координаты игрока в пространстве, угол поворота игрока (синус и косинус
данного угла), состояние глаз игрока (цветовая палитра) и некоторые другие
параметры.
Исходный текст функции Init ValTabforPlayer:
void Init_ValTab_forPlayer (register MAN *m)
{
register THING *t,
register unsigned int 1;
register char *cl;
t = m->PlayerThing;f PozX ofPlayer = t-korX; // позиция X игрока в пространстве
PozYofPlayer = t-korY; // позиция Y игрока в пространстве
AngofPlayer = t->Ang+Ang View,
И значение утла поворота игрока И фаза стрельбы игрока (управляет освещенностью)
ShootingStat_ofPlayer = m->StepShooting;
// позиция таз игрока (высота)
Height^ofHeadPlayer = ni->LevelOfHead;
1 = (unsigned)Ang ofPlayer » 19; // значение угла
SrnePlayer= Sine|l|; // значение синуса для утла И поворота игрока
CosinePlayer = Cosinefl]; И значение косинуса для утла // поворота игрока
Playerlnfo = m; CountSubSectors = 0; if (m->palitra ) // адрес данных игрока И счетчик субсекторов // если используется палитра для глаз И игрока зеленая, красная или другая
{
CurMapColor = &TabOffset_AltemColor, И текущая карта цветов cl = adcolor + (m->palitra « 8); // адрес таблицы цветов для данной палитры for (1=0; 1!=48; 1++ ) // заполнение таблицы альтернативной палитры { TabOffset AltemColorfl] = cl; }
} else //иначе
cl = NULL;
Counter-н-,
// альтернативная таблица отсутствует
CountFrames++; // счетчик кадров
// стартовый адрес экрана
StartAdrScreen = AdVga3 + (StartPozXtoScreen » 2) +
StartPozY_toScreen*80;
AlternTabColor = cl; // таблица градаций альтернативных цветов
}
Функция lnitTable_WallPosX.
Функция InitTableWallPosX яляется весьма оригинальным и вместе с тем
очевидным решением для удаления скрытых поверхностей. Каждый
программист, работающий в жанре компьютерных игр, знает, что техника
быстрого удаления скрытых поверхностей яляется высшим пилотажем.
Неудивительно, что достаточно длительное время мы не видели ни одной более-
менее реалистичной пространственной игры. Причина заключается именно в
данном алгоритме. Существовало достаточно много подобных методов, но все
они были очень ограничены и, кроме того, их быстродействие не позволяло
использовать подобную технологию в динамичных компьютерных играх.
В DOOM’e техника удаления скрытых поверхностей вытекает из самого
принципа BSP, который мы рассмотрели в разделе 7.7. Но это далеко не все.
Существует таблица, которая представляет собой зависимость физической
координаты X на экране от угла между направлением зрения игрока и позицией
одной из вершин стены. Эта таблица имеет название PerspPosX ofAngle.
Рассмотрим короткую строку':
X = PerspPosX_ofAngle[(ang+0x40000000)>> 19];
Здесь ang - угол между направлением зрения игрока и позицией вершины
стены. А теперь разберемся, зачем к значению угла мы прибавили величину'
0x40000000. Вспомним, что бинарный угол 0x40000000 соответствует углу в 90
градусов. Но поле зрения любого человека не может охватить вселенную в
большем диапазоне, чем от -90 градусов (крайнее левое направление) до +90
градусов (крайнее правое направление). В DOOM ’e для полного экрана дипазон
зрения игрока находится в пределах от -90 градусов до +90 градусов Таким
образом, к значению утла ang мы прибавляем 90 градусов, чтобы сократить
данную таблицу путем исключения значений углов, лежащих за пределами 90
градусов (не требуется учитывать отрицательные значения углов) . В таком
случае данная таблица учитывает значения углов в пределах от 0 до 180
градусов, что более чем достаточно.
X в данной строке представляет собой физическую коордиииналу X на
экране.
Угол ang можно рассчитать так:
ang = dir(sg->FrVert->krX, sg->FrVert->krY) -AngofPlayer;
Здесь sg - это указатель на сегмент стены.
Функция dir возвращает бинарный угол между вектором, который соединяет
позицию игрока и вершину, координаты которой передаются в функцию
dir двумя параметрами в системе реальных двумерных координат
автоматической карты.
В самой технике удаления скрытых поверхностей участвует таблица
AlreadyOnScreenWallPosX. Ее назначение напрямую вытекает из названия
таблицы. Эта таблица представляет собой список горизонтальных физических
координат сегментов, которые уже были выведены. Исходя из принципов
узловой структуры, описанной в разделе 7.7., первыми прорисовываются
сегменты, которые относятся непосредственно к тому субсекгору, “внутри”
которого находится собственно сам игрок. При прорисовке сегментов данного
субсектора координаты вершин стен запоминаются в таблице
AlreadyOnScreen WallPosX. Далее, если необходимо, прорисовываются
субсекгора, которые должны быть видны из предыдущего субсекгора. Очевидно,
что часть сегментов текущего субсектора будет скрыта сегментами
предыдущего субсектора (который выводился на экран ранее). Данное
маскирование сегментов осуществляется с помощью таблицы
AlreadyOnScreen WallPosX путем несложного сравнения.
Вот и все, что я хотел сказать о данной таблице. Функция InitTable WallPosX
инициализирует ее начальными значениями, которые определяют, что в
настоящее время экран пугст и никакое маскирование поверхностей не
требуется.
Исходный текст функции InitTable WallPosX:
void InitTable_WallPosX (void)
{
AlreadyOnScreen_WallPosX[0].minX = 0x80000001;
AlreadyOnScreen_WallPosX[0].maxX = -1;
Already* )nScroen_WallPosX[l ].minX = DetailSizeScreenX;
AlreadyOnScreen_WallPosX[ 1 ].maxX = 0х7ЙЙЙГ;
Offs Proj AlreadyX = &AlreadyOnScreen_WallPosX[2];
}
Функция lnit_DrawSegs.
В процессе прорисовки сегментов заполняется таблица DrawSegs. Она
представляет собой 128 структур wall (128 - это максимально возможное
количество сегментов, одновременно видимых на экране). Данная таблица в
основном используется для прорисовки объектов и спрайтов для осуществления
битового отсечения. Функция InitDrawSegs инициализирует данную таблицу
путем установки указателя DrawSegsOffset на начало таблицы.
Исходный текст функции Init DrawSegs:
struct wall DrawSegs[128];
struct wall *DrawSegsOffset;
void InitDrawSegs (void)
{
DrawSegsOffset = &DrawSegs,
}
Функция InitTableSprite.
Информация о спрайтах также накапливается при прорисовке сегментов
субсекторов. Если вы не забыли, в структуре каждого сектора имеется
информация о всех объектах, находящихся в пределах данного сектора. В свою
очередь каждый субсекгор обязательно связан с одним конкретным сектором.
Следовательно, было бы очевидным решением данную таблицу заполнял,
именно в процессе прорисовки сегментов субсекгора. Однако, не следует
забывать, что один сектор может быть поделен на несколько субсекгоров.
Поэтому’, чтобы не делать одну' и ту же работу несколько раз, должен быть
механизм для предотвращения обработки объектов конкретного сектора, в
случае, если объекты данного сектора уже обрабатывались.
Исходный текст функции nitTableSprite
struct draw_sprit TableSpritef 128];
struct drawjsprit *OffsJTableSprite;
void InitTableSprite (void)
{
OffsTableSprite = &TableSprite;
}
Функция ProcessTree.
А сейчас мы опишем функцию, о которой уже шла речь в разделе 7.7.1.
Функция ProcessTree проходит по узлам рекурсивного дерева и осуществляет
вывод на экран соответствующих субсекторов. Кроме того, она осуществляет
заполнение вышеописанных структур для максимальной скорости вывода.
Исходный текст функции ProcessTree:
void ProcessTree (register int nd)
{
register int ni;
register NODE *n;
while ( !(nd & 0x8000) )
{
n = &anodes[nd]; И текущий узел
m = GetNextBranch_ofTree(PozX_ofPlayer,
PozY ofPlayer, n);
ProcessTree((unsigned int)n->ssect[m]);
// обработать следующий узел
m Л= 1;
if(!CheckVisual_OppArea(&n->locat[m]))
// проверка видимости противоположной области
return;
nd = n->ssect[m] & Oxffff;
}
DrawSubsectoifnd & 0xfflF7fff);
// прорисовка субсектора
}
Алгоритм работы дайной функции мы здесь рассматривать не будем. Об
этом мы достаточно рассказали в разделе 7.7.1.
Функции Draw_Subsector.
Она осуществляет прорисовку сегментов стен конкретного субсекгора,
номер которого передается в нее единственным параметром.
Исходный текст функции Draw Subsector:
void Draw_Subsector (register^nt v)
register inti;
register SECTOR *se;
register SUB SECTOR *ss;
register SEGS *sg;
if ( v = numssec ) // если номер субсектора превышает
// общее количество субсекторов, то фатальный выход
// с ошибкой
ExitWithMsg(“R_Subsector. ss%i with numss = %i”, v,numssec),
ss = &assec[v]; И указатель на субсектор
CountSubSectors++; И инкремент счетчика субсекторов
se = ss->SectSeg, // сектор, относящийся к данному
И субсектору
DrawingSector = se;
sg - &asegs[ss->segs];
// указатель на список сегментов стен
1 = ss->num; // количество сегментов стен
И если уровень глаз игрока выше пола,
if ( Height ofHeadPlayer > se->floorZ )
И то искать соо тветствующий план для пола
PlaneFloor = FindPlane(se->floorZ, se->NumFioor,
se->light);
else II иначе
PlaneFloor = 0; // плана для пола нет
// если уровень глаз игрока ниже потолка или потолок
И является небом,
if ( HeightofHeadPlayer < se->ceilZ ||
se->NumCeil == numfskyl )
// то искать соответствующий план для потолка
PlaneCeil = FindPlane(se->ceilZ, se->NumCeil,
se-> light);
else // иначе
PlaneCeil = 0; // плана для потолка нет
ProjectSpritesInSector(se);
И заполнение информации о спрайтах в данном секторе
// (заполнение таблицы TableSprite).
while (1- ) И прорисовка 1 сегментов
{
DrawSegment(sg); // прорисовка сегмента sg
sg++; И взять следующий сегмент
} }
Функция FindPlane ищет план с конкретными атрибутами и параметрами.
План представляет собой структуру vplan, которая необходима для прорисовки
потолков и полов.
struct vplan
{
int PozZ;
intNumTextur,
int Light,
int MinPozX;
int MaxPozX;
unsigned char MinY[322];
unsigned char MaxY[322];
(/ высота плана
// номер текстуры плана
И освещенность плана
И минимальная физическая
И координата X
//максимальная физическая
// координата X
И таблица минимальных значений
// Y в зависимости от X (от 0 до 200)
И таблица максимальных значений
// Y в зависимости от X (от 0 до 200)
};
Зачем мы осуществляем поиск плана? Почему' нельзя заполнить структуру
для конкретного субсектора, не осуществляя никакого поиска? Дело в том, что
сектор может быть поделен на несколько субсекторов. Поэтому’ нет смысла для
всех субсекторов, из которых состоит один сектор, использовать разные
структуры vplan Кроме того, вполне вероятно, что план для одного
видимого сектора будет иметь те же характеристики, что и для друтого
видимого. Сверяемыми параметрами, или критериями совместимости,
являются:
- высота, или координата Z плана;
- номер текстуры плана;
-освещенность.
Если все три параметра текущего сектора совпадают с параметрами
конкретного плана, то функция FindPlane возвращает указатель на данную
структуру. Если хотя бы один из этих параметров не совпал, то инкрементируется
указатель на таблицу плана VisPlanesOffset и создается новая структура,
указатель на которую и возвращается.
Исходный текст функции FindPlanes:
struct vplan VisPlanesf 128];
struct vplan * VisPlanesOffset;
struct vplan * FindPlane (int Z, int txt, int lig)
register struct vplan *vpl, *vp2;
if( txt == numfskyl )
II если номер текстуры - небо
Z = lig = O;
vpl = VisPlanesOffset;
И указатель на последнюю структуру vplan
vp2 = &VisPlanes;
// указатель на первую структуру vplan
while ( vp2 < vpl ) И обработать все указатели
{
if ( Z == vp2->Poz7. &&
txt == vp2->NumTextur &&
lig == vp2->Light)
break; // план найден
vp2++;
LimitVisualAngle
}
if( vp2 = vpl )
{ 11 проверка граничных условий
if( ((int)vpl-(int)&VisPlanes)/
sizeof(struct vplan) == 128 )
ExitWithMsg(“R_FindPlane: no more visplanes”);
vp2->MinPozX = 320;
vp2->MaxPozX = -1;
VisPlanesOffset++;
vp2->PozZ = Z;
vp2->NumTextur = txt;
vp2->Light = lig;
memset(&vp2->MinY[l], 255, 320);
}
return vp2,
Функция ProjectSpritesInSector.
Данная функция создает структуры для всех спрайтов и объектов,
находящихся в конкретном секторе. Д ля исключения многократной обработки
одного и того же сектора в самом начале функции ProjectSpritesInSector
осуществляется сравнение со значением Counter.
Исходный текст функции ProjectSpritesInSector.
char * *MapColors_Sprites; II таблица цветов для ci файлов в данном секторе
void ProjectSpritesInSector (register SECTOR *se)
register int 1;
register THING *t;
if (Counter != se-CountUseSector)
И проверка для исключения многократной
// обработки одного сектора
se->CountUseSector = Counter,
И уровень освещенности для данного набора спрайтов
1 = ShootingStat ofPlayer + (se->light» 4);
if (1 < 0 ) /7 минимальная освещенность (ничего не видно)
MapColorsSprites = &MapColor[0][0];
else
if (1 >= 16 ) // максимальная освещенность
MapColors_Sprites = &MapColor[15][0J;
else
освещенности
// иначе промежуточное значение
MapColors_Sprites = &MapColor[l][0];
t = se->Things!nSector, // первый спрайт в секторе
while (t)
// обработать все объекты
ProjectSprite(t);
t = t->NextTh;
//спроектировать спрайт
И взять следующий спрайт
Функция DrawSegment
Она осуществляет прорисовку сегмента стены для конкретного субсекгора
Принцип действия здесь очень прост (смотрите рисунок). Сначала
вычисляются значения углов между' вектором зрения игрока и каждой из
вершин сегмента (соответственно утлы 1 и ш). Затем проверяется выход
значений углов за объем просмотра и. если угол выходит за пределы угла зрения
(Limit Visual Angle), то производится отсечение. После этого, с использованием
вышеупомянутой таблицы PerspPosXofAngle, вычисляются физические
координаты X вершин сегмента на экране, и с данными параметрами
вызывается функция прорисовки сегмента.
Исходный текст функции DrawSegment
void DrawSegment (register SEGS *sg)
{
register unsigned int 1, m, n, xl, x2;
register SECTOR *sel, *se;
DrawingSegment = sg; // рисуемый сегмент стены
И угол между игроком и первой вершиной сегмента
1 = dir(sg->FrVert->krX, sg->FrVert->krY);
И угол между игроком и второй вершиной сегмента
in = dir(sg-ToVert-krX, sg-ToVert-krY);
n = 1 - nr, 11 угол обзора данного сегмента
if (n < 0x80000000) И если утол п меньше 180
7/ градусов, то продолжить обработку
{
Angle StartVert forSegm=l;
77 стартовый утол сегмента
1 -= AngofPlqyer;
xl = 1;
И угол между вектором просмотра и вершиной 1
х2 = m - Ang_ofPlayer;
И угол между вектором просмотра и вершиной 2
1 += LimitVisualAngle;
m = LimitVisualAngle * 2;
И двойной граничный утол
if (1 > m)
7/ сегмент должен отсекаться со стороны первой
И вершины
{
if((l-m)>=n)
7/ если утол отсекания превышает утол обзора, то
7/ данный сегмент игрок видеть не может
return;
xl = LimitVisualAngle; И граничный утол слева
}
1 = LimitVisualAngle - х2;
if (1 > m )
7/ сегмент должен отсекаться со стороны второй
7/ вершины
{
if((l-m)>=n)
И если утол отсекания превышает утол обзора,
7/ то данный сегмент игрок видеть не может
return;
х2 = -LimitVisualAngle; И граничный угол справа
И координата X для первой вершины
xl = PerspPosX_ofAngle[(xl+0x40000000)»19];
7/ координата X для второй вершины
х2 = PerspPosX_ofAngle[(x2+0x40000000)»19];
if ( xl != х2 )
И если координаты X равны, то сегмент лежит на
И векторе просмотра и виден не будет
{
sei = sg->OppSecSeg;
И сектор на другой стороне сегмента
DravvingOppSector = sei;
se = DrawingSector, И текущий сектор
if ( !sel || sel->ceilZ<= se->floorZ ||
sel->floorZ >= se->ceilZ )
DrawMonolitWall(x 1, x2-1);
// прорисовка цельной стены
else
if( sel->ceilZ!=se->ceilZ ||
sel->floorZ!=se->floorZ ||
sel->NumCeil!=se->NumCeil ||
sel->NumFloor!=se->NumFloor ||
sel->light!=se->light ||
sg->SidSeg->FulTex )
DrawOtherWall(xl, x2-l);
// прорисовка других стен
}
} }
Функция прорисовки сегмента вызывается только в том случае, если хотя-
бы малая часть сегмента попадает в область обзора. Для проверки граничных
условий используется глобальная переменная LimitVisualAngle, которая
определяет максимальный угол для границы объема просмотра, и зависит от
размера объема просмотра, т.е. от размера экрана (изменяется клавишами *+’
и Угол обзора сегмента п - это угол, противолежащий стороне сегмента.
Если угол одной из вершин сегмента относительно вектора просмотра игрока
превышает по абсолютному значению граничный угол LimitVisualAngle, то
данная вершина должна отсекаться. Прорисовка сегментов стен осуществляется
одной из двух функций:
- DrawMonolitWall - для монолитных (цельных стен);
- DrawOtherWall - для других случаев.
Отличаются эти две функции прежде всего принципом организации
алгоритма удаления скрытых поверхностей. В функции DrawMonolitWall
прорисовывается сегмент стены, который может представлять препятствие для
других, выводимых позднее сегментов. Это цельные, монолитные стены.
Функция DrawOtherWall выводит чаще всего сегменты стен, имеющих нижние
и/или верхние текстуры, т.е. представляют собой определенное взаимное
повышение/понижение уровня полов/потолков соседних секторов. С
конструктивной стороны эти две функции отличаются тем, что функция
DrawMonolitWall обновляет таблицу Already OnScreen WallPosX и сдвигает
указатель на эту таблицу Offs Proj AlreadyX, а функция DrawOtherWall таблицу'
не обновляет, а просто использует ее.
Для примера рассмотрим работу функции DrawOtherWall.
void DrawOtherWall (register int xl, register int x2)
{
register int m;
register struct boundary ♦ml;
ml = AlreadyOnScreenWallPosX;
II указатель на начало таблицы
while( xl-1 >ml-maxX)ml++;
// искать указатель на тот сегмент, который может
И участвовать в перекрытии текущего сегмента
if ( xl <ml->minX) //слева сегмент не отсекается
{
ш = ml->minX-l; И минимальное значение X в таблице по
// текущему указателю
if(x2<m)
{
RenderWallRange(xl, х2);
И нарисовать сегмент полностью
return;
}
RenderWalIRange( х 1, m);
// нарисовать сегмент с отсечением справа
while ( х2> ml->maxX )
// цикл для тех случаев, когда сегмент отсекается
// посередине
{
m = ml [1].minX-1; // минимальн о значение X следующего
И (справа) сегмента
if(x2<m) //если правая координата сегмента
И меньше минимального значения X
//следующего сегмента,
{
RenderWal 1 Range/m 1 ->maxX+1, x2);
11 то дорисовать сегмент до конца
return;
}
RenderWallRange(ml->maxX+l, m);
// нарисовать очередную часть сегмента
ml++; И инкрементировать указатель
} }
Сейчас я подробней объясню принцип действия алгоритма удаления
скрытых поверхностей с использованием таблицы AlreadyOnScreen WallPosX.
Каждый элемент таблицы - это две координаты: minX и тахХ. Эти значения
определяют, соответственно, минимальную и максимальную физические
координаты X на экране. По мере того, как прорисовывается очередной
монолитный сегмент стены, его горизонтальные координаты записываются в
таблицу с применением сортировки “слева направо” и с использованием
отсечения, если необходимо. Например, если уже было выведено три участка
стены с координатами:
4,20 35,50 66,84 и нам необходимо вывести новый сегмент стены с
координатами:
54,60, то таблица будет выглядеть так:
4,2035,50 54,6066,84
Если же новый участок стены имеет координаты:
44,64, то таблица будет иметь вид:
4,2035,6466,84
Таким образом, новый сегмент просто увеличил в размере второй участок
в таблице. При выводе на экран данного сегмента произошло отсечение справа
участка от 44 до 50, а на экране мы увидели часть от 50 до 64. Если бы вторая
(правая) координата нового сегмента была больше значения 66, но меньше
значения 84, то сегмент бы отсекся с обеих сторон, а на экран вывелся участок
с координатами: 50,66. При этом таблица бы выглядела так:
4,2035,84
Если бы вторая координата нового сегмента превышала значение 84
(например 112), то у данного сегмента произошло бы отсечение справа (44,50)
и в середине (66,84). При этом таблица выглядела бы так:
4,20 35,112
Как видно, все очень просто, если учитывать, что первыми
прорисовываются сегменты на переднем плане и так далее, по мере удаления
от глаза, до тех пор, пока не прорисуются все видимые сегменты стен. Данный
рендеринг нам гарантирует система BSP дерева (рекурсивного дерева).
Функция RenderWallRange прорисовывает участок сегмента стены на
экране. Ее исходный текст мы здесь приводить не будем.
Функция DrawPlanes.
Функция DrawPlanes прорисовывает полы и потолки трехмерного
лабиринта, используя сформированную ранее таблиц}' VisPlanes. Принцип
действия очень прост. Данная функция закрашивает участки экрана
соответствующими текстурами в тех местах, куда не выводились сегменты стен.
Для этого в структуре vplan используются значения:
MinPozX, MaxPozX - соответственно минимальная и максимальная
координаты X закрашиваемой области.
MinY[322| и MaxY[322] - массив минимальных и максимальных значений
Y в зависимости отХ.
Исходный текст функции DrawPlanes:
void DrawPlanes (void)
{
register int xl, x2;
register struct vplan *vp;
register char *ch;
II проверка граничных условий для сегментов стен
xl = ((int)DrawSegsOffset - (int)&DrawSegs) I sizeof(struct wall);
if(xl > 128)
ExitWithMsg(“R_DrawPlanes: drawsegs overflow (%i)”,xl);
И проверка граничных условий для планов
xl =((int)VisPlanesOfI'set-(int)&VisPlanes)/sizeof( struct vplan);
if ( xl > 128)
ExitWithMsg(“R_DrawPlanes: visplane overflow (%i)”,x 1);
vp= &VisPlanes; II указатель на первую структуру vplan
while ( vp < VisPlanesOffset) // обработать все планы
{
xl = vp->MinPozX; // минимальная координата X, с которой
И начать вывод плана
х2 = vp->MaxPozX; И млксималытая координата X, на
И которой закончить вывод плана,
if ( xl <= х2 ) И если минимальная координата X меньше
И максимальной координаты X
if ( vp->NumTextur == numfskyl )
И если текстура - небо
{
DrawScaleToY = DrawScaleToYSky;
И коэффициент масштабирования для неба по
И вертикали (равен 1)
CurAdrMapColor = adcolor,
И карта затемнения для неба
И (максимальная освещенность)
HeighDrawObjectoffiye = HeighSkyofEye;
// высота рисуемого объекта от уровня глаз.
И Для неба - это значение равно 100, что
И означает половину экрана.
ch = &vp-MinY[xl+l];
// указатель на таблицу минимальных и
// максимальных значений Y для плана в
//зависимости от X
while(xl <=х2)
// рисовать весь план от х 1 до х2
{
DrawStartPosY = (int)ch[OJ;
И стартовая позиция Y
DrawEndPosY = (int)ch[322];
И конечная позиция Y
if (DrawStartPosY <= DrawEndPosY )
{
Draw_CurPosX = xl;
// текущая физическая координата X
И загрузил, один столбец текстуры NumSky (неба)
AdrTexture=CacheRead_TextureColurnn (NumSky,
(Ang_ofPlayer+ VisAngle_ofX[xl])>> 22);
FnDraw Current();
/7 вывести на экран один столбец
//плана (неба)
}
ch++;
И инкрементировать указатель координат
Х1++;
И инкрементировать текущую координату X
}
}
else И иначе, если текстура - не небо
{
И загрузить плоскую текстуру плана размерами 64x64
GrData_Plane=ReadLumpFoiNum(fstart+adftex[vi»NumTextur], 1);
// абсолютное значение высоты плана
И относительно уровня глаз игрока
VisHeightPlane = abs(vp->PozZ -HeightofHeadPlayer);
И вычисление ucrtei ценности плана
xl = (vp->Light » 4) + ShootingStat_ofPlayer;*
if( xl >= 16)
xl = 15; II максимальная освещенность
if( xl<0)
xl = 0; II минимальная освещенность
И таблица цветов плана
ТаЬМарСо1ог_Р1апе = &tabcol[xl«7],
vp->MmY[vp->MaxPozX+2] = 255;
vp->MinY[vp->MinPozX] = 255;
х2++;
xl = vp->MinPozX; И минимальная координатах
ch = &vp->MinY[l];
while (xl <= x2)
II рисовать весь план от xl до х2
{
И сформировать один столбец плана
ProjectCuiColumn_forPlane(xl, ch[O], ch[322],ch[ 1 ], ch[323]);
ch++;
И инкрементировать указатель на таблицу
И координат
Х1++;
И инкрементировать текущую координату X
// проверка корректности распределения памяти
if (((struct memory *)
(GrDatai_Plane)-l j->flag != ZONEID)
ExitWithMsg(“Z_CT atr_plane.c: %i”, 408);
ChangeTag((int * )GrData_Plane, 101);
II изменить номер региона памяти на 101 (Кэш)
} }
vp++; И инкрементировать указатель на план
}
Функция DrawPlanes состоит из двух частей:
1 часть - это вывод неба (номер текстуры соответствует значению
numfsky 1.) Размер текстуры соответствует 256 х 128.
2 часть - это вывод всех остальных текстур для плана. Размер текстуры
имеет фиксированный размер 64 х 64.
Рассмотрим каждую из названных частей по отдельности.
В выводе текстуры неба участвуют две функции:
CacheRead TextureColumn - эта функция загружает текстуру, номер
которой передается в нее первым параметром, с диска и передает указатель на
столбец текстуры, номер которого передается вторым параметром. Загрузка с
диска кэшируется по принципу, описанному в разделе 3.4.
FnDraw Current - это глобальная переменная, которая указывает на одну из
функций вывода на экран: Asm DrawWallHigh (вывод с высокой
детализацией) или AsmDrawWallLow (вывод с низкой детализацией) Обе
эти функции написаны на ассемблере для ускорения вывода. Они
осуществляют вывод на экран одного столбца текстуры с позиции по X
DrawCurPosX. Переменные DrawStartPosY и DrawEndPosY определяют
соответственно начальную и конечную физические координаты Y экрана.
Переменная DrawScaleToY - определяет коэффициент масштабирования
вывода по вертикали. Эта переменная имеет формат с фиксированной
точкой. При этом значение меньше 1 означает, что объект необходимо
выводить в увеличенном формате (например, значению переменной 0.5
соответствует увеличение объекта в два раза). Значение 1 говорит о том,
что масштабирование не требуется и объект выводится в натуральную
величину. Значение больше 1 означает, что объект выводится в
уменьшенном виде (Например, значению 2 соответствует уменьшение
объекта в два раза). А сейчас в качестве примера приведем исходный текст
функции AsmDrawWallfiigh:
_DATA segment use32 para public ‘DATA’
assume ds:_DATA
StartAdrScreen dd 0
Draw Height dd 0
_Draw_Height dd 0
CurAdrMapColor dd 0
AdrTexture dd 0
_Draw_CurPosX dd 0
DrawEndPosY dd 0
_Draw StartPosY dd 0
__HeigJi-DrawObjectofEye dd 0
_Draw' ScaleToYddO
-HalfSizeScreenY ddO
public _StartAdrScreen
public _Draw_Height
public _Draw'_Height
public -CurAdrMapColor
public _AdrTexture
public _Draw_CurPosX
public _Draw'_EndPosY
public -DrawStartPosY
public -Heigh-DrawObjectofEye
public _Draw_ScaleToY
public -HalfSizeScreenY
DATA ends
TEXT segment use32 para public ‘CODE’
assume cs:_TEXT
386p
public Asm_DrawWallHigh_
Asm_DrawWallHigh_ proc near
; начальная координата Y должна быть меньше или равна конечной координате
pushad
mov esi ,_Draw_StartPosY ; начальная координата Y
cmp esi,_Draw_EndPosY ; конечная координата Y jgfgrlO leaedi,[esi+esi*8] ;умножение координаты Y add edi,esi shl edi,3 mov ebx,_Draw_CurPosX ; текущая позиция X movecx,ebx shr ebx,2 addedi,ebx add edi,_StartAdrScreen
; EDI указывает на начальный адрес экрана (куда начинать вывод)
fgrl6: and ecx,3 mov eax, 1 shleax,cl mov edx,3c5h ; установить номер out dx,al ; битовой плоскости mQv eax,_Draw_EndPosY inceax
sub eax,esi
mov _Draw_Height,eax
jsfgrlO
shreax,l
mov Draw Height,eax
; высота вывода
; высота вывода пополам
; далее идет вычисление смещения внутри текстуры
; (откуда начинать извлекать данные)
movecx. Draw ScaleToY
mov eax,_HalfSizeScreenY ; высота игрового пространства
sub eax,esi; пополам
imulecx
mov ebp,_Heigh_DrawObject_ofEye
sub ebp,eax
; EBP указывает на смещение текстуры (формат с фиксированной точкой)
shl ebp,9 ; сдвиг для зациклюания текстуры
mov esi,_AdrTexture ; ESI указывает на адрес текстуры
mov ebx,_Draw_ScaleToY ; коэффициент масштабирования
shl ebx,9 ; текстуры
; модификация кода программы для скорости
mov eax, offset fgrl 1 +2
mov [eax],ebx
mov eax, offset fgrl 2+2
mov [eax],ebx
mov ecx,ebp
add ebp,ebx
shrecx,19h
mov edx,ebp
add ebp,ebx
shredx,19h
mov eax,_CurAdrMapColor
movebx,eax
mov al,[esi+ecx]
movbl,[esi+edx]
mov al,[eax]
mov bl,[ebx]
; указатель на карту цветов
; взять два байта текстуры
; модификация кодов в соответствии
; с картой цветов
; если высота вывода меньше двух, то на выход
test dword ptr _Draw_Height,OFFFFFFFEh
je fgrl3
; далее - цикл вывода столбца текстуры
fgrl4: mov ecx,ebp
fgrll: add ebp,12345678b
fgrl2: mov [edi],al , вывести первый код на экран shrecx,19h mov edx,ebp add ebp,12345678h ; прибавить коэффициент масштабирования mov [edi+80],bl ; вывести следующий байт на 1 строку ниже shredx,19h mov al,[esi+ecx] add edi, 160 ; указатель экрана на 2 строки ниже mov bl,[esi+edx] dec dword ptr _Draw_Height ; декремент счетчика строк mov al,[eax] mov bl,[ebx] jnefgrl4
; если количество выводимых строк нечетно, то вывести последний код
fgrl 3: test dword ptr Draw Height, 1
je fgrlO
mov [edi],al
fgrlO: popad
retn
Asm_DrawWallHigh_ endp
TEXT ends
end
В данной, очень несложной, функции единственный вопрос, который у вас
может возникнуть - зачем смещение в текстуре и коэффициент
масштабирования текстуры сдвигаются влево на 9. Эго сделано для зацикливания
текстуры. Поясню, в чем дело. Смещение в текстуре и коэффициент
масштабирования имеют формат с фиксированной точкой (целых чисел здесь
быть не может, иначе масштабирование будет невозможно). При этом нас
интересует в конечном итоге только целая часть (16 старших бит). Теперь мы
данное значение с фиксированной точкой сдвинем влево на 9. При этом,
фактически, мы получаем 7 бит целой части и 25 бит (32-7) дробной части.
Теперь осталось сообщить, что все текстуры имеют размер по вертикали -128.
Ну!... Улавливаете, в чем дело? Дело в том, что 7-битным двоичным числом
можно представить числа в диапазоне от 0 до 127.
Допустим, коэффициент масштабирования у нас при выводе текстуры
равен 3, а текущее смещение в текстуре равно 126. Таким образом, следующее
смещение будет равно 1 (если к 126 прибавить 3, то получим 129. При
переполнении 7-битного числа получается 1).
Вы, конечно, можете возразить: зачем сдвигать, если можно
использовать операцию маскирования с константой (для фиксированного числа
- со значением 0x7ffiff; для целого числа - со значением 0x7f). А теперь подумаем
о скорости. Данная функция неслучайно написана на ассемблере. Она очень
критична по времени исполнения, и нам совершенно ии к чему использовать
дополнительную операцию маскирования внутри цикла вывода. Если же число
сдвинуть влево на 9, то маскирование осуществляется автоматически (при
переполнении в операции сложения). Хочу напомнить вам, что операции сдвига
в 386 процессорах выполняется за 1 такт независимо от количества сдвигаемых
разрядов. В функции AsmDrawWallHigh есть еще один метод.ускорения
вывода: это вывод в одном цикле двух байт. Дело в том, что любые ветвления
кода программ сильно тормозят работу из-за необходимости перезагрузки
очереди команд (положительный шаг по оптимизации работы с очередью
команд был впервые предпринят в процессоре INTEL Pentium).
Как вы понимаете, функции Asm DravvWallHigh и AsmDrawWallLow
используются не только для вывода текстур “неба”, но и для изображения
текстур для “стен”. Таким образом, стены рисуются также столбец за столбцом
(вывод по вертикали).
Функции ProjectCurColumn_forPlane.
А теперь рассмотрим, как осуществляется вывод “плоских” текстур для
полов/потолков, имеющих фиксированный размер 64x64. В отличие от стенных
текстур, “плоские” текстуры выводятся не по вертикали, а по горизонтали, т.е.
строка за строкой. Однако, как мы видели выше (при рассмотрении функции
DrawPlanes), сканирование осуществляется по координате X, т.е. создается
впечатление, что текстуры выводятся столбец за столбцом при увеличении
координаты X. Дело в том, что не каждый вызов функции
ProjectCurColumnforPlane сопровождается выводом на экран. Для вывода
линии здесь требуются левая и правая границы (стены) линии. Поэтому' в
промежутке от стены до стены просто заполняется таблица TabPosX Plane,
которая представляет собой таблицу' начальных значений X, необходимых
для вывода прямой, в зависимости от текущей координаты Y.
Исходный текст функции ProjectCurColumn forPlane:
int TabPosX_Plane[200];
void ProjectCurColumnforPlane (int x,int minyl,
int maxy 1 ,int miny2,
int maxy2)
{
II правая граница для линии плана (тупой угол линии ограничения)
while ( minyl < miny2 )
{
if( minyl > rnaxyl )
break;
// вывести линию плана
MapPlane(minyl, TabPosX_Plane[minyl], x-1),
minyl++; //инкремент текущей позиции Y
}
// правая граница для
// линии плана (острый угол линии ограничения)
while (maxyl > maxy2)
{
if ( maxyl < minyl )
break;
MapPlane(maxyl, TabPosX_Plane[maxy 1 ], x-1);
maxyl-; //декремент текущей позиции Y
}
// левая граница доя
// линии плана (острый угол линии ограничения)
while ( miny2 < minyl)
{
if ( miny2 > maxy2 )
break;
TabPosX_Plane[miny2] = x;
miny2++; //инкремент текущей позиции Y
}
И левая граница для
И линии плана (тупой угол линии ограничения)
while (maxy2 > maxyl )
{
if ( таху2 < miny2 )
break;
TabPosX_Plane[maxy2] = x;
maxy2-; И декремент текущей позиции Y
Рассмотрим следующий рисунок:
Этот рисунок представляет собой
план, который сужается по мере
удаления от смотрящего, в
соответствии с законом перспективы.
Минимальная координата плана, с
которой начинается сканирование - х 1,
максимальная - х4. При вызове
функции ProjectCurColumnforPlane в
диапазоне координат от xl до хЗ
производится заполнение таблицы
TabPosX Plane. Начиная с позиции хЗ
осуществляется вызов функции
MapPlane, которая рисует
соответствующую линию плана.
Исходный текст функции MapPlane я приводить здесь не буду.
Функция DrawSprites.
Функция DrawSprites осуществляет вывод изображения спрайтов и оружия
на экран. В процессе ранжирования и прорисовки объектов участвует таблица
TableSprite, сформированная ранее в функции ProjectSpritesInSector. (смотрите
описание функции ProcessTree).
Немного сокращенный исходный текст функции DrawSprites.
struct drawjsprit TableSprite[128];
struct draw_sprit *Offs_TableSprite;
struct draw sprit RangeSpr;
void DrawSprites (void)
register struct draw_sprit *sp;
RangeSpntesQ;
if ( Offs_TableSprite > &TableSprite )
// таблица спрайтов для прорисовки
// указатель на таблицу спрайтов
// начало цепочки спрайтов
// ранжирование спрайтов и объектов в
// зависимости от удаления от глаза
sp = RangeSpr.next; // взять первый спрайт (самый дальний)
while ( sp != &RangeSpr )
{
DrawCurSprite(Sp); // прорисовка спрайта
sp = sp->next; // взять следующий спрайт
}
}
DrawWeapon(); // прорисовка оружия
Функция RangeSprites “выстраивает” весь список спрайтов в цепочку,
первым звеном которой является самый дальний спрайт от плоскости
просмотра (от глаза игрока), а последнее звено - самый ближний спрайт. Таким
образом, спрайты прорисовываются от дальнего к ближнем}' с тем, чтобы
объект на переднем плане перекрывал объект на заднем плане.
Функция DrawCurSprite прорисовывает текущий спрайт.
Функция DrawWeapon.
Эта функция прорисовывает оружие в “руках” игрока.
Исходный текст функции DrawWeapon:
// таблица конечных координат Y в зависимости от X
// для спрайтов по умолчанию
short EndPosY_ofX[320];
// таблица начальных координат Y в -зависимости от X
// для спрайтов по умолчанию
short StartPosY_ofX[320];
// указатель на таблицу начальных координат Y в
// зависимости от X для спрайтов
short *AdrStartPosY_ofX;
// указатель на таблицу конечных координат Y в
// зависимости от X для спрайтов
short *AdrEndPosY_ofX;
void DrawWeapon (void)
{
register int 1;
register struct weapon *am;
// освещенность для объекта
l=PlayerInfo->PlayerThing->ssect->SectSeg->light;
// учет фазы стрельбы из оружия
1 = (1» 4) + ShootingStat_ofPlayer;
if(l<0)
// минимальная освещенность
MapColorsSprites = &MapColor[0][0];
else
if( 1>= 16 )
// максимальная освещенность
MapColorsSprites = &MapColor[15][0];
else
MapColorsSprites = &MapColor[l][0];
// таблица максимальных граничных значений Y в
// зависимости от X
AdrEndPosYofX = &EndPosY_ofX;
// таблица минимальных граничных значецрй Y в
// зависимости от X
AdrStartPosY_ofX = &StartPosY_ofX;
И указатель на структуры weapon
am = &Playerlnfo->WeaponData[0];
for (1=0; 1<2; 1++) ?
{
if (am->CurSprite)
{
ProjectAmmoSprite(ain);
am++;
}
}
}
Несколько слов о таблицах AdrEndPosY ofX и AdrStartPosYofX. Они
представляют собой соответственно максимальные и минимальные граничные
физические координаты Y в зависимости от значений X. Представьте ситуацию,
что перед вами высокая платформа, на которой почти к краю подошел монстр.
В данном случае вы смотрите на него снизу вверх, и нижняя часть монстра
частично перекрывается краем платформы. Метод прорисовки всех объектов
- тот же, что у стен - вертикальный, т. е. они выводятся столбец за столбцом. При
прорисовке очередного столбца используются данные вышеназванных таблиц,
как граничных значений координат Y в зависимости от текущей координаты X
на экране.
При выводе спрайтов оружия считается, что ограничений снизу нет.
Поэтому' таблица StartPosY ofX заполнена значениями -1. Значения в таблице
EndPosYofX зависят от размера объема просмотра и при полноэкранном
просмотре соответствуют 200.
Функция ProjectAmmoSprite выводит на экран изображение
соответствующего спрайта оружия. Исходный текст данной функции мы здесь
приводить не будем.
Глава 10
Создание собственной графики.
10.1. Введение.
Не будет преувеличением, если я скажу, что более 80% успеха любой
игры зависит от качества графики. Даже игры с не очень удачным сценарием,
но с отличной динамичной графикой и анимацией довольно длительное время
имеют достаточно высокий рейтинг. Качество графики и видео некоторых
современных игр уже приближается к телевизионному.
Большинство игр достаточно длительное время писались и пишутся с
использованием VGA режима 320*200*256 (номер режима: 13h). Этот
видеорежим является самым быстрым для платформы IBM и, кроме того, имеет
одно очень существенное преимущество: объем оперативной памяти для экрана
в режиме 13h равен 64000 байт и не превышает размера сегмента, что очень
удобно при обновлении экрана, особенно если вы используете реальный режим
процессора. Однако, в настоящее время режим 13h полностью исчерпал себя.
Если первые игры с использованием VGA режима порой вызывали у нас
невольное восклицание “ого”, то сейчас добиться от нас этого восклицания
старыми приемами и трюками не получится. Именно поэтому сегодня все
чаще разработчики компьютерных игр используют SVGA (или VESA) режимы
(чаще всего нстречакчсх режимы 640x400 или 640x480, реже 800x600). Однако, в
большинстве случаев, игры с SVGA разрешением не отличаются динамичной
графикой или видео (исключением является Rebel Assault 2), очень
требовательны к ресурсам компьютера (процессор - Pentium, память - 8М,
быстрый CD-ROM, и др.). Большинство пользователей не могут себе позволить
роскошь в лице процессора Pentium. Поэтому, на более слабых машинах,
приходится довольствоваться задумчивыми движениями какого-либо
летательного объекта или отказаться от замечательного SVGA режима в пользу
более цривычного VGA режима 13h. На мой взгляд, массовый переход
пользователей на платформу' Intel Pentium, резкое удешевление памяти и
внедрение технологии MPEG Video знаменуют собой новый виток в индустрии
компьютерных развлечений. Так же, как когда-то стандартом де-факто стал
режим 13h, сейчас может укрепиться, в качестве стандарта, один из SVGA
режимов (640x400 или 640x480). Что ж... Время покажет.
В большинстве западных фирм созданием графики для игр занимаются
компьютерные художники, мультипликаторы, аниматоры. В штате
разработчиков игры художников - большинство, т.к. большая часть работы из
всего объема приходится именно на создание и обработку графики и анимации.
Но что делать небольшим российским фирмам, а тем более программистам-
одиночкам, которые решили испытать себя на новом поприще (написать
красивую игру)? Хорошего художника найти не так просто. А если вы даже
найдете такого человека, то ему необходимо платить (и желательно твердой
валютой). Ведь компьютерный художник - профессия редкая, и поэтому
высокооплачиваемая. Так что, если вы не располагаете тугим кошельком, то
для вас... все же есть выход. Графику можно создать собственными силами, не
обращаясь за помощью к художникам (заодно и деньги сэкономите). Для этого
вам нужна лишь бытовая видеокамера в системе VHS или лучше S VHS и Video
Blaster RT-ЗОО от Creative Labs (к моменту подготовки данного материала Video
Blaster RT-ЗОО стоил около 400$).
10.2. Основные этапы создания
графики.
Video Blaster RT-ЗОО позволяет сохранять отдельные кадры разрешением до
640x480 (65536 цветов) или “живое” видео с частотой до 30 кадров/с разрешением
320x240 алгоритмами сжатия Intel Indeo(TM) Video R3.2 и Intel Indeo(TM) Video
Raw. Для создания графики я советую вам использовать алгоритм Intel Indeo(TM)
Video Raw, т.к. этот формат не сжатый и, следовательно, более качественно
позволяет захватывать кадры (алгоритм Intel Indeo(TM) Video R3.2 хоть и хорошо
сжимает, но делает это с потерей качества по цветам, что не всегда приемлемо).
Итак...
Первый пункт - съемка любительской камерой объектов или текстур,
которые необходимо использовать в вашей игре;
Второй пункт - это захват видео-кадров (Capture).
Для этого используется программа “Adobe Capture” (входит в стандартный
комплект поставки Video Blaster’a). Если вам необходим в данный момент только
один кадр, то можно захватить кадр в режиме “Single Frame”. Этот режим
позволяет записать единственный выбранный кадр в формате BMP. Если же вы
хотите захватить с данной видеокассеты (или камеры) несколько кадров, то
воспользуйтесь режимом “Step Capture”, который захватывает кадр при нажатии
кнопки “Add a Frame” в диалоговом окне “Step Capture”. При нажатии ESC все
кадры находятся в файле adobecap.avi. Данные кадры можно записать в BMP
формате на винчестер, воспользовавшись программой “Adobe Premier”. В
ней вы, в данном случае, можете воспользоваться функцией “Miniatures...”
(вызывается из меню “File”, далее - “Tools” и затем “Miniatures...”). Данная
функция записывает последовательные кадры из файлов AVI в BMP файлы.
Причем делает все автоматически.
Третий пункт - обработка изображений.
Это, пожалуй, самый длительный и неинтересный этап в процессе создания
графики. На данном этапе осуществляется чистка каждого кадра, при желании
- изменение цветовых и яркостных тонов и привязка к основной палитре вашей
игры. Все эти действия удобней всего проводить в программе “Picture Publisher
v5.0” фирмы Micrografx. Для того, чтобы проще было отделить мусор от
полезного объекта, советую при съемке на камеру объекты снимать на белом
фоне (например на фоне простыни). В этом случае проще будет воспользоваться
мощной системой выделения программы “Picture Publisher” - “Wizard Mask”.
После того как объект выделен, вы инвертируете маску и заливаете черным
цветом, после чего выделение опять инвертируется и выделенный объект
копируется в новое изображение (“Copy to New Image”). Приведение кпалигре
необходимо по той причине, что созданное вами изображение имеет формат
True Color (16 млн. цветов), а вам необходим 256-цветный режим. Кроме того,
палитра всех объектов, используемых в игре, должна совпадать (для правильной
цветопередачи). Дтя привязки к определенной палитре вы как минимум, должны
располагать этой палитрой. Выбор оптимальной палитры - процесс в высшей
степени творческий. Это целиком зависит от ваших потребностей и вкуса. Хотя
поначалу может показаться, что 256 цветов - чересчур много и вам их хватит с
головой. Это не так. Не забывайте о всевозможных затенениях (например,
перспективное затенение). Поэтому в палитре наверняка будут находиться
одинаковые цвета с различной интенсивностью. Когда палитра наконец создана,
она сохраняется на диске под любым именем (например, “game.pal”). Далее
все объекты, используемые в игре, должны приводиться к данной палитре (меню
“Image”, далее “Convert to”, затем “Palette Color... ”, опции: Dither - None; Palette
- Custom; Palette Name - {имя файла палитры, к которому приводить})
Итак, для создания графики, вам понадобятся следующие аппаратные
средства:
- бытовая видеокамера и/или видеомагнитофон;
- Video-Blaster RT-ЗОО от Creative Labs или аналогичный;
- винчестер с большим свободным пространством и малым временем
доступа.
Из программных средств вам пригодится следующий минимум:
Adobe Capture - программа для захвата видео (входит в стандартный
комплект поставки Video Blaster RT-300);
Adobe Premier от фирмы Adobe Systems Incorporated - программа для
редактирования и конвертирования видео;
Picture Publisher v5.0 от фирмы Micrografx - программа для
редактирования BitMap изображений.
На рисунке показан пример создания спрайта одного из монстров.
Изображение спрайта еще “неочищенное”.
Ниже приведены различные фазы монстра “Барон Ада” (из DOOM’a) во
всех ракурсах.
Фаза “движение”
Фаза “попадание и смерть”.
Другие монстры из DOOM’a имеют раздельные фазы “попадание” и
“смерть”
10.2. Создание видео.
Современные компьютерные CD-ROM игры все чаще используют
видеотехнологии. Это уже давно стало привычным и никого уже не шокирует.
Как эффектно смотрится игра, если, например, ее начало (Introduction)
сопровождается качественным видеоклипом вместо статических картинок.
Также “живое” видео можно использовать между уровнями игры. В игру тогда
играть интереснее. В большинстве случаев видеоролики используются на CD-
ROM версиях игр из-за значительного объема видеофайлов. При создании видео
учитываются два критерия, чаще взаимоисключающих, хотя игра Rebel Assault
2 доказала обратное:
Первое - это степень сжатия видеофайла;
Второе - качество видео (зернистость, использование палитры).
Для создания видео в домашних условиях я нашел наиболее простой путь.
Вам понадобятся опять (как и в предыдущем разделе) видеокамера (или
видеомагнитофон) и Video,- Blaster RT-ЗОО. За основной Кодек для сжатия
видеофайла я взял слегка модифицированный алгоритм Cinepak Codec. Видео
создается в четыре этапа:
- на первом этапе вы вводите видеоклип в компьютер с помощью Video
Blaster’a (для лучшего качества желательно использовать распакованный
формат RAW);
- на втором этапе вы редактируете видеоролик (вырезаете ненужные
кадры, вставляете титры, используете спецэффекты) с использованием той же
программы Adobe Premier или Ulead Video Studio от фирмы Ulead Systems, Inc.
- на третьем этапе - записываете получившийся видеофайл с
использованием алгоритма упаковки Cinepak Codec. Этот метод хоть и долго
упаковывает, но дает наиболее высокую степень сжатия по сравнению с другими
алгоритмами сжатия (MPEG не в счет) - до 10% от исходного.
- на четвертом этапе вы, с помощью моей утилиты Cine2vid.exe
(ОО8’овская утилита), конвертируете получившийся видеофайл AVI в конечный
файл VID, который и будете проигрывать в своей игре. Утилита Cine2Vid.exe
создает оптимальную 256-цветовую палитру и оптимизирует видеофайл для
работы с 256 цветами, что дает немалый выигрыш в размере конечного файла
(дополнительное сжатие). При желании эта утилита может осуществлять
дополнительное сжатие упаковкой быстрым LZ алгоритмом каждого кадра
(функция проигрывания файлов VID может воспроизводить файлы как без
двойной упаковки, так и с двойной упаковкой - Cinepak Codec+LZ). Звук остается
в распакованном формате.
Глава 11
Поговорим о “СТЕРЕО”
11.1 Введение.
Мультимедиа, Мультимедиа... Да, превосходный звук, отличная графика,
“живое” видео. Но сегодня и этого некоторым уже мало! Хочется абсолютной
реальности - по-настоящему объемного изображения. Псевдотрехмерности
игры DOOM не хватает “живого” объема! Некоторые пытаются использовать
для ощущения объема трехмерные очки фирмы Valiant Vision, которые обычно
применяются для рассматривания соответствующих комиксов. Их действие
основано на эффекте призмы - создается впечатление, что объекты красного
цвета выступают на передний план, в то время как синие остаются сзади. Правда,
остальные цвета при этом несколько искажаются.
Объемное черно-белое изображение можно наблюдать также через зелено-
красные очки (на каждый глаз свой цвет), если при этом на экране будут
одновременно выводиться два изображения - красное и зеленое для
соответствующих глаз. Но потеря цветов ради объема - не лучший вариант.
Качественное цветное объемное изображение дает метод, использующий
свойства поляризованного света. Очки в этом случае обладают способностью
пропускать изображение только для того глаза, на который оно рассчитано
(сдвиг 90 градусов). Это более дорогой путь.
Недавно появился очень простой метод получения объемного изображения
на компьютерах.
На Западе сейчас бурно развивается направление, называемое
“виртуальная реальность” (“Virtual Reality”) .которое предусматривает
использование средств, позволяющих создавать объемное изображение и
объемный звук. Это касается не только сверхмощных рабочих станций, ио и,
прежде всего, обычных IBM-совместимых ПК.
Идея метода чрезвычайно проста. Известно, что человеческий глаз перестает
замечать смену кадров изображения, если их менять с частотой 25-30 кадров в
секунду. В то же время у большинства компьютерных мониторов кадры
меняются с частотой 70Гц и выше. Этот запас сделан для устранения эффекта
мерцания. Мерцание утомляет глаза прн длительной работе с текстами. В играх
же заметить мерцание было бы возможно только при частоте кадров около 50
Гц. Для получения объемного полноцветного изображения на обычном
компьютере достаточно поочередно показывать две картинки: для левого глаза
и для правого. Специальные очки на жидких кристаллах также поочередно,
синхронно с переключением картинок на экране, будут закрывать обзор то
правому глазу, то левому. Частота смены кадров для каждого глаза составляет
35 кадров в секунду, и мерцание очень слабое (полное отсутствие мерцания
будет на мониторах с частотой кадров 100Гц и выше: 50 Гц доя каждого глаза).
Ощущение пространства и объема - абсолютное! Все цвета нормальные.
Очки с жидкокристаллическими затворами были разработаны специально
для этих целей. Они подключаются к последовательному или параллельному
порту компьютера. На такие очки уже появились стандарты, а стоимость их
незначительна. Если вы в таких очках посмотрите по сторонам, впечатление -
как будто на вас обычные солнцезащитные очки.
Программа, способная показывать объемное изображение, создает два
разных изображения на двух видеостраницах и затем переключает эти страницы
во время каждого обратного хода луча, одновременно переключая затворы
ЖКИ-очков.
Это простое и качественное решение идеально подходит именно для
компьютера: частота монитора 70 Гц и выше, а не 50 Гц (25 кадр/с), как в TV,
близость экрана монитора к пользователю (что усиливает эффект объемности),
наличие готового выхода для подключения очков, реализуется довольно
несложно программными методами. Уже появились первые подобные
программы, и в обозримом будущем их ассортимент должен весьма
расшириться.
Недостаток такой системы в том, что она показывает перспективный вид
сцены, но не позволяет зрителю ощутить себя внутри этого пространства.
К констру ктивным недостаткам можно отнести инерционность очков (из-
за ЖК), что не позволяет использовать эту технологию на частотах более 100 Гц.
А сейчас рассмотрим технические принципы реализации подобной
системы.
11.2 . Итак, за дело.
Прежде всего, необходимо определиться, что нам может понадобиться из
Hardware для реализации объемного изображения. Прежде всего мы должны
располагать одним свободным СОМ портом, к которому подключить очки.
Пусть это будет, например COM2 (базовый адрес ввода/вывода Ox2f8). К этому
же порту мы подключим источник прерываний с частотой 70 Гц, синхронный
с видеоадаптером. Откуда же взять эти прерывания? Некоторые видеоадаптеры
имеют программный механизм генерации прерываний по обратному' ходу
кадровой развертки. Но после моих неудачных экспериментов я убедился, что
такой механизм имеет лишь малая часть быстрых акселераторов. Поэтому вам
придется взяться за паяльник и подпаять проводок к выводу кадровой
синхронизации видеоадаптера или разъема монитора. К обратной стороне
данного проводка подпаять штырек, что бы его без труда можно было вставить
в одно из гнезд СОМ порта. Этот проводок подключается к выводу RTS
(Запрос на пересылку) СОМ порта. Очки подключаются к выводу DTR
(сигнал готовности ввода данных), ну и, конечно, GND.
Ну вот, с проводами вроде бы разобрались. Теперь несколько слов об
объеме памяти видеоадаптера. Если ваша необъемная игра использует только
одну' видеостраницу (в чем я сомневаюсь), то для объемного изображения
вам потребуются две видеостраницы. Для этого подойдет абсолютно любой
видеоадаптер. Если же вы пользуетесь двумя страницами, то в стерео вам
понадобятся четыре видеостраницы. При этом на видеоадаптере с памятью 1М
можно пользоваться любыми разрешениями вплоть до 640x400x256 (размер
одной страницы: 640*400=256000 байт, четырех: 256000*4=1024000 байт). Для
разрешения 640x480x256 вам понадобится уже 2М памяти.
Теперь я позволю себе объяснить методы создания объемного изображения
из “плоского”. Принцип до смешного прост. Представим себе, что нам
необходимо вывести объект посредине экрана. При этом, если для правого
глаза объект формируется правее на N точек, а д ля левого глаза левее на N
точек, то через очки этот объект кажется выдвинутым за экран (ближе). Если же
наоборот, для правого глаза объект формируется правее на -N точек (левее), а
для левого глаза левее на -N точек (правее), то через очки этот объект кажется
задвинутым вглубь экрана (дальше). В том случае, если объект в обеих страницах
формируется в центре без сдвига, то объект будет выглядеть на уровне с экраном.
Нетрудно догадаться, что чем Дольше N, тем больше глубина. N можно
вычислять из коэффициента масштабирования. Например, если объект
иьгацдигся без масштабирования (в натуральную величину), то N=0. Если объект
при масштабировании уменьшается, то N - ве личина отрицательная, если объект
увеличивается, то N - положительно.
Теперь определимся, какие видеостраницы использовать для какого глаза.
Предположим, что страницы 0 н 2 используются для левого глаза, а 1 и 3 - для
правого.
Приведем ряд исходников, которые вам помогут разобраться в принципах
реализации объемного изображения.
Функция инициализации
Эта функция настраивает порты и обработчик прерываний.
Исходный текст функции InitStereo:
Adeline PORTINT 0x318
Adefine PORT_EYE PORTINT+4
void InitStereo (void)
{
char ms;
disable();
if ( PORTINT == 0x3f8 )
ms = Oxef;
else
ms = 0xf7;
mask= bin(0x21);
bout(0x21 ,mask & ms);
MM1 = bin(PORTINT+3);
bout(PORTINT<3,MMl | 3);
ММ2 = bin(PORTINT+l);
bout(PORTINT+l,MM2 | 8);
bin(PORTTNT+6);
if ( PORTINT == Ox3f8 )
interruptCOM 1 _=0xc;
else
iptermptCOM 1 _=0xb;
_enable();
// запретить прерывания
// маска для кошроллера
//прерывании
//8 бит данных
//генерация прерываний
// по любому изменению
И номер вектора прерываний
// разрешить прерывания
// старый вектор прерывания
oldComl vect=_dos_getvect(interruptCOMl_);
// новый обработчик прерывания
_dos_setvect( interruptCOM 1 InterCom 1);
StereoAvailable = 1; Стерео доступно
}
Обработчик прерывания
Исходный текст функции interruptCOMl _:
void___interrupt_far InterCom 1()
{
bin(PORTINT+6);
if (StereoOn && StereoMode)
{
StereoPage л= 1;
bout(PORTEYE,StereoPage 12);
SetDisplStart(BaseStereoPage | StereoPage);
}
else
SetDisplStart( BaseStereoPage);
bout(0x20,0x20);
Здесь BaseStereoPage - это базовый номер видеостраницы, которая в данный
момент воспроизводится на экране (в нашем случае 0 или 2).
Функция деструктора.
Эта функция вызывается при выходе из программы. Она восстанавливает
все порты и вектор прерывания.
Исходный текст функции CloseStereo:
void CloseSterco(void)
{
StereoMode = 0;
if( StereoAvailable)
{
_dos_setvect(intemiptCOM I _,oldCom 1 vect),
bout(PORTINT+3,MM 1);
bout(PORTINT+1 ,MM2);
bout(0x21 ,mask),
bout(PORTEYE,bin(PORTEYE & Oxfc));
}
}
Приложение
Как сделать, чтобы ваши программы
работали быстрее
Здесь мы дадим вам несколько советов, которые помогут вам на первом
этапе писать “быстрые” и эффективные программы на ассемблере и на “С/
СРР”. Некоторые из положений мы уже рассматривали, но я позволю себе
повториться.
1. Использование механизма предварительных вычислений.
По возможности, не занимайтесь сложными вычислениями внутри циклов
и внутри функций, вызов которых осуществляется очень часто. Следуйте
принципу: никогда не откладывай на потом то, что можно сделать сейчас. Если
внутри программы используются однотипные вычисления с одними и теми же
параметрами, то эти вычисления лучше выполнить заранее, присвоив
результаты каким-либо глобальным (или локальным) переменным.
2. Использование табличных данных.
По возможности, результаты всех сложных длительных вычислений
лучше свести в таблицы. Например, если вы пользуетесь
тригонометрическими функциями (синусами или косинусами), то лучше
всего, на начальном этапе, результаты этих функций свести в таблицу,
назвав ее, например, sine или cosine. Результаты лучше хранить в формате
с фиксированной точкой. Не рекомендую использовать формат с плавающей
точкой, т.к это сильно затормозит обработку. Размер таблицы, в нашем
случае, зависит от того, насколько плавно, например, необходимо вам
поворачивать какой-либо объект, иными словами, от дискретности значений
углов. Например, если дискретность углов у вас 1/2 градуса, то таблица
будет состоять из 720 значений типа Fixed (360 * 2). Этот размер можно
определить, как MAXDEGREES: #define MAXDEGREES 720.
В этом случае, если необходимо определить синус угла Ang, то это можно
записать как
sine[Angle(Ang)];
где Angle можно определить как:
«define Angle(a) ((а) * MAXDEGREES/360)
Инициализацию таблиц синусов и косинусов можно представить так:
void InitSinCosTables(void)
int i;
for(i=0; i<MAXDEGREES; i++)
{
cosine[i]=Float2Fixed(cos((float)i*360/
MAXDEGREES *M_PI/180.0));
sine[ i]=Float2F ixed( sin((float)i *360/
MAXDEGREES*M_PI/180.0));
}
}
В этой функции быстродействие не важно, т.к. она вызывается только
один раз при старте программы. Следовательно, никто не мешает вам
использовать здесь “плавающую” арифметику.
3. Желательно, чтобы количество передаваемых
параметров при вызове функции не превышало 4 (3).
При работе на WATCOM C/C++, с использованием регистровой передачи
параметров (ключи компилятора: /Зг - для 386 процессора; /4г - для 486
процессора; /5г - для PENTIUM’a), первые четыре параметра в функцию
передаются в регистрах процессора, а остальные через стек. При этом
в регистре еах передается первый параметр;
в edx - второй;
в ebx - третий;
в есх - четвертый.
Как вы, знаете, регистровая передача параметров работает намного
быстрее. Кроме того, если вызываемая функция написана на ассемблере, то
использование в ней параметров, передача которых осуществлялась через стек,
связано с определенными трудностями, заключающимися в локализации этих
переменных внутри стека (нахождение смещения внутри стека с учетом того,
что:
а) 4 байта вычитается из стека при вызове функции near;
b) сохранение регистров в стеке. На каждый 32-разрядный регистр из
стека так же вычитается 4 байта).
Если вы работаете на СРР и используете классы, то желательно, чтобы
количество параметров передаваемых в функцию не превышало 3, т.к. первым
параметром в функцию (в регистре ЕАХ) передается указатель на класс.
4. При работе на ассемблере используйте неявные
операции умножения и деления на константу.
При работе с целочисленной арифметикой одними из самых медленных
операций являются умножение и деление. Однако, если вы проводите эту
операцию над константой, то в ряде случаев этот процесс можно значительно
ускорить, если использовать команды неявного умножения и деления.
Покажем, как это делается при умножении на простых примерах, где
источником и приемником является регистр ЕАХ, но это можно проделать с
любым регистром общего назначения, и при этом регистр-источник
необязательно может совпадать с регистром-приемником, что в ряде случаев
полезно, т.к. позволяет сохранить исходный регистр. Итак:
Умножение на 4: lea еах,[еах*4] или shl еах,2
Умножение на 5: lea еах,[еах+еах*4]
Умножение на 6: lea еах,(еах*2+еах*4]
Умножение на 7: lea edx,[eax+eax*2]
lea eax,[edx+eax*4]
Умножение на 8: lea еах,[еах*8] или shl еах,3
н так далее.
С неявным делением дела обстоят не так благополучно, как с умножением.
Делителем при неявном делении может быть число, в двоичном эквиваленте
которого только одна единица: 2,4,8,16 и т.д.:
Деление на 2: shr еах, 1
Деление на 4: shr еах,2
Деление на 8: shr еах,3
и так далее.
5. Старайтесь не пользоваться универсальными функциями
и прерываниями, а применяйте свои собственные, узко
специализированные быстродействующие функции.
Надеюсь, вам не надо объяснять, почему' в играх при выводе точки на
экран не следует использовать 10-прерывание. По возможности, старайтесь
не использовать прерывания 16Н (клавиатуры)и ЮН (видео - это прерывание
используйте только для инициализации режима). Также советую некоторые
часто используемые стандартные библиотечные функции переписать иа
ассемблер и использовать их как макросы. Мы уже показывали, как это
делается, на примере макроса move, который использовали вместо функции
memcpy.
6. Оптимизируйте работу с циклами.
Как правило, большую часть времени программа крутится в каких-либо
циклах. Поэтому’ очень актуально ускорить время выполнения одной итерации.
При этом если, например, время одной итерации вы уменьшили на 50 тактов
работы процессора, а всего итераций -10000, то в целом получится выигрыш:
10000*50 = 500000 тактов работы процессора. Ускорение работы цикла можно
получить таким образом: если количество итераций - константа, например -
10000, то внутри тела цикла можно последовательно написать, допустим 5
итераций, а цикл сделать по 2000 (10000/5). Это будет работать быстрее,
т.к. ветвления в семействе процессоров 80x86 вызывают перезагрузку
очереди команд, что дает некоторое замедление.
При работе с циклами старайтесь пользоваться только регистровыми
переменными и использовать механизм предварительных вычислений.
7. Оптимальная работа с видеобуфером.
Как известно, наибольшее замедление практически во всех играх получается
при выводе изображения на экран. Это неслучайно. Дело в том, что работа с
видеопамятью в два и более раз медленней, чем с обычной оперативной
памятью. Поэтому в своих играх вывод информации на экран надо сделать
настолько оптимальным, чтобы не происходил несколько раз подрад вывод в
одно и то же место на экране. Обычно изображение формируют в буфере
экрана (виртуальный экран или теневой экран), коим является участок
оперативной памяти размером с экран (для разрешения 320x200x256 размер
буфера равен 64000 байт). После этого измененные участки или целиком буфер
копируют на экран. В DOOM’e использован другой подход, и он даже быстрее
этого. В нем изображение формируется в невидимой части видеопамяти, но
таким образом, что ни одна точка на экране не прорисовывается дважды. После
этого производится переключение страницы. В некоторых играх, где
производится постоянное обновление экрана, использован интересный подход,
который с первого взгляда кажется неэффективным, ио на практике, в
некоторых случаях, дает ощутимый выигрыш в скорости. Этот подход основан
на том предположении, что каждый последующий кадр изображения
незначительно отличается от предыдущего. На практике обычно так оно н
есть. Для этого в оперативной памяти выделяются два буфера размером с
экран. С каждым кадром изображение попеременно формируется то в первом,
то во втором буфере (просто меняются местами указатели этих буферов). Для
вывода на экран эти два буфера сравниваются между собой и выводятся только
несовпадающие участки. Казалось бы, что проще и быстрее воспользоваться
командой “rep movsd” для вывода целиком буфера. Однако, учитывая те факты,
что обращение к видеопамяти осуществляется медленнее, чем к обычной
памяти, и что два соседних кадра чаще всего несильно отличаются друг от
друга, метод сравнения получается быстрее, что особенно заметно в режиме
высокого разрешения (640x400 и выше). Метод сравнения, при выводе на
экран, используется в таких играх как “Nascar Racing”, “Rebel Assault” и
др. А сейчас я приведу' программу на ассемблере, которая сравнивает два
буфера и выводит несовпадающие участки на экран в режиме 640x480. Это
очень эффективная программа, и что-то более оптимальное вам вряд ли
удастся создать, поэтому данный подход вы можете использовать в своих
играх. Правда, вам придется в этом случае пожертвовать памятью более 600
Кбайт (в разрешении 640x480x256 размер экрана равен 307200 байт. Для двух
буферов: 307200*2=614400 байт). Один буфер обычно выделяется в нижней
памяти (AllocLow), а второй - в верхней или DPMI памяти (malloc или zmalloc).
Итак, вот эта программа:
public OutToScreen_
public SetBank_
OutToScreenproc near
pushad
mov esi,_BufScr2
xchgesi, BufScrl
mov _Bi3Scr2,esi
mov edi,BufScrl
xoreax,eax
ots: call SetBank_
mov ecx, 16384
call MoveBank
inceax
cmpeax,4
jne ots
call SetBank_
mov ecx, 11264
call MoveBank
popad
retn
OutToScreen_ endp
SetBank_ proc near
pushad ; Vesalnfo - указатель на
; буфер информации
mov edi, Vesalnfo
and eax, 15
emp al.CurBank
jestbl
mov _CurBank,al
emp word ptr +4[edi],0x40
je stb2
; общая функция обновления экрана
; поменять местами указатели
; буферов экрана
; установить банк памяти
; вывод банка размером 65536 байт на
;экран(16384*4=65536)
, выводить четыре банка (0-3)
; установить последний банк
; Вывод последнего (4-го) банка размером
, 45056 байт на экран (11264*4=45056)
; потный размер получается
£5536*4+45056=307200
; конец процедуры OutToScreen_
; функция установки активного
; банка памяти
; о VESA режиме. Информацию
; можно получить,
; используя VESA функцию 0x4fD 1
; сравнение с текущим
; установленным банком
; если совпали, то устанавливать не надо
shleax,6 mov cx,+4[edi] div cl
stb2: movzx ebx.byte ptr WmNum ; обычно 0 mov edx,eax mov eax,0x4fl)5 ; установить банк int 0x10
stbl: popad retn
SetBank_ endp
MoveBank proc near
push eax
lea eax,-0xa0000|edi]
push eax;
mov ebx,-4
repe cmpsd
jemvbl
lea ebpjebx+esi]
lea eaxjebx+edi]
movedx,ecx
repne cmpsd
; конец процедуры SetBank_
; вывод на экран текущего
; банка памяти
; запомнить в стеке адрес
; для вычисления смещения в видеобуфере
; сравнение буферов до первого
; несовпадающего
; значения
; если все совпало, то выход без обновления
; отсюда начинать вывод на экран
; текущий адрес буфера сравнения
; запомнить счетчик
; сравнение с текущего места до
jne mvb2 ; первого совп. ; значения, и если все не совпало
; до конца далее
mvb3: sub edx,ecx ; в edx - размер несовпадающего
; участка
xchg edx,ecx; поменять местами edx и ecx
mov esi,ebp; адрес источника
mov edi,eax; адрес буфера сравнения
sub edijesp]; адрес видеобуфера
repe rnovsd; вывод на экран
add edijesp]; адрес буфера сравнения
sub esi,ebx; коррекция адреса источника (+4)
sub edi.ebx ; коррекция буфера сравнения (+4)
mov ecx,edx; дальнейшее сравнение до первого
repe cmpsd
jemvbl
lea ebp,[ebx+esi]
lea eaxjebx+edi]
movedx,ecx
repne cmpsd
je mvb3
mvb2: movecx,edx
mov edi,eax
incecx
mov esi,ebp
sub edijesp]
repe rnovsd
; несовпадающего значения
; если все совпало, то выход без
; обновления
; отсюда начинать вывод на
;экран
; текущий адрес буфера сравнения
; запомнить счетчик
; сравнение с текущего места до
, первого совпадающего
; значения, и если все не совпало до конца далее
; размер несовпадающего участка
; адрес буфера сравнения
;адрес источника
; адрес видеобуфера
; вывод на экран
add edijesp] , адрес буфера сравнения
mvbl: pop eax
pop eax
retn
MoveBank endp , конец процедуры MoveBank
Для использования функции OutToScreen вы должны каждый раз
формировать изображение в буфере по указателю BufScrl. Указатель BufScr2
не трогайте вообще, как будто его и нет. Этот пример вы можете использовать
и с другими VESA совместимыми режимами (640x400, 800x600 и др.). При
этом менять почти ничего не нужно. Например, для разрешения 640x400x256
в функции OutToScreen обновляются три (а не четыре) банка размером 65536
байт (в цикле) и последний - четвертый размером 59392 байта (14848*4).
Размеры буферов для этого разрешения будут равны 256000 байт (640*400).
Ничего другого менять не надо. Вот этим и приятно работать с VESA
режимами.
8. При работе на ассемблере используйте
32-разрядные регистры даже там, где это не обязательно. •
Поясню, в чем дело. При работе в 32-битном режиме процессора размер
операнда по умолчанию для процессора равен 32 бита (4 байта). Если вы
используете другой размер (например, вместо EDX используете DX или DL),
то код команды становится на один байт длиннее: перед непосредственно
командой вставляется “префикс размера операнда” (обычно 0x66 или Oxfe). На
быстродействие это сильно не влияет, но на результирующий размер кода может
повлиять.
9. Все критичные по времени функции и циклы после
отладки желательно переписать на ассемблер.
Хоть WATCOM и создает очень оптимальный код, идеальным его не
назовешь. Поэтому' некоторые критичные по времени процедуры лучше писать
на ассемблере. Чаще всего это относится к функциям работы с графикой,
масштабирования и др.
10. Избегайте пользоваться прерываниями реального
режима.
Переключение из защищенного режима в реальный и наоборот -
достаточно длительный процесс. Поэтому в цикле игры старайтесь не
пользоваться прерываниями реального режима (эмуляция прерываний
реального режима осуществляется с помощью DPMI функции 0x300).
/ /. Выключите менеджеры expanded памяти.
При запуске программ, написанных под DOS4GW, желательно
выключать менеджеры expanded памяти (QEMM или EMM386), т.к. они
могут затормозить работу. Настоятельно не рекомендую осуществлять
запуск WATCOM программ при наличии в памяти драйвера DPMI (типа
QDPMI от Quarterdeck).
Специальная информация.
АОН Energy Star и др.
Телефоны с автоматическим определением номера (АОН)
прочно вошли в жизнь многих людей. Они очень удобны в экс-
плуатации: при наборе видно набираемый номер, память
позволяет просмотреть номера звонивших, есть голосовой режим,
записная книжка, будильник и др. Самая главная функция —
определение номера звонящего. Все эти функции стали возмож-
ными потому, что по устройству АОН — это маломощный
компьютер.
Но при всех своих достоинствах у обычных телефонов с АОН
есть один серьезнейший недостаток. Их питание осуществляется
от сети 220 V через блок питания. Последний делают, как
правило, весьма компактным (помещают прямо в штепсельной
коробке). А так как телефон включен постоянно и потребляет
относительно много энергии, блок питания нагревается, что чр-
евато своею пожароопасностью.
Но самое главное, что электричество в сети иногда кра-
тковременно пропадает или его могут отключить на несколько
часов. Информация, хранящаяся в телефоне, при этом пропадает.
После появления напряжения, как правило, телефон самостоя-
тельно не может выйти на рабочий режим. Если, например, это
произошло ночью, то утром не срабатывают будильники и никто
не может к вам дозвониться, пока вы сами не заметите, что
телефон не работает. В этом состоянии телефон выдает на линию
сигналы "занято", что является не лучшим вариантом. Если у вас
отключили электроэнергию днем или вечером на несколько
часов, то вы на все это время останетесь без связи — ни к вам ни
от вас позвонить будет невозможно. Для таких ситуаций
необходимо иметь еще один, запасной, обычный телефон.
А представьте, что вы уезжатете в отпуск или даже просто на
выходные (напимер, на дачу)! АОН в таких случаях желательно
отключать, что ведет к сбою всех установок и памяти номеров.
Все эти изделия (от АОН до микро-АТС) разработаны фирмой
“ТЕЛЕСИСТЕМ”. Приобрести их можно в фирме “МИКРОАРТ”
(адрес см. на 4 стр. обложки), или по почте (порядок высылки см.
далее).
Фирма “МИКРОАРТ”
предлагает
Советы профессионала и все необходимое на
дискетах : DOOM изнутри, система програмирования
3D, драйвера для создания звука и видео-вставок,
файлы с готовыми "монстрами".
Книга и дискеты имеются в продаже в фирме
"МикроАрт", а также высылаются по почте.
А. Хонич
X 35 Как самому создать трехмерную игру. — М.: фирма
МИКРОАРТ, 1996. - 290 с.: ил.
ISBN 5—88579—006—X
Книга предназначена для тех, кто хочет познакомиться с
устройством трехмерной игры типа DOOM и попробовать свои
силы в создании увлекательных игр. В книге шаг за шагом
изложены структура программы, все процедуры и их взаимосвязь,
что будет интересно программистам пишущим объемистые
программы. Также изложены интересные алгоритмы и рассмотрены
тонкие места.
Для программистов, владеющих языком «С», а также для
всех тех, кто хочет повысить свою квалификацию в программи-
ровании.
х 2404020000-006 без объям БКК 32.97
37Г (03)—96
Компьютерная графика: Бекренев В. И.
Макетирование и компьютерная верстка: Швидкая Н. В. и
Бекренев В. И.
Лицензия на издательскую деятельность
ЛР № 063421 от 26.05.94.
Подписано в печать 11.07.96. Формат 60х90Ий.
Гарнитура тайме. Бумага офсетная №1.
Печать офсетная. Тираж 10 000 экз. Зак. 1481.
Фирма «МИКРОАРТ»
123022, г.Москва, а/я 76.
АООТ «Ярославский полиграфкомбииат»
150049, г.Ярославль, ул.Свободы, 97.
Для заметок
Фирма "МИКРОАРТ"
ПРЕДЛАГАЕТ К ПРОДАЖЕ
Q Комплектующие к ПК типа IBM (проверка при продаже).
Сборка на заказ. Модернизация.
Конвертор SVGA-PAL (для подключения компьютера к
цветному телевизору).
Q Печатные платы универсального программатора и
настроенные программаторы на ВСЕ виды ППЗУ, ПЛМ,
микроЭВМ и др., с программным обеспечением.
Q АОН (с питанием только от телефонной линии, 2ККИ
и пр.), КАЛЬКОФОН (АОН-приставка в одном корпусе с
калькулятором SITIZE14 888, не требует дополнительного
источника питания), телефонные микроАТС.
Q Литература по компьютерной технике.
Q АДРЕС: ул.Кольская, д. 1, институт ЦНИИС, 3 эт., ком. 324.
Q ПРОЕЗД: Метро 'Свиблово', выход из первого вагона (от
центра) и налево по подземному переходу, серое 8-и этажное
здание в 100 метрах от перехода.
Q ТЕЛЕФОН: (095) 189-28-01; ФАКС: (095) 180-85-98
Q МЕСТО НА МИТИНСКОМ РАДИОРЫНКЕ: 'П-4-