Статьи

Сборка ракетной команды с помощью набора спрайтов

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


Посмотрите на следующий скриншот, чтобы понять, к чему мы стремимся.



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


Среда Sprite Kit включает физический движок, имитирующий физические объекты. Физический движок платформы Sprite Kit работает через протокол SKPhysicsContactDelegate . Чтобы включить физический движок в нашей игре, нам нужно изменить класс MyScene . Начните с обновления файла заголовка, как показано ниже, чтобы сообщить компилятору, что класс SKScene соответствует протоколу SKPhysicsContactDelegate .

1
2
3
4
5
#import <SpriteKit/SpriteKit.h>
 
@interface MyScene : SKScene <SKPhysicsContactDelegate>
 
@end

Протокол SKPhysicsContactDelegate позволяет нам обнаружить, столкнулись ли два объекта друг с другом. Экземпляр MyScene должен реализовывать протокол SKPhysicsContactDelegate , если он хочет получать уведомления о коллизиях между объектами. Объект, реализующий протокол, уведомляется всякий раз, когда коллизия начинается и заканчивается.

Поскольку мы будем иметь дело со взрывами, ракетами и монстрами, мы определим категорию для каждого типа физического объекта. Добавьте следующий фрагмент кода в файл MyScene класса MyScene .

01
02
03
04
05
06
07
08
09
10
11
#import <SpriteKit/SpriteKit.h>
 
typedef enum : NSUInteger {
    ExplosionCategory = (1 << 0),
    MissileCategory = (1 << 1),
    MonsterCategory = (1 << 2)
} NodeCategory;
 
@interface MyScene : SKScene <SKPhysicsContactDelegate>
 
@end

Прежде чем мы сможем приступить к изучению физического движка инфраструктуры Sprite Kit, нам нужно установить свойство gravity в мире физики, а также его contactDelegate . Обновите метод initWithSize: как показано ниже.

01
02
03
04
05
06
07
08
09
10
11
12
13
— (id)initWithSize:(CGSize)size {
    if (self = [super initWithSize:size]) {
        self.backgroundColor = [SKColor colorWithRed:(198.0/255.0) green:(220.0/255.0) blue:(54.0/255.0) alpha:1.0];
         
        // … //
         
        // Configure Physics World
        self.physicsWorld.gravity = CGVectorMake(0, 0);
        self.physicsWorld.contactDelegate = self;
    }
     
    return self;
}

В нашей игре физический движок используется для создания трех типов физических тел, пуль, ракет и монстров. При работе со средой Sprite Kit вы используете динамические и статические тома для моделирования физических объектов. Том для группы объектов — это том, который содержит каждый объект группы. Динамические и статические тома являются важным элементом для повышения производительности физического движка, особенно при работе со сложными объектами. В нашей игре мы определим два типа томов: круги с фиксированным радиусом и пользовательские объекты.

Хотя круги доступны через класс SKPhysicsBody , пользовательский объект требует от нас дополнительной работы. Поскольку тело монстра не круглое, мы должны создать для него специальный том. Чтобы облегчить эту задачу, мы будем использовать физический генератор путей тела . Инструмент прост в использовании. Импортируйте спрайты вашего проекта и определите окружающий путь для каждого спрайта. Код Objective C для воссоздания пути показан ниже спрайта. В качестве примера рассмотрим следующий спрайт.


На следующем снимке экрана показан тот же спрайт с наложением пути, созданного генератором пути физического тела.


Если какой-либо объект касается или перекрывает физическую границу объекта, мы уведомляемся об этом событии. В нашей игре объектами, которые могут коснуться монстров, являются поступающие ракеты. Давайте начнем с использования сгенерированных путей для монстров.

Чтобы создать физическое тело, нам нужно использовать структуру CGMutablePathRef , которая представляет изменяемый путь. Мы используем его, чтобы определить очертания монстров в игре.

Пересмотрите addMonstersBetweenSpace: и создайте изменяемый путь для каждого типа монстров, как показано ниже. Помните, что в нашей игре есть два типа монстров.

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
— (void)addMonstersBetweenSpace:(int)spaceOrder {
    for (int i = 0; i< 3; i++) {
        int giveDistanceToMonsters = 60 * i -60;
        int randomMonster = [self getRandomNumberBetween:0 to:1];
 
        SKSpriteNode *monster;
        CGMutablePathRef path = CGPathCreateMutable();
 
        if (randomMonster == 0) {
            monster = [SKSpriteNode spriteNodeWithImageNamed:@»protectCreature4″];
 
            CGFloat offsetX = monster.frame.size.width * monster.anchorPoint.x;
            CGFloat offsetY = monster.frame.size.height * monster.anchorPoint.y;
            CGPathMoveToPoint(path, NULL, 10 — offsetX, 1 — offsetY);
            CGPathAddLineToPoint(path, NULL, 42 — offsetX, 0 — offsetY);
            CGPathAddLineToPoint(path, NULL, 49 — offsetX, 13 — offsetY);
            CGPathAddLineToPoint(path, NULL, 51 — offsetX, 29 — offsetY);
            CGPathAddLineToPoint(path, NULL, 50 — offsetX, 42 — offsetY);
            CGPathAddLineToPoint(path, NULL, 42 — offsetX, 59 — offsetY);
            CGPathAddLineToPoint(path, NULL, 29 — offsetX, 67 — offsetY);
            CGPathAddLineToPoint(path, NULL, 19 — offsetX, 67 — offsetY);
            CGPathAddLineToPoint(path, NULL, 5 — offsetX, 53 — offsetY);
            CGPathAddLineToPoint(path, NULL, 0 — offsetX, 34 — offsetY);
            CGPathAddLineToPoint(path, NULL, 1 — offsetX, 15 — offsetY);
            CGPathCloseSubpath(path);
 
        } else {
            monster = [SKSpriteNode spriteNodeWithImageNamed:@»protectCreature2″];
 
            CGFloat offsetX = monster.frame.size.width * monster.anchorPoint.x;
            CGFloat offsetY = monster.frame.size.height * monster.anchorPoint.y;
            CGPathMoveToPoint(path, NULL, 0 — offsetX, 1 — offsetY);
            CGPathAddLineToPoint(path, NULL, 47 — offsetX, 1 — offsetY);
            CGPathAddLineToPoint(path, NULL, 47 — offsetX, 24 — offsetY);
            CGPathAddLineToPoint(path, NULL, 40 — offsetX, 43 — offsetY);
            CGPathAddLineToPoint(path, NULL, 28 — offsetX, 53 — offsetY);
            CGPathAddLineToPoint(path, NULL, 19 — offsetX, 53 — offsetY);
            CGPathAddLineToPoint(path, NULL, 8 — offsetX, 44 — offsetY);
            CGPathAddLineToPoint(path, NULL, 1 — offsetX, 26 — offsetY);
            CGPathCloseSubpath(path);
        }
 
        monster.zPosition = 2;
        monster.position = CGPointMake(position * spaceOrder — giveDistanceToMonsters, monster.size.height / 2);
 
        [self addChild:monster];
    }
}

Теперь, когда путь готов к использованию, нам нужно обновить свойство physicsBody монстра, а также ряд других свойств. Взгляните на следующий фрагмент кода для пояснения.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
— (void)addMonstersBetweenSpace:(int)spaceOrder {
    for (int i = 0; i< 3; i++) {
        // … //
         
        monster.physicsBody = [SKPhysicsBody bodyWithPolygonFromPath:path];
        monster.physicsBody.dynamic = YES;
        monster.physicsBody.categoryBitMask = MonsterCategory;
        monster.physicsBody.contactTestBitMask = MissileCategory;
        monster.physicsBody.collisionBitMask = 1;
        monster.zPosition = 2;
        monster.position = CGPointMake(position * spaceOrder — giveDistanceToMonsters, monster.size.height / 2);
         
        [self addChild:monster];
    }
}

