如何使用Box2D和Cocos2D製作一款像Fruit Ninja一樣的遊戲-第2部分

這篇文章還可以在這裏找到 英語西班牙語

Create a Sprite-Cutting Game with Cocos2D!

Create a Sprite-Cutting Game with Cocos2D!

本篇教程是由iOS教程組的成員Allen Tan發佈的,Allen是一位IOS開發者和White Widget的創始人。

這是教你如何製作一款像Halfbrick Studios公司的Fruit Ninja一樣的切割精靈遊戲系列教程的第2篇。

第1篇中,你學會了如何創建紋理多邊形,並基於它製作了一個西瓜。

在第1部分中所做的努力將在第2部分中收到回報,在此部分中,你將能夠切割sprite。

和第1部分一樣,本篇教程需要你熟悉Cocos2D和Box2D。如果你是剛剛接觸它們的話,請先學習本網站的Cocos2D入門Box2D入門

準備工作

如果你還沒有第1部分結束時的工程,請下載sample project來繼續本篇教程。

接下來,對PolygonSprite的結構體進行一些修改以讓它能處理切割。

打開PolygonSprite.h並作如下修改:

// Add inside the @interface
BOOL _sliceEntered;
BOOL _sliceExited;
b2Vec2 _entryPoint;
b2Vec2 _exitPoint;
double _sliceEntryTime;
 
// Add after the @interface
@property(nonatomic,readwrite)BOOL sliceEntered;
@property(nonatomic,readwrite)BOOL sliceExited;
@property(nonatomic,readwrite)b2Vec2 entryPoint;
@property(nonatomic,readwrite)b2Vec2 exitPoint;
@property(nonatomic,readwrite)double sliceEntryTime;

然後,打開PolygonSprite.mm並作如下修改:

// Add inside the @implementation
@synthesize entryPoint = _entryPoint;
@synthesize exitPoint = _exitPoint;
@synthesize sliceEntered = _sliceEntered;
@synthesize sliceExited = _sliceExited;
@synthesize sliceEntryTime = _sliceEntryTime;
 
// Add inside the initWithTexture method, inside the if statement
_sliceExited = NO;
_sliceEntered = NO;
_entryPoint.SetZero();
_exitPoint.SetZero();
_sliceExited = 0;

編譯並檢查語法錯誤。

以上的代碼對PolygonSprite類及其子類進行了改進,儲存了切割需要的變量信息:

  • entryPoint: 切割線首次和多邊形接觸的點。
  • exitPoint: 切割線第二次和多邊形接觸的點。
  • sliceEntered: 判斷多邊形是否已經有切割線進入了。
  • sliceExited: 判斷多邊形是否被完整切割過一次。
  • sliceEntryTime: 切割線進入多邊形時的準確時間。用來決定過慢的輕掃動作不被視爲切割動作。

使用Ray Casts與Sprites相交

爲了切割sprite,你必須能夠判斷點在哪兒。這就需要用到Box2D的ray casting。

在ray casting中,你需要指定一個起始點和一個結束點,Box2D會根據它們組成的線段判斷哪些Box2D的fixtures和它有相交。不只如此,它還會觸發一個回調函數來告訴你具體每一個與其碰撞的fixture。

你將要使用ray casts,基於玩家觸摸屏幕的點,來判斷出所有觸摸經過的fixtures,並使用回調函數來記錄每個具體的相交的點。

打開HelloWorldLayer.h並在@interface中加入如下內容:

CGPoint _startPoint;
CGPoint _endPoint;

切換到HelloWorldLayer.mm並做如下修改:

// Add inside the draw method after kmGLPushMatrix()
ccDrawLine(_startPoint, _endPoint);
 
// Add this method
-(void)ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    for (UITouch *touch in touches){
        CGPoint location = [touch locationInView:[touch view]];
        location = [[CCDirector sharedDirector] convertToGL:location];
        _startPoint = location;
        _endPoint = location;
    }
}
 
// Add this method
- (void)ccTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    for (UITouch *touch in touches){
        CGPoint location = [touch locationInView:[touch view]];
        location = [[CCDirector sharedDirector] convertToGL:location];
        _endPoint = location;
    }
}

以上代碼爲觸摸事件指定了起始點和結束點。

當玩家觸摸屏幕時,起始點在ccTouhesBegan方法中被記錄下來,結束點跟隨玩家手指的滑動,相應的在ccTouhesMoved方法中被記錄。

ccDrawLine方法從起始點到結束點畫一條線。

編譯並運行,試着在屏幕中畫一條線:

Draw the Line

這條線將會代表你接下來要創建的ray cast。

爲了使用Box2D的ray casting,你只需簡單的調用world對象中的RayCast,並提供給它起始和結束點即可,每和任意一個fixture有交集的時候,就會觸發一個回調函數。

