مساحة اعلانية

آخر المواضيع

إنشاء لعبة بمنطق اللعب - Bonjour


المقدمة
في هذا المقال سنناقش موضوعين: (1) إنشاء اللعبة (2) الاستفادة من الأساس الذي أنشأناه في مقالات سابقة.
في المقال السابق وجدنا مشكلة في مشروعنا، وقد أخذت مني بعض الوقت لاكتشاف المشكلة، ولكن لا داعي للقلق فخلال الشرح سيتم توضيح سبب المشكلة والأماكن التي كانت تؤثر عليها، ومع أني أستطيع تعديل المقال السابق والتخلص من المشكلة،

إلا أني أفضل أن أريك كيف تجد وتحل المشكلة وخاصة لأنها ستساعدك على فهم عمل مكتبة 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 {}


 الشكل(2): إضافة زر ال replay

الخطوة الثالثة: إضافة تسمية الحالة

يجب إخبار اللاعب بحالة اللعبة، والدور على من، ومن فاز باللعب، لذلك قمنا بإضافة ال label لهذا الغرض ويتم تحديثها حسب حالة اللعب. قم بفتح  MTViewController.xib  وأضف (UILabellabel كما في الشكل(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
2- عرض اللوحة

الخطوة الأولى: إنشاء class خلية اللوحة

كما ذكرت مسبقاً، شكل اللوحة يحتوي 24 جزء أو خلية. سننشئ UIView subclass لجعل كل خلية أسهل في التعامل. أنشئ UIView subclass وأسميه MTBoardCell  كما في الشكل(4). الكلاس MTBoardCellclass فيه خاصية واحدة 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. ولأني أفضل أن تكون viewDidLoadmethod موجزة، أقوم بنقل الكود الخاص بإعداد العرض إلى setupViewhelper 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:   ل MTViewControllerinstance.
- (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];}
4- تحسين التفاعل
حتى الآن لا يوجد حد أقصى لعدد الأقراص التي يمكن للاعب وضعها على اللوحة والأحداث عند اللاعب 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 للعبة لأننا لن نحتاجه مجدداً. وهذا يعني أن deallocmethod تستدعي عندما يتم تحرير ال 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; }}
5- الفوز باللعب
في هذه الحالة، ليس من الممكن الفوز باللعب لأننا لم نبن الخوارزمية الخاصة بفحص إذا ما قام أحد اللاعبين بالفعل بوضع 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;
// 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) تحديث ال 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];}
typedef enum { MTPacketTypeUnknown = -1, MTPacketTypeDidAddDisc, MTPacketTypeStartNewGame} MTPacketType;
في parseBody: method من كلاس MTGameController نقوم بإرسال رسالة controllerDidStartNewGame: للمندوب إذا كان نوع ال packet يساوي toMTPacketTypeStartNewGame.

- (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: delegate method
في MTViewController class، نقوم باستدعاء resetGame كما فعلنا في replay action، ونحدث خاصية حالة اللعبة gameState.

- (void)controllerDidStartNewGame:(MTGameController *)controller { // Reset Game [self resetGame];
// Update Game State self.gameState = MTGameStateYourTurn;}
شغل نموذجين من التطبيق والعب اللعبة مع أصدقائك لترىإذا كان كل شيء يعمل كما يجب.
الملخص
مع أننا لدينا الآن لعبة، أعتقد أنك توافقني الرأي أنها ما زالت لبعض التحسين. فالتصميم يعتبر بدائي جداً وبعض الحركات ستكون جيدة أيضاً. ومع ذلك هدف هذا المشروع قد تحقق، وهو بناء لعبة متعددة اللاعبين باستخدام مكتبة Bonjour و CocoaAsyncSocket. الآن أنت يجب أن تكون فهمت أساسيات هذه المكتبة ومعرفة ماذا يمكن أن تستفيد منها.

الكــاتــب

    • مشاركة

ليست هناك تعليقات:

جميع الحقوق محفوظة لــ الشبح للمعلوميات 2019 ©