SaveLoad
Namespace: Vortex.Sdk.UIs.SaveLoad
Assembly: ru.vortex.sdk.game.uis.saveload
Активация
Пакет подключается через SdkSettingsSystem:
- Тоггл:
saveLoadWrapperSdkв инспекторе ассетаSdkSettings - Define-символ:
USING_VORTEX_SAVE_LOAD_WRAPPER - Меню:
Tools → Vortex → Configs → SDK Settings
При выключенном тоггле define снимается из PlayerSettings, и пакет не компилируется (asmdef содержит defineConstraints: ["USING_VORTEX_SAVE_LOAD_WRAPPER"]). Канон активации — Vortex/Sdk/SdkSettingsSystem/README.ru.md.
Назначение
UI-обёртка над SaveController (Core SaveSystem): представления списка сейвов, отдельного слота, всплывашка завершения сохранения, а также хэндлеры кнопок Save/Load/Remove. Дополнительно — захват скриншота сцены через многослойный композитинг камер и канвасов, превью и тип сейва как метаблоки сводки (SaveSummary.Meta).
Возможности:
- Список сейвов (
SaveListView) с пулом слотов и асинхронным восстановлением превью - Фильтры списка по метаданным сводки: ключ метаблока + регулярное выражение для значения; задаются в инспекторе, из кода (
SetFilter/ClearFilters/Refresh) или хэндлеромSaveListFilterHandler - Слот-карточка (
SaveView) с подсветкой фокуса, шаблоном имениauto_N/manual_Nи локализованным текстом - Кнопки Save/Load/Remove как
MonoBehaviour-хэндлеры наUIComponent - Многослойный захват экрана (
CameraCaptureHandler) с src_over блендингом черезHidden/AlphaBlit - Превью сейва — метаблок
preview(SavePreviewController): снимается сам на начале каждого сохранения, JPEG Medium в base64. Старая форма — превью в имени через||— устарела и читается только для старых сейвов - Тип сейва — метаблок
kind(SaveKindController):auto/manual, точка сохранения передаёт его вSaveKindController.Save. Для старых сейвов тип берётся из префикса имени - Префиксы имени и значения типа:
auto,manual(SavingSystemConstants)
Вне ответственности:
- Хранение и сериализация сейвов (Core
SaveController) - Структура самого
SaveSummary(CoreSaveSystem) - Сама модель UI-кнопок и пула (
Vortex.Unity.UI.UIComponents,PoolSystem)
Зависимости
Core
Vortex.Core.SaveSystem.Bus—SaveController(Save,Load,Remove,GetIndex,AddMeta,OnSaveRequest,OnSaveComplete,OnLoadComplete,OnRemove,GetNumberLastSave). Операции идут через очередьSaveController(см. README Core SaveSystem)Vortex.Core.SaveSystem.Abstraction—SaveSummary(GetMeta,SetMeta)Vortex.Core.System.Abstractions—IDataStorageVortex.Core.Extensions.LogicExtensions—Base64ToTexture,TextureToBase64,TextureEncodingRulesVortex.Core.Extensions.ReactiveValues—StringDataVortex.Core.Extensions.DefaultEnums—SwitcherStateVortex.Core.LocalizationSystem—Translate
Unity
Vortex.Unity.UI.UIComponents—UIComponent(SetAction,SetText,SetSwitcher)Vortex.Unity.UI.PoolSystem—PoolVortex.Unity.UI.Misc—DataStorageVortex.Unity.UI.TweenerSystem—TweenerHubVortex.Unity.AppSystem.System.TimeSystem—TimeController.Call/RemoveCallVortex.Unity.LocalizationSystem—[LocalizationKey]Vortex.Unity.EditorTools.Attributes—[ClassFilter],[AutoLink]
Sdk
Vortex.Sdk.SdkSettingsSystem— partialSdkSettings(тоггл активации)
External
- UniTask —
UniTask,UniTask.WaitForEndOfFrame,Forget - Sirenix Odin Inspector —
[Button] - Шейдер
Hidden/AlphaBlit(Always Included Shaders)
Использование
Сцена со списком сейвов
- На корне UI разместить
SaveListViewсо ссылками наPool(префаб слота соSaveView) иDataStorageдля текущего фокуса. - На префабе слота:
SaveViewс[AutoLink]на источникIDataStorage, ссылками наslotButton(UIComponent),slotName,timestamp,RawImage slotImageи ключи локализацииautoSavePattern/manualSavePattern. - Кнопки
Load/Remove:LoadGameHandler/RemoveSaveGameHandlerс тем жеDataStorage.
Фильтры списка по метаданным
Фильтр — ключ метаблока сводки и регулярное выражение для его значения. Значение сверяется с выражением целиком (якоря ^…$ добавляются сами, регистр учитывается). В списке остаются сейвы, прошедшие все фильтры; без фильтров — все сейвы.
Отсутствующий метаблок считается пустой строкой — так одно выражение покрывает и сейвы, записанные до появления метаданных:
| key | pattern | В списке |
|---|---|---|
gameMode |
Main| |
основная игра и старые сейвы без маркера |
gameMode |
Dlc1 |
только DLC1 |
gameMode |
Dlc.* |
любые DLC |
kind |
.+ |
только сейвы с записанным типом |
gameMode |
.* |
все — фильтр ничего не отсекает |
Для kind пустое значение у старого сейва значит «тип не записан», а не «ручной»: manual| пропустит и старые автосейвы. Тип старого сейва различим только по префиксу имени (SaveKindController.GetKind), фильтр же смотрит в метаданные.
Три способа задать фильтры:
- Инспектор
SaveListView— массивfilters. - Хэндлер
SaveListFilterHandler— ссылка наSaveListView, массив{key, pattern}и флагclearFilters(по умолчанию включён: сначала снимает фильтры списка, в т.ч. из его инспектора). На включении выставляет фильтры и перестраивает список. Порядок включения со списком не важен: включённый список перестроится, выключенный соберётся при включении уже с фильтрами. - Из кода:
saveList.ClearFilters();
saveList.SetFilter(GameModeSaveMeta.MetaKey, "Main|"); // прежние фильтры с этим ключом заменяются одним
saveList.SetFilter(SaveKindController.MetaKey, "manual|");
saveList.Refresh(); // SetFilter/ClearFilters список не перестраивают
Фильтры, заданные из кода или хэндлером, меняют экземпляр в рантайме и в префаб не попадают.
Кнопка «Сохранить»
// На GameObject с UIComponent (кнопкой) добавить SaveGameHandler.
// Имя сейва: manual_N; в сводке — метаблоки preview (снимок экрана) и kind = manual.
Своя точка сохранения
// Конец кадра обязателен: превью снимается синхронно на запросе сохранения (SaveController.OnSaveRequest).
// Ждать без привязки к объекту: корутина на выключенном GO умрёт, и ожидание не завершится
await UniTask.WaitForEndOfFrame();
// С типом — метаблок kind будет записан
var guid = await SaveKindController.Save(SavingSystemConstants.AutoName, $"{SavingSystemConstants.AutoName}_{day}");
// В обход SaveKindController — сейв без типа (превью и прочие метаблоки пишутся как обычно)
var guid2 = await SaveController.Save("custom");
Сохранение встаёт в очередь SaveController и может исполниться позже, но превью, тип и режим — снимок момента запроса: они пишутся на OnSaveRequest и едут вместе с операцией. null — запрос отклонён (идёт или ждёт загрузка), отменён удалением этого сейва или не удался. Новый сейв очередь не дедуплицирует: защиту от повторного клика держит сама точка сохранения (как SaveGameHandler).
Захват экрана
// На каждой камере / канвасе разместить CameraCaptureHandler,
// проставить priority (меньше — ниже).
// В Always Included Shaders добавить Hidden/AlphaBlit.
await UniTask.WaitForEndOfFrame();
var screenshot = CameraCaptureHandler.Capture();
// ... использовать ...
UnityEngine.Object.Destroy(screenshot);
Превью и тип существующего сейва
SaveSummary summary = ...;
Texture2D preview = SavePreviewController.GetPreview(summary); // метаблок preview, у старых сейвов — из имени; либо null
string kind = SaveKindController.GetKind(summary); // метаблок kind, у старых сейвов — префикс имени; либо null
string pureName = summary.GetSavePureName(); // у старых сейвов отрезает превью
Метаблоки preview и kind выключаются кнопками в SaveSettings (Unity SaveSystem, блок «Метаданные сейва»). Кнопки и ключи (SaveSettings.PreviewMetaKey, SaveSettings.KindMetaKey) объявляет partial SaveSettingsExt/SaveSettings.SaveLoad.cs (asmref в ru.vortex.unity.settings, под #if USING_VORTEX_SAVE_LOAD_WRAPPER). Выключенное превью SavePreviewController не снимает; выключенный тип отбрасывает драйвер.
Архитектура
SaveLoad/
├── SavePreviewController.cs ← метаблок preview: снимок на запросе сохранения, чтение (с запасным путём из имени)
├── SaveKindController.cs ← метаблок kind: Save(kind, name), GetKind (с запасным путём из имени)
├── SaveNameFormatter.cs ← отображаемое имя: шаблон auto_N / manual_N с локализацией
├── SavingSystemConstants.cs ← префиксы и типы AutoName/ManualName
├── AlphaBlit.shader ← Hidden/AlphaBlit (src_over)
├── DefineSettings/
│ ├── SdkSettings.SaveLoad.cs ← partial-кусок SdkSettings (тоггл)
│ └── sdk.settings.system.ext.asmref
├── SaveSettingsExt/
│ ├── SaveSettings.SaveLoad.cs ← partial SaveSettings: ключи и кнопки метаблоков preview/kind
│ └── ru.vortex.unity.settings.asmref
├── Handlers/
│ ├── CameraCaptureHandler.cs ← многослойный захват экрана
│ ├── LoadGameHandler.cs ← кнопка загрузки
│ ├── RemoveSaveGameHandler.cs ← кнопка удаления
│ ├── SaveListFilterHandler.cs ← фильтры списка сейвов на включении
│ └── SaveGameHandler.cs ← кнопка сохранения (manual)
├── Models/
│ └── SaveSlotData.cs ← враппер SaveSummary + lazy Texture2D, IDisposable
├── Views/
│ ├── SaveCompleteView.cs ← всплывашка о новом сейве
│ ├── SaveListView.cs ← список сейвов на Pool + фильтры по метаданным
│ └── SaveView.cs ← карточка одного слота
└── ru.vortex.sdk.game.uis.saveload.asmdef
Компоненты
| Класс | Тип | Назначение |
|---|---|---|
SavePreviewController |
static class |
Метаблок MetaKey = "preview". Подписан на SaveController.OnSaveRequest: метаблок выключен — выход; иначе CameraCaptureHandler.Capture() → EncodePreview (JPEG Medium, base64) → SaveController.AddMeta, текстура уничтожается. GetPreview(SaveSummary) — из метаданных, иначе из имени по ||; битое превью — null, временная текстура уничтожается. GetSavePureName(this SaveSummary). GetSaveNameWithPreview(Texture2D, string) — [Obsolete], старая форма |
SaveKindController |
static class |
Метаблок MetaKey = "kind". Save(kind, name, guid?) держит тип только на время синхронной постановки запроса и пишет его на OnSaveRequest — тип едет с запросом через очередь; GetKind(SaveSummary) — из метаданных, иначе из префикса имени (auto/manual), иначе null |
SaveNameFormatter |
static class |
GetDisplayName(this SaveSummary, autoKey, manualKey): тип в имени распознан целиком (auto/manual) — локализованный шаблон с индексом (и хвост через «: »), иначе чистое имя как есть |
SavingSystemConstants |
static class |
Константы префиксов имени и значений типа: AutoName = "auto", ManualName = "manual" |
SaveSlotData |
class : IDisposable |
Враппер SaveSummary для IDataStorage. Guid, Summary, lazy Preview (Texture2D); Dispose уничтожает текстуру |
CameraCaptureHandler |
MonoBehaviour |
Один слой захвата (Camera или Canvas). Самореестр Handlers, priority (0–10), Render(RT), статический Capture() |
SaveGameHandler |
MonoBehaviour |
Привязка кнопки UIComponent к Save(). Повторный клик, пока свой сейв не завершён, игнорируется. Ждёт конец кадра (UniTask.WaitForEndOfFrame(), без привязки к объекту — для снимка превью), вызывает SaveKindController.Save(ManualName, "manual_N") |
LoadGameHandler |
MonoBehaviour |
Берёт SaveSlotData из IDataStorage, вызывает SaveController.Load(guid) |
RemoveSaveGameHandler |
MonoBehaviour |
Подписан на IDataStorage.OnUpdateLink, вызывает SaveController.Remove(guid). Публичный RemoveSave() (вход из UnityEvent) без данных — LogError и выход |
SaveListView |
MonoBehaviour |
Заполняет Pool слотами в порядке убывания UnixTimestamp, пропуская сейвы, не прошедшие фильтры filters (метаблок + регулярное выражение, AND). Хранит StringData _focused и кладёт текущий SaveSlotData в DataStorage. Перезаполнение по OnSaveComplete/OnLoadComplete/OnRemove с сохранением фокуса. Публичные SetFilter(key, pattern), ClearFilters(), Refresh(). Disposable через TimeController.Call |
SaveListFilterHandler |
MonoBehaviour |
На OnEnable выставляет фильтры {key, pattern} на пролинкованный SaveListView (с предварительным ClearFilters, если отмечен clearFilters) и вызывает Refresh() |
SaveView |
MonoBehaviour |
Карточка слота. Подписан на _focused.OnUpdateData, переключает SwitcherState.On/Off (фокуса может не быть). Имя — SaveNameFormatter.GetDisplayName |
SaveCompleteView |
MonoBehaviour |
Всплывашка по SaveController.OnSaveComplete. Прогоняет массив TweenerHub (Back→Forward), выводит имя последнего сейва (SaveNameFormatter.GetDisplayName) и Date |
SdkSettings (partial) |
— | Поле saveLoadWrapperSdk с [ToggleButton] и [DefineSymbol("USING_VORTEX_SAVE_LOAD_WRAPPER")] |
Контракт
Вход
- Слой захвата:
CameraCaptureHandlerс заданнымиcameraилиcanvasиpriority - UI-кнопки сохранения/загрузки/удаления:
UIComponentчерезSetAction - Список сейвов берётся из
SaveController.GetIndex()(IDictionary<string, SaveSummary>) - Передача выбранного слота между View и хэндлерами — через
IDataStorage(DataStorage-MonoBehaviour) с модельюSaveSlotData
Выход
- Сохранение:
SaveKindController.Save(kind, name)→SaveController.Save(name); в сводку — метаблокиpreviewиkindчерезSaveController.AddMeta - Загрузка/удаление:
SaveController.Load(guid)/SaveController.Remove(guid) - Превью слота:
RawImage.texture = SaveSlotData.Preview
Гарантии
SaveListViewфильтрует индекс до сортировки и сортирует по убываниюUnixTimestamp- Фокус
SaveListView— выбор пользователя, на него опираются загрузка и удаление:- при включении — самый свежий из прошедших фильтры;
Refresh()(смена фильтров) — прежний, если прошёл фильтры, иначе самый свежий;- сохранение, загрузка, удаление (в т.ч. фоновые, напр. автосейв) — прежний; пропал из списка — фокуса нет, а не самый свежий: открытое подтверждение не удалит другой сейв
- Слот в фокусе публикуется в
DataStorageсразу на сборке, до появления элементов — окна «фокуса нет» между сборками не остаётся SaveListView.Refresh()на выключенном представлении ничего не делает — список соберётся при включении- Пересборка, пришедшая во время построения списка, отменяет прежнюю сборку; отменённая состояние не трогает
- Выключение и включение
SaveListViewв одном кадре пересобирают список - Выражение фильтра компилируется один раз и пересобирается только при смене текста
SaveSlotData.Previewсоздаётся лениво,DisposeуничтожаетTexture2DSaveListView.Disposeотписывается от событий, отменяет CTS, чистит пул и слотыCameraCaptureHandler.Capture()сортирует активные хэндлеры поpriority(меньше — ниже), композитит через src_overCameraCaptureHandlerи при сбое рендера восстанавливает режим Overlay-канваса,targetTextureкамер иRenderTexture.active, освобождает временные RT и камеруSaveGameHandlerждёт конец кадра перед сохранением — снимок превью наOnSaveRequestидёт в консистентном пайплайне при вызове из EventSystem- Тип сейва не утекает в чужой запрос:
SaveKindController.Saveдержит его только на время синхронной постановки - Превью, тип и режим — снимок момента запроса, даже если сохранение исполнится позже из очереди
- Старые сейвы (превью и тип в имени) читаются через запасной путь
GetPreview/GetKind RemoveSaveGameHandlerпере-инициализируется наIDataStorage.OnUpdateLink
Ограничения
- Шейдер
Hidden/AlphaBlitобязан быть в Always Included Shaders (Project Settings → Graphics) - Один
CameraCaptureHandlerнесёт ровно одну сущность:cameraилиcanvas. При обоих null —LogWarning - Для
Canvas.ScreenSpaceCameraбезworldCamera—LogWarning, слой пропускается - Имя сейва не должно содержать подстроки
||:GetSavePureNameи запасной путьGetPreviewрежут по ней имя - Точка сохранения обязана дождаться конца кадра перед
Save: превью снимается синхронно наOnSaveRequest - Возвращённую
Capture()текстуру обязан уничтожить вызывающий код (Destroy)
Граничные случаи
| Ситуация | Поведение |
|---|---|
| Пустой индекс сейвов | SaveListView чистит пул, фокус не выставляется |
| Ни один сейв не прошёл фильтры | Как при пустом индексе |
| Невалидное регулярное выражение в фильтре | Ошибка в лог (ключ и текст выражения), фильтр не пропускает ни одного сейва |
| Несколько фильтров с одним ключом (в инспекторе) | Применяются все (AND); SetFilter по этому ключу оставит один |
SaveListFilterHandler без ссылки на SaveListView |
Ошибка в лог, фильтры не выставляются |
| Новый сейв (превью в метаданных) | GetPreview берёт метаблок preview, имя чистое |
Старый сейв: Name с ` |
|
| Нет ни метаблока, ни ` | |
Сохранение без активных CameraCaptureHandler |
Сейв пишется без метаблока preview |
Сохранение в обход SaveKindController |
Метаблока kind нет; GetKind пробует префикс имени |
GetKind у сейва с произвольным именем без метаблока |
null |
Имя слота не подходит под шаблон auto_N / manual_N (тип сравнивается целиком: autosave_3 — не автосейв) |
Выводится чистое имя без локализации |
IDataStorage без SaveSlotData |
LoadGameHandler и RemoveSaveGameHandler.RemoveSave пишут LogError и выходят; кнопка RemoveSaveGameHandler отвязывается |
| Сейв в фокусе удалён (в т.ч. фоново) | После пересборки фокуса нет; Load/Remove без выбора — LogError |
| Сохранение при идущей или ожидающей загрузке | SaveController.Save возвращает null; SaveGameHandler пишет LogError |
| Повторный клик «Сохранить», пока свой сейв в очереди | Игнорируется |
Превью выключено в SaveSettings |
Снимок не делается, сейв без метаблока preview |
| Битое превью (base64 не декодируется) | GetPreview возвращает null, временная текстура уничтожается |
Capture() без активных хэндлеров |
LogWarning, возврат null |
| Изменение размера экрана между кадрами | CameraCaptureHandler.Render пересоздаёт RenderTexture |
Canvas.ScreenSpaceOverlay |
Захват через временную ортографическую камеру с cullingMask = 1 << layer, режим/worldCamera/planeDistance восстанавливаются |
Disable SaveListView |
Dispose запланирован через TimeController.Call, отписки выполняются вне OnDisable-кадра; включение в том же кадре снимает отложенный Dispose и пересобирает список |
| Исключение при рендере слоя захвата | Канвас, камеры и RenderTexture.active восстанавливаются, временные ресурсы освобождаются, исключение уходит вызывающему |