Danil Iarosh cefb2fd345 Build initial vertical platformer foundation
Подробные изменения:
- Добавлены сцены Bootstrap, MainMenu и Game с текущей вертикальной 9:16 структурой проекта.
- Добавлены runtime-скрипты gameplay-архитектуры: DI через Extenject, спавнер платформ, прогрессия этапов, one-way платформы, игрок, камера и оборудование JumpPad.
- Добавлены ScriptableObject-этапы сложности, weighted-настройки платформ, оборудования, противников и collectibles.
- Добавлены UI Toolkit ресурсы главного меню, preview-материалы, шрифты, спрайты, анимации и игровые prefab-ассеты.
- Перенесена и оформлена новая COM-система для работы с аппаратными портами и StreamingAssets.
- Обновлены проектные гайды, AGENTS.md, правила Git/Git LFS, .gitignore, Unity Packages и ProjectSettings.
- Удалён локальный skill start-required-mcp-servers из репозитория.
2026-05-11 07:41:14 +03:00

461 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Собственные надстройки и абстракции над Unity-классами
## Зачем это нужно
Unity даёт много удобных API, но если использовать их напрямую везде, проект быстро получает:
- жёсткую привязку к `MonoBehaviour`
- сложный unit testing
- размытые границы между логикой и инфраструктурой
- трудности с заменой реализации
Поэтому для прикладного проекта полезно строить собственные надстройки над Unity API:
- обёртки
- фасады
- адаптеры
- базовые интерфейсы
- прикладные сервисы
Для `Danro Jump` это особенно важно из-за сочетания:
- gameplay-систем
- UI
- сцен
- `COM`-оборудования
- призовой логики
- сервисного режима
## Legacy как источник абстракций
`Assets/Temp/Legacy` содержит много старых `MonoBehaviour`, managers, UI-контроллеров и editor extensions. Их стоит рассматривать как материал для выделения новых абстракций, а не как готовый код для прямого переноса.
Особенно полезные источники:
- `Assets/Temp/Legacy/Scripts/Controls/Inputs/ComSystem` — аппаратный протокол, handshake, роли устройств, витрина
- `Assets/Temp/Legacy/Scripts/Managers/PaymentManager.cs` — кредиты, free play, старт игры
- `Assets/Temp/Legacy/Scripts/Managers/PrizeSystemManager.cs` — prize orchestration
- `Assets/Temp/Legacy/Scripts/Settings` — сервисные настройки
- `Assets/Temp/Legacy/Scripts/UI/Modal` — модальные окна призов и настроек
- `Assets/Temp/Legacy/Scripts/Editor` — editor tooling для настроек и иерархии
- `Assets/Temp/Legacy/Scripts/Utilities` — логирование, `QR`, телефонные форматы
Перед переносом старого класса нужно ответить на вопрос: какую стабильную обязанность он выполняет в новом проекте?
## Что считать надстройкой
Под надстройкой над Unity-классами в этом гайде понимаются не только editor extensions, но и прикладные архитектурные слои над Unity API.
Чаще всего это:
- интерфейс над Unity API
- сервис над статическим классом
- базовый runtime-компонент
- конфигурационный слой на `ScriptableObject`
- editor tooling для удобства настройки
## Когда надстройка действительно нужна
Надстройка оправдана, если:
- Unity API мешает тестировать код
- доступ к системе идёт из слишком многих мест
- нужно заменить поведение в будущем
- есть разница между доменной логикой и конкретной Unity-реализацией
Надстройка не нужна, если:
- это одноразовый локальный вызов
- абстракция ничего не упрощает
- она только дублирует Unity API без пользы
## Хорошие кандидаты на абстракции в Unity
### SceneManager
Вместо прямого доступа:
```csharp
SceneManager.LoadSceneAsync("Gameplay");
```
лучше:
```csharp
public interface ISceneFlowService
{
UniTask OpenGameplayAsync();
}
```
### Time
Вместо прямого `Time.deltaTime` везде можно иметь:
```csharp
public interface ITimeProvider
{
float DeltaTime { get; }
float UnscaledDeltaTime { get; }
}
```
Это упрощает тестирование и управление скоростью.
### Random
Вместо прямого `UnityEngine.Random`:
```csharp
public interface IRandomService
{
int Range(int minInclusive, int maxExclusive);
float Value();
}
```
### PlayerPrefs
Для project-scale логики лучше не разбрасывать вызовы `PlayerPrefs` напрямую.
Лучше:
```csharp
public interface ILocalSettingsStorage
{
void SetInt(string key, int value);
int GetInt(string key, int defaultValue = 0);
}
```
### COM-порт и внешние устройства
Это один из самых сильных кандидатов на абстракцию в `Danro Jump`.
Не стоит смешивать:
- порт
- протокол
- игровую логику
- сервисную диагностику
Лучше разделять:
- `IComPortGateway`
- `IDeviceProtocol`
- `IPrizeShowcaseGateway`
- `IHardwareDiagnosticsService`
Legacy reference:
- `ComNativeWrapper` показывает границу с native `ComNative`
- `ComHub` и `ComDeviceClient` показывают модель нескольких устройств
- `PrizeStandController` показывает прикладной протокол витрины
- `ComInputToInputSystemAdapter` показывает, как аппаратный ввод можно отдавать в Unity Input System
## Базовые Unity-типы, над которыми чаще всего строят свои слои
### MonoBehaviour
`MonoBehaviour` хорош как Unity lifecycle entry point, но плох как контейнер всей бизнес-логики.
Практика:
- `MonoBehaviour` лучше использовать как view, adapter или composition point
- доменную и прикладную логику лучше выносить в обычные C# классы
Хорошая схема:
```csharp
public sealed class PrizeScreenView : MonoBehaviour
{
[SerializeField] private Button _claimButton;
public event Action ClaimClicked;
private void Awake()
{
_claimButton.onClick.AddListener(() => ClaimClicked?.Invoke());
}
}
```
А логика живёт отдельно:
```csharp
public sealed class PrizeScreenPresenter
{
private readonly PrizeScreenView _view;
private readonly IPrizeService _prizeService;
}
```
### ScriptableObject
`ScriptableObject` хорошо подходит для:
- конфигурации
- таблиц баланса
- статических наборов данных
- editor-friendly настроек
Для `Danro Jump` через `ScriptableObject` удобно хранить:
- стоимость игры
- настройки сложности
- параметры призов
- таблицы выдачи
- конфигурацию аппаратных режимов
Практика:
- `ScriptableObject` хранит данные
- поведение и orchestration живут в сервисах
Legacy reference:
- `Assets/Temp/Legacy/Scripts/Settings/MainSettingsDatabase.asset`
- `Assets/Temp/Legacy/Scripts/Settings/Core/SettingsDatabase.cs`
- `Assets/Temp/Legacy/Scripts/ScriptableObjects/MainSettings.asset`
При переносе этих материалов важно не тащить старую базу настроек как единственный источник истины без ревизии. Нужно выделить новые группы настроек: оплата, сложность, призы, `QR`, витрина, диагностика.
### EditorWindow / CustomEditor / PropertyDrawer
Если под "надстройками" понимать editor-расширения, то это тоже важный слой.
Они полезны, когда нужно:
- удобнее настраивать сервисные параметры
- валидировать конфигурацию
- упростить работу дизайнеру или оператору
- сделать собственные окна диагностики
Типичные инструменты:
- `CustomEditor`
- `PropertyDrawer`
- `EditorWindow`
- `MenuItem`
Legacy reference:
- `Assets/Temp/Legacy/Scripts/Editor/SettingsDatabaseEditor.cs`
- `Assets/Temp/Legacy/Scripts/Editor/SettingEntryDrawer.cs`
- `Assets/Temp/Legacy/Scripts/Editor/HierarchyBulkRenamerWindow.cs`
- `Assets/Temp/Legacy/Scripts/Editor/MouseWorldPositionOverlay.cs`
Эти инструменты можно использовать как идеи для нового tooling, но лучше переподключать их к новой модели настроек и новым namespace.
## Полезные паттерны над Unity API
### Adapter
Используется, когда нужно скрыть конкретный Unity API за интерфейсом.
Пример:
- `SceneManager` -> `ISceneFlowService`
- `PlayerPrefs` -> `ILocalSettingsStorage`
- `AudioSource` -> `IAudioPlaybackService`
### Facade
Используется, когда несколько Unity-подсистем должны выглядеть как одна прикладная операция.
Пример:
- показать экран награды
- проиграть анимацию
- активировать витрину
- ожидать подтверждение выдачи
Вместо разброса логики по сцене можно иметь:
```csharp
public interface IRewardPresentationFacade
{
UniTask PresentRewardAsync(RewardResult rewardResult);
}
```
### Wrapper around static API
Подходит для:
- `Time`
- `Random`
- `PlayerPrefs`
- `SceneManager`
- `Application`
### Presenter / Controller layer
Подходит для UI:
- view знает только про кнопки и поля
- presenter знает про бизнес-реакции
Legacy UI-классы из `Assets/Temp/Legacy/Scripts/UI` чаще всего нужно переносить именно через этот паттерн:
- старый `MonoBehaviour` становится `View`
- бизнес-решения уходят в `Presenter` или application service
- прямые обращения к singleton-manager'ам заменяются интерфейсами
- переходы сцен уходят в `ISceneFlowService`
Например, `QRCodeModal` и `PrizeStandModal` стоит рассматривать как reference для будущих `QrRewardView` и `PrizeShowcaseView`, а не как финальные controllers.
## Что лучше не делать
Плохо:
- наследоваться от `MonoBehaviour` для всего подряд
- хранить бизнес-логику прямо в `Update`
- вызывать `SceneManager`, `PlayerPrefs`, `Random`, `Time` напрямую в доменных классах
- делать толстые `Manager`-классы, которые знают про всё
Хорошо:
- вводить точечные интерфейсы
- отделять runtime view от логики
- держать конфигурацию отдельно от поведения
- строить прикладные сервисы поверх Unity API
## Практические абстракции, которые особенно полезны для Danro Jump
### 1. Аппаратный слой
```csharp
public interface IPrizeShowcaseGateway
{
UniTask OpenAsync(int prizeSlotId);
}
```
Legacy mapping:
- `PrizeStandController` -> `IPrizeShowcaseGateway`
- `ComSystemManager` -> `IHardwareSessionService`
- `ComNativeWrapper` -> `IComNativePortApi`
### 2. QR-выдача
```csharp
public interface IQrRewardService
{
UniTask<QrRewardResult> CreateRewardCodeAsync(RewardRequest request);
}
```
Legacy mapping:
- `QRCodeUtils` -> `IQrTextureFactory`
- `QRCodeInfo` -> новая `QrRewardConfig`
- `QRCodePrize` -> `QrPrizeExecutor`
### 3. Баланс игры
```csharp
public interface IGameBalanceProvider
{
int GetSessionPrice();
float GetDifficultyMultiplier();
}
```
### 4. Сервисные настройки
```csharp
public interface IServiceSettingsService
{
void SetSessionPrice(int value);
void SetDifficulty(float value);
}
```
Legacy mapping:
- `SettingsManager` -> `IServiceSettingsService`
- `SettingsDatabase` -> `ISettingsDefinitionRepository`
- `SettingsUIGenerator` -> новый service mode presenter/view generator
### 5. Время и рандом
```csharp
public interface ITimeProvider
{
float DeltaTime { get; }
}
public interface IRandomService
{
int Range(int minInclusive, int maxExclusive);
}
```
## Когда делать editor-надстройки
Editor-расширения стоит делать, когда:
- ручная настройка через стандартный inspector становится слишком тяжёлой
- нужно валидировать таблицы призов
- нужно дать персоналу или дизайнеру безопасный интерфейс настройки
- нужно сделать диагностику подключённого оборудования
Хорошие кандидаты для editor tooling в этом проекте:
- окно сервисной конфигурации
- custom inspector для таблиц призов
- property drawer для настроек весов и диапазонов
- diagnostic window для `COM`-связи
## Когда остановиться и не переусложнять
Надстройки полезны, пока они:
- уменьшают связанность
- улучшают тестируемость
- делают архитектуру понятнее
Если абстракция:
- не скрывает ничего важного
- не даёт тестовой выгоды
- не помогает сопровождению
то она, скорее всего, лишняя.
## Рекомендуемый путь внедрения
1. Сначала выбрать legacy-подсистему из `Assets/Temp/Legacy`.
2. Выписать её реальные обязанности.
3. Вынести эти обязанности за новые интерфейсы.
4. Перенести только нужные модели данных.
5. Подключить новую реализацию через `Extenject`.
6. После этого при необходимости делать editor tooling.
Для `Danro Jump` разумный старт:
1. `SceneManager`
2. `PlayerPrefs`
3. `Time` и `Random`
4. аппаратные шлюзы
5. editor tooling для сервисных настроек
Для миграции legacy-кода разумный старт:
1. `COM` и витрина
2. призовой контур
3. сервисные настройки
4. `QR`-выдача
5. UI-модалки и admin screens
## Официальные и полезные источники
- https://docs.unity3d.com/6000.1/Documentation/Manual/class-ScriptableObject.html
- https://docs.unity3d.com/ja/2023.2/ScriptReference/ScriptableObject.html
- https://docs.unity3d.com/es/current/ScriptReference/CustomEditor.html
- https://docs.unity3d.com/ja/current/ScriptReference/PropertyDrawer.html
- https://docs.unity3d.com/es/current/ScriptReference/EditorWindow.html
- https://docs.unity3d.com/cn/2022.3/ScriptReference/MenuItem.html