ray cast的方法需要存儲在一個b2RayCastCallback類當中。

在Xcode中,進入FileNewNew File菜單,選擇 iOSC and C++Header File,並點擊Next。爲新的頭文件命名爲RayCastCallback.h,點擊Save。

把該文件替換爲以下內容:

#ifndef CutCutCut_RaycastCallback_h
#define CutCutCut_RaycastCallback_h
 
#import "Box2D.h"
#import "PolygonSprite.h"
 
class RaycastCallback : public b2RayCastCallback
{
public:
RaycastCallback(){
}
 
float32 ReportFixture(b2Fixture *fixture,const b2Vec2 &point,const b2Vec2 &normal,float32 fraction)
{
    PolygonSprite *ps = (PolygonSprite*)fixture->GetBody()->GetUserData();
    if (!ps.sliceEntered)
    {
        ps.sliceEntered = YES;
 
        //you need to get the point coordinates within the shape
        ps.entryPoint  = ps.body->GetLocalPoint(point);
 
        ps.sliceEntryTime = CACurrentMediaTime() + 1;
        CCLOG(@"Slice Entered at world coordinates:(%f,%f), polygon coordinates:(%f,%f)", point.x*PTM_RATIO, point.y*PTM_RATIO, ps.entryPoint.x*PTM_RATIO, ps.entryPoint.y*PTM_RATIO);
    }
    else if (!ps.sliceExited)
    {
        ps.exitPoint = ps.body->GetLocalPoint(point);
        ps.sliceExited = YES;
 
        CCLOG(@"Slice Exited at world coordinates:(%f,%f), polygon coordinates:(%f,%f)", point.x*PTM_RATIO, point.y*PTM_RATIO, ps.exitPoint.x*PTM_RATIO, ps.exitPoint.y*PTM_RATIO);
    }
    return 1;
}
};
 
#endif

每當Box2D檢測到一次接觸,就會調用ReportFixture方法。如果多邊形還沒有切割線進入,那麼就把相交點設置爲entry point,如果已經有切割線進入了,就把相交點設置爲exit point。

你使用GetLocalPoint轉換了座標點是因爲你需要知道在多邊形內部的座標,而不是世界座標。世界座標是起始於屏幕左下角,而本地座標起始於形狀的左下角。

最後,你返回 1 來告訴Box2D,ray cast在檢測到第一個fixture之後,還應該繼續檢測其他fixtures。返回其他的值會另次方法有其他表現,但是這已經超出了本篇教學的範疇。

切換到HelloWorldLayer.h並作如下修改:

// Add to top of file
#import "RaycastCallback.h"
 
// Add inside the @interface
RaycastCallback *_raycastCallback;

接下來,切換到HelloWorldLayer.mm並做如下修改:

// Add inside the init method, right after [self initSprites]
_raycastCallback = new RaycastCallback();
 
// Add at the end of the ccTouchesEnded method
world->RayCast(_raycastCallback, 
               b2Vec2(_startPoint.x / PTM_RATIO, _startPoint.y / PTM_RATIO),
               b2Vec2(_endPoint.x / PTM_RATIO, _endPoint.y / PTM_RATIO));
 
world->RayCast(_raycastCallback, 
               b2Vec2(_endPoint.x / PTM_RATIO, _endPoint.y / PTM_RATIO),
               b2Vec2(_startPoint.x / PTM_RATIO, _startPoint.y / PTM_RATIO));

你聲明瞭一個RayCastCallback類並將其作爲RayCast方法的參數。目前你只在玩家觸摸結束的時刻調用RayCast。

你調用兩次ray cast是因爲Box2D ray casting只在一個方向上檢測相交。解決的方法是在反方向上再次調用RayCast。

編譯並運行。試着畫一條線並檢查logs。

Check the Logs

分隔多邊形

分隔多邊形也許是本教程中最難的一部分,主要是因爲此操作需要很多的計算,同時有很多的Box2D的規則需要遵守。

不要着急,這同時也是最cool的一部分,我會一點一點的帶你學會它!

切換到HelloWorldLayer.h並作如下修改:

// Add to top of file
#define calculate_determinant_2x2(x1,y1,x2,y2) x1*y2-y1*x2
#define calculate_determinant_2x3(x1,y1,x2,y2,x3,y3) x1*y2+x2*y3+x3*y1-y1*x2-y2*x3-y3*x1
 
// Add after the properties
-(b2Vec2*)arrangeVertices:(b2Vec2*)vertices count:(int)count;
-(void)splitPolygonSprite:(PolygonSprite*)sprite;
-(BOOL)areVerticesAcceptable:(b2Vec2*)vertices count:(int)count;
-(b2Body*)createBodyWithPosition:(b2Vec2)position rotation:(float)rotation vertices:(b2Vec2*)vertices vertexCount:(int32)count density:(float)density friction:(float)friction restitution:(float)restitution;

切換到HelloWorldLayer.mm並添加如下方法:

-(void)splitPolygonSprite:(PolygonSprite*)sprite
{
    //declare & initialize variables to be used for later
    PolygonSprite *newSprite1, *newSprite2;
 
    //our original shape's attributes
    b2Fixture *originalFixture = sprite.body->GetFixtureList();
    b2PolygonShape *originalPolygon = (b2PolygonShape*)originalFixture->GetShape();
    int vertexCount = originalPolygon->GetVertexCount();
 
    //our determinant(to be described later) and iterator
    float determinant;
    int i;
 
    //you store the vertices of our two new sprites here
    b2Vec2 *sprite1Vertices = (b2Vec2*)calloc(24, sizeof(b2Vec2));
    b2Vec2 *sprite2Vertices = (b2Vec2*)calloc(24, sizeof(b2Vec2));
    b2Vec2 *sprite1VerticesSorted, *sprite2VerticesSorted;
 
    //you store how many vertices there are for each of the two new sprites here
    int sprite1VertexCount = 0;
    int sprite2VertexCount = 0;
 
    //step 1:
    //the entry and exit point of our cut are considered vertices of our two new shapes, so you add these before anything else
    sprite1Vertices[sprite1VertexCount++] = sprite.entryPoint;
    sprite1Vertices[sprite1VertexCount++] = sprite.exitPoint;
    sprite2Vertices[sprite2VertexCount++] = sprite.entryPoint;
    sprite2Vertices[sprite2VertexCount++] = sprite.exitPoint;
 
    //step 2:
    //iterate through all the vertices and add them to each sprite's shape
    for (i=0; i<vertexCount; i++)
    {
        //get our vertex from the polygon
        b2Vec2 point = originalPolygon->GetVertex(i);
 
        //you check if our point is not the same as our entry or exit point first
        b2Vec2 diffFromEntryPoint = point - sprite.entryPoint;
        b2Vec2 diffFromExitPoint = point - sprite.exitPoint;
 
        if ((diffFromEntryPoint.x == 0 && diffFromEntryPoint.y == 0) || (diffFromExitPoint.x == 0 && diffFromExitPoint.y == 0))
        {
        }
        else 
        {
            determinant = calculate_determinant_2x3(sprite.entryPoint.x, sprite.entryPoint.y, sprite.exitPoint.x, sprite.exitPoint.y, point.x, point.y);
 
            if (determinant > 0)
            {
                //if the determinant is positive, then the three points are in clockwise order
                sprite1Vertices[sprite1VertexCount++] = point;
            }
            else
            {
                //if the determinant is 0, the points are on the same line. if the determinant is negative, then they are in counter-clockwise order
                sprite2Vertices[sprite2VertexCount++] = point;
 
            }//endif
        }//endif
    }//endfor
 
    //step 3:
    //Box2D needs vertices to be arranged in counter-clockwise order so you reorder our points using a custom function
    sprite1VerticesSorted = [self arrangeVertices:sprite1Vertices count:sprite1VertexCount];
    sprite2VerticesSorted = [self arrangeVertices:sprite2Vertices count:sprite2VertexCount];
 
    //step 4:
    //Box2D has some restrictions with defining shapes, so you have to consider these. You only cut the shape if both shapes pass certain requirements from our function
    BOOL sprite1VerticesAcceptable = [self areVerticesAcceptable:sprite1VerticesSorted count:sprite1VertexCount];
    BOOL sprite2VerticesAcceptable = [self areVerticesAcceptable:sprite2VerticesSorted count:sprite2VertexCount];
 
    //step 5:
    //you destroy the old shape and create the new shapes and sprites
    if (sprite1VerticesAcceptable && sprite2VerticesAcceptable)
    {
        //create the first sprite's body        
        b2Body *body1 = [self createBodyWithPosition:sprite.body->GetPosition() rotation:sprite.body->GetAngle() vertices:sprite1VerticesSorted vertexCount:sprite1VertexCount density:originalFixture->GetDensity() friction:originalFixture->GetFriction() restitution:originalFixture->GetRestitution()];
 
        //create the first sprite
 
        newSprite1 = [PolygonSprite spriteWithTexture:sprite.texture body:body1 original:NO];
        [self addChild:newSprite1 z:1];
 
        //create the second sprite's body
        b2Body *body2 = [self createBodyWithPosition:sprite.body->GetPosition() rotation:sprite.body->GetAngle() vertices:sprite2VerticesSorted vertexCount:sprite2VertexCount density:originalFixture->GetDensity() friction:originalFixture->GetFriction() restitution:originalFixture->GetRestitution()];
 
        //create the second sprite
        newSprite2 = [PolygonSprite spriteWithTexture:sprite.texture body:body2 original:NO];
        [self addChild:newSprite2 z:1];
 
        //you don't need the old shape & sprite anymore so you either destroy it or squirrel it away
        if (sprite.original)
        {   
            [sprite deactivateCollisions];
            sprite.position = ccp(-256,-256);   //cast them faraway
            sprite.sliceEntered = NO;
            sprite.sliceExited = NO;
            sprite.entryPoint.SetZero();
            sprite.exitPoint.SetZero();
        }
        else 
        {
            world->DestroyBody(sprite.body);
            [self removeChild:sprite cleanup:YES];
        }
    }
    else
    {
        sprite.sliceEntered = NO;
        sprite.sliceExited = NO;
    }
 
    //free up our allocated vectors
    free(sprite1VerticesSorted);
    free(sprite2VerticesSorted);
    free(sprite1Vertices);
    free(sprite2Vertices);
}

Wow,好多的代碼啊。先編譯一下確保沒有錯誤,然後讓我們循序漸進的過一遍這個方法:

準備階段
聲明變量。此部分最重要的是你聲明瞭兩個PolygonSprites對象,並使用兩個數組保存了他們多邊形的頂點。

階段 1
第一步,分別向代表每個形狀中頂點的數組中加入分割點。
下邊的圖例說明了這個步驟的意義:

Intersection Points Belong to Both Shapes

兩個相交點同時屬於兩個形狀的頂點。

階段 2
你分派原形狀中剩餘的頂點。你知道這個形狀永遠都會被切成兩部分,新的兩個形狀分別會在切割線的兩端。

你僅僅需要一個新的規則來決定原形狀上的頂點該屬於哪個新的形狀。

想象一下你有一個方法可以判斷任意給定的三個點是順時針的,還是逆時針的。如果你有了這個方法,那麼你就可以根據起始點,結束點和原圖形上的一點來做如下判斷:

“如果這三個點是順時針的,那麼把這個點加到形狀2中,否則,加入到形狀1!”

Clockwise & Counter-Clockwise

好消息是,有一個方法可以用來決定這種順序,通過使用一個叫做determinants的數學概念來實現它!

在幾何學中,determinants是一種數學方法,它可以判斷一個點和一條線的關係,根據返回值結果的不同(正,負,0)來決定點在線的位置。

determinant方程定義在HelloWorldLayer.h中,接收的參數爲entry point,exit point,還有原圖形上其中一個頂點。

如果結果是正的,那麼3個點就是順時針的,如果結果是負的,它們就是逆時針的。如果結果是0,那麼它們就在一條線上。

你把所有的順時針的點都加入到第1個sprite中,其他的加入到第2個sprite中。

階段 3
Box2D需要所有的頂點都以逆時針順序組織,所以你使用arrangeVertices方法爲兩個新sprite需要重新排列頂點。

階段 4
這一步確保了這些經過重新排列的頂點滿足Box2D的定義多邊形的規則。如果areVerticesAcceptable方法認爲這些頂點是不滿足條件的,那麼就把本次切割的信息從原sprite中移除。

階段 5
這一步初始化了兩個新的PolygonSprite對象並使用createBody方法創建了它們的Box2D body。新的sprite的屬性會繼承原sprite。

如果是一個原sprite被切割了,它的狀態會被重置。如果是一片被切割了,那麼它將會被銷燬並從場景中移除。

呼…還跟着我呢嗎?好,在你運行程序之前,還有額外的一些內容要添加:

仍然在HelloWorldLayer.mm中,作如下修改:

// Add before the @implementation
int comparator(const void *a, const void *b) {
    const b2Vec2 *va = (const b2Vec2 *)a;
    const b2Vec2 *vb = (const b2Vec2 *)b;
 
    if (va->x > vb->x) {
        return 1;
    } else if (va->x < vb->x) {
        return -1;
    }
    return 0;    
}
 
// Add these methods
-(b2Body*)createBodyWithPosition:(b2Vec2)position rotation:(float)rotation vertices:(b2Vec2*)vertices vertexCount:(int32)count density:(float)density friction:(float)friction restitution:(float)restitution
{
    b2BodyDef bodyDef;
    bodyDef.type = b2_dynamicBody;
    bodyDef.position = position;
    bodyDef.angle = rotation;
    b2Body *body = world->CreateBody(&bodyDef);
 
    b2FixtureDef fixtureDef;
    fixtureDef.density = density;
    fixtureDef.friction = friction;
    fixtureDef.restitution = restitution;
 
    b2PolygonShape shape;
    shape.Set(vertices, count);
    fixtureDef.shape = &shape;
    body->CreateFixture(&fixtureDef);
 
    return body;
}
 