Свойства categoryBitMask и contactTestBitMask объекта physicsBody являются важной частью и могут нуждаться в пояснении. Свойство categoryBitMask объекта physicsBody определяет, к каким категориям относится узел. Свойство contactTestBitMask определяет, какие категории тел вызывают уведомления о пересечении с узлом. Другими словами, эти свойства определяют, какие объекты могут сталкиваться с какими объектами.

Поскольку мы настраиваем узлы монстров, мы устанавливаем для categoryBitMask значение MonsterCategory а для contactTestBitMask значение MissileCategory . Это означает, что монстры могут сталкиваться с ракетами, и это позволяет нам определять, когда монстр поражен ракетой.

Нам также необходимо обновить нашу реализацию addMissilesFromSky: Определить физическое тело для ракет гораздо проще, поскольку каждая ракета имеет круглую форму. Посмотрите на обновленную реализацию ниже.

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
— (void)addMissilesFromSky:(CGSize)size {
    int numberMissiles = [self getRandomNumberBetween:0 to:3];
 
    for (int i = 0; i < numberMissiles; i++) {
        SKSpriteNode *missile;
        missile = [SKSpriteNode spriteNodeWithImageNamed:@»enemyMissile»];
        missile.scale = 0.6;
        missile.zPosition = 1;
 
        int startPoint = [self getRandomNumberBetween:0 to:size.width];
        missile.position = CGPointMake(startPoint, size.height);
 
        missile.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius:missile.size.height/2];
        missile.physicsBody.dynamic = NO;
        missile.physicsBody.categoryBitMask = MissileCategory;
        missile.physicsBody.contactTestBitMask = ExplosionCategory |
        missile.physicsBody.collisionBitMask = 1;
 
        int endPoint = [self getRandomNumberBetween:0 to:size.width];
        SKAction *move =[SKAction moveTo:CGPointMake(endPoint, 0) duration:15];
        SKAction *remove = [SKAction removeFromParent];
        [missile runAction:[SKAction sequence:@[move,remove]]];
 
        [self addChild:missile];
    }
}

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

Задача: Задачи для этого раздела следующие.

  • Прочитайте и поймите класс SKPhysicsBody .
  • Создавайте разные физические тела для монстров.

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

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

Чтобы определить взрыв, нам нужно сфокусироваться на SKSpriteNode взрыва, его zPosition , scale и position . position — это местоположение касания пользователя.

Чтобы создать физическое тело взрыва, нам нужно установить свойство physicsBody узла, как мы делали ранее. Не забудьте правильно установить свойства categoryBitMask и contactTestBitMask физического тела. Мы создаем взрыв в touchesBegan: как показано ниже.

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
— (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    for (UITouch *touch in touches) {
        // … //
         
        SKSpriteNode *bullet = [SKSpriteNode spriteNodeWithImageNamed:@»flowerBullet»];
        bullet.zPosition = 1;
        bullet.scale = 0.6;
        bullet.position = CGPointMake(bulletBeginning,110);
        bullet.color = [SKColor redColor];
        bullet.colorBlendFactor = 0.5;
        float duration = (2 * location.y)/sizeGlobal.width;
        SKAction *move =[SKAction moveTo:CGPointMake(location.x,location.y) duration:duration];
        SKAction *remove = [SKAction removeFromParent];
         
        // Explosion
        SKAction *callExplosion = [SKAction runBlock:^{
            SKSpriteNode *explosion = [SKSpriteNode spriteNodeWithImageNamed:@»explosion»];
            explosion.zPosition = 3;
            explosion.scale = 0.1;
            explosion.position = CGPointMake(location.x,location.y);
            explosion.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius:explosion.size.height/2];
            explosion.physicsBody.dynamic = YES;
            explosion.physicsBody.categoryBitMask = ExplosionCategory;
            explosion.physicsBody.contactTestBitMask = MissileCategory;
            explosion.physicsBody.collisionBitMask = 1;
            SKAction *explosionAction = [SKAction scaleTo:0.8 duration:1.5];
            [explosion runAction:[SKAction sequence:@[explosionAction,remove]]];
            [self addChild:explosion];
        }];
         
        [bullet runAction:[SKAction sequence:@[move,callExplosion,remove]]];
         
        [self addChild:bullet];
    }
}

