Статьи

Создайте игру Caterpillar с Cocos2D: обнаружение столкновений

Это шестая часть нашей серии руководств по Cocos2D по клонированию сороконожки для iOS. Убедитесь, что вы завершили предыдущие части, прежде чем начать.


В последнем уроке я показал вам, как создать массив ракетных объектов и запустить их постоянный поток. Вы также узнали об основных взаимодействиях с игроком в Cocos2D для перемещения игрока.

В сегодняшнем уроке мы рассмотрим, как настроить базовое обнаружение столкновений в Cocos2D. Хотя это не всегда оптимальное решение, оно определенно является самым быстрым и простым в реализации.


Столкновение ракеты с ростками очень похоже на столкновение игрока с ростками. Мы просто проверяем границы каждой ракеты в игре против границ каждого ростка в игре и определяем, сталкиваются ли они. Когда прорастают, мы уменьшаем его жизнь, изменяем его непрозрачность и удаляем его, если его жизнь достигает 0.

Откройте Missile.m, импортируйте Sprout.h и добавьте следующий код в конец метода обновления:

1
CGRect missileRect = [self getBounds];

По мере обновления каждой ракеты она перечисляет все побеги в игре, проверяя, пересекаются ли их ограничивающие стороны. Если происходит столкновение, мы устанавливаем ракету на «грязную» и соответственно уменьшаем жизнь ростка. Мы уже написали код для изменения непрозрачности ростков в зависимости от их жизни, поэтому, если вы запустите игру в этот момент, вы должны увидеть, как ростки меняются при попадании. Проблема в том, что они все еще в игре. Нам нужно удалить ростки с 0 жизнями.

Это можно сделать внутри метода update: в GameLayer.m. Откройте GameLayer.m и добавьте следующий код в конец update: метод:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
// 1
__block Sprout *deadSprout = nil;
[self.sprouts enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
    Sprout *sprout = (Sprout *)obj;
    if(sprout.lives == 0) {
        deadSprout = sprout;
        *stop = YES;
    }
}];
 
// 2
if(deadSprout) {
    [self.spritesBatchNode removeChild:deadSprout.sprite cleanup:YES];
    [self.sprouts removeObject:deadSprout];
}
  1. Перечислим каждый из ростков в поисках мертвого. Для упрощения мы очищаем только один росток за итерацию.
  2. Если мертвый росток существует, мы удаляем его спрайт из пакетного узла и удаляем росток из нашего массива ростков.

Теперь запустите игру, и вы увидите, как ростки исчезают при попадании и в конечном итоге исчезают.


Теперь нам нужно добавить обнаружение столкновений между ракетой и гусеницей. Именно это взаимодействие делает ваше приложение игрой. После попадания гусеница должна расколоться на участке удара, и каждая новая «гусеница» должна двигаться в разных направлениях. Сегмент, который был поражен, затем превращается в росток.

Для начала откройте Missile.m, импортируйте Caterpillar.h и Segment.h и добавьте следующий код в конец update: метод:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
__block Caterpillar *hitCaterpillar = nil;
__block Segment *hitSegment = nil;
 
// 1
[self.gameLayer.caterpillars enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
    Caterpillar *caterpillar = (Caterpillar *)obj;
    [caterpillar.segments enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
        Segment *segment = (Segment *)obj;
        CGRect segmentRect = [segment getBounds];
        // 2
        if(CGRectIntersectsRect(missileRect, segmentRect)) {
            self.dirty = YES;
            hitCaterpillar = [caterpillar retain];
            hitSegment = [segment retain];
            *stop = YES;
        }
 
    }];
}];
 
// 3
if(hitCaterpillar && hitSegment) {
    [self.gameLayer splitCaterpillar:hitCaterpillar atSegment:hitSegment];
    [hitSegment release];
    [hitCaterpillar release];
}
  1. Перечислите всех гусениц в игре и перечислите все их сегменты.
  2. Проверьте на столкновение и помните, какой сегмент какой гусеницы был поражен.
  3. Если у нас есть удар, мы разделяем гусеницу на текущий сегмент. Не волнуйтесь, мы вскоре реализуем этот метод.

Теперь, когда у нас ракета сталкивается с гусеницей, нам нужно сделать несколько вещей. Первый — написать логику для разделения гусеницы на данный сегмент. Это будет сделано в GameLayer.m. Сначала откройте GameLayer.h и добавьте следующие объявления класса forward.

1
2
@class Caterpillar;
@class Segment;

Затем объявите следующий метод:

1
— (void)splitCaterpillar:(Caterpillar *) caterpillar atSegment:(Segment *)segment;

