Skip to content

Глава 2.1: 5-уровневая иерархия скриптов

Главная | 5-уровневая иерархия скриптов | Следующая: Подробный разбор config.cpp >>



Содержание


Обзор

Движок DayZ компилирует скрипты за пять отдельных проходов, называемых модулями скриптов. Каждый модуль соответствует пронумерованной папке в директории Scripts/ вашего мода:

Scripts/
  1_Core/          --> engineScriptModule
  2_GameLib/       --> gameLibScriptModule
  3_Game/          --> gameScriptModule
  4_World/         --> worldScriptModule
  5_Mission/       --> missionScriptModule

Каждый уровень строится поверх предыдущих. Номера не произвольны --- они определяют строгий порядок компиляции и зависимостей, обеспечиваемый движком.


Стек уровней

+---------------------------------------------------------------+
|                                                               |
|   5_Mission   (missionScriptModule)                           |
|   UI, HUD, жизненный цикл миссии, экраны меню                |
|   Может ссылаться на: всё ниже (1-4)                          |
|                                                               |
+---------------------------------------------------------------+
|                                                               |
|   4_World     (worldScriptModule)                             |
|   Сущности, предметы, транспорт, менеджеры, игровая логика    |
|   Может ссылаться на: 1_Core, 2_GameLib, 3_Game               |
|                                                               |
+---------------------------------------------------------------+
|                                                               |
|   3_Game      (gameScriptModule)                              |
|   Конфиги, регистрация RPC, классы данных, привязки ввода     |
|   Может ссылаться на: 1_Core, 2_GameLib                       |
|                                                               |
+---------------------------------------------------------------+
|                                                               |
|   2_GameLib   (gameLibScriptModule)                           |
|   Низкоуровневые привязки движка (редко используются модами)   |
|   Может ссылаться на: только 1_Core                           |
|                                                               |
+---------------------------------------------------------------+
|                                                               |
|   1_Core      (engineScriptModule)                            |
|   Фундаментальные типы, константы, чистые утилитарные функции |
|   Может ссылаться на: ничего (это фундамент)                  |
|                                                               |
+---------------------------------------------------------------+

        ПОРЯДОК КОМПИЛЯЦИИ: 1 --> 2 --> 3 --> 4 --> 5
        НАПРАВЛЕНИЕ ЗАВИСИМОСТЕЙ: только вверх (нижние не видят верхние)

Уровень 1: 1_Core (engineScriptModule)

Назначение

Абсолютный фундамент. Код здесь выполняется на уровне движка до существования каких-либо игровых систем. Это самая ранняя точка, где может выполняться код мода.

Что размещать здесь

  • Константы и перечисления, общие для всех уровней
  • Чистые утилитарные функции (математические хелперы, строковые утилиты)
  • Инфраструктура логирования (сам логгер, а не то, что логируется)
  • Определения препроцессора и typedef
  • Определения базовых классов, которые должны быть видны везде

Реальные примеры

Community Framework размещает свою базовую систему модулей здесь:

c
// 1_Core/CF_ModuleCoreManager.c
class CF_ModuleCoreManager
{
    static ref array<typename> s_Modules = new array<typename>;

    static void _Insert(typename module)
    {
        s_Modules.Insert(module);
    }
};

MyFramework размещает свои константы логирования здесь:

c
// 1_Core/MyLogLevel.c
enum MyLogLevel
{
    TRACE = 0,
    DEBUG = 1,
    INFO  = 2,
    WARN  = 3,
    ERROR = 4
};

Когда использовать

Используйте 1_Core только когда вам нужно что-то доступное всем другим уровням, и это не имеет зависимостей от игровых типов вроде PlayerBase, ItemBase или MissionBase. Большинству модов этот уровень вообще не нужен.


Уровень 2: 2_GameLib (gameLibScriptModule)

Назначение

Низкоуровневые привязки библиотек движка. Этот уровень существует в ванильной иерархии скриптов, но редко используется модами. Он находится между сырым движком и игровой логикой.

Что размещать здесь

  • Абстракции уровня движка (привязки рендеринга, звукового движка)
  • Математические библиотеки сверх того, что предоставляет 1_Core
  • Базовые типы виджетов/UI-движка

Реальные примеры

DabsFramework --- один из немногих модов, использующих этот уровень:

c
// 2_GameLib/DabsFramework/MVC/ScriptView.c
// Низкоуровневая инфраструктура привязки представлений
class ScriptView : ScriptedWidgetEventHandler
{
    // ...
};

Когда использовать

Почти никогда. Если вы не строите фреймворк, которому нужны привязки уровня движка ниже игрового уровня, полностью пропустите 2_GameLib. Подавляющее большинство модов используют только уровни 3, 4 и 5.


Уровень 3: 3_Game (gameScriptModule)

Назначение

