One of the most basic of usability features, undo/redo is also fairly straightforward to implement. Most engineers will tell you that undo functionality needs to be planned for from the beginning of a project. So why is it still just an afterthought for most game development tools? We recently had to implement an undo solution for a client in a design tool. This is a general implementation similar to those I’ve seen at other companies. I’m sharing it with you here so you can plan for it when you sit down to design your next tool.
For undo to work, we need an object to encapsulate the functionality of executing an action, along with the undoing of the action We also need to support redoing the action, which is a variation of the initial execution. For those who like design patterns, this functionality fits nicely in the “command” (a.k.a. “action”) pattern. The execute, undo, and redo are all functions that need be added to the command object to support the functionality we need, along with a specific constructor.
The constructor is responsible for storing all of the necessary data within the command class to be used later on by our execute, undo and redo functions. We simply copy values passed in to the constructor to member variables.
The execute function does everything that would have been done had the code not been moved into an undo-able command. This fact makes it relatively easy (but tedious) to implement undo after much of the tool is written, or to move functionality that didn’t support undo into the undo system.
Some commands need to perform multiple actions at once. For instance, we may need to overwrite data that existed before. In this case, we perform two undo-able actions at once, the deletion of the old data and the creation of the new data. So, we pass in a list of pointers to the command base class. This list will contain this action and any other commands that need to be executed along with this one. We’ll create and execute those actions within the execute function. The current action is added to the list last, for consistent ordering. The importance of this will come later.
The undo function is a simple matter of reverse engineering ourselves back to the previous state using the data passed to the constructor. For everything the execute did, we basically do the opposite, with the exception of the multiple command functionality, which is called from code outside of this class, from the undo stack, itself.
Redo basically does everything the initial execute did, but like undo, we skip the multiple command functionality. We call the execute function with a null list of commands (we could use a boolean parameter) to signify we should skip the execution of the multiple command code.
In order to have more than one level of undo, we need to store these actions on a stack. In our case, we’ll actually use two stacks (for undo and redo), encapsulated in a command stack class. Similar to the commands themselves, there are three basic functions in our command stack — execute, undo, and redo.
A command is passed to the execute function of the command stack. The execute function creates the list that is passed to the command’s execute function, calls that function, and pushes the resulting list (which has been populated by the command’s execute function with one or more actions) onto the undo stack. The redo stack is then cleared, since there are no commands to redo after a brand new command has been executed.
Undo simply pops a command list from the undo stack, and executes undo on all items in the list in reverse order. Items were added to the list in the order that they would normally be executed. In our example above where a new item overwrote an existing one, the deletion of the existing item was first in this list. We want to restore the old item after deleting the new one. Finally, the command list is pushed onto the redo stack.
Redo pops a command list from the redo stack, executes redo on all items in the list (beginning to end, this time), and pushes the list onto the undo stack.
Once this is ready, you simply need to create the commands where appropriate, and call execute on the command stack, with the command as parameter. Once the commands are added to the stack, you can simply call undo and redo on the command stack from a key command or menu item.
This simple implementation will give you unlimited levels of undo functionality. The only limitation here is that some commands can use a lot of memory. That’s generally not a problem when talking about tools being run on PCs with virtually unlimited resources, but it can be a concern. How the commands, themselves, are implemented will affect this, so that takes careful consideration. A simple fix for this would be to represent the undo and redo stacks as circular buffers with fixed sizes. Presumably, you can find a reasonable number of undo levels for your application.
Undo: one of the greatest boons to productivity ever devised. So important that the Atari ST had an UNDO key on its keyboard. I feel that the pinnacle of this would be the ability to never have to pop a confirmation box up to the user. Allow me to recover from accidentally exiting without saving from every program I use, and I’ll be a happier man!
(Also: undoing sending an angry, drunken mass e-mail out, but perhaps that’s too much to ask.)
Ah, if only it were that simple…
- You have to make sure that the existing redo commands are still operational – i.e. create the object, set property, undo twice, redo twice – if the ‘set property’ command stored the pointer to the object that got destroyed and recreated, you’re in trouble.
- Generally, making something the user does not a part of undo/redo stack is going to backfire. For example, if object selection is not undoable, you’ll probably want the object to activate upon set property redo – or the user won’t see what the redo command did if the edited property does not have a visible effect on the view. Manual object activation will lead to all sorts of problems once you consider multiselection.
- However, there are obviously a lot of things you don’t want to get to undo stack (i.e. camera manipulation or brush radius modification or whatever); for this reason, it’s often necessary to plan for undo/redo a bit in the process of implementing the functionality – i.e. you can’t use the mouse position, camera state, current tool settings, etc., if you want the redo to work.
- For proper user experience, automatical – but tuneable – command merging is usually necessary (i.e. dragging the object should only leave a permanent command once the mouse button is up, but an object should be moved with the mouse). Can be either implemented via automatic merging (i.e. commands try to merge with the stack top on push) or manual merging (on mouse move look at the top of the stack explicitly); anyway, that’ll require proxy commands to mark points of the stack where the merging stops.
- Tracking of last saved point is necessary for proper modified state support (though that’s easier than the above).
As for multiple commands, I prefer to make a special container command instead of making it an intrinsic part of undo/redo stack, though YMMV.
Good point with the create/set/undo… problem. Obviously, you should not use pointers to refer to objects that might get destroyed. For our case, we didn’t have this problem, but I would recommend using a unique identifier (handle) for referencing the object instead of a pointer. When the object is created or recreated with redo, the same ID can be used, thereby eliminating this problem.
For many tools, navigation and selection should be undoable actions, in order for the user to see the undoing of the important stuff.
For moving items with the mouse, I would allow the drop (releasing the mouse button) to set the position (an undoable action), but the drag just moves the visual of the object without really setting the final position of the object. If you want to, you can use the command list to link all movements of an object together in one undoable action.
Of couse, all of this still fits within the system as described. These are just details of how you might use it, but that will vary depending on the tool you’re creating.
Thanks for the comments!