Статьи

iOS SDK: создание игры с фактами — создание интерфейса

Из этого туториала вы узнаете, как использовать инфраструктуру Sprite Kit для создания игры с фактами на основе вопросов. Он предназначен как для начинающих, так и для опытных пользователей. По пути вы будете применять ядро ​​Sprite Kit. Обучающие материалы по игре «Факты» разделены на три части, чтобы полностью охватить каждый раздел. После этого урока, состоящего из трех частей, читатели смогут создать простую игру с ответами на вопросы и ответы, включающую звуки, анимацию, меню, правила, таймеры и взаимодействие с UIKit.


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

Это вторая часть нашей учебной серии «Игра с фактами» в Sprite Kit. В этом уроке вы запрограммируете выбор уровня и интерфейс основной игровой сцены. В этом руководстве рассматриваются несколько аспектов, таких как пользовательский UITableView , пользовательские инициализаторы классов, списки свойств и SKActions . Мы объясним все ниже. Если вы еще не завершили первую часть серии, вы можете скачать проект и получить именно то место, где мы остановились.

Вот как будет выглядеть наш конечный результат:

image0
Иллюстрация окончательного результата — факты

Основная цель этой игры — создать несколько вопросов, разделенных на несколько уровней. Таким образом, вам нужно сформировать индивидуальный интерфейс, чтобы выбрать уровень, который вы хотите играть. Для этого вам нужно добавить еще один класс 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. В этот момент кнопка не требует пояснений; это помогает пользователю вернуться от интерфейса выбора уровня к основному интерфейсу.

Далее сосредоточимся на 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];

Конфигурирование 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 , необходимо реализовать дополнительные методы, а именно:

  1. -(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
  2. -(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;
}

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 проект и протестировать новый интерфейс. Вы должны увидеть нечто похожее на следующее изображение:

Изображение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;
}

На данный момент 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];
    }
}

Таймер игры — это 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]];
}

Отсутствует еще один метод — removeUIViews . Он удаляет представления UIKit из представления, когда происходит переход.

1
2
3
4
-(void)removeUIViews{
    [_trueButton removeFromSuperview];
    [_falseButton removeFromSuperview];
}

Настало время Run проект и увидеть экран «Факты»!

Image2
Иллюстрация экрана фактов

Теперь пришло время добавить некоторые данные в приложение. Перейдите в File -> New -> File и выберите File -> New -> File списка свойств (plist). Назовите его «LevelDescription» и настройте его следующим образом:

Image3
Иллюстрация списка недвижимости

Чтобы не помещать все данные в файл 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.