Undo system
BlitzMax Forums/BlitzMax Programming/Undo system
| ||
Hi! I've been trying to think of an implementation of an undo system that can handle undoing pretty much any operation. So far I cam up with something like a global undo list filled with containers which in turn contain a list of fields that have modified along with their old/new value. Type UndoContainer Global UndoList:TList = New TList Field Values:TList = New TList End Type Type UndoField Field OldVal:Float Field NewVal:Float Field Pointer:Float ptr End Type The problem here is the system above can only handle floats. Since there's no templates in BMax, I don't see an easy way of being able to handle any data type. The other way I can think of is having a list like this: Type UndoContainer Global UndoList:TList = New TList Field Values:TList = New TList End Type Type UndoField Field OldVal:Object Field NewVal:Object Field Caller:Object End Type And save an entire object whenever I am modifying something. Even though this works, it would be an awful waste of memory. Anyone got any brilliant ideas for this? |
| ||
I would implement this with a variant of the command pattern and a stack. Something like:' If you have a Stack with Push(), Top(), and Pop(), you're good to go. ' This won't compile without one. SuperStrict ' Undo command interface Type IUndoCommand Method Undo() Abstract End Type ' Undoes setting a float. Unfortunately, the lack of generics in BlitzMax ' does mean you'll also need UndoInt, etc. as well, but at least you'll ' be able to have them all in one UndoStack now. ' To undo other things, create other subclasses of IUndoCommand and ' override the Undo method. These should hold a reference to the object ' to act on. For example, to undo RotateRightBy90, ' you might create a RotateLeftBy90 extension of IUndoCommand that would ' rotate the image left by 90. To undo calling TurnOn(), create an ' IUndoCommand that would call TurnOff(). Type UndoFloat Extends IUndoCommand Field _float:Float Ptr Field _oldValue:Float Method Set:UndoFloat(fl:Float Var) _float = Varptr fl _oldValue = fl Return Self End Method Method Undo() _float[0] = _oldValue End Method End Type ' Holds all of the things to undo. It's just a wrapper around a stack ' that undoes things when they get popped off. Type TUndoStack Field _stack:Stack = New Stack Method Push(x:IUndoCommand) _stack.Push(x) End Method Method Undo() Local undo:IUndoCommand = IUndoCommand(_stack.Top()) _stack.Pop() undo.Undo() End Method End Type ' A myfloat to test on. Global myFloat:Float = 20 ' I'm just pretending you already have a stack class available... Local stack:TUndoStack = New TUndoStack Local undo:IUndoCommand ' Save undo command undo = New UndoFloat(myFloat) stack.Push(undo) ' Set myfloat myFloat = 672 Print myFloat ' Undo setting myfloat stack.Undo() Print myFloat |
| ||
I see. I wouldn't however use a stack due to the simple fact that I might need "Redo" :) I feared I might have to write a class for each type. Thanks for the answer. I'll go with this one. |
| ||
You could use a Byte Ptr and a length. Then for Floats you'd actually copy four bytes and for Doubles eight. You could even copy arrays or multiple consecutive Fields if you are careful. As long as you only keep it in memory, you won't have to care about the meaning of the data or endianness. |
| ||
Otus, that was my first idea, because I am a big fan of "stuff that applies to anything and just works", however I've hit a brick wall with strings. As unusual as they are, I still need to handle them. |
| ||
That's true, but there is a hack around that, if you use the threaded GC. :) Just save the address of the old string instead of the actual string data. The GC recognizes the reference even if it lives as an Int or a Byte Ptr instead of a String variable, so the old string isn't deleted. You need a lot of casting, though, to make sure the correct value is stored. You probably shouldn't rely on that behavior, though. |
| ||
That's true, but there is a hack around that, if you use the threaded GC. :) I don't think this works unless you're using the BDW GC?, and that was removed if I recall correctly. I see. I wouldn't however use a stack due to the simple fact that I might need "Redo" :) Could just do this for redo (sort of modified eel's code - and by modified I mean the principle is probably the same, but the implementation differs): Granted you seem to already know what you want to do, but I was bored and felt like writing bad code. (Warning: very likely to be bugs.) |
| ||
Yeah I think it's best to put all possible changes you can make into different kinds of "command" objects, rather than modifying things directly. Collect these in some kind of command list, which every now and again (every frame?) you empty and execute. Then to Undo, the opposite to these command objects in an undo list in reverse order. If you want a Redo, then, whenever you Undo, put the reverse again into a Redo list ;) |
| ||
I don't think any of these strategies will undo the deletion of an object, unless I am mistaken. For that you'd need a TList or something to store deleted objects. |
| ||
The object would have to be retained within the Undo system. |
| ||
Every frame, save a memory dump of the entire system on to the hard drive and keep an index. Then you can revert to any prior state with ease :D |
| ||
Undo frameworks are interesting :-) Here's an example of an undo framework working in BlitzMax (Win32, 4.5meg) This example shows Undo and Redo in action using a command-pattern type system. :-) |
| ||
The UAC will ask for confimation every time you do that. If you can click 60 times per second Ok, this will work. |
| ||
Nice example, Brucey, but 15mb (uncompressed) seems a bit too much for an undo system. (yes, i know I can use other features of qt). Currently my game editor has less than 800kb with all interface icons and images incbin-ed. @Zakk Type ObjectDeletionUndo Field _value:Object Field originalList:TList Ptr (does this work?) Method Set(list:TList Ptr, value:Object) EndType You get the idea. Having the ability to make a derived type from the base interface means I can pretty much do whatever I want. Sadly, it would mean some heavy modifications to the core to enable a list of commands that execute once per frame and are saved in a global list (or multiple lists by type). As with other features, I'll probably do this like Roy and Nillium said just to give the level designers something to work with, then work on doing it properly :) |
| ||
...seems a bit too much for an undo system. Indeed :-) But it does show that a "proper" undo/redo is easily appliable to BlitzMax. I think the clickable commands-list is also a nice feature. Hope you get it sorted out :-) |
| ||
...it does show that a "proper" undo/redo is easily appliable to BlitzMax. I think the clickable commands-list is also a nice feature. If I have time, I will eventually end up having something like that, because I love "proper" design :) |
| ||
I have an open source command stack implementation you could use here: http://code.google.com/p/muttmod/downloads/list |
| ||
Muttley, thanks! I finally got to undo/redo on my TODO list and your mod is great. Saves a lot of work. I got support to undo the basics (place, move, delete objects) in a couple of minutes. |
| ||
Glad you found it useful _JIM. And, yes, it really did take me 7 months to spot this post. ;) |
| ||
I started using your module after I saw this post a week or so ago, Muttley. It's extremely useful. Thanks for creating it! |
| ||
Cool. It's nice to see it being used. :) |