A professional developer on the gamedev.net forums once said “if you’ve implemented Undo and Redo in your app, then you’re in the top 1% of applicants for a tools developer position”. That’s funny to me, because I have no idea how you could possibly have a useful tool without such a fundamental element of GUI application development. I mean…people screw up. It’s nice for users to know that their mistake can go away with a single press of “ctrl+z”.
When I first started working on the map editor for my game JumpSwitch, I put Undo/Redo functionality in there right away. Like I said, it’s fundamental. The problem was, I implemented it probably the most naive way possible. See I handled saving and loading by having my Level class dump a whole bunch of data into a LevelData class, and then serializing that to XML. So I thought “hey, I’ll just hang onto an array of these LevelData instances and then load them up when the user wants to Undo or Redo!” Yeah I know, dumb. Feel free to laugh. Worked okay when my level had under a dozen entities hanging around, but anything after that meant you were waiting 3 or more seconds. Not exactly what I’d call a fast and responsive UI.
So….what had to be done? I had to stop being lazy and do Undo and Redo for real. This we got a little more sophisticated:
-Make an abstract “EditAction” class, that represents a single action taken by the user to edit the level. Give it virtual methods for “Undo” and “Redo”
-Make a few classes that inherit from EditAction and implement Undo and Redo for a specific action (moving an object, rotating an object, adding/removing an object, changing a Property, etc.)
-MapEditorLevel class has two stacks of EditAction’s: an Undo stack and a Redo stack. When the user performs an action, it’s pushed onto the Undo stack and the Redo stack is cleared. When the user does an Undo, the Undo stack is popped, Undo is called on that EditAction, and the action is pushed onto the Redo stack. When the user does a Redo, the Redo stack is popped, Redo is called, and the action is pushed onto the Undo stack.
/// <summary>
/// Represents a single action taken by the user to edit the level,
/// and that can be Undone/Redone.
/// </summary>
public abstract class EditAction
{
protected MapEditorLevel level;
public EditAction(MapEditorLevel level)
{
this.level = level;
}
/// <summary>
/// Undoes the action
/// </summary>
public abstract void Undo();
/// <summary>
/// Redoes the action
/// </summary>
public abstract void Redo();
}
/// <summary>
/// Handles undo/redo for a rotation of a GameObject
/// </summary>
public class RotateAction : EditAction
{
GameObject[] targetObjects;
Matrix rotation;
Matrix inverseRotation;
public RotateAction(MapEditorLevel level, GameObject[] targetObjects, Matrix rotation)
: base(level)
{
this.targetObjects = targetObjects;
this.rotation = rotation;
inverseRotation = Matrix.Invert(rotation);
}
public override void Undo()
{
foreach (GameObject targetObject in targetObjects)
targetObject.PropogateRotation(ref inverseRotation);
}
public override void Redo()
{
foreach (GameObject targetObject in targetObjects)
targetObject.PropogateRotation(ref rotation);
}
}
/// <summary>
/// Handles UndoRedo for properties
/// </summary>
public class PropertyEditAction : EditAction
{
PropertyInfo property;
object[] oldValues;
object newValue;
object[] propertyOwners;
public PropertyEditAction(MapEditorLevel level, object[] propertyOwners, object[] oldValues, string propertyName)
: base(level)
{
this.oldValues = oldValues;
this.propertyOwners = propertyOwners;
Type ownerType = propertyOwners[0].GetType();
// Find the property
property = ownerType.GetProperty(propertyName);
// Get the new value
newValue = property.GetValue(propertyOwners[0], null);
}
public override void Undo()
{
for (int i = 0; i < propertyOwners.Length; i++)
property.SetValue(propertyOwners[i], oldValues[i], null);
}
public override void Redo()
{
for (int i = 0; i < propertyOwners.Length; i++)
property.SetValue(propertyOwners[i], newValue, null);
}
}
// You get the idea...
Alright, looks like a solid plan. It worked out very nicely for movement and rotation. Movement is just translation which is a vector, so if you negate it you get the Undo. For the rotations, it’s a matrix so you just invert it. Piece of cake. Adding and removing…a little more tricky since I had to also keep track of an object’s parent so I’d know where to re-add the object. But still not too bad. Changing properties…not so nice. The problem is that when the user changes the value of a property with the PropertyGrid, it fires a PropertyValueChanged event that lets you know what changed, and what the old value was. Perfect, right? Well yes…but only for single objects. When you have multiple objects selected in the PropertyGrid you get “null” instead of the old value. Fantastic. Workaround time…
-Handle the SelectedGridItemChanged event for the PropertyGrid
-Store the current value of the selected Property for all selected objects in an array
-Handle PropertyValueChanged
-Make an array of current Property values
-Send it all off to the MapEditorLevel so it can create an appropriate EditAction and push it onto the stack
Okay, so this works. Only problem left with that is that when you set the value of a Property through PropertyInfo.SetValue, the PropertyGrid doesn’t reflect the changes. Still haven’t figured out a workaround for that one…
So in the end it was a little messy, but not that bad. It only took me a single evening in fact. Certainly nothing worthy of that frightening 99% statistic, IMO. Besides…if you don’t implement it, it’s going to be first thing your designers and artists complain to you about anyway.
