March 25

Как я строю UE проект чтобы с ним мог работать и я, и нейронка

Давайте сразу определимся: мы используем гит для записи изменений в проекте, не ленитесь комментить изменения без нейронки, потому что комменты от нейронки вам потом ничего не дадут для понимания.

Создаём .gitignore, чтобы git игнорировал тяжёлые и ненужные файлы типа кешей, всяких объектов и прочего при коммите, обычно вот таких: Binaries, Build, Intermediate, Saved, DerivedDataCache и т.д. В целом я советую сделать это через ИИ агентов, пусть он посмотрит структуру проекта и сделает gitignore на его основе.

Сорс контроль

Наш проект будет модульным и больше напоминать фреймворк. Держим в уме, "а ничё тот факт что", контекст нейронок не бесконечный и при работе с какими-то классами и объектами, он может сразу переполняться и начинать сессию по новой, где снова будет читать всё заново. Так же если какие-то траблы с классами или системами, он не положит весь наш проект и в случае чего, такую проблемную хрень, можно будет легко изолировать и работать с ним отдельно

У агентов сейчас есть чекпоинты, и если контекст переполняется, он может продолжить работу с того, на чём остановился, но он уже не будет помнить именование объектов, например, и будет написано всё вразнобой, поэтому стараемся сохранять контекст как можно дольше.

Поэтому будем следовать несложным правилам:

  1. State Machine — управление состояниями.
  2. Component — декомпозиция функциональности в компоненты.
  3. Observer — делегаты для событий
  4. Interface Segregation — разенлннные интерфейсы
  5. Coordinator — координируют подсистемы
  6. Data-Driven — DataAssets для конфигурации оружия, предметов и т.д.

С документами у нас также довольно важная и сложная работа. Многие агенты имеют набор для работы с UE, который по дефолту что-то подобное включает, но тут я лучше вручную задам всё.

Мы создаём мапу проекта — это такой короткий контекст для нейронки или для людей, которым, не дай бог, придётся в этом всём разбираться. Обычно это выглядит вот так:

  1. Overview (Обзор)
    - Название проекта, тип, движок
    - Ключевые принципы архитектуры
    - Диаграмма слоёв
  2. Project Structure (Структура проекта)
    - Дерево директорий
    - Соглашения об именовании (Naming Conventions)
  3. Core Systems (Основные системы)
    - Описание главных систем (Character, Weapon, AI)
    - Композиция компонентов
  4. Design Patterns (Паттерны)
    - Какие паттерны используются
    - Примеры кода
  5. Data Flow (Потоки данных)
    - Как данные проходят через систему
    - Диаграммы
  6. Component Architecture
    - Таблица компонентов и их responsibilities
  7. Coding Standards (Стандарты кодирования)
    - Стиль именования
    - Правила
  8. Extension Guidelines (Руководство по расширению)
    - Как добавлять новые фичи

Контекст AI-агента не бесконечный. Когда ты начинаешь новую сессию и просишь добавить новую механику — агент ничего не знает о твоём проекте. Ты либо каждый раз объясняешь всё с нуля, либо скидываешь карту проекта в начале сессии — и агент сразу понимает структуру, паттерны, соглашения по именованию.

State Machine (конечный автомат) — это способ организации логики, когда объект может находиться в одном из нескольких состояний и переключаться между ними по определённым правилам.

Аналогия

Представь дверь с замком:

  • Дверь может быть в состояниях: Закрыта, Открыта, Заперта
  • Из состояния "Закрыта" можно перейти в "Заперта" (повернуть ключ)
  • Из "Заперта" можно перейти в "Открыта" (правильный ключ)
  • Из "Открыта" можно перейти в "Закрыта"

Дверь не может быть одновременно "Открыта" и "Заперта" — только в одном состоянии. Каждое состояние имеет свою логику.

Зачем нужен?

  1. Чёткая логика — нет запутанных if-else.
  2. Нельзя сделать невозможное — персонаж не может бежать и целиться одновременно например.
  3. Легко добавить новое — просто создать новый класс состоянияю

Component Pattern — это главный структурный принцип: вместо того чтобы писать весь код персонажа в одном монолитном классе, функциональность разбивается на небольшие, сфокусированные компоненты, каждый из которых отвечает ровно за одну вещь. Так мы не раздуваем наш компонент, можем переиспользовать "модуль" в любом подходящем месте, а так-же просто отключить его за ненадабностью.

Так-же это позовляет работать с выделеным компонентом отдельно, т.е. опять-же нейронки не запутаются и не переполнит контекст.

Компоненты внутри моего персонажа

Аналогия

