Объяснение устройства игры Tomb Raider 1

Игровой уровень состоит из комнат.

Каждая комната разбита на секторы размером 1024 на 1024 единиц. Все координаты комнат (стены например) кратны числу 1024. То есть например левая стенка комнаты начинается по X координате кратной 1024 и по Z координате кратной 1024. Это упрощает поиск пересечений (коллизий) Лары, объектов, и геометрии уровня, комнат.

Секторы объединяются в боксы (блоки).

Боксы (блоки) объединяются в зоны.

Один и тот же сектор может принадлежать только одному блоку.

Одна зона состоит из нескольких боксов (блоков).

Зная текущю позицию Лары (или врага) на сцене мы можем определить сектор, по сектору можем определить блок в котором находться Лара (враг).

В коде есть массивы: g_FlyZone, g_GroundZone, g_GroundZone2.


	int16_t *g_GroundZone[2] = {NULL};
	int16_t *g_GroundZone2[2] = {NULL};
	int16_t *g_FlyZone[2] = {NULL};

Например в уровне 64 блока. Значит эти массивы будут размером 64 (индексы от 0 до 63).

У каждого Item в уровне есть своя позиция на сцене x,y,z.

Береться позиция Item, по позиции вычисляется в каком секторе Item (один сектор размером 1024 на 1024).

По сектору определяется блок в котором находиться Item.

Теперь у нас есть блок в котором расположен Item - например 20.

Значение g_GroundZone[0][20] или g_GroundZone[1][20] будет указывать в какой зоне находиться Item.

Эта информация - зона в какой находится Item и зона в какой находиться Лара, используется для того что бы Item (враг) по алогритму мог найти путь к Ларе и ее атаковать (например летучая мышь, волк, медведь и т.п.). В игре используется модификация Волнового алгоритма поиска пути, что бы враги находили путь к Ларе.

Например есть следующий код, по позиции Лары определяет зону в которой Лара находится:


	int32_t g_FlipStatus = 0;

	int16_t *g_GroundZone2[2] = {NULL};

	zone = g_GroundZone2[g_FlipStatus];

	ROOM_INFO r = &g_RoomInfo[g_LaraItem->room_number];

	x_floor = (g_LaraItem->pos.z - r->z) >> WALL_SHIFT;
	y_floor = (g_LaraItem->pos.x - r->x) >> WALL_SHIFT;

	g_LaraItem->box_number = r->floor[x_floor + y_floor * r->x_size].box;
	
	info->enemy_zone = zone[g_LaraItem->box_number];

Здесь info это структура которая отвечает за врага Лары и после структуры info->enemy_zone хранит зону в которой находится Лара. У каждой комнаты есть пол floor который отвечает за каждый сектор комнаты (сектор 1024 на 1024). Так же есть глобальный массив пола (общий для всех комнат):


	int16_t *data = &g_FloorData[floor->index];

Здесь мы получили data текущего сектора. Например data сообщает что в секторе находиться рычаг. И так же Лара в данный момент находится в этом секторе. Лара нажимает рычаг. Код игры видит что в data в том секторе где Лара есть рычаг и он нажат- для кода игры это значит нужно открыть дверь (например). Значение из g_FloorData используется в триггерах - как в этом примере с рычагом и дверью.

Почему есть три массива - g_FlyZone, g_GroundZone, g_GroundZone2. Массив g_FlyZone для определения зоны летающий объектов (например летучая мышь). Массив g_GroundZone для объектов которые могут преодолеть высоту не более 256 единиц. Массив g_GroundZone2 для объектов которые могут преодолеть высоту 256 единиц.

Почему эти массивы из 2х элементов? Нулевой индекс int16_t *zone = g_GroundZone[0]; для стандартного состояния всех комнат, zone = g_GroundZone[1]; для измененной комнаты. Почему бывают стандартные состояния - после загрузки уровня например и второе состояние комнаты может быть после нажатия на рычаг - комната меняеться, в комнате меняеться геометрия. За это отвечает переменная g_FlipStatus, например:


	int32_t g_FlipStatus = 0;
	int16_t *zone;

	//летающие объекты
	if (creature->LOT.fly)
	{
		zone = g_FlyZone[g_FlipStatus];
	}
	//объекты у которых возможность подъем/спуск не более 256
	else if (creature->LOT.step == STEP_L)
	{
		zone = g_GroundZone[g_FlipStatus];
	}
	//объекты у которых возможность подъем/спуск более 256
	else
	{
		zone = g_GroundZone2[g_FlipStatus];
	}

