AppDot5-MiniJoc

Aquest post em quedarà una mica llarg, així que comencem...

Amb l'o après a la serie de exercicis de dot, he volgut fer un petit joc, que segur es pot millorar en moltes areas, es per això que el penjo, a veure que aprenc (i si algú més aprèn quelcom, millor que millor :))

Aquest joc l'he fet per l'iPhone, però es pot adaptar sense problemes per l'escriptori.

En el joc el que passa es que cauen dots aleatòriament i els has d'agafar abans de que surtin de la finestra. Cada vegada que encertis un, et donen un punt! Així de senzill, però suficient per començar :)

Bé, l'o primer ha sigut adaptar una mica el propi dot :

//Dot.h
#import <UIKit/UIKit.h>


@interface Dot : NSObject {
CGPoint center;
CGFloat radius;
UIColor *color;
int speed;
}

- (id) init;

#pragma mark -
#pragma mark Getters
#pragma mark -

- (CGPoint) center;
- (float) radius;
- (UIColor*) color;
- (int) speed;
- (float) y;

#pragma mark -
#pragma mark Setters
#pragma mark -

- (void) setCenter:(CGPoint)newCenter;
- (void) setRadius:(float)newRadius;
- (void) setColor:(UIColor *)newColor;
- (void) setSpeed:(int)newSpeed;
- (void) setDotAt:(CGPoint)newCenter withRadius:(float)newRadius withColor:(UIColor *)newColor withSpeed:(int)newSpeed;

#pragma mark -
#pragma mark Methods
#pragma mark -

- (void) move;

@end

Com veieu hi ha una nova variable, speed, que es la velocitat amb la que caurà el dot i es definirà des de el view. No cal un mètode per accedir a la velocitat, però l'he posat per propers canvis. El que si cal es un mètode per accedir a la posició, així podem treure el dot quan surti de la finestra. Es podia fer amb "center" directament, però m'ha semblat més net així.

Un altre mètode que he afegit es el de canviar totes les variables d'un cop

- (void) setDotAt:(CGPoint)newCenter withRadius:(float)newRadius withColor:(UIColor *)newColor withSpeed:(int)newSpeed;

Ho he fet així en comptes de fer un initWithCenter:withRadius... per un altre canvi que estic pensant fer, però potser ho canvio...

Bé, la implementació d'aquesta classe l'he feta així;

#import "Dot.h"


@implementation Dot

- (id) init{
if (self = [super init]) {
center = CGPointMake(100,100.0);
radius = 20.0;
color= [UIColor redColor];
speed=0;
}
return self;
}

#pragma mark -
#pragma mark Getters
#pragma mark -

- (CGPoint) center{
return center;
}

- (float) radius{
return radius;
}

- (UIColor*) color{
return color;
}

- (int) speed{
return speed;
}

- (float) y{
return center.y;
}

#pragma mark -
#pragma mark Setters
#pragma mark -

- (void) setCenter:(CGPoint)newCenter{
center = newCenter;
}

- (void) setRadius:(float)newRadius{
radius = newRadius;
}

- (void) setColor:(UIColor *)newColor{
color = newColor;
}

- (void) setSpeed:(int)newSpeed{
speed = newSpeed;
}

- (void) setDotAt:(CGPoint)newCenter
withRadius:(float)newRadius
withColor:(UIColor *)newColor
withSpeed:(int)newSpeed
{
[self setCenter:newCenter];
[self setRadius:newRadius];
[self setColor:newColor];
[self setSpeed:newSpeed];
}

#pragma mark -
#pragma mark Methods
#pragma mark -

- (void) move{
[self setCenter:CGPointMake(center.x,center.y+speed)];
}


#pragma mark -
#pragma mark Sortir
#pragma mark -

- (void) dealloc {
[color release];
[super dealloc];
}

@end

Ara anem a veure els canvis de la pròpia vista:

Jaime  sep  09 juliol 2008 13:03

Hm.. encara no, m'he oblidat comentar el mètode move, i es el que s'ocupa de moure el dot afegint la seva pròpia velocitat (speed) a la posició

Ara si, vegem com ha quedat el view:

//MainView.h

#import <UIKit/UIKit.h>
#import <CoreGraphics/CoreGraphics.h>
#import "Dot.h"


