SpriteKit — Автослалом (окончание) — Часть 16

11scr

Почему на скрине нарисован воздушный шар и вертолет робинсон я расскажу в конце статьи. А пока продолжим писать Автослалом. Это пародия на древнюю игру Электроника ИМ-23. В реализации нет ничего сложного, за исключением написания кастомного SKAction (см. предыдущую часть). Сегодня мы доделаем игру. Итак все по порядку:

4. Добавим барьеры

4.1. Сначала скачайте этот as_barriers архив с картинками барьеров. Распакуйте их и добавьте в проект.

4.2. Теперь в GameScene.h добавьте переменные для текстур и анимации:

SKTexture *barrierLeftTexture;
SKTexture *barrierCenterTexture;
SKTexture *barrierRightTexture;

SKAction *forBarrierLeft;
SKAction *forBarrierCenter;
SKAction *forBarrierRight;

4.3. В GameScene.m добавим методы для предзагрузки текстур и настройки анимации:

-(void)prepareTexturesBarriers
{
    barrierLeftTexture=[SKTexture textureWithImageNamed:@"barrier_left"];
    barrierCenterTexture=[SKTexture textureWithImageNamed:@"barrier_center"];
    barrierRightTexture=[SKTexture textureWithImageNamed:@"barrier_right"];
}

-(void)prepareActionsBarriers
{
    NSDictionary *propR=[[NSDictionary alloc] initWithObjectsAndKeys:
                         [NSNumber numberWithFloat:1.5],@"DURATION",
                         [NSNumber numberWithFloat:1.4],@"SCALETO",
                         [NSNumber numberWithFloat:148],@"X1",
                         [NSNumber numberWithFloat:430],@"Y1",
                         [NSNumber numberWithFloat:300],@"X2",
                         [NSNumber numberWithFloat:-50],@"Y2",
                         [NSNumber numberWithFloat:1],@"V1",
                         [NSNumber numberWithFloat:0.05],@"V2",
                         nil];
    SKAction *moveBarrierRight=[self perspectiveAction:propR];
    
    NSDictionary *propC=[[NSDictionary alloc] initWithObjectsAndKeys:
                         [NSNumber numberWithFloat:1.5],@"DURATION",
                         [NSNumber numberWithFloat:1.4],@"SCALETO",
                         [NSNumber numberWithFloat:133],@"X1",
                         [NSNumber numberWithFloat:430],@"Y1",
                         [NSNumber numberWithFloat:150],@"X2",
                         [NSNumber numberWithFloat:-50],@"Y2",
                         [NSNumber numberWithFloat:1],@"V1",
                         [NSNumber numberWithFloat:0.05],@"V2",
                         nil];
    SKAction *moveBarrierCenter=[self perspectiveAction:propC];
    
    NSDictionary *propL=[[NSDictionary alloc] initWithObjectsAndKeys:
                         [NSNumber numberWithFloat:1.5],@"DURATION",
                         [NSNumber numberWithFloat:1.4],@"SCALETO",
                         [NSNumber numberWithFloat:118],@"X1",
                         [NSNumber numberWithFloat:430],@"Y1",
                         [NSNumber numberWithFloat: 0],@"X2",
                         [NSNumber numberWithFloat:-50],@"Y2",
                         [NSNumber numberWithFloat:1],@"V1",
                         [NSNumber numberWithFloat:0.05],@"V2",
                         nil];
    SKAction *moveBarrierLeft=[self perspectiveAction:propL];
    
    
    SKAction *addScore=[SKAction runBlock:^{
        score++; //когда барьер преодолен, добавляем одно очко
    }];

    forBarrierLeft=[SKAction sequence:@[moveBarrierLeft,addScore, [SKAction removeFromParent]]];
    forBarrierCenter=[SKAction sequence:@[moveBarrierCenter,addScore, [SKAction removeFromParent]]];
    forBarrierRight=[SKAction sequence:@[moveBarrierRight,addScore, [SKAction removeFromParent]]];
}

Для анимации барьеров используем наш кастомный экшн. Вызов этих методов поместим в didMoveToView:

[self prepareTexturesBarriers];
[self prepareActionsBarriers];

4.4. Теперь в GameScene.m добавим методы для создания барьеров:

