Tutorial: zerstörbares Terrain mit cocos2d

Hallo Zusammen,

bei iBallerburg musste ich zerstörbares Terrain programmieren. In diesem Tutorial möchte ich erklären wie man sotwas realisieren kann. Die für das Tutorial benötigten Ressourcen stelle ich euch als zip zur Verfügung.

Grafik eines Zip Ordners Und so sieht es aus wenn wir fertig sind:

zerstörbares Terrain mit cocos2d

Es gibt sicherlich viele Wege zerstörbares Terrain zu programmieren. Der hier präsentierte Weg arbeitet mit einem unsichbaren logischen Layer gegen den die Kollisionen geprüft werden und einen visuellen Layer, der die Spielgrafik des Terrains beinhaltet. Wenn ich mich recht erinnere arbeitet der Klassiker Worms nach einem ähnlichem Prinzip.

In diesem Tutorial werden wir uns eines nützlichen Helfers bedienen: den Chipmunk-Spacemanager. Auch der Spacemanager ist im zip bereits hinterlegt und muss nur noch dem Projekt hinzugefügt werden. Erstellen wir also ein neues cocos2d Projekt, fügen den Spacemanager hinzu und aktivieren ARC wie es hier beschrieben ist. Wir ersetzten HelloWorldLayer durch die bereinigte Version im zip.

Anschließend ergänzen wir unseren Spacemanager als Instanzvariabeln in HelloWorldLayer.h

#import "SpaceManagerCocos2d.h"

@interface Game : CCLayer
{
	//Spacemanager
	SpaceManagerCocos2d *smgr;

}
@property (readonly) SpaceManager* spaceManager;

Im HelloWorldLayer.m synthetisieren wir die Getter und Setter Methoden des Spacemanagers und erweitern wir unsere init Methode.

// direkt vor @implementation definieren wir uns ein paar Konstanten
#define kMountainBlockCollisonType      1
#define kBombCollisionType              2
#define kBoarderCollisionType           3
#define kBottomOffset                   13

// direkt nach @implementation synthetisieren wir die Getter und Setter Methode unserer Spacemanger Property
@synthesize spaceManager = smgr;

- (id)init{

    if ((self = [super init])) {

       [[[CCDirector sharedDirector] touchDispatcher] addTargetedDelegate:self priority:0 swallowsTouches:NO];

        // Spacemanager erzeugen
        smgr = [[SpaceManagerCocos2d alloc] init];
        smgr.constantDt = 1/55.0; 
        [smgr start];
        [self addChild:[smgr createDebugLayer] z:999];

        // die Begrenzungen der Welt bauen
	// [self setupWorldBounds];

        // das "logische" Terrain erstellen
        //[self setupLogicTerrain];

    }
    return self;
}

Zunächst registrieren wir uns beim TouchDispatcher, damit wir auch Touch Events weitergeleitet bekommen und verarbeiten können. Anschließend wird der Spacemanager erzeugt und auch gleich gestartet. Der DebugLayer ist zunächst nur eine Programmierhilfe und kann später abgestellt werden. Anschließend rufen wir zwei Methoden auf, die wir nun als nächstes angehen:

-(void)setupWorldBounds{

    CGSize winSize = [[CCDirector sharedDirector] winSize];

    cpShape *ground;
    ground = [smgr addSegmentAtWorldAnchor:cpv(0,kBottomOffset) toWorldAnchor:cpv(winSize.width ,kBottomOffset) mass:STATIC_MASS radius:1];
    ground->collision_type = kBoarderCollisionType;
    ground = [smgr addSegmentAtWorldAnchor:cpv(winSize.width,kBottomOffset) toWorldAnchor:cpv(winSize.width,winSize.height) mass:STATIC_MASS radius:1];
    ground->collision_type = kBoarderCollisionType;
    ground = [smgr addSegmentAtWorldAnchor:cpv(winSize.width,winSize.height) toWorldAnchor:cpv(0,winSize.height) mass:STATIC_MASS radius:1];
    ground->collision_type = kBoarderCollisionType;
    ground = [smgr addSegmentAtWorldAnchor:cpv(0,winSize.height) toWorldAnchor:cpv(0,kBottomOffset) mass:STATIC_MASS radius:1];
    ground->collision_type = kBoarderCollisionType;
}