@interface MainView : UIView {
IBOutlet UILabel *comptador;
NSTimer *timer;
NSMutableArray *dots;
int punts;
int nextDotCount;
}

// Animacions
- (void) animacio;

// Gestio de Dots
- (void) newRandomDot;

// Puntuacio
- (void) mostrarPuntuacio;

// Metodes Auxiliars
- (int) randomNumberFrom:(int)from to:(int)to;
- (UIColor *) randomColor;

@end

Aquí trobem varies variables noves, la primera es un outlet anomenat comptador, que mostrarà els punts que tenim. Després trobem un NSTimer, que utilitzarem per que cada cert temps actualitzem la posició dels dots i així fer l'animació.
També trobem un NSMutableArray que portarà la relació de dots que tenim a la view.
Tenim un int que utilitzarem per sapiguer els punts que tenim i un altre que es diu nextDotCount, i que utilitzarem per decidir quan apareixerà el proper dot.

Els mètodes que tenim son "animació" que el cridarà el NSTimer, i que farà que cada dot caigui una mica i finalment cridarà el drawRect que mostrarà els canvis.

Tenim un altre mètode que s'ocuparà de crear un dot amb valors aleatoris, la raó del perquè es troba al view es perquè potser més endavant voldré fer diferents efectes en el mètode de crear nous dots, però potser serà millor fer una classe nova. Ja ho veurem.

També hi ha un mètode per mostrar els punts acumulats al comptador.

Finalment, he fet dos mètodes auxiliars per facilitar la feina, un ens dona un valor aleatori entre dos nombres que facilitem i l'altre escollirà un color aleatori.

Veiem la implementació

Jaime  sep  09 juliol 2008 13:18

Aquí es on he fet el que he pogut, però bé, funciona :)

Anem per parts, el primer es com s'inicia el joc:

-(void) awakeFromNib{	
// iniciar array de dots
dots=[[NSMutableArray alloc] init];

// iniciar temporitzador per les animacions
timer = [NSTimer scheduledTimerWithTimeInterval:(1.0/30.0) target:self selector:@selector(animacio) userInfo:nil repeats:YES];

// compte enrera per un nou dot
srand((unsigned)time(0));
nextDotCount=[self randomNumberFrom:10 to:50];

// posar comptador a zero
punts=0;
[self mostrarPuntuacio];

}

Primer iniciem l'array "dots" així podrem afegir els dots quan volguem, després posem a funcionar el timer dient-l'hi que volem que cridi el mètode animació. En aquest cas he fet que es cridi 30 vegades per segon.
Donem una llavor (?) al generador de nombres aleatoris i decidim quan apareixerà el primer dot.
Posem el comptador a zero i.... a veure els altres mètodes.
- (void) newRandomDot{
// crear el dot amb dades aleatories
Dot *nouDot=[[Dot alloc] init];
int radius=[self randomNumberFrom:10 to:40];
[nouDot setDotAt:CGPointMake([self randomNumberFrom:50 to:250],-radius)
withRadius:radius
withColor:[self randomColor]
withSpeed:[self randomNumberFrom:4 to:15]];

// afegir el nouDot al llistat de dots
[dots addObject:nouDot];
[nouDot release];

// definir l'aparicio del proper dot
nextDotCount=[self randomNumberFrom:5 to:20];
}
Amb aquest mètode creem els dots, el primer es crear l'instancia d'un dot. Després definim el radius, ho fem aquí perquè així podem posar el dot just a sobre el marge superior( restem de 0 el radi i aquest serà el center.y)
Ara donem valors aleatoris en aquest dot.
LListem el dot al array dots i ja que el tenim en el array, podem treure la referència nouDot.
Finalment definim quan apareixerà el proper dot.
Passem a l'animació
- (void) animacio{
// Fer caure els dots
Dot *dot;
int i;
for(i=[dots count]-1;i>=0;i--){
dot=[dots objectAtIndex:i];
[dot move];
// esborrar el dot si surt de la finestra
if(dot.y-dot.radius>480)[dots removeObjectAtIndex:i];
}

// Crear un dot aleatoriament
if(nextDotCount--<1)[self newRandomDot];

// Fer caure els dots
[self setNeedsDisplay];
}
Aquí passem per tots els objectes de l'array i fem que un per un es mogui, i si surt del view, l'esborrem. La raó de que el for() vagi cap enrere es per no alterar l'indexació.
Quan tots els dots s'han mogut, restem 1 de nextDotCount, i quan arribi a zero, es crearà un nou dot.
Ara podem fer que la view s'actualitzi.
- (void)drawRect:(CGRect)rect {

// Definir el contexte
CGContextRef ctx = UIGraphicsGetCurrentContext();

// Dibuixar els punts
Dot *dot;
int i;
for(i=0;i<[dots count];i++){
dot=[dots objectAtIndex:i];
CGRect dotRect= CGRectMake(dot.center.x - dot.radius,
dot.center.y - dot.radius,
2*dot.radius,
2*dot.radius);
[[dot color] set];
CGContextFillEllipseInRect(ctx, dotRect);
}
}
En aquest cas, es igual l'ordre en el que es mou el for, així que l'únic que s'ha de fer es veure la llista de dots i dibuixar-los.