Представь смартфон:

  • Камера отвечает только за съёмку
  • Батарея отвечает только за питание
  • Динамик отвечает только за звук
  • Wi-Fi модуль отвечает только за связь

Зачем нужен?

  1. Изоляция изменений — правишь логику инвентаря, не боясь сломать камеру или здоровье персонажа.
  2. ПереиспользованиеUHealthComponent просто вешается и на игрока, и на врагов. Код написан один раз.
  3. Лёгкое расширение — новая механика (например, система шума для AI) — это просто новый компонент, который цепляется к актору без изменения существующего кода.

Interface Segregation (разделение интерфейсов) — это принцип, при котором объект не обязан знать о чужих возможностях. Он описывает только минимальный контракт: "что ты умеешь делать", без лишних деталей.

Аналогия

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

  • Оба устройства управляются пультом — но у каждого свой, заточенный под него
  • Телевизору не нужна кнопка "температура", кондиционеру — кнопка "канал"
  • Ты не обязан знать, как устроен телевизор внутри — тебе достаточно знать, что у него есть кнопки включить, переключить канал, громкость

Интерфейс — это и есть такой пульт. Минимальный набор кнопок, который нужен для взаимодействия. Что происходит внутри — тебя не касается.

Зачем нужен?

  1. Слабая связность — код взаимодействует не с конкретным классом, а с контрактом. Можно подменить реализацию, не меняя того, кто её использует.
  2. Один интерфейс — много реализаций — дверь, ящик, NPC могут быть разными объектами, но все реализуют IInteractable. Код игрока работает одинаково с любым из них.
  3. Минимальная зависимость — компоненты знают друг о друге ровно столько, сколько нужно. Не больше.

Observer (наблюдатель) — объект сообщает другим что что-то произошло, не зная кто именно будет слушать. Подписался — получаешь уведомления, отписался — нет. Источник об этом даже не знает.

Аналогия

Представь YouTube-канал:

  • Автор просто выкладывает видео — он не знает кто подписан
  • Подписчики сами решают подписываться или нет
  • Вышло видео — все подписчики получили уведомление автоматически, автор никого не обзванивал

Подписалось ещё сто человек — автор ничего не менял, всё работает само.

Зачем нужен?

  • Слабая связность — источник события не знает ничего о тех кто реагирует. Добавляешь новых слушателей не трогая источник.
  • Один сигнал — много реакций — враг умер, и на это реагируют сразу UI, счётчик убийств и система лута. Каждый сам подписывается.
  • Никакого опроса — не нужно каждый кадр проверять "а не умер ли враг?". Событие прилетит само когда нужно.

Зачем нужен?

  1. Слабая связность — код взаимодействует не с конкретным классом, а с контрактом. Можно подменить реализацию, не меняя того, кто её использует.
  2. Один интерфейс — много реализаций — дверь, ящик, NPC могут быть разными объектами, но все реализуют IInteractable. Код игрока работает одинаково с любым из них.
  3. Минимальная зависимость — компоненты знают друг о друге ровно столько, сколько нужно. Не больше.

Coordinator (координатор) — это как понятно из названия, объект, который не делает работу сам, а управляет взаимодействием между несколькими подсистемами. В моем прокте такими объектами выступают, сложные системы по типу дверей или того же персонажа, которые иначе могут превратиться в God Object что плохо и для чтения и для работы и для контекста нейросетей, у меня в целом все объекты и классы строяттся так, что бы избегать хардкода и всего что с ним связанно.

Аналогия

Представь диспетчера в аэропорту:

  • Он не пилотирует самолёты
  • Он не заправляет их
  • Он не продаёт билеты

Но без него самолёты не знают, когда взлетать, кто заходит на посадку и кто кому мешает. Диспетчер знает о всех сразу и координирует их действия.

Зачем нужен?

  1. Разгрузка компонентов — каждая система делает своё дело и не лезет в чужое. Координатор берёт на себя логику "кто с кем и когда".
  2. Единая точка управления — вместо того чтобы системы общались друг с другом напрямую и запутывались, все вопросы идут через координатора.
  3. Легко контролировать сложное поведение — например, чтобы враги не атаковали все одновременно.

Data-Driven — это подход, при котором поведение объектов настраивается не в коде, а в данных. Хочешь поменять урон оружия — не лезешь в C++, просто меняешь значение в файле. Получается что бы ты не лез в код или блюпринт, создаешь Data Asset файл с параметрами которые можно менять без компиляции, а многие из них можно менять даже в рантайме, у меня это довольно важная часть, т.к. тот же геймфил настравиать крайне комфортно

Список моих Data Driven компонентов
Содержание Data Asset

Аналогия

Представь рецепт в ресторане:

  • Повар готовит по рецепту — он не меняется
  • Но шеф-повар может поменять рецепт: больше соли, другой соус, новый ингредиент
  • Повар об этом даже не знает — он просто читает новый рецепт и готовит

Код — это повар. DataAsset — это рецепт. Логика одна, данные меняются.

Зачем нужен?

  1. Не трогаешь код — баланс, звуки, дальность атаки, урон — всё меняется без перекомпиляции.
  2. Легко добавлять контент — новое оружие это не новый класс, а новый DataAsset с заполненными полями.
  3. Дружит с нейронкой — агенту не нужно лезть в логику, чтобы добавить новый предмет. Просто создаёт новый файл данных.

Ну я думаю с базой, разобрались, перейдем к ФУНДАМЕНТУ кода, давайте с ходу определим что мы стараемся не говнокодить, определять мы это будем с нейронкой, добавим в рулсы ей такое:

  1. NO MAGIC NUMBERS — никакого хардкода с установленными значениями прямо в коде. Например вместо того чтобы писать float movementSpeed = 10.f прямо в методе — выносим это в Constants файл и обращаемся к нему: movementSpeed = MOVEMENT_SPEED. Чисто, красиво, не нужно бегать по тысяче строк в поисках одной цифры — и гарантия что нигде в проекте не окажется 10.f в одном месте и 10.5f в другом.
  2. АНГЛИЙСКИЙ ЯЗЫК КОММЕНТАРИЕВ — я вот не озоботился прописать это сразу и теперь где-то английский, а где-то русский, так что сразу обозначьте этот момент. Проект на английском, нейронка лучше понимает английский контекст, и не будет каши когда код читает кто-то другой
  3. SOFT POINTERS ДЛЯ АССЕТОВ — Data Assets всегда через TSoftObjectPtr, никаких TObjectPtr. Hard pointer держит ассет в памяти всегда, даже когда он не нужен. Soft pointer загружает только тогда, когда реально используется — это важно когда ассетов много.
  4. UPROPERTY/UFUNCTION МАКРОСЫ — нейронка часто забывает их или ставит неправильные спецификаторы. Без UPROPERTY переменная невидима для Blueprint и не защищена от сборщика мусора. Без UFUNCTION метод недоступен из Blueprint вообще.
  1. Debug Code — всё в #if WITH_EDITORчто бы при сборке билда, все это игнорировалось компилятором
  2. ПОРЯДОК ИНКЛЮДОВ*.generated.h всегда последний. IntelliSense и нейронки тащат инклюды наверх автоматически — проверяй, иначе проект не скомпилируется.
  3. SUPER:: ВЫЗОВЫ — всегда вызывай Super:: в переопределённых методах жизненного цикла. Нейронка легко забывает, логика родительского класса тихо ломается и найти это потом хер найдешь.
  4. ДОКУМЕНТАЦИЯ — после любых изменений в архитектуре или системах обновляй ARCHITECTURE.md и соответствующие гайды. Нейронка работает с тем что написано, а не с тем что у тебя в голове.
  5. NO TICK БЕЗ ПРИЧИНЫ — по дефолту bCanEverTick = false на всех компонентах и акторах. Tick вызывается каждый кадр — это дорого, и в большинстве случаев просто не нужен. Вместо него три альтернативы:
  • Делегаты — если нужно среагировать на событие
  • Таймеры — если нужно что-то сделать через время или с интервалом
  • Кеш — если нужна ссылка на компонент, кешируем в BeginPlay а не ищем каждый кадр
Все рулсы должны быть на английском*
Есть еще такой момент с документами, сам ARCHITECTURE.md со временем раздувается шо ппц, поэтому можно создать компактный контекст для агента вместо того чтобы скармливать весь ARCHITECTURE.mdможно для определенной задачи дать: ARCH_AI_Map.md — быстрый обзор, зависимости, ссылки на файлы ARCH_AI_CPP.md — для работы с C++ ARCH_AI_Content.md — для работы с контентом Агент берёт только нужный файл под задачу — контекст не раздувается

Я думаю что этого достаточно для грамотной архитектуры и стабильной работы с минимальным рефакторингом в будущем, и работой с отдельно взятой системой не перегружая контекст — большинство наших систем довольно изолированные. Так что пожалуй для старта этого более чем достаточно, в тг прикреплю файл с своей архитектурой для примера и файлы со скиллами для написания кода для UE, чтобы вы не искали отдельно или чтобы нейронка не грузила контекст с постоянной прогонкой текущих скиллов.