Appdot5-MiniJoc-Menu del Joc
En el meu intent de millorar el minijoc basat en l'AppDot m'he adonat de que per fer-ho caldrà fer uns quants canvis. La meva idea es que hi hagin diferents puntuacions i que també es pugui perdre, això vol dir que en algun moment la finestra canviarà per un altre en la que digui "Has perdut" amb un botó per tornar a jugar(o el que vulguem), i després intentaré fer un rànquing de puntuacions.
Per fer aquestes millores cal estructurar el joc d'un altre manera i com aquest canvis no tenen rés a veure amb el joc en si mateix, he decidit obrir un nou post. Per cert, aquest joc serà per l'iPhone/iPod Touch així que no sé si estaria bé obrir una nova secció a programació que es digui "Cocoa Touch" i no barrejar-ho tot dins de "Cocoa", però com volguis 
Per entendre bé el sistema de finestres el que he intentat fer es el menú principal del joc, amb tres seccions: joc, instruccions i rànquing. Cada secció tindrà un botó per tornar enrere i poguer triar un altre opció. En aquesta primera fase, no integraré ni el joc, ni faré el rànquing ja que el que voldria es entendre bé el sistema de finestres i veure quins conceptes no entenc del tot.
A sobre el mètode que utilitzat, com sempre hi han moltes maneres de fer-ho, i en aquest cas no he volgut fer una barra de navegació (com fa la AppStore o InstallApp) ja que agafa una bona part de la finestra i en un joc normalment es vol controlar completament l'aspecte que té.
Finalment voldria dir que m'he basat en diferents exemples que he trobat i hi han alguns conceptes que no acabo d'entendre, per això esteu convidats a fer les correccions que penseu oportunes. Aclarit tot això, comencem!
El primer que hem de fer es crear un nou projecte del tipus "Window-Based Application" ja que nosaltres definirem quines finestres volem i com ens mourem.
En el nou projecte ara no tenim mes que un delegate. Ens caldrà dons una classe que s'ocupi de controlar la finestra que es veu en cada moment. Fem una nova classe del tipus UIViewController, li diem RootViewController i el carreguem al GameMenuAppDelegate de la següent manera:
#import <UIKit/UIKit.h>
@class RootViewController;
@interface MenuGeneralAppDelegate : NSObject <UIApplicationDelegate> {
IBOutlet UIWindow *window;
IBOutlet RootViewController *rootViewController;
}
@property (nonatomic, retain) UIWindow *window;
@property (nonatomic, retain) RootViewController *rootViewController;
@end
#import "MenuGeneralAppDelegate.h"
#import "RootViewController.h"
@implementation MenuGeneralAppDelegate
@synthesize window;
@synthesize rootViewController;
- (void)applicationDidFinishLaunching:(UIApplication *)application {
// Carregar el rootViewController
RootViewController *viewController = [[RootViewController alloc] init];
self.rootViewController = viewController;
[viewController release];
// fer visible el view del rootViewController
[window addSubview:[rootViewController view]];
[window makeKeyAndVisible];
}
- (void)dealloc {
[rootViewController release];
[window release];
[super dealloc];
}
@end
D'aquesta manera fem que el delegate carregui i inicialitzi el rootViewController i mostri el seu view, que definirem a la pròpia classe.
Ara el que podem fer es crear el menú principal a on trobarem les diferents opcions que volem crear. Dins de IB creem un nou arxiu del tipus View, el guardem com a "MenuPrincipal.xib" i adjuntem al nostre projecte.
Ara podem "decorar" la finestra i posar un títol amb un label i adjuntar tres botons, un que s'anomeni "joc", un altre "Instruccions" i finalment "Rànquing". Al "atributs inspector" de cada botó, li podem assignar un tag que ens servirà de referència més endavant, per mantenir l'ordre el botó de joc serà el "0", el de instruccions "1" i el de rànquing "2".
Ara a la instància del UIView la fem del tipus MenuPrincipal i al menú, a sota File triem "Write class files...", creem la classe amb la seva implementació i la fem del tipus UIView ja que en Cocoa Touch no es fa servir NSView. El resultat es aquest:
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
@interface MenuPrincipal : UIView {
}
@end
#import "MenuPrincipal.h"
@implementation MenuPrincipal
@end
Ara el que he intentat es seguir el model MVC i es separar les funcions de visualitsació i control, per això he creat una nova classe UIViewController a la que he anomenat MenuPrincipalController, que tindrà una única funció i que es "seleccionarOpcioMenuPrincipal", que derivarà al rootViewController la opció escollida. El codi quedarà d'aquesta manera:
#import <UIKit/UIKit.h>
@class RootViewController;
@interface MenuPrincipalController : UIViewController {
RootViewController *rootViewController;
}
@property (nonatomic, assign) RootViewController *rootViewController;
-(IBAction)seleccionarOpcioMenuPrincipal:(id)sender;
@end
#import "MenuPrincipalController.h"
@implementation MenuPrincipalController
@synthesize rootViewController;
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) {
// Initialization code
}
return self;
}
-(IBAction)seleccionarOpcioMenuPrincipal:(id)sender{
[rootViewController seleccionarOpcioMenuPrincipal:sender];
}
- (void)dealloc {
[super dealloc];
}
@end
La funció seleccionarOpcioMenuPrinicpal el que fa es cridar al rootViewController per que aquest mostri la finestra que volem.
Per que això funcioni hem de tornar al IB i fer que el File's Owner sigui del tipus MenuPrincipalController i que cada botó es connecti amb aquest per cridar la funció eleccionarOpcioMenuPrinicpa.
En realitat podria haver fet que File's Owner fos del tipus RootViewController i cridar la funció directament, però penso que es millor que cada View tingui el seu propi controlador.
Ara per fi tenim que definir el RootViewController i per fer-ho tindrem que fer varies coses, peró comencem per el que tenim fet. El header quedaría així
#import <UIKit/UIKit.h>
@class MenuPrincipalController;
@interface RootViewController : UIViewController {
MenuPrincipalController *menuPrincipalController;
}
@property (nonatomic, retain) MenuPrincipalController *menuPrincipalController;
- (void) seleccionarOpcioMenuPrincipal:(id)sender;
@end
i fer la implementació, que de moment la podem fer així:
#import "RootViewController.h"
#import "MenuPrincipalController.h"
@implementation RootViewController
@synthesize menuPrincipalController;
- (void)loadView {
[super loadView];
// carreguem el menu principal
MenuPrincipalController *menuController=[[MenuPrincipalController alloc] initWithNibName:@"MenuPrincipal" bundle:nil];
self.menuPrincipalController=menuController;
[menuController release];
// mostrem el view
self.menuPrincipalController.rootViewController=self;
[self.view addSubview:menuPrincipalController.view];
}
- (void) seleccionarOpcioMenuPrincipal:(id)sender{
NSLog(@"Opcio: %d",[sender tag]);
}
- (void)dealloc {
[menuPrincipalController release];
[super dealloc];
}
@end
El primer que farà aquesta classe es crear un view del tipus MenuPrincipal a través del MenuPrincipalController i ho fem des de loadView. Hem cridat la classe MenuPrincipalController i com argument li hem donat el nom de l'arxiu .xib que volíem carregar.
Seguidament, he definit el mètode "seleccionarOpcioMenuPrincipal" que l'únic que far es mostrar el tag del botó que premem a la consola. Ara podem exportar i veurem que tot funciona com esperàvem.
Per continuar amb el menú, pensem un moment a sobre com funcionarà les seccions del menú i veurem que cadascuna tindrà la seva pròpia funcionalitat, però tindran en comú que tindran l'opció de tornar al menú principal. Per això he decidit crear una classe que es digui "SeccioController" i que tant les noves classes que creem heretin d'aquesta. Potser hi ha una forma millor de fer això? Aquí mostro com ho he fet jo:
#import <UIKit/UIKit.h>
@class RootViewController;
@interface SeccioController : UIViewController {
RootViewController *rootViewController;
}
@property (nonatomic, assign) RootViewController *rootViewController;
- (IBAction) tornarAlMenuPrincipal;
@end
#import "SeccioController.h"
#import "RootViewController.h"
@implementation SeccioController
@synthesize rootViewController;
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) {
// Initialization code
}
return self;
}
- (IBAction) tornarAlMenuPrincipal{
[rootViewController tornarAlMenuPrincipal];
}
- (void)dealloc {
[super dealloc];
}
@end
La classe hereta de UIViewController
El mètode "tornarAlMenuPrincipal" l'únic que far es cridar al mètode del mateix nom de la classe RootViewController i que aviat definirem.
Ara podem crear les classes per les diferent seccions, en concret crearem el següents arxius:
-"Joc.h", "Joc.m", "JocController.h", "JocController.m"
-"Instruccions.h", "Instruccions.m", "InstruccionsController.h", "InstruccionsController.m"
-"Ranquing.h", "Ranquing.m", "RanquingController.h", "Ranquing.m"
Com les tres seccions es faran exactament igual, només canviant el títol de cada finestra, només descriuré el process per la primera.
La classe Joc, ara per ara no farà res, es per això que ens queda així:
#import <UIKit/UIKit.h>
@interface Joc : UIView {
}
@end
#import "Joc.h"
@implementation Joc
- (void)dealloc {
[super dealloc];
}
@end
La classe JocController hereta de SeccioController i de moment, tampoc cal fer-hi res més, ens queda així:
#import <UIKit/UIKit.h>
#import "SeccioController.h"
@interface JocController : SeccioController {
}
@end
#import "JocController.h"
@implementation JocController
- (void)dealloc {
[super dealloc];
}
Ara a InterfaceBuilder anem a File>New... i escollim View, guardem l'arxiu com a "Joc.xib" i l'adjuntem al projecte. Ara el View el fem de la classe "Joc" y el File's Owner el fem de la classe "JocController". Ara podem decorar la finestra. Posem un label i li donem el nom "Joc". Posem també un botó, posem com a text "Tornar" i fem una connexió al File's Owner, en la que l'acció serà "tornarAlMenuPrincipal". Finalment, fem una connecxió des de el File's Owner cap a Joc per definir-lo com la view. Guardem, tanquem i tornem a Xcode.
Ara per fer una petita prova tindrem que definir tant "seleccionarOpcioMenuPrincipal" com "tornarAlMenuPrincipal" de RootViewController. Per fer-ho caldrà també una variable del tipus "JocController" així que el header quedarà així:
#import <UIKit/UIKit.h>
@class MenuPrincipalController;
@class JocController;
@interface RootViewController : UIViewController {
MenuPrincipalController *menuPrincipalController;
JocController *jocController;
UIView *viewActual;
}
@property (nonatomic, retain) MenuPrincipalController *menuPrincipalController;
@property (nonatomic, retain) JocController *jocController;
- (void) loadJocController;
- (void) seleccionarOpcioMenuPrincipal:(id)sender;
- (void) tornarAlMenuPrincipal;
@end
A més de definir una nova variable del tipus JocController, hem fet un punter que es diu "viewActual" i que aviat utilitzarem. També hem fet un mètode que carregarà la secció "Joc". Veiem la implementació:
#import "RootViewController.h"
#import "MenuPrincipalController.h"
#import "JocController.h"
@implementation RootViewController
@synthesize menuPrincipalController;
@synthesize jocController;
- (void)loadView {
[super loadView];
// carreguem el menu principal
MenuPrincipalController *menuController=[[MenuPrincipalController alloc] initWithNibName:@"MenuPrincipal" bundle:nil];
self.menuPrincipalController=menuController;
[menuController release];
// mostrem el view
self.menuPrincipalController.rootViewController=self;
[self.view addSubview:menuPrincipalController.view];
}
- (void) loadJocController{
JocController *seccioController=[[JocController alloc] initWithNibName:@"Joc" bundle:nil];
self.jocController=seccioController;
self.jocController.rootViewController=self;
[seccioController release];
}
- (void) seleccionarOpcioMenuPrincipal:(id)sender{
if(jocController==nil)[self loadJocController];
[menuPrincipalController.view removeFromSuperview];
[self.view addSubview:jocController.view];
}
- (void) tornarAlMenuPrincipal{
[jocController.view removeFromSuperview];
[self.view addSubview:menuPrincipalController.view];
}
- (void)dealloc {
[menuPrincipalController release];
[super dealloc];
}
@end
El mètode loadJocController es igual que com hem creat el View general i el MenuPrincipal. Sembla una mica rebuscat, però es veu que per alguna raó es millor fer-ho així. Sento no tenir una millor expliació, però es com està fet en els exemples d'Apple... Bé, aquest mètode només es llençarà una vegada en cas de que sigui necessari, des de "seleccionarOpcioMenuPrincipal". Aquest mètode esborrarà el menú principal i dibuixarà la finestra del joc. Finalment, el mètode "tornarAlMenuPrincipal" farà la inversa i ens tornarà al menú principal. Ara podem fer una prova i veurem com funciona!
Ara només cal repetir lo mateix per la resta de seccions i casi ho tindrem. Per que funcioni, tindrem que ajustar el mètode "seleccionarOpcioMenuPrincipal" per que esculli quina finestra volem, però es ben senzill gracies al "tag" del sender:
- (void) seleccionarOpcioMenuPrincipal:(id)sender{
switch ([sender tag]) {
case 0:
if(jocController==nil)[self loadJocController];
viewActual=jocController.view;
break;
case 1:
if(instruccionsController==nil)[self loadInstruccionsController];
viewActual=instruccionsController.view;
break;
case 2:
if(ranquingController==nil)[self loadRanquingController];
viewActual=ranquingController.view;
break;
}
// canviar la View
[menuPrincipalController.view removeFromSuperview];
[self.view addSubview:viewActual];
}La raó per la que hem utilitzar la variable "viewActual" es per poguer esborrar la finestra que estem veient, i només cal un petit canvi que sería aquest:
- (void) tornarAlMenuPrincipal{
// tornar al menú principal
[viewActual removeFromSuperview];
[self.view addSubview:menuPrincipalController.view];
}I amb això... hem acabat!!!! Visca!!! Bé, però si no ha sigut suficient i voleu una mica més, podem fer que el canvi de finestra sigui animat, com si fos un panell al que li donem la volta. Per fer-ho aprofitem un mètode de la classe UIViewController i en comptes del mètode que hem fet servir per mostrar una secció, fem:
// canviar la View
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:1];
[UIView setAnimationTransition:UIViewAnimationTransitionFlipFromRight forView:self.view cache:YES];
[menuPrincipalController.view removeFromSuperview];
[self.view addSubview:viewActual];
[UIView commitAnimations];
i per tornar, lo mateix, però a la inversa:
- (void) tornarAlMenuPrincipal{
// tornar al menú principal
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:1];
[UIView setAnimationTransition:UIViewAnimationTransitionFlipFromLeft forView:self.view cache:YES];
[viewActual removeFromSuperview];
[self.view addSubview:menuPrincipalController.view];
[UIView commitAnimations];
/*[viewActual removeFromSuperview];
[self.view addSubview:menuPrincipalController.view];*/
}I ara si, fins aquí es on he arribat, espero les vostres propostes!
16 juliol 2008 00:40
Hm... veig que m'he fet un embolic entre tants comentaris i codis... si hi ha alguna manera, ho corregiria 
Bé, per ser constructius, canviaré de tema i el que voldria comentar es que si voleu treure la barra superior on diu l'estat de la bateria, teniu que adjuntar una nova propietat al Info.plist que es digui "UIStatusBarHidden" amb un valor del tipus Boolean i seleccionar-lo.
Per que tot quedi mes clar,he pujat l'exercici a la plana:
Menu General - 1.5MB
http://rapidshare.com/files/129999300/MenuGeneral.zip.html
16 juliol 2008 00:54
Al final hauré d'activar l'edició de comentaris, encara que no m'agrada. De moment l'arreglo jo. Ja veurem més endavant si permeto editar-lo.
Sembla ser que el problema, ha vingut de ficar dos "====" seguits. Ja que per comprovar-ho, cerco "\n====+\n" i al trobar-ne dos de seguits, hi troba a faltar un "\n".
====
====
16 juliol 2008 08:57
Per cert, encara no havia provat el teu joc, ho acabo de fer ara. És desesperant, amb l'emulador, no n'encerto ni la meitat, i n'hi molts que baixen molt ràpid. Però és veu prou entretingut. 
16 juliol 2008 09:01
Em costa una mica d'entendre el que fas.
La veritat, jo tampoc sé com funcionen les finestres a l'iPhone, així que no em fassis massa cas. 
El que si puc dir-te és que veig que a cada vista li has creat una classe, però la classe no fa res d'especial. Així que és una ximpleria fer-ho. Només cal deixar la classe que hi ha per defecte "UIView". Només és necessari heretar classes de vistes si voleu que la vista faci coses fora del normal, com en el cas de dibuixar els punts. Però si la finestra només ha de mostrar botons i altres controls no cal.
El mateix passa amb els controladors. Si tens el "SeccioController" que ja fa tota la funcionalitat, no cal crear una classe controlador per a cada vista, si no ha de fer res d'especial. Tanmateix puc entendre que en un futur, la funcionalitat serà específica, així que no està de més tenir-les ja.
El que no feia servir jo, eren els controladors amb "NSViewController", i veig que són més útils del que pot semblar.
Jo, que sóc ximplet
, hem feia una controlador que repetia la funcionalitat del "NSViewController".
Tanmateix, has de conèixer que hi ha altres possibilitats de carregar fitxers NIB, i associar el "File's Owner" a la mateixa classe (per exemple la "RootViewController". Però llavors s'ha de tenir en compte que cada cop que es carregui un fitxer NIB, es cridarà al mètode "awakeFromNib" del "File's Owner" (el RootViewController).
Bé, continuarem més tard.
16 juliol 2008 09:28
La veritat es que en aquest exemple no calia fer una classe per cada view i controlador, però ho he fet perquè el normal es que cada secció tingui la seva funcionalitat. Aquest exercici es per fer una plantilla que es pugui adaptar fàcilment per altres jocs i volia mostrar la manera en que volia fer-ho. Potser quan ajunti el menú al joc es veurà més clarament.
A sobre l'explicació que he donat.... la veritat es que em costa bastant explicar-me, però he deixat un link de l'arxiu final i que potser serà més útil.
Per cert, gràcies per corregir el post! I per mi, no cal que activis l'edició dels comentaris, era només que em sabia greu després de la "parrafada" veure que no es mostrava com jo pensava, però ara ja està bé!
16 juliol 2008 11:03
