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 родителя и детей не гарантирован, и снятие при выключении могло бы опередить откат хэндлера.

Настройка экрана

  1. На корень экрана — RollbackHandler: свитчер индикатора, твинер запроса на выход, onExit.
  2. В список источников — UIControlsRollback.
  3. На каждый выпадающий список и слайдер — RollbackControl.
  4. Кнопки: выход — 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 у хэндлера Ошибка в лог, контрол не регистрируется
Повторная регистрация того же контрола Без действия
Выпадающий список был пуст на момент точки Откат его пропускает
Уничтоженный контрол Снимается с регистрации; при сравнении и откате пропускается