Рабочий уровень для конфигурации, определений данных и систем, не взаимодействующих напрямую с мировыми сущностями. Это первый уровень, где доступны игровые типы.

Что размещать здесь

  • Классы конфигурации (настройки, которые можно загружать/сохранять)
  • Регистрация RPC и идентификаторы
  • Классы данных и DTO (объекты передачи данных)
  • Регистрация привязок ввода
  • Системы регистрации плагинов/модулей
  • Общие перечисления и константы, зависящие от игровых типов
  • Обработчики пользовательских клавиатурных привязок

Реальные примеры

MyFramework --- система конфигурации:

c
// 3_Game/MyMod/Config/MyConfigBase.c
class MyConfigBase
{
    // Базовая конфигурация с автоматической JSON-персистентностью
    void Load();
    void Save();
    string GetConfigPath();
};

COT определяет свои идентификаторы RPC здесь:

c
// 3_Game/COT/RPCData.c
class JMRPCData
{
    static const int WEATHER_SET  = 0x1001;
    static const int PLAYER_HEAL  = 0x1002;
    // ...
};

VPP Admin Tools регистрирует свои чат-команды:

c
// 3_Game/VPPAdminTools/ChatCommands/ChatCommandBase.c
class ChatCommandBase
{
    string GetCommand();
    bool Execute(PlayerIdentity sender, array<string> args);
};

Когда использовать

Если сомневаетесь, размещайте в 3_Game. Это уровень по умолчанию для большей части не-сущностного кода. Классы конфигурации, перечисления, константы, определения RPC, классы данных --- всё это принадлежит сюда.


Уровень 4: 4_World (worldScriptModule)

Назначение

Игровая логика, взаимодействующая с 3D-миром. Этот уровень имеет доступ к сущностям, предметам, транспорту, зданиям и всем мировым объектам.

Что размещать здесь

  • Пользовательские предметы и оружие (расширение ItemBase, Weapon_Base)
  • Пользовательские сущности (расширение Building, DayZAnimal и т.д.)
  • Мировые менеджеры (системы спавна, менеджеры лута, AI-директоры)
  • Расширения игрока (модифицированное поведение PlayerBase)
  • Кастомизация транспорта
  • Системы действий (расширение ActionBase)
  • Триггерные зоны и эффекты области

Реальные примеры

MyMissions Mod спавнит маркеры миссий в мире:

c
// 4_World/Missions/MyMissionMarker.c
class MyMissionMarker : House
{
    void MyMissionMarker()
    {
        SetFlags(EntityFlags.VISIBLE, true);
    }

    void SetPosition(vector pos)
    {
        SetPosition(pos);
    }
};

MyAI Mod реализует сущности ботов здесь:

c
// 4_World/AI/MyAIBot.c
class MyAIBot : SurvivorBase
{
    protected ref MyAIBrain m_Brain;

    override void EOnInit(IEntity other, int extra)
    {
        super.EOnInit(other, extra);
        m_Brain = new MyAIBrain(this);
    }
};

Ванильный DayZ определяет все предметы здесь:

c
// 4_World/Entities/ItemBase/Edible_Base.c
class Edible_Base extends ItemBase
{
    // Все продукты питания наследуют от этого
};

Когда использовать

Всё, что касается физического игрового мира: создание сущностей, модификация предметов, обработка взаимодействий игроков, управление мировым состоянием. Если ваш класс расширяет EntityAI, ItemBase, PlayerBase, Building или взаимодействует с GetGame().GetWorld(), он принадлежит 4_World.


Уровень 5: 5_Mission (missionScriptModule)

Назначение

Самый верхний уровень. Жизненный цикл миссии, UI-панели, HUD-оверлеи и финальная точка инициализации. Здесь находится код запуска клиентской и серверной стороны.

Что размещать здесь

  • Хуки классов миссий (переопределения MissionServer, MissionGameplay)
  • HUD и UI-панели
  • Экраны меню
  • Регистрация и инициализация модов (последовательность «загрузки»)
  • Клиентские оверлеи рендеринга
  • Обработчики запуска/остановки сервера

Реальные примеры

MyFramework подключается к миссии для инициализации всех подсистем:

c
// 5_Mission/MyMod/MyModMissionClient.c
modded class MissionGameplay
{
    override void OnInit()
    {
        super.OnInit();
        MyFramework.Init();
    }

    override void OnMissionFinish()
    {
        MyFramework.ShutdownAll();
        super.OnMissionFinish();
    }
};

COT добавляет своё меню администратора здесь:

c
// 5_Mission/COT/gui/COT_Menu.c
class COT_Menu : UIScriptedMenu
{
    override Widget Init()
    {
        // Построение UI панели администратора
    }
};

MyMissions Mod регистрируется в Core:

