Из этого туториала вы узнаете, как использовать инфраструктуру Sprite Kit для создания игры с фактами на основе вопросов. Он предназначен как для начинающих, так и для опытных пользователей. По пути вы будете применять ядро Sprite Kit. Обучающие материалы по игре «Факты» разделены на три части, чтобы полностью охватить каждый раздел. После этого урока, состоящего из трех частей, читатели смогут создать простую игру с ответами на вопросы и ответы, включающую звуки, анимацию, меню, правила, таймеры и взаимодействие с UIKit.
Вступление
Эта серия разделена на три части: настройка проекта, создание интерфейса и игровая логика. Каждая часть даст практический результат, а сумма всех частей — финальную игру. Несмотря на то, что каждая часть может быть прочитана независимо, для лучшего понимания и руководства, мы предлагаем следовать этому руководству шаг за шагом. Мы также включили исходный код для каждой части отдельно. Таким образом, предоставляя способ начать учебник в любой части серии.
Это вторая часть нашей учебной серии «Игра с фактами» в Sprite Kit. В этом уроке вы запрограммируете выбор уровня и интерфейс основной игровой сцены. В этом руководстве рассматриваются несколько аспектов, таких как пользовательский UITableView
, пользовательские инициализаторы классов, списки свойств и SKActions
. Мы объясним все ниже. Если вы еще не завершили первую часть серии, вы можете скачать проект и получить именно то место, где мы остановились.
Вот как будет выглядеть наш конечный результат:
1. Выбор уровня
Шаг 1
Основная цель этой игры — создать несколько вопросов, разделенных на несколько уровней. Таким образом, вам нужно сформировать индивидуальный интерфейс, чтобы выбрать уровень, который вы хотите играть. Для этого вам нужно добавить еще один класс Objective-C
( File -> New -> File
). Назовите его LevelSelect
и выберите SKScene
в качестве суперкласса. Вы увидите два новых файла в вашем проекте.
Для выбора уровня вы будете использовать представление UITableView
и настроить его с несколькими свойствами. Кроме того, вам также нужно назвать уровни и их описания. В LevelSelect.h
вы должны добавить эти свойства. Следующий фрагмент поможет вам:
1
2
3
4
|
@property (nonatomic, retain) UIButton* backButton;
@property (retain, nonatomic) IBOutlet UITableView *tableView;
@property (strong, nonatomic) NSArray *levelsArray;
@property (strong, nonatomic) NSArray *levelsDescriptionArray;
|
Фрагмент также содержит UIButton
именем backButton. В этот момент кнопка не требует пояснений; это помогает пользователю вернуться от интерфейса выбора уровня к основному интерфейсу.
Шаг 2
Далее сосредоточимся на LevelSelect.m
. Первый шаг — добавить метод -(id)initWithSize:(CGSize)size
. В этом классе вы будете настраивать только цвет фона. Вы можете выбрать цвет, который вам нравится больше всего.
1
2
3
4
5
6
|
-(id)initWithSize:(CGSize)size{
if (self = [super initWithSize:size]) {
self.backgroundColor = [SKColor colorWithRed:0.25 green:0.35 blue:0.15 alpha:1.0];
}
return self;
}
|
Поскольку вы используете представления UIKit
, вам необходимо добавить метод представления -(void) didMoveToView:(SKView *)view
. Этот метод определяет и настраивает backButton и tableView, размещает метку заголовка для представления, выделяет и инициализирует levelsArray и levelsDescriptionArray и добавляет tableView к основному представлению. backButton
может быть настроен следующим образом:
1
2
3
4
5
6
7
8
9
|
_backButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
_backButton.frame = CGRectMake(CGRectGetMidX(self.frame)-100, CGRectGetMaxY(self.frame)-100, 200, 70.0);
_backButton.backgroundColor = [UIColor clearColor];
[_backButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal ];
UIImage *buttonExitImageNormal = [UIImage imageNamed:@»back.png»];
UIImage *strechableButtonExitImageNormal = [buttonExitImageNormal stretchableImageWithLeftCapWidth:12 topCapHeight:0];
[_backButton setBackgroundImage:strechableButtonExitImageNormal forState:UIControlStateNormal];
[_backButton addTarget:self action:@selector(moveToHome) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:_backButton];
|
Кроме того, вы должны создать метод moveToHome
и import
MyScene.h
.
1
2
3
4
5
|
-(void) moveToHome{
MyScene* myScene = [[MyScene alloc] initWithSize:CGSizeMake(CGRectGetMaxX(self.frame), CGRectGetMaxY(self.frame))];
[self removeUIViews];
[self.scene.view presentScene:myScene];
}
|
Поскольку вам нужно удалить представления UIKIt
более чем в одном месте, давайте создадим метод, который делает именно это. Метод называется removeUIViews
и показан ниже:
1
2
3
4
|
-(void)removeUIViews{
[_backButton removeFromSuperview];
[_tableView removeFromSuperview];
}
|
Ярлык для этого интерфейса очень прост. Попробуйте запрограммировать это самостоятельно. Если у вас возникнут проблемы, вам поможет следующий фрагмент кода.
1
2
3
4
5
|
SKLabelNode *titleLabel = [SKLabelNode labelNodeWithFontNamed:@»Chalkduster»];
titleLabel.text = @»Level Select!!»;
titleLabel.fontSize = 60;
titleLabel.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame)+300);
[self addChild:titleLabel];
|
Шаг 3
Конфигурирование tableView
немного сложнее, поскольку нам нужно настроить несколько свойств, таких как размер и расположение кадра, источник данных и делегат.
1
2
3
|
_tableView = [[UITableView alloc] initWithFrame:CGRectMake(CGRectGetMidX(self.frame)-150, CGRectGetMidY(self.frame)-250, 300, 400)];
_tableView.dataSource = self;
_tableView.delegate = self;
|
Вторая и третья строка вышеупомянутого фрагмента требует дополнительного шага, так как вы определяете одновременно два представления. Источник данных UITableView
является самоопределяемым, и у UITableView
есть делегат действия. В LevelSelect.h
вы должны расширить свой класс с помощью UITableViewDataSource
и UITableViewDelegate
. Ваш LevelSelect.h
должен выглядеть так:
1
|
@interface LevelSelect : SKScene < UITableViewDataSource, UITableViewDelegate >
|
Поскольку вы расширяете UITableViewDataSource
и UITableViewDelegate
, необходимо реализовать дополнительные методы, а именно:
-
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
-
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
Первый используется только для того, чтобы знать в реальном времени количество строк, существующих в табличном представлении. Второй метод сложен, так как он используется для заполнения и настройки табличного представления. Конфигурация табличного представления включает заголовок, описание и изображение строки. Чтобы использовать описание в каждой строке, мы должны инициализировать каждую ячейку стилем ячеек UITableViewCellStyleSubtitle
.
Этот метод отвечает за дополнительный шаг: он проверяет текущий уровень игрока и отключает уровни перед actualPlayerLevel
. Полное объявление методов можно увидеть ниже:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
|
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSString *levels = [_levelsArray objectAtIndex:indexPath.row];
NSString *descriptions = [_levelsDescriptionArray objectAtIndex:indexPath.row];
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@»Identifier»];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@»Identifier»];
}
if (indexPath.row >= actualPlayerLevel)
[cell setUserInteractionEnabled:FALSE];
[cell.textLabel setText:levels];
cell.imageView.image = [UIImage imageNamed:@»appleLogo.png»];
[cell.detailTextLabel setText:descriptions];
return cell;
}
|
1
2
3
|
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [_levelsArray count];
}
|
Вот два примечания относительно -(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
метод -(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
. Вы еще не инициализировали свойства levelsArray, levelsDescriptionArray и actualPlayerLevel. Все будет определено в -(void) didMoveToView:(SKView *)view
метод представления. Не забудьте добавить свойство actualPlayerLevel
в ваш класс:
1
2
3
|
@implementation LevelSelect{
long actualPlayerLevel;
}
|
Шаг 4
levelsArray
определяется именами уровней. Вы можете назвать его «Уровень 1», «Уровень 2» или любое другое имя по вашему выбору. levelsDescriptionArray
следует тому же принципу, это описание для каждого уровня и может быть определено с именами по вашему выбору. Возможная реализация:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
_levelsArray = [[NSArray alloc] initWithObjects:
@»Level 1.»,
@»Level 2.»,
@»Level 3.»,
@»Level 4.»,
@»Level 5.»,
@»Level 6.»,
@»Level 7.»,
@»Level 8.»,
@»Level 9.»,
nil];
_levelsDescriptionArray = [[NSArray alloc] initWithObjects:
@»The adventure begins.»,
@»A new step.»,
@»Achivements?!»,
@»Level 4 description»,
@»Level 5 description»,
@»Level 6 description»,
@»Level 7 description»,
@»Level 8 description»,
@»Level 9 description»,
nil];
|
Наконец, actualPlayerLevel
— это тип long
значений, который представляет уровень игрока. Пока скажем, что текущий уровень равен 1.
1
|
actualPlayerLevel = 1;
|
-(void) didMoveToView:(SKView *)view
метод представления заканчивается, когда вы добавляете представление таблицы на экран:
1
|
[self.view addSubview:_tableView];
|
Наконец, вам необходимо внести дополнительные изменения в метод MyScene.m
-(void) moveToGame
. Вы должны назвать этот новый класс вместо старого. Это может быть легко достигнуто через:
1
2
3
4
5
|
-(void) moveToGame{
LevelSelect* factsScene = [[LevelSelect alloc] initWithSize:CGSizeMake(CGRectGetMaxX(self.frame), CGRectGetMaxY(self.frame))];
// Same code
// …
}
|
На этом этапе вы должны Run
проект и протестировать новый интерфейс. Вы должны увидеть нечто похожее на следующее изображение:
2. Факты Интерфейс
Шаг 1
Интерфейс фактов — это то, где происходит реальное действие. Интерфейс имеет несколько представлений, которые напрямую преобразуются в свойства, такие как количество оставшихся жизней, текущий уровень, таймер, а также кнопки true и false. Более того, для этого класса вы создадите собственный инициализатор. Преимущество этого пользовательского инициализатора в том, что мы можем пройти через игру и значения относительно уровня. Кроме того, жизнь передается классу, и класс реагирует (анализирует данные) соответственно.
Еще раз, мы будем использовать SKLabelNode
, UIButtons
и NSMutableArray
. Полный FactsScene.h
выглядит следующим образом:
01
02
03
04
05
06
07
08
09
10
|
@interface FactsScene : SKScene{
NSMutableArray* heartArray;
}
@property (nonatomic,weak) SKLabelNode* currentLevelLabel;
@property (nonatomic,weak) SKLabelNode* timerLevel;
@property (nonatomic, retain) UIButton* trueButton;
@property (nonatomic, retain) UIButton* falseButton;
-(id) initWithSize:(CGSize)size inLevel:(NSInteger)level withPlayerLives:(int)lives;
|
Теперь FactsScene.m
время перейти к FactsScene.m
и реализовать несколько объектов. Вам нужны дополнительные объекты для извлечения и хранения данных, полученных инициализатором. Кроме того, вам нужно хранить максимальное время, которое игрок должен ответить на каждый вопрос. Измените файл реализации соответствующим образом.
1
2
3
4
5
6
7
8
|
@implementation FactsScene{
NSUserDefaults* defaults;
NSString* musicPath;
NSInteger playerLives;
NSInteger playerLevel;
int maximumTime;
}
|
Теперь вы должны написать инициализатор и сохранить его значения. Пользовательский инициализатор определяется как обычный инициализатор и имеет такую же структуру. Тем не менее, он имеет больше свойств в качестве параметров. Это должно выглядеть так:
01
02
03
04
05
06
07
08
09
10
11
12
|
-(id) initWithSize:(CGSize)size inLevel:(NSInteger)level withPlayerLives:(int)lives{
if (self = [super initWithSize:size]) {
self.backgroundColor = [SKColor colorWithRed:0.35 green:0.25 blue:0.5 alpha:1.0];
defaults = [NSUserDefaults standardUserDefaults];
playerLives = lives;
playerLevel = level;
maximumTime = 30;
}
return self;
}
|
Шаг 2
На данный момент maximumTime
составляет 30 секунд, но в будущем значение изменится на 60 секунд (или любое другое время по вашему выбору). Теперь в -(void) didMoveToView:(SKView *)view
добавьте фоновое изображение, фронтальное изображение, жизни игрока, таймер, кнопки «истина» и «ложь», а также вопросы о текущем и общем уровне. Для background
и front
изображения код прост, и вы должны легко это сделать (используя SKSpriteNode
).
1
2
3
4
5
6
7
8
9
|
SKSpriteNode *background = [SKSpriteNode spriteNodeWithImageNamed:@»background.png»];
background.position = CGPointMake(CGRectGetMidX(self.frame),CGRectGetMidY(self.frame));
background.size = CGSizeMake(768, 1024);
[self addChild:background];
SKSpriteNode *frontImage = [SKSpriteNode spriteNodeWithImageNamed:@»transparentCenterBorder.png»];
frontImage.position = CGPointMake(CGRectGetMidX(self.frame),CGRectGetMidY(self.frame));
frontImage.size = CGSizeMake(600, 450);
[self addChild:frontImage];
|
Жизнь игрока представлена изображением сердца. Поскольку вы объявите три жизни, вы должны поместить три сердца на экране. Вы также будете использовать NSMutableArray
поскольку нам нужно динамически изменять его размер. Следующий фрагмент поможет вам:
1
2
3
4
5
6
7
8
|
heartArray = [[NSMutableArray alloc] init];
for(NSInteger i = 0; i < playerLives; i++){
SKSpriteNode* liveImage = [SKSpriteNode spriteNodeWithImageNamed:@»hearth.png»];
liveImage.scale = .6;
liveImage.position = CGPointMake(CGRectGetMaxX(self.frame)-40-(i*50),CGRectGetMaxY(self.frame)-40);
[heartArray insertObject:liveImage atIndex:i];
[self addChild:liveImage];
}
|
Кнопки истина и ложь настроены как:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
|
_trueButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
_trueButton.frame = CGRectMake(CGRectGetMidX(self.frame)-350, CGRectGetMidY(self.frame)+300, 335, 106);
_trueButton.backgroundColor = [UIColor clearColor];
[_trueButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal ];
UIImage *buttonTrueImageNormal = [UIImage imageNamed:@»trueBtn.png»];
UIImage *strechableButtonTrueImageNormal = [buttonTrueImageNormal stretchableImageWithLeftCapWidth:12 topCapHeight:0];
[_trueButton setBackgroundImage:strechableButtonTrueImageNormal forState:UIControlStateNormal];
[_trueButton addTarget:self action:@selector(touchWillProduceASound) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:_trueButton];
_falseButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
_falseButton.frame = CGRectMake(CGRectGetMidX(self.frame)+10, CGRectGetMidY(self.frame)+300, 335, 106);
_falseButton.backgroundColor = [UIColor clearColor];
[_falseButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal ];
UIImage *buttonFalseImageNormal = [UIImage imageNamed:@»falseBtn.png»];
UIImage *strechableButtonFalseImageNormal = [buttonFalseImageNormal stretchableImageWithLeftCapWidth:12 topCapHeight:0];
[_falseButton setBackgroundImage:strechableButtonFalseImageNormal forState:UIControlStateNormal];
[_falseButton addTarget:self action:@selector(touchWillProduceASound) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:_falseButton];
|
Обратите внимание, что обе кнопки вызывают метод touchWillProduceASound
. Этот метод проверяет, является ли данный ответ правильным или неправильным. В этой части мы используем только одно звуковое событие (ложное).
01
02
03
04
05
06
07
08
09
10
11
12
|
-(void) touchWillProduceASound{
long soundFlag = [defaults integerForKey:@»sound»];
NSString* answer = @»False»;
if (soundFlag == 1){
SKAction* sound;
if ([answer isEqualToString:@»False»]) {
sound = [SKAction playSoundFileNamed:@»wrong.mp3″ waitForCompletion:YES];
NSLog(@»inside»);
}
[self runAction:sound];
}
}
|
Шаг 3
Таймер игры — это SKLabelNode
который меняется каждую секунду. Однако его инициализация выполняется в виде простого SKLabelNode
:
1
2
3
4
5
|
_timerLevel = [SKLabelNode labelNodeWithFontNamed:@»Chalkduster»];
_timerLevel.text = @»30″;
_timerLevel.fontSize = 70;
_timerLevel.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame)+350);
[self addChild:_timerLevel];
|
Чтобы обновить метку, вам нужно создать SKAction
который определяет пользовательский таймер для вызова пользовательского метода. Затем мы должны создать последовательность SKAction
:
1
2
3
4
5
6
7
|
SKAction *wait = [SKAction waitForDuration:1];
SKAction *updateTimer = [SKAction runBlock:^{
[self updateTimer];
}];
SKAction *updateTimerS = [SKAction sequence:@[wait,updateTimer]];
[self runAction:[SKAction repeatActionForever:updateTimerS]];
|
Вы увидите предупреждение о методе - (void)updateTimer
потому что вы его еще не создали. Этот метод выполняет несколько действий одновременно, принимая во внимание несколько свойств:
- Проверяет, включен ли звук
-
maximumTime
свойствоmaximumTime
и метку, уменьшаясь на одно значение каждую секунду. - Проверяет
maximumTime
, и если значение равно нулю, оно либо завершит игру, либо перейдет к другому вопросу (подробнее об этом в следующем уроке)
Мы советуем вам попытаться написать метод, используя вышеупомянутый псевдокод. Но если у вас возникли проблемы, полный метод представлен ниже:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
— (void)updateTimer{
maximumTime—;
if (maximumTime == 0){
long soundFlag = [defaults integerForKey:@»sound»];
if (soundFlag == 1){
SKAction* sound;
sound = [SKAction playSoundFileNamed:@»beep.mp3″ waitForCompletion:YES];
[self runAction:sound];
}
if (playerLives < 1){
SKTransition* transition = [SKTransition fadeWithDuration:2];
MyScene* myscene = [[MyScene alloc] initWithSize:CGSizeMake(CGRectGetMaxX(self.frame), CGRectGetMaxY(self.frame))];
[self removeUIViews];
[self.scene.view presentScene:myscene transition:transition];
} else{
// other
}
}
[_timerLevel setText:[[NSNumber numberWithInt:maximumTime] stringValue]];
}
|
Шаг 4
Отсутствует еще один метод — removeUIViews
. Он удаляет представления UIKit
из представления, когда происходит переход.
1
2
3
4
|
-(void)removeUIViews{
[_trueButton removeFromSuperview];
[_falseButton removeFromSuperview];
}
|
Настало время Run
проект и увидеть экран «Факты»!
3. Списки недвижимости
Теперь пришло время добавить некоторые данные в приложение. Перейдите в File -> New -> File
и выберите File -> New -> File
списка свойств (plist). Назовите его «LevelDescription» и настройте его следующим образом:
Чтобы не помещать все данные в файл plist, вы можете загрузить файл прямо в zip-файл (начало страницы). Этот учебник не охватывает подробно анализ данных, поэтому для анализа этого файла используется следующий фрагмент. Если у вас есть какие-либо сомнения, используйте поле для комментариев в нижней части страницы.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
NSString* plistPath = [[NSBundle mainBundle] pathForResource:@»LevelDescription» ofType:@»plist»];
NSMutableDictionary* dictionary = [[NSMutableDictionary alloc] initWithContentsOfFile:plistPath];
if ([dictionary objectForKey:@»Questions» ] != nil ){
NSMutableArray *array = [dictionary objectForKey:@»Questions»];
for(int i = 0; i < [array count]; i++){
NSMutableDictionary *questions = [array objectAtIndex:i];
NSLog(@»ID %@», [questions objectForKey:@»id»]);
NSLog(@»%@», [questions objectForKey:@»statement»]);
NSLog(@»%@», [questions objectForKey:@»isCorrect»]);
NSLog(@»%@», [questions objectForKey:@»additionalInfo»]);
}
}
|
Теперь Run
проект и просмотрите журналы консоли при входе в представление Facts. Обратите внимание, что файл plist может быть настроен несколькими различными способами. Если вы хотите, вы можете изменить Dictionary
, типы Array
и конфигурации. Обратите внимание, что изменения должны быть объявлены в цикле анализа. Мы настоятельно рекомендуем вам немного поиграть с файлом plists и анализом данных.
Вывод
На этом этапе вы сможете использовать и настраивать UITableView, взаимодействовать между средами SpriteKit и UIKit, создавать SKTransitions и SKActions, а также создавать и анализировать файлы свойств. В последнем разделе этой серии вы узнаете об Game Logic.