In setupWorldBound begrenzen wir unsere Spielwelt. Um dies tun zu können müssen wir erst wissen mit welchem Gerät wir arbeiten bzw. wie groß der Bildschirm ist. Der CCDirector liefert uns diese Information. Zum Begrenzen der Welt nutzen wir eine statische (STATIC_MASS) Chipmunk From (cpShape). Bei Ereugen hilft uns der Spacemanager. Zusätzlich setzen wir noch einen Kollisionstypen, um später auf Kollisionen unterschiedlicher Spielelemente unterschiedlich reagieren zu können.

// im Header fügen wir eine Instanzvariable hinzu:
NSMutableArray      *_mountainArray;

-(void) setupLogicTerrain
{
    NSDictionary *mountainDictionary = [self getDictionaryFromPlist:@"mountainParameter"];
    NSMutableArray *myTerrainDataArray=[NSMutableArray array];
    myTerrainDataArray = [mountainDictionary objectForKey:@"array"];
    _mountainArray = [NSMutableArray array];                                               

    // **************
    //
    // Shape the mountain with blocks
    //
    // **************

    int mountainSegmentWidth = 5;
    int counter = 0;

    for(int x=0 ; x<=[myTerrainDataArray count]-1; x++){
        NSInteger tempShapeHeight = [[myTerrainDataArray objectAtIndex:x] integerValue];
        for (int i=0; i < tempShapeHeight/10 ; i++) {                           cpShape	*mountainShape = [smgr addRectAt:ccp(counter*mountainSegmentWidth,[[myTerrainDataArray objectAtIndex:x] integerValue]-10*i) mass:STATIC_MASS width:mountainSegmentWidth height:mountainSegmentWidth rotation:0];             
            mountainShape->u=1.0;
            mountainShape->collision_type=kMountainBlockCollisonType;
            cpShapeNode *nodeMountain = [cpShapeNode nodeWithShape:mountainShape];
            nodeMountain.spaceManager = smgr;
            nodeMountain.visible = YES;
            nodeMountain.color = ccRED;
            [self addChild:nodeMountain];
            [_mountainArray addObject:nodeMountain];
        }
        counter++;
    }
}

Als erstes lesen wir die Daten aus der mountainParameter.plist, welche ich dem zip beigelegt habe. Die dazugehörigen Methoden folgen im nächsten Absatz. Dieses Array aus der plist wird in myTerrainDataArray gespeichert. In der äußeren Schleife durchlaufen wir nun myTerrainDataArray und lesen den hinterlegten Wert aus (tempShapeHeight). tempShapeHeight ist die Höhe des Berges an der entsprechenden x-Koordinate. In einer inneren Schleife zählen wir nun von dieser Höhe nach unten und erstellen alle 10 Pixel eine cpShapeNode. Auch hier wird wieder die bequeme Methode des Spacemanagers verwendet. Achtet darauf, dass wir nun den Kollisionstyp des Berges verwenden.  Das erstelle Objekt fügen wir dem Layer (self) hinzu und auch unserem. _mountainArray.

-(NSDictionary *)getDictionaryFromPlist:(NSString *)fileName
{
	return (NSDictionary *)[self readPlist:fileName];
}

-(id)readPlist:(NSString *)fileName
{
	NSData *plistData;
	NSString *error;
	NSPropertyListFormat format;
	id plist;

	NSString *localizedPath = [[NSBundle mainBundle] pathForResource:fileName ofType:@"plist"];
	plistData = [NSData dataWithContentsOfFile:localizedPath];

	plist = [NSPropertyListSerialization propertyListFromData:plistData mutabilityOption:NSPropertyListImmutable format:&format errorDescription:&error];

	if (!plist) {
		NSLog(@"Error reading plist from file '%s', error = '%s' ", [localizedPath UTF8String], [error UTF8String]);
		[error release];
	}
	return plist;	
}

