Sunday, August 23, 2009

iPhone Coding Part2

Η πρώτη μας κλάση είναι η "Delecaτη" μας. Πολύ απλή η λειτουργία της μιας και το μόνο μόνο που έχει να κάνει είναι να δημιουργήσει ένα instance του View που θα χρησιμοποιήσουμε να το γυρίσει 90 μοίρες ώστε το παιχνίδι μας να γίνει Landscape και εξαφανίσει την μπάρα πάνω-πάνω.
Στα γρήγορα τα δύο αρχεία μας θα μοιάζουν κάπως έτσι:

To .h...

#import <UIKit/UIKit.h>

@class DeflectorViewController;

@interface DeflectorAppDelegate : NSObject <UIApplicationDelegate, UIScrollViewDelegate> {
IBOutlet UIWindow *window;

}

@property (nonatomic, retain) UIWindow *window;

@end

είναι πολύ απλό, δεν έχουμε αλλάξει και πολλά εκτός από το πρωτόκολλο (θα τα μάθουμε στα Objective-C tutorials) για την UIScrollViewDelegate.
Η @class γνωστοποιεί στον compiler ότι χρειαζόμαστε και την DeflectorViewController. Θα μπορούσαμε να κάνουμε ένα απλό #import αλλά αυτό είναι πιο λειτουργικό:)
To IBOutlet είναι κάτι το αδιάφορο, υπάρχει απλά και μόνο για να μπορεί ο Interface Builder να "δει" την μεταβλητή window. Μιας και δεν θα χρησιμοποιήσουμε πολύ τον Interface Builder σε αυτό το παιχνίδι το προσπερνάμε.
Μιας και είπαμε IB πρέπει κάποια στιγμή να πάμε να ορίσουμε τον ViewController...ελπίζω να το θυμηθώ :)
Το @property δημιουργεί getters και setters για την Window. Βασικά η synthesize το κάνει αυτό αλλά εδώ δηλώνουμε την πρόθεση μας.

Το .m:

#import "DeflectorAppDelegate.h"
#import <QuartzCore/QuartzCore.h>
#import <CoreGraphics/CoreGraphics.h>
#import "MainView.h"
#define degreesToRadians(x) (M_PI * x / 180.0)

@implementation DeflectorAppDelegate
@class MainView;
@synthesize window;

- (void)applicationDidFinishLaunching:(UIApplication *)application {

MainView* mainView = [[MainView alloc] initWithFrame: CGRectMake(0,0,480, 320)];

mainView.transform = CGAffineTransformIdentity;
mainView.transform = CGAffineTransformMakeRotation(degreesToRadians(90));

mainView.frame = CGRectMake(0,0,320,480);
[[UIApplication sharedApplication] setStatusBarHidden:YES animated:NO];
[UIApplication sharedApplication].statusBarOrientation = UIInterfaceOrientationLandscapeRight;
[window addSubview:mainView];
[window makeKeyAndVisible];
}

- (void)dealloc {
[MainView release];
[window release];
[super dealloc];
}
@end


Τίποτα τρομερό, μερικά imports, ένα macro που όπως μαρτυρά και το όνομα του μετατρέπει radians->degrees και όλη η ουσία είναι στη μέθοδο applicationDidFinish.
Οι Delecate κλάσεις είναι βοηθητικές κλάσεις. Ποιο συγκεκριμένα η DeflectorAppDelegate έχει δηλωθεί αυτόματα από τον Wizard ότι είναι η βοηθητική κλάση της εφαρμογής μας. Το iPhone ψάχνει να βρει αυτή την κλάση όταν φορτώνει το παιχνίδι μας και καλεί κάποιες από τις ρουτίνες που περιέχει.
Όταν φορτωθεί το παιχνίδι εκτελείται η μέθοδος applicationDidFinishLaunching, παλιά λέγανε αυτές τις ρουτίνες hooks.
Η δικιά μας applicationDidFinish δημιουργεί ένα νέο View στο οποίο πάνω και θα δουλέψουμε. Το View ας το δούμε σαν ένα καμβά που πάνω εκεί ζωγραφίζουμε. Μπορούμε να έχουμε πολλά views το ένα μέσα στο άλλο και να φτιάξουμε πολύπλοκα σχήματα, αρκεί να τα κάνουμε instantiate και να δηλώνουμε τον πατέρα τους.
Εμείς χρειαζόμαστε μόνο ένα. Θα σχεδιάσουμε την κλάση MainView σιγά-σιγά η οποία θα υποστηρίζει τη λειτουργία του mainView (έτσι ονομάζουμε το ένα και μοναδικό view μας).
Με την alloc κάνουμε allocate χώρο στην μνήμη και με την initWithFrame δημιουργούμε το view μας με τις διαστάσεις (480,320).
Αυτό φαντάζει λάθος μιας και η ανάλυση του iPhone είναι 320χ480 αλλά αμέσως μετά με την transform γυρίζουμε το view μας με pivot point το κεντρικό του σημείο και έτσι έρχεται "ακριβώς" με την ανάλυση που έχουμε σε Landscape.
Καλώντας την addSubView κάνουμε κύριο view το mainView στο μοναδικό παράθυρο που έχει το iPhone και αμέσως μετά το εμφανίζουμε.
Η dealloc καλείται στο τέλος για να καθαρίσει ότι έχουμε κάνει instantiate να πούμε εδώ ότι το iPhone δεν έχει τον υπέροχο Garbage Collector του MacOS, είναι ένα από τα λίγα πράγματα που αφήσανε έξω και κατά τη γνώμη μου καλά κάνανε. Το iPhone έχει μια αμεσότητα που δεν την έχουν τα υπόλοιπα τηλέφωνα, δεν χρειαζόμαστε πολλά OS threads να τρέχουν στο background.

Πάμε σε μερικά header files που θα μας βοηθήσουν να οργανώσουμε λίγο τη σκέψη μας. Πάντα έχω ένα global.h να υπάρχει, κακογραμμένο μεν, βοηθάει δε :)

#define LAZER_LAYER_ID 297
#define ROTTOR_LAYER_ID 298
#define POWER_LAYER_ID 299
#define SCORE_LAYER_ID 300

#define EMPTY_TILE 0
#define MIRROR 1
#define DOUBLE_MIRROR 2
#define PRISM 3
#define BOMB 4
#define GATES 5
#define Y_TILE 6
#define END 7
#define TELEPORT 8
#define RED_FILM 9
#define BLUE_FILM 10
#define GREEN_FILM 11

#define BLOCK_1_FULL 20
#define BLOCK_1_UP 21 //2 rows Standard up block
#define BLOCK_1_DOWN 22
#define BLOCK_1_LEFT 23
#define BLOCK_1_RIGHT 24
#define BLOCK_1_UP_LEFT_CORNER 25
#define BLOCK_1_UP_RIGHT_CORNER 26
#define BLOCK_1_DOWN_LEFT_CORNER 27
#define BLOCK_1_DOWN_RIGHT_CORNER 28
#define BLOCK_EMPTY 29

#define DANGER_WALL 50
#define DANGER_WALL_CORNER 51

#define ROTTOR 98
#define LAZER 99

#define REAL_LAZER_BEAM YES
#define BEAM_EXTENTION NO
#define START_FROM_CENTER YES
#define START_FROM_END_OF_TILE NO

#define STORE_ANGLE_IN_ARRAY YES
#define DO_NOT_STORE_ANGLE_IN_ARRAY NO

#define TELEPORTATION YES


Να θυμήσω μόνο ότι στην Objective-C το YES/NO είναι Boolean.

Πάμε να φτιάξουμε ένα άλλο header file που θα μας βοηθήσει στην ανάπτυξη. Θα λέγεται Maps.h
Κανονικά ο χάρτης θα πρέπει να είναι σε αρχείο και να φορτώνεται αργότερα και όχι βέβαια να είναι "καρφωτός".

static const int objectSize = 32;
static const int objectHalfSize=16;

typedef struct {
int objectsInMap;
int objectsPerRow;
int rowsInMap;
NSString* backgroundImage;
Byte map[300];
float rot[300];
} LevelStruct;

static const LevelStruct level[1]={
// Level 1
150, //objectsInMap
15, //objectsPerRow
10, //rowsInMap
@"dummyPattern.png",
{ //Objects
0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,
0,2,0,5,0,0,1,0,0,0,0,0,0,0,0, //29
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, //44
0,0,0,0,1,0,0,0,11,0,0,0,0,0,0, //59
0,0,0,0,0,0,0,9,0,0,0,0,0,0,0, //74
0,0,0,0,0,0,6,0,0,0,0,0,0,0,0, //89
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, //104
0,0,3,0,0,0,0,0,0,0,0,0,0,0,0, //119
99,0,0,0,0,0,1,0,0,0,0,0,0,0,0, //134
8,0,0,0,0,0,0,0,0,0,0,0,0,0,0
},
{ //Angles
0,0,0,0,0,0,0,0,0,0,0,135,0,0,0,
0,0,0,0,0,0,315,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,90,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,225,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
}}

Στην αρχή δηλώνουμε το μέγιστο μέγεθος του κάθε "αντικειμένου" μας. Θα ονομάσουμε τα αντικείμενα μας tiles. Tiles θα λέγονται τα τετράγωνα 32χ32 pixels που θα βρίσκονται στην οθόνη μας και θα είναι τοποθετημένα δίπλα δίπλα ανά 15άδες. Κάπως έτσι δηλαδή


Το halfsize μπορεί να σας φαίνεται χαζό αλλά αν μπορούμε να γλυτώσουμε μια διαίρεση ας τη γλυτώσουμε.
Παρακάτω φτιάχνουμε μια απλή structure που θα έχει τα βασικά για να ξεκινήσουμε και είναι όλα ευκολονόητα εκτός από τα 2 τελευταία.
Το map array 300 bytes θα είναι ο χάρτης της πίστα με τα tiles μας.
Το rot float array είναι ένα παράλληλο array που θα περιέχει τις γωνίες των tiles όπως είναι ορισμένες από τον level designer....ε οκ είναι καρφωτές προς το παρόν :)
Οι γωνίες έχουν το κλασσικό orientation με τις 0 μοίρες να είναι στα δεξιά (East) και να μετράμε counterclockwise.
Αν ορίσουμε ας πούμε το laser να "κοιτάει" προς τα πάνω, αυτό θα είναι στις 90 μοίρες. Να πω μόνο ότι το orientation έχει άμεση σχέση με το πως έχουν σχεδιαστεί τα PNGs. Ολα θα πρέπει να έχουν το ίδιο orientation.
Αμέσως δηλώνουμε μια μούφα χάρτη. Τα μηδενικά δηλώνουν ότι εκεί δεν θα υπάρχει tile (είπαμε είναι δοκιμαστικός χάρτης).
Τα υπολοιπα νούμερα αντιστοιχούν στα tiles που δηλώσαμε στο Globals.h. Θα μπορούσαμε να χρησιμοποιήσουμε εδώ ολογράφως (MIRROR, LAZER...) αλλά θα μας χάλαγε το όμορφο layout :)
Αμέσως μετά δηλώνονται και οι γωνίες, ΟΙ ΑΡΧΙΚΕΣ γωνίες.

Πάμε να γράψουμε την MainView κλαση μας τώρα. ΔΕΝ θα δώσω το .h file με τη μια γιατί είναι λίγο ζώο :) και θα το πάμε σιγά-σιγά γράφοντας πρώτα το implementation και δηλώνοντας ότι χρειάζεται στο .h.
H κλαση μας θα κάνει inherit την UIScrollView μιας και αργότερα θα προσθέσουμε scrolling ιδιότητες οπότε θα είναι κάπως έτσι

@interface MainView : UIScrollView

Όταν γίνει initialize θα εκτελεστεί η initWithFrame οπότε πρέπει να την γράψουμε :)

- (id)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
selectedTile = -1;
lazerCount = -1;
[self lazyLoadImages:0];
[self setupTileMask:0];
[self setupLevel];
}
return self;
}

Εδώ κάνουμε initialize 2 μεταβλητές που θα χρησιμοποιοήσουμε αργότερα και εκτελούμε 3 μεθόδους.
Η LazyLoadImages παίρνει σαν παράμετρο το level number που στη περίπτωση μας έχουμε καρφωτο το 0 και φορτώνει τα PNGs που χρειάζεται. Λέγεται lazy γιατί όπως θα δείτε σε κοιτάει να δει τι χρειάζεται πραγματικά να φορτώσει μιας και κρατάει τα κοινά PNGs που έχουν τα Levels.
H setupTileMask δημιουργεί μια μάσκα πάνω από τα βοηθητικά tiles (γωνίες, blocks, τοίχοι κτλ). Η μάσκα αυτή βοηθάει στο να ξέρουμε που θα σταματήσουμε την ακτίνα μας.

