Как стать автором
Обновить

Как сделать простую игру с мультиплеером через Game Center. Часть 1: подключение Game Center'a в приложение

Время на прочтение 14 мин
Количество просмотров 22K
Автор оригинала: Ray Wenderlich


Перевод замечательного туториала по интегрированию мультиплеера с использованием Game Center в iOS-игру. На сайте автора есть множество уроков, призванных помочь начинающим разработчикам игр.

Игра, над которой вы будете работать, очень проста. Это гонка, участниками которой являются собака и ребенок. Тапайте как можно быстрее, чтобы победить!

Данный туториал подразумевает, что вы знакомы с основами Cocos2D. В противном случае советую ознакомиться с другими Cocos2D-уроками, которые вы можете найти тут.

Примечание: для того, чтобы проверить работу Game Center'а в приложении, вы должны быть зарегистрированным iOS-разработчиком. Также вам необходимо как минимум одно физическое iOS-устройство (для того, чтобы одна копия приложения работала в симуляторе, а другая на девайсе). В конце концов, вам нужно как минимум два различных Game Center-аккаунта для тестирования (не волнуйтесь, вы можете создать их бесплатно, только вам нужен другой e-mail-адрес).

Готовы? Тогда ПОЕХАЛИ!

Итак, начнём

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

Скачайте код и запустите проект. Вы должны увидеть что-то подобное:

image

Игра очень проста, и код хорошо прокомментирован. Просмотрите его и удостоверьтесь, что вам всё понятно.

Включаем Game Center

В данный момент у вас есть простая игра, в которую можно поиграть, за исключением того, что в неё очень скучно играть одному! Было бы намного веселее использовать Game Center, чтобы приглашать друзей поиграть с вами или использовать поиск матчей, чтобы играть со случайными людьми. Но перед тем, как начать использовать Game Center, необходимо сделать две вещи:

  1. Создать и настроить App ID;
  2. Зарегистрировать приложение в iTunes Connect.

Давайте же приступим.

Создать и настроить App ID

Первый шаг — создать и настроить App ID. Чтобы сделать это, войдите в iOS Dev Center, а оттуда — в iOS Provisioning Portal. Откройте вкладку App IDs и создайте новый App ID для приложения, так же, как и на скриншоте:

image

Самая важная часть — это Bundle Identifier. Это должна быть уникальная строка (то есть, он не может совпадать с тем, который использую я!) Хорошим способом является использование вашего доменного имени, за которым следует уникальная строка, чтобы избежать совпадений имен.

Когда вы закончили, нажмите Submit. Затем откройте проект Cat Race, выберите Resources\Info.plist (прим. пер.: в поздних версиях Xсode этот файл находится в папке Supporting Files и называется %Название_проекта%-Info.plist) и измените Bundle Identifier на введенный вами в iOS Provisioning Portal, как показано ниже (конечно же, вы будете вводить другое значение):

image

И последняя вещь. Чтобы не было неприятностей со стороны Xcode, выполните следующие шаги:

  • Удалите все копии приложения с вашего устройства или симулятора;
  • Выйдите из симулятора, если он запущен;
  • Выполните Project\Clean.

Поздравляю, теперь у вас есть App ID для приложения, и оно настроено для использования! Теперь вы можете зарегистрировать приложение в iTunes Connect и включить Game Center.

Зарегистрировать приложение в iTunes Connect

Следующим шагом будет вход в iTunes Connect и регистрация нового приложения.

Как только вы вошли в iTunes Connect, выберите Manage Your Applications, затем кликните по голубой кнопке Add New App сверху слева. Введите название приложения, SKU Number и выберите Bundle ID, по подобию раннего образца, также как и на скриншоте:

image

Нажмите Continue и заполните базовую информацию о приложении. Не волнуйтесь о правильности, пока это не так важно, ведь вы сможете изменить это позже. А сейчас вам нужно просто что-то внести (включая иконку и скриншот), чтобы сделать iTunes Connect счастливым.

Когда вы закончите, нажмите Save, и если всё сделано как надо, то статус приложения будет «Prepare for Upload», как на скрине:

image

Нажмите голубую кнопку «Manage Game Center» справа сверху, затем нажмите «Enable». После этого кликните «Done». Вот и всё, Game Center включен для вашего приложения, и вы можете начинать писать код!

Между прочим, в разделе «Manage Game Center», вы также можете управлять списками лидеров и, конечно же, достижениями. В этом уроке работа с ними разобрана не будет.

Авторизация локального игрока: теория

Когда игра запускается, первым делом необходимо авторизовать локального игрока. Это очень просто. Вы только должны вызвать authenticateWithCompletionHandler. По желанию вы можете сделать так, чтобы определённый блок кода выполнялся сразу после авторизации пользователя.

Но есть одна уловка. Есть еще один способ авторизации. Для этого следует переключиться на приложение Game Center, залогиниться оттуда и вернуться обратно в ваше приложение.

Ваше приложение должно знать, когда авторизационный статус меняется. Об этом можно узнать, зарегистрировавшись для получения “authentication changed” уведомления. Итак, чтобы авторизовать игрока нужно:

  • Создать синглтон-объект (прим. пер.: синглтон — это шаблон проектирования, гарантирующий, что класс имеет только один экземпляр, и обеспечивающий глобальный доступ к этому экземпляру), чтобы держать код Game Center'а в одном месте;
  • Когда синглтон-объект создастся, он зарегистрируется для получения “authentication changed” уведомления;
  • Игра вызовет метод синглтон-объекта, чтобы авторизовать игрока;
  • Когда игрок авторизуется или выходит, будет выполнен обратный вызов функции “authentication changed”;
  • Функция обратного вызова будет отслеживать, авторизован ли игрок в настоящее время.

Авторизация локального игрока: реализация

В проекте Cat Race создайте новый файл, выберите iOS\Cocoa Touch\Objective-C class и нажмите Next. Введите в поле Subclass NSObject, нажмите Next и назовите новый класс как GCHelper, кликните Finish. Далее замените содержимое GCHelper.h на следующее:

#import <Foundation/Foundation.h>
#import <GameKit/GameKit.h>

@interface GCHelper : NSObject {
  BOOL gameCenterAvailable;
  BOOL userAuthenticated;
}

@property (assign, readonly) BOOL gameCenterAvailable;

+ (GCHelper *)sharedInstance;
- (void)authenticateLocalUser;

@end

Этот код импортирует заголовочный файл GameKit и создает объект с двумя булевыми переменными. Одна из них необходима для слежения за состоянием доступности Game Center'а, а вторая для определения, когда игрок авторизован.

Также создаются: свойство, чтобы игра смогла сообщить, что Game Center доступен; статичный метод, чтобы поддерживать единственность экземпляра объекта и еще один метод, для авторизации локального пользователя (который будет вызываться при запуске приложения).

Далее перейдите в GCHelper.m и добавьте следующий код внутри @implementation:

@synthesize gameCenterAvailable;

#pragma mark Initialization

static GCHelper *sharedHelper = nil;
+ (GCHelper *) sharedInstance {
  if (!sharedHelper) {
      sharedHelper = [[GCHelper alloc] init];
  }
  return sharedHelper;
}

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

Далее добавьте следующий метод сразу после метода sharedInstance:

- (BOOL)isGameCenterAvailable {
    // check for presence of GKLocalPlayer API
    Class gcClass = (NSClassFromString(@"GKLocalPlayer"));
 
    // check if the device is running iOS 4.1 or later
    NSString *reqSysVer = @"4.1";
    NSString *currSysVer = [[UIDevice currentDevice] systemVersion];
    BOOL osVersionSupported = ([currSysVer compare:reqSysVer 
        options:NSNumericSearch] != NSOrderedAscending);
 
    return (gcClass && osVersionSupported);
}

Этот метод был взят прямо из гайда Apple по использованию Game Kit. Этот метод проверяет доступность Game Kit на конкретном устройстве. Благодаря этому, приложение сможет запускаться на iOS 4.0 или ранее (только без мультиплеера).

