Developer

さくさく理解する Godot 入門(ただし2Dに限る)応用編 お絵かきパズル【第5回】
2021.12.20
Lv1

さくさく理解する Godot 入門(ただし2Dに限る)応用編 お絵かきパズル【第5回】

目次

  • メイン画面
  • Undo/Redo

メイン画面

前回(第1回~第4回)まで、パズルのメイン画面の基本機能を実装してきた。 今回以降は、使い勝手を上げるための Undo/Redo、全消去機能などを実装していく。

■Undo/Redo

Undo/Redo はツール系ソフトでは必須だ。表計算やワープロソフトを使っていて、これに助けられた人も少なくないはずだ。 パズルでも Undo/Redo 機能があると、 途中で間違ってしまった場合でも簡単に間違う前の状態に戻すことができるので、パズルを安心して解けるようになる。

というわけで、ユーザの操作を記録しておき、それを戻す(Undo)機能と、Undo の取り消しである Redo(進める)を実装する。

画面右下に TextureButton を配置し、ノード名を UndoButton とする。 黒・白・灰色アイコンを用意し、それぞれ通常・押下・ディセーブル画像とする。 子ノードとして Label を配置し、日本語フォントを割り当て、テキストを「戻す」にする。 同様に、Undo ボタンも配置する(下記参照)。

次に、Undo/Redo 機能を実装していく。

まずは、Undoアイテム種別とUndoスタックと現位置変数の宣言だ(下図参照)。

# Undo アイテム種別
enum { SET_CELL, SET_CELL_BE, CLEAR_ALL, ROT_LEFT, ROT_RIGHT, ROT_UP, ROT_DOWN}
var undo_ix = 0         # Undo アイテム位置
var undo_stack = []     # Undo アイテムスタック

ユーザの操作種別ごとに Undo/Redo を実行するために操作情報(これを Undoアイテムと呼ぶ)を配列で保持する。 例えば、セルの情報を変更した場合は [SET_CELL, x, y, old, new] という配列が Undoアイテムとなる。

Undoアイテムは push_to_undo_stack(item) により Undoスタックに保存される。 Undo スタックに操作情報をすべて保存しておけば、後でそれを保存の時とは逆順に取り出して状態を元に戻すことができる。 というわけだ。

下図に、Undo アイテムを積んだ場合の Undo スタックの状態を示す。

下図に、Redo を行ったときの Undo スタック状態を示す。スタックに積まれているアイテムはすぐに削除されず、 Redo のために保持しておく。

下記に、Undo スタックに Undo アイテムを積む関数 push_to_undo_stack(item) と、 undo_ix の値により Undo/Redo ボタンをイネーブル・ディセーブルする関数 update_undo_redo() のコードを示す。

func push_to_undo_stack(item):
    if undo_stack.size() > undo_ix:
        undo_stack.resize(undo_ix)
    undo_stack.push_back(item)
    undo_ix += 1
func update_undo_redo():
    $UndoButton.disabled = undo_ix == 0
    $RedoButton.disabled = undo_ix == undo_stack.size()

Undo の後に Redo を行う場合があるので、通常のスタックと違い、Undo を行っても Undoアイテム を Undoスタックから取り除くのではなく、 undo_ix を減らすだけ。よって、Undoアイテム をスタックに積む場合は、スタック末尾に追加するのではなく、 undo_ix 以降を削除してからスタック末尾に Undoアイテム を追加する必要がある。

ユーザがセルをクリックすると、_input(event) がコールされ、セル状態が変更される。 このとき、種別(SET_CELL)、セル位置、元の状態、新しい状態を要素とする配列を Undo スタックに積む(下記参照)。

func _input(event):
    .....
    var v0 = $BoardBG/TileMap.get_cell(xy.x, xy.y)      # 現状のセル状態
    var v = 新しいセル状態
    $BoardBG/TileMap.set_cell(xy.x, xy.y, v)            # セル状態更新
    push_to_undo_stack([SET_CELL, xy.x, xy.y, v0, v])    # UndoItem 保存
    update_undo_redo()

エディタにて画面右でノードタブを選び、シグナルの pressed をダブルクリックし、 Undo ボタン押下シグナルを _on_UndoButton_pressed() に接続し、押下時にこの関数がコールされるようにする。

_on_UndoButton_pressed() の実装は以下のようになる。

func _on_UndoButton_pressed():
    undo_ix -= 1
    var item = undo_stack[undo_ix]
    if item[0] == SET_CELL: # セル状態変更
        var x = item[1]
        var y = item[2]
        var v0 = item[3]
        set_cell_basic(x, y, v0)
    elif item[0] == SET_CELL_BE:    # 複数セル状態変更
        set_cell_basic(item[1], item[2], item[3])
        while true:
            undo_ix -= 1
            item = undo_stack[undo_ix]
            set_cell_basic(item[1], item[2], item[3])
            if item[0] == SET_CELL_BE:
                break;
    elif item[0] == CLEAR_ALL:  # 全消去
        .....
    elif item[0] == ROT_LEFT:   # 左移動
        rotate_right_basic()    # 逆の右方向に移動処理することで、元に戻す
    elif item[0] == ROT_RIGHT:
        rotate_left_basic()
    elif item[0] == ROT_UP:
        rotate_down_basic()
    elif item[0] == ROT_DOWN:
        rotate_up_basic()
    update_undo_redo()

item[0] が SET_CELL、すなわちセル状態変更だった場合は、位置、変更前の状態を item から取り出し、 set_cell_basic(x, y, v0) をコールしてセルの状態を元に戻す。
SET_CELL_BE はドラッグして黒を複数のセルに設定した場合で、スタック上に SET_CELL_BE が現れるまで、 位置、変更前の状態をスタック上の item から取り出し、セルの状態を元に戻していく。
全消去、上下左右移動の場合も、それを元に戻す処理を行う。

全消去の場合の処理は、全消去の節で説明する。

上下左右移動の場合は、その逆方向移動処理関数を呼ぶ。

下記は Redo 処理のコードだ。
内容は Undo の場合の逆なだけなので、説明は省略する。

func _on_RedoButton_pressed():
    var item = undo_stack[undo_ix]
    if item[0] == SET_CELL: # セル状態変更
        var x = item[1]
        var y = item[2]
        var v = item[4]
        set_cell_basic(x, y, v)
    elif item[0] == SET_CELL_BE:    # 複数セル状態変更
        set_cell_basic(item[1], item[2], item[4])
        while true:
            undo_ix += 1
            item = undo_stack[undo_ix]
            set_cell_basic(item[1], item[2], item[4])
            if item[0] == SET_CELL_BE:
                break;
    elif item[0] == CLEAR_ALL:
        clear_all_basic()
    elif item[0] == ROT_LEFT:
        rotate_left_basic()
    elif item[0] == ROT_RIGHT:
        rotate_right_basic()
    elif item[0] == ROT_UP:
        rotate_up_basic()
    elif item[0] == ROT_DOWN:
        rotate_down_basic()
    undo_ix += 1
    update_undo_redo()