STYLY StudioのUndoの仕組み
STYLY Studioの開発を担当しているせぎゅと申します!
今回はUndo・Redoの設計について、STYLY Studioで実践している内容を解説したいと思います!
Mementoパターン
Undoの設計にはさまざまなアプローチがありますが、もっとも直感的なのが Mementoパターン
です。STYLY Studioは、この設計パターンを土台としています!
まず、シーンの状態がA→B→Cと変化した時に、各状態のコピーを作成します。
以下のように、インスタンスを差し替えることで、Undo・Redoを実現します。
- 状態CでUndoを実行したら、状態Cのインスタンスを破棄して、状態Bのインスタンスを復元する。
- 状態BでUndoを実行したら、状態Bのインスタンスを破棄して、状態Aのインスタンスを復元する。
- 状態AでRedoを実行したら、状態Aのインスタンスを破棄して、状態Bのインスタンスを復元する。
- 状態BでRedoを実行したら、状態Bのインスタンスを破棄して、状態Cのインスタンスを復元する。
この状態を記録する場所を、UndoStackと呼びます。
Mementoパターンについては、こちらの記事がわかりやすかったです!
UndoStackに何を記録するのか?
UndoStackに何を記録するのか?というのを定義する必要があります。
Stepを踏んで説明します!
Step1: GameObjectのインスタンスを記録する案
たとえば、GameObjectのインスタンスをまるごと記録したら、どうなるでしょう?
いわゆるスナップショットです!
Undoした際には、スナップショットされたインスタンスを有効化するだけで、状態の復元ができ、設計・実装は非常にシンプルになりそうです。
しかしUndoStackは、ユーザーがなにかの操作をするたびに、データが記録・蓄積されるものです。
容量の大きいスナップショットが、どんどんメモリに溜まっていき、あっというまにあふれることは想像に難くありません…
Step2: STYLYシーンのXMLを記録する案
STYLY Studioは、STYLYシーンを編集するためのツールです。
STYLYシーンの情報は、XMLファイルとしてサーバーに保管しています。
STYLYでシーンを読み込む際には、このXMLを解析し、GameObjectを描画しています。
このXMLをUndo用に記録する案はどうでしょうか?
UndoやRedoをするたびに、XMLをもとに再描画するのです!
これはStep1よりメモリ負荷が圧倒的に軽減されています!
しかし、XMLからシーン全体のインスタンスを生成する処理は、非常に重たい処理であり、読込に時間がかかります。
Undo・Redoをするたびに、プログレスバーを表示することになってしまいそうです…
Step3: 変更箇所のXMLを記録する案
STYLYシーンは、以下のような階層構造を持っています。
例: シーンの階層構造
- Scene
- Asset A
- Modifier AA
- Modifier AB
- Asset B
- Modifier BA
- Modifier BB
- Asset C
- Modifier CA
- Modifier CB
- Asset A
例:
- Modifier AAを変更したときに、Modifier ABは変化していません。
- Asset Aを変更したときに、Asset BやAsset Cは変化していません。
この変更箇所の位置と値のみを記録すれば、メモリも節約できますし、復元処理も高速化できます!
Step4: 変更箇所のStateを記録する案
Step3だとXML形式で記録していますが、XMLの読み書きは時間のかかる処理です。
さらなる最適化を図るために、Stateという概念を導入します!
ここでのStateとは、HTMLにおけるDOMのような役割を果たします。
XMLを解析した結果を、Stateというオブジェクトに保管します。
このStateをもとに、GameObjectを描画します。
ユーザー操作による状態変更は、このStateオブジェクトに記録します。
このStateという単位を、UndoStackに記録します!
この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という概念を導入します!
以下のシーケンス図のようなイメージです。
Undo操作をしたときは、以下のような挙動になります。
これで、Undoの仕組みの解説は以上です!
おつかれさまでした!
さいごに
本記事作成にあたり、以下のサイトを参考にさせていただきました。ありがとうございました。