c
// 5_Mission/Missions/MyMissionsRegister.c
class MyMissionsRegister
{
    void MyMissionsRegister()
    {
        MyFramework.RegisterMod("Missions", "1.0.0");
        MyFramework.RegisterModConfig(new MyMissionsConfig());
    }
};

Когда использовать

UI, HUD, экраны меню и инициализация мода, зависящая от активной миссии. Также финальное место, где сервер подключается к жизненному циклу запуска/завершения.


Критическое правило

Нижние уровни НЕ МОГУТ ссылаться на типы из верхних уровней.

Это самое важное правило в архитектуре скриптов DayZ. Движок обеспечивает его на этапе компиляции.

РАЗРЕШЕНО:
  Код 5_Mission ссылается на класс из 4_World       OK
  Код 4_World ссылается на класс из 3_Game           OK
  Код 3_Game ссылается на класс из 1_Core            OK

ЗАПРЕЩЕНО:
  Код 3_Game ссылается на класс из 4_World           ОШИБКА КОМПИЛЯЦИИ
  Код 4_World ссылается на класс из 5_Mission        ОШИБКА КОМПИЛЯЦИИ
  Код 1_Core ссылается на класс из 3_Game            ОШИБКА КОМПИЛЯЦИИ

Почему это существует

Каждый уровень компилируется отдельно и последовательно. Когда компилируется 3_Game, 4_World и 5_Mission ещё не существуют. Компилятор не знает об этих типах.

Что происходит при нарушении

Сообщение об ошибке часто бывает неинформативным:

SCRIPT (E): Undefined type 'PlayerBase'

Обычно это означает, что вы разместили код в 3_Game, который ссылается на PlayerBase, определённый в 4_World. Решение --- переместить ваш код в 4_World или выше.

Обходное решение: приведение через базовые типы

Когда коду 3_Game нужно работать с объектом, который в рантайме будет PlayerBase, используйте базовый тип Object или Man (определённый в 3_Game) и приведите позже:

c
// В 3_Game -- мы не можем напрямую ссылаться на PlayerBase
class MyConfig
{
    void HandlePlayer(Man player)
    {
        // 'Man' доступен в 3_Game
        // В рантайме это будет PlayerBase, но мы не можем назвать его здесь
    }
};

// В 4_World -- теперь можно безопасно привести
class MyWorldLogic
{
    void ProcessPlayer(Man player)
    {
        PlayerBase pb;
        if (Class.CastTo(pb, player))
        {
            // Теперь у нас полный доступ к PlayerBase
        }
    }
};

Порядок загрузки и тайминг

Порядок компиляции

Движок компилирует скрипты всех модов для каждого уровня перед переходом к следующему:

Шаг 1: Компиляция скриптов 1_Core ВСЕХ модов
Шаг 2: Компиляция скриптов 2_GameLib ВСЕХ модов
Шаг 3: Компиляция скриптов 3_Game ВСЕХ модов
Шаг 4: Компиляция скриптов 4_World ВСЕХ модов
Шаг 5: Компиляция скриптов 5_Mission ВСЕХ модов

На каждом шаге моды упорядочиваются по цепочке зависимостей requiredAddons в config.cpp. Если ModB зависит от ModA, скрипты ModA для данного уровня компилируются первыми.

Порядок инициализации

После компиляции инициализация в рантайме следует другой последовательности:

1. Движок загружается, считывает конфиги
2. Скрипты 1_Core доступны (статические конструкторы выполняются)
3. Скрипты 2_GameLib доступны
4. Скрипты 3_Game доступны
   --> Функции входа CfgMods выполняются (напр., "CreateGameMod")
   --> Привязки ввода регистрируются
5. Скрипты 4_World доступны
   --> Можно создавать сущности
6. Миссия загружается
7. Скрипты 5_Mission доступны
   --> MissionServer.OnInit() / MissionGameplay.OnInit() срабатывают
   --> UI и HUD становятся доступными

Когда выполняется код каждого уровня

УровеньСтатическая инициализацияГотов в рантаймеКлючевое событие
1_CoreПервыйНемедленноЗагрузка движка
2_GameLibВторойПосле инициализации движкаПодсистемы движка готовы
3_GameТретийПосле инициализации игрыCreateGame() / пользовательская функция входа
4_WorldЧетвёртыйПосле загрузки мираНачинается спавн сущностей
5_MissionПятый (последний)После старта миссииMissionServer.OnInit() / MissionGameplay.OnInit()

Важно: Статические переменные и код глобальной области видимости каждого уровня выполняются на этапе компиляции/линковки, до вызова OnInit(). Не размещайте сложную логику инициализации в статических инициализаторах.


Практические рекомендации

«Если сомневаетесь, размещайте в 3_Game»

Это наиболее распространённый уровень для кода модов. Если только ваш код:

  • Не должен быть доступен до существования игровых типов --> 1_Core
  • Не расширяет сущность/предмет/транспорт/игрока --> 4_World
  • Не касается UI, HUD или жизненного цикла миссии --> 5_Mission

...то он принадлежит 3_Game.

Чек-лист по уровням

Перед размещением файла задайте эти вопросы:

  1. Расширяет ли он EntityAI, ItemBase, PlayerBase, Building или другую мировую сущность? Размещайте в 4_World.

  2. Ссылается ли он на MissionServer, MissionGameplay или создаёт UI-виджеты? Размещайте в 5_Mission.

  3. Это чистый класс данных, конфиг, перечисление или определение RPC? Размещайте в 3_Game.

  4. Это фундаментальная константа или утилита без игровых зависимостей? Размещайте в 1_Core.

  5. Ничего из вышеперечисленного? По умолчанию 3_Game.

Держите уровни тонкими

Типичная ошибка --- сваливание всего в 4_World. Это создаёт сильно связанный код. Вместо этого:

ХОРОШО:
  3_Game/  --> Класс конфига, перечисления, ID RPC, структуры данных
  4_World/ --> Менеджер, использующий конфиг, классы сущностей
  5_Mission/ --> UI, отображающий состояние менеджера

ПЛОХО:
  4_World/ --> Конфиги, перечисления, RPC, менеджеры И классы сущностей вперемешку

Краткое руководство по выбору

                    Расширяет ли мировую сущность?
                          (EntityAI, ItemBase и т.д.)
                         /                    \
                        ДА                    НЕТ
                        |                      |
                    4_World              Касается UI/HUD/миссии?
                                        /                    \
                                      ДА                    НЕТ
                                       |                      |
                                   5_Mission         Это чистая утилита
                                                     без игровых зависимостей?
                                                      /                \
                                                    ДА                НЕТ
                                                     |                  |
                                                  1_Core            3_Game

Распространённые ошибки

1. Ссылка на PlayerBase из 3_Game

c
// НЕПРАВИЛЬНО: в 3_Game/MyConfig.c
class MyConfig
{
    void ApplyToPlayer(PlayerBase player)  // ОШИБКА: PlayerBase ещё не определён
    {
    }
};

// ПРАВИЛЬНО: в 3_Game/MyConfig.c
class MyConfig
{
    ref array<float> m_Values;  // Чистые данные, без ссылок на сущности
};

// ПРАВИЛЬНО: в 4_World/MyManager.c
class MyManager
{
    void ApplyConfig(PlayerBase player, MyConfig config)
    {
        // Теперь можно использовать оба
    }
};

2. Размещение UI-кода в 4_World

c
// НЕПРАВИЛЬНО: в 4_World/MyPanel.c
class MyPanel : UIScriptedMenu  // UIScriptedMenu работает в 4_World,
{                                // но хуки MissionGameplay в 5_Mission
    // Это вызовет проблемы при попытке регистрации UI
};

// ПРАВИЛЬНО: в 5_Mission/MyPanel.c
class MyPanel : UIScriptedMenu
{
    // UI принадлежит 5_Mission, где доступен жизненный цикл миссии
};

3. Размещение констант в 4_World, когда они нужны в 3_Game

c
// НЕПРАВИЛЬНО: Константы определены в 4_World
// 4_World/MyConstants.c
const int MY_RPC_ID = 12345;

// 3_Game/MyRPCHandler.c
class MyRPCHandler
{
    void Register()
    {
        // ОШИБКА: MY_RPC_ID не виден здесь (определён на верхнем уровне)
    }
};

// ПРАВИЛЬНО: Константы определены в 3_Game (или 1_Core)
// 3_Game/MyConstants.c
const int MY_RPC_ID = 12345;  // Теперь виден и в 3_Game, и в 4_World, и в 5_Mission

4. Излишнее усложнение с 1_Core

Если ваши «константы» ссылаются на любой игровой тип, они не могут быть в 1_Core. Даже такая вещь как const string PLAYER_CONFIG_PATH допустима в 1_Core, но класс, принимающий параметр CGame --- нет.


Итоги

УровеньПапкаЗапись в конфигеОсновное назначениеЧастота
11_Core/engineScriptModuleКонстанты, утилиты, база логированияРедко
22_GameLib/gameLibScriptModuleПривязки движкаОчень редко
33_Game/gameScriptModuleКонфиги, RPC, классы данныхНаиболее частый
44_World/worldScriptModuleСущности, предметы, менеджерыЧасто
55_Mission/missionScriptModuleUI, HUD, хуки миссийЧасто

Запомните: Нижние уровни не видят верхние. Если сомневаетесь, используйте 3_Game. Перемещайте код наверх только когда вам нужен доступ к типам, определённым на верхнем уровне.


Следующая: Глава 2.2: Подробный разбор config.cpp

Released under CC BY-SA 4.0 | Code examples under MIT License