Jaime  sep  09 juliol 2008 13:40

Ara el que voldrem es que quan encertem un punt, aquest desaparegui i ens donin el nostres apreciats punts! Per fer-ho:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
//trobar el punt
CGPoint touch=[[touches anyObject] locationInView:self];

// comparar amb tots els dots
Boolean encerta=NO;
Dot *dot;
double distanceA;
double distanceB;
int i;
for(i=[dots count]-1;i>=0;i--){
dot=[dots objectAtIndex:i];
distanceA=touch.x-dot.center.x;
distanceB=touch.y-dot.center.y;
// veure si encerta dins del dot
if(dot.radius> sqrt( (distanceA*distanceA)+(distanceB*distanceB) ))
{
// treure el dot
[dots removeObjectAtIndex:i];
// guanyar punts
encerta=YES;
punts++;
}
}
// actualitzar comptador només si cal
if(encerta)[self mostrarPuntuacio];
}

Primer trobem on em pressionat, aquest punt el tindrem que comparar amb tots els punts, llistats al array dots. Aquí un altre vegada el for ()es a la inversa perquè en cas de que encertem un o mes dots, els treurem. Utilitzo el boolean "encerta" per només actualitzar el comptador quan calgui. I bé, això es tot ! Només em queda mostrar els métodes auxiliars
- (int) randomNumberFrom:(int)from to:(int)to{
to++;
return rand()%to+from;
}

- (UIColor *) randomColor{
switch ([self randomNumberFrom:0 to:5]) {
case 0:
return [UIColor redColor];
break;
case 1:
return [UIColor greenColor];
break;
case 2:
return [UIColor blueColor];
break;
case 3:
return [UIColor orangeColor];
break;
case 4:
return [UIColor brownColor];
break;
case 5:
return [UIColor blackColor];
break;
}
return [UIColor redColor];
}
- (void) dealloc {
[dots release];
[super dealloc];
}

Amb aixó tenim un joc de lo més senzill, però que espero que us inspiri. Ara a veure quan Apple em dona el codi perquè el pogui jugar al meu iPod Touch!!!!

Millores que espero fer aviat: diferents puntuacions, dots que restin punts, objectes especials, so....

Altres millores que encara no sé com les faré: pantalla d'inici de joc, pantalla d'instruccions, ranking...

Bé, agrairia tot l'ajut que podeu donar-me en la qüestió de codi,però també ideas i millores que faríeu.

Jaime  sep  09 juliol 2008 13:56

Bufff, ho deixaré per demà. Que és molt llarg.

Estaria bé pengessis el projecte comprimit en algun servidor d'aquests gratuïts que hi ha per Internet. Així ens el podríem descarregar i provar-lo. ;-)

Xin  sep  09 juliol 2008 22:00