В touchesBegan: мы обновили действие bullet . Новое действие должно вызвать действие callExplosion прежде чем оно будет удалено со сцены. Для этого мы обновили следующую строку кода в touchesBegan:

1
[bullet runAction:[SKAction sequence:@[move,remove]]];

Последовательность действия теперь содержит callExplosion как показано ниже.

1
[bullet runAction:[SKAction sequence:@[move,callExplosion,remove]]];

Создайте проект и запустите приложение, чтобы увидеть результат нашей работы. Как видите, нам все еще нужно обнаруживать столкновения между взрывами и поступающими ракетами. Здесь вступает в SKPhysicsContactDelegate протокол SKPhysicsContactDelegate .

Есть один метод делегата, который представляет для нас особый интерес, didBeginContact: метод. Этот метод скажет нам, когда происходит столкновение между взрывом и ракетой. Метод didBeginContact: принимает один аргумент, экземпляр класса SKPhysicsContact , который сообщает нам все, что нам нужно знать о столкновении. Позвольте мне объяснить, как это работает.

Экземпляр SKPhysicsContact имеет bodyA и bodyB . Каждое тело указывает на физическое тело, которое участвует в столкновении. Когда didBeginContact: вызывается, мы должны определить, с каким типом столкновения мы имеем дело. Это может быть (1) столкновение между взрывом и ракетой или (2) столкновение между ракетой и монстром. Мы обнаруживаем тип столкновения, SKPhysicsContact свойство categoryBitmask физических тел экземпляра SKPhysicsContact .

Выяснить, с каким типом столкновения мы имеем дело, довольно просто благодаря свойству categoryBitmask . Если bodyA или bodyB имеет categoryBitmask типа ExplosionCategory , то мы знаем, что это столкновение между взрывом и ракетой. Посмотрите на фрагмент кода ниже для пояснения.

1
2
3
4
5
6
7
— (void)didBeginContact:(SKPhysicsContact *)contact {
    if ((contact.bodyA.categoryBitMask & ExplosionCategory) != 0 || (contact.bodyB.categoryBitMask & ExplosionCategory) != 0) {
        NSLog(@»EXPLOSION HIT»);
    } else {
        NSLog(@»MONSTER HIT»);
    }
}

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

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

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
— (void)didBeginContact:(SKPhysicsContact *)contact {
    if ((contact.bodyA.categoryBitMask & ExplosionCategory) != 0 || (contact.bodyB.categoryBitMask & ExplosionCategory) != 0) {
        // Collision Between Explosion and Missile
        SKNode *missile = (contact.bodyA.categoryBitMask & ExplosionCategory) ?
        [missile runAction:[SKAction removeFromParent]];
         
        //the explosion continues, because can kill more than one missile
        NSLog(@»Missile destroyed»);
         
        // Update Missile Exploded
        missileExploded++;
        [labelMissilesExploded setText:[NSString stringWithFormat:@»Missiles Exploded: %d»,missileExploded]];
         
        if(missileExploded == 20){
            SKLabelNode *ganhou = [SKLabelNode labelNodeWithFontNamed:@»Hiragino-Kaku-Gothic-ProN»];
            ganhou.text = @»You win!»;
            ganhou.fontSize = 60;
            ganhou.position = CGPointMake(sizeGlobal.width/2,sizeGlobal.height/2);
            ganhou.zPosition = 3;
            [self addChild:ganhou];
        }
         
    } else {
        // Collision Between Missile and Monster
    }
}

Если мы имеем дело со столкновением между ракетой и монстром, мы удаляем узел ракеты и монстра со сцены, добавляя действие [SKAction removeFromParent] в список действий, выполняемых узлом. Мы также увеличиваем переменную экземпляра monstersDead и проверяем, равно ли она 6 . Если это так, игрок проиграл игру, и мы отображаем сообщение о том, что игра окончена.

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
— (void)didBeginContact:(SKPhysicsContact *)contact {
    if ((contact.bodyA.categoryBitMask & ExplosionCategory) != 0 || (contact.bodyB.categoryBitMask & ExplosionCategory) != 0) {
        // Collision Between Explosion and Missile
        // … //
         
    } else {
        // Collision Between Missile and Monster
        SKNode *monster = (contact.bodyA.categoryBitMask & MonsterCategory) ?
        SKNode *missile = (contact.bodyA.categoryBitMask & MonsterCategory) ?
        [missile runAction:[SKAction removeFromParent]];
        [monster runAction:[SKAction removeFromParent]];
         
        NSLog(@»Monster killed»);
        monstersDead++;
        if(monstersDead == 6){
            SKLabelNode *perdeu = [SKLabelNode labelNodeWithFontNamed:@»Hiragino-Kaku-Gothic-ProN»];
            perdeu.text = @»You Lose!»;
            perdeu.fontSize = 60;
            perdeu.position = CGPointMake(sizeGlobal.width/2,sizeGlobal.height/2);
            perdeu.zPosition = 3;
            [self addChild:perdeu];
            [self moveToMenu];
        }
    }
}

Прежде чем запускать игру на вашем iPad, нам необходимо реализовать метод moveToMenu . Этот метод вызывается, когда игрок проигрывает игру. В moveToMenu игра возвращается к сцене меню, чтобы игрок мог начать новую игру. Не забудьте добавить оператор импорта для класса MenuScene .

1
2
3
4
5
— (void)moveToMenu {
    SKTransition* transition = [SKTransition fadeWithDuration:2];
    MenuScene* myscene = [[MenuScene alloc] initWithSize:CGSizeMake(CGRectGetMaxX(self.frame), CGRectGetMaxY(self.frame))];
    [self.scene.view presentScene:myscene transition:transition];
}
1
2
3
4
5
6
7
8
9
#import «MyScene.h»
 
#import «MenuScene.h»
 
@interface MyScene () {
    // … //
}
 
@end

Пришло время построить проект и запустить игру, чтобы увидеть конечный результат.

Задача: Задачи для этого раздела следующие.

  • Измените правила игры, изменив количество монстров и пуль.
  • Сделайте игру более сложной, изменив динамику игры. Например, вы можете увеличить скорость ракеты, если применили пять пуль.

В многопользовательском режиме игры два игрока могут соревноваться друг с другом в режиме разделенного экрана. Многопользовательский режим не меняет саму игру. Основные различия между однопользовательским и многопользовательским режимами перечислены ниже.

  • Нам нужны два набора активов.
  • Положение и ориентация активов должны быть обновлены.
  • Нам нужно реализовать игровую логику для второго игрока.
  • Взрывы должны быть проверены и зафиксированы для каждого взрыва.
  • Только один игрок может выиграть игру.

В многопользовательском режиме игра должна выглядеть как на скриншоте ниже.

Многопользовательский режим игры

Это последний вызов этого урока. Это не так сложно, как может показаться. Цель задачи — воссоздать ракетное командование, включив многопользовательский режим. Исходные файлы этого учебника содержат два проекта XCode, один из которых (Missile Command Multi-Player) содержит неполную реализацию многопользовательского режима, чтобы вы могли приступить к этой задаче. Обратите внимание, что класс MultiScene является неполным, и ваша задача — завершить его реализацию, чтобы успешно выполнить задачу. Вы найдете подсказки и комментарии ( /* Work HERE - CODE IS MISSING */ ), чтобы помочь вам с этой задачей.

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

На следующем снимке экрана показано текущее состояние многопользовательского режима.

Неполный многопользовательский режим

В этой короткой серии о Sprite Kit мы рассмотрели много вопросов. Теперь вы сможете создавать игры, похожие на Missile Command, используя инфраструктуру Sprite Kit. Если у вас есть какие-либо вопросы или комментарии, не стесняйтесь, напишите нам в комментариях.