REALbasic University Resources:

RBU: Glossary Defines common REALbasic programming terms
  Archives Previously published columns
Translations: Dutch Courtesy of Floris van Sandwijk
  Japanese Courtesy of Kazuo Ishizuka
  Chinese Courtesy of Dong Li
  RBU Translation Guide Information on Translating RBU into other languages
Books: Matt's Book (2nd Edition!) Ideal for experienced programmers
  Erick's Book Best for beginning programmers
Websites: Mother Ship The publisher of REALbasic
  RB Webring Links to hundreds of REALbasic websites
  RESExcellence Another REALbasic programming column
  REALbasic Developer Magazine The premiere source for REALbasic instruction.

REALbasic University is Sponsored by

Make your Mac do what YOU want it to. Create games, utilities, cool Mac OS X tricks. Download REALbasic now and create your own software.


Print This Article

REALbasic University: Column 043

RBU Pyramid XVI: Undo Part 2

Last 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 State

The 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:

  
eraseUndo

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:

  
theUndo.pyramidCard1 = index
saveUndo

Beyond the else, at the next if, we'll insert this after the // Temp discard pile comment.

  
theUndo.tempDiscard = gTempDiscard.card
saveUndo

After the next else and the // It's on the discard pile! comment, put this:

  
theUndo.mainDiscard = gDiscard.card
saveUndo

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:

  
eraseUndo

// Is the King on a discard pile?
if index <> 30 and index <> 31 then
// not a on discard pile, so just remove it
cardCanvas(index).visible = false
cardCanvas(index).refresh
theUndo.pyramidCard1 = index
saveUndo
else
// It's on one of the discard piles
// Figure out which

if index = 30 then
// Temp discard pile
theUndo.tempDiscard = gTempDiscard.card
saveUndo

gTempDiscard.card = 0 // erase card
gTempDiscard.refresh
gTempDiscard.clickable = false
else

// It's on the discard pile!
theUndo.mainDiscard = gDiscard.card
saveUndo

// Delete top card
gTheDiscard.remove uBound(gTheDiscard)
if uBound(gTheDiscard) <> 0 then
// Still some left, display top card
gDiscard.card = gTheDiscard(uBound(gTheDiscard))
gDiscard.clickable = true
else
// None left, empty discard pile
gDiscard.card = 0
gDiscard.clickable = false
end if // uBound(gTheDiscard) <> 0

gDiscard.selected = false
gDiscard.refresh
end if // index = 30
end if

// Increase score
gSelection = -1
gScore = gScore + 1
updateCards

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:

  
theUndo = new undoClass
eraseUndo

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:

  
if uBound(gUndoList) > 0 then
editUndo.enabled = true
end if

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:

  
eraseUndo

// "Delete" the matched cards
cardCanvas(index).visible = false
cardCanvas(gSelection).visible = false

theUndo.pyramidCard1 = index
theUndo.pyramidCard2 = gSelection
saveUndo

// Increase the score
gScore = gScore + 2
gSelection = -1
updateCards

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:

    
playSound("deck")
eraseUndo

// Is there an old selection?
if gSelection <> -1 then
// Deselect it!
cardCanvas(gSelection).selected = false
cardCanvas(gSelection).refresh
gSelection = -1
end if

// Are there are cards left?
if uBound(gTheDeck) > 0 then
theUndo.deckAdvance = true
theUndo.tempDiscard = gTempDiscard.card
theUndo.mainDiscard = gDiscard.card
saveUndo

gSelection = -1

// Is the temp discard empty?
if gTempDiscard.card > 0 then
// Move the temp discard card to main discard pile
gTheDiscard.append gTempDiscard.card
gTempDiscard.card = 0
end if

// Add one to temp discard
gTempDiscard.card = gTheDeck(uBound(gTheDeck))

// Remove last item from deck
gTheDeck.remove uBound(gTheDeck)

gTempDiscard.clickable = true
gTempDiscard.selected = false
gTempDiscard.refresh

// Redraw discard card
gDiscard.card = gTheDiscard(uBound(gTheDiscard))
gDiscard.selected = false
gDiscard.clickable = true
gDiscard.refresh

drawCard(29, true)
if uBound(gTheDeck) > 0 then
wait(6)
drawCard(29, false)
end if
else
// No cards left, reset the deck
theUndo.deckReset = true
if gTempDiscard.card > 0 then
theUndo.tempDiscard = gTempDiscard.card
end if
if gDiscard.card > 0 then
theUndo.mainDiscard = gDiscard.card
end if
saveUndo

// No cards left -- reset the deck if this is first time through
if gFirstTimeThrough then
resetDeck
gFirstTimeThrough = false
end if
end if // uBound(gTheDeck) > 0 (there are cards left in the deck)

updateCards

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):

  
// It's on one of the discard piles!
//
// Figure out which one is the discard and "delete" the other
// (We don't want to "delete" the discard canvases since they are reused.)

eraseUndo

if index <> 30 and index <> 31 then
cardCanvas(index).visible = false
theUndo.pyramidCard1 = index
end if

if gSelection <> 30 and gSelection <> 31 then
cardCanvas(gSelection).visible = false
theUndo.pyramidCard1 = gSelection
end if

// if it's the temp Discard, we set its contents to zero
if index = 30 or gSelection = 30 then
theUndo.tempDiscard = gTempDiscard.card

gTempDiscard.card = 0 // erase card
gTempDiscard.selected = false
gTempDiscard.clickable = false
gTempDiscard.refresh
end if