Al final m'ho he llegit ara. Algunes coses que he vist i que et poden interessar:
- La inicialització de l'array de punts i d'altres variables és millor fer-ho als "init". El timer ja has fet ve de crear-lo aquí.
- Vigila amb la funció "awakeFromNib" perquè aquests dies he descobert que si carregues manualment un fitxer Nib, i el propietari és aquesta classe, un cop carregat també es crida al mètode "awakeFromNib".
- Quan fas el "for" prova de provar-ho amb l'estil 2.0: "for ( Dot *aDot in dots )". Això et passarà per tots els punts i cada cop te l'assigna a la variable "aDot". :D
- No és bo esborrar els punts de l'array mentre hi estàs treballant. És millor guardar-los en un altre array i esborrar-los després amb "removeObjectsInArray" o guardar els índexs i esborrar-los amb "removeObjectsAtIndexes". Jo utilitzaria el primer. ;-)
- Pensa que encara que vagis en sentit invers, si un punt va més ràpid, pot arribar abans al fons que un altre.
- Si utilitzes un binding per la puntuació, t'estalviaràs feina. Només canviar el valor, s'actualitzarà el valor.

Bé, una mica per sobre el que he vist. Espero no haver burxat massa. :)

Xin  sep  09 juliol 2008 22:28

Gràcies per els teus consells, sempre son benvinguts, els miraré demà. Si em recomanes un servidor, pujaré l'arxiu comprimit, es que no en coneix-ho cap...

Jaime  sep  09 juliol 2008 22:41
Xin  sep  09 juliol 2008 23:17

Bé, ha sigut molt fàcil, aquí està el link:

MiniJoc -1,4 MB
http://rapidshare.com/files/128576338/MiniJoc.zip.html

Jaime  sep  10 juliol 2008 10:43

Ja m'ho he mirat tot! Et comento el que he aconseguit i el que no...

Bé, he intentat passar algunes coses al init, però no sé perquè, si ho faig, no funciona el joc... Així que encara que no acabo d'entendre el First Responder del tot, he intentat solucionar el problema passant-ho tot al initWithCoder:

- (id) initWithCoder:(NSCoder *)coder {
if (self = [super initWithCoder:coder]) {
// iniciar array de dots
dots=[[NSMutableArray alloc] init];

// compte enrera per un nou dot
srand((unsigned)time(0));
nextDotCount=[self randomNumberFrom:10 to:30];

// iniciar temporitzador per les animacions
timer = [NSTimer scheduledTimerWithTimeInterval:(1.0/30.0) target:self selector:@selector(animacio) userInfo:nil repeats:YES];

// posar comptador a zero
punts=0;
[self mostrarPuntuacio];
}
return self;
}

Desprès he fet les correccions al mètode amb el que passo per els arrays, i ho he fet de les dues maneres que m'has dit, per practicar. Veig que les dues son més àgils, però no estic segur de utilitzar-les correctament... Ho he fet així:
// Fer caure els dots i treure els que surten de la finestra
/*NSMutableIndexSet *indexes=[[NSMutableIndexSet alloc] init];
int i=0;
for(Dot *dot in dots){
[dot move];
if(dot.y-dot.radius>480)[indexes addIndex:i];
i++;
}
[dots removeObjectsAtIndexes:indexes];
[indexes release];*/

/**/
NSMutableArray *dotsFora=[[NSMutableArray alloc] init];
for(Dot *dot in dots){
[dot move];
if(dot.y-dot.radius>480)[dotsFora addObject:dot];
}
[dots removeObjectsInArray:dotsFora];
[dotsFora release];
Per cert, quina opció creus que es més ràpida? A mi em sembla que deu ser "removeObjectsAtIndexes:indexes", ja que sap exactament quins son el index dels objectes a treure.

I sobre el problema de que un dot desaparegui abans que un altre, no es cap problema, l'ordre de comprovació bé de l'ordre en l'array, no de la posició a la view. Al fer-ho a la inversa el que passa es que al treure un dot de l'array, l'ordre canvia, però només des de l'index actual fins al últim objecte en l'array, però com aquests ja els has mogut, no passa res. De totes maneres, ho feia així perquè en Actionscript no hi ha l'opció "removeObjectsAtIndexes" o "removeObjectsInArray"

Finalment, he intentat fer el binding, però.... no he trobat els bindings! Sembla que no n'hi ha en el iPhone! Que estrany, em semblava haver-los vist, però deu ser que no...

Bé, ara continuaré amb d'altres millores :)

Jaime  sep  10 juliol 2008 12:39

Bé, una petita correcció, la crida [self mostrarPuntuacio]; si que té que estar a awakeFromNib, perquè sinó, el UILabel encara no és a la finestra

Jaime  sep  10 juliol 2008 12:45

Selecciona'l abans d'enviar el commentari