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 040

RBU Pyramid XIII

Last week we started the process of adding a high scores system to RBU Pyramid. We created a high scores window and added a menu command to show and hide the window. Like many things that seem simple, this is one that effects many areas of the program. Today we'll finish this part of the program, creating a data structure for the high score info, adding routines to display the data, and methods that will save and retrieve the information.

The High Scores Data Structure

If you've learned anything about the way we do things here at RBU, you'll know that creating an appropriate data structure is way up there on the Importance list. The most basic data structure is an array, so we'll use that, but for the data organized within that array, we'll create a custom class.

Add a new class to your project window (File menu, "New Class"). Name it highScoreClass. Within it, add the following properties (Edit menu, "New Property"):

Now open globalsModule and insert this property: gHighScoreList(10) as highScoreClass. That will give us a ten-position array of our class (yes, technically it's eleven as the zeroth position is a valid array element, but we'll only use elements 1 through 10).

Cool! Our data structure is established, but we need to initialize that structure as well as write routines that will load and save the data.

Load and Save Data Methods

It's pretty obvious that data loading routines are exactly the opposite of data saving routines: the question is, which do you write first? Does it make a difference?

The answer is yes, it does make a difference. Always write your data saving routine first, then use that as the basis for your file loading routine. The reason is that it's easy to forget a step during the writing of the first routine. After all, it's the first. A mistake in the save routine usually isn't fatal (your program will still work, it just won't save all the data), but an error in the loading routine and your program might crash.

Open your prefModule module and let's add a couple methods (Edit menu, "New Method"). The first is saveHighScores. Here's the code:

  
dim out as binaryStream
dim f as folderItem
dim i, n as integer

f = preferencesFolder.child(kHighScoreName)
if f <> nil then

out = f.CreateBinaryFile("myBinary")
if out <> nil then

for i = 1 to 10
out.writeByte len(gHighScoreList(i).name)
out.write gHighScoreList(i).name
out.writeLong gHighScoreList(i).score
out.writeDouble gHighScoreList(i).theDate.totalSeconds
next // i
out.close
return

end if // out = nil
end if // f = nil

As you can see, the routine is fairly basic: we obtain a folderitem pointing to our high score file (the name is the in the constant kHighScoreName which we'll add in a moment). Then we create a binary file.

What's a binary file? Nothing but a series of binary data (i.e. numbers). That means if you write the number 73 as a byte to a binaryStream, it will not be stored as the two-character string "73" but as character "I" (which is ASCII 73).

If you look in REALbasic's online help for the binaryStream object, you'll see that unlike the textOutputStream object, which only has methods for writing lines of text or text, binaryStream has options for writing bytes, doubles, longs, etc. That can be helpful if you need to store exact data (such as a date or a very long number) since converting a number to a string (text) tends to introduce rounding errors. The problem (or difficulty) with a binaryStream is that you must know the exact order of the data in the file in order to retrieve it. For instance, to read back a string of text, you must know exactly how many letters are in the string.

That's easily solved, however, by simply storing the length of the string first, then the actual string data. As you can see in the above, that's what we do: we first write a byte representing the length of the data, then we write the data as a string. For the score and the date, we don't need to do this, since we're writing those as fixed-length pieces of data: a long and a double are both 4 bytes (32 bits) long.

Since there are ten scores to save, we do this ten times (using a for-next loop). Then we close the file and we're done. Simple!

To load the data is simply the reverse. Add loadHighScores and paste this in:

  
dim in as binaryStream
dim f as folderItem
dim i, n as integer

f = preferencesFolder.child(kHighScoreName)
if f <> nil then

in = f.openAsBinaryFile(false)
if in <> nil then

for i = 1 to 10
gHighScoreList(i) = new highScoreClass
n = in.readByte
gHighScoreList(i).name = in.read(n)
gHighScoreList(i).score = in.readLong
gHighScoreList(i).theDate = new date
gHighScoreList(i).theDate.totalSeconds = in.readDouble
next // i
in.close
return

end if // in = nil
end if // f = nil

// if no file is read, set high scores to defaults
for i = 1 to 10
gHighScoreList(i) = new highScoreClass
gHighScoreList(i).name = kDefaultName
gHighScoreList(i).score = 0
gHighScoreList(i).theDate = new date
next // i

This is only a little more complicated. The outer parts of the routine are nearly identical, but once we get ready to read in the data, we must create new instances of each array element (of type custom class highScoreClass) using the new command.

Once we've got an instance initialized, we're ready to input the data. This part is exactly the reverse of the save routine. First we read in a single byte -- that tells us the length of scorer's name. Then we read in that name and put it into the name property of gHighScoreList(i). Then we read in a long and put it into score. For theDate, we must first instantiate it with the new command (it's a date object, after all). When we do that, it defaults to today's date. We can set the date by storing the saved date into the date's totalSeconds property.