-(SKSpriteNode*)getBarrier:(int)type
{
    SKSpriteNode *barrier;
    
    if (type==1) {
        barrier=[SKSpriteNode spriteNodeWithTexture:barrierLeftTexture];
        [barrier setPosition:CGPointMake(118, 430)];
    }else if (type==2){
        barrier=[SKSpriteNode spriteNodeWithTexture:barrierCenterTexture];
        [barrier setPosition:CGPointMake(133, 430)];
    }else if (type==3){
        barrier=[SKSpriteNode spriteNodeWithTexture:barrierRightTexture];
        [barrier setPosition:CGPointMake(148, 430)];
    }
    
    [barrier setAlpha:0];
    [barrier setSize:CGSizeMake(71, 48)];
    
    
    CGPoint pointEdge1=CGPointMake(-barrier.size.width/2,-barrier.size.height/2);
    CGPoint pointEdge2=CGPointMake(barrier.size.width/2,-barrier.size.height/2);
    
    barrier.physicsBody=[SKPhysicsBody bodyWithEdgeFromPoint:pointEdge1 toPoint:pointEdge2];
    [barrier.physicsBody setDynamic:YES];
    [barrier.physicsBody setAffectedByGravity:YES];
    [barrier.physicsBody setAllowsRotation:YES];
    
    [barrier setScale:0.2];
    [barrier setName:@"BARRIER"];
    
    return barrier;
}

-(id)createBarrier:(int)type
{
    SKSpriteNode *barrier=[self getBarrier:type];
    
    if (type==1) {
        [barrier runAction:forBarrierLeft withKey:@"BARRIER_ACTION" ];
    }else if (type==2){
        [barrier runAction:forBarrierCenter withKey:@"BARRIER_ACTION"];
    }else if (type==3){
        [barrier runAction:forBarrierRight withKey:@"BARRIER_ACTION"];
    }
    
    return barrier;
}

-(void)addBarriers
{
    [self runAction:[SKAction repeatActionForever:[SKAction sequence:@[[SKAction waitForDuration:0.5],[SKAction runBlock:^{
        
        int rnd=[self randomFloatBetween:1 and:4];
        SKSpriteNode *barrier=[self createBarrier:rnd];
        [self addChild:barrier];
        
    }]]]] withKey:@"BARRIER_ACTION"];
}

Обратите внимание, что барьеру сразу прикрепляем SKPhysicsBody типа Edge, в виде линии по нижней границе барьера. В методе addBarriers случайным образом выбираем какой барьер будет создан (слева, справа или в центре). У меня один барьер в строке, можно и усложнить в этом месте алгоритм.
Теперь необходимо вызвать метод addBarriers из метода startGame:

[self addBarriers];

Запускайте и проверяйте. После нажатия на Play должны показаться барьеры. Их пока можно не объезжать ) Они легко проезжают сквозь авто.
9scr

5. Добавим физику и геймовер
5.1. В метод didMoveToView добавим делегирование:

[self.physicsWorld setContactDelegate:self];

Это позволит нам узнать о контактах физических тел.
5.2. Теперь в самом верху GameScene.m добавим категории для барьеров и автомобиля:

static const uint32_t carCategory =  0x1 << 0;
static const uint32_t barrierCategory =  0x1 << 1;

5.3 Теперь добавим физическое тело и категорию в метод newCarNode:

- (SKSpriteNode *)newCarNode:(int)type
{
    car=[SKSpriteNode spriteNodeWithTexture:carCenterTexture];
    [car setSize:CGSizeMake(50, 120)];
    [car setScale:0.8];
    [car setName:@"CAR"];
    
    ///////SKPhysicsBody///////
    car.physicsBody=[SKPhysicsBody bodyWithRectangleOfSize:car.size];
    [car.physicsBody setDynamic:YES];
    [car.physicsBody setAffectedByGravity:NO];
    [car.physicsBody setAllowsRotation:YES];
    car.physicsBody.categoryBitMask = carCategory;
    //////////////////////////
    
    statecar=type;
    [self changeStateCar:statecar sprite:car];
    [car setZPosition:1];
    return car;
}

Запустите и нажмите Play. Барьеры должны сносить машину вниз экрана.

5.4. Теперь надо присвоить категорию контакта для барьеров. Для этого надо в метод добавить несколько строчек кода:

-(void)addBarriers
{
    [self runAction:[SKAction repeatActionForever:[SKAction sequence:@[[SKAction waitForDuration:0.5],[SKAction runBlock:^{
        
        int rnd=[self randomFloatBetween:1 and:4];
        SKSpriteNode *barrier=[self createBarrier:rnd];
        
        /////////ADD CATEGORY
        barrier.physicsBody.categoryBitMask = barrierCategory;
        barrier.physicsBody.contactTestBitMask = carCategory;
        
        [self addChild:barrier];
        
    }]]]] withKey:@"BARRIER_ACTION"];
}

5.5. Теперь нам просто надо отловить момент столкновения, это просто. Добавьте в GameScene.m:

- (void)didBeginContact:(SKPhysicsContact *)contact
{
    NSLog(@"BOOM");
}

Заупскайте и проверяйте, при столкновении в консоль должно выводиться BOOM.

5.6. Добавьте в самое начало GameScene.m дефайн для перевода градусов в радианы:

#define DEGREES_RADIANS(angle) ((angle) / 180.0 * M_PI)

5.7. Теперь в метод didBeginContact добавим наполнение:

- (void)didBeginContact:(SKPhysicsContact *)contact
{
    SKPhysicsBody *barrierBody, *carBody;
    barrierBody = contact.bodyA;
    carBody = contact.bodyB;

    [carBody setDynamic:NO];
    
    //Удаляем все экшены, которые отвечают за эффект движения
    for (SKSpriteNode *spr in self.children) {
        [spr removeActionForKey:@"ROADSIDE_ACTION"];
        [spr removeActionForKey:@"BARRIER_ACTION"];
    }
    [self removeActionForKey:@"BARRIER_ACTION"];

    //Немного сминаем автомобиль и барьер
    [carBody.node runAction:[SKAction scaleYTo:0.5 duration:0.15]];
    [barrierBody.node runAction:[SKAction rotateByAngle:DEGREES_RADIANS(45) duration:0.15]];
    
    //Удаляем свайпы
    for (UIGestureRecognizer *gr in self.view.gestureRecognizers) {
        [self.view removeGestureRecognizer:gr];
    }
    
    [self showPlayButton];
}

Запускайте и проверяйте. После столкновения должно все останавливаться и появляться кнопка Play.
10

5.8. При повторном нажатии на кнопку Play получится ерунда, поскольку появятся новые спрайты автомобиля, барьеров и всего что на обочине. Поэтому нам нужен метод, который удаляет все старое. Добавим в GameScene.m метод:

-(void)removeOldSprites
{
    for (SKSpriteNode *spr in self.children) {
        
        if ([spr.name isEqualToString:@"ROADSIDE"] || [spr.name isEqualToString:@"BARRIER"]) {
            [spr removeFromParent];
        }
    }
    [car removeFromParent];
}

И добавим вызов метода в touchesBegan:

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [touches anyObject];
    CGPoint location = [touch locationInNode:self];

    SKNode *node = [self nodeAtPoint:location];
    
    if ([node.name isEqualToString:@"PLAYBTN"]) {
        [self removeOldSprites];
        [self startGame];
        [node removeFromParent];
    }
}

Запускайте и проверяйте. После дтп нажмите на Play, все должно быть круто.

6. Добавим еще две плюшки

6.1. Подсчет очков. Очки у нас уже плюсуются, мы это прописали в анимации для барьеров (поиском score++). Добавим Label для отображения очков. Сначала в GameScene.h добавьте:

SKLabelNode *scoreLabel;

Потом в метод didMoveToView добавьте создание метки:

scoreLabel = [SKLabelNode labelNodeWithFontNamed:@"Chalkduster"];
[scoreLabel setText:@"0"];
[scoreLabel setFontColor:[SKColor yellowColor]];
[scoreLabel setFontSize:30];
[scoreLabel setPosition:CGPointMake(10,self.size.height-40)];
[scoreLabel setZPosition:1];
[scoreLabel setName:@"SCORELAB"];
[scoreLabel setHorizontalAlignmentMode:SKLabelHorizontalAlignmentModeLeft];
[self addChild:scoreLabel];

6.2. Теперь в обработчик анимации барьеров добавим обновление метки (поиском найдите score++):

    SKAction *addScore=[SKAction runBlock:^{
        score++;
        [scoreLabel setText:[NSString stringWithFormat:@"%d", score]];
    }];

6.3. Теперь добавим в метод startGame обнуление:

score=0;
[scoreLabel setText:@"0"];

Запускайте и проверяйте, очки должны подсчитываться и обнуляться при новой игре.

6.4. И наконец добавим стук столкновения. Скачайте этот mp3 boom.mp3, распакуйте и перетащите в дерево проекта. Теперь в самое начало метода didBeginContact вставьте:

[self runAction:[SKAction playSoundFileNamed:@"boom.mp3" waitForCompletion:NO]];

Запускайте и проверяйте, должен появиться звук. В общем, самая примитивная реализация игры готова. Все прописано в одном классе для наглядности. Если запускать такую игру в аппстор, над ней надо еще очень много работать.
В этом примере я ничего не показал по поводу применения сил и импульсов, потому что их тут некуда применять. Для небольшой демонстрации сил и импульсов я написал небольшой пример — вот архив SKExample. В примере все просто, при тапе по экрану воздушный шар набирает вертикальную высоту, потом начинает снижаться, при поворачивании устройства шар получает горизонтальную скорость. А вертолет вообще сам по себе летает. Можете экспериментировать. Напомню, проект заточен под 4х дюймовый экран (iphone5 и выше) и если вы хотите погонять шар по всему экрану, то надо запускать на реальном устройстве, а не на симуляторе.

В следующей (последней) части расскажу про нововведения в SpriteKit c выходом 6го XCode. И начинаю цикл статей про SceneKit и мир 3D.

По всем вопросам пишите в комменты.

P.S. Теперь можете следить за публикациями на сайте через социалки:
Facebook.com
Vk.com
Twitter.com

  • Никитос

    Класс, уроки очень хорошие. Есть идея для нового — создание в игре джойстика, с помощью которого можно управлять спрайтом (двигать вверх, вниз, влево, вправо)