Archivi tag: iOS

Cocos2d, Sprite, Nodi e Bitizens

Rieccoci con un breve tutorial, nato dalla passione per una serie di giochi per iphone, come Tiny Towers e Pocket Planes.

In questi giochi vediamo la presenza dei Bitizens, dei piccoli “cittadini” a 8 bit, customizzabili e realizzati con un inconfondibile stile retro a 8 bit.

Incuriosito dalle infinite possibilità di personalizzazione, apro il package del gioco nella sua versione mac, prendo le immagini che compongono i bitizens e provo a ricostruirli, assemblando in un piccolo progetto una classe MyBitizens che permette la creazione di questi sprite formati da piu livelli. La costruzione dei Bitizens e’ relativamente facile. Basta usare un programma di grafica che permette il disegno su piu livelli e disegnare la testa e le braccia su un livello, la maglia su un altro , un altro livello per i pantaloni e uno per il cappello. Inoltre e’ possibile sovrapporre anche occhiali e intere suite di vestiti gia assemblati. Esportando tutti i livelli come png differenti, con trasparenza, possiamo poi riassemblarle in maniera molto semplice. Per l’esempio è stato utilizzato il motore 2d Cocos2D per Mac Osx e Xcode come ambiente di sviluppo.

Screenshots dell'applicazione BitizenExamples,

Screenshots dell’applicazione BitizenExamples,

Definiamo innanzitutto un header per la nostra classe MyBitizen, definendo le caratteristiche pubbliche di questo oggetto:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#import "cocos2d.h"
#import "CCSprite.h"
typedef enum {
BitizenTags_hair=1,
BitizenTags_head=2,
BitizenTags_eyes=3,
BitizenTags_shirt=4,
BitizenTags_pants=5,
BitizenTags_shoes=6,
BitizenTags_glasses=7,
BitizenTags_hat=8,
BitizenTags_suite=100
} BitizenTags;

@interface MyBitizen : CCNode
{
float scaleFactor;
int startSpriteX;
int startSpriteY;
CCSprite *eyes;
}

-(id) initWithScaleFactor:(float)pScaleFactor withPositionX:(int)positionX withPositionY:(int)positionY;
-(void)showSuite:(BOOL)flag;
-(void)showShirt:(BOOL)flag;
-(void)showGlasses:(BOOL)flag;
-(void)showHat:(BOOL)flag;
-(void)setSuite:(int)suiteId;
-(void)setFemale:(BOOL)flag;

@end

Definiamo cosi che il nostro MyBitizen estende un CCNode ha delle caratteristiche come uno scaleFactor che ci permetterà di fare lo scale di tutto il nodo, una x e una y iniziale e dei metodi per mostrare un vestito unico, mostrare o no la maglietta, gli occhiali, il cappello e se maschio o femmina.

Il costruttore del nostro Bitzen accetta in input il parametro dello scale da effettuare (per fare lo zoom perche sono a bassa risoluzione e quindi piccoli), la x e la y di partenza per calcolare poi gli offsets di ogni pezzo da montare.

Il nostro Bitizen si comporrà cosi di un insieme di sprite , ognuno rappresentante uno dei livelli che compone il nostro personaggio. Questo ci permette di avere personaggi dalle customizzazioni praticamente infinite.