-(b2Vec2*)arrangeVertices:(b2Vec2*)vertices count:(int)count
{
    float determinant;
    int iCounterClockWise = 1;
    int iClockWise = count - 1;
    int i;
 
    b2Vec2 referencePointA,referencePointB;
    b2Vec2 *sortedVertices = (b2Vec2*)calloc(count, sizeof(b2Vec2));
 
    //sort all vertices in ascending order according to their x-coordinate so you can get two points of a line
    qsort(vertices, count, sizeof(b2Vec2), comparator);
 
    sortedVertices[0] = vertices[0];
    referencePointA = vertices[0];          //leftmost point
    referencePointB = vertices[count-1];    //rightmost point
 
    //you arrange the points by filling our vertices in both clockwise and counter-clockwise directions using the determinant function
    for (i=1;i<count-1;i++)
    {
        determinant = calculate_determinant_2x3(referencePointA.x, referencePointA.y, referencePointB.x, referencePointB.y, vertices[i].x, vertices[i].y);
        if (determinant<0)
        {
            sortedVertices[iCounterClockWise++] = vertices[i];
        }
        else 
        {
            sortedVertices[iClockWise--] = vertices[i];
        }//endif
    }//endif
 
    sortedVertices[iCounterClockWise] = vertices[count-1];
    return sortedVertices;
}
 
-(BOOL)areVerticesAcceptable:(b2Vec2*)vertices count:(int)count
{
    return YES;
}

這是以上方法的分類說明:

  • createBody: 此方法創建了活躍的可以和其他body產生碰撞的Box2D body。
  • arrangeVertices: 此方法按照逆時針的順序重排頂點。它使用qsort方法按x座標升序排列,然後使用determinants來完成最終的重排。
  • comparator: 此方法被qsort使用,它完成頂點比較並返回結果給qsort。
  • areVerticesAcceptable: 目前,此方法假設所有的頂點都是合理的。

就是它了!理論上說,你現在就可以把一個多邊形切成兩部分了。但是…等等…我們最好用上你剛剛創建的方法! :]

還是在HelloWorldLayer.mm,添加以下修改:

// Add this method
-(void)checkAndSliceObjects
{
    double curTime = CACurrentMediaTime();
    for (b2Body* b = world->GetBodyList(); b; b = b->GetNext())
    {
        if (b->GetUserData() != NULL) {
            PolygonSprite *sprite = (PolygonSprite*)b->GetUserData();
 
            if (sprite.sliceEntered && curTime > sprite.sliceEntryTime) 
            {
                sprite.sliceEntered = NO;
            }
            else if (sprite.sliceEntered && sprite.sliceExited)
            {
                [self splitPolygonSprite:sprite];
            }
        }
    }
}
 
// Add this in the update method
[self checkAndSliceObjects];

編譯並運行,你可以試着去切割你的西瓜。

等等它…
The Power of Math Cuts the Watermelon

成功了!原來數學公式也能切水果啊!

注意: 如果遊戲突然掛掉了請不要着急。在完成了areVerticesAcceptable方法之後,這就會被修復了。

一種更好的Swipe技術

目前,切割感覺有一點不自然,因爲玩家的手指可以移動一個曲線,但是我們僅僅把它當作直線來處理了。另外還有一點導致不自然的原因是,必須玩家的手指擡起來,切割纔會生效。

爲了修復這個問題,打開HelloWorldLayer.mm並作如下修改:

// Add this method
-(void)clearSlices
{
    for (b2Body* b = world->GetBodyList(); b; b = b->GetNext())
    {
        if (b->GetUserData() != NULL) {
            PolygonSprite *sprite = (PolygonSprite*)b->GetUserData();
            sprite.sliceEntered = NO;
            sprite.sliceExited = NO;
        }
    }
}
 
// Add this at the end of ccTouchesMoved
if (ccpLengthSQ(ccpSub(_startPoint, _endPoint)) > 25)
{
    world->RayCast(_raycastCallback, 
                   b2Vec2(_startPoint.x / PTM_RATIO, _startPoint.y / PTM_RATIO),
                   b2Vec2(_endPoint.x / PTM_RATIO, _endPoint.y / PTM_RATIO));
 
    world->RayCast(_raycastCallback, 
                   b2Vec2(_endPoint.x / PTM_RATIO, _endPoint.y / PTM_RATIO),
                   b2Vec2(_startPoint.x / PTM_RATIO, _startPoint.y / PTM_RATIO));
    _startPoint = _endPoint;
}
 
// Remove these from ccTouchesEnded
world->RayCast(_raycastCallback, 
               b2Vec2(_startPoint.x / PTM_RATIO, _startPoint.y / PTM_RATIO),
               b2Vec2(_endPoint.x / PTM_RATIO, _endPoint.y / PTM_RATIO));
 