Прежде чем мы начнем с реализации, нам нужно добавить два файла в проект. Скачайте NSArray + Reverse , разархивируйте его и перетащите оба файла в свой проект. Это просто категория в NSMutableArray, которая дает нам обратный метод. Теперь откройте GameLayer.m, импортируйте Segment.h и NSArray + Reverse.h и реализуйте следующий метод:

01
02
03
04
05
06
07
08
09
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
— (void)splitCaterpillar:(Caterpillar *)caterpillar atSegment:(Segment *)segment
{
// 1
if([caterpillar.segments count] == 1) {
    [self.spritesBatchNode removeChild:segment.sprite cleanup:NO];
    [self.caterpillars removeObject:caterpillar];
    [self createSproutAtPostion:segment.position];
    return;
}
 
// 2
[self.spritesBatchNode removeChild:segment.sprite cleanup:NO];
 
// 3
[self createSproutAtPostion:segment.position];
 
// 4
NSInteger indexOfSegement = [caterpillar.segments indexOfObject:segment];
NSMutableArray *headSegments = [NSMutableArray array];
NSMutableArray *tailsSegments = [NSMutableArray array];
 
// 5
[caterpillar.segments enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
    if(idx < indexOfSegement) {
    [headSegments addObject:obj];
    } else if(idx > indexOfSegement) {
    [tailsSegments addObject:obj];
    }
}
];
 
// 6
if([tailsSegments count] > 0) {
 
    // 7
    [tailsSegments reverse];
 
    // 8
    Caterpillar *newCaterpillar = [[[Caterpillar alloc] initWithGameLayer:self segments:tailsSegments level:self.level] autorelease];
    newCaterpillar.position = [[tailsSegments objectAtIndex:0] position];
 
    // 9
    if(caterpillar.currentState == CSRight ||
       caterpillar.previousState == CSRight) {
    // Was heading right
    if(caterpillar.currentState == CSDownLeft ||
       caterpillar.currentState == CSDownRight) {
        // Is heading down
        newCaterpillar.previousState = CSUpRight;
    } else {
        // Is heading up
        newCaterpillar.previousState = CSDownRight;
    }
    newCaterpillar.currentState = CSLeft;
    } else {
    // Was heading left
    if(caterpillar.currentState == CSDownLeft ||
       caterpillar.currentState == CSDownRight) {
        // Is heading down
        newCaterpillar.previousState = CSUpRight;
    } else {
        // Is heading up
        newCaterpillar.previousState = CSDownRight;
    }
    newCaterpillar.currentState = CSRight;
    }
 
    [self.caterpillars addObject:newCaterpillar];
}
 
// 10
if([headSegments count] > 0) {
    caterpillar.segments = headSegments;
} else {
    [self.caterpillars removeObject:caterpillar];
}
 
}
  1. Если мы ударяем гусеницу из одного сегмента (только голову), мы удаляем эту гусеницу из игры и превращаем ее в росток.
  2. Удалите спрайт сегмента, который был поражен от узла пакета.
  3. Преобразуйте сегмент попадания в росток (мы реализуем этот метод на мгновение).
  4. Нам нужно разделить гусеницу на два массива: головной и хвостовой.
  5. Определите, где другие сегменты падают (голова или хвостовая часть) и добавьте их в соответствующие массивы.
  6. Проверьте, есть ли какие-либо хвостовые сегменты.
  7. Переверните массив хвостовых сегментов. Это категория на NSMutableArray, которая была упомянута выше. Нам нужно повернуть сегменты, чтобы переместить гусеницу в противоположном направлении.
  8. Создайте новый объект гусеницы, используя хвостовую часть. Мы реализуем этот метод на мгновение.
  9. Это смелость этого метода. Здесь мы определяем текущее направление (влево или вправо) и текущее общее направление (вверх или вниз) пораженной гусеницы, чтобы отправить новую гусеницу в противоположном направлении.
  10. Если осталась еще голова, мы просто устанавливаем гусеницу, в которую попали сегменты, на оставшиеся сегменты головы.

Вау, это было много, чтобы принять. Ты все еще со мной? Есть несколько методов, которые мы использовали здесь и в предыдущем коде, которые еще нужно реализовать. Первый метод для реализации — это createSproutAtPosition: Добавьте следующую декларацию метода в свой приватный интерфейс вверху GameLayer.m:

1
— (void)createSproutAtPostion:(CGPoint)position;

Теперь реализуем следующий метод:

01
02
03
04
05
06
07
08
09
10
11
— (void)createSproutAtPostion:(CGPoint)position {
    // 1
    int x = (position.x — kGameAreaStartX) / kGridCellSize;
    int y = (kGameAreaStartY — kGridCellSize + kGameAreaHeight + kGridCellSize/2 — position.y) / kGridCellSize;
 
    // 2
    Sprout *sprout = [[Sprout alloc] initWithGameLayer:self];
    sprout.position = position;
    [self.sprouts addObject:sprout];
    _locations[x][y] = YES;
}
  1. Это переводит нашу позицию на экране координаты в координаты сетки. Используя наши навыки алгебры 8-го класса, мы получаем это из кода позиционирования, написанного во второй части .
  2. Создайте новый росток и добавьте его в игру.

Последний метод, который нам нужно реализовать, чтобы все это работало, это initWithGameLayer:segments:level: в классе гусеницы. Этот метод будет отвечать за построение новой гусеницы из переданных сегментов. Откройте Caterpillar.h и добавьте следующее объявление метода:

1
— (id)initWithGameLayer:(GameLayer *)layer segments:(NSMutableArray *)segments level:(NSInteger)level;

Теперь откройте Caterpillar.m и выполните следующий метод:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
— (id)initWithGameLayer:(GameLayer *)layer segments:(NSMutableArray *)segments level:(NSInteger)level {
if(self = [super initWithGameLayer:layer]) {
    self.segments = segments;
    self.level = level;
    self.currentState = CSRight;
    self.previousState = CSDownLeft;
 
    // set the position of the rest of the segments
    __block int x = 0;
    __block Segment *parentSegment = [self.segments objectAtIndex:0];
    parentSegment.parent = nil;
 
    [self.segments enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
    Segment *segment = (Segment *)obj;
 
    if(x++ > 0) {
 
        if(![segment isEqual:parentSegment]) {
        segment.parent = parentSegment;
        }
 
        parentSegment = segment;
    }
 
    }];
 
}
return self;
}

Этот метод практически идентичен нашему предыдущему initWithGameLayer:level:position: method. Единственное отличие состоит не в том, чтобы выделять массив сегментов, а в том, что он устанавливает массив сегментов для входящих сегментов, которые передаются.

Продолжайте и запустите игру на этом этапе. Вы должны быть в состоянии полностью убить гусеницу, которая находится в игре.

Гусеничный убийца

Последнее, что нам нужно, чтобы завершить жизненный цикл обнаружения столкновений, это реализовать столкновения между гусеницей и игроком. Если какая-либо часть гусеницы попадает в игрока, мы хотим уменьшить количество жизней игрока. Кроме того, игрок ненадолго станет непобедимым, так что гусеница не просто пронзит его.

Начните с открытия GameConfig.h и добавления следующей опции:

1
#define kPlayerInvincibleTime 15

Теперь откройте Caterpillar.m, импортируйте Player.h и добавьте следующий код в конец update: метод непосредственно перед тем, как установить положение гусеницы:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
static int playerInvincibleCount = kPlayerInvincibleTime;
static BOOL playerHit;
 
CGRect playerRect = [self.gameLayer.player getBounds];
 
// 1
[self.segments enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
    Segment *segment = (Segment *)obj;
    CGRect segmentRect = [segment getBounds];
 
    if(CGRectIntersectsRect(segmentRect, playerRect) && playerInvincibleCount == kPlayerInvincibleTime) {
        *stop = YES;
        playerHit = YES;
        // 2
    self.gameLayer.player.lives—;
        [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationPlayerLives object:nil];
    }
 
}];
 
// 3
if(playerHit) {
    if(playerInvincibleCount > 0) {
        playerInvincibleCount—;
    } else {
        playerHit = NO;
        playerInvincibleCount = kPlayerInvincibleTime;
    }
}
  1. Перечислите каждый из сегментов, чтобы определить, сталкиваются ли они с игроком.
  2. Если игрок получил удар, уменьшите его жизнь и отправьте уведомление. Это должно автоматически отражаться в пользовательском интерфейсе на основе кода, который вы написали в части 3.
  3. Если игрок был поражен, проверьте, были ли они все еще в непобедимом периоде. Если нет, сбросьте непобедимый счетчик, чтобы они могли получить удар снова.

Теперь запустите игру и позвольте гусенице ударить вас. Это должно удалить одну из ваших жизней из верхней части экрана.


Обнаружение столкновения никогда не было легкой задачей, и мы только поцарапали поверхность. Если вы хотите углубиться в обнаружение столкновений, взгляните на использование Box2D с Cocos2D.


В следующем и последнем уроке серии мы обсудим игровой процесс. Это будет включать в себя условия победы, очки, звуки, игру и высокий балл.

Удачного кодирования!