| |||||||||||||||||||||||||||||||
|
| |||||||||||||||||||||||||||||||
Print This Article REALbasic University: Column 043
RBU Pyramid XVI: Undo Part 2Last we started on a complex part of RBU Pyramid: adding an undo system after the fact. I mentioned this is much harder to do after the program's written than if you plan for it at the design stage. If you didn't believe me before, you'll see why today. We're going to be jumping around a lot between routines, so pay close attention.
Saving the Undo StateThe main thing that needs to happen to get our undo system is working is we must save the correct undo state every time the user does something. So that means we'll be adding code into a dozen different places -- no getting around that. Let's start at the top: we'll just go through our list of methods and make the appropriate changes to each routine. Open gameWindow's Code Editor and reveal the list of methods. The top one is called cardKing (it gets called when a King is removed). Click on it to bring up the code on the right. At the very top, before anything else, put in this line:
Gee, that wasn't hard, right? Well, that's the easy one. Next, we need to go to the first if-then-else statement. Just before the else, put in a couple blank lines and type in this:
Beyond the else, at the next if, we'll insert this after the // Temp discard pile comment.
After the next else and the // It's on the discard pile! comment, put this:
Wow, that was fun, wasn't it? Inserting code in the correct places after the fact is yucky. Just in case you made a mistake, here's the complete cardKing routine with the undo code in place:
So what did we do with our additions? Basically, for each condition (depending on where the King was located), the appropriate card information was saved into the theUndo object and then we called saveUndo to add that undo happening to our undo array. The result is that now the King being picked and removed has been saved. Theoretically, the program now supports undoing a King removal. However, if you try to run it, you'll find a couple problems. The first is a strange "nilObjectException" error message when you click on a King. You'll notice the error takes you to the eraseUndo method: that's where the error happened. But why? Our error message tells use we're attempting to use an object that doesn't exist. But where are we doing that in eraseUndo? It's such a simple routine there doesn't seem to be much of anything there. The only object is theUndo and it... Think back: remember when I said classes must always be instantiated as objects? We have an object, theUndo, but we never instantiated it! Quick, go to the gameWindow's Open event and put this in at very top:
Ah, that fixes it! No more error. But then you realize something really dumb: the undo menu command is grayed out! Of course that's easily fixable, but it does take a couple steps. First, go to the enableMenuItems event and put in this code:
That basically says, if there are undos available, make the undo menu command available (enabled). This works because we have an array of undos -- if the number of elements in that array are less than one, there are no undos saved in it. Next, go to the Edit menu and choose "New Menu Handler." In the dialog box that comes up, select EditUndo from the popup menu and click okay. Then, within the EditUndo menu handler, type in doUndo. Now if you run RBU Pyramid and you find a clickable King, you can remove it and then undo it! Very cool, eh? But of course that only works for just the King, and only for that move. We must add the saveUndo code to every action the player can perform. Here we go! Fortunately, the cardMatch method is simpler. Just make sure your code looks like this:
Since this is just made up of two cards from the pyramid, this is easy. We just save the two cards into theUndo and call saveUndo to preserve it. Then we've got the deckAdvance method. This one's a little more complicated. Rather than detail every change (there are only three) I'll just give you the full routine with the new code:
As you probably figured out, except for the initial eraseUndo command there are really just two situations here: either the Deck advanced or the Deck was reset. We simply save the appropriate settings into theUndo and save it. Next we've got our discardMatch routine. Again, it's more complicated since there are multiple possibilities of what happened (for instance, we have to figure out which discard was involved). Here's the complete new code (with several additions):
For this routine, two cards have been picked: index is one of them, and gSelection is the other. We need to save both values, as appropriate, into theUndo. But we don't want to saveUndo until both values have been saved. This was a bug in my original code: I called saveUndo after each setting (just like in the other routines), resulting in multiple calls to saveUndo within the same method! Remember, the undo array grows each time you call saveUndo, so that caused phantom undos to be saved. The revision above, has a single call to saveUndo at the end.
Well, that's it for another column. I hope you learned something, and I truly hope you plan your undo system better than I did. Think about how we could have done this better: for instance, what if our original data structure included undo information as part of the structure? If you would like the complete REALbasic project file for this week's tutorial (including resources), you may download it here.
Next WeekWe're almost but not quite finished with RBU Pyramid -- we're going to have a "miscellaneous" column next time, throwing in a number of little fixes and enhancements to many areas of the program that should just about get us done.
LettersHere's a note from Charles, with a question about canvases.
You've got a great start, Charles, and you've done the listBox portion correctly (using an invisible column). However I see your problem. You've defined imagename as a string, which is bad. That's because a canvas' backDrop property will only accept an item of type picture. If you'd said dim imagename as picture the canvas1.backdrop = imagename line would work (but of course the program would stop at the line above, where you assign a string to imagename). That's the part where I get a little confused: I don't know enough about your data structure to understand what you are doing with imagename. For example, it appears gTheSetMembers(n).Index is a number, and it looks to me like you're attempting to automatically generate a picture's name by combining the word "image" with the number (i.e. "image7"). If that's the case, you're almost there: you just need to load the actual picture data into a picture type variable. There are several ways to do this, depending on where the pictures are stored. If your pictures are in separate files in a particular folder, you could pass the string name you generated above to a routine that loads the picture from the named file and returns it. If you had this routine:
This would be valid:
Your loadPicture routine is just grabbing a picture from disk and passing it to Canvas1.backdrop. Of course this assumes you've got a "Pictures" folder (with pictures in it) in the same directory as your program. If, on the other hand, you want your pictures stored inside your program, you'd have to take a different tack. Let's say you dragged images into your RB project window and they were named image1, image2, image3, etc. You can't refer to them via your imagename variable, since it's only going to contain a string. What you are wanting to do is store a variable inside a variable, and that can't be done. If you store the name "image1" inside imagename, it's just text, not the actual variable named image1. Remember, pictures dragged into your project are the names of objects -- while you can dynamically generate the name of the object, you can't store the actual object inside your variable. How to get around this? You'd have to write a routine like this:
See how this works? It takes the name (as a string) of the picture you want, and then, hard-coded within your program, returns the appropriate picture object. You'd have to have a case for every picture in your program. Not a problem for 20 or 30, but if you've got hundreds, this could become a pain. Here's a better solution: store the pictures as picture resources inside either a separate resource document or inside your program! The approach is very similar for either one. Using Apple's free ResEdit program, create a new document. One by one, paste in the pictures you want in the document. They should be of type PICT. Then, name each one according to your scheme. (In ResEdit, selecting "Get Info" on a resource will allow you to name it.) Your PICT resource list might look something like this: ![]() If you name your resource file "resources" (no quotes) you can drag it into your RB project's window: that will embed the contents of that file within your program when you compile it. (The file can contain any resources you want, not just PICT resources.) Note that the file is not copied into your project, just linked: you can edit the external file and when you compile your program, it will use the file's current contents. Then all you have to do is pass imagename to this routine to retrieve the picture:
The above routine simply gets the named picture from your program's resource fork. If you wanted to use a separate resource file instead, you could just modify this routine to get the data from that file (instead of rf = app.resourceFork you'd write something like rf = getFolderItem("").child("picturedata").openResourceFork). With any of these methods, the loadPicture method returns a picture object, so you'd just assign that to Canvas1 like this:
Of course one disadvantage of using resources is that your program won't work under Windows: resources and resource fork objects are Mac only (they work fine under Mac OS X, by the way). That may or may not be an issue for you.
About the Column REALbasic University is a weekly instructional column on programming with REALbasic and is brought to you by REALbasic Developer, the magazine for REALbasic programmers. Each week we answer select reader questions, and we're always open to ideas for future columns. Send your questions to . (Keep your questions simple and specific. General queries like "How do I write my own web browser?" will be neglected.) Your question won't be answered immediately, but will be answered in a future column. (If you don't want your correspondence published, just be sure to indicate that when you write. Otherwise it's fair game.) About the Author See the REALbasic University Archives
REALbasic University contents ©2001-2004 by Marc Zeedar and REALbasic Developer. All Rights Reserved.
| |||||||||||||||||||||||||||||||