if index = 31 or gSelection = 31 then
// It's the discard pile
theUndo.mainDiscard = gDiscard.card

// Delete top card of discard pile
gTheDiscard.remove uBound(gTheDiscard)

if uBound(gTheDiscard) <> 0 then
// Still some left, display top card
gDiscard.card = gTheDiscard(uBound(gTheDiscard))
gDiscard.clickable = true
else
// None left, empty discard pile
gDiscard.card = 0
gDiscard.clickable = false
end if

gDiscard.selected = false
gDiscard.refresh
end if // index = 30

saveUndo

// Increase the score
gScore = gScore + 2
gSelection = -1
updateCards

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.

In Detail

It's quite possible that this is also a bug in the other routines, but the undo system seems to work pretty well now. I'm planning to have a "bug followup" column after we're finished with RBU Pyramid, where I'll track down and reveal fixes to the bugs I've found, so I'll cover that in the future.

Meanwhile, test the program in a variety of situations and see if you can find problems with undo. Even for such a simple program as a card game, there are a number of tricky situations that might not undo correctly (and it's tough to find them without playing Pyramid a lot). Feel free to send me bug reports and/or fixes.

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 Week

We'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.

REALbasic University Quick Tip

I picked this up from REAL Software head Geoff Perlman, at MacWorld, when I complained about how difficult it is to select underlying objects within the REALbasic IDE. He revealed that REALbasic 4 has a new contextual menu: Control-clicking on a window brings up a menu with a Select submenu which lists every object and control in that window! Awesome. Worth the price of an RB 4 upgrade to me.

Letters

Here's a note from Charles, with a question about canvases.

Great columns, Marc!

Here's my question.

I have a listbox with about 200 items in it. In the Change event, I have a static text field display text associated with that item when it is selected. I also have a Canvas control that I would like to display an image associated with that item.

In the example below, there is an invisible column that contains an index number associated with the item in the list. That is how I associate the text to be displayed, and how I hope to associate the image to be displayed.

It usually runs like:

  
dim n, c as integer
dim imagename as string

c= me.columnCount -1

if me.selCount > 0 then
//this is the index value for the field
n = val(me.cell(me.listindex, c))

//sets the text
StaticTextDescription.text = gTheSetMembers(n).DisplayText

//creates a string, like image1
imagename = "image" + str(gTheSetMembers(n).Index)
Canvas1.backdrop = imagename //shoot!
else
StaticTextDescription.text =""
end if

The line right before the "else" is where it fails.

I would like to embed the image files as picts or pict resources in the application, but I can't figure out a way to get the canvas to take a variable name as a picture.

Any insight would be very much appreciated.

Charles

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:

  
sub loadPicture(name as string) as picture
dim f as folderitem

f = getFolderItem("").child("Pictures").child(name)
if f <> nil then
return f.openAsPicture
else
return nil
end if

end sub

This would be valid:

  
Canvas1.backdrop = loadPicture(imagename)

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:

  
sub loadPicture(name as string) as picture

select case
case "image1"
return image1
case "image2"
return image2
case "image3"
return image3
// etc.
end select

end sub

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:

  
sub loadPicture(name as string) as picture
dim rf as resourceFork

rf = app.resourceFork
if rf <> nil then
return rf.getNamedPicture(name)
end if

return nil
end sub

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:

  
Canvas1.backdrop = loadPicture(imagename)

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
is an author, philosopher, graphic designer, photographer, film director, soccer fanatic, and programmer (among other things). He writes for MacOpinion, runs his own software company, Stone Table Software, which sells the revolutionary Z-Write word processor, and is Publisher and Editor of REALbasic Developer. He lives in Northern California with his cats, Mischief and Mayhem, and is rapidly running out of free time.

See the REALbasic University Archives


REALbasic University contents ©2001-2004 by Marc Zeedar and REALbasic Developer. All Rights Reserved.

Email This Article - Comment On This Article

.

Reader Specials

Server Racks Online:
Apple Xserve CompatibleServer Racks and Universal Network Racks
42U KVM Switch Solutions:
High-End Mac and Multi-Platform KVM Matrix switching solutions!
Digital Camera Online:
Great prices on Digital Cameras and accessories!
KVM Switches Online:
Great prices on Mac KVM Switches from the leading manufacturers!
LCD Monitors Online:
Great prices on LCD Monitors from the leading manufacturers!
LCD Projectors Online:
Shop online for LCD Projectors from the leading manufacturers!
USB 2.0 Online:
Great prices on USB 2.0 products from the leading manufacturers

Serious Business Software:
Accounting, Sales, Inventory, CRM, Shipping, Payroll & more!

KVM Switch solutions for MACs:
DAXTEN is a KVM switch, KVM extender and monitor splitter specialist for PC, SUN and MAC applications from name brand manufacturers - offices worldwide.

The "Think Different Store: The iPod Accessories Store - iPod cases, iPod mini, iPod photo, speakers, itrip, inMotion, Soundstage and all other iPod accessories

Earn Cash with the ThinkDifferent Store Affiliates Program

Need A Web Site?
Applelinks Web Hosting Starting at 19.95 a Month

iTunes_RGB_9mm

.

iTunes_RGB_9mm

Cool Mac Gear


iPod 1G-2G
iPod 3G
iPod 4G
iPod Mini
PowerBook-iBook
Keyboard Skins
Garageband