world->RayCast(_raycastCallback, 
               b2Vec2(_endPoint.x / PTM_RATIO, _endPoint.y / PTM_RATIO),
               b2Vec2(_startPoint.x / PTM_RATIO, _startPoint.y / PTM_RATIO));
 
// Add this inside ccTouchesEnded
[self clearSlices];

你把RayCast方法從ccTouchesEnded移動到了ccTouchesMoved,現在多邊形就能夠在手指移動過程中被切割了。Box2D ray cast不能被觸發太頻繁,也不能太不頻繁,所以你設置每達到5個座標長度時觸發一次。

使用ccpLengthSQ比較距離只是一種更優化的方式(與distance > 5相比)。處理距離需要用到開方公式,開方操作的消耗比較大,不能很頻繁的使用。僅僅把等式兩邊都平方即可解決。

每當RayCast方法執行,你都把結束點重新當成起始點處理。最後,當玩家結束觸摸屏幕時,你清除所有的相交點。

編譯並運行,現在滑動感覺更自然了。

A More Natural Swipe

使用這個方法,你將更容易破壞Box2D的規則。嘗試創建一個結束點和起始點在同一邊的切割線,看看會發生什麼。同時還可以嘗試能把sprite切割成多少個小片。

這就來處理這些問題,切換到RaycastCallback.h並作如下修改:

// Remove the CCLOG commands
 
// Add to top of file
#define collinear(x1,y1,x2,y2,x3,y3) fabsf((y1-y2) * (x1-x3) - (y1-y3) * (x1-x2))
 
// Remove this line from the else if statement
ps.sliceExited = YES;
 
// Add this inside the else if statement, right after setting the exitPoint
b2Vec2 entrySide = ps.entryPoint - ps.centroid;
b2Vec2 exitSide = ps.exitPoint - ps.centroid;
 
if (entrySide.x * exitSide.x < 0 || entrySide.y * exitSide.y < 0)
{
    ps.sliceExited = YES;
}
else {
    //if the cut didn't cross the centroid, you check if the entry and exit point lie on the same line
    b2Fixture *fixture = ps.body->GetFixtureList();
    b2PolygonShape *polygon = (b2PolygonShape*)fixture->GetShape();
    int count = polygon->GetVertexCount();
 
    BOOL onSameLine = NO;
    for (int i = 0 ; i < count; i++)
    {
        b2Vec2 pointA = polygon->GetVertex(i);
        b2Vec2 pointB;
 
        if (i == count - 1)
        {
            pointB = polygon->GetVertex(0);
        }
        else {
            pointB = polygon->GetVertex(i+1);
        }//endif
 
        float collinear = collinear(pointA.x,pointA.y, ps.entryPoint.x, ps.entryPoint.y, pointB.x,pointB.y);
 
        if (collinear <= 0.00001)
        {
            float collinear2 = collinear(pointA.x,pointA.y,ps.exitPoint.x,ps.exitPoint.y,pointB.x,pointB.y);
            if (collinear2 <= 0.00001)
            {
                onSameLine = YES;
            }
            break;
        }//endif
    }//endfor
 
    if (onSameLine)
    {
        ps.entryPoint = ps.exitPoint;
        ps.sliceEntryTime = CACurrentMediaTime() + 1;
        ps.sliceExited = NO;
    }
    else {
        ps.sliceExited = YES;
    }//endif
}

在接受一個結束點之前,這個回調函數檢查兩點的位置,如果起始點和結束點處在多邊形中心點的兩側,那麼這次切割是合理的。

如果不在多邊形中心點的兩側,那麼繼續檢測切割線起始點和結束點是否在原圖形所有的頂點形成的線上。如果他們在一條線上,那麼就意味着相交點是另一個起始點,否則,就是一次完整的切割。

切換回HelloWorldLayer.mm並把areVerticesAcceptable方法替換爲如下:

-(BOOL)areVerticesAcceptable:(b2Vec2*)vertices count:(int)count
{
    //check 1: polygons need to at least have 3 vertices
    if (count < 3)
    {
        return NO;
    }
 
    //check 2: the number of vertices cannot exceed b2_maxPolygonVertices
    if (count > b2_maxPolygonVertices)
    {
        return NO;
    }
 
    //check 3: Box2D needs the distance from each vertex to be greater than b2_epsilon
    int32 i;
    for (i=0; i<count; ++i)
    {
        int32 i1 = i;
        int32 i2 = i + 1 < count ? i + 1 : 0;
        b2Vec2 edge = vertices[i2] - vertices[i1];
        if (edge.LengthSquared() <= b2_epsilon * b2_epsilon)
        {
            return NO;
        }
    }
 
    //check 4: Box2D needs the area of a polygon to be greater than b2_epsilon
    float32 area = 0.0f;
 
    b2Vec2 pRef(0.0f,0.0f);
 
    for (i=0; i<count; ++i)
    {
        b2Vec2 p1 = pRef;
        b2Vec2 p2 = vertices[i];
        b2Vec2 p3 = i + 1 < count ? vertices[i+1] : vertices[0];
 
        b2Vec2 e1 = p2 - p1;
        b2Vec2 e2 = p3 - p1;
 
        float32 D = b2Cross(e1, e2);
 
        float32 triangleArea = 0.5f * D;
        area += triangleArea;
    }
 
    if (area <= 0.0001)
    {
        return NO;
    }
 
    //check 5: Box2D requires that the shape be Convex.
    float determinant;
    float referenceDeterminant;
    b2Vec2 v1 = vertices[0] - vertices[count-1];
    b2Vec2 v2 = vertices[1] - vertices[0];
    referenceDeterminant = calculate_determinant_2x2(v1.x, v1.y, v2.x, v2.y);
 
    for (i=1; i<count-1; i++)
    {
        v1 = v2;
        v2 = vertices[i+1] - vertices[i];
        determinant = calculate_determinant_2x2(v1.x, v1.y, v2.x, v2.y);
        //you use the determinant to check direction from one point to another. A convex shape's points should only go around in one direction. The sign of the determinant determines that direction. If the sign of the determinant changes mid-way, then you have a concave shape.
        if (referenceDeterminant * determinant < 0.0f)
        {
            //if multiplying two determinants result to a negative value, you know that the sign of both numbers differ, hence it is concave
            return NO;
        }
    }
    v1 = v2;
    v2 = vertices[0]-vertices[count-1];
    determinant = calculate_determinant_2x2(v1.x, v1.y, v2.x, v2.y);
    if (referenceDeterminant * determinant < 0.0f)
    {
        return NO;
    }
    return YES;
}

你做了5步檢查來決定一個多邊形是否滿足Box2D的標準:

  • Check 1: 一個多邊形至少需要3個頂點。
  • Check 2: 多邊形的頂點數最多不能超過預定義的b2_maxPolygonVertices,目前是8.
  • Check 3: 每個頂點之間的距離必須大於b2_epsilon。
  • Check 4: 多邊形的面積必須大於b2_epsilon。這對於我們來說有點太小了,所以你適當調整爲0.0001。
  • Check 5: 形狀必須的凸的。

前兩個檢查直截了當,第3個和第4個檢查都是Box2D庫要求的。最後的一個再次使用了determinants。

一個凸的形狀的頂點應該總是想一個方向拐彎。如果方向突然改變了,那麼這個形狀就會變爲凹的。你遍歷多邊形的頂點並比較determinant結果的符號。如果符號突然改變了,就意味着多邊形頂點的方向變了。

編譯並運行,切些水果併爲你自己做些水果沙拉吧!

Fruit Grinder!

結束調試模式

現在你已經可以確定Box2D部分的工作都如你所料了,所以你不再需要調試繪製模式了。

還是在HelloWorldLayer.mm中,作如下修改:

// Comment these out from the draw method
ccDrawLine(_startPoint, _endPoint);
world->DrawDebugData();
 
// Add inside the init method
[self initBackground];
 
// Add this method
-(void)initBackground
{
    CGSize screen = [[CCDirector sharedDirector] winSize];
    CCSprite *background = [CCSprite spriteWithFile:@"bg.png"];
    background.position = ccp(screen.width/2,screen.height/2);
    [self addChild:background z:0];
}

編譯並運行,你會看到一個漂亮的背景,它是由Vicki爲本篇教學創作的。

Monkey Forest

使用CCBlade使切割可視化

沒有了調試繪製,你需要一個新方法來顯示切割動作。由Ngo Duc Hiep製作的CCBlade是一個完美的解決方案。

下載 CCBlade,解壓它,在 Xcode 中按 Option+Command+A 添加 CCBlade.m 和 CCBlade.h
到你的工程中。確保“Copy items into destination group’s folder”和“Create groups for any added folders”是選中的。

CCBlade是由第三方維護的,所以本篇教學所用的版本也許不是最新的。你可以從resource kit的Class文件夾中得到本篇教學所用的CCBlade版本。

你需要把CCBlade更新到Cocos2D 2.X,打開CCBlade.m,將其重命名爲CCBlade.mm,並作如下修改:

// Replace everything starting from glDisableClientState in the draw method with this
CC_NODE_DRAW_SETUP();
 
ccGLBlendFunc( CC_BLEND_SRC, CC_BLEND_DST );
 
ccGLBindTexture2D( [_texture name] );    
glVertexAttribPointer(kCCVertexAttrib_Position, 2, GL_FLOAT, GL_FALSE, sizeof(vertices[0]), vertices);
glVertexAttribPointer(kCCVertexAttrib_TexCoords, 2, GL_FLOAT, GL_FALSE, sizeof(coordinates[0]), coordinates);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 2*[path count]-2);
 
