Как я строю UE проект чтобы с ним мог работать и я, и нейронка
Давайте сразу определимся: мы используем гит для записи изменений в проекте, не ленитесь комментить изменения без нейронки, потому что комменты от нейронки вам потом ничего не дадут для понимания.
Создаём .gitignore, чтобы git игнорировал тяжёлые и ненужные файлы типа кешей, всяких объектов и прочего при коммите, обычно вот таких: Binaries, Build, Intermediate, Saved, DerivedDataCache и т.д. В целом я советую сделать это через ИИ агентов, пусть он посмотрит структуру проекта и сделает gitignore на его основе.
Наш проект будет модульным и больше напоминать фреймворк. Держим в уме, "а ничё тот факт что", контекст нейронок не бесконечный и при работе с какими-то классами и объектами, он может сразу переполняться и начинать сессию по новой, где снова будет читать всё заново. Так же если какие-то траблы с классами или системами, он не положит весь наш проект и в случае чего, такую проблемную хрень, можно будет легко изолировать и работать с ним отдельно
У агентов сейчас есть чекпоинты, и если контекст переполняется, он может продолжить работу с того, на чём остановился, но он уже не будет помнить именование объектов, например, и будет написано всё вразнобой, поэтому стараемся сохранять контекст как можно дольше.
Поэтому будем следовать несложным правилам:
- State Machine — управление состояниями.
- Component — декомпозиция функциональности в компоненты.
- Observer — делегаты для событий
- Interface Segregation — разенлннные интерфейсы
- Coordinator — координируют подсистемы
- Data-Driven — DataAssets для конфигурации оружия, предметов и т.д.
С документами у нас также довольно важная и сложная работа. Многие агенты имеют набор для работы с UE, который по дефолту что-то подобное включает, но тут я лучше вручную задам всё.
Мы создаём мапу проекта — это такой короткий контекст для нейронки или для людей, которым, не дай бог, придётся в этом всём разбираться. Обычно это выглядит вот так:
- Overview (Обзор)
- Название проекта, тип, движок
- Ключевые принципы архитектуры
- Диаграмма слоёв - Project Structure (Структура проекта)
- Дерево директорий
- Соглашения об именовании (Naming Conventions) - Core Systems (Основные системы)
- Описание главных систем (Character, Weapon, AI)
- Композиция компонентов - Design Patterns (Паттерны)
- Какие паттерны используются
- Примеры кода - Data Flow (Потоки данных)
- Как данные проходят через систему
- Диаграммы - Component Architecture
- Таблица компонентов и их responsibilities - Coding Standards (Стандарты кодирования)
- Стиль именования
- Правила - Extension Guidelines (Руководство по расширению)
- Как добавлять новые фичи
Контекст AI-агента не бесконечный. Когда ты начинаешь новую сессию и просишь добавить новую механику — агент ничего не знает о твоём проекте. Ты либо каждый раз объясняешь всё с нуля, либо скидываешь карту проекта в начале сессии — и агент сразу понимает структуру, паттерны, соглашения по именованию.
State Machine (конечный автомат) — это способ организации логики, когда объект может находиться в одном из нескольких состояний и переключаться между ними по определённым правилам.
Аналогия
- Дверь может быть в состояниях: Закрыта, Открыта, Заперта
- Из состояния "Закрыта" можно перейти в "Заперта" (повернуть ключ)
- Из "Заперта" можно перейти в "Открыта" (правильный ключ)
- Из "Открыта" можно перейти в "Закрыта"
Дверь не может быть одновременно "Открыта" и "Заперта" — только в одном состоянии. Каждое состояние имеет свою логику.
Зачем нужен?
- Чёткая логика — нет запутанных if-else.
- Нельзя сделать невозможное — персонаж не может бежать и целиться одновременно например.
- Легко добавить новое — просто создать новый класс состоянияю
Component Pattern — это главный структурный принцип: вместо того чтобы писать весь код персонажа в одном монолитном классе, функциональность разбивается на небольшие, сфокусированные компоненты, каждый из которых отвечает ровно за одну вещь. Так мы не раздуваем наш компонент, можем переиспользовать "модуль" в любом подходящем месте, а так-же просто отключить его за ненадабностью.
Так-же это позовляет работать с выделеным компонентом отдельно, т.е. опять-же нейронки не запутаются и не переполнит контекст.
Аналогия
- Камера отвечает только за съёмку
- Батарея отвечает только за питание
- Динамик отвечает только за звук
- Wi-Fi модуль отвечает только за связь
Зачем нужен?
- Изоляция изменений — правишь логику инвентаря, не боясь сломать камеру или здоровье персонажа.
- Переиспользование —
UHealthComponentпросто вешается и на игрока, и на врагов. Код написан один раз. - Лёгкое расширение — новая механика (например, система шума для AI) — это просто новый компонент, который цепляется к актору без изменения существующего кода.
Interface Segregation (разделение интерфейсов) — это принцип, при котором объект не обязан знать о чужих возможностях. Он описывает только минимальный контракт: "что ты умеешь делать", без лишних деталей.
Аналогия
Представь пульт от телевизора и пульт от кондиционера:
- Оба устройства управляются пультом — но у каждого свой, заточенный под него
- Телевизору не нужна кнопка "температура", кондиционеру — кнопка "канал"
- Ты не обязан знать, как устроен телевизор внутри — тебе достаточно знать, что у него есть кнопки включить, переключить канал, громкость
Интерфейс — это и есть такой пульт. Минимальный набор кнопок, который нужен для взаимодействия. Что происходит внутри — тебя не касается.
Зачем нужен?
- Слабая связность — код взаимодействует не с конкретным классом, а с контрактом. Можно подменить реализацию, не меняя того, кто её использует.
- Один интерфейс — много реализаций — дверь, ящик, NPC могут быть разными объектами, но все реализуют
IInteractable. Код игрока работает одинаково с любым из них. - Минимальная зависимость — компоненты знают друг о друге ровно столько, сколько нужно. Не больше.
Observer (наблюдатель) — объект сообщает другим что что-то произошло, не зная кто именно будет слушать. Подписался — получаешь уведомления, отписался — нет. Источник об этом даже не знает.
Аналогия
- Автор просто выкладывает видео — он не знает кто подписан
- Подписчики сами решают подписываться или нет
- Вышло видео — все подписчики получили уведомление автоматически, автор никого не обзванивал
Подписалось ещё сто человек — автор ничего не менял, всё работает само.
Зачем нужен?
- Слабая связность — источник события не знает ничего о тех кто реагирует. Добавляешь новых слушателей не трогая источник.
- Один сигнал — много реакций — враг умер, и на это реагируют сразу UI, счётчик убийств и система лута. Каждый сам подписывается.
- Никакого опроса — не нужно каждый кадр проверять "а не умер ли враг?". Событие прилетит само когда нужно.
Зачем нужен?
- Слабая связность — код взаимодействует не с конкретным классом, а с контрактом. Можно подменить реализацию, не меняя того, кто её использует.
- Один интерфейс — много реализаций — дверь, ящик, NPC могут быть разными объектами, но все реализуют
IInteractable. Код игрока работает одинаково с любым из них. - Минимальная зависимость — компоненты знают друг о друге ровно столько, сколько нужно. Не больше.
Coordinator (координатор) — это как понятно из названия, объект, который не делает работу сам, а управляет взаимодействием между несколькими подсистемами. В моем прокте такими объектами выступают, сложные системы по типу дверей или того же персонажа, которые иначе могут превратиться в God Object что плохо и для чтения и для работы и для контекста нейросетей, у меня в целом все объекты и классы строяттся так, что бы избегать хардкода и всего что с ним связанно.
Аналогия
Представь диспетчера в аэропорту:
Но без него самолёты не знают, когда взлетать, кто заходит на посадку и кто кому мешает. Диспетчер знает о всех сразу и координирует их действия.
Зачем нужен?
- Разгрузка компонентов — каждая система делает своё дело и не лезет в чужое. Координатор берёт на себя логику "кто с кем и когда".
- Единая точка управления — вместо того чтобы системы общались друг с другом напрямую и запутывались, все вопросы идут через координатора.
- Легко контролировать сложное поведение — например, чтобы враги не атаковали все одновременно.
Data-Driven — это подход, при котором поведение объектов настраивается не в коде, а в данных. Хочешь поменять урон оружия — не лезешь в C++, просто меняешь значение в файле. Получается что бы ты не лез в код или блюпринт, создаешь Data Asset файл с параметрами которые можно менять без компиляции, а многие из них можно менять даже в рантайме, у меня это довольно важная часть, т.к. тот же геймфил настравиать крайне комфортно
Аналогия
- Повар готовит по рецепту — он не меняется
- Но шеф-повар может поменять рецепт: больше соли, другой соус, новый ингредиент
- Повар об этом даже не знает — он просто читает новый рецепт и готовит
Код — это повар. DataAsset — это рецепт. Логика одна, данные меняются.
Зачем нужен?
- Не трогаешь код — баланс, звуки, дальность атаки, урон — всё меняется без перекомпиляции.
- Легко добавлять контент — новое оружие это не новый класс, а новый DataAsset с заполненными полями.
- Дружит с нейронкой — агенту не нужно лезть в логику, чтобы добавить новый предмет. Просто создаёт новый файл данных.
Ну я думаю с базой, разобрались, перейдем к ФУНДАМЕНТУ кода, давайте с ходу определим что мы стараемся не говнокодить, определять мы это будем с нейронкой, добавим в рулсы ей такое:
- NO MAGIC NUMBERS — никакого хардкода с установленными значениями прямо в коде. Например вместо того чтобы писать
float movementSpeed = 10.fпрямо в методе — выносим это вConstantsфайл и обращаемся к нему:movementSpeed = MOVEMENT_SPEED. Чисто, красиво, не нужно бегать по тысяче строк в поисках одной цифры — и гарантия что нигде в проекте не окажется10.fв одном месте и10.5fв другом. - АНГЛИЙСКИЙ ЯЗЫК КОММЕНТАРИЕВ — я вот не озоботился прописать это сразу и теперь где-то английский, а где-то русский, так что сразу обозначьте этот момент. Проект на английском, нейронка лучше понимает английский контекст, и не будет каши когда код читает кто-то другой
- SOFT POINTERS ДЛЯ АССЕТОВ — Data Assets всегда через
TSoftObjectPtr, никакихTObjectPtr. Hard pointer держит ассет в памяти всегда, даже когда он не нужен. Soft pointer загружает только тогда, когда реально используется — это важно когда ассетов много. - UPROPERTY/UFUNCTION МАКРОСЫ — нейронка часто забывает их или ставит неправильные спецификаторы. Без
UPROPERTYпеременная невидима для Blueprint и не защищена от сборщика мусора. БезUFUNCTIONметод недоступен из Blueprint вообще.
- Debug Code — всё в
#if WITH_EDITORчто бы при сборке билда, все это игнорировалось компилятором - ПОРЯДОК ИНКЛЮДОВ —
*.generated.hвсегда последний. IntelliSense и нейронки тащат инклюды наверх автоматически — проверяй, иначе проект не скомпилируется. - SUPER:: ВЫЗОВЫ — всегда вызывай
Super::в переопределённых методах жизненного цикла. Нейронка легко забывает, логика родительского класса тихо ломается и найти это потом хер найдешь. - ДОКУМЕНТАЦИЯ — после любых изменений в архитектуре или системах обновляй
ARCHITECTURE.mdи соответствующие гайды. Нейронка работает с тем что написано, а не с тем что у тебя в голове. - NO TICK БЕЗ ПРИЧИНЫ — по дефолту
bCanEverTick = falseна всех компонентах и акторах. Tick вызывается каждый кадр — это дорого, и в большинстве случаев просто не нужен. Вместо него три альтернативы:
- Делегаты — если нужно среагировать на событие
- Таймеры — если нужно что-то сделать через время или с интервалом
- Кеш — если нужна ссылка на компонент, кешируем в
BeginPlayа не ищем каждый кадр
Все рулсы должны быть на английском*
Есть еще такой момент с документами, самARCHITECTURE.mdсо временем раздувается шо ппц, поэтому можно создать компактный контекст для агента вместо того чтобы скармливать весьARCHITECTURE.mdможно для определенной задачи дать:ARCH_AI_Map.md— быстрый обзор, зависимости, ссылки на файлыARCH_AI_CPP.md— для работы с C++ARCH_AI_Content.md— для работы с контентом Агент берёт только нужный файл под задачу — контекст не раздувается
Я думаю что этого достаточно для грамотной архитектуры и стабильной работы с минимальным рефакторингом в будущем, и работой с отдельно взятой системой не перегружая контекст — большинство наших систем довольно изолированные. Так что пожалуй для старта этого более чем достаточно, в тг прикреплю файл с своей архитектурой для примера и файлы со скиллами для написания кода для UE, чтобы вы не искали отдельно или чтобы нейронка не грузила контекст с постоянной прогонкой текущих скиллов.