STYLY StudioのUndoの仕組みを解説！

# STYLY StudioのUndoの仕組み

STYLY Studioの開発を担当しているせぎゅと申します！

今回はUndo・Redoの設計について、STYLY Studioで実践している内容を解説したいと思います！

# Mementoパターン

Undoの設計にはさまざまなアプローチがありますが、もっとも直感的なのが `Mementoパターン` です。STYLY Studioは、この設計パターンを土台としています！

まず、シーンの状態がA→B→Cと変化した時に、各状態のコピーを作成します。

```mermaid
flowchart LR;
    StateA(状態A)
    StateB(状態B)
    StateC(状態C)
    
    StateA -->|Redo| StateB;
    StateB -->|Redo| StateC;
    StateB -->|Undo| StateA;
    StateC -->|Undo| StateB;
```

以下のように、インスタンスを差し替えることで、Undo・Redoを実現します。

- 状態CでUndoを実行したら、状態Cのインスタンスを破棄して、状態Bのインスタンスを復元する。
- 状態BでUndoを実行したら、状態Bのインスタンスを破棄して、状態Aのインスタンスを復元する。
- 状態AでRedoを実行したら、状態Aのインスタンスを破棄して、状態Bのインスタンスを復元する。
- 状態BでRedoを実行したら、状態Bのインスタンスを破棄して、状態Cのインスタンスを復元する。

この状態を記録する場所を、UndoStackと呼びます。

Mementoパターンについては、こちらの記事がわかりやすかったです！
- https://qiita.com/Azleep/items/861ee64867d7b016a966


# UndoStackに何を記録するのか？

UndoStackに何を記録するのか？というのを定義する必要があります。
Stepを踏んで説明します！

## Step1: GameObjectのインスタンスを記録する案

たとえば、GameObjectのインスタンスをまるごと記録したら、どうなるでしょう？
いわゆるスナップショットです！

Undoした際には、スナップショットされたインスタンスを有効化するだけで、状態の復元ができ、設計・実装は非常にシンプルになりそうです。

![20230213_Studio_Undo_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221845/950aa677-4734-ba4a-2188-5cd32b1f9c2c.png)

しかしUndoStackは、ユーザーがなにかの操作をするたびに、データが記録・蓄積されるものです。

容量の大きいスナップショットが、どんどんメモリに溜まっていき、あっというまにあふれることは想像に難くありません…

## Step2: STYLYシーンのXMLを記録する案

STYLY Studioは、STYLYシーンを編集するためのツールです。
STYLYシーンの情報は、XMLファイルとしてサーバーに保管しています。