Wie man plists ausliest habe ich hier schonmal vorgestellt. Der Quellcode ist der Vollständigkeit halber ergänzt.

Wenn wir nun auf Play drücken sehen wir schonmal einen bergiges Gebilde aus roten Klötzen.

destructible Terrain nur logisch

Zeit für etwas mehr Action – wir brauchen Kanonenkugeln. Und nicht nur eine. Wir wollen bei jedem Touch ein neue Kugel erstellen:

// im Header fügen wir abermals eine Instanzvariable hinzu:
cpCCSprite          *_bomb;

- (BOOL)ccTouchBegan:(UITouch*)touch withEvent:(UIEvent *)event
{
    if (!_bomb) {
        CGPoint pt = [self convertTouchToNodeSpace:touch];
        cpShape *shape = [smgr addCircleAt:pt mass:10 radius:8];
        _bomb = [[cpCCSprite alloc] initWithFile:@"cannonball_b.png"];
        _bomb.shape = shape;

        shape->collision_type = kBombCollisionType;

        _bomb.spaceManager = smgr;

        [self addChild:_bomb z:1];
    }

    return YES;
}

In ccTouchBegan können wir den Code zur Bombenerzeugung unterbringen. Wir wollen immer nur eine Bombe aktiv habe, da mehrere Bomben das Tutorial nur unnötig kompliziert machen. Daher prüfen wir zunächst, ob es bereits auch wirklich keine Bombe vorhanden ist. Anschließend erstellen wir mit Hilfe des Spacemanagers ein cpCCSprite Objekt. Dieses Objekt verknüpft ein CCSprite mit einem Chipmunk shape, wie wir es bereits bei dem Berg schon benutzt haben. Schauen wir was nun passiert:

destructible Terrain mit Bomb