Далее добавьте следующий код сразу после предыдущего метода:

- (id)init {
    if ((self = [super init])) {
        gameCenterAvailable = [self isGameCenterAvailable];
        if (gameCenterAvailable) {
            NSNotificationCenter *nc = 
            [NSNotificationCenter defaultCenter];
            [nc addObserver:self 
                   selector:@selector(authenticationChanged) 
                       name:GKPlayerAuthenticationDidChangeNotificationName 
                     object:nil];
        }
    }
    return self;
}
 
- (void)authenticationChanged {    
 
    if ([GKLocalPlayer localPlayer].isAuthenticated && !userAuthenticated) {
       NSLog(@"Authentication changed: player authenticated.");
       userAuthenticated = TRUE;           
    } else if (![GKLocalPlayer localPlayer].isAuthenticated && userAuthenticated) {
       NSLog(@"Authentication changed: player not authenticated");
       userAuthenticated = FALSE;
    }
}

Метод init проверяет доступность Game Center'а, и в случае положительного результата регистрируется для уведомления “authentication changed”. Важно, что приложение регистрируется для этого уведомления перед попыткой авторизовать пользователя, таким образом оно срабатывает после завершения авторизации.

Функция обратного вызова authenticationChanged очень проста: она проверяет, авторизован ли был пользователь во время смены состояния, и меняет флаг соответствующим образом.

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

Наконец, добавим метод для авторизации локального игрока сразу после метода authenticationChanged:

#pragma mark User functions
 
- (void)authenticateLocalUser { 
 
    if (!gameCenterAvailable) return;
 
    NSLog(@"Authenticating local user...");
    if ([GKLocalPlayer localPlayer].authenticated == NO) {     
        [[GKLocalPlayer localPlayer] authenticateWithCompletionHandler:nil];        
    } else {
        NSLog(@"Already authenticated!");
    }
}

Здесь вызывается метод authenticateWithCompletionHandler, который будет упомянут позднее. Он служит для того, чтобы сообщить Game Kit'у о необходимости авторизовать игрока. Замечу, что он не передает completion handler. С тех пор, как вы зарегистрировались для получения “authentication changed” уведомления, это необязательно.

Окей, теперь GCHelper содержит весь код, необходимый для авторизации игрока, поэтому теперь осталось только использовать его! Переключитесь на AppDelegate.m и сделайте следующие изменения:

// В самом начале файла
#import "GCHelper.h"
 
// В конце applicationDidFinishLaunching, сразу перед
// последней строкой, которая вызывает runWithScene:
[[GCHelper sharedInstance] authenticateLocalUser];

Это создаст единственный экземпляр (который зарегистрируется для обратного вызова “authentication changed” в процессе инициализации), затем вызовет метод authenticateLocalUser.

Почти готово! Последний шаг — добавить фреймворк Game Kit в проект. Чтобы это сделать, выберите проект Cat Race слева сверху вкладки Groups & Files, перейдите на кладку Build Phases, разверните “Link Binary with Libraries” и нажмите на кнопку "+". Выберите GameKit.framework и кликните Add. Смените тип с Required на Optional, и ваш экран должен выглядеть похоже на следующее:

image

Вы сделали это! Соберите и запустите проект, и если вы выполнили вход в Game Center, то вы должны увидеть что-то подобное:

image

Теперь, когда вы авторизовали игрока, вы можете начинать развлекаться, например, найти кого-нибудь, с кем можно поиграть!

Game Center, Game Center, сделай мне матч

Есть два способа, чтобы найти кого-нибудь, с кем поиграть, через Game Center. Это поиск матча программно или использование встроенного интерфейса создания матчей.

В этом уроке вы изучите встроенный интерфейс создания матчей. Идея заключается в том, что когда вы хотите найти матч, вы передаёте некоторые параметры в объект GKMatchRequest, затем создаёте и отображаете экземпляр GKMatchmakerViewController'а.

Давайте посмотрим, как это работает. Сначала внесём некоторые правки в CGHelper.h:

