HWW Portfolio

Software and Hardware Developer

📑 Технический стандарт: Архитектура и законы игрового мира


📜 ЧАСТЬ I. АРХИТЕКТУРНЫЙ МАНИФЕСТ## Два правила «живого» движка## 1. Подписывайся на смыслы, а не на имена (Принцип отчуждения)

Мы никогда не привязываем логику к конкретному объекту в памяти. Связывать системы напрямую — значит плодить баги и путаться в порядке их создания.

2. Данные — мертвы, Системы — вдыхают жизнь (Принцип пассивной сцены)

Объекты в редакторе мира — это просто «тупые» коробки со свойствами. Они статичны и не имеют собственного разума или функций обновления.


📜 ЧАСТЬ II. СОГЛАШЕНИЕ О СТРУКТУРЕ МИРА## Гибридный Data-Driven подход (Опыт Naughty Dog & Guerrilla Games)## 1. Концепция сущностей и именования

2. Контур связей (Кросс-референсы)

3. Разделение Иерархий (Понятийный контур)

Движок жестко разделяет три вида «деревьев», которые в коммерческих движках часто спутаны в одну кучу:

  1. Дерево Проекта (Levels / Assets Tree): Структура папок на жестком диске. Отвечает только за то, в каком файле на диске лежат настройки зоны.
  2. Дерево Трансформаций (Transform Hierarchy): Физическая привязка (Parent/Child). Нужна только для матриц перемещения (колесо крутится за машиной). Сущности в логическом списке при этом лежат раздельно.
  3. Логическое Дерево (Отсутствует): В логике сцены дерева нет. Все сущности равны и общаются через плоскую шину данных.

📜 ЧАСТЬ III. КОНТУР ДИНАМИЧЕСКИХ ДАННЫХ (TAGS & FACTS)## 1. Принцип Динамических Свойств (Dynamic Property Overlay)

2. Сравнение UX-подходов к редактированию динамических данных

3. Техническое решение: Компромиссная Unity-реализация (DynamicFacts)

Для достижения минимализма ND в коде и наглядности GUI в рантайме применяется класс DynamicFacts. Через интерфейс ISerializationCallbackReceiver он автоматически преобразует сырую JSON-строку из инспектора в быстрый типизированный словарь для Систем:

namespace RA.Runtime
{
    [Serializable]
    public class DynamicFacts : BaseFacts, ISerializationCallbackReceiver
    {
        [SerializeField, TextArea(3, 20)]
        private string jsonData = "{}"; // Text field input (Naughty Dog TAGS style)

        private Dictionary<string, string> facts = new Dictionary<string, string>(); // Fast runtime registry

        // Unity serialization processing via single-pass tokenization
        public void OnBeforeSerialize() => jsonData = ConvertToJsonString();
        public void OnAfterDeserialize() => facts = ParseJsonStringToDictionary(jsonData);

        protected override bool TryGetInternal<T>(string name, out T value)
        {
            value = default;
            if (facts.TryGetValue(name, out string jsonValue) && !string.IsNullOrEmpty(jsonValue))
            {
                value = ConvertFromJson<T>(jsonValue); // Safe types resolution (int, float, Vector3, Color)
                return true;
            }
            return false;
        }

        protected override void SetInternal<T>(string name, T value) => facts[name] = ConvertToJson(value);
        protected override bool RemoveInternal(string name) => facts.Remove(name);
        protected override bool ContainsInternal(string name) => facts.ContainsKey(name);
        protected override void ClearInternal() => facts.Clear();
    }
}

📜 ЧАСТЬ IV. АСИНХРОННЫЙ СТРИМИНГ И АКТОРЫ (СЦЕНЫ-КОНТЕЙНЕРЫ)

Мир не является единым монолитным списком. Он состоит из Глобальной сцены (Мастер-мира) и динамически накладываемых Подсцен (Контейнеров).


📜 ЧАСТЬ V. МАНИФЕСТ СКРИПТОВОГО КОНТУРА## Взаимодействие миров: Кто главный в игре?## 1. Первопричина: Отказ от Юнити-центричности

Мы полностью уничтожаем хаос, в котором каждый гейм-объект живет своей жизнью и тянет спагетти-связи к соседям. Мы отказываемся от логики «умных объектов» (подход OnEnable/Update), которая делает сцену неуправляемой.

2. Источник правды: Lua-Директор сверху (Top-Down)

В нашей архитектуре Скрипт — это Постановщик Трагедии, а C#-объекты сцены — пассивные марионетки.

3. Архитектура пространств имен (Изолированные стейты)

Чтобы память не засорялась, а переменные разных квестов не конфликтовали, мы разделяем мир скриптов на два уровня:


📜 ЧАСТЬ VI. ВЫСОКОПРОИЗВОДИТЕЛЬНЫЙ LUA-DCS МОСТ## 1. Передача типов по ID (Zero String Lookups)

В рантайме на горячих путях (hot paths) строки не используются. При старте игры глобальный скрипт Lua один раз опрашивает C#-реестр пулов компонентов и строит таблицу числовых констант COMPONENT.Имя = ID. Все обращения из Lua к ядру DCS происходят строго через unboxed-числа. Аллокация компонентов в C#-пулах выполняется по прямому индексу массива за O(1).

2. Перманентные подписки классов (No-Boilerplate Events)

Объекты в Lua не подписываются на события динамически при смене стейтов. При создании экземпляра объекта (в его базовом абстрактном классе Process) автоматический итератор сканирует таблицу его состояний, находит все функции формата on_event_[ИмяСобытия] и регистрирует их в C# пуле подписок EventSubscription один раз на весь жизненный цикл объекта. Контекстное поведение (игнорирование или обработка) настраивается подменой локальных Lua-таблиц состояний, не нагружая C#.