Die Kugel wird erstellt und rollt den Berg runter – noch nicht sonderlich viel Action :(
Wir müssen noch definieren, was bei der Kollision eigentlich geschehen soll. Dazu fügen wir einen Kollision Callback in der init Methode hinzu.

[smgr addCollisionCallbackBetweenType:kBombCollisionType	  			    otherType:kMountainBlockCollisonType			       target:self					             selector:@selector(handleMountainCollision:arbiter:space:)						  		              moments:COLLISION_POSTSOLVE,nil];

Immer wenn chipmunk Shapes der Kollsisionstypen kBombCollisionType und kMountainBlockCollisonType wird nun die Methode handleMountainCollision:arbiter:space: aufgerufen.

-(BOOL) handleMountainCollision:(CollisionMoment)moment arbiter:(cpArbiter*)arb space:(cpSpace*)space
{
    if (_bomb) {
        CP_ARBITER_GET_SHAPES(arb, bombShape, otherShape);
        CGPoint hitPT = ccp((cpArbiterGetPoint(arb, 0)).x,(cpArbiterGetPoint(arb, 0)).y);

        // die shapes entfernen
        NSMutableArray *nodesToDelete = [NSMutableArray array];
        float radiusSQ = 18*18;

        for (cpShapeNode *mountainNode in _mountainArray) {

            //Get the vector from hitpoint ot nodes
            CGPoint vector = ccpSub(mountainNode.position, hitPT);

            //check if close enough to hitpoint?
            if (ccpLengthSQ(vector) < radiusSQ) {                 
               if (![nodesToDelete containsObject:mountainNode]) {                     [nodesToDelete addObject:mountainNode];                            [smgr removeAndFreeShape:mountainNode.shape];                      [self removeChild:mountainNode cleanup:YES];                  }             
            }         
        }         
        [_mountainArray removeObjectsInArray:nodesToDelete];       

        //    cpCCSprite *currentBomb = (__bridge cpCCSprite *)(bombShape->data);
        [smgr removeAndFreeShape:_bomb.shape];
        [self removeChild:_bomb cleanup:YES];
        _bomb = nil; 
    }
    return YES;
}

Wir ziehen uns zunächst die beiden kollidierenden Shapes, als auch den Kollisionspunkt vom arbiter. nodesToDelete ist ein Hilfsarray – wir wollen Objekte aus _mountainArray löschen und können dies jedoch nicht während wir gleichzeitig _mountainArray in einer Schleife durchlaufen. In der Schleife prüfen wir alle Objekte im _mountainArray (die kleinen roten Blöcke), ob Sie dich genug am Kollisionspunkt sind.
Falls dem so ist werden die Objekte zu Löschen in das nodesToDelete Array aufgenommen. Anschließend entfernen wir die Objekte vom Layer (self). Im letzten Schritt entfernen wir noch die Bombe. Die auskommentierte Zeile ist ein Weg wie man die Bombe ohne die Instanzvariable _bomb referenzieren kann. Dabei wird vom cpShape ausgegangen, welches uns der arbiter liefert und mit der darin enthaltenen Referenz hangeln wir uns zum cpCCSprite.

HIT PLAY!

destructible Terrain mit Kollision

Unsere Bombe sprengt nun tolle Löcher in den logischen Berg. Der Berg könnte allerdings noch etwas Make Up gebrauchen.

        // Instanzvariable dem Header hinzufügen
	CCRenderTexture		*_mountainLayer;

-(void) setupVisualTerrain
{
    CCSprite *mountain=[CCSprite spriteWithFile:@"mountain.png"];
    mountain.position=ccp(mountain.contentSize.width/2,mountain.contentSize.height/2+kBottomOffset);

    // Mountain Layer
    _mountainLayer = [CCRenderTexture renderTextureWithWidth:mountain.contentSize.width height:mountain.contentSize.height];
    _mountainLayer.position = ccp(mountain.contentSize.width/2,mountain.contentSize.height/2);
    [[_mountainLayer sprite] setBlendFunc: (ccBlendFunc) { GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA }];
    [self addChild:_mountainLayer z:10];

    [_mountainLayer clear:0.0f g:0.0f b:0.0f a:0.0f];
    [_mountainLayer begin];
    [mountain visit];
    [_mountainLayer end];
}

In der Methode erstellen wir eine CCRenderTexture. Eine CCRenderTexture kann man sich als eine Art durchsichtiges Blatt Papier vorstellen auch dem man zur Programmlaufzeit zeichnen kann. Wir zeichnen hier den Inhalt von mountain.png auf unser Blatt. Mit [_mountainLayer begin] startet man den Zeichenvorgang, mit visit wird gezeichnet und anschließend beenden wir die Bearbeitung mit [_mountainLayer end].

Zu guter Letzt müssen wir noch die Löcher, die in unser logisches Terrain geschossen werden auch aus unser CCRenderTexture rausschneiden. Wir fügen hierfür den folgenden Code in unseren Kollsisionshandler ein (handleMountainCollision).

        CCSprite *cutSprite = [CCSprite spriteWithFile:@"cutSprite.png"];
        [cutSprite setBlendFunc: (ccBlendFunc) { GL_ZERO, GL_ONE_MINUS_SRC_ALPHA }];
        cutSprite.position = ccp(hitPT.x-_mountainLayer.contentSize.width/2,hitPT.y-_mountainLayer.contentSize.height/2);

        // Update the render texture
        [_mountainLayer begin];

        // Limit drawing to the alpha channel
        glColorMask(0.0f, 0.0f, 0.0f, 1.0f);

        // Draw
        [cutSprite visit];

        // Reset color mask
        glColorMask(1.0f, 1.0f, 1.0f, 1.0f);

        [_mountainLayer end];

Außerdem sollten wir noch den Debug Modus deaktivieren und das logische Terrain auf visible = NO setzen.

destructable Terrain komplett

Viel Spaß beim zerstören und bis zum nächsten mal.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.