/* We create a "mask" over the tiles, covering the pixels where the lazer beam stops or cannot penetrate
the array has 320 rows with 15 integers.
*/
-(void)setupTileMask:(int)_level
{ //First init array to 0s
for (int i=0 ; i<320 ; i++)
for (int j=0;j<level[_level].objectsPerRow ;j++)
blockMask[i][j] = 0;

//Scan the Map
for (int rowFirstTileIdx=0 ; rowFirstTileIdx < level[_level].objectsInMap ; rowFirstTileIdx+level[_level].objectsPerRow) //0, 15, 30, 45, 60, 75.....135
{
/* tileIdx points to

*/
int filler = 0;
for (int tileIdx = rowFirstTileIdx ; tileIdx < rowFirstTileIdx + level[_level].objectsPerRow ; tileIdx++) //Points to those 15 tiles per row
{
switch (level[_level].map[tileIdx]){ //Check Tile Type
case BLOCK_1_FULL:
for (filler=0;filler<objectSize;filler++) //Filler points to those 32 pixels hight of every tile
blockMask[(rowFirstTileIdx * objectSize) + filler][tileIdx] = 0xFFFFFFFF;
break;
case BLOCK_EMPTY:
for (filler=0;filler<objectSize;filler++)
blockMask[(rowFirstTileIdx * objectSize) + filler][tileIdx] = 0x0;
break;
case BLOCK_1_UP:
for (filler=0;filler<objectHalfSize ;filler++) // Mask first 16 rowpixels
blockMask[(rowFirstTileIdx * objectSize) + filler][tileIdx] = 0xFFFFFFFF;
for (filler=objectHalfSize;filler<objectSize ;filler++) //Make transparent the rest
blockMask[(rowFirstTileIdx * objectSize) + filler][tileIdx] = 0x0;
break;
case BLOCK_1_DOWN:
for (filler=0;filler<objectHalfSize ;filler++) // Mask first 16 rowpixels
blockMask[(rowFirstTileIdx * objectSize) + filler][tileIdx] = 0xFFFFFFFF;
for (filler=objectHalfSize;filler<objectSize ;filler++) //Make transparent the rest
blockMask[(rowFirstTileIdx * objectSize) + filler][tileIdx] = 0x0;
break;
case BLOCK_1_LEFT:
for (filler=0;filler<objectSize;filler++)
blockMask[(rowFirstTileIdx * objectSize) + filler][tileIdx] = 0xFFFF0000;
break;
case BLOCK_1_RIGHT:
for (filler=0;filler<objectSize;filler++)
blockMask[(rowFirstTileIdx * objectSize) + filler][tileIdx] = 0x0000FFFF;
break;
case BLOCK_1_UP_LEFT_CORNER:
for (filler=0;filler<objectHalfSize ;filler++) // Mask first 16 rowpixels
blockMask[(rowFirstTileIdx * objectSize) + filler][tileIdx] = 0xFFFF0000;
for (filler=objectHalfSize;filler<objectSize ;filler++) //Make transparent the rest
blockMask[(rowFirstTileIdx * objectSize) + filler][tileIdx] = 0x0;
break;
case BLOCK_1_UP_RIGHT_CORNER:
for (filler=0;filler<objectHalfSize ;filler++) // Mask first 16 rowpixels
blockMask[(rowFirstTileIdx * objectSize) + filler][tileIdx] = 0x0000FFFF;
for (filler=objectHalfSize;filler<objectSize ;filler++) //Make transparent the rest
blockMask[(rowFirstTileIdx * objectSize) + filler][tileIdx] = 0x0;
break;
case BLOCK_1_DOWN_LEFT_CORNER:
for (filler=0;filler<objectHalfSize ;filler++) // Mask first 16 rowpixels
blockMask[(rowFirstTileIdx * objectSize) + filler][tileIdx] = 0x0;
for (filler=objectHalfSize;filler<objectSize ;filler++) //Make transparent the rest
blockMask[(rowFirstTileIdx * objectSize) + filler][tileIdx] = 0xFFFF0000;
break;
case BLOCK_1_DOWN_RIGHT_CORNER:
for (filler=0;filler<objectHalfSize ;filler++) // Mask first 16 rowpixels
blockMask[(rowFirstTileIdx * objectSize) + filler][tileIdx] = 0x0;
for (filler=objectHalfSize;filler<objectSize ;filler++) //Make transparent the rest
blockMask[(rowFirstTileIdx * objectSize) + filler][tileIdx] = 0x0000FFFF;
break;
}
}
}
}