Для летающий объектов так же само как и для наземных объектов определяется сектор, блок, зона - по их координатам x,z в комнате (координата z для определения сектора не используется). Для летающих объектов не используются трехмерные определения блоков, зон- так можно подумать потому что летающие объекты перемещаются в 3d пространстве а не по проской поверхности как например волк, медведь. Для летающих объектов из блоки и зоны определяются так же само как и для наземных - по плоской поверхности блоков, зон в которых координаты x,z этого летающего объекта.

Так же к этой теме относятсья перекрытия массив g_Overlap. Это массив для каждого блока на карте уровня. g_Overlap используется для хранения смежных боксов которые есть у определеного блока.

Например мы знаем координаты Item x,z и можем определить сектор, потом по сектору определяем блок. У блока которому принадлежит сектор есть overlap_index - смещение в массиве g_Overlap.


	int index = box->overlap_index

	int16_t box_number = g_Overlap[index++];

	if (box_number & END_BIT)
	{
		//exit from function
		return;

То есть:


	g_Overlap[] = { 12, 37, 45, 50, 0x8019 }

Означает, что данные бокс смежный с: боксом 12, боксом 37, боксом 45, боксом 50, боксом 25 (последний, т.к. установлен END_BIT).

Константы:


	#define END_BIT     0x8000
	#define BOX_NUMBER  0x7FFF

Таким образом зная координаты Item x,z мы определяем сектор, по сектору определяем блок, и у блока мы берем значение box->overlap_index - это смещение в массиве g_Overlap, по этому смещению мы двигаемся вперед по массиву g_Overlap пока не встретим значение END_BIT то есть 0x8000 - все остальные значения из массива 0x8000 это номера блоков которые смежные с нужным нам блоком - то есть Item определяет что из того блока в котором он находиться он может двигаться через смежные блоки в поисках пути по направлению к Ларе.

У врагов Лары есть разные настроения (состояния). При старте игры всем врагам изначально (Item) присваивается состояние MOOD_BORED - скучающее настроение. Далее идет определение в коде - в какой зоне враг, в какой зоне Лара. Если Item и Лара находятся в одной зоне - враг меняет настроение MOOD_BORED в MOOD_ATTACK и начинает двигаться к Ларе и атаковать ее. Например Лара взобралась на препятствие так что враг не может ее достать - уже Лара в другой зоне. Враг меняет настроение опять на MOOD_BORED, и если Лара находясь вне досягаемости начинает стрелять по врагу из оружия, атаковать- то враг меняет настроение из MOOD_BORED в MOOD_ESCAPE и начинает убегать. Настроение влияет какая анимация в данный момент времени включена у врага.

Почему в игре есть два массива Items и Objects? Массив Objects хранит общее описание каждого объекта в игре (например Лара, летучая мышь, волк, маленькая аптечка, большая аптечка и т.п.) - хранит указатель на геометрию этого объекта, то есть на то что будет нарисовано на экране. Массив Items - что хранит? Например у нас есть в массиве Objects модель волка. Но в комнате на Лару нападает сразу 3 волка - эта модель волка одна- она описана в массиве Objects, но три волка - координаты каждого на сцене, поизция, анимация для каждого и т.п. хранится в массиве Items. Таким образом модель которая рисуется на экране для всех трех волков описана в массиве Objects, но каждый из трех волков имеет свой индекс в массиве Items - потому что волка три, у каждого своя позиция, состояние анимации, настроение и другое - это все хранится в массиве Items. Например в Tomb Raider 3 есть уровень Nevada - в самом начале уровня в пустыне три куста - так вот для всех трех кустов описание модели храниться в массиве Objects, но каждый из кустов имеет свой индекс в массиве Items так как у каждого куста своя позиция x,y,z на сцене. В массиве Objects один куст (его модель) в массиве Items три куста (каждый со своими координатами и углом поворота на сцене).

Как работает анимация (например) Лары в игре. Есть анимация и состояния анимации. После загрузки уровня есть начальное значение стотояния анимации AS_STOP (animation state) и самой анимации. Когда игрок нажимает клавишу "Вперед" в коде определяется переключить на состояние анимации AS_RUN (бег). Как это происходит? Код игры просматривает набор состояний анимации Стоп в которые можно перейти из Стоп - то есть каждая анимация у Лары может иметь набор состояний AS в которые возможен переход из этой анимации. Если Например Лара в состоянии прыжок вперед - AS_FORWARDJUMP (прыжок вперед) а пользователь в это время нажимает клавишу "Назад" - то ничего не произодет, будет дальше анимация Прыжок Вперед и состояние AS_FORWARDJUMP потому что в анимации (например) Стоп (anim state AS_STOP) клавиша "Назад" переключает на анимацию Шаг Назад (состояние AS_BACK) а у анимации Прыжок вперед (сотоянияе AS_FORWARDJUMP) нету в списке возможных переходов анимаций состояния AS_BACK. Таким образом Лара находится в состоянии анимации стоп AS_STOP, у этой анимации есть набор AS состояний в которые возможен переход далее из анимации Стоп (AS_STOP). Пользователь нажимает "Вперед" это означает включить Бег (AS_RUN). Код просматривает список для анимации Стоп (состояние AS_STOP) - есть в этом списке состояние AS_RUN - значит переключает на анимацию Бег, и Лара начинает бег вперед.

Как например озвучивается звук шагов Лары во время бега. Лара в состоянии бега имеет состояние анимации AS_RUN - это состояние входит в анимацию Бега для Лары. Не путайте анимацию и состояние анимации. Анимация длиться к примеру 18 кадров - лара бежит вперед, далее анимация зациклена и Лара бежит только вперед. Считаются кадры - например Лара на 5 кадре анимации. В структуре анимации есть член- смещение в массиве команд анимации g_AnimCommands и количество команд анимации. Например на 15 кадре должен быть слышен звук шагов Лары (левой или правой ноги). В каждом кадре анимации для всех 18 кадов игра смотрит массив g_AnimCommands если значение из массива равно AC_SOUND_FX значит во время этой анимации должно быть проиграно звук, берется следующее значение из массива g_AnimCommands - если оно равно к примеру 15 - и сейчас 15 кадр анимации то воспроизводиться sound effect шаг Лары.

Как переключаются анимации. Например Лара стоит на месте - это одна анимация. Пользователь нажимает клавишу "Вперед" Лара делает рывок вперед - это вторая анимация Рывок. Третья анимация Бег начинается когда закончится анимация Рывок. Из анимации Лара стоит на месте идет переход в анимацию Рывок, а у анимации рывок указано что когда она закончиться- следующая анимация должна быть Бег. Автоматически у анимации Бег указано, что когда закончиться анимация Бег, то следующая тоже анимация Бег - то есть зацикливается анимация. Так происходит переход между анимациями - у каждой анимации в структуре есть член - указатель на следующую анимацию. Член "следующая анимация" для Рывок это Бег, член "следующая анимация" для анимации Бег это Бег - зациклено.

Каждая анимация у Лары уникальна, но состояния анимации одинаковые могут быть для разных анимаций. Например в начале Tomb Raider 1 уровень City of Vilcabamba Лара стоит на краю бассейна. Возможны два варианта погружения в воду- нажать клавишу Вперед - тогда Лара просто спрыгнет с края бассейна в воду. Второй вариант - анимация Гимнаста погружения в воду - Лара на краю бассейна нажать Shit + Up то есть клавишу Shift плюс вперед - и Лара гимнастически нырнет в воду. Это пример двух разных анимаций - но anim state одно и то же AS_DIVE - нырок.

Что важнее анимация или anim state? Важнее анимация. Когда идет переключение - например из AS_STOP в AS_RUN есть текущая анимация Стоп - Лара на месте. Когда пользователь нажимает клавишу Вперед это значит переключить на бег - и код игры ищет в анимации Стоп если ли состояние AS_RUN если есть на него переключает и Лара бежит вперед переключается на анимацию Бег и состояние анимации AS_RUN.

В коде есть система LOT (Level Object Targets или Level Object Traversal или Logical Object Tree или List of Targets).

Что это такое?

LOT — это структура, описывающая навигационную логику врага, включая:

Почему Logical Object Tree?

Core Design использовала терминологию "tree" (дерево), потому что: каждая коробка (box) может иметь дочерние связи — следующую, альтернативную, и т.д., маршрут строится как обход по логической структуре. Таким образом: LOT — это "дерево логических маршрутов", по которому враг строит путь к своей цели.

LOT->target_box - это основная цель, куда враг хочет добраться.

Устанавливается при смене поведения (например, TargetBox(...) в MOOD_ATTACK, MOOD_STALK, MOOD_BORED и т.д.).

Используется для построения пути (pathfinding).

Меняется редко — только когда враг "решил", куда идти.

Навигационная цель — место, куда строится маршрут.

LOT->required_box - это временная цель, которую нужно достичь на текущем этапе маршрута.

Задаётся, если необходимо пройти через определённую коробку.

Может быть очищена, если путь невозможен или уже пройден.

Используется, чтобы ИИ "настаивал" на прохождении конкретного сегмента маршрута.

Вспомогательная промежуточная цель, а не основная.