箱を押すパズルに、何手でも戻せるアンドゥを入れました。詰みやすいゲームなので、戻せることは遊びの前提です。
やり方は2つありました。
手を逆にたどる(選ばなかった)
- 「上に動いた」なら「下に動かす」と、操作ごとに逆を書く
- 箱を押していたら、箱も一緒に戻す必要がある
- 持つのは操作だけなので軽い
- 逆の条件を1つ間違えると、盤が静かに壊れる
状態を丸ごと積む(選んだ)
- 動かす前の状態を、そのまま積んでおく
- 戻すときは、積んだものを取り出して差し替えるだけ
- 盤の形が変わっても、書き足すことがない
- そのぶんメモリを使う(あとで実測した)
戻したときに「元と同じ」であることを、考えずに保証したかった。
アンドゥの作り方(中身はこれだけ)
// PuzzleSession.cs(抜粋)
readonly List<LevelState> _history = new List<LevelState>();
readonly List<bool> _pushes = new List<bool>(); // その手で箱を押したか
public bool Move(Move move)
{
if (Cleared) return false;
if (!Rules.TryMove(_board, State, move, out var next, out bool pushed)) return false;
_history.Add(State); // ← 動かす前を積む
_pushes.Add(pushed);
State = next;
if (pushed) Pushes++;
return true;
}
public bool Undo()
{
if (_history.Count == 0) return false;
int last = _history.Count - 1;
State = _history[last];
if (_pushes[last]) Pushes--; // ← 数えていた値も一緒に戻す
_history.RemoveAt(last);
_pushes.RemoveAt(last);
return true;
}
「最初からやり直す」も同じ考え方で済みます。最初の状態を持っておいて、履歴を空にするだけです。
戻し忘れやすいのは「数えていた値」
上のコードで _pushes を持っているのは、押した回数を数えながら進めているからです。この面の難しさは押した回数で測っているので、画面にも出しています。
状態だけ戻して押した回数を戻さないと、アンドゥを繰り返すほど数字だけが増えていきます。しかも盤は正しいので、見ていても気づきにくい。
戻す必要があるのは「盤から計算し直していない値」
盤を見れば分かる値(クリアしたか・箱が目印に載っているか)は、戻したあとに計算し直せば正しくなります。戻す必要があるのは、進めながら足していた値だけです。この区別をつけると、積むものが減ります。
本当に重くないのかを測った
「状態を丸ごと積む」で気になるのはメモリです。いちばん大きい面(8×8・箱3個)で100手動かして、増えたぶんを測りました。
盤=8x8 箱=3 最短押し=8 積んだ手=100 増えたメモリ=4096バイト 1手あたり=40バイト
100手で4,096バイト。1面をどれだけ長く遊んでも、まず問題になりません。
理由は、積んでいる状態が小さいからです。盤(壁と目印)は面の中で変わらないので積んでいません。積むのは「プレイヤーの位置」と「箱の位置の一覧」だけ。箱3個なら、数字が4つです。
盤ごと積むと、いきなり重くなる
- 症状
- 1手ごとに盤の大きさぶんの配列が増える
- 原因
- 壁や目印は面の中で変わらないのに、状態と一緒に持ってしまう
- 対処
- 変わらないものは1つだけ持つ。積むのは変わるものだけ
積んだ状態の中身を、あとから書き換える
この形にしてから、履歴が壊れる不具合は出ていない- 症状
- 戻したはずなのに、戻った先も同じ盤になっている
- 原因
- 箱の位置の配列を使い回して、その中身を直接書き換えていると、積んだ過去も一緒に変わる
- 対処
- 動かすときは新しい配列を作って差し替える。過去は読むだけのものとして扱う
画面はこうなった

Unity に依存しない場所に置いた
盤・移動・押し・アンドゥは、Unity の型を1つも使わずに書いています(Core という場所にまとめている)。おかげでアンドゥのテストは、シーンもゲームオブジェクトも作らずに動きます。
run_tests true 36/36 件成功 (EditMode, 3.86s)
テストで見張っているのは、たとえばこういうことです。
アンドゥで見張っていること
まとめ
- アンドゥは状態を丸ごと積む方式にした。戻したときに元と一致することを、考えずに保証できる
- 積むのは変わるものだけ(プレイヤーと箱)。盤は積まない
- 100手で 4,096バイト。心配するほど重くなかった
- 戻し忘れやすいのは、進めながら数えていた値
- 遊びの中身をエンジンから切り離すと、アンドゥのテストが軽く回る