- [STYLY XMLについてはこちら](https://www.docswell.com/s/KENTO/5NDRVK-2022-10-02-211126#p19)

STYLYでシーンを読み込む際には、このXMLを解析し、GameObjectを描画しています。

このXMLをUndo用に記録する案はどうでしょうか？

UndoやRedoをするたびに、XMLをもとに再描画するのです！

![20230213_Studio_Undo_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221845/3a802bf2-ffba-d408-0651-0bd5af02dd31.png)

これはStep1よりメモリ負荷が圧倒的に軽減されています！

しかし、XMLからシーン全体のインスタンスを生成する処理は、非常に重たい処理であり、読込に時間がかかります。

Undo・Redoをするたびに、プログレスバーを表示することになってしまいそうです…


## Step3: 変更箇所のXMLを記録する案

STYLYシーンは、以下のような階層構造を持っています。

```mermaid
erDiagram
    Scene ||--o{ Asset : ""
    Asset ||--o{ Modifier : ""
```

例: シーンの階層構造

- Scene
  - Asset A
    - Modifier AA
    - Modifier AB
  - Asset B
    - Modifier BA
    - Modifier BB
  - Asset C
    - Modifier CA
    - Modifier CB

例: 
- Modifier AAを変更したときに、Modifier ABは変化していません。
- Asset Aを変更したときに、Asset BやAsset Cは変化していません。


この変更箇所の位置と値のみを記録すれば、メモリも節約できますし、復元処理も高速化できます！

![20230213_Studio_Undo_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221845/9353e054-9f08-6b27-8aa4-27e86ea8a1a7.png)


## Step4: 変更箇所のStateを記録する案

Step3だとXML形式で記録していますが、XMLの読み書きは時間のかかる処理です。

さらなる最適化を図るために、Stateという概念を導入します！

ここでのStateとは、HTMLにおけるDOMのような役割を果たします。

```mermaid
flowchart LR;
    Xml(XML)
    State(State)
    Instance(GameObject)

    Xml -->|解析| State;
    State -->|描画| Instance;
    State -->|書込| Xml;
```

XMLを解析した結果を、Stateというオブジェクトに保管します。
このStateをもとに、GameObjectを描画します。

ユーザー操作による状態変更は、このStateオブジェクトに記録します。

このStateという単位を、UndoStackに記録します！

![20230213_Studio_Undo_4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221845/ffb0d209-f144-3c71-eeb6-1e01db37e32b.png)


このStateをUndoStackに記録することで、メモリの節約と、復元処理の高速化を実現できます！
STYLY Studioでは、この案を採用しています！



# 複数箇所を同時に変更する操作を記録したい場合

ここまでは、ユーザーの操作1回につき、変更箇所1つ分の記録をするという前提で説明をしてきました。
しかし、実態はもっと複雑で、操作と変更箇所が1対1で対応するとは限りません…

たとえば、3つのAssetを選択した状態で、削除ボタンを押したケースを考えます。

ユーザーは以下の挙動を期待します。

- 3つのAssetを選択した状態で、削除ボタンを押した時
  - 3つのAssetが削除される。
- 上記の直後にUndoをした時
  - 3つのAssetが**同時に**復活する。

しかし、実際には3つのAssetのStateが変更されるため、変更箇所は3箇所あります。

ここまで説明した仕組みだと、3つのAssetが同時に復活するという挙動を実現できません…！

## RecordGroupの導入

この問題を解決するために、RecordGroupという概念を導入します！

以下のシーケンス図のようなイメージです。

```mermaid
sequenceDiagram
    participant User as ユーザー
    participant UserWatcher as ユーザー操作を検知するクラス
    participant UndoStackController as UndoStackを制御するクラス
    participant UndoStack as UndoStack

    User ->> UserWatcher: 3つのAssetを選択して削除
    UserWatcher ->> UndoStackController: 記録開始宣言
    UndoStackController ->> UndoStackController: 空のRecordGroupを生成する

    UserWatcher ->> UndoStackController: Asset1の状態を記録
    UndoStackController ->> UndoStackController: RecordGroupにRecordを追加する
    UserWatcher ->> UndoStackController: Asset2の状態を記録
    UndoStackController ->> UndoStackController: RecordGroupにRecordを追加する
    UserWatcher ->> UndoStackController: Asset3の状態を記録
    UndoStackController ->> UndoStackController: RecordGroupにRecordを追加する

    UserWatcher ->> UndoStackController: 記録終了宣言
    UndoStackController ->> UndoStack: RecordGroupをUndoStackに追加する
```

Undo操作をしたときは、以下のような挙動になります。

```mermaid
sequenceDiagram
    participant User as ユーザー
    participant UserWatcher as ユーザー操作を検知するクラス
    participant UndoStackController as UndoStackを制御するクラス
    participant UndoStack as UndoStack

    User ->> UserWatcher: Undo
    UserWatcher ->> UndoStackController: Undo
    UndoStackController ->>+ UndoStack: RecordGroupを取得
    UndoStack ->>- UndoStackController: RecordGroup

    UndoStackController ->> UndoStackController: RecordGroupから3つのRecordを取得

    UndoStackController ->> UndoStackController: Asset1の状態を復元
    UndoStackController ->> UndoStackController: Asset2の状態を復元
    UndoStackController ->> UndoStackController: Asset3の状態を復元
```

これで、Undoの仕組みの解説は以上です！

おつかれさまでした！


# さいごに

本記事作成にあたり、以下のサイトを参考にさせていただきました。ありがとうございました。

- [Undoの実装の考え方](https://qiita.com/Azleep/items/861ee64867d7b016a966)