RollbackSystem
Namespace: Vortex.Unity.UI.RollbackSystem, Vortex.Unity.UI.RollbackSystem.Sources
Сборка: ru.vortex.unity.ui.rollback
Назначение
Откат несохранённых изменений экрана. RollbackHandler объединяет источники отката, показывает признак «есть несохранённые изменения» и ведёт два пути — «Сохранить» и «Откатить». Выключение экрана — это «Откатить».
Возможности:
- Любая область отката — наследник
RollbackSource; хэндлер не знает, что именно откатывает - Реактивность: источники сами сообщают об изменениях, покадровых проверок нет
- Точное сравнение: «поменял и вернул» — изменений нет
- Запрос на выход через
TweenerHub, выход без изменений —UnityEvent - Готовый источник для выпадающих списков и слайдеров с маркером
RollbackControlна контроле
Вне ответственности:
- Сворачивание запроса на выход и закрытие окна после выбора игрока — вёрстка
- Хранение данных: источник возвращает значения через тот API, которым их меняет игрок
- Сравнение с заводскими настройками — у каждой системы своё (например,
ChangesStateHandlerв Rebind)
Зависимости
| Зависимость | Назначение |
|---|---|
ru.vortex.unity.ui.misc |
UIStateSwitcher, TweenerHub, DropDownComponent |
ru.vortex.unity.app |
TimeController — фиксация точки и регистрация контролов в конце кадра |
ru.vortex.extensions |
SwitcherState |
ru.vortex.extenums, ru.vortex.unity.extenums |
Перегрузки UIStateSwitcher.Set |
ru.vortex.unity.editortools |
[AutoLink] в RollbackControl |
UnityEngine.UI |
Slider |
| Odin Inspector | [SerializeReference]-список источников, строка типа источника |
Архитектура
RollbackSystem/
├── RollbackSource.cs # абстракция источника
├── RollbackHandler.cs # хэндлер экрана
└── Sources/
├── UIControlsRollback.cs # выпадающие списки и слайдеры
└── RollbackControl.cs # маркер контрола под откат
RollbackHandler (корень экрана)
├── [SerializeReference] RollbackSource[] ← UIControlsRollback, RebindRollback, свои источники
├── UIStateSwitcher (SwitcherState) ← Off — изменений нет, On — есть
├── TweenerHub ← запрос на выход
└── UnityEvent onExit ← выход без изменений
Жизненный цикл
| Событие | Хэндлер | Источники |
|---|---|---|
OnEnable |
TweenerHub.Back(true), свитчер Off, фиксация точки в конце кадра |
Подписка на свои события |
| Изменение данных | Пересчёт общего признака, свитчер | Пересчёт своего признака по событию; уведомление, только если признак сменился |
Save() |
Новая точка у всех источников, свитчер Off |
Запоминают текущее состояние |
Rollback() |
Откат у всех, затем новая точка, свитчер Off |
Возвращают состояние точки |
CallExit() |
Изменения есть — TweenerHub.Forward(), нет — onExit |
— |
OnDisable |
При изменениях — Rollback() |
Отписка от событий |
Точка отката фиксируется в конце кадра включения, а не сразу в OnEnable: к этому моменту контролы успевают получить свои значения. После отката точка фиксируется заново — состояние после отката считается исходным, даже если откат вернул данные не в точности.
Save() и Rollback() твинер не трогают: как свернуть запрос и закрыть окно, решает вёрстка.
API
RollbackHandler
handler.Save(); // текущее состояние — новая точка отката
handler.Rollback(); // откат к точке, затем новая точка
handler.CallExit(); // изменения есть — запрос на выход, нет — onExit
bool changed = handler.HasChanges; // есть несохранённые изменения
var source = handler.GetSource<UIControlsRollback>(); // первый источник типа; null — нет
| Поле Inspector | Тип | Описание |
|---|---|---|
sources |
RollbackSource[] |
Источники отката ([SerializeReference]) |
switcher |
UIStateSwitcher (SwitcherState) |
Off — изменений нет, On — есть |
exitRequest |
TweenerHub |
Запрос на выход |
onExit |
UnityEvent |
Выход без изменений |
Свой источник
[Serializable]
public class VolumeRollback : RollbackSource
{
private float _checkpoint;
protected override void Subscribe() => Audio.OnVolumeChanged += OnChanged;
protected override void Unsubscribe() => Audio.OnVolumeChanged -= OnChanged;
protected override void MakeCheckpoint() => _checkpoint = Audio.Volume;
protected override bool DiffersFromCheckpoint() => !Mathf.Approximately(Audio.Volume, _checkpoint);
protected override void Restore() => Audio.SetVolume(_checkpoint);
private void OnChanged(float value) => Refresh(); // пересчёт признака — только по событию
}
| Член | Кто вызывает | Что делает |
|---|---|---|
Subscribe() / Unsubscribe() |
хэндлер (включение / выключение) | Подписка на события своих данных; в обработчиках — Refresh() |
MakeCheckpoint() |
хэндлер (включение, «Сохранить», после отката) | Запомнить текущее состояние |
DiffersFromCheckpoint() |
Refresh() |
Сравнение с точкой; вызывается только по событию |
Restore() |
хэндлер («Откатить») | Вернуть состояние точки |
Refresh() |
реализация | Пересчитать признак и уведомить хэндлер, если он сменился |
IsInitialized |
реализация | Хэндлер включён, источник подписан — для регистрации данных на лету |
HasChanges |
хэндлер | Кэшированный признак изменений |
До фиксации точки Refresh() признак не поднимает, а Rollback() не вызывает Restore(). В списке хэндлера каждый источник показывает строку с именем своего типа — у источников без сериализуемых полей элемент списка иначе пуст.
Источник UI-контролов
UIControlsRollback откатывает выпадающие списки (DropDownComponent) и слайдеры (Slider). Точка — индексы списков и значения слайдеров; откат выставляет значения обратно в контролы, и контролы сами пишут их в настройки. Изменения замечаются по DropDownComponent.OnValueSelected (выбор игроком или SetValue) и Slider.onValueChanged.
Контролы не задаются списком — каждый регистрируется маркером RollbackControl на самом контроле:
| Поле | Описание |
|---|---|
handler |
RollbackHandler; подставляется ближайший у родителей (OnValidate) |
dropdown |
DropDownComponent на том же объекте ([AutoLink]) |
slider |
Slider на том же объекте ([AutoLink]) |
Маркер переезжает вместе с контролом при копировании — параметр не выпадает из отката. Регистрация — в конце кадра включения, значение контрола запоминается в этот момент; фиксация точки хэндлером перезаписывает значения всех зарегистрированных контролов. Снимается регистрация только при уничтожении объекта: порядок OnDisable родителя и детей не гарантирован, и снятие при выключении могло бы опередить откат хэндлера.
Настройка экрана
- На корень экрана —
RollbackHandler: свитчер индикатора, твинер запроса на выход,onExit. - В список источников —
UIControlsRollback. - На каждый выпадающий список и слайдер —
RollbackControl. - Кнопки: выход —
CallExit(); в запросе —Save()илиRollback()плюс закрытие окна своими событиями.
Другие источники
| Источник | Пакет | Что откатывает |
|---|---|---|
UIControlsRollback |
RollbackSystem | Выпадающие списки и слайдеры |
RebindRollback |
RebindSystem (сборка ru.vortex.sdk.rebind.views) |
Переназначения клавиш и активность групп: точка — снимок Export(), откат — Import() |
Упрощённая альтернатива
RollbackSettings и RollbackSlaveObserver в Vortex/Sdk/Core/RollbackSettingsHandlers — прежний механизм только для выпадающих списков и слайдеров, с опросом каждый кадр и тремя состояниями свитчера. Оставлен для экранов, которым новая система избыточна.
Контракт
Гарантии
- Сразу после фиксации точки источник не видит изменений
- После «Откатить» ни один источник не видит изменений
- Свитчер в
Onтогда и только тогда, когда изменения есть хотя бы у одного источника - Признак пересчитывается только по событиям источников; покадровой работы нет
- При выключенном хэндлере источники не подписаны на события
- Закрытие экрана без изменений ничего не откатывает
- Ошибка отката одного источника пишется в лог и не мешает остальным
Ограничения
- Изменение данных в обход событий, на которые подписан источник (например, значение контрола кодом без
OnValueSelected/onValueChanged), не видно - У двух хэндлеров над одними данными свои точки отката
- Точка фиксируется в конце кадра включения: контрол, который получает значение позже, покажет изменения сразу после открытия
Граничные случаи
| Ситуация | Поведение |
|---|---|
| Пустой список источников | Предупреждение в редакторе; хэндлер всегда Off |
| Пустой элемент в списке | Ошибка в редакторе; элемент пропускается |
CallExit() без изменений |
onExit |
Save() / Rollback() при открытом запросе |
Выполняются; твинер не трогается |
| Выключение экрана с изменениями | Rollback(), затем отписка источников |
RollbackControl без UIControlsRollback у хэндлера |
Ошибка в лог, контрол не регистрируется |
| Повторная регистрация того же контрола | Без действия |
| Выпадающий список был пуст на момент точки | Откат его пропускает |
| Уничтоженный контрол | Снимается с регистрации; при сравнении и откате пропускается |