-(void)loadImageNeededInLevel:(int)_imageId
:(int)_level
:(CGImageRef)_imageRef
:(NSString*)_imageFilename
{
for (tmpGlobalCounterVariable=0 ; tmpGlobalCounterVariable<level[_level].objectsInMap ; tmpGlobalCounterVariable++)
{
if (level[_level].map[tmpGlobalCounterVariable] == _imageId){
image = [UIImage imageNamed:_imageFilename];
_imageRef = image.CGImage;
}
}
}

/* Load only the images that are used in current level
DO NOT unload all the images but only those that are not used
*/
-(void)lazyLoadImages:(int)_level
{
[self loadImageNeededInLevel:MIRROR: _level: mirrorImageRef: @"mirror64.png"];
[self loadImageNeededInLevel:DOUBLE_MIRROR: _level: doubleMirrorImageRef: @"doubleMirror.png"];
[self loadImageNeededInLevel:BLOCK_1_FULL: _level: block1FullImageRef:@"BLOCK_1_FULL"];
[self loadImageNeededInLevel:BLOCK_1_UP: _level: block1UpImageRef:@"BLOCK_1_UP"];
[self loadImageNeededInLevel:BLOCK_1_DOWN: _level: block1DownImageRef:@"BLOCK_1_DOWN"];
[self loadImageNeededInLevel:BLOCK_1_LEFT: _level: block1LeftImageRef:@"BLOCK_1_LEFT"];
[self loadImageNeededInLevel:BLOCK_1_RIGHT: _level: block1RightImageRef: @"BLOCK_1_RIGHT"];
[self loadImageNeededInLevel:BLOCK_1_UP_LEFT_CORNER: _level: block1UpLeftCornerImageRef: @"BLOCK_1_UP_LEFT_CORNER"];
[self loadImageNeededInLevel:BLOCK_1_UP_RIGHT_CORNER: _level: block1UpRightImageRef: @"BLOCK_1_UP_RIGHT_CORNER"];
[self loadImageNeededInLevel:BLOCK_1_DOWN_LEFT_CORNER: _level: block1DownLeftImageRef: @"BLOCK_1_DOWN_LEFT_CORNER"];
[self loadImageNeededInLevel:BLOCK_1_DOWN_RIGHT_CORNER: _level: block1DownRightImageRef: @"BLOCK_1_DOWN_RIGHT_CORNER"];
[self loadImageNeededInLevel:LAZER: _level: lazerImageRef: @"lazer_-.png"];
[self loadImageNeededInLevel:Y_TILE: _level: YImageRef: @"Y.png"];
[self loadImageNeededInLevel:ROTTOR: _level: rottorImageRef: @"arrowRottor.png"];
[self loadImageNeededInLevel:PRISM: _level: prismImageRef: @"prism.png"];
[self loadImageNeededInLevel:BOMB: _level: bombImageRef: @"bomb.png"];
[self loadImageNeededInLevel:END: _level: endImageRef: @"end.png"];
[self loadImageNeededInLevel:GATES: _level: gatesImageRef: @"gates.png"];
[self loadImageNeededInLevel:RED_FILM: _level: redImageRef: @"red.png"];
[self loadImageNeededInLevel:GREEN_FILM: _level: greenImageRef: @"green.png"];
[self loadImageNeededInLevel:TELEPORT: _level: teleportImageRef: @"teleport.png"];

}

Ελπίζω να είναι κατανοητός ο κώδικας προσπάθησα να τον γράψω όσο πιο απλά γίνεται ώστε να τον καταλάβω όταν τον ξαναδιαβάσω 2 μήνες αργότερα μιας και το αρνητικό IQ μου δε με βοηθάει πολλές φορές :)

Το setupLevel έχει πολύ γράψιμο και να πω την αλήθεια κουράστηκα. Στο επόμενο.....

0 σχόλια:

Post a Comment