Building a small level editor tool scene in Godot 4 for my top-down game. Drag/drop object placement, property tweaks, that kind of thing. The second I added a second placement action I knew I needed undo/redo, so I went with the command pattern. Nothing revolutionary but it's working well and I figured I'd share.
Base class is basically nothing:
class_name EditorCommand
func execute() -> void:
pass
func undo() -> void:
pass
History stack with a cursor for redo:
class_name EditorHistory
const MAX_HISTORY := 50
var _history: Array[EditorCommand] = []
var _cursor := -1
func execute(cmd: EditorCommand) -> void:
if _cursor < _history.size() - 1:
_history = _history.slice(0, _cursor + 1)
cmd.execute()
_history.append(cmd)
if _history.size() > MAX_HISTORY:
_history.pop_front()
_cursor = _history.size() - 1
func undo() -> void:
if _cursor < 0:
return
_history[_cursor].undo()
_cursor -= 1
func redo() -> void:
if _cursor >= _history.size() - 1:
return
_cursor += 1
_history[_cursor].execute()
func can_undo() -> bool:
return _cursor >= 0
func can_redo() -> bool:
return _cursor < _history.size() - 1
Concrete example, placing a scene object:
class_name PlaceObjectCommand extends EditorCommand
var _parent: Node
var _prefab: PackedScene
var _transform: Transform3D
var _placed: Node3D
func _init(parent: Node, prefab: PackedScene, xform: Transform3D) -> void:
_parent = parent
_prefab = prefab
_transform = xform
func execute() -> void:
_placed = _prefab.instantiate()
_placed.global_transform = _transform
_parent.add_child(_placed)
_placed.owner = EditorInterface.get_edited_scene_root()
func undo() -> void:
if _placed:
_placed.queue_free()
_placed = null
The part I didn't think through ahead of time: compound commands. Dragging an object updates position and rotation simultaneously, and I don't want those as two separate undo steps. I ended up writing a CompositeCommand that just holds an array of commands and delegates to them in order. Works fine, but I can already see it getting unwieldy as the editor grows.
Anyone doing something smarter for batching related operations? Also wondering if it's worth serializing the history to disk for crash recovery, or if that's deep overkill for a level editor that auto-saves anyway.