В предыдущих статьях мы преимущественно фокусировались на сетевом аспекте игры. В этой последней части пришло время увеличить саму игру. Мы реализуем игру и используем основы, заложенные в предыдущих статьях, для создания многопользовательской игры.
Вступление
В этой статье мы обсудим две темы: (1) создание игры и (2) использование основы, которую мы создали в предыдущих статьях. В предыдущей статье в наш проект попала ошибка. Я должен признать, что мне потребовалось довольно много времени, чтобы обнаружить это мерзкое, маленькое существо. Не беспокойся, хотя. Мы исправим эту ошибку, и я покажу вам, где именно она вызывает хаос. Несмотря на то, что я мог бы обновить предыдущую статью, чтобы избавиться от ошибки, я предпочитаю показать вам, как найти и исправить ошибку, так как она поможет понять, как работает библиотека CocoaAsyncSocket. У нас впереди немало работы, так что давайте начнем.
1. Обновление пользовательского интерфейса
Позвольте мне начать эту статью с краткого разговора об игре «Четыре в ряд». Если вы не слышали о «Четыре в ряд», тогда я предлагаю вам посетить Википедию . Кстати, «Четыре в ряд» известен под многими именами, такими как « Подключить четыре» , «Найти четыре» и «Сюжет четыре». Концепция проста. У нас есть доска или сетка с семью колонками, каждая из которых содержит шесть ячеек. Пользователь может нажать на столбец, чтобы добавить диск в этот столбец. Каждый раз, когда игрок добавляет диск в колонку, мы вызываем метод, чтобы проверить, выиграл ли игрок в игру, то есть четыре диска подряд. Ряды могут быть горизонтальными, вертикальными или диагональными.
Это подразумевает, что нам нужно отслеживать довольно много переменных. Чтобы отслеживать состояние игры, мы создаем структуру данных, массив массивов, зеркальное отображение доски или сетку ячеек. Каждый массив в массиве представляет столбец. Всякий раз, когда игрок добавляет диск в колонку, мы обновляем структуру данных, которая поддерживает игру, и проверяем, выиграл ли игрок в игре.
Я не эксперт в разработке игр, и подход, который мы используем в этом проекте, не является единственным решением для реализации Four in a Row. Возможно, это не самая эффективная реализация. Однако, используя хорошо известные паттерны Objective-C и придерживаясь базовых классов, большинство из вас смогут без проблем идти в ногу.
Исследуя «Четыре в ряд», я наткнулся на ответ «Переполнение стека», в котором описывается алгоритм для четырех в ряд с использованием битбордов. Это очень эффективное и быстрое решение, поэтому, если вы серьезно относитесь к настольным играм, таким как крестики-нолики или шахматы, то я рекомендую изучить битборды более подробно.
Как я уже сказал, мы будем использовать массив массивов в качестве структуры данных игры. Сама доска будет простым представлением с 42 подпредставлениями или ячейками доски. Каждое подпредставление или ячейка доски соответствует позиции в структуре данных. Поскольку нам нужен простой способ сохранить ссылку на каждую ячейку платы, мы управляем второй структурой данных, другим массивом массивов, чтобы хранить ссылку на каждую ячейку платы. Это позволяет легко обновлять представление доски, но также имеет некоторые другие преимущества, которые станут очевидными чуть позже в этом уроке.
Шаг 1: Добавление представления доски
Давайте начнем с создания представления доски. Откройте MTViewController.xib , добавьте экземпляр UIView
в представление контроллера представления и установите его размеры 280 точек на 240 точек (рисунок 1). Измените ограничения вида таким образом, чтобы вид доски имел фиксированную ширину и высоту. Вид платы также должен быть горизонтально и вертикально центрирован в виде контроллера вида. Autolayout делает это бризом.
Создайте выход в MTViewController.h для представления доски и назовите его boardView
. В Интерфейсном Разработчике подключите розетку к представлению платы. Мы добавим подпредставления представления доски программно.
01
02
03
04
05
06
07
08
09
10
|
#import <UIKit/UIKit.h>
@interface MTViewController : UIViewController
@property (weak, nonatomic) IBOutlet UIView *boardView;
@property (weak, nonatomic) IBOutlet UIButton *hostButton;
@property (weak, nonatomic) IBOutlet UIButton *joinButton;
@property (weak, nonatomic) IBOutlet UIButton *disconnectButton;
@end
|
Шаг 2: Добавление кнопки воспроизведения
Когда игра заканчивается, мы хотим дать игроку возможность начать новую игру. Добавьте новую кнопку в представление контроллера представления и присвойте ей название Replay (рисунок 2). Создайте розетку replayButton
для кнопки в MTViewController.h и действие с именем replay:
в MTViewController.m . Подключите розетку и действие к кнопке воспроизведения в Интерфейсном Разработчике (рисунок 2).
01
02
03
04
05
06
07
08
09
10
11
|
#import <UIKit/UIKit.h>
@interface MTViewController : UIViewController
@property (weak, nonatomic) IBOutlet UIView *boardView;
@property (weak, nonatomic) IBOutlet UIButton *hostButton;
@property (weak, nonatomic) IBOutlet UIButton *joinButton;
@property (weak, nonatomic) IBOutlet UIButton *replayButton;
@property (weak, nonatomic) IBOutlet UIButton *disconnectButton;
@end
|
1
2
3
|
— (IBAction)replay:(id)sender {
}
|
Шаг 3: Добавление метки состояния
Игрок игры должен быть проинформирован о состоянии игры. Чья очередь? Кто выиграл игру? Мы добавляем метку в представление контроллера представления и обновляем ее всякий раз, когда изменяется состояние игры. Повторно посетите MTViewController.xib и добавьте метку ( UILabel
) в представление контроллера представления (рисунок 3). Создайте выход для метки в заголовочном файле контроллера представления, назовите его gameStateLabel
и подключите его к метке в Интерфейсном Разработчике (рисунок 3).
01
02
03
04
05
06
07
08
09
10
11
12
|
#import <UIKit/UIKit.h>
@interface MTViewController : UIViewController
@property (weak, nonatomic) IBOutlet UIView *boardView;
@property (weak, nonatomic) IBOutlet UIButton *hostButton;
@property (weak, nonatomic) IBOutlet UIButton *joinButton;
@property (weak, nonatomic) IBOutlet UIButton *replayButton;
@property (weak, nonatomic) IBOutlet UIButton *disconnectButton;
@property (weak, nonatomic) IBOutlet UILabel *gameStateLabel;
@end
|
2. Раскладка доски
Шаг 1: Создание класса ячеек доски
Как я упоминал ранее, представление доски содержит 42 подпредставления или ячейки доски. Мы создадим подкласс UIView
чтобы каждая ячейка доски была немного умнее и проще в использовании. Создайте подкласс UIView
и назовите его MTBoardCell
(рисунок 4). Класс MTBoardCell
имеет одно свойство, cellType
типа MTBoardCellType
, которое объявлено в верхней части заголовочного файла.
01
02
03
04
05
06
07
08
09
10
11
12
13
|
#import <UIKit/UIKit.h>
typedef enum {
MTBoardCellTypeEmpty = -1,
MTBoardCellTypeMine,
MTBoardCellTypeYours
} MTBoardCellType;
@interface MTBoardCell : UIView
@property (assign, nonatomic) MTBoardCellType cellType;
@end
|
В указанном инициализаторе мы устанавливаем cellType
в MTBoardCellTypeEmpty
чтобы пометить ячейку доски как пустую. В файле реализации класса мы также переопределяем установщик cellType
. В setCellType:
мы обновляем представление, вызывая updateView
, вспомогательный метод, в котором мы обновляем цвет фона представления.
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
|
#import «MTBoardCell.h»
@implementation MTBoardCell
#pragma mark —
#pragma mark Initialization
— (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
// Cell Type
self.cellType = MTBoardCellTypeEmpty;
}
return self;
}
#pragma mark —
#pragma mark Setters & Getters
— (void)setCellType:(MTBoardCellType)cellType {
if (_cellType != cellType) {
_cellType = cellType;
// Update View
[self updateView];
}
}
#pragma mark —
#pragma mark Helper Methods
— (void)updateView {
// Background Color
self.backgroundColor = (self.cellType == MTBoardCellTypeMine) ?
}
@end
|
Шаг 2: Настройка игры
Чтобы настроить новую игру, мы resetGame
метод resetGame
контроллера основного вида. Мы будем вызывать resetGame
в разных местах нашего проекта. Одним из таких мест является метод viewDidLoad
контроллера представления. Поскольку я предпочитаю сохранять метод viewDidLoad
кратким, я обычно перемещаю логику настройки представления в отдельный вспомогательный метод setupView
который вызывается в viewDidLoad
. В setupView
мы также скрываем все setupView
представления, за исключением узла и кнопки соединения.
1
2
3
4
5
6
|
— (void)viewDidLoad {
[super viewDidLoad];
// Setup View
[self setupView];
}
|
01
02
03
04
05
06
07
08
09
10
|
— (void)setupView {
// Reset Game
[self resetGame];
// Configure Subviews
[self.boardView setHidden:YES];
[self.replayButton setHidden:YES];
[self.disconnectButton setHidden:YES];
[self.gameStateLabel setHidden:YES];
}
|
Прежде чем мы сможем реализовать resetGame
, нам нужно создать структуру данных, в которой хранится состояние игры, и структуру данных, в которой хранятся ссылки на ячейки доски представления доски. Добавьте расширение класса вверху MTViewController.h и создайте два свойства: board
( NSArray
) и matrix
( NSMutableArray
). Мы также импортируем заголовочный файл MTBoardCell
и определяем константы kMTMatrixWidth
и kMTMatrixHeight
, которые хранят размеры платы.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
#import «MTViewController.h»
#import «MTBoardCell.h»
#import «MTGameController.h»
#import «MTHostGameViewController.h»
#import «MTJoinGameViewController.h»
#define kMTMatrixWidth 7
#define kMTMatrixHeight 6
@interface MTViewController () <MTGameControllerDelegate, MTHostGameViewControllerDelegate, MTJoinGameViewControllerDelegate>
@property (strong, nonatomic) MTGameController *gameController;
@property (strong, nonatomic) NSArray *board;
@property (strong, nonatomic) NSMutableArray *matrix;
@end
|
Реализация resetGame
не является ракетостроением, как вы можете видеть ниже. Поскольку resetGame
также будет вызываться, когда игрок нажимает кнопку воспроизведения, реализация начинается со скрытия кнопки воспроизведения. Мы вычисляем размер ячейки доски, создаем изменяемый массив для каждого столбца доски и добавляем шесть ячеек доски к каждому столбцу. Этот массив массивов хранится в свойстве класса класса как неизменяемый массив. Свойство matrix
класса очень похоже. Он также хранит массив массивов. Основные отличия заключаются в том, что (1) столбцы не содержат объектов при перезагрузке игры и (2) каждый столбец является экземпляром NSMutableArray
.
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)resetGame {
// Hide Replay Button
[self.replayButton setHidden:YES];
// Helpers
CGSize size = self.boardView.frame.size;
CGFloat cellWidth = floorf(size.width / kMTMatrixWidth);
CGFloat cellHeight = floorf(size.height / kMTMatrixHeight);
NSMutableArray *buffer = [[NSMutableArray alloc] initWithCapacity:kMTMatrixWidth];
for (int i = 0; i < kMTMatrixWidth; i++) {
NSMutableArray *column = [[NSMutableArray alloc] initWithCapacity:kMTMatrixHeight];
for (int j = 0; j < kMTMatrixHeight; j++) {
CGRect frame = CGRectMake(i * cellWidth, (size.height — ((j + 1) * cellHeight)), cellWidth, cellHeight);
MTBoardCell *cell = [[MTBoardCell alloc] initWithFrame:frame];
[cell setAutoresizingMask:(UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight)];
[self.boardView addSubview:cell];
[column addObject:cell];
}
[buffer addObject:column];
}
// Initialize Board
self.board = [[NSArray alloc] initWithArray:buffer];
// Initialize Matrix
self.matrix = [[NSMutableArray alloc] initWithCapacity:kMTMatrixWidth];
for (int i = 0; i < kMTMatrixWidth; i++) {
NSMutableArray *column = [[NSMutableArray alloc] initWithCapacity:kMTMatrixHeight];
[self.matrix addObject:column];
}
}
|
3. Добавление взаимодействия
Шаг 1. Добавление распознавателя жестов
Добавить взаимодействие с игрой так же просто, как добавить распознаватель жестов касания к представлению на плате в методе setupView
контроллера setupView
. Каждый раз, когда игрок addDiscToColumn:
на представление addDiscToColumn:
сообщение addDiscToColumn:
отправляется нашему экземпляру MTViewController
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
— (void)setupView {
// Reset Game
[self resetGame];
// Configure Subviews
[self.boardView setHidden:YES];
[self.replayButton setHidden:YES];
[self.disconnectButton setHidden:YES];
[self.gameStateLabel setHidden:YES];
// Add Tap Gesture Recognizer
UITapGestureRecognizer *tgr = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(addDiscToColumn:)];
[self.boardView addGestureRecognizer:tgr];
}
|
Прежде чем мы реализуем addDiscToColumn:
нам нужно сделать обход и поговорить о состоянии игры. Класс MTViewController
должен отслеживать состояние игры. По состоянию игры я не имею в виду структуры данных ( board
и matrix
), которые мы создали ранее. Я просто имею в виду свойство, которое отслеживает, чей это ход и выиграл ли игрок в игре. Чтобы упростить задачу, рекомендуется объявить пользовательский тип для состояния игры. Поскольку мы будем использовать этот пользовательский тип в различных местах нашего проекта, лучше всего объявить его в отдельном файле MTConstants.h и добавить оператор импорта для MTConstants.h в предварительно скомпилированный заголовочный файл проекта.
Создайте новый подкласс MTConstants
именем MTConstants
(рисунок 5), удалите файл реализации ( MTConstants.m ) и очистите содержимое файла MTConstants.h . В MTConstants.h мы определяем MTGameState
как показано ниже.
1
2
3
4
5
6
7
|
typedef enum {
MTGameStateUnknown = -1,
MTGameStateMyTurn,
MTGameStateYourTurn,
MTGameStateIWin,
MTGameStateYouWin
} MTGameState;
|
Добавьте оператор импорта для MTConstants.h в предварительно скомпилированный заголовочный файл проекта, чтобы его содержимое было доступно по всему проекту.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
#import <Availability.h>
#ifndef __IPHONE_4_0
#warning «This project uses features only available in iOS SDK 4.0 and later.»
#endif
#ifdef __OBJC__
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
#import «GCDAsyncSocket.h»
#import «MTConstants.h»
#endif
|
В MTConstants.h
мы объявляем различные состояния игры. В более сложной игре это может быть не самой лучшей стратегией, или вам может потребоваться добавить дополнительные состояния. Для этого проекта такого подхода будет достаточно. Поскольку игра «Четыре в ряд» является пошаговой игрой, большая часть игры проводится в состояниях MTGameStateMyTurn
и MTGameStateYourTurn
, то есть это ваш ход или ход вашего противника, чтобы добавить диск на доску. Последние два состояния используются, когда игра заканчивается, когда один из игроков становится победителем игры.
С MTGameState
определенным в MTConstants.h
, пришло время объявить свойство MTViewController
расширении класса MTViewController
которое мы создали ранее. Как вы уже догадались, свойство gameState
имеет тип MTGameState
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
|
#import «MTViewController.h»
#import «MTBoardCell.h»
#import «MTGameController.h»
#import «MTHostGameViewController.h»
#import «MTJoinGameViewController.h»
#define kMTMatrixWidth 7
#define kMTMatrixHeight 6
@interface MTViewController () <MTGameControllerDelegate, MTHostGameViewControllerDelegate, MTJoinGameViewControllerDelegate>
@property (assign, nonatomic) MTGameState gameState;
@property (strong, nonatomic) MTGameController *gameController;
@property (strong, nonatomic) NSArray *board;
@property (strong, nonatomic) NSMutableArray *matrix;
@end
|
Настало время реализовать метод addDiscToColumn:
. Реализация addDiscToColumn:
показанная ниже является неполной, как вы можете видеть по комментариям в ее реализации. Мы завершим его реализацию по ходу дела. Основным элементом, на котором нужно сосредоточиться в этой точке, является метод потока. Мы начнем с проверки, выиграл ли уже один из игроков. Если это так, то нет необходимости добавлять больше дисков на плату. Вторая проверка, которую мы делаем, состоит в том, может ли игрок добавить диск, то есть очередь игрока добавить диск на доску. Если это не так, то мы показываем предупреждение, информирующее игрока о том, что не их очередь.
Интересная часть addDiscToColumn:
что происходит, если игра еще не закончилась и игроку разрешено добавлять диск на доску. Мы вычисляем, какой столбец игрок columnForPoint:
вызывая columnForPoint:
и передаем местоположение в виде доски, которое игрок коснулся. Затем переменная column
передается в качестве аргумента addDiscToColumn:withType:
Вторым параметром этого метода является тип ячейки, в данном случае это MTBoardCellTypeMine
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
— (void)addDiscToColumn:(UITapGestureRecognizer *)tgr {
if (self.gameState >= MTGameStateIWin) {
// Notify Players
} else if (self.gameState != MTGameStateMyTurn) {
NSString *message = NSLocalizedString(@»It’s not your turn.», nil);
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@»Warning» message:message delegate:nil cancelButtonTitle:NSLocalizedString(@»OK», nil) otherButtonTitles:nil];
[alertView show];
} else {
NSInteger column = [self columnForPoint:[tgr locationInView:tgr.view]];
[self addDiscToColumn:column withType:MTBoardCellTypeMine];
// Update Game State
// Send Packet
// Notify Players if Someone Has Won the Game
}
}
|
columnForPoint:
метод — не более чем простое вычисление, позволяющее вывести столбец на основе координат point
.
1
2
3
|
— (NSInteger)columnForPoint:(CGPoint)point {
return floorf(point.x / floorf(self.boardView.frame.size.width / kMTMatrixWidth));
}
|
В addDiscToColumn:withType:
мы обновляем состояние игры, обновляя свойство matrix
контроллера вида. Затем мы извлекаем ссылку на соответствующую ячейку платы, сохраненную в свойстве board
контроллера представления, и устанавливаем для ее типа ячейки значение cellType
. Поскольку мы setCellType:
метод setCellType:
в MTBoardCell
, цвет фона ячейки платы будет обновлен автоматически.
1
2
3
4
5
6
7
8
9
|
— (void)addDiscToColumn:(NSInteger)column withType:(MTBoardCellType)cellType {
// Update Matrix
NSMutableArray *columnArray = [self.matrix objectAtIndex:column];
[columnArray addObject:@(cellType)];
// Update Cells
MTBoardCell *cell = [[self.board objectAtIndex:column] objectAtIndex:([columnArray count] — 1)];
[cell setCellType:cellType];
}
|
Перед тестированием игры нам нужно изменить startGameWithSocket:
и endGame
. В этих методах мы обновляем представление контроллера представления в зависимости от состояния игры. Запустите два экземпляра приложения и проверьте игру в ее текущем состоянии.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
— (void)startGameWithSocket:(GCDAsyncSocket *)socket {
// Initialize Game Controller
self.gameController = [[MTGameController alloc] initWithSocket:socket];
// Configure Game Controller
[self.gameController setDelegate:self];
// Hide/Show Buttons
[self.boardView setHidden:NO];
[self.hostButton setHidden:YES];
[self.joinButton setHidden:YES];
[self.disconnectButton setHidden:NO];
[self.gameStateLabel setHidden:NO];
}
|
01
02
03
04
05
06
07
08
09
10
11
12
|
— (void)endGame {
// Clean Up
[self.gameController setDelegate:nil];
[self setGameController:nil];
// Hide/Show Buttons
[self.boardView setHidden:YES];
[self.hostButton setHidden:NO];
[self.joinButton setHidden:NO];
[self.disconnectButton setHidden:YES];
[self.gameStateLabel setHidden:YES];
}
|
4. Улучшение взаимодействия
На данный момент нет ограничений на количество дисков, которые игрок может добавить, и действия игрока A не видны игроку B, и наоборот. Давайте это исправим.
Шаг 1: Ограничение взаимодействия
Чтобы ограничить взаимодействие, нам нужно обновить свойство gameState
контроллера представления в соответствующее время. Взаимодействие с доской уже ограничено значением gameState
в addDiscToColumn:
но это не очень полезно, если мы не обновляем свойство gameState
.
Прежде всего, нам нужно решить, чья очередь, когда начинается новая игра. Мы могли бы сделать что-то причудливое, например, бросить монету, но давайте будем простыми и позволим игроку, принимающему игру, сделать первый ход. Это достаточно просто. Мы просто обновляем свойство gameState
в controller:didHostGameOnSocket:
и controller:didJoinGameOnSocket:
делегировать методы. В результате только игрок, принимающий игру, может добавить диск на доску.
1
2
3
4
5
6
7
8
9
|
— (void)controller:(MTHostGameViewController *)controller didHostGameOnSocket:(GCDAsyncSocket *)socket {
NSLog(@»%s», __PRETTY_FUNCTION__);
// Update Game State
[self setGameState:MTGameStateMyTurn];
// Start Game with Socket
[self startGameWithSocket:socket];
}
|
1
2
3
4
5
6
7
8
9
|
— (void)controller:(MTJoinGameViewController *)controller didJoinGameOnSocket:(GCDAsyncSocket *)socket {
NSLog(@»%s», __PRETTY_FUNCTION__);
// Update Game State
[self setGameState:MTGameStateYourTurn];
// Start Game with Socket
[self startGameWithSocket:socket];
}
|
Второе изменение, которое нам нужно сделать, это обновить состояние игры всякий раз, когда игрок делает правильный ход. Мы делаем это в addDiscToColumn:
как показано ниже. Каждый раз, когда игрок добавляет диск на доску, состояние игры устанавливается на MTGameStateYourTurn
, что означает, что игрок не может добавлять больше дисков на доску, пока состояние игры не обновлено. Прежде чем продолжить, протестируйте приложение еще раз, чтобы увидеть результат наших изменений.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
— (void)addDiscToColumn:(UITapGestureRecognizer *)tgr {
if (self.gameState >= MTGameStateIWin) {
// Notify Players
} else if (self.gameState != MTGameStateMyTurn) {
NSString *message = NSLocalizedString(@»It’s not your turn.», nil);
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@»Warning» message:message delegate:nil cancelButtonTitle:NSLocalizedString(@»OK», nil) otherButtonTitles:nil];
[alertView show];
} else {
NSInteger column = [self columnForPoint:[tgr locationInView:tgr.view]];
[self addDiscToColumn:column withType:MTBoardCellTypeMine];
// Update Game State
[self setGameState:MTGameStateYourTurn];
// Send Packet
// Notify Players if Someone Has Won the Game
}
}
|
Шаг 2: Отправка обновлений
Несмотря на то, что мы устанавливаем связь при запуске новой игры, мы пока мало что сделали с этой связью. Класс, отвечающий за соединение, — MTGameController
, который мы создали в предыдущей статье. Откройте MTGameController.h и объявите метод экземпляра с именем addDiscToColumn:
Контроллер вида вызовет этот метод, чтобы сообщить игровому контроллеру, что другой игрок должен быть обновлен об измененном игровом состоянии. Это также хороший момент для расширения протокола MTGameControllerDelegate
. Когда игровой контроллер получает обновление, он должен уведомить своего делегата, контроллер основного вида, об обновлении, потому что контроллер основного вида отвечает за обновление вида платы. Взгляните на обновленный заголовочный файл класса MTGameController
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
#import <Foundation/Foundation.h>
@class GCDAsyncSocket;
@protocol MTGameControllerDelegate;
@interface MTGameController : NSObject
@property (weak, nonatomic) id<MTGameControllerDelegate> delegate;
#pragma mark —
#pragma mark Initialization
— (id)initWithSocket:(GCDAsyncSocket *)socket;
#pragma mark —
#pragma mark Public Instance Methods
— (void)addDiscToColumn:(NSInteger)column;
@end
@protocol MTGameControllerDelegate <NSObject>
— (void)controller:(MTGameController *)controller didAddDiscToColumn:(NSInteger)column;
— (void)controllerDidDisconnect:(MTGameController *)controller;
@end
|
Метод addDiscToColumn:
очень легко реализовать благодаря основам, которые мы делали в предыдущих статьях. Я обновил заголовочный файл класса MTPacket
, добавив MTPacketTypeDidAddDisc
к перечислению типов пакетов. Даже если мы объявили свойство action
в классе MTPacket
, оно нам не понадобится в этом проекте.
1
2
3
4
5
6
|
— (void)addDiscToColumn:(NSInteger)column {
// Send Packet
NSDictionary *load = @{ @»column» : @(column) };
MTPacket *packet = [[MTPacket alloc] initWithData:load type:MTPacketTypeDidAddDisc action:0];
[self sendPacket:packet];
}
|
1
2
3
4
|
typedef enum {
MTPacketTypeUnknown = -1,
MTPacketTypeDidAddDisc
} MTPacketType;
|
parseBody:
метод также должен быть обновлен. В его текущей реализации все, что мы делаем, это регистрируем данные пакета на консоли. В обновленной реализации мы проверяем тип пакета и уведомляем делегата о том, что оппонент добавил диск в столбец, если тип пакета равен MTPacketTypeDidAddDisc
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
— (void)parseBody:(NSData *)data {
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
MTPacket *packet = [unarchiver decodeObjectForKey:@»packet»];
[unarchiver finishDecoding];
/*
NSLog(@»Packet Data > %@», packet.data);
NSLog(@»Packet Type > %i», packet.type);
NSLog(@»Packet Action > %i», packet.action);
*/
if ([packet type] == MTPacketTypeDidAddDisc) {
NSNumber *column = [(NSDictionary *)[packet data] objectForKey:@»column»];
if (column) {
// Notify Delegate
[self.delegate controller:self didAddDiscToColumn:[column integerValue]];
}
}
}
|
MTGameControllerDelegate
новый метод MTGameControllerDelegate
протокола MTViewController
классе MTViewController
как показано ниже. Мы вызываем addDiscToColumn:withType:
и передаем столбец и тип ячейки ( MTBoardCellTypeYours
). Свойство gameState
контроллера представления также обновляется, чтобы игрок мог добавить новый диск на доску.
1
2
3
4
5
6
7
|
— (void)controller:(MTGameController *)controller didAddDiscToColumn:(NSInteger)column {
// Update Game
[self addDiscToColumn:column withType:MTBoardCellTypeYours];
// Update State
[self setGameState:MTGameStateMyTurn];
}
|
И последнее, но не менее важное: нам нужно вызвать метод addDiscToColumn:
метод класса MTGameController
в addDiscToColumn:
метод контроллера addDiscToColumn:
. Это последний кусок головоломки.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
|
— (void)addDiscToColumn:(UITapGestureRecognizer *)tgr {
if (self.gameState >= MTGameStateIWin) {
// Notify Players
} else if (self.gameState != MTGameStateMyTurn) {
NSString *message = NSLocalizedString(@»It’s not your turn.», nil);
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@»Warning» message:message delegate:nil cancelButtonTitle:NSLocalizedString(@»OK», nil) otherButtonTitles:nil];
[alertView show];
} else {
NSInteger column = [self columnForPoint:[tgr locationInView:tgr.view]];
[self addDiscToColumn:column withType:MTBoardCellTypeMine];
// Update Game State
[self setGameState:MTGameStateYourTurn];
// Send Packet
[self.gameController addDiscToColumn:column];
// Notify Players if Someone Has Won the Game
}
}
|
Запустите два экземпляра приложения и протестируйте игру еще раз. Вы столкнулись с проблемой? Настало время устранить эту ошибку, о которой я говорил вам ранее в этой статье. Ошибка находится в классе MTJoinGameViewController
. В socket:didConnectToHost:port:
метод протокола GCDAsyncSocketDelegate
мы уведомляем делегата класса MTJoinGameViewController
и передаем ему ссылку на сокет. Мы прекращаем просмотр новых сервисов и закрываем контроллер вида присоединения к игре.
Отвергнув контроллер представления присоединения к игре, мы неявно избавились от контроллера представления присоединения к игре, поскольку он больше не нужен. Это означает, что метод dealloc
класса вызывается при освобождении объекта. Текущая реализация метода dealloc
показана ниже.
01
02
03
04
05
06
07
08
09
10
|
— (void)dealloc {
if (_delegate) {
_delegate = nil;
}
if (_socket) {
[_socket setDelegate:nil delegateQueue:NULL];
_socket = nil;
}
}
|
В методе dealloc
класса MTJoinGameViewController
мы все MTJoinGameViewController
. Однако, поскольку этот сокет управляется игровым контроллером, мы не должны устанавливать делегат nil
а также мы не должны устанавливать очередь делегатов равной NULL
. Игровой контроллер создается перед dealloc
метода dealloc
, что означает, что делегат сокета игрового контроллера (повторно) устанавливается nil
когда освобождается контроллер игрового представления присоединения. Другими словами, даже если игровой контроллер имеет ссылку на сокет, для делегата сокета устанавливается значение nil
и это делает сокет непригодным для нас. Решение так же просто, как удаление нескольких последних строк метода dealloc
в котором мы устанавливаем делегат сокета равным nil
а очередь делегатов сокета — NULL
. Запустите приложение еще раз, чтобы увидеть, успешно ли мы исправили эту неприятную ошибку.
1
2
3
4
5
|
— (void)dealloc {
if (_delegate) {
_delegate = nil;
}
}
|
5. Победа в игре
В его текущем состоянии невозможно выиграть игру, потому что мы не реализовали алгоритм, который проверяет, есть ли у одного из игроков четыре его собственных диска подряд. Я создал hasPlayerOfTypeWon:
метод для этой цели. Он принимает один аргумент типа MTPlayerType
и проверяет доску, выиграл ли игрок прошедшего типа. Тип MTPlayerType
определен в MTConstants.h
. Даже при том, что мы могли бы передать 0
для игрока A и 1
для игрока B, наш код становится намного более читабельным (и поддерживаемым), объявив пользовательский тип.
1
2
3
4
|
typedef enum {
MTPlayerTypeMe = 0,
MTPlayerTypeYou
} MTPlayerType;
|
Как и следовало ожидать, hasPlayerOfTypeWon:
возвращает логическое значение. Я не буду обсуждать его реализацию подробно, потому что это довольно долго и не так сложно. Суть в том, что мы проверяем все возможные выигрышные комбинации. Он ищет горизонтальные, вертикальные и диагональные совпадения. Это, конечно, не лучший способ проверки совпадений, но я уверен, что этот метод понятен большинству из вас без особых затруднений. В конце метода hasPlayerOfTypeWon:
мы также обновляем свойство gameState
контроллера представления, если это необходимо.
001
002
003
004
005
006
007
008
009
010
011
012
013
014
+015
016
+017
018
019
020
021
022
023
024
025
026
027
028
029
+030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
+055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
|
— (BOOL)hasPlayerOfTypeWon:(MTPlayerType)playerType {
BOOL _hasWon = NO;
NSInteger _counter = 0;
MTBoardCellType targetType = playerType == MTPlayerTypeMe ?
// Check Vertical Matches
for (NSArray *line in self.board) {
_counter = 0;
for (MTBoardCell *cell in line) {
_counter = (cell.cellType == targetType) ?
_hasWon = (_counter > 3) ?
if (_hasWon) break;
}
if (_hasWon) break;
}
if (!_hasWon) {
// Check Horizontal Matches
for (int i = 0; i < kMTMatrixHeight; i++) {
_counter = 0;
for (int j = 0; j < kMTMatrixWidth; j++) {
MTBoardCell *cell = [(NSArray *)[self.board objectAtIndex:j] objectAtIndex:i];
_counter = (cell.cellType == targetType) ?
_hasWon = (_counter > 3) ?
if (_hasWon) break;
}
if (_hasWon) break;
}
}
if (!_hasWon) {
// Check Diagonal Matches — First Pass
for (int i = 0; i < kMTMatrixWidth; i++) {
_counter = 0;
// Forward
for (int j = i, row = 0; j < kMTMatrixWidth && row < kMTMatrixHeight; j++, row++) {
MTBoardCell *cell = [(NSArray *)[self.board objectAtIndex:j] objectAtIndex:row];
_counter = (cell.cellType == targetType) ?
_hasWon = (_counter > 3) ?
if (_hasWon) break;
}
if (_hasWon) break;
_counter = 0;
// Backward
for (int j = i, row = 0; j >= 0 && row < kMTMatrixHeight; j—, row++) {
MTBoardCell *cell = [(NSArray *)[self.board objectAtIndex:j] objectAtIndex:row];
_counter = (cell.cellType == targetType) ?
_hasWon = (_counter > 3) ?
if (_hasWon) break;
}
if (_hasWon) break;
}
}
if (!_hasWon) {
// Check Diagonal Matches — Second Pass
for (int i = 0; i < kMTMatrixWidth; i++) {
_counter = 0;
// Forward
for (int j = i, row = (kMTMatrixHeight — 1); j < kMTMatrixWidth && row >= 0; j++, row—) {
MTBoardCell *cell = [(NSArray *)[self.board objectAtIndex:j] objectAtIndex:row];
_counter = (cell.cellType == targetType) ?
_hasWon = (_counter > 3) ?
if (_hasWon) break;
}
if (_hasWon) break;
_counter = 0;
// Backward
for (int j = i, row = (kMTMatrixHeight — 1); j >= 0 && row >= 0; j—, row—) {
MTBoardCell *cell = [(NSArray *)[self.board objectAtIndex:j] objectAtIndex:row];
_counter = (cell.cellType == targetType) ?
_hasWon = (_counter > 3) ?
if (_hasWon) break;
}
if (_hasWon) break;
}
}
// Update Game State
if (_hasWon) {
self.gameState = (playerType == MTPlayerTypeMe) ?
}
return _hasWon;
}
|
hasPlayerOfTypeWon:
метод вызывается в двух местах в классе MTViewController
. Первое место в addDiscToColumn:
метод. После того, как игрок добавил диск на доску, мы проверяем, выиграл ли игрок в игру, передавая MTPlayerMe
в качестве аргумента hasPlayerOfTypeWon:
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
|
— (void)addDiscToColumn:(UITapGestureRecognizer *)tgr {
if (self.gameState >= MTGameStateIWin) {
// Notify Players
[self showWinner];
} else if (self.gameState != MTGameStateMyTurn) {
NSString *message = NSLocalizedString(@»It’s not your turn.», nil);
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@»Warning» message:message delegate:nil cancelButtonTitle:NSLocalizedString(@»OK», nil) otherButtonTitles:nil];
[alertView show];
} else {
NSInteger column = [self columnForPoint:[tgr locationInView:tgr.view]];
[self addDiscToColumn:column withType:MTBoardCellTypeMine];
// Update Game State
[self setGameState:MTGameStateYourTurn];
// Send Packet
[self.gameController addDiscToColumn:column];
// Notify Players if Someone Has Won the Game
if ([self hasPlayerOfTypeWon:MTPlayerTypeMe]) {
// Show Winner
[self showWinner];
}
}
}
|
Если игрок выиграл игру, мы вызываем showWinner
, который вскоре будет реализован. Обратите внимание, что мы также showWinner
метод showWinner
в начале метода addDiscToColumn:
если пользователь касается представления доски, когда игра уже закончилась.
hasPlayerOfTypeWon:
метод также вызывается в controller:didAddDiscToColumn:
метод протокола MTGameControllerDelegate
. Посмотрите на его обновленную реализацию ниже. Если соперник игрока выиграл игру, мы также showWinner
метод showWinner
.
01
02
03
04
05
06
07
08
09
10
11
12
13
|
— (void)controller:(MTGameController *)controller didAddDiscToColumn:(NSInteger)column {
// Update Game
[self addDiscToColumn:column withType:MTBoardCellTypeYours];
if ([self hasPlayerOfTypeWon:MTPlayerTypeYou]) {
// Show Winner
[self showWinner];
} else {
// Update State
[self setGameState:MTGameStateMyTurn];
}
}
|
В методе showWinner
мы обновляем представление, отображая кнопку воспроизведения и показывая представление предупреждений, которое сообщает игроку о победителе игры.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
|
— (void)showWinner {
if (self.gameState < MTGameStateIWin) return;
// Show Replay Button
[self.replayButton setHidden:NO];
NSString *message = nil;
if (self.gameState == MTGameStateIWin) {
message = NSLocalizedString(@»You have won the game.», nil);
} else if (self.gameState == MTGameStateYouWin) {
message = NSLocalizedString(@»Your opponent has won the game.», nil);
}
// Show Alert View
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@»We Have a Winner» message:message delegate:self cancelButtonTitle:NSLocalizedString(@»OK», nil) otherButtonTitles:nil];
[alertView show];
}
|
6. Заполнение пробелов
Есть два компонента функциональности, которые я хотел бы добавить перед завершением этого проекта: (1) обновление метки состояния игры при каждом изменении состояния игры и (2) включение кнопки воспроизведения. И то, и другое легко реализовать.
Шаг 1: Обновление метки состояния игры
Чтобы обновить метку состояния игры, нам нужно обновлять представление всякий раз, когда gameState
свойство gameState
. Для этого мы могли бы использовать KVO (Key Value Observing), но я предпочитаю просто переопределить установщик свойства gameState
. Всякий раз, когда значение _gameState
изменяется, мы вызываем updateView
, другой вспомогательный метод.
1
2
3
4
5
6
7
8
|
— (void)setGameState:(MTGameState)gameState {
if (_gameState != gameState) {
_gameState = gameState;
// Update View
[self updateView];
}
}
|
Метод updateView
, как и setupView
, является вспомогательным методом. В updateView
мы обновляем text
свойство gameStateLabel
.
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)updateView {
// Update Game State Label
switch (self.gameState) {
case MTGameStateMyTurn: {
self.gameStateLabel.text = NSLocalizedString(@»It is your turn.», nil);
break;
}
case MTGameStateYourTurn: {
self.gameStateLabel.text = NSLocalizedString(@»It is your opponent’s turn.», nil);
break;
}
case MTGameStateIWin: {
self.gameStateLabel.text = NSLocalizedString(@»You have won.», nil);
break;
}
case MTGameStateYouWin: {
self.gameStateLabel.text = NSLocalizedString(@»Your opponent has won.», nil);
break;
}
default: {
self.gameStateLabel.text = nil;
break;
}
}
}
|
Шаг 2: Включение кнопки воспроизведения
Чтобы включить кнопку воспроизведения, мы должны начать с реализации действия replay:
. Это действие вызывается, когда игрок нажимает кнопку воспроизведения, которая появляется, когда игра заканчивается. Мы делаем три вещи при replay:
(1) вызываем resetGame
для сброса игры, (2) обновляем состояние игры до MTGameStateMyTurn
и отправляем игровому контроллеру сообщение startNewGame
. Это означает, что игрок, инициирующий новую игру, может сделать первый ход.
01
02
03
04
05
06
07
08
09
10
|
— (IBAction)replay:(id)sender {
// Reset Game
[self resetGame];
// Update Game State
self.gameState = MTGameStateMyTurn;
// Notify Opponent of New Game
[ self .gameController startNewGame ]; }
|
Нам нужно реализовать startNewGame
метод в MTGameController
классе и расширить MTGameControllerDelegate
протокол. Откройте файл заголовка MTGameController
класса и объявите startNewGame
метод и новый метод делегата MTGameControllerDelegate
протокола.
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
|
#import <Foundation/Foundation.h>
@class GCDAsyncSocket ; @protocol MTGameControllerDelegate ; @interface MTGameController : NSObject @property ( weak , nonatomic ) id <MTGameControllerDelegate> delegate; #pragma mark —
#pragma mark Initialization - ( id )initWithSocket:( GCDAsyncSocket *)socket; #pragma mark —
#pragma mark Public Instance Methods - ( void )startNewGame; - ( void )addDiscToColumn:(NSInteger)column; @end
@protocol MTGameControllerDelegate <NSObject> - ( void )controller:( MTGameController *)controller didAddDiscToColumn :(NSInteger)column; - ( void )controllerDidStartNewGame:( MTGameController *)controller; - ( void )controllerDidDisconnect:( MTGameController *)controller; @end
|
Опять же, благодаря фундаменту, который мы заложили в предыдущей статье, startNewGame
метод является коротким и простым. Чтобы все это работало, нам нужно повторно посетить MTPacket
класс и обновить MTPacketType
перечисление.
1
2
3
4
5
6
|
- ( void )startNewGame { // Send Packet NSDictionary *load = nil ; MTPacket *packet = [[ MTPacket alloc ] initWithData :load type : MTPacketTypeStartNewGame action : 0 ]; [ self sendPacket :packet]; }
|
1
2
3
4
5
|
typedef enum { MTPacketTypeUnknown = - 1 , MTPacketTypeDidAddDisc , MTPacketTypeStartNewGame } MTPacketType ; |
В parseBody:
методе MTGameController
класса мы отправляем делегату сообщение о controllerDidStartNewGame:
том, что тип пакета равен MTPacketTypeStartNewGame
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
- ( void )parseBody:( NSData *)data { NSKeyedUnarchiver *unarchiver = [[ NSKeyedUnarchiver alloc ] initForReadingWithData :data]; MTPacket *packet = [unarchiver decodeObjectForKey : @"packet" ]; [unarchiver finishDecoding ]; /*
NSLog(@"Packet Data > %@", packet.data); NSLog(@"Packet Type > %i", packet.type); NSLog(@"Packet Action > %i", packet.action); */
if ([packet type ] == MTPacketTypeDidAddDisc) { NSNumber *column = [( NSDictionary *)[packet data ] objectForKey : @"column" ]; if (column) { // Notify Delegate
[ self .delegate controller : self didAddDiscToColumn :[column integerValue ]]; }
} else if ([packet type ] == MTPacketTypeStartNewGame) { // Notify Delegate
[ self .delegate controllerDidStartNewGame : self ]; }
}
|
Последнее, что нам нужно сделать, — реализовать controllerDidStartNewGame:
метод делегата в MTViewController
классе. Мы вызываем resetGame
, как мы это делали в replay:
действии, и обновляем gameState
свойство.
1
2
3
4
5
6
7
|
- ( void )controllerDidStartNewGame:( MTGameController *)controller { // Reset Game [ self resetGame ]; // Update Game State self .gameState = MTGameStateYourTurn ; }
|
Запустите два экземпляра приложения и поиграйте в игру с другом, чтобы увидеть, все ли работает как надо.
Вывод
Несмотря на то, что теперь у нас есть играбельная игра, я думаю, вы согласны с тем, что она все еще нуждается в доработке и доработке. Дизайн довольно простой, и несколько анимаций тоже подойдут. Однако цель этого проекта была достигнута, создав многопользовательскую игру с использованием Bonjour и библиотеки CocoaAsyncSocket. Теперь вы должны иметь общее представление о Bonjour и библиотеке CocoaAsyncSocket и знать, что каждый может сделать для вас.