こんにちは みにくい社長です。
エディターを作るのって、普通のアプリを作るより大変だったりしますよね。
機能が多くて自由度が高いほど、作れるものの幅は広がりますが、エディターを作る側は大変です。
特にエディターの必須要件のUndo/Redoですが、簡単に見えて実装が難しい機能の1つです。
開発途中に欲しくなって入れようと思っても、簡単に実装できるものではなく、データ設計の時点で想定しておく必要があります。
今日はUndo/Redoの実装方法を紹介します。
実装方法には大きく2種類あります。
(A)操作するたびにデータを全て保存し、Undo/Redo時に復元する
長所:一度実装してしまえばデータが壊れる等のバグが発生しない
短所:保存データのサイズが大きい場合、全ての操作が重くなる
(B)操作した内容のみを履歴として保存しておく
長所:操作が重くならない
短所:全ての操作に対して実装する必要があり、どこかでバグが発生するとデータが壊れ続ける
基本的には(A)の実装の方が安全で容易に実装できますが、エディターで編集するデータが大きい場合は(B)の方法を検討する必要が出てきます。
今日は(A)の方法を紹介します。
言語はC#で、保存フォーマットはXMLを例にします。
(1)保存するデータをシリアライズ/デシリアライズに対応する
まずは、シリアライズ/デシリアライズを実装します。
この機能はUndo/Redoだけでなく、ファイルの読み込み/保存にも流用できます。
保存するデータは変数をpublicにするだけで保存できます。
保存しておきたくないデータがある場合は、privateにするかXmlIgnore属性を設定しましょう。
public class Editor { public class SaveData { public Vector2 Position; // 保存するデータ [System.Xml.Serialization.XmlIgnore] public int TempValue; // 保存したくないデータ } private SaveData saveData; // シリアライズ private string serialize() { System.Xml.Serialization.XmlSerializer serializer = new System.Xml.Serialization.XmlSerializer(typeof(SaveData)); StringBuilder builder = new StringBuilder(); System.IO.StringWriter writer = new System.IO.StringWriter(builder); serializer.Serialize(writer, saveData); writer.Close(); return builder.ToString(); } // デシリアライズ private void deserialize(string xml) { System.Xml.Serialization.XmlSerializer serializer = new System.Xml.Serialization.XmlSerializer(typeof(SaveData)); System.IO.StringReader reader = new System.IO.StringReader(xml); saveData = (SaveData)serializer.Deserialize(reader); reader.Close(); } }
(2)操作履歴保存用クラスを作成する
操作履歴をリングバッファに保存するためのクラスを作ります。
Undo/Redoに対応するため、前回の履歴と次の履歴が取得出来るようにしておきます。
class History { // 履歴の最大数 private static readonly int HISTORY_MAX = 32; // 現在のインデックス private int _current = 0; // 先頭のインデックス private int _top = 0; // 末尾のインデックス private int _tail = 0; // 履歴 private string[] _history = new string[HISTORY_MAX]; // コンストラクタ public History() { } // 初期化 public void initialize(string text) { _history[0] = text; _current = _top = _tail = 0; } // 履歴を追加 public void addHistory(string text) { _current = (_current + 1) % HISTORY_MAX; _history[_current] = text; _top = _current; if (_top == _tail) { _tail = (_current + 1) % HISTORY_MAX; } } // 前回の履歴を取得 public string getPrevious() { if (_current != _tail) { _current = (_current + HISTORY_MAX - 1) % HISTORY_MAX; return _history[_current]; } return null; } // 次の履歴を取得 public string getNext() { if(_current != _top) { _current = (_current + 1) % HISTORY_MAX; return _history[_current]; } return null; } }
(3)操作履歴を保存する
準備ができたので、履歴保存オブジェクトをエディタに追加し、初期化します。
後は操作を行う度に履歴を追加していきます。
private History _history = new History(); public Editor() { // 履歴を初期化 _history.initialize(serialize()); } public void SetPosition(Vector2 position) { saveData.Position = position; // 履歴を追加 _history.addHistory(serialize()); }
(4)操作履歴から復元する
ここまでくれば、後は単純です。
Undo/Redoを実行するときは、前回の履歴を読み込むだけです。
読み込んだ後は、必要に応じて画面をリフレッシュしてください。
public void undo() { string text = _history.getPrevious(); if (text != null) { deserialize(text); // 必要に応じて画面のリフレッシュを行う } } private void redo() { string text = _history.getNext(); if (text != null) { deserialize(text); // 必要に応じて画面のリフレッシュを行う } }
非常にシンプルですね。
開発初期からSaveDataクラスをしっかり設計しておけば、簡単に導入できるのがわかると思います。
(3)と(4)が重くてどうしようもない場合は(B)案で実装することになりますが、(B)案は次回にしましょう。
またねー