// Add inside the initWithMaximumPoint method
self.shaderProgram = [[CCShaderCache sharedShaderCache] programForKey:kCCShader_PositionTexture];
 
// Remove from the setWidth method
* CC_CONTENT_SCALE_FACTOR()
 
// Remove from the push method
if (CC_CONTENT_SCALE_FACTOR() != 1.0f) {
    v = ccpMult(v, CC_CONTENT_SCALE_FACTOR());
}

你使用和之前轉換PRFilledPolygon中的drawing代碼一樣的方式,並移除了縮放係數,因爲shader程序已經處理它了。

CCBlade有一個由點組成的path(路徑)數組,並穿過這些點繪製一個紋理直線。目前它是在draw方法中更新的這個數組。不過,一種更推薦的方式是隻在draw方式中繪製,其他的內容放到update方法中去。

爲了更好的管理path數組,你在HelloWorldLayer的update方法中更新它們。

打開CCBlade.h並在@interface中加入以下內容:

@property(nonatomic,retain)NSMutableArray *path;

切換到CCBlade.mm並在@implementation中加入以下內容:

@synthesize path;

接下來,切換到HelloWorldLayer.h並作如下修改:

// Add to top of file
#import "CCBlade.h"
 
// Add inside the @interface
CCArray *_blades;
CCBlade *_blade;
float _deltaRemainder;
 
// Add after the @interface
@property(nonatomic,retain)CCArray *blades;

最後,切換到HelloWorldLayer.mm並做如下修改:

// Add inside the @implementation
@synthesize blades = _blades;
 
// Add inside dealloc
[_blades release];
_blades = nil;
 
// Add inside init, after _raycastCallback
_deltaRemainder = 0.0;
_blades = [[CCArray alloc] initWithCapacity:3];
CCTexture2D *texture = [[CCTextureCache sharedTextureCache] addImage:@"streak.png"];
 
for (int i = 0; i < 3; i++)
{
    CCBlade *blade = [CCBlade bladeWithMaximumPoint:50];
    blade.autoDim = NO;
    blade.texture = texture;
 
    [self addChild:blade z:2];
    [_blades addObject:blade];
}
 
// Add inside update, right after [self checkAndSliceObjects]
if ([_blade.path count] > 3) {
    _deltaRemainder+=dt*60*1.2;
    int pop = (int)roundf(_deltaRemainder);
    _deltaRemainder-=pop;
    [_blade pop:pop];
}
 
// Add inside ccTouchesBegan
CCBlade *blade;
CCARRAY_FOREACH(_blades, blade)
{
    if (blade.path.count == 0)
    {
        _blade = blade;
        [_blade push:location];
        break;
    }
}
 
// Add inside ccTouchesMoved
[_blade push:location];
 
// Add inside ccTouchesEnded
[_blade dim:YES];

你爲path數組製作了一個屬性,這樣就可以在HelloWorldLayer中訪問它們了。然後你創建了3個在遊戲中公用的CCBlade對象。對每一個blade,你設置最大的點個數爲50來防止軌跡太長,並設置blade的紋理爲Resources文件夾中的streak。

你設置每個blade的autoDim變量爲NO。CCBlade使用術語“Dim”來說明此blade會自動從尾巴到頭的漸變消失。CCBlade自動從path數組中移除這些點。

雖然這很方便,但是CCBlade在它自己的draw方法中已經實現了自動彈出效果,所以最好把這個屬性設置爲NO並由我們自己在update方法中控制它的dim特性。

每當玩家觸摸屏幕,你都指定一個目前空閒的CCBlade,並把觸摸到的點壓入它的path數組中。

最後,當玩家結束觸摸屏幕時,你通知CCBlade設置其dim爲YES,讓其自動漸隱銷燬。

你讓update方法來處理目前活躍的CCBlade的dimming。你想讓它無視幀率來恰當的漸隱,所以你把delta time乘上一個常數。

因爲delta time並不一定是一個整數,所以你需要用一個remainder變量將其存儲,下次循環到來時再作計算。

編譯並運行,試試看你的新的漂亮的刀光效果吧!

Cool Blade Effect

何去何從?

這是到目前爲止的教程的示例工程

這就是第2部分的全部內容了,在第1部分中,你創建了西瓜的紋理多邊形,但它最終會落到地上。現在,你已經可以用一個很cool的刀光效果把這隻西瓜切成小細塊兒了。

在接下來的系列教程的第3部分中,你將會把所有內容合併成一款完整的遊戲!


本篇教程是由iOS教程組的成員Allen Tan發佈的,Allen是一位IOS開發者和White Widget的創始人。

發佈了12 篇原創文章 · 獲贊 1 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章