|Delphi Clinic||C++Builder Gate||Training & Consultancy||Delphi Notes Weblog||Dr.Bob's Webshop|
iOS App: Game of Memory
In this article, I want to describe my first application that was published in the App Store: the Game of Memory. In the past couple of years, I've implemented this little game as VCL, WinForms, VCL for .NET, and even in an old version of Delphi Prism (when it was still called Chrome). The idea and design is still the same: based on the input for the number of rows and columns, a dynamic array of buttons is created and shown on screen. Each button is associated with a number or character, and each number or character will occur only twice (i.e. on two buttons). However, you do not see the associated number or character until you click on the button. Then the value is shown. The trick is to click on two buttons (in succession) with the same value associated with it. At that point, the two buttons will be disabled (taken out of the game), and you continue with the remainder buttons. If the two buttons do not match, you get about 1 second to try to remember them, before their text is cleared again, and you are faced with a screen full of "empty" buttons.
The game consists of only one form, the MainForm (type TMemoryForm) with a ToolBar containing a TCornerButton and TLabel, plus two TSpinBoxes to allow the user to specify the number of columns (X) and rows (Y) for the dynamic arrays of buttons. A TCheckbox is used to allow the user to select "characters" instead of numbers, and finally a big button is used to start the game.
Note the TStyleBook, which is present because I needed to change the Fill.Color property of the TLabel (which by default is a solid gray colour, but that's hardly visible on the toolbar, so I changed it to Azure).
The class definition for the TMemoryForm is as follows:
type TMemoryForm = class(TForm) btnStart: TButton; lbX: TLabel; cbChars: TCheckBox; edX: TSpinBox; edY: TSpinBox; lbCaption: TLabel; btnReplay: TCornerButton; lbAbout: TLabel; ToolBar1: TToolBar; StyleBook1: TStyleBook; procedure btnStartClick(Sender: TObject); procedure btnClick(Sender: TObject); procedure FormResize(Sender: TObject); procedure btnReplayClick(Sender: TObject); private const MinX = 2; MaxX = 8; MinY = 3; MaxY = 12; Chars: array[1..(MaxX*MaxY) div 2] of Char = ('A','B','C','D','E','F','G','H','I','J','K','L','M','N', 'O','P','Q','R','S','T','U','V','W','X','Y','Z', '2','3','4','5','6','7','8','9','@','#','%','&',':','!', '?','[',']','\','/','-','+','='); protected Turns: Integer; // number of guesses First: Boolean; // first button? Sleeping: Boolean; // sleeping after 2 button clicks? UseChars: Boolean; // characters instead of numbers? Buttons: array of array of TButton; end;Note the array of Chars that we can use if the user decides to play with characters instead of number captions of the buttons. Feel free to change these to an even more complex list of Unicode characters of course. An even nicer edition would use bitmaps or photos on the buttons, but I leave that as exercise for the reader.
There's hardly anything we need to do in the OnCreate event, because the settings (characters or numbers, and the X and Y values of the playing board) are taken into account in the btnStart's OnClick event.
The btnStart's OnClick event is used to initialize the game, and create the dynamic array of buttons. For each button, we need to create it with Self as Owner as well as Parent, and I explicitly set the Disable FocusEffect and StaysPressed to True (the former for efficiency reasons, the latter just because it looks nice if the first button remains pressed).
In a double loop we calculate the Hight, WIdth and position of each button, depending on the current ClientHeight and ClientWidth of the iOS form, and the number of buttons in each row (plus number of rows).
procedure TMemoryForm.btnStartClick(Sender: TObject); var i,j,Tag: Integer; X1,X2,Y1,Y2: Integer; begin btnStart.Visible := False; btnReplay.Text := 'replay'; btnReplay.Visible := True; edX.Visible := False; edY.Visible := False; lbAbout.Visible := False; lbX.Visible := False; cbChars.Visible := False; Application.ProcessMessages; Randomize; UseChars := cbChars.IsChecked; Turns := 0; Tag := 0; Caption := Format(Caption,[Turns]); First := True; Sleeping := False; SetLength(Buttons,max(MinX,min(MaxX,StrToIntDef(edX.Text,MinX)))); for i:=0 to High(Buttons) do begin SetLength(Buttons[i],max(MinY,min(MaxY,StrToIntDef(edY.Text,MinY)))); for j:=0 to High(Buttons[i]) do begin Buttons[i,j] := TButton.Create(Self); Buttons[i,j].Parent := Self; Buttons[i,j].DisableFocusEffect := True; Buttons[i,j].StaysPressed := True; Buttons[i,j].Position.X := 4 + (ClientWidth div Length(Buttons)) * i; Buttons[i,j].Position.Y := 38+((ClientHeight-32) div Length(Buttons))*j; Buttons[i,j].Width := (ClientWidth div Length(Buttons)) - 8; Buttons[i,j].Height := ((ClientHeight-32) div Length(Buttons)) - 8; Buttons[i,j].Tag := 1 + (i * Length(Buttons) + j) div 2; if Tag = Buttons[i,j].Tag then Buttons[i,j].Tag := -Tag; Tag := Buttons[i,j].Tag; Buttons[i,j].Text := '?'; Buttons[i,j].Font.Family := 'Comic Sans MS'; if High(Buttons) > 3 then Buttons[i,j].Font.Size := 20 else Buttons[i,j].Font.Size := 24; Buttons[i,j].OnClick := btnClick end end;Note that we should also free the buttons at the end of the game. At the end of the OnClick event, we use the Tab property to determine the "value" of each button. Two buttons that "belong" to each other have the same (absolute) Tag value. The shuffle of buttons is just a loop to swap Tags around:
for i:=1 to (Length(Buttons) + 1) * (Length(Buttons) + 1) do // shuffle begin X1 := Random(Length(Buttons)); X2 := Random(Length(Buttons)); Y1 := Random(Length(Buttons)); Y2 := Random(Length(Buttons)); Tag := Buttons[X1,Y1].Tag; Buttons[X1,Y1].Tag := Buttons[X2,Y2].Tag; Buttons[X2,Y2].Tag := Tag end end;Each of the dynamic buttons shares the same OnClick event handler in method btnClick (as seen on the previous page). In this event handler, we use the First field to check if this is the first or second button in a pair of buttons that the user clicked on. For the First button, we don't have to do much, but for the second button we must check to see if they are a pair, and perform different actions depending on the answer.
Note that we should first and foremost check if we're not currently sleeping (two buttons being shown, just before they are turned again), in which case we should ignore the event, to prevent a third button from showing its text before the other two are cleared again.
procedure TMemoryForm.btnClick(Sender: TObject); var i,j: Integer; Done: Boolean; begin if Sleeping then Exit; // still waiting for delay to finish... if First then begin First := False; Inc(Turns); lbCaption.Text := Format('Dr.Bob''s Game of Memory... %d',[Turns]); Tag := TButton(Sender).Tag; if UseChars then TButton(Sender).Text := Chars[abs(Tag)] else TButton(Sender).Text := IntToStr(abs(Tag)); end else // second button begin Done := False; // no match if TButton(Sender).Text <> '?' then begin ShowMessage('You just clicked on this one...'); Exit // no further processing of click end; First := True; if UseChars then TButton(Sender).Text := Chars[abs(TButton(Sender).Tag)] else TButton(Sender).Text := IntToStr(abs(TButton(Sender).Tag)); if TButton(Sender).Tag = -Tag then // the same begin for i:=0 to High(Buttons) do for j:=0 to High(Buttons[i]) do if abs(Buttons[i,j].Tag) = abs(Tag) then begin Done := True; // match Buttons[i,j].Enabled := False; if UseChars then Buttons[i,j].Text := Chars[abs(Tag)] else Buttons[i,j].Text := Format('[%d]',[abs(Tag)]) end end else // no match begin Application.ProcessMessages; Sleeping := True; Sleep(1000); Sleeping := False end; Application.ProcessMessages; Done := True; for i:=0 to High(Buttons) do for j:=0 to High(Buttons[i]) do if Buttons[i,j].Enabled then begin Done := False; Buttons[i,j].Text := '?'; Buttons[i,j].IsPressed := False end; if Done then if Turns > Length(Buttons) * Length(Buttons) then if Turns < 1.5 * Length(Buttons) * Length(Buttons) then ShowMessage('All Done - Good Memory.') else ShowMessage('All Done.') else if Turns > 0.75 * Length(Buttons) * Length(Buttons) then ShowMessage('All Done - Great Memory.') else // even smaller ShowMessage('All Done - Excellent Memory!') end end;Note that it's necessary to call the Application.ProcessMessages from time to time in order to allow the screen to update (especially just before the Sleep(1000), otherwise we would not see the text on the buttons before they are cleared again.
At the end of the btnClick the variable Done is used to check if there are no buttons left that are enabled (in other words: if the game is "Done"), in which case the number of guesses is used to give the user a notification message regarding "All Done" wth or without an excellent, great of just good memory (depending on the number of guesses needed).
The buttons are divided over the available form space, so the Width of a button is the width of the form divided by the number of buttons on a row (minus a certain value to allow for some padding between the buttons), and the same applies to the Height.
As soon as the iOS devices is rotated, the OnResize event is fired, and we can respond to is by recalculating the Height, Width but also the Position of the dynamic buttons as follows:
procedure TMemoryForm.FormResize(Sender: TObject); var i,j: Integer; begin for i:=0 to High(Buttons) do for j:=0 to High(Buttons[i]) do begin Buttons[i,j].Position.X := 4 + (ClientWidth div Length(Buttons)) * i; Buttons[i,j].Position.Y := 38 + ((ClientHeight-32) div Length(Buttons)) * j; Buttons[i,j].Width := (ClientWidth div Length(Buttons)) - 8; Buttons[i,j].Height := ((ClientHeight-32) div Length(Buttons)) - 8 end end;Note that the font will remain unchanged, so when using the maximum number of buttons on the Game of Memory form, it may result in unreadable text (compare the left screenshot below, showing a 8x12 button field with the right screenshot, showing the same fields of buttons, but this time horizontally oriented on the iPhone).
A click on the Replay button will clear all dynamic game buttons, and display the initial controls again (the TSpinBoxes, the checkbox, etc). Note that clearing the buttons means that we need to loop over the dynamic array and actually call Free for each button in order to prevent a memory leak.
This is coded as follows:
procedure TMemoryForm.btnReplayClick(Sender: TObject); var i,j: Integer; begin btnReplay.Visible := False; lbCaption.Text := 'Dr.Bob''s Game of Memory'; for i:=High(Buttons) downto 0 do begin for j:=High(Buttons[i]) downto 0 do Buttons[i,j].Free; SetLength(Buttons[i],0) end; SetLength(Buttons,0); btnStart.Visible := True; lbAbout.Visible := True; edX.Visible := True; edY.Visible := True; lbX.Visible := True; cbChars.Visible := True end;
In order to deploy an application to the AppStore, we need to perform a number of steps. You must be a member of the Apple iOS Development program (for 99 US$ or 79 Euro per year). This will also give you access to iTunes Connect, where you need to manage your content in the AppStore.
Before you can submit an app to the AppStore, you must use iTunes connect to create a record for your application. For each change (for example a version 1.01 update), you also need to add a new record, by clicking on the Add Version button.
From that moment on, the application (with the specific version) is in the "Prepare for Upload" state.
Once you're a iOS Developer Program member, you can not only deploy applications in the AppStore, but also deploy applications on your own iOS devices, using a Mac OS X running Xcode and the Xcode Organiser. In both cases, the Xcode Origaniser is the important tool, and it can be used to display registered devices, but also projects and archives.
The archives are used to validate (before submitting to the AppStore) or actual submission of your project. When your project is ready to be validated and submitted, you need to make a number of changes to the .xcodeproj files. First of all, you need to make a special "Scheme" (this is like a new "Build Configuration"), for the AppStore deployment. Where the old Scheme could work in iOS Simulators as well as actual iOS devices, the AppStore theme will typically only work for submission to the AppStore (at least in my case), because the settings in the Scheme make it incompatible with the iOS Simulators on my Mac.
The Build Settings that we need to modify for the AppStore Scheme include the Valid Architectures (arm6 for AppStore) and iOS Deployment Target (iOS 4.0 for AppStore).
The reason is that the currently used FPC cannot compile for arm7 (required for iOS 5).
Note: this has changed with Delphi XE2 Update #4, which included FPC 2.6 to add arm7 support for iOS.
Then, for the AppStore - Bob's iPhone target, we need to do a Product | Build for - Build For Archiving.
And then we can do Product | Archive to actually create the new archive.
The new Archive can be found in the Organiser window again, which will automatically be opened. I can now click on the Validate button to validate the archive, before submitting it to the AppStore.
Note that before we can validate a new version, we should also create a new record in iTunes Connect, otherwise you'll get a notification that no record could be found for this application / version. Once the application is submitted, the status changes from "Prepare for Upload" to "Prepare for Review" and finally "Ready for Sale" or "Rejected" with or without reason.
My Game of Memory can be found in the AppStore now. For more information about Delphi XE2 iOS Development, see my courseware manual about this topic (for sale for 99 Euro, which includes free updates to the PDF until the end of 2012).