في هذا المقال سنناقش موضوعين: (1) إنشاء اللعبة (2) الاستفادة من الأساس الذي أنشأناه في مقالات سابقة.
في المقال السابق وجدنا مشكلة في مشروعنا، وقد أخذت مني بعض الوقت لاكتشاف المشكلة، ولكن لا داعي للقلق فخلال الشرح سيتم توضيح سبب المشكلة والأماكن التي كانت تؤثر عليها، ومع أني أستطيع تعديل المقال السابق والتخلص من المشكلة،
إلا أني أفضل أن أريك كيف تجد وتحل المشكلة وخاصة لأنها ستساعدك على فهم عمل مكتبة CocoaAsyncSocket. لنبدأ الآن بالعمل.
المبدأ سهل، لدينا لوحة أو شبكة تحتوي 7 أعمدة كل عمود يحتوي 6 خلايا. المستخدم يستطيع امساك عمود ليضع فيه قرص. في كل مرة يضيف المستخدم قرص لعمود، نقوم باستدعاء method لفحص إذا تم فوز اللاعب، ويتمثل الفوز بوضع أربع أقراص في صف واحد، ممكن أن يكون الصف عمودي، أفقي أو قطري.
وهذا يعني أنه يجب تتبع قيم بعض المتغيرات، ولعمل هذا أنشأنا مصفوفة المصفوفات، لتقابل خلايا الشبكة، كل مصفوفة مصفوفات تعبر عن عمود، عندما يضيف اللاعب قرص لعمود، نقوم بتحديث ال data structure وفحص إذا فاز اللاعب أم لا.
أنا لدي خبرة كبيرة في تطوير الألعاب. والطريقة التي سنبني فيها التطبيق في مشروعنا ليست الوحيدة، وممكن القول أنها ليست الأفضل أيضاً، باستخدام أنماط Objective-C ولصقها لل classes الأساسية، معظمكم سيكون قادراً على فهم الموضوع بيسر.
بينما كنت أبحث في لعبة الأربعة في سطر واحد وجدت Stack Overflow answer والذي يوضح خوارزمية اللعبة باستخدام bitboards. هذا حل صعب ولكنه سريع إذا كنت جاداً بخصوص board games مثل لعبة tic-tac-toe أو لعبة الشطرنج chess ولذلك أنصح بقراءة المزيد حول bitboards.
وكما قلت سنستخدم مصفوفة المصفوفات كبنية أساسية لللعبة. ال board ستكون بسيطة تتكون من 24 خلية، كل خلية يمثلها موقع واحد في بنية البيانات التي سنستخدمها وهي مصفوفة المصفوفات. ولأننا نحتاج لحفظ إشارة على كل خلية في مصفوفة المصفوفات سنستخدم مصفوفة مصفوفات أخرى لتخزين reference على الخلايا المقابلة لها، مما سيساعد على تحديث عرض لوحة اللعب board.
لنبدأ بإنشاء عرض اللوحة. افتح MTViewController.xib, وأضف UIViewinstance لعرض المتحكم وأعطه الأبعاد 280 points by 240 points كما في الشكل(1)
عدل على قيود العرض بحيث أن لوحة اللعب يكون لها طول وعرض محددين. وكذلك عرض اللوحة يجب أن يكون في وسط متحكم اللوحة طولياً وعرضياً. ال . Autolayout تجعل هذا سهلاً.
الخطوة الثانية: إضافة زر إعادة اللعب
الخطوة الثالثة: إضافة تسمية الحالة
2- عرض اللوحة
الخطوة الأولى: إنشاء class خلية اللوحة
الخطوة الثانية: إعداد اللعبة
3- إضافة التفاعلالخطوة الأولى: إضافة Gesture Recognizer
4- تحسين التفاعل
الخطوة الأولى: تحديد التفاعل
الخطوة الثانية: إرسال التحديثات
5- الفوز باللعب
الخطوة الأولى: تحديث state label
الخطوة الثانية: تفعيل زر ال replay
بفضل م أسسنا له في المقالة السابقة كان بناء ال method بسيط وقصير، ولجعل كل هذا يعمل سنفتح ال MTPacketclass وتحديث ال MTPacketType
آخر جزئية من العمل هي بناء controllerDidStartNewGame: delegate method
في MTViewController class، نقوم باستدعاء resetGame كما فعلنا في replay action، ونحدث خاصية حالة اللعبة gameState.
الملخص
مع أننا لدينا الآن لعبة، أعتقد أنك توافقني الرأي أنها ما زالت لبعض التحسين. فالتصميم يعتبر بدائي جداً وبعض الحركات ستكون جيدة أيضاً. ومع ذلك هدف هذا المشروع قد تحقق، وهو بناء لعبة متعددة اللاعبين باستخدام مكتبة Bonjour و CocoaAsyncSocket. الآن أنت يجب أن تكون فهمت أساسيات هذه المكتبة ومعرفة ماذا يمكن أن تستفيد منها.
في المقال السابق وجدنا مشكلة في مشروعنا، وقد أخذت مني بعض الوقت لاكتشاف المشكلة، ولكن لا داعي للقلق فخلال الشرح سيتم توضيح سبب المشكلة والأماكن التي كانت تؤثر عليها، ومع أني أستطيع تعديل المقال السابق والتخلص من المشكلة،
إلا أني أفضل أن أريك كيف تجد وتحل المشكلة وخاصة لأنها ستساعدك على فهم عمل مكتبة CocoaAsyncSocket. لنبدأ الآن بالعمل.
1- تحديث واجهة المستخدم
سأبدأ هذا المقال بالتحدث عن اللعبة، أربعة في سطر واحد، إذا لم تكن سمعت عن (أربعة في سطر واحد) من قبل، أقترح عليك زيارة Wikipedia، وبالمناسبة (أربعة في سطر واحد) Four in a Row معروفة بعدة أسماء مثل Connect Four، Find Four، و Plot Four.المبدأ سهل، لدينا لوحة أو شبكة تحتوي 7 أعمدة كل عمود يحتوي 6 خلايا. المستخدم يستطيع امساك عمود ليضع فيه قرص. في كل مرة يضيف المستخدم قرص لعمود، نقوم باستدعاء method لفحص إذا تم فوز اللاعب، ويتمثل الفوز بوضع أربع أقراص في صف واحد، ممكن أن يكون الصف عمودي، أفقي أو قطري.
وهذا يعني أنه يجب تتبع قيم بعض المتغيرات، ولعمل هذا أنشأنا مصفوفة المصفوفات، لتقابل خلايا الشبكة، كل مصفوفة مصفوفات تعبر عن عمود، عندما يضيف اللاعب قرص لعمود، نقوم بتحديث ال data structure وفحص إذا فاز اللاعب أم لا.
أنا لدي خبرة كبيرة في تطوير الألعاب. والطريقة التي سنبني فيها التطبيق في مشروعنا ليست الوحيدة، وممكن القول أنها ليست الأفضل أيضاً، باستخدام أنماط Objective-C ولصقها لل classes الأساسية، معظمكم سيكون قادراً على فهم الموضوع بيسر.
بينما كنت أبحث في لعبة الأربعة في سطر واحد وجدت Stack Overflow answer والذي يوضح خوارزمية اللعبة باستخدام bitboards. هذا حل صعب ولكنه سريع إذا كنت جاداً بخصوص board games مثل لعبة tic-tac-toe أو لعبة الشطرنج chess ولذلك أنصح بقراءة المزيد حول bitboards.
وكما قلت سنستخدم مصفوفة المصفوفات كبنية أساسية لللعبة. ال board ستكون بسيطة تتكون من 24 خلية، كل خلية يمثلها موقع واحد في بنية البيانات التي سنستخدمها وهي مصفوفة المصفوفات. ولأننا نحتاج لحفظ إشارة على كل خلية في مصفوفة المصفوفات سنستخدم مصفوفة مصفوفات أخرى لتخزين reference على الخلايا المقابلة لها، مما سيساعد على تحديث عرض لوحة اللعب board.
الخطوة الأولى: إضافة عرض اللوحة
لنبدأ بإنشاء عرض اللوحة. افتح MTViewController.xib, وأضف UIViewinstance لعرض المتحكم وأعطه الأبعاد 280 points by 240 points كما في الشكل(1)
عدل على قيود العرض بحيث أن لوحة اللعب يكون لها طول وعرض محددين. وكذلك عرض اللوحة يجب أن يكون في وسط متحكم اللوحة طولياً وعرضياً. ال . Autolayout تجعل هذا سهلاً.
الشكل(1): إضافة لوحة اللعب
أنشئ ملف MTViewController.h لعرض اللوحة وسميه
boardVie
. في ال Interface Builder قم بإيصال ال outlet مع لوحة اللعب. وسنضيف أجزاء لوحة اللعب برمجياً.#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
الخطوة الثانية: إضافة زر إعادة اللعب
عندما تنتهي اللعبة، نريد إعطاء اللاعب فرصة بدء لعبة جديدة، قم بإضافة زر جديد لمتحكم عرض لوحة اللعب وسميه Replay كمل في الشكل(2). أنشئ outlet replayButton للزر في
MTViewController.h وأنشئ action اسمه replay للزر وقم بإيصال ال outlet و action مع الزر. كما في الشكل(2)#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- (IBAction)replay:(id)sender {}
الخطوة الثالثة: إضافة تسمية الحالة
يجب إخبار اللاعب بحالة اللعبة، والدور على من، ومن فاز باللعب، لذلك قمنا بإضافة ال label لهذا الغرض ويتم تحديثها حسب حالة اللعب. قم بفتح MTViewController.xib وأضف (
UILabel
) label كما في الشكل(3). أنشئ outlet لل label في ملف ال header وأسميه gameStateLabel
, وقم يإيصاله مع ال label.(كما في الشكل 3)
الشكل(3) إضافة عرض حالة اللعب
#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
الخطوة الأولى: إنشاء class خلية اللوحة
كما ذكرت مسبقاً، شكل اللوحة يحتوي 24 جزء أو خلية. سننشئ
UIView
subclass لجعل كل خلية أسهل في التعامل. أنشئ UIView
subclass وأسميه MTBoardCell
كما في الشكل(4). الكلاس MTBoardCell
class فيه خاصية واحدة cellType
من نوع MTBoardCellType
, والمعرفة في بداية ملف ال header.
الشكل(4): إنشاء الكلاس الخاص بالخلية
#import <UIKit/UIKit.h>
typedef enum { MTBoardCellTypeEmpty = -1, MTBoardCellTypeMine,
MTBoardCellTypeYours
} MTBoardCellType;
@interface MTBoardCell : UIView@property (assign, nonatomic) MTBoardCellType cellType;@end
في ال initializer نضع قيمة
cellType
هي MTBoardCellTypeEmpty
لوسم اللوحة بالفارغة. في ملف ال implementation نقوم بعمل override لل setter الخاص ب cellType
.. وهي method مساعدة لتغيير لون الخلفية .#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) ? [UIColor yellowColor] : (self.cellType == MTBoardCellTypeYours) ? [UIColor redColor] : [UIColor whiteColor];}@end
الخطوة الثانية: إعداد اللعبة
لإعداد لعبة جديدة، نستدعي method المتحكم الرئيسي
resetGame
وسنستدعيها في أماكن عديدة في مشروعنا، أحدها في viewDidLoad
method. ولأني أفضل أن تكون viewDidLoad
method موجزة، أقوم بنقل الكود الخاص بإعداد العرض إلى setupView
helper method، والتي يتم استدعائها في viewDidLoad
. ، في ال setupView
, نقوم أيضاً بإخفاء كل أجزاء ال view مع وجود استثناء لل host وزر ال join.- (void)viewDidLoad { [super viewDidLoad]; // Setup View [self setupView];}- (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
) كما ونقوم بعمل استيراد import لملف ال header MTBoardCell
وتعريف اثنين من الثوابت kMTMatrixWidth
و kMTMatrixHeight
، والتي سيتم تخزين أبعاد اللوحة فيها.#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
لن يكون معقداً. لأنها ستستدعى عندا ينقر اللاعب على زر replay، يبدأ البناء بإخفاء زر ال replay. نقوم بحساب حجم خلية اللوحة، وإنشاء مصفوفة لكل عمود. مصفوفة المصفوفات هذه يتم تخزينها في خاصية board
في الكلاس، خاصية matrix
مشابهة تماماً. الفرق الرئيسي (1) الأعمدة لا تحتوي على أي كائنات عند عمل reset للعبة (2) كل عمود عبارة عن نموذج من NSMutableArra
..- (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- إضافة التفاعلالخطوة الأولى: إضافة Gesture Recognizer
إضافة التفاعل إلى اللعبة بسيط ببساطة إضافة gesture recognizer tap للوحة في
setupView
method في متحكم العرض. في كل مرة ينقر اللاعب لوحة اللعب يتم إرسال رسالة addDiscToColumn:
ل MTViewController
instance.- (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
). ولكني أعني الخاصية التي تحفظ اللاعب الذي عليه الدو وإذا كان اللاعب كسب اللعب أم لا. لجعل الأمور أوضح سنعرف data type جديد لحالة اللعبة لأننا سنستخدم هذا النوع عدة مرات في مشروعنا، ومن الأفضل تعريفه في ملف منفصل MTConstants.h, ونقوم بعمل import له في ملف ال header.
أنشئ
NSObject
subclass جديد وأسميه MTConstants
كما في الشكل(5)، وقم بحذف ملف (MTConstants.m), وامسح محتويات الملف MTConstants.h، ونقوم بتعريف MTGameState
كما ترى في الأسفل:
الشكل(5): إنشاء الملف MTConstants.h
typedef enum {
MTGameStateUnknown = -1, MTGameStateMyTurn, MTGameStateYourTurn, MTGameStateIWin, MTGameStateYouWin} MTGameState;
أضف جملة ال import في ملف ال header وبالتالي تكون محتويات الملف MTConstants.h متاحة في مشروعنا.
#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
,، فإنه الوقت لتعريف خاصية gameState
في امتداد كلاس MTViewController
الذي أنشأناه سابقاً، و خاصية gameState
هي من نوع MTGameState
.#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:
method. الكود الموضح بالأسفل ليس كاملاً كما ترى في التعليقات في الكود. وسنقوم بإتمامه لاحقاً. العنصر الرئيسي الذي نود التركيز عليه هنا هو تسلسل ال method. نحن نبدأ بفحص إذا ما فاز أحد اللاعبين فعلياً باللعبة، إذا كان الأمر كذلك فلا حاجة لإضافة المزيد من الأقراص للوحة اللعب، ثم نقوم بفحص إذا كان اللاعب يستطيع إضافة قرص للوحة أي أنه دوره في اللعب وعندها نقوم بتنبيه اللاعب أنه دوره عن طريق عرض تنبيه.
الجزء الممتع في
addDiscToColumn:
هو أن تكون اللعبة لم تنتهي بعد، نقوم بفحص أي جزء من اللوحة نقره المستخدم باستدعاء columnForPoint:
وتمرير الأبعاد التي نقرها اللاعب على اللوحة. المتغير column
يتم تمريره كوسيط إلى addDiscToColumn:withType:
.. والوسيط parameter الاخر لهذه ال method هو نوع الخلية وهي MTBoardCellTypeMine
في هذه الحالة.- (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:
method ليست إلا حساب لاستنتاج العمود بناء على الإحداثيات للنقطة point
..- (NSInteger)columnForPoint:(CGPoint)point { return floorf(point.x / floorf(self.boardView.frame.size.width / kMTMatrixWidth));}
في
addDiscToColumn:withType:
، نقوم بتحديث حالة اللعبة بتحديث خاصية متحكم العرض matrix
. ثم نقوم بجلب ال reference على خلية لوحة اللعب، والمخزنة في خاصة board
في متحكم العرض وإعطاء الخلية نوع cellType
.. ولأننا عملنا override ل setCellType:
method في MTBoardCell
,، سيتم تحديث لون خلفية الخلية سيتغير أوتوماتيكياً.- (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:
method و endGame
method. في هذه ال methods نقوم بتحديث متحكم عرض اللعبة بناء على حالة اللعبة. شغل نموذجين من اللعبة وقم بفحص اللعبة في حالتها الحالية.- (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];}- (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];}
حتى الآن لا يوجد حد أقصى لعدد الأقراص التي يمكن للاعب وضعها على اللوحة والأحداث عند اللاعب A لا تظهر عند اللاعب B والعكس صحيح. لنصحح هذا.
الخطوة الأولى: تحديد التفاعل
لتحديد التفاعل، نحتاج لتحديث خاصية متحكم العرض
gameState
في الوقت المناسب. التفاعل مع اللوحة فعلياً محدود بالقيمة gameState
في addDiscToColumn:
,، ولكن هذا ليس مفيداً إذا لم نقم بتحديث خاصية gameState
.
بداية يجب أن نعرف لمن الدور في بداية اللعبة. يمكننا عمل ذلك ببراعة مثلاً عن طريق تخمين وجه العملة بعد رميها، ولكن دعنا ننجزها بطريقة بسيطة وهي أن يكون الدور في البداية على اللاعب المستضيف، وهذا سهل للغاية. ببساطة نقوم بتحديث خاصية
gameState
في controller:didHostGameOnSocket:
وcontroller:didJoinGameOnSocket:
(ال helper methods). النتيجة أن اللاعب مستضيف اللعبة فقط هو من يستطيع البدء بوضع قرص على اللوحة.- (void)controller:(MTHostGameViewController *)controller didHostGameOnSocket:(GCDAsyncSocket *)socket { NSLog(@"%s", __PRETTY_FUNCTION__); // Update Game State [self setGameState:MTGameStateMyTurn]; // Start Game with Socket [self startGameWithSocket:socket];}- (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
، مما يعني أن اللاعب لا يمكنه إضافة المزيد من الأقراص مالم يتم تحديث حالة اللعب. قبل أن نكمل افحص اللعبة الآن لترى نتيجة التعديلات الأخيرة.- (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 }}
الخطوة الثانية: إرسال التحديثات
مع أننا نقوم بإنشاء اتصال عند بدء لعبة جديدة، إلا أننا لم تفعل شيء بهذا الاتصال. الكلاس الذي له علاقة بالاتصال هو
MTGameController
,، الذي أنشأناه في المقال السابق.
افتح الملف MTGameController.h وعرف instance method وأسميها
addDiscToColumn:
.. متحم العرض سيستدعي هذه الmethod لإعلام متحكم اللعب بأن اللاعب الآخر يجب إعلامه بتحديث حالة اللعب وهنا من الجيد أن نعرج على بروتوكول MTGameControllerDelegate
. عندا يستلم متحكم اللعب تحديث، يحتاج لإعلام مندوبه المتحكم الرئيسي للعرض main view controller عن التحديث ليعدل على عرض لوحة اللعب. انظر إلى header file المعدل لكلاس MTGameController
.#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:
method سهلة للغاية في البناء وذلك يرجع إلى العمل الذي قمنا به في المقالات السابقة. قمت بتحديث ملف ال header لكلاس MTPacket
بإضافة MTPacketTypeDidAddDisc
إلى تعداد ال types. ومع أننا قمنا بتعريف خاصية action
في كلاس MTPacket
إلا أننا لن نحتاجها في هذا المشروع.- (void)addDiscToColumn:(NSInteger)column { // Send Packet NSDictionary *load = @{ @"column" : @(column) }; MTPacket *packet = [[MTPacket alloc] initWithData:load type:MTPacketTypeDidAddDisc action:0]; [self sendPacket:packet];}typedef enum { MTPacketTypeUnknown = -1, MTPacketTypeDidAddDisc} MTPacketType;
ال
parseBody:
method تحتاج أيضاً للتحديث، في بنائها الحالي كل ما نفعله هو تخزين بيانات ال packet إلى ال console. في البناء المعدل نقوم بفحص نوع ال packet وإعلام المندوب أن الطرف الآخر أضاف قرص إلى عمود إذا كان نوع ال packet يساوي MTPacketTypeDidAddDisc
..- (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]]; } }}
قم ببناء delegate method جديدة من بروتوكول
MTGameControllerDelegate
في كلاس MTViewController
كما ترى في الأسفل. قمنا باستدعاء invokeaddDiscToColumn:withType:
ومررنا العمود ونوع الخلية (MTBoardCellTypeYours
).. خاصية متحكم العرض gameState
أيضاً يتم تحديثها للتأكد أن اللاعب يستطيع إضافة قرص جديد للوح اللعب.- (void)controller:(MTGameController *)controller didAddDiscToColumn:(NSInteger)column { // Update Game [self addDiscToColumn:column withType:MTBoardCellTypeYours];
// Update State [self setGameState:MTGameStateMyTurn];}
وأخيراً سنستدعي
addDiscToColumn:
method من كلاس MTGameController
في متحكم العرض addDiscToColumn:
method..- (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
. في thesocket:didConnectToHost:port:
method في بروتوكول GCDAsyncSocketDelegate
، راقبنا مندوب كلاس MTJoinGameViewController
وأرسلناه مرجع أو إشارة إلى ال socket. توقفنا عن استعراض المزيد من الخدمات الجديدة وتجاهلنا متحكم عرض اللعبة.
بتجاهل ارتباط متحكم عرض اللعبة game view controller، تخلصنا تلقائياً من view controller للعبة لأننا لن نحتاجه مجدداً. وهذا يعني أن
dealloc
method تستدعي عندما يتم تحرير ال object. البناء الحالي لل dealloc
method كما ترى بالأسفل.- (void)dealloc { if (_delegate) { _delegate = nil; }
if (_socket) { [_socket setDelegate:nil delegateQueue:NULL]; _socket = nil; }}
في dealloc method من كلاس MTJoinGameViewController، نقوم بتنظيف كل شيء. ومع ذلك لأن هذه ال socket يتم إدارتهابواسطة ال game controller، يجب أن نعطي المندوب القيمة
nil
ولن نحتاج أبدا لإعطاء ال delegate queue القيمة NULL
متحكم اللعب يتم بدؤه قبل استدعاء
dealloc
method مما يعني أن مندوب game controller's socket is (re) يأخذ القيمة nil
عندما يتم تحرير the join game view controller ومع ذلك متحكم اللعب لديه إشارة على ال socket، مندوب ال socket يأخذ القيمة nil وهذا ما يجعلها غير قابلة للاستخدام بالنسبة لنا. الحل بسيط بإزالة آخر أسطر من dealloc
method والتي فيها يتم وسم مندوب ال socket بالقيمة nil، و socket's delegate queue القيمة NULL. شغل التطبيق أكثر من مرة لترى إذا قمنا فعلاً بحل هذه المشكلة.- (void)dealloc { if (_delegate) { _delegate = nil; }}
في هذه الحالة، ليس من الممكن الفوز باللعب لأننا لم نبن الخوارزمية الخاصة بفحص إذا ما قام أحد اللاعبين بالفعل بوضع 4 أقراص في سطر واحد. قمت بإنشاء
hasPlayerOfTypeWon:
method لهذا الغرض، وهي تأخذ وسيط من نوع MTPlayerType
وتفحص اللوحة إذا ما كان اللاعب من النوع الممرر لل method فاز باللعب. نوع البيانات MTPlayerType
معرف في ملف
MTConstants.h
.. ومع ذلك ممكن أن نمرر 0 للاعب Aو 1 للاعب B، الكود أصبح أسهل للقراءة والتعديل بتعريف نوع مخصص custom type.typedef enum { MTPlayerTypeMe = 0, MTPlayerTypeYou} MTPlayerType;
و
hasPlayerOfTypeWon:
تقوم بإرجاع قيمة من نوع Boolean. لن أقوم بمناقشة بنائها بالتفصيل لأنها طويلة قليلاً وليس بتلك الصعوبة لتفهمها. يكمن جوهرها أننا نفحص كل حالات الفوز الممكنة. فهي تبحث عن ارع اقراص في سطر واحد سواء افقي أو عمودي أو قطري. بالطبع هذه ليست أفضل الطرق ولكنها الأكثر فهماً والأسهل للتعلم. في نهاية hasPlayerOfTypeWon:
method نحتاج لتحديث خاصية متحكم العرض gameState
إذا كان ذلك مناسباً.- (BOOL)hasPlayerOfTypeWon:(MTPlayerType)playerType { BOOL _hasWon = NO; NSInteger _counter = 0; MTBoardCellType targetType = playerType == MTPlayerTypeMe ? MTBoardCellTypeMine : MTBoardCellTypeYours;
// Check Vertical Matches for (NSArray *line in self.board) { _counter = 0;
for (MTBoardCell *cell in line) { _counter = (cell.cellType == targetType) ? _counter + 1 : 0; _hasWon = (_counter > 3) ? YES : _hasWon;
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) ? _counter + 1 : 0; _hasWon = (_counter > 3) ? YES : _hasWon;
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) ? _counter + 1 : 0; _hasWon = (_counter > 3) ? YES : _hasWon;
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) ? _counter + 1 : 0; _hasWon = (_counter > 3) ? YES : _hasWon;
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) ? _counter + 1 : 0; _hasWon = (_counter > 3) ? YES : _hasWon;
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) ? _counter + 1 : 0; _hasWon = (_counter > 3) ? YES : _hasWon;
if (_hasWon) break; }
if (_hasWon) break; } }
// Update Game State if (_hasWon) { self.gameState = (playerType == MTPlayerTypeMe) ? MTGameStateIWin : MTGameStateYouWin; }
return _hasWon;}
ال
hasPlayerOfTypeWon:
method يتم استدعاؤها في مكانين في كلاس MTViewController
.
المكان الأول في addDiscToColumn:
method بعد أن يقوم اللاعب بإضافة قرص للوحة، نقوم بفحص إذا فاز اللاعب باللعبة بتمرير MTPlayerMe
كوسيط ل
hasPlayerOfTypeWon:
:
- (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
method في بداية addDiscToColumn:
method إذا نقر اللاعب لوحة اللعب عندما تكون اللعبة قد انتهت فعلياً.
ال
hasPlayerOfTypeWon:
method يتم استدعائها أيضاً في controller:didAddDiscToColumn:
method في بروتوكول MTGameControllerDelegate
. ألق نظرة على البناء المعدل المعروض في الأسفل. إذا فاز اللاعب المقابل باللعبة، نستدعي أيضا showWinner
method.- (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
method نقوم بتحديث العرض بواسطة عرض زر replay وإظهار تنبيه للاعب بفوزه باللعبة.- (void)showWinner { if (self.gameState < MTGameStateIWin) return;6- ملء الفراغات
// 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];}
هناك وظيفتين أود إضافتهم قبل الانتهاء من المشروع. (1) تحديث ال state label بتغير حالة اللعبة و(2) تفيل زر ال replay. وكلاهما سهلة البناء.
الخطوة الأولى: تحديث state label
لتحديث state label، نحتاج لتعديل العرض بمجرد تغير خاصية
gameState
. بإمكاننا استخدام KVO (Key Value Observing) لعمل ذلك، ولكني أفضل عمل override لخاصية setter of the gameState
.بتغير قيمة _gameState
، يتم استدعاء updateView
, helper method..- (void)setGameState:(MTGameState)gameState { if (_gameState != gameState) { _gameState = gameState; // Update View [self updateView]; }}
ال
updateView
method هي مثل setupView
في updateView
,، حيث نقوم بتعديل خاصية ال text في gameStateLabel
..- (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; } }}
الخطوة الثانية: تفعيل زر ال replay
لتفعيل زر ال replay، يجب البدء ببناء replay: action. والذي يتم استدعاؤه عندما يقوم اللاعب بالنقر على زر ال replay عند انتهاء اللعبة. وفي replay action نقوم بثلاث خطوات، (1) استدعاء resetGame لعمل reset game، (2) تحديث حالة اللعبة ل MTGameStateMyTurn,، وإرسال ال game controller رسالة من نوع startNewGame. وهذا يعني أن اللاعب الذي يبدأ اللعب يستطيع عمل الحركة الأولى.
- (IBAction)replay:(id)sender { // Reset Game [self resetGame]; // Update Game State self.gameState = MTGameStateMyTurn; // Notify Opponent of New Game [self.gameController startNewGame];الآن نحتاج لبناء startNewGame method في كلاس MTGameController وعمل extend من بروتوكول MTGameControllerDelegate . افتح ملف ال header لكلاس MTGameController وعرف startNewGame method وال method المساعدة الجديدة ل MTGameControllerDelegate protocol.
}
#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
بفضل م أسسنا له في المقالة السابقة كان بناء ال method بسيط وقصير، ولجعل كل هذا يعمل سنفتح ال MTPacketclass وتحديث ال MTPacketType
- (void)startNewGame { // Send Packet NSDictionary *load = nil; MTPacket *packet = [[MTPacket alloc] initWithData:load type:MTPacketTypeStartNewGame action:0]; [self sendPacket:packet];}في parseBody: method من كلاس MTGameController نقوم بإرسال رسالة controllerDidStartNewGame: للمندوب إذا كان نوع ال packet يساوي toMTPacketTypeStartNewGame.
typedef enum { MTPacketTypeUnknown = -1, MTPacketTypeDidAddDisc, MTPacketTypeStartNewGame} MTPacketType;
- (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]; }}
في MTViewController class، نقوم باستدعاء resetGame كما فعلنا في replay action، ونحدث خاصية حالة اللعبة gameState.
- (void)controllerDidStartNewGame:(MTGameController *)controller { // Reset Game [self resetGame];شغل نموذجين من التطبيق والعب اللعبة مع أصدقائك لترىإذا كان كل شيء يعمل كما يجب.
// Update Game State self.gameState = MTGameStateYourTurn;}
الملخص
مع أننا لدينا الآن لعبة، أعتقد أنك توافقني الرأي أنها ما زالت لبعض التحسين. فالتصميم يعتبر بدائي جداً وبعض الحركات ستكون جيدة أيضاً. ومع ذلك هدف هذا المشروع قد تحقق، وهو بناء لعبة متعددة اللاعبين باستخدام مكتبة Bonjour و CocoaAsyncSocket. الآن أنت يجب أن تكون فهمت أساسيات هذه المكتبة ومعرفة ماذا يمكن أن تستفيد منها.
ليست هناك تعليقات: