473 lines
26 KiB
Markdown
473 lines
26 KiB
Markdown
# II. Запуск, архитектура и игровой цикл
|
||
|
||
Этот том описывает путь от запуска `iron_3d.exe` до устойчивого кадра:
|
||
загрузку `iron3d.dll`, создание shell/game objects, поднятие платформенных
|
||
сервисов, запуск World3D, расчёт simulation step, безопасное удаление объектов,
|
||
рендер и завершение программы.
|
||
|
||
Главная особенность Iron3D -- это не один монолитный engine object, а связка
|
||
небольшого Win32 bootstrap и набора DLL, которые обмениваются фабриками,
|
||
singleton-интерфейсами и C++ vtable. Совместимая реализация может изменить
|
||
физическое деление на библиотеки, но не может произвольно менять порядок
|
||
инициализации, object identity, правила владения, fallback ресурсов и порядок
|
||
событий.
|
||
|
||
```text
|
||
iron_3d.exe
|
||
-> iron3d.dll
|
||
-> services.dll
|
||
-> World3D.dll
|
||
-> Terrain.dll
|
||
-> Ngi32.dll
|
||
-> AniMesh.dll / ArealMap.dll / Effect.dll
|
||
-> ai.dll / Behavior.dll / Wizard.dll
|
||
-> Control.dll / MisLoad.dll / Net.dll / Joystick.dll
|
||
```
|
||
|
||
## Карта модулей
|
||
|
||
Во внешней архитектуре обнаружено пятнадцать DLL. Экспортов сравнительно мало:
|
||
они обычно создают объект, возвращают singleton или дают доступ к уже поднятой
|
||
подсистеме. Основная работа выполняется через C++-интерфейсы, поэтому порядок
|
||
виртуальных слотов является частью ABI, особенно для compatibility shim эпохи
|
||
MSVC6.
|
||
|
||
```text
|
||
iron_3d.exe
|
||
|
|
||
v
|
||
iron3d.dll -- композиция игры, shell и главный цикл
|
||
|
|
||
+-- services.dll -- доступ к display, GUI, ресурсам, звуку, таймеру и сети
|
||
+-- World3D.dll -- объекты, очередь, время, камера и кадр
|
||
+-- Terrain.dll -- ландшафт, свет, атмосфера и визуальный слой мира
|
||
+-- ai.dll / Behavior.dll / Wizard.dll
|
||
+-- Control.dll / Effect.dll / MisLoad.dll
|
||
+-- Net.dll / Joystick.dll
|
||
+-- Ngi32.dll -- ресурсы, графика, звук, математика и CPU dispatch
|
||
```
|
||
|
||
Циклы импортов между DLL ожидаемы. Terrain создаёт визуальные объекты и
|
||
обращается к World3D, а World3D получает world-interface из Terrain. Это не
|
||
значит, что обе библиотеки совместно владеют всем состоянием. Реальные границы
|
||
задаются интерфейсами, refcount, очередью объектов и порядком shutdown.
|
||
|
||
Практичная новая структура может быть внутренним набором модулей `platform`,
|
||
`resources`, `world`, `mission`, `terrain`, `render`, `animation`, `effects`,
|
||
`behavior`, `physics`, `audio` и `network`. Важно сохранить не DLL-границы, а
|
||
контракты: имена ресурсов, порядок поиска, fallback-ветки, object ID, момент
|
||
создания mirror objects, численное поведение и последовательность событий.
|
||
|
||
## Роли модулей
|
||
|
||
`iron3d.dll` создаёт shell и game objects, читает `iron_3d.ini`, поднимает
|
||
display, sound, CD-audio, network и настройки World3D, загружает миссионные и
|
||
UI-конфигурации, содержит message pump и вызывает расчёт/рендер игры.
|
||
|
||
`services.dll` работает как service locator. Через него запрашиваются display,
|
||
GUI, network manager, resource manager, sound server и timer. Этот слой отделяет
|
||
высокоуровневую игру от деталей создания устройств.
|
||
|
||
`World3D.dll` -- центральный runtime: очередь объектов, идентификаторы,
|
||
события, отложенное удаление, game time, pause, manual input, камера,
|
||
material/texture/lightmap managers, сетевые mirrors, расчёт и 3D-проход.
|
||
|
||
`Terrain.dll` отвечает не только за землю. В его область входят ландшафт,
|
||
здания, визуальный слой мира, камера, shade/state layer, primitive buffers,
|
||
сортировочные слои, источники света, тени, microtextures, атмосфера, дождь,
|
||
молнии, солнце и flares.
|
||
|
||
`Ngi32.dll` содержит низкоуровневые сервисы: DirectDraw/Direct3D-era renderer,
|
||
DirectSound, readers `NRes`/`RsLi`, память, часы, математику, пересечения,
|
||
определение CPU и таблицу быстрых процедур `g_FastProc`.
|
||
|
||
Предметные DLL закрывают отдельные области. `AniMesh.dll` загружает модели и
|
||
агентов. `ArealMap.dll` строит spatial graph и маршруты. `Behavior.dll`
|
||
реализует поведение юнитов. `ai.dll` содержит стратегический AI и миссионные
|
||
сценарии. `Wizard.dll` корректирует локальное движение. `Control.dll`
|
||
обслуживает физическую модель и столкновения. `Effect.dll` создаёт runtime-FX.
|
||
`MisLoad.dll` читает миссионные данные. `Net.dll` инкапсулирует DirectPlay.
|
||
`Joystick.dll` работает через DirectInput.
|
||
|
||
## Поток данных
|
||
|
||
Миссия не создаёт готовый кадр напрямую. Данные проходят через несколько
|
||
уровней: описание объекта, прототипы, ресурсы, runtime-object, контроллеры,
|
||
simulation state, render items и только затем платформенный renderer.
|
||
|
||
```text
|
||
mission data
|
||
-> object identity and properties
|
||
-> prototype registry
|
||
-> model/material/texture/effect resources
|
||
-> World3D object + domain controllers
|
||
-> simulation state
|
||
-> visible render items
|
||
-> Ngi32 render interface
|
||
-> DirectX-era device
|
||
```
|
||
|
||
Этот поток объясняет, почему нельзя объединять физический архив, metadata entry,
|
||
декодированный payload и готовый runtime-кэш. У каждого уровня свой срок жизни,
|
||
собственный refcount и собственные ошибки. Детали ресурсного конвейера описаны
|
||
в [Томе III](03-resources.md), а сборка мира из миссии -- в [Томе IV](04-world.md).
|
||
|
||
## Bootstrap
|
||
|
||
`iron_3d.exe` -- небольшой PE32/x86 bootstrap размером 36 864 байта. Основная
|
||
игровая логика находится в `iron3d.dll`. Исполняемый файл создаёт Win32-процесс,
|
||
подготавливает окружение, загружает библиотеку и получает восемь публичных
|
||
точек входа:
|
||
|
||
```text
|
||
createShell deleteShell
|
||
createGame deleteGame
|
||
createSubsystems deleteSubsystems
|
||
getIGame getIShell
|
||
```
|
||
|
||
Эти функции образуют внешнюю границу игры. `createShell` создаёт оболочку
|
||
интерфейса и меню, `createGame` -- объект игровой логики, `createSubsystems` --
|
||
аппаратные и runtime-сервисы. Getter-функции возвращают уже созданные объекты.
|
||
|
||
Запуск удобно читать как конечный автомат:
|
||
|
||
```text
|
||
PROCESS_CREATED
|
||
-> LIBRARY_READY
|
||
-> ENTRYPOINTS_READY
|
||
-> SHELL_CREATED
|
||
-> GAME_CREATED
|
||
-> SUBSYSTEMS_READY
|
||
-> MAIN_LOOP
|
||
-> SUBSYSTEMS_CLOSED
|
||
-> GAME_DELETED
|
||
-> SHELL_DELETED
|
||
```
|
||
|
||
Каждый переход имеет обратное действие. Если display, sound или другой
|
||
обязательный сервис не создан, главный цикл не начинается, но уже созданные
|
||
объекты освобождаются в обратном порядке. Новая оболочка запуска должна
|
||
работать из каталога оригинальной установки, сохранять смысл относительных
|
||
путей, создавать окно до графической подсистемы и закрывать частично поднятые
|
||
сервисы без предположения, что init дошёл до конца.
|
||
|
||
Bootstrap обеих полных частей побайтно одинаков, хотя файл второй части может
|
||
иметь другое имя:
|
||
|
||
```text
|
||
size 36 864
|
||
entry RVA 0x147E
|
||
SHA-256 f476af85c034a4b4f34f49d0806e4dff397b5da0ee26d382a7674231144979f7
|
||
```
|
||
|
||
Следовательно, различия полных частей начинаются после передачи управления DLL
|
||
и игровым данным. Адреса executable демоверсии относятся к другой binary
|
||
profile и не должны переноситься на полные версии без проверки hash.
|
||
|
||
## Инициализация подсистем
|
||
|
||
Iron3D разделяет создание высокоуровневых объектов и создание подсистем.
|
||
`createShell` конструирует оболочку пользовательского интерфейса, `createGame`
|
||
создаёт объект игры, а `createSubsystems` связывает их с display, sound,
|
||
network и World3D.
|
||
|
||
Высокоуровневая последовательность выглядит так:
|
||
|
||
```text
|
||
прочитать iron_3d.ini
|
||
-> получить display service
|
||
-> создать окно и графическое устройство
|
||
-> проверить доступность 3D-драйвера
|
||
-> выбрать CURRENT_D3DCARD
|
||
-> получить sound service и настроить громкость
|
||
-> создать network instance и передать application GUID
|
||
-> создать World3D game settings
|
||
```
|
||
|
||
Ошибка отсутствующего 3D-устройства обрабатывается отдельно от ошибок ресурсов:
|
||
это разные стадии запуска. Конфигурация влияет не только на разрешение. В
|
||
runtime попадают графическая карта, громкость эффектов, CD-audio, режим
|
||
CD-sound, сетевое приложение и World3D settings. Application GUID сетевой
|
||
подсистемы:
|
||
|
||
```text
|
||
{3C1D1F01-A870-11D1-8400-000021B14415}
|
||
```
|
||
|
||
Один и тот же GUID передаётся сетевому объекту и service layer. Если он
|
||
разойдётся, экземпляры игры станут логически разными приложениями, даже при
|
||
исправном транспорте.
|
||
|
||
## `stdInitGame`
|
||
|
||
После платформенных сервисов World3D создаёт внутренний runtime:
|
||
|
||
1. Создаёт глобальную очередь объектов.
|
||
2. Сохраняет window handle и режим игры.
|
||
3. При нужном режиме ограничивает курсор областью окна.
|
||
4. Получает или создаёт 3D sound object.
|
||
5. Загружает реестр адресов компонентов из `Comp.ini`.
|
||
6. Получает или создаёт 3D renderer.
|
||
7. Читает профиль возможностей renderer.
|
||
8. Загружает component type 6.
|
||
9. Для multiplayer создаёт NetWatcher.
|
||
10. Получает world-interface из Terrain.
|
||
11. Устанавливает исходные параметры света и тумана.
|
||
|
||
Порядок важен. World objects не должны появляться до queue, ресурсы рендера --
|
||
до renderer, сетевые mirror objects -- до NetWatcher. В новой реализации у
|
||
каждого этапа должен быть явный признак успешного создания, чтобы shutdown мог
|
||
безопасно разобрать неполный init.
|
||
|
||
## Завершение
|
||
|
||
Shutdown идёт в обратном направлении: прекращаются игровые расчёты и сетевые
|
||
наблюдатели, разбираются отложенные операции, освобождаются world objects и
|
||
менеджеры, затем renderer и sound, затем game settings и platform services.
|
||
Ограничение курсора снимается, глобальные ссылки очищаются.
|
||
|
||
Полезный протокол завершения:
|
||
|
||
1. Запретить новые события и новые объекты.
|
||
2. Дождаться выхода из calculation/render traversal.
|
||
3. Разобрать очередь deferred operations.
|
||
4. Отсоединить объекты от очереди, контроллеров и менеджеров.
|
||
5. Освободить managers и singletons после их consumers.
|
||
6. Закрыть устройства и платформенные сервисы.
|
||
|
||
Такой порядок защищает от dangling-ссылок между World3D, Terrain, renderer,
|
||
sound и сетевым слоем.
|
||
|
||
## Главный цикл
|
||
|
||
Главный цикл -- не одна функция `update_and_render`, а расписание, связывающее
|
||
Win32 messages, input, игровые события, таймеры, сеть и renderer. Системная
|
||
очередь сообщает об активации окна, вводе, изменении состояния процесса и
|
||
выходе. Очередь World3D рассчитывает игровые объекты. У этих очередей разные
|
||
правила времени и владения, поэтому их нельзя смешивать в один контейнер.
|
||
|
||
Подтверждённые точки вызова в одном из профилей:
|
||
|
||
```text
|
||
stdCalculateGame RVA 0x5FA94, 0x604C1, 0x6086B
|
||
ClearManualEventsList RVA 0x6052F
|
||
stdRenderGame RVA 0x60B2F
|
||
UpdateManualEventsList в обработчике сообщений около RVA 0xA3759
|
||
```
|
||
|
||
Смысловой skeleton:
|
||
|
||
```c
|
||
while (running) {
|
||
stdCalculateGame();
|
||
clear_keyboard_snapshot();
|
||
update_shell_and_mode();
|
||
ClearManualEventsList();
|
||
process_window_messages();
|
||
update_timers_ui_gameplay_network();
|
||
if (mode_requires_extra_step) stdCalculateGame();
|
||
if (render_enabled) {
|
||
stdSetCurrentCamera(camera);
|
||
stdRenderGame(camera);
|
||
} else {
|
||
sleep_briefly();
|
||
}
|
||
update_post_render_state();
|
||
}
|
||
```
|
||
|
||
Ввод из window messages накапливается между расчётными шагами. Если читать
|
||
клавиатуру только внутри рендера, события будут теряться при пропущенных кадрах
|
||
или отключённом выводе.
|
||
|
||
## `stdCalculateGame`
|
||
|
||
Calculation pass сначала очищает или подготавливает список manual events,
|
||
увеличивает внутренний depth/counter и опрашивает input device. Если устройство
|
||
временно потеряно, выполняется повторное получение доступа и чтение повторяется.
|
||
Затем при незамороженной игре выставляется признак `in_calculation` и вызывается
|
||
основной traversal очереди объектов.
|
||
|
||
```text
|
||
prepare input/events
|
||
-> enter calculation
|
||
-> dispatch queue events
|
||
-> objects update behavior and transforms
|
||
-> leave calculation
|
||
-> apply deferred operations
|
||
-> occasional cache maintenance
|
||
```
|
||
|
||
После traversal разбирается deferred-delete list. Объект может запросить
|
||
собственное удаление во время события, но память освобождается только после
|
||
завершения обхода. Периодически также очищаются давно неиспользуемые ресурсы и
|
||
объекты по порогам часов порядка 20 и 60 секунд.
|
||
|
||
Совместимый runtime должен иметь явный traversal depth или флаг
|
||
`in_calculation`. Нельзя полагаться на то, что контейнер выдержит удаление
|
||
текущего элемента из обработчика события.
|
||
|
||
## Жизненный цикл кадра
|
||
|
||
Рендер читает состояние, подготовленное расчётом. Кадр начинается до renderer-а:
|
||
message pump уже накопил ввод, World3D уже обновил объекты, отложенные операции
|
||
и анимации, после чего выбирается камера и обновляется listener звука.
|
||
|
||
```text
|
||
system messages and input
|
||
-> simulation calculation
|
||
-> deferred object operations
|
||
-> animation and transforms
|
||
-> camera and sound listener
|
||
-> visibility and render queues
|
||
-> materials and draw passes
|
||
-> renderer completion
|
||
-> end-of-render callbacks and UI
|
||
```
|
||
|
||
В `World3D::stdRenderGame` виден крупный каркас: установка camera/viewport,
|
||
renderer frame boundaries, traversal мира, завершение world/shade path,
|
||
renderer completion, снятие `in_render`, восстановление viewport и рассылка
|
||
end-of-render callbacks. Эти callbacks позволяют объектам безопасно обновить
|
||
временные ресурсы после того, как draw-команды больше их не используют.
|
||
|
||
Один calculation step не обязан соответствовать одному изображению. Главный
|
||
цикл допускает дополнительный вызов `stdCalculateGame` и режим, в котором
|
||
расчёт продолжается без вывода кадра. Поэтому нужно хранить отдельно:
|
||
|
||
1. монотонные платформенные часы;
|
||
2. игровое время с pause и масштабированием;
|
||
3. длительность текущего calculation step;
|
||
4. локальное время анимации и FX;
|
||
5. реальные часы обслуживания кэшей.
|
||
|
||
Игровую логику нельзя выводить из render delta: изменение частоты кадров тогда
|
||
изменит движение, камеру и сценарные таймеры. Подробности render item и рисков
|
||
кадровой совместимости вынесены в справочник [Render frame](../reference/render-frame.md).
|
||
|
||
## World3D
|
||
|
||
World3D связывает игровые объекты, события, время, ввод, камеру, сетевые
|
||
отражения и визуальное представление. Он не содержит всю предметную логику:
|
||
движение делегируется Behavior/Wizard, физика -- Control, мир -- Terrain. Его
|
||
задача -- общая идентичность, порядок вызовов и безопасный жизненный цикл.
|
||
|
||
`CreateQueue` создаёт singleton-объект размером 20 байт, а `GetQueue`
|
||
возвращает его. Очередь служит центральным маршрутизатором событий и операций
|
||
над объектами.
|
||
|
||
Публичный слой предоставляет отдельные функции для локальных и сетевых
|
||
объектов:
|
||
|
||
```text
|
||
CreateObject
|
||
AddObjectToGame
|
||
AddNewObjectToGame
|
||
CreateMirrorObject
|
||
AddMirrorObjectToGame
|
||
AddNewMirrorToGame
|
||
```
|
||
|
||
Разделение "создать" и "добавить в игру" означает два этапа: сначала выделить и
|
||
настроить instance, затем зарегистрировать его в общей системе. Это позволяет
|
||
loader-у заполнить свойства до появления объекта в расчётной очереди.
|
||
|
||
## Идентичность объектов
|
||
|
||
Object ID кодирует не только порядковый номер. Проверки диапазонов показывают
|
||
разбиение на номер игрока, класс и индекс. Mirror object представляет объект,
|
||
владельцем которого является другой участник. Локальный runtime хранит его
|
||
видимое состояние, но источник авторитетных изменений находится удалённо.
|
||
|
||
```c
|
||
struct ObjectId {
|
||
uint32_t raw;
|
||
uint16_t owner_player;
|
||
uint16_t class_and_index;
|
||
};
|
||
```
|
||
|
||
Точное битовое разбиение нужно брать из сетевых функций. На уровне API уже
|
||
сейчас полезно разделить логические свойства: `is_local`, `is_mirror`, `owner`,
|
||
`class` и `index`.
|
||
|
||
Минимальный runtime-object должен хранить identifier, type, owner, transform,
|
||
active state, ordered property bag, ссылки на controllers, участие в расчёте и
|
||
рендере, сетевой статус и флаг отложенного удаления. Специализированные DLL
|
||
могут быть представлены компонентами, но порядок их вызовов задаёт World3D.
|
||
|
||
## Отложенное удаление
|
||
|
||
`DeleteGameObject` проверяет, идёт ли calculation pass. Если обход активен и
|
||
удаление не принудительное, объект помещается в deferred list. `KillGameObject`
|
||
отправляет запрос через очередь, а не освобождает память напрямую.
|
||
|
||
```c
|
||
void request_delete(Object* o) {
|
||
if (world.in_calculation) {
|
||
world.deferred_delete.push_back(o);
|
||
o->pending_delete = true;
|
||
} else {
|
||
world.detach_and_release(o);
|
||
}
|
||
}
|
||
```
|
||
|
||
Это защищает итераторы, связи и текущий стек вызовов. Любая новая подсистема,
|
||
способная удалить объект из обработчика события, обязана пользоваться тем же
|
||
механизмом.
|
||
|
||
Регистрация в очереди и владение памятью -- разные понятия. Удаление из мира не
|
||
всегда означает немедленное освобождение instance: часть объектов и managers
|
||
использует intrusive reference count, а renderer, sound и resource managers
|
||
могут возвращать уже существующий singleton с увеличенным счётчиком. Поэтому
|
||
global manager закрывается после всех объектов, которые на него ссылаются.
|
||
|
||
## Детерминизм
|
||
|
||
Даже при одинаковых формулах результат зависит от порядка. Стабильный runtime
|
||
сохраняет последовательность queue traversal, момент формирования input
|
||
snapshot, порядок сетевых сообщений, обработку deferred operations и порядок
|
||
обращений к RNG. Оптимизация и многопоточность допустимы только при
|
||
детерминированном объединении результатов.
|
||
|
||
Для переносимой реализации полезно разделить scheduler phases и immutable
|
||
render snapshot. Это архитектурная рекомендация для новой реализации, а не
|
||
утверждение о точном layout исходных C++ classes.
|
||
|
||
## Стабильность между сборками
|
||
|
||
Внешняя архитектура полных Частей 1 и 2 сохраняет те же пятнадцать DLL, 313
|
||
exports, имена, ordinals и import sets. Побайтно идентичны:
|
||
|
||
```text
|
||
ai.dll, Behavior.dll, Joystick.dll, MisLoad.dll, Net.dll,
|
||
Ngi32.dll, Terrain.dll, Wizard.dll, World3D.dll
|
||
```
|
||
|
||
Пересобраны:
|
||
|
||
```text
|
||
AniMesh.dll, ArealMap.dll, Control.dll, Effect.dll,
|
||
iron3d.dll, services.dll
|
||
```
|
||
|
||
Это разделяет переносимость выводов. World3D lifecycle, Terrain, NRes/RsLi
|
||
readers, mission loader, AI/Behavior/Wizard, DirectPlay wrapper и joystick
|
||
adapter подтверждаются одной машинной реализацией. Model/agent runtime,
|
||
collision, effects, shell/composition и service layer требуют отдельного
|
||
сравнения поведения Частей 1 и 2.
|
||
|
||
Для `World3D.dll` Частей 1 и 2 применим общий hash:
|
||
|
||
```text
|
||
World3D.dll SHA-256
|
||
17e4a3089b2583a8cf2356c9db0390b1aba138356a09130d79b4e7e4791da61e
|
||
```
|
||
|
||
RVA внутри `iron3d.dll` нельзя считать общими без проверки конкретного файла:
|
||
эта DLL пересобрана между частями, а демоверсия имеет отдельный binary profile.
|
||
Смысловая последовательность цикла переносится как контракт scheduler-а, но
|
||
адреса остаются build-specific.
|