We do this ten times, once for each element in the array, and we're done.

The final part of the code is used in case there is no high score file found: if that's the case, we have no data to read, so we instantiate the array elements and insert default values. You may notice one of these values is the default name, kDefaultName. While we're thinking about it, let's add this constant.

Open globalsModule and add a new constant (Edit menu, "New Constant"). Give it the name kDefaultName and the value "No Name" (no quotes).

Do this again, adding a constant named kHighScoreName with the value "RBU Pyramid High Scores" (no quotes). That should take care of establishing the name of the high score file.

There's one more thing we need to do: we must define the "myBinary" file type so that RBU Pyramid knows what kind of binary file to create. (This just helps associate our high score file with RBU Pyramid.)

Go to the Edit menu and choose "File Types...". Click the "Add" button and fill out the dialog like this (save it when you're done):

As long as we're here, let's set the Creator type for our application. From the Edit menu, choose "Project Settings" and make it look like this:

Excellent! That will help when we compile our finished application to a stand-alone application.

Okay, our high score saving and retrieving routines are written, but we haven't called them yet, so they won't do anything. We need to tell our program to load the high scores when the game is launched, and save the high scores when the game is finished.

In your app class, find the Open event and put loadHighScores just before the loadPrefs line. In the Close event, add saveHighScores after savePrefs. That's it!

Saving the Window Settings

Last week we made it so HighScoreDialog remembers its position and size, but we didn't save those to our preference file. We'll do that now. Using our preference system, it's easy to add this with just a few lines.

Within prefModule, open savePrefs. After the t.writeline "#version 1.0" line, add the following:

  
// Save High Score Window status (open/closed and position/size)
t.writeline "#scorewindow " + bStr(gScoresShowing) + comma + gScorePos

This simply saves a string of data beginning with "#scorewindow " and continuing with either a "true" or a "false" (depending on whether gScoresShowing is true or false). Then we add a comma, followed by gScorePos (which, if you will remember, is a string of numbers separated by commas, representing the left, top, width, and height values.

It might look like this:

  
#scorewindow true,629,45,325,220

We could have saved this info using two separate preferences, of course, but there's nothing wrong with doing it this way (though it is a little more complicated). To retrieve the data, we just do the reverse.

Open loadPrefs and within the select case statement, add the following case:

  
case "scorewindow"
// finds first comma
j = inStr(gDirectives(i).text, comma)
// puts the rest of the line into gScorePos
gScorePos = mid(gDirectives(i).text, j + 1)
gScoresShowing = nthField(gDirectives(i).text, comma, 1) = "true"

Remember, our preference system automatically strips off the leading number sign ("#") and trailing space, so all we search for here is the keyword "scorewindow". The rest of the preference is stored in the gDirectives(i).text variable, so we search for the first comma: everything to the left of that comma is our true/false window open/closed setting, and everything to the right of that comma is our gScorePos string.

The mid() function returns the middle portion of a string, starting at the position where you tell it, and retrieving the number of letters you tell it. But there's a cool variation: if you don't specify the number of letters you want back, it just returns everything to the end of the string! That's how we fill gScorePos above: everything to the right of the comma goes into gScorePos.

The final line sets gScoresShowing to true or false. The condition is if the first field (separated by a comma) matches the word "true" -- that condition is either true or false, and thus gScoresShowing is either true or false.

There's one more thing we need to do: if gScoresShowing is true, we must display the high scores window. We do this within the Open event of gameWindow. Add in this code there:

  
if gScoresShowing then
showScores
end if

Displaying the High Scores

We've now done just about everything for preserving the high scores, but we haven't set up a system to display them. HighScoreDialog just displays an empty window!

Open gameWindow and go to the showScores method we added last time. This time we're going to fill it with some code. Put in the following (the final line is the same as before):

  
dim i, n as integer

highScoreDialog.scoreList.deleteAllRows
for i = 1 to 10
highScoreDialog.scoreList.addRow str(i)
n = highScoreDialog.scoreList.lastIndex
highScoreDialog.scoreList.cell(n, 1) = gHighScoreList(i).name
highScoreDialog.scoreList.cell(n, 2) = format(gHighScoreList(i).score, "#,###")
highScoreDialog.scoreList.cell(n, 3) = gHighScoreList(i).theDate.shortDate
highScoreDialog.scoreList.cellBold(n, 2) = true
next // i
highScoreDialog.show

This mess just fills the listBox in HighScoreDialog with the current high score info. We go through a loop of ten scores. We first add a new row, putting in the high score number (which goes into the first column by default). Then we put in the actual score info from our data structure array. Each element in the structure is put into a separate column: name, score, and date. Score is formatted with a comma in the thousands' position (if necessary). Then we bold the actual score value with the cellBold method. The final line displays the score window, if it's not already visible.

Now you might be wondering why we put this in a method instead of putting this inside HighScoreDialog's Open event. The reason is simple: the scores change. If the player has the score list always visible and they generate a new high score, the high score list must update immediately. So we need a routine like this to refresh the score listing. That's also why the first line of this method is highScoreDialog.scoreList.deleteAllRows -- if we didn't have that and the scores were already visible, you'd just get more and more scores displayed (added to the existing list) instead of erasing the current list (if there) and starting fresh.

Whew! We've done a lot so far in the last two lessons, but believe it or not, we're not finished!

If you run the program now, it will display the high score list, you can view/hide the high score window, you can move it and it will remember its size and location, even between program launches. However, we never added in any code to save a high score to the high score list, so if you finish a game, it won't be added to the list!

Saving a High Score

Again, we've got a situation that seems simple, but is more complicated than meets the eye. What happens when a user generates a high score? First, we've got to compare the current score to all the others. If this is higher than any of those, we've got a new high score. If that happens, we need to ask the user for his or her name. Yes, that means we must add another dialog box!

Adding a dialog means we need a way to pass info back and forth between it. So go to globalsModule and add a new property (Edit menu, "New Property"): gDialogReturn as string.

Now, I don't know about you, but I hate it when games continually ask me for the same name over and over -- I like it when they remember who I am. So let's also add a gLastName as string property. We can use that to remember the player's default name.

With that done, let's write the routine that will add a new high score to the list. For now, we'll just pretend the askNameDialog has already been created.

Go to the endOfGame method we added last time. Put this code in it:

  
dim i, n as integer
dim hs as highScoreClass

//
// This means the game is over, so we see if the player has
// a high score.
//

n = 0
for i = 1 to 10
if gScore >= gHighScoreList(i).score then
n = i
exit
end if
next // i

// do we have a new high score?
if n > 0 then
hs = new highScoreClass
gDialogReturn = gLastName
askNameDialog.ShowModal
hs.name = gDialogReturn
hs.score = gScore
hs.theDate = new date // today's date
gHighScoreList.insert i, hs
gHighScoreList.remove 11 // delete last element
gLastName = gDialogReturn
showScores
end if

The first part is simple: we step through each score in the high score list and compare it to the current game's score. If the current score is higher, we've got a new high score. Since n represents the array element number of the beaten score, if n is greater than zero, we've got a new high score.

Once we've gotten a new high score, we create a new variable of type highScoreClass. We pass the old name (gLastName) to the askNameDialog, and what we get back we store into our variable. Then we insert that into the gHighScoreList array and remove the last element.

Finally, we set gLastName to whatever name was returned in gDialogReturn, and we display the high score window.

Adding an Ask Name Dialog

For the last routine to work, we must create a new window (File menu, "New Window"). Name it askNameDialog and give it the following settings:

The dialog box itself should look like this:

The gray box in the upper left corner is a 32-by-32 pixel canvas object (called canvas1). The editField is called nameEdit, and the pushButton is named okButton.

The code for askNameDialog is simple. Within nameEdit's Open event, put this me.text = gLastName. For Canvas1's Paint event, put g.drawNoteIcon 0, 0 -- that draws the standard "note" icon.

For okButton, this goes in the Action event:

  
gDialogReturn = nameEdit.text
if gDialogReturn = "" then
gDialogReturn = kDefaultName
end if
self.close

This simply saves the contents of the editField. If it's empty, it puts in the default name (using the kDefaultName constant we created earlier). Then it closes the dialog box.

Now that we've added another item to remember, let's save it via our preference routine. In prefModule, open savePrefs and add this line in the middle (the exact positioning is irrelevant, as long as it's before t.close):

  
// Save last name entered
t.writeline "#lastname " + gLastName

And within loadPrefs, add the following case:

  
case "lastname"
if gDirectives(i).text <> "" then
gLastName = gDirectives(i).text
else
gLastName = kDefaultName
end if

This just saves and restores the gLastName variable so we can remember the player's name even between game launches -- how thoughtful is that!

Well, that's enough for this week. We've finished adding the high score system to our program: it wasn't difficult in terms of understanding, but it was a lot of work scattered throughout the game. Unfortunately, some features are like that: there's no way to avoid complexity.

If you would like the complete REALbasic project file for this week's tutorial (including resources), you may download it here.

Next Week

Actually, next week's column won't be published next week -- I'm going to take a couple of weeks off and enjoy the holidays. I suggest you do the same. Relax and spend time with your families. After the break, we'll add a helpful instructions window to our game, and then in the column after that we'll tackle the biggie: adding an undo system to RBU Pyramid. See you next year!

Letters

Our first letter is from Dustin, who writes:

I've been using realbasic for about 3 days now and find it a pretty kewl language. I have quite a bit of VB experience but I'm encountering an error that I have never seen before. When I try to run debug I get an unexpected token error. See for yourself. I'm just trying to create a simple program based on loose statistics that plays a whole bunch of hands of cards at given probabilities.

An "unexpected token" error, Dustin, means that REALbasic encountered a character it didn't expect while attempting to decipher your code. This is usually just a simple typo, like when you insert an incorrect character in the middle of some normal code.

For instance, the msgBox command is looking for whatever follows to be a string (either quoted text or a variable name). If you follow it with a strange character like an equals sign, REALbasic will generate an "unexpected token" error:

  
// These all generate "unexpected token" errors
msgBox =
msgBox \
msgBox ]

In your case, you've got the code:

  
if WinOrLoss > then bank=bank+Wager

That's the problem: REALbasic is expecting an end-of-line character after then. Since you've added bank=bank+Wager, that confuses the parser. The solution, of course, is to move that code to its own line:

  
if WinOrLoss > then
bank=bank+Wager
end if

Unlike some BASICs, which might allow you to combine the two statements on one line, REALbasic is particular about its structure: an if-then statement cannot have anything more than "if condition then" on a single line. I hope that helps you get going!

Next, we've got a letter from Paul, who's got a slew of questions!

Hi Marc,

First, thanks for your tutorials. They're very helpful. I have both Real Basic books and agree with you, one's too simple and the other expects a level of knowledge that I just don't have yet. I've got a few questions. I'm trying to create an app to go on a CD-ROM for the windows platform and have run into my inability to do certain things. The project is for a family history and has lots of text and scanned photos that I need to display in windows (read only, the user can only look at them and select another window to view).

[1] What's the best way to display formatted text given to me in MS Word format? The CD has to run as an app that you put the CD in your drive, double-click and go. Should I put the files in a folder on the CD and use getFolderItem? Or static text?(seems limited) Or edit text? What is the native file format of text for Real Basic?

[2] One of the graphics is the family tree chart. It's way wide(like 400 by 6000) with little boxes on it with people's names. Is there a way to put it in a scrolling window and put buttons on top of the boxes? Or invisible buttons above the boxes to go to that person's bio page/window?

If I put it in as a graphic on a canvas I can't see it to line up the buttons. If I put it in as a backdrop it doesn't seem to scroll. Do I need to recreate this graphic inside a window? There is about 200 of these boxes with lines connecting them, so it's real problem if I need to do it that way.

There's also an index of the names on the chart that I'd like to be able to have the user be able to double-click on to go that name's bio page/window. However it's not a one to one. Some names (wife, child) would all go to one bio (the Husband,father's bio).

[3] List box(how do I tell it to open the window?)? or static text with buttons? Again there's about 200 of these.

[4] Now, I'm wondering if I've bitten off more than I can chew and wondered if you knew of someone in the NYC area who consults that I could hire to help me do this? Or perhaps someone willing to do phone or email help with the code?

Thanks again,

Paul Goodrich

To make sure I didn't miss answering any of these, Paul, I've numbered your questions and I'll respond to them by number. Hopefully this will help: if I've misunderstood or haven't explained something well, please let me know.

Answer 1: Word is a proprietary format owned by Microsoft. They don't go telling the world how the format works. You could reverse engineer it, but that's a lot of work. A better way is to convert Word files to RTF (Rich Text Format) which is designed for portability. But you still have to get RTF into a format REALbasic can deal with.

You asked what REALbasic's built-in file format for text is, and the answer is styled text. Styled text is a basic standard for text with certain attributes, such as fonts, various text sizes, bold, italic, etc. It's used by the Mac OS as a way to let you copy and paste formatted text between applications.

I have created a free program, called RTF-to-Styled Text, which converts RTF documents to styled text files REALbasic can open (using a folderItem's openStyledEditField method). Once your files are in styled text format, you should be able to put those into an editField with all the styling intact (at least the styling REALbasic supports).

Answer 2: You are correct that a backdrop graphic does not scroll. If you want to scroll the graphic, you must draw it manually yourself within a canvas. You'll need to work with separate scrollBar controls and adjust the drawing of the picture accordingly (i.e. if the user scrolls, you've got to redraw the picture with the new position). For a good example of how to do this, look at RBU 021.

As for the controls on top of the picture, you can do this as well, though it gets more complicated. You'd have to use a canvas' scroll method, being sure to pass true for the scrollControls parameter.

The bigger problem I would foresee would be that with so many of these controls, it would complicated to put controls in all those locations. You'd have hundreds of them to keep track of, not a fun job.

A better way would be to use the mouseDown event of the canvas and use math to figure out which part of the picture the user is clicking on. You'd have to take into consideration that part of the image may be cut off (scrolled).

For example, in the above diagram, the distance for xDif and yDif would have to be added to the x and y coordinates where the user clicked to give you the true coordinates of which part of the picture was being clicked. So if the user clicked at position 10, 5 and the picture was scrolled 5 left and 5 down, the actual picture coordinates clicked on would be 15, 10.

Of course for this to work, you'd have to know the coordinates of all the "clickable" places on your picture. If this is a fixed picture and not likely to change, you could manually input in all the coordinates (in rectangles). If you stored them in an array structure, each time the user clicked, you could look through the array to see if the user clicked inside a valid "hot spot" and the act accordingly.

The last thing you talk about, having various points bring up the same reference data, makes me think you'd be best off creating some kind of database system. Within the database you could allow links between records. For each "hot spot" you'd need to store (invisibly) information (such as an id number) that would uniquely describe the record you are linking to. Obviously this would be more difficult to set up than a single-minded system designed for your one graphic, but it would be more flexible in case the graphic changes and/or you want to expand the program beyond the original purpose. Either way, you are talking about a complicated project -- you might want to start with a simpler program.

Answer 3: How to tell a listBox to open a window? Well, with any object, you can include code in the format of windowname.show to display the window named windowname. Where you put this code depends on how you want your program to work. For instance, putting it within a canvas' mouseDown event will open the window when the user clicks there. Putting it within a listBox's doubleClick event would do it when then user double-clicks on a line, etc.

Answer 4: REAL Software maintains an excellent list of REALbasic consultants on their website. I'm sure one of them would be perfect for your project.


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.

.

.