Nel costruttore generiamo la base del personaggio (capelli, occhi, maglia, pantaloni e scarpe)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
-(id) initWithScaleFactor:(float)pScaleFactor withPositionX:(int)positionX withPositionY:(int)positionY
{
    if( (self=[super init])) {
        scaleFactor = pScaleFactor;

        startSpriteX = positionX;
        startSpriteY = positionY;
        //disegno sprite
       
        [self setFemale:NO];
       
        //maglietta
        int shirtOffsetX=1 *scaleFactor;
        int shirtOffsetY=-3 *scaleFactor;
        CCSprite *shirt = [CCSprite spriteWithFile:@"bitizen-shirt.png"];
        [shirt setPosition:ccp(startSpriteX+shirtOffsetX,startSpriteY+shirtOffsetY)];
        shirt.scale = scaleFactor;
        ccColor3B shirtColor = ccc3(arc4random() % 255,arc4random() % 255,arc4random() % 255);
        [shirt setColor:shirtColor];
        [shirt.texture setAliasTexParameters];
        [self addChild:shirt z:200 tag:BitizenTags_shirt];
       
        //testa
        int headOffsetX = 1*scaleFactor;
        int headOffsetY = 2*scaleFactor;
        CCSprite *head = [CCSprite spriteWithFile:@"bitizen-head.png"];
        [head setPosition:ccp(startSpriteX+headOffsetX,startSpriteY+headOffsetY)];
        head.scale = scaleFactor;
        ccColor3B headColor = skinColors[ arc4random() % 5];
        [head setColor:headColor];
        [head.texture setAliasTexParameters];
        [self addChild:head z:100 tag:BitizenTags_head];
       
       
        //occhi
        int eyesOffsetX = 1*scaleFactor;
        int eyesOffsetY = 5*scaleFactor;
        eyes = [CCSprite spriteWithFile:@"bitizen-eyes.png"];
        [eyes setPosition:ccp(startSpriteX+eyesOffsetX,startSpriteY+eyesOffsetY)];
        eyes.scale = scaleFactor;
        ccColor3B eyesColor = ccc3(0,0,0);
        [eyes setColor:eyesColor];
        [self addChild:eyes z:600 tag:BitizenTags_eyes];
        //blink =[CCBlink actionWithDuration:1.5 blinks:1] ;
       
        //CCActionInterval *blink =[CCRepeatForever actionWithAction:[CCBlink actionWithDuration:1.5 blinks:1]];
        CCBlink *blink =[CCBlink actionWithDuration:.2 blinks:arc4random() %2];
        CCDelayTime *delay = [CCDelayTime actionWithDuration:(arc4random()%5)+3];
       
        CCSequence *sequence = [CCSequence actions:blink,[CCShow action],delay,[CCShow action],nil];
       
        CCRepeatForever *forever = [CCRepeatForever actionWithAction:sequence];
       
        [eyes runAction:forever];
       
        //pantaloni
        int pantsOffsetX=1*scaleFactor;
        int pantsOffsetY=-6*scaleFactor;
        CCSprite *pants = [CCSprite spriteWithFile:@"bitizen-pants.png"];
        [pants setPosition:ccp(startSpriteX+pantsOffsetX,startSpriteY+pantsOffsetY)];
        pants.scale = scaleFactor;
        ccColor3B pantsColor = ccc3(arc4random() % 255,arc4random() % 255,arc4random() % 255);
        [pants setColor:pantsColor];
        [pants.texture setAliasTexParameters];
        [self addChild:pants z:520];
       
        //scarpe
        int shoesOffsetX=1*scaleFactor;
        int shoesOffsetY=-7*scaleFactor;
        CCSprite *shoes = [CCSprite spriteWithFile:@"bitizen-shoes.png"];
        [shoes setPosition:ccp(startSpriteX+shoesOffsetX,startSpriteY+shoesOffsetY)];
        shoes.scale = scaleFactor;
        shoes.flipY=YES;
        ccColor3B shoesColor = ccc3(arc4random() % 255,arc4random() % 255,arc4random() % 255);
        [shoes setColor:shoesColor];
        [shoes.texture setAliasTexParameters];
        [self addChild:shoes z:510];
                     
    }
    return self;
}

Negli altri metodi utilizziamo la getChildByTag per trovare di volta in volta lo sprite contenuto nel nostro CCNODE esteso (self) e cercare il livello interessato per interagire con le sue caratteristiche, ad esempio per mostrare gli occhiali o rimuoverli:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-(void)showGlasses:(BOOL)flag{
   
    if (flag){
        //occhiali
        int glassesOffsetX = 1*scaleFactor;
        int glassesOffsetY = 5*scaleFactor;
        CCSprite *glasses = [CCSprite spriteWithFile:@"bitizen-glasses0.png"];
        [glasses setPosition:ccp(startSpriteX+glassesOffsetX,startSpriteY+glassesOffsetY)];
        glasses.scale = scaleFactor;
        [glasses.texture setAliasTexParameters];
        [self addChild:glasses z:620 tag:BitizenTags_glasses];
    }else{
        [self removeChildByTag:BitizenTags_glasses cleanup:NO];
    }

}

Ogni ccSprite che compone l’immagine deve essere posizionato tramite degli offset x e y che vanno calcolati sulla base delle immagini che andiamo a sovrapporre.

Inoltre lavorando ad 8 bit, e’ importante che ogni texture supporti l’alias,mediante il metodo:

1
[skin.texture setAliasTexParameters];

Nel nostro layer di cocos2d che descrive la scena, andiamo a utilizzare in maniera molto semplice la nostra classe bitizen per mostrarne alcuni a video:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
        double bitizenScaleFactor = 2.0;
        int posY = 120;
        MyBitizen *bitizen1 =[[MyBitizen alloc]initWithScaleFactor:bitizenScaleFactor withPositionX:100 withPositionY:posY];
       
        [bitizen1 showHat:YES];
        [self addChild:bitizen1];
       
       
        MyBitizen *bitizen2 =[[MyBitizen alloc]initWithScaleFactor:bitizenScaleFactor withPositionX:150 withPositionY:posY];
        [bitizen2 setSuite:2];
        [bitizen2 showShirt:NO];
        [self addChild:bitizen2];

        //bitizens random
        for (int i =0; i<10; i++) {
            int xpos = arc4random() %600 +20;
            MyBitizen *bitizenX =[[MyBitizen alloc]initWithScaleFactor:bitizenScaleFactor withPositionX:xpos withPositionY:posY];
            [bitizenX showHat:CCRANDOM_0_1()<0.5f];
            [bitizenX showGlasses:CCRANDOM_0_1()<0.5f];
            [bitizenX setFemale:CCRANDOM_0_1()<0.5f];
            [self addChild:bitizenX];
        }

Infine una caratteristica di questi Bitizens è il fatto che sbattono gli occhi. Realizziamo questo rendendo visibile invisibile il layer degli occhi, registrando una sequenza di animazione che sfrutta il Blink , un DelayTime e un RepeatForever:

1
2
3
4
5
6
7
8
        CCBlink *blink =[CCBlink actionWithDuration:.2 blinks:arc4random() %2];
        CCDelayTime *delay = [CCDelayTime actionWithDuration:(arc4random()%5)+3];
       
        CCSequence *sequence = [CCSequence actions:blink,[CCShow action],delay,[CCShow action],nil];
       
        CCRepeatForever *forever = [CCRepeatForever actionWithAction:sequence];
       
        [eyes runAction:forever];

Clicca qui per scaricare il progetto XCode dell’esempio BitizenExample

Parte del codice è stato preso da questo tutorial.

 

Kingdom:Undead Control Handlers

Nel disegno del codice, mi sono accorto di una cosa che a tendere rendeva illeggibile e poco gestibile il codice, la gestione degli eventi mouse, touch e keyboard da parte dei CCLayer di cocos2d.

Dovendo mostrare diversi CCNode contenenti UI realizzate con cocos, come ad esempio la scheda del giocatore, piuttosto che un menu di scelta, una popup modale magari realizzata con texture fatte adhoc, la gestione degli eventi rimaneva tutta comunque nell’implementazione del cclayer contenente tutta la parte principale del gioco.

Così ho deciso di estendere il cclayer, aggiungendo la possibilità di fornire gli eventi a dei delegati. Questo mi permette di “responsabilizzare” il codice al meglio, lasciando ad esempio la gestione del click su un certo pulsante solo nel ccnode che lo contiene.

Questo e’ un esempio di come estendere il CCLayer:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//
// CCLayerControlServer.h
// KingdomUndead
//
// Created by Andrea Magini on 04/04/12.
// Copyright (c) 2012 metalide. All rights reserved.
//

#import
#import "cocos2d.h"
#import "KeyboardControlHandler.h"
#import "MouseControlHandler.h"

@interface CCLayerControlServer : CCLayer{
@private

idkeyboardDelegate;
id mouseDelegate;
}
@property (nonatomic,retain)id keyboardDelegate;
@property (nonatomic,retain)id mouseDelegate;

@end

E questa la sua implementazione:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
//
// CCLayerControlServer.m
// KingdomUndead
//
// Created by Andrea Magini on 04/04/12.
// Copyright (c) 2012 metalide. All rights reserved.
//

#import "CCLayerControlServer.h"

@implementation CCLayerControlServer
@synthesize keyboardDelegate;
@synthesize mouseDelegate;
-(id) init
{
if( (self=[super init])) {
self.isMouseEnabled = YES;
self.isKeyboardEnabled = YES;
}
return self;
}

-(BOOL) ccKeyUp:(NSEvent *)event
{

[keyboardDelegate handleKeyUp:event];
return NO;
}

-(BOOL) ccMouseUp:(NSEvent *)event{
[mouseDelegate handleMouseUp:event];
return YES;
}

-(BOOL) ccMouseDragged:(NSEvent *)event{
[mouseDelegate handleMouseDragged:event];
return YES;
}

-(void)dealloc{
[keyboardDelegate release];
[mouseDelegate release];
[super dealloc];
}
@end

In questo modo tutti gli eventi intercettati dal layer vengono passati di volta in volta ai delegati registrati.
In ogni momento posso ad esempio deregistrare il layer come delegato per gli eventi e registrare un particolare CCNODE che gestisce una popup che si e’ appena mostrata a video.

I delegati devono avere un particolare Protocol per rispondere agli eventi mouse, o tastiera (o touch su iOS)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//
//  KeyboardControlHandler.h
//  KingdomUndead
//
//  Created by Andrea Magini on 04/04/12.
//  Copyright (c) 2012 metalide. All rights reserved.
//

#import <Foundation/Foundation.h>
#import <Carbon/Carbon.h>

//per gestire il controllo sui vari ccnode e layers in modo da abilitare e disabilitare il controller

@protocol KeyboardControlHandler <NSObject>
@required
-(void) handleKeyUp:(NSEvent *)event;
@end

Questo permette di snellire il codice del CCLayer evitando di mettere tonnellate di if nei vari mousebuttonup, down, keyup etc e andando invece a gestire gli eventi
in maniera opportuna nei vari ccnode contenuti nella scena principale. Un esempio pratico è quello per la gestione degli eventi sull’HUD , lasciandoli gestire direttamente al CCNODE in questione, separandolo dagli eventi di gioco.

Come registrare un delegato:

1
2
3
@interface HudNode : CCNode<KeyboardControlHandler>{

}

Nel codice del CCLayer principale, durante l’init registro il CCNode dell’hud, e poi lo imposto come delegato per la gestione degli eventi tastiera

1
2
   [self addChild:hudNode];
   [self setKeyboardDelegate:hudNode];

e poi nel CCNode che implementa l’hud, implemento il metodo del Protocol KeyboardControlHandler

1
2
3
4
5
6
7
-(void) handleKeyUp:(NSEvent *)event{
    CCLOG(@"key up: %@", [event characters] );
    UInt16 keyCode = [event keyCode];
    if (keyCode == kVK_Space) {
       //fai qualcosa :)
    }
}

Spero sia chiaro 🙂

Creare Classi Statiche e Singleton in Objective C

Per creare una classe che sia raggiungibile da ogni parte del nostro codice, abbiamo due alternative: realizzare una classe statica con metodi e attributi statici, oppure implementare il pattern del Singleton.
Preferisco generalmente la seconda scelta più orientata ad un design pulito del nostro codice e rimanendo legato alla programmazione ad oggetti.

Per creare una classe statica, con attributi e metodi statici definiamo l’interfaccia:

1
2
3
4
@interface MyStaticClass : NSObject
+ (int)getIntMethod;
+ (void)setIntMethod:(int)value;
@end

e poi implementiamola:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#import "MyStaticClass.h"

@implementation MyStaticClass

static int staticValue = 1;

+ (int)getIntMethod {
  return staticValue;
}

+ (void)setIntMethod:(int)value {
  staticValue = value;
}

+ (id)alloc {
  [NSException raise:@"Non può essere istanziata format:@"Static class 'ClassName' non può essere instanziato"];
  return nil;
}

@end

importante l’override dell’alloc per non permettere alla classe di essere istanziata.

Per un approccio più elegante e sicuramente più riusabile, implementiamo invece il Design Pattern del Singleton:

1
2
3
4
5
6
7
8
9
@interface MySingleton : NSObject
{
}
-(int)getIntMethod;
-(void)setIntMethod:(int)value;

+ (MySingleton *) m_MySingleton;
 
@end

Implementiamo la nostra interfaccia:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#import "MySingleton.h"
 
@implementation MySingleton
 
static MySingleton *m_MySingleton =nil;
static int staticValue = 1;


+ (MySingleton *) m_MySingleton
{
  if (m_MySingleton == nil)
  {
    //creo l'istanza
    m_MySingleton = [[super allocWithZone:NULL] init];
  }
  return m_MySingleton;
}
 
- (int)getIntMethod {
  return staticValue;
}

- (void)setIntMethod:(int)value {
  staticValue = value;
}
+ (id)allocWithZone:(NSZone *)zone
{
 //sincronizzo in modo da rendere la fase di allocazione ThreadSafe
 //in modo da gestire più thread che richiedono contemporaneamente il Singleton
 @synchronized(self)
 {
   if (m_MySingleton == nil)
   {
     m_MySingleton = [super allocWithZone:zone];
     return m_MySingleton;
   }
 }
 return nil;
}
 
- (id)copyWithZone:(NSZone *)zone
{
 //non e' possibile clonarlo e quindi ritorna sempre la stessa istanza.
 return self;
}
 
- (id)retain
{
 return self;
}
 
- (NSUInteger)retainCount
{
 //in questo modo il suo refCount non scenderà mai a zero.
 return NSUIntegerMax;
}
 
- (void) release
{
  //non fa nulla.
}
 
- (id)autorelease
{
 return self;
}
 
@end

Piccola correzione. I metodi nel singleton ovviamente non devono essere statici (tranne il metodo che permette l’accesso all’istanza).