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

Возьмите, где мы остановились
Если вы еще этого не сделали, мы настоятельно рекомендуем вам завершить предыдущий урок, чтобы убедиться, что мы можем опираться на фундамент, заложенный в первом уроке. В этом уроке мы рассмотрим ряд тем, таких как физика, столкновения, взрывы и добавление многопользовательского режима.
1. Включение физики
Среда 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. - Создавайте разные физические тела для монстров.
2. Столкновения и взрывы
Столкновения и взрывы — это два тесно связанных элемента. Каждый раз, когда пуля, выпущенная цветком, достигает места назначения, при прикосновении пользователя она взрывается. Этот взрыв может вызвать столкновение между взрывом и любыми ракетами поблизости.
Чтобы создать взрыв, когда пуля достигнет своей цели, нам нужен еще 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
|
Пришло время построить проект и запустить игру, чтобы увидеть конечный результат.
Задача: Задачи для этого раздела следующие.
- Измените правила игры, изменив количество монстров и пуль.
- Сделайте игру более сложной, изменив динамику игры. Например, вы можете увеличить скорость ракеты, если применили пять пуль.
3. Многопользовательский
В многопользовательском режиме игры два игрока могут соревноваться друг с другом в режиме разделенного экрана. Многопользовательский режим не меняет саму игру. Основные различия между однопользовательским и многопользовательским режимами перечислены ниже.
- Нам нужны два набора активов.
- Положение и ориентация активов должны быть обновлены.
- Нам нужно реализовать игровую логику для второго игрока.
- Взрывы должны быть проверены и зафиксированы для каждого взрыва.
- Только один игрок может выиграть игру.
В многопользовательском режиме игра должна выглядеть как на скриншоте ниже.

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

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