3. Двухфазная доставка и извлечение данных (Unboxed Payload)

События в DCS — это короткоживущие (1 кадр) компоненты данных на стороне отправителя. При срабатывании C# EventSystem пробрасывает в Lua-роутер только два unboxed-числа: HostID получателя и DenseIndex самого компонента события в пуле. Роутер находит объект в Lua по ID за O(1). Если объекту требуются числовые/векторные данные события, он точечно вытягивает их из C# по индексу через метод getData() без аллокаций мусора в куче.

4. Асинхронные потоки состояний (Coroutine Threading)

Чтобы избавить дизайнеров от написания покадровых счетчиков времени в циклах Update, базовый абстрактный класс Process имеет встроенную поддержку корутин Lua. Если состояние объявляет метод co_update, он изолируется в поток. Это позволяет писать сложную последовательную логику (паузы, ожидания анимаций) в виде линейного текста с командами coroutine.yield(), замораживающими выполнение строго на 1 игровой кадр


📜 ЧАСТЬ VII. ТЕХНИЧЕСКИЙ КОД СТРИМИНГА И АКТОРОВ## 1. Пассивная марионетка сцены (Actor.cs)

using UnityEngine;

namespace DynamicComponent.Lua
{
    /// <summary>
    /// A passive scene object representation. Acts as a data container anchor 
    /// and a bridge between Unity's hierarchy and the Data-Oriented dynamic core.
    /// </summary>
    [DisallowMultipleComponent]
    public class Actor : MonoBehaviour
    {
        [Header("Entity Metadata")]
        [Tooltip("The unique long string name of this object, validated by the editor environment.")]
        [SerializeField] private string displayName;

        [Header("Dynamic Properties")]
        [Tooltip("The JSON-backed dynamic overlay data container (Naughty Dog TAGS style).")]
        [SerializeField] private DynamicFacts dynamicFacts;

        public string DisplayName => displayName;
        public DynamicFacts Facts => dynamicFacts;

        private void OnValidate()
        {
            if (string.IsNullOrEmpty(displayName))
            {
                displayName = gameObject.name.ToLower().Replace(" ", "_");
            }
        }
    }
}

2. Директор Асинхронной Загрузки (ZoneManager.cs)

using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

namespace DynamicComponent.Lua
{
    /// <summary>
    /// Global manager responsible for asynchronous scene streaming, local scope allocation,
    /// and routing world entities from Unity subscenes straight into the script engine layer.
    /// </summary>
    public class ZoneManager : MonoBehaviour
    {
        private static ZoneManager _instance;
        public static ZoneManager Instance => _instance;

        private readonly Dictionary<string, Actor> _activeActors = new Dictionary<string, Actor>(StringComparer.OrdinalIgnoreCase);
        private readonly Dictionary<string, ZoneScriptContext> _activeZones = new Dictionary<string, ZoneScriptContext>(StringComparer.OrdinalIgnoreCase);

        void Awake()
        {
            if (_instance != null && _instance != this)
            {
                Destroy(gameObject);
                return;
            }
            _instance = this;
            DontDestroyOnLoad(gameObject);
        }

        public void LoadZoneAsync(string sceneName, string luaScriptRelativePath)
        {
            if (_activeZones.ContainsKey(sceneName))
            {
                Debug.LogWarning($"[ZoneManager] Zone scene '{sceneName}' is already loaded.");
                return;
            }

            var asyncLoad = SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive);
            asyncLoad.completed += (operation) =>
            {
                InitializeLoadedZone(sceneName, luaScriptRelativePath);
            };
        }

        private void InitializeLoadedZone(string sceneName, string luaScriptRelativePath)
        {
            Scene loadedScene = SceneManager.GetSceneByName(sceneName);
            if (!loadedScene.IsValid()) return;

            GameObject[] rootObjects = loadedScene.GetRootGameObjects();
            List<Actor> zoneActors = new List<Actor>();

            foreach (var root in rootObjects)
            {
                var actorsInRoot = root.GetComponentsInChildren<Actor>(true);
                zoneActors.AddRange(actorsInRoot);
            }

            foreach (var actor in zoneActors)
            {
                if (!_activeActors.ContainsKey(actor.DisplayName))
                {
                    _activeActors[actor.DisplayName] = actor;
                }
            }

            Func<string, BaseFacts> registryLookupDelegate = (entityName) =>
            {
                if (_activeActors.TryGetValue(entityName, out Actor actor)) return actor.Facts;
                return null;
            };

            var zoneContext = new ZoneScriptContext(sceneName, registryLookupDelegate);
            _activeZones[sceneName] = zoneContext;

            string scriptCode = LuaManager.Instance.ReadScriptFile(luaScriptRelativePath);
            if (!string.IsNullOrEmpty(scriptCode))
            {
                zoneContext.RunDirectorScript(scriptCode);
            }
        }

        public void UnloadZoneAsync(string sceneName)
        {
            if (_activeZones.TryGetValue(sceneName, out ZoneScriptContext context))
            {
                context.Dispose();
                _activeZones.Remove(sceneName);

                Scene sceneToUnload = SceneManager.GetSceneByName(sceneName);
                if (sceneToUnload.IsValid())
                {
                    foreach (var root in sceneToUnload.GetRootGameObjects())
                    {
                        foreach (var actor in root.GetComponentsInChildren<Actor>(true))
                        {
                            _activeActors.Remove(actor.DisplayName);
                        }
                    }
                    SceneManager.UnloadSceneAsync(sceneToUnload);
                }
            }
        }
    }
}