// Добавьте в начало файла
@protocol GCHelperDelegate 
- (void)matchStarted;
- (void)matchEnded;
- (void)match:(GKMatch *)match didReceiveData:(NSData *)data 
    fromPlayer:(NSString *)playerID;
@end
 
// Измените строчку @interface для поддержки протоколов таким образом
@interface GCHelper : NSObject <GKMatchmakerViewControllerDelegate, GKMatchDelegate> {
 
// Добавьте внутрь @interface
UIViewController *presentingViewController;
GKMatch *match;
BOOL matchStarted;
id <GCHelperDelegate> delegate;
 
// Добавьте после @interface
@property (retain) UIViewController *presentingViewController;
@property (retain) GKMatch *match;
@property (assign) id <GCHelperDelegate> delegate;
 
- (void)findMatchWithMinPlayers:(int)minPlayers maxPlayers:(int)maxPlayers 
    viewController:(UIViewController *)viewController 
    delegate:(id<GCHelperDelegate>)theDelegate;

Давайте быстро пройдёмся по этому коду.

  • Вы определяете протокол GCHelperDelegate, который используется, чтобы уведомить другой объект о важных событиях, таких как: начало матча, окончание или получение данных. В этой игре ваш Cocos2D слой будет реализовывать этот протокол;
  • Объект GCHelper реализует два протокола. Первый служит для того, чтобы интерфейс создания матчей мог уведомить этот объект, когда матч найден или нет. А второй — чтобы Game Center мог уведомить этот объект, когда получены данные или изменился статус соединения;
  • Создаются новые переменные и свойства экземпляра, чтобы следить за view controller'ом, который будет отображать интерфейс создания матчей. А также ссылка на матч, переменная, которая позволяет узнать, начался ли матч или нет, и делегат;
  • Создается новый метод, чтобы Cocos2D слой мог найти кого-нибудь, с кем можно поиграть.

Теперь переключитесь на GCHelper.m и внесите следующие изменения:

// В начале файла
@synthesize presentingViewController;
@synthesize match;
@synthesize delegate;
 
// Добавьте новый метод, сразу после authenticateLocalUser
- (void)findMatchWithMinPlayers:(int)minPlayers maxPlayers:(int)maxPlayers   
    viewController:(UIViewController *)viewController 
    delegate:(id<GCHelperDelegate>)theDelegate {
 
    if (!gameCenterAvailable) return;
 
    matchStarted = NO;
    self.match = nil;
    self.presentingViewController = viewController;
    delegate = theDelegate;               
    [presentingViewController dismissModalViewControllerAnimated:NO];
 
    GKMatchRequest *request = [[[GKMatchRequest alloc] init] autorelease]; 
    request.minPlayers = minPlayers;     
    request.maxPlayers = maxPlayers;
 
    GKMatchmakerViewController *mmvc = 
        [[[GKMatchmakerViewController alloc] initWithMatchRequest:request] autorelease];    
    mmvc.matchmakerDelegate = self;
 
    [presentingViewController presentModalViewController:mmvc animated:YES];
}

Этот метод, который будет вызываться слоем Cocos2D для поиска матча. Если же Game Center недоступен, то этот метод ничего не делает.

Он инициализирует матч как еще не начавшийся и объект матча как nil. Он сохраняет снаружи view controller и делегат для дальнейшего использования и отменяет любой view controller, существовавший до него (в случае, если GKMatchmakerViewController в данный момент отображается).

Затем идет важная часть. Объект GKMatchRequest позволяет задать тип матча, который вы ищете, такой как минимальное и максимальное количество игроков. Этот метод устанавливает их такими, какими они были переданы в него (для этой игры минимум — 2 игрока, максимум — тоже 2 игрока).

Далее он создает новый экземпляр GKMatchmakerViewController'а с полученным запросом (прим. пер.: экземпляром GKMatchRequest), устанавливает делегатом объект GCHelper и выводит переданный view controller на экран.

С этого момента управление передаётся GKMatchmakerViewController'у, который разрешает пользователю найти случайного игрока и начать игру. После этого некоторые функции обратного вызова будут вызваны, поэтому давайте добавим их:

#pragma mark GKMatchmakerViewControllerDelegate
 
// Игрок отклонил создание матча
- (void)matchmakerViewControllerWasCancelled:(GKMatchmakerViewController *)viewController {
    [presentingViewController dismissModalViewControllerAnimated:YES];
}
 
// Создание матча вылетело с ошибкой
- (void)matchmakerViewController:(GKMatchmakerViewController *)viewController didFailWithError:(NSError *)error {
    [presentingViewController dismissModalViewControllerAnimated:YES];
    NSLog(@"Error finding match: %@", error.localizedDescription);    
}
 
// Матч найден, игра должна начаться
- (void)matchmakerViewController:(GKMatchmakerViewController *)viewController didFindMatch:(GKMatch *)theMatch {
    [presentingViewController dismissModalViewControllerAnimated:YES];
    self.match = theMatch;
    match.delegate = self;
    if (!matchStarted && match.expectedPlayerCount == 0) {
        NSLog(@"Ready to start match!");
    }
}

Если игрок отменит поиск матча, или возникла ошибка, то вид создания матча просто закроется.

А если матч найден, то он сохранит объект матча и установит GCHelper делегатом делегатом матча, чтобы он мог быть уведомлен о поступающих данных и изменении статуса соединения.

Также он проверяет, пришло ли время начинать матч. Объект матча хранит количество игроков, которое необходимо найти в переменной expectedPlayerCount. Если она равна 0, то все готовы начать. Теперь мы просто выведем это в лог, позднее мы сделаем здесь кое-что интересное.

Далее, добавьте реализацию функций обратного вызова GKMatchDelegate:

#pragma mark GKMatchDelegate
 
// Объект матча получил данные от игрока
- (void)match:(GKMatch *)theMatch didReceiveData:(NSData *)data fromPlayer:(NSString *)playerID {    
    if (match != theMatch) return;
 
    [delegate match:theMatch didReceiveData:data fromPlayer:playerID];
}
 
// Состояние игрока изменилось (например, присоединился, отсоединился)
- (void)match:(GKMatch *)theMatch player:(NSString *)playerID didChangeState:(GKPlayerConnectionState)state {   
    if (match != theMatch) return;
 
    switch (state) {
        case GKPlayerStateConnected: 
            // handle a new player connection.
            NSLog(@"Player connected!");
 
            if (!matchStarted && theMatch.expectedPlayerCount == 0) {
                NSLog(@"Ready to start match!");
            }
 
            break; 
        case GKPlayerStateDisconnected:
            // a player just disconnected. 
            NSLog(@"Player disconnected!");
            matchStarted = NO;
            [delegate matchEnded];
            break;
    }                     
}
 
// Объект матча не смог соединиться с игроком из-за ошибки
- (void)match:(GKMatch *)theMatch connectionWithPlayerFailed:(NSString *)playerID withError:(NSError *)error {
 
    if (match != theMatch) return;
 
    NSLog(@"Failed to connect to player with error: %@", error.localizedDescription);
    matchStarted = NO;
    [delegate matchEnded];
}
 
// Матч не смог начаться из-за ошибки
- (void)match:(GKMatch *)theMatch didFailWithError:(NSError *)error {
 
    if (match != theMatch) return;
 
    NSLog(@"Match failed with error: %@", error.localizedDescription);
    matchStarted = NO;
    [delegate matchEnded];
}

match:didReceiveData:fromPlayer вызывается, когда другой игрок отправляет вам данные. Этот метод просто перенаправляет данные в делегат (в этой игре им будет Cocos2D слой), таким образом, их может использовать движок игры.

match:player:didChangState используется при подсоединении игрока для проверки, все ли игроки подключены, чтобы можно было начать матч. В противном случае, если игрок отключился, то матч заканчивается и поступает уведомление в делегат.

Окей, теперь у нас есть код для создания матча, давайте используем его в нашем HelloWorldLayer'e. Переключитесь на HelloWorldLayer.h и внесите следующие изменения:

// Добавьте в начало файла
#import "GCHelper.h"
 
// Обозначим @interface как реализующий GCHelperDelegate
@interface HelloWorldLayer : CCLayer <GCHelperDelegate>

Теперь переключитесь на HelloWorldLayer.m и внесите такие поправки:

// Добавьте в начало файла
#import "AppDelegate.h"
#import "RootViewController.h"
 
// Добавьте в конец метода init, сразу после setGameState
AppDelegate * delegate = (AppDelegate *) [UIApplication sharedApplication].delegate;                
[[GCHelper sharedInstance] findMatchWithMinPlayers:2 maxPlayers:2 viewController:delegate.viewController delegate:self];
 
// Добавьте новые методы в конец файла
#pragma mark GCHelperDelegate
 
- (void)matchStarted {    
    CCLOG(@"Match started");        
}
 
- (void)matchEnded {    
    CCLOG(@"Match ended");    
}
 
- (void)match:(GKMatch *)match didReceiveData:(NSData *)data fromPlayer:(NSString *)playerID {
    CCLOG(@"Received data");
}

Самая важная часть здесь — это метод init. Он получает RootViewController из App Delegate, потому что этот view controller будет отображать view controller создания матча. Далее, вызывается метод для поиска матча, путем отображения view controller'а создания матча. Это метод, который вы только что написали в GCHelper'е.

Далее идут заготовки для функций начала или конца матчей, которые вы реализуете позднее.

Последняя вещь. По умолчанию шаблон Cocos2D не содержит свойство для RootViewController'а в App Delegate, поэтому вы должны его добавить. Переключитесь на AppDelegate.h и добавьте следующее:

@property (nonatomic, retain) RootViewController *viewController;

Синтезируйте его в AppDelegate.m:

@synthesize viewController;

Готово! Соберите и запустите приложение, и вы должны увидеть, как появляется view controller создания матча:

image

Теперь запустите приложение на другом устройстве, чтобы у вас было две запущенные копии одновременно (например, это могут быть симулятор и ваш iPhone).

Важно: убедитесь, что вы используете разные аккаунты Game Center'a на каждом устройстве, в противном случае ничего не выйдет.

Нажмите «Play Now» на обоих устройствах и, после некоторого времени, view controller создания матча исчезнет, и вы должны увидеть что-то подобное в логе:

CatRace[16440:207] Authentication changed: player authenticated.
CatRace[16440:207] Player connected!
CatRace[16440:207] Ready to start match!

Поздравляю! Вы создали матч между двумя устройствами! Теперь вы на пути создания сетевой игры!

Ландшафтная ориентация и GKMatchmakerViewController

Вы должно быть заметили, что по умолчанию GKMatchmakerViewController появляется в портнетной ориентации. Очевидно, что это очень раздражает, ведь эта Cocos2D игра в ландшафтной!

К счастью, вы можете исправить это, заставив GKMatchmakerViewController принимать только ландшафтные ориентации. Чтобы это сделать, создайте новый файл, выберите iOS\Cocoa Touch\Objective-C class и создайте подкласс NSObject'а под именем GKMatchmakerViewController-LandscapeOnly.

Замените содержимое GKMatchmakerViewController-LandscapeOnly.h на следующее:

#import <Foundation/Foundation.h>
#import <GameKit/GameKit.h>
 
@interface GKMatchmakerViewController(LandscapeOnly)
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation;
@end

А затем и содержимое GKMatchmakerViewController-LandscapeOnly.m:

#import "GKMatchmakerViewController-LandscapeOnly.h"
 
@implementation GKMatchmakerViewController (LandscapeOnly)
 
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation { 
    return ( UIInterfaceOrientationIsLandscape( interfaceOrientation ) );
}
 
@end

И всё! Соберите приложение и запустите его. Теперь view controller должен отобразиться в ландшафтном виде:

image

Что же дальше?

Вот пример проекта со всем кодом, который был разобран в этом уроке.

Во второй части мы изучим, как передавать данные между устройствами, и превратим игру в замечательную гонку кошка vs ребенок!
Теги:
Хабы:
+12
Комментарии 8
Комментарии Комментарии 8

Публикации

Истории

Работа

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн