| |||||||||||||||||||||||||||
|
| |||||||||||||||||||||||||||
Print This Article REALbasic University: Column 040
RBU Pyramid XIIILast 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 StructureIf 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 MethodsIt'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:
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:
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 SettingsLast 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:
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:
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:
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:
Displaying the High ScoresWe'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):
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 ScoreAgain, 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:
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 DialogFor 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:
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):
And within loadPrefs, add the following case:
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 WeekActually, 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!
LettersOur first letter is from Dustin, who writes:
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:
In your case, you've got the code:
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:
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!
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 See the REALbasic University Archives
REALbasic University contents ©2001-2004 by Marc Zeedar and REALbasic Developer. All Rights Reserved.
|
. |
| |||||||||||||||||||||||||