使用UIKit製做卡牌遊戲(一)ios遊戲篇

轉自朋友Tommy 的翻譯,本身只翻譯了第三篇教程。數組

譯者: Tommy | 原文做者: Matthijs Hollemans寫於2012/06/29 
原文地址: http://www.raywenderlich.com/12735/how-to-make-a-simple-playing-card-game-with-multiplayer-and-bluetooth-part-1xcode


image

這篇文章是由iOS教程團隊成員Matthijs Hollemans發表的,一個經驗豐富的開發工程師和設計師。你能夠在Google+Twitter上找到他。安全

紙牌遊戲在App Store上是很是流行的,超過2500個app了而且還在持續增加,因此是時候由raywenderlich.com來來教你們作紙牌遊戲了。服務器

另外,該系列教程總共7篇,用來展現如何作一個多人紙牌遊戲,你能夠和你的小夥伴們能夠經過GameKit的peer-to-peer的特性使用藍牙或Wi-Fi來玩。網絡

雖說你用這篇教程來作的是個遊戲,但你並不須要使用OpenGL或像Cocos2D同樣的遊戲框架,使用的僅僅是一些標準的UIImageView和一些UIView的基本動畫。session

不使用OpenGL和Cocos2D的緣由是咱們不須要!對於作這個,UIKit足夠了,而且UIKit擅長用於紙牌和棋盤遊戲的製做,這類遊戲的內容大都在屏幕內,而且只須要對一些view作一些簡單的動畫就能夠了。架構

要一步一步地學習下面的教程,你須要使用4.3或更高版本的Xcode,如何你如今使用的是4.2,那麼是時候升級了!app

還有,要測試多用戶的功能,你至少須要兩臺運行5.0或者更高版本系統的手機。若是你有Wi-Fi網絡環境,你也能夠用一個設備來玩,但最好仍是用多個(我在寫這邊教程的時候用四個不一樣的設備)。框架

繼續下面的教程吧,作你本身的多人紙牌遊戲,用你那神乎其神的牌技,給你的小夥伴們留下深入印象。ide

介紹: snap!


你將要作的這個紙牌遊戲是一個叫作的snap的兒童遊戲。這就是你最後完成遊戲的畫面。

image

什麼,你還不清楚遊戲的規則!好吧,玩這個遊戲,須要2到4我的,使用52張牌,經過卡片配對的方式來贏牌,你的目標就是贏得全部牌。

在每輪開始前,都會從新洗牌,而後順時針依次發牌,直到牌發完爲止。這些牌都會正面朝下襬在玩家面前。

玩家順時針依次翻牌。若是輪到你了,翻過你最上方的牌,若是你看到翻開的牌中有能和你的牌造成配對的時候,快速大喊一聲"snap",則匹配成功。兩個張牌具備相同的值,就能匹配,好比兩張王,無論大王仍是小王。

最快速喊出"snap!"而且兩張牌確實匹配的那個玩家贏得這兩張牌,而後將這兩張牌放入本身正面朝下的那些牌中。直到一個玩家贏得了全部牌。若是玩家喊出了"snap!",可是並無可匹配的牌,那麼他要給其餘每一個玩家一張牌做爲懲罰。

基本流程


這是一個經過藍牙或Wi-Fi鏈接的多人遊戲,你將使用GameKit框架來實現它。這裏只用到了GameKit的peer-to-peer鏈接的特性,並無使用Game Center相關知識,其實這個教程主要使用的只有一個類:GKsession。

在該教程的第一部分,你將學到如何鏈接玩家的設備,讓這些設備可使用Snap經過藍牙或Wi-Fi傳遞信息。玩這個遊戲呢,總要有我的先創建遊戲,做爲遊戲的"服務器",其餘玩家做爲"客戶端",加到這個已經建好的服務器中來。

這個遊戲的流程大致是下面這個樣子:

image

上面這張圖就是遊戲的主屏幕,打開遊戲玩家首先看到的畫面。如何你想玩一局,你能夠創建遊戲,讓其餘玩家加進來,或者加進別人創建的遊戲,還能夠一我的玩單機模式。

image

這個"Host Game"畫面列出了已經加進來的玩家,點擊start按鈕開始遊戲;從這一刻起,其餘沒有加進來的玩家就不能在進入到該局遊戲了。在玩遊戲前,一般玩家都約定好了誰來創建遊戲,而後其餘玩家加入。

image

"Join Game"畫面很像Host Game畫面,惟一的差異就是這個界面誒有開始按鈕,這個tableview列出了能夠加入的遊戲(列表裏頗有可能列了多個遊戲)。選擇一個你想加入的,而後等待主機點擊他畫面上的開始按鈕。

image

game screen畫面展現的是玩家們都圍着桌子坐下,桌子上拍着各自牌面朝上和朝下的牌。點擊屏幕右下角的按鈕能夠發出"snap!"的響聲(玩遊戲時,你不必讓滿屋子都是那響聲)。當有玩家按下"snap"按鈕時,該玩家暱稱旁邊會出現一個氣泡。

項目開始


爲了節省你時間,我已經建好了一個帶有圖片資源和一些nib文件的項目,在這裏下載源代碼,用Xcode打開Snap.xcodeproj。

若是你看了源代碼,你會發現,項目裏只有一個view controller,MainViewController。運行項目,你會發現界面很是簡單。

image

這個界面有5個UIImageView對象,組成一個logo(S,N,A,P和大王),還有3個UIButton。你能夠在這些imageView上作些簡單動畫,讓其更加生動一些,哦,不過你最好先把這些按鈕弄好看些。

你下載的文件還有一個叫作Action_Man.ttf的文件。這是你將在項目中用到的字體文件,他將代替系統的標準字體Helvetica或者你iPhone的內置字體。若是你在Mac下雙擊這個文件,這個文件將會在字體庫中被打開:

image

若是你問我,問什麼要用這種字體,由於我以爲這種字體看起來更能讓人興奮。但不幸的是這種字體不能裝在Mac上,也就是意味着不能在Interface Builder中使用,你必須在代碼裏面進行控制。然而,首先,你須要告訴UIKit有這種字體,這樣app才能加載它。

在Xcode中打開Snap-Info.plist文件,添加一行,key選中"Fonts provided by application",值爲數組類型。把第一項設置成那個字體文件的名字Action_Man.ttf :

image

你還須要將那個TTF字體文件加到項目中去,將文件拖拽至Supporting Files下:

image

注意,你須要確保Add to Targets這一項是選中的,要否則,字體文件是不會被包含在項目中的。

image

如今你能夠想下面這樣給你的button和label設置字體了:

UIFont *font = [UIFont fontWithName:@"Action Man" size:16.0f];
someLabel.font = font;

爲了不代碼重複,咱們建立個類別。打開File菜單,選擇New->File…選項,而後選擇"Objcect-C category"模板。建立一個"UIFont"類的類別"SnapAdditions":

image

這樣將會建立兩個文件,UIFont+SnapAdditions.hUIFont+SnapAddtions.m。爲了保持項目結構整潔,我將這兩個文件加進了剛建立的Categories組中。

image

UIFont+SnapAdditions.h文件中的內容替換爲:

@interface UIFont (SnapAdditions)

+ (id)rw_snapFontWithSize:(CGFloat)size;

@end

將.m中的內容替換爲:

#import "UIFont+SnapAdditions.h"

@implementation UIFont (SnapAdditions)

+ (id)rw_snapFontWithSize:(CGFloat)size
{
    return [UIFont fontWithName:@"Action Man" size:size];
}

@end

這只是一個簡單的類別,給UIFont類加了個方法:rw_snapFontWithSize:,這個方法能夠用Action Man文件建立一個UIFont對象。

注意,這個字體文件的名字時Action_Man.ttf,是帶有下劃線的,可是這種字體的名字是沒有下劃線的,因此你應該用字體的名字,而不是文件名字。要找到字體的名字,雙擊它,該文件將會在字體庫中打開。(注意,再打開的窗口上方顯示的就是字體的名字Action Man而不是Action_Man。)

第二個要注意的就是,我在這個方法前加了"rw_"。在給標準庫中的類添加類別時,在方法前加前綴(或者其它的惟一標示符)是個不錯的注意。作這些是爲了避免和蘋果內置的或者未來要添加的方法有衝突。雖然看起來蘋果不會去內置一個snapFontWithSize:方法,可是爲了安全考慮老是沒錯的。

這些準備工做以後,你就能夠給你Main View Controller上的button設置字體了。在MainViewController.m的最上方,導入類別文件:

#import "UIFont+SnapAdditions.h"

viewDidLoad方法中實現以下內容:

- (void)viewDidLoad
{
    [super viewDidLoad];
    self.hostGameButton.titleLabel.font = [UIFont rw_snapFontWithSize:20.0f];
    self.joinGameButton.titleLabel.font = [UIFont rw_snapFontWithSize:20.0f];
    self.singlePlayerGameButton.titleLabel.font = [UIFont rw_snapFontWithSize:20.0f];
}

如今運行項目。按鈕上應該顯示了新的字體:

image

若是你看到的仍然是老的字體,那麼在檢查一下Action_Man.ttf是否放入了項目中。選中文件,確保在Target membership這一項是選中的(在Xcode窗口右邊的Inspector面板中):

image

注意:在使用任何字體的時候都要仔細閱讀它的的許可證書,字體文件是受版權保護的,若是你想把它做爲你項目中的一部分一塊兒發佈,每每是要收取費用的, 但幸運的是Action Man字體是能夠無償使用和發佈的。

如今這些按鈕看起來好多了,可是一個好的按鈕還要有個邊框,咱們用一些拉伸的圖片來給按鈕加上邊框。這些圖片已經加在項目裏了,叫作Button.pngButtonPressed.png

由於這幾個不一樣的畫面都須要一樣樣式的按鈕,因此你最好把這個自定義樣式的功能放到類別中去。

給項目添加一個新的類別,叫作"SnapAdditions",可是此次類別是加在UIButton類上。這樣就又建立了兩個文件,UIButton+SnapAddtions.hUIButton+SnapAdditions.m,而後把這兩個文件放到Categories組中,用下面的代碼替換.h中的內容:

@interface UIButton (SnapAdditions)

- (void)rw_applySnapStyle;

@end

用下面代碼提換.m中的內容:

#import "UIButton+SnapAdditions.h"
#import "UIFont+SnapAdditions.h"

@implementation UIButton (SnapAdditions)

- (void)rw_applySnapStyle
{
    self.titleLabel.font = [UIFont rw_snapFontWithSize:20.0f];

    UIImage *buttonImage = [[UIImage imageNamed:@"Button"] stretchableImageWithLeftCapWidth:15 topCapHeight:0];
    [self setBackgroundImage:buttonImage forState:UIControlStateNormal];

    UIImage *pressedImage = [[UIImage imageNamed:@"ButtonPressed"] stretchableImageWithLeftCapWidth:15 topCapHeight:0];
    [self setBackgroundImage:pressedImage forState:UIControlStateHighlighted];
}

@end

看看,咱們又一次建立了個帶有"rw_"前綴的方法。當你調用這個方法做用到按鈕上時,它將會給按鈕一個新的背景圖片和新的字體樣式。

MainViewController.m中引入新的類別文件:

#import "UIButton+SnapAdditions.h"

如今你能夠用下面的代碼替換viewDidLoad中的內容了:

- (void)viewDidLoad
{
    [super viewDidLoad];
    [self.hostGameButton rw_applySnapStyle];
    [self.joinGameButton rw_applySnapStyle];
    [self.singlePlayerGameButton rw_applySnapStyle];
}

再次運行項目,如今按鈕是這個樣子的了:

image

動畫介紹


如今,你應該讓主畫面活躍一些。在遊戲啓動的時候,讓logo卡片飛進來如何?

咱們將下面的代碼加入MainViewControllerm中來實現這個效果:

- (void)prepareForIntroAnimation
{
    self.sImageView.hidden = YES;
    self.nImageView.hidden = YES;
    self.aImageView.hidden = YES;
    self.pImageView.hidden = YES;
    self.jokerImageView.hidden = YES;
}

- (void)performIntroAnimation
{
    self.sImageView.hidden = NO;
    self.nImageView.hidden = NO;
    self.aImageView.hidden = NO;
    self.pImageView.hidden = NO;
    self.jokerImageView.hidden = NO;

    CGPoint point = CGPointMake(self.view.bounds.size.width / 2.0f, self.view.bounds.size.height * 2.0f);

    self.sImageView.center = point;
    self.nImageView.center = point;
    self.aImageView.center = point;
    self.pImageView.center = point;
    self.jokerImageView.center = point;

    [UIView animateWithDuration:0.65f
                          delay:0.5f
                        options:UIViewAnimationOptionCurveEaseOut
                     animations:^
     {
         self.sImageView.center = CGPointMake(80.0f, 108.0f);
         self.sImageView.transform = CGAffineTransformMakeRotation(-0.22f);

         self.nImageView.center = CGPointMake(160.0f, 93.0f);
         self.nImageView.transform = CGAffineTransformMakeRotation(-0.1f);

         self.aImageView.center = CGPointMake(240.0f, 88.0f);

         self.pImageView.center = CGPointMake(320.0f, 93.0f);
         self.pImageView.transform = CGAffineTransformMakeRotation(0.1f);

         self.jokerImageView.center = CGPointMake(400.0f, 108.0f);
         self.jokerImageView.transform = CGAffineTransformMakeRotation(0.22f);
     }
    completion:nil];
}

第一個方法prepareForIntroAnimation,只是簡單的隱藏帶有logo的卡片。實際的動畫在performIntroAnimation方法中。首先,將卡片放置在屏幕之外,水平居中且在屏幕下方。而後,經過實現動畫的block讓卡片移動到最終的位置。這樣看起來就這些卡片就像是從中間散開同樣。

分別在viewWillAppear:viewDidApper:中調用這兩個方法:

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];

    [self prepareForIntroAnimation];
}

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];

    [self performIntroAnimation];
}

如今,啓動遊戲,這些卡片會飛進屏幕並散開,是否是很cool,哈哈。

image

僅僅這些動畫還不夠完美。我想,當卡片飛向目標位置時,讓按鈕漸漸出來,這樣效果會更好。將下面方法中的幾行代碼放在prepareForIntroAnimation:方法的最後:

- (void)prepareForIntroAnimation
{
    . . .

    self.hostGameButton.alpha = 0.0f;
    self.joinGameButton.alpha = 0.0f;
    self.singlePlayerGameButton.alpha = 0.0f;

    _buttonsEnabled = NO;
}

上面這些代碼是爲了讓按鈕先透明。將下面的block放到performIntroAnimation:方法的最後:

- (void)performIntroAnimation 
{
        . . .

        [UIView animateWithDuration:0.5f
                              delay:1.0f
                            options:UIViewAnimationOptionCurveEaseOut
                         animations:^
         {
             self.hostGameButton.alpha = 1.0f;
             self.joinGameButton.alpha = 1.0f;
             self.singlePlayerGameButton.alpha = 1.0f;
         }
                         completion:^(BOOL finished)
         {
             _buttonsEnabled = YES;
         }];
}

這樣按鈕就有了從透明到徹底顯現的動畫。_buttonsEnabled這個變量又有什麼用處呢?它的用途在於,確保在按鈕徹底顯示以後才接收點擊事件,你確定不想在按鈕作透明度變化的時候讓玩家去點擊它。

在按鈕作動畫的時候,用_buttonsEnabled這個變量來忽略玩家對按鈕的點擊。如今,將這個變量加到@implementation中:

@implementation MainViewController
{
    BOOL _buttonsEnabled;
}

運行項目,看下動畫,是否是很流暢!

GameKit和多人遊戲


GameKit是iOS SDK中一個標準的framework。它主要用於Game Center(在這篇教程中不會用到)和語音聊天,可是也有在多臺設備之間peer-to-peer鏈接的通信的特性。若是全部的設備都在同一個Wi-Fi網絡環境,GameKit還能夠用Wi-Fi來代替藍牙。(這是經過網絡實現peer-to-peer的方式,並且你必須花些時間本身實現大部分代碼,這種方式更擅長被用於Game Center。)

GameKit的peer-to-peer特性,對於在同一個房間,玩家們各自使用本身的設備玩遊戲是很是棒的。可是聽說,玩家在使用藍牙的時候,他們之間的距離不能超過10米(或30步)。

那麼,到底什麼是peer-to-peer鏈接呢?每一個參與GameKit網絡會話的設備稱做一個"peer"。一個設備能夠做爲提供服務的"server",也能夠做爲尋找服務器的"client",或者即做爲服務器又做爲客戶端。GameKit是使用Bonjour技術來實現這些的,可是你並不須要直接使用Bonjour,由於使用封裝好的GameKit就能夠了。

當你使用藍牙時,設備並不必定要配對,就像用藍牙鼠標或者鍵盤跟你的設備配對同樣。GameKit很簡單地就能夠實現客戶端和服務器的鏈接,一旦鏈接,設備之間就能夠經過本地網絡發送信息了。

你不可以選擇是使用藍牙仍是Wi-Fi;GameKit會爲你選擇的。模擬器是不支持藍牙的,可是支持Wi-Fi。

在開發和測試這篇教程時,我發現使用模擬器和一兩個真機經過本地Wi-Fi進行鏈接,就能夠很容易地實現多人玩遊戲。若是你要想經過藍牙來玩,那就須要至少兩個帶有藍牙功能的真機了。

注意:其實不用GameKit框架,使用Bonjour和藍牙也能夠實現網絡通信,可是,若是你想建立一個多人遊戲,使用GameKit是很是簡單的。它隱藏了不少讓你很是厭惡的網絡開發的東西,而且給你封裝了一個簡單的類GKSession來使用。這個類也是在該教程中,涉及到GameKit的惟一的類(和它的委託,GKSessionDelegate)。

若是你只想作一個經過藍牙或者Wi-Fi來玩的兩人遊戲,你可使用GameKit的GKPeerPickerController來建立設備間的鏈接。就像下面這個樣子。

image

GKPeerPickerController的使用很簡單,可是隻能兩臺設備鏈接。可是Snap!須要同時四我的一塊兒玩,因此經過這篇教程來教你經過本身寫代碼實現多人鏈接。

"Host Game"界面


在這部分,你將添加一個"Host Game"界面。這個界面容許玩家創建一個房間,讓其餘玩家加進來。當你完成的時候,應該是這個樣子:

image

這裏有個列出了鏈接到當局遊戲的玩家的列表,一個開始按鈕,還有個能夠輸入玩家暱稱的文本框(默認裏面是你機器的名字)。

添加一個UIViewController的子類,命名爲HostViewController。建立時不要選中"With XIB for user interface"選項。這個界面上的基本控件已經在一個xib文件中擺放好了,你能夠在"Snap/en.lproj"文件夾中找到這個xib文件。把HostViewController.xib加到項目中。這個xib文件打開後是下面這個樣子:

image

這些UI控件都有跟代碼裏的屬性和方法相關聯,所以,你應該把這些控件和HostViewController類關聯起來,不然,項目運行起來去加載xib時會掛掉的。

HostViewController.m裏面,將下面幾行加到類擴展中(在文件的最上方):

@interface HostViewController ()
@property (nonatomic, weak) IBOutlet UILabel *headingLabel;
@property (nonatomic, weak) IBOutlet UILabel *nameLabel;
@property (nonatomic, weak) IBOutlet UITextField *nameTextField;
@property (nonatomic, weak) IBOutlet UILabel *statusLabel;
@property (nonatomic, weak) IBOutlet UITableView *tableView;
@property (nonatomic, weak) IBOutlet UIButton *startButton;
@end

注意,你是將IBoutlet屬性加到了.m文件中,而不是.h文件中。這是新版Xcode(4.2或者更高)中LLVM編譯器的新特性。這樣可使你的.h文件更加簡潔明,能夠把一些不想被別的類看到的屬性隱藏起來。

固然,你還須要synthesize這些屬性,將下面的代碼添加到@implementation下面:

@synthesize headingLabel = _headingLabel;
@synthesize nameLabel = _nameLabel;
@synthesize nameTextField = _nameTextField;
@synthesize statusLabel = _statusLabel;
@synthesize tableView = _tableView;
@synthesize startButton = _startButton;

提示:聽說在下個版本的Xcode(或許正是你在看這篇教程的時候),你就不須要寫@synthesize這行代碼了,可是若是你用的是Xcode4.3,仍然須要放進去。

用下面的代碼替換shouldAutorotateToInterfaceOrientation:這個方法,限制設備只支持橫屏:

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
    return UIInterfaceOrientationIsLandscape(interfaceOrientation);
}

而後,將下面的幾個還未用到的方法放在文件的下面:

- (IBAction)startAction:(id)sender
{
}

- (IBAction)exitAction:(id)sender
{
}

#pragma mark - UITableViewDataSource

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return 0;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    return nil;
}

最後,用下面這行代碼替換HostViewController.h文件中的@interface這行:

@interface HostViewController : UIViewController <UITableViewDataSource, UITableViewDelegate, UITextFieldDelegate>

Host Game界面的基本工做已經作完,可是當用戶點擊主界面上的按鈕時,還須要觸發動做顯示Host Game界面。將下面的代碼添加到MainViewController.h中:

#import "HostViewController.h"

而且像下面這樣實現MainViewContoller.m文件中的HostGameAction:方法:

- (IBAction)hostGameAction:(id)sender
{
    if (_buttonsEnabled)
    {
        HostViewController *controller = [[HostViewController alloc] initWithNibName:@"HostViewController" bundle:nil];

        [self presentViewController:controller animated:NO completion:nil];
    }
}

若是你如今啓動app,點擊Host Game按鈕,屏幕上將會呈現Host Game界面,可是這還不夠炫。你用modal的方式來顯示這個新的viewController,可是沒有把animated這個參數設置爲YES,因此也就沒有從下往上滑動的效果。

由於像這樣的動畫用在這裏並非太好,因此咱們並無使用讓Host Game界面從主界面向上滑動出現的效果,咱們在MainViewController.m中建立了一個新的方法,用來生成咱們要使用的新動畫:

- (void)performExitAnimationWithCompletionBlock:(void (^)(BOOL))block
{
    _buttonsEnabled = NO;

    [UIView animateWithDuration:0.3f
        delay:0.0f
        options:UIViewAnimationOptionCurveEaseOut
        animations:^
        {
            self.sImageView.center = self.aImageView.center;
            self.sImageView.transform = self.aImageView.transform;

            self.nImageView.center = self.aImageView.center;
            self.nImageView.transform = self.aImageView.transform;

            self.pImageView.center = self.aImageView.center;
            self.pImageView.transform = self.aImageView.transform;

            self.jokerImageView.center = self.aImageView.center;
            self.jokerImageView.transform = self.aImageView.transform;
        }
        completion:^(BOOL finished)
        {
            CGPoint point = CGPointMake(self.aImageView.center.x, self.view.frame.size.height * -2.0f);

            [UIView animateWithDuration:1.0f
                delay:0.0f
                options:UIViewAnimationOptionCurveEaseOut
                animations:^
                {
                    self.sImageView.center = point;
                    self.nImageView.center = point;
                    self.aImageView.center = point;
                    self.pImageView.center = point;
                    self.jokerImageView.center = point;
                }
                completion:block];

            [UIView animateWithDuration:0.3f
            delay:0.3f
                options:UIViewAnimationOptionCurveEaseOut
                animations:^
                {
                    self.hostGameButton.alpha = 0.0f;
                    self.joinGameButton.alpha = 0.0f;
                    self.singlePlayerGameButton.alpha = 0.0f;
                }
                completion:nil];
        }];
}

提示:你沒必要在乎這個方法放在哪一個位置(只要在@implementation和@end之間)。以前你必須在.h文件或者.m擴展中聲明,或者將該方法放在使用的代碼前面。如今不須要了,這一切都要感謝Xcode4.3中的LLVM編譯器。無論你把代碼放在哪裏,甚至你在使用以前沒有任何聲明,這個編譯器均可以很聰明地找到須要的方法。

performExitAnimationWithCompletionBlock:中的動畫:logo卡片從屏幕外飛進來,同時,界面上的按鈕漸漸出現。當動畫結束以後,會執行做爲參數傳進來的block。

如今將hostGameAction:方法改成以下:

- (IBAction)hostGameAction:(id)sender
{
    if (_buttonsEnabled)
    {
        [self performExitAnimationWithCompletionBlock:^(BOOL finished)
        {   
            HostViewController *controller = [[HostViewController alloc] initWithNibName:@"HostViewController" bundle:nil];

            [self presentViewController:controller animated:NO completion:nil];
        }];
    }
}

代碼跟之前差很少,可是如今的邏輯是:將建立和呈現Host Game界面的代碼放在了一個動畫執行以後會調用的block中。運行項目看看效果吧。你把動畫的代碼放在了一個單獨的方法中,當這樣用戶點擊其它按鈕時,也能夠用這個方法來作動畫。

正如你所看到的,Host Game界面使用的仍是默認的Helvetica字體,而且它的開始按鈕也沒有邊框。其實很容易就能夠改好。將下面兩個頭文件導入到HostViewController.m文件中:

#import "UIButton+SnapAdditions.h"
#import "UIFont+SnapAdditions.h"

而後用下面的代碼替換viewDidLoad方法:

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.headingLabel.font = [UIFont rw_snapFontWithSize:24.0f];;
    self.nameLabel.font = [UIFont rw_snapFontWithSize:16.0f];
    self.statusLabel.font = [UIFont rw_snapFontWithSize:16.0f];
    self.nameTextField.font = [UIFont rw_snapFontWithSize:20.0f];

    [self.startButton rw_applySnapStyle];
}

由於你建立了這些類別,因此能夠很方便地添加字體和按鈕樣式,真是不費吹灰之力就讓界面如此漂亮,哈哈(原句: it’s a snap (ha ha) to make the screen look good)。運行項目看看效果吧。

如今還有點東西要改進。這裏有個讓用戶輸入名字的文本框,當用戶點擊時,在界面的最上方會彈出個鍵盤。這個鍵盤蓋住了半個屏幕,關鍵是如今尚未辦法讓它下去。

image

第一種讓鍵盤消失的方法:就是用那個又大又藍帶有"Done"(中文狀態下應該是"完成")的按鈕。如今你點擊以後是什麼都不會發生的,不過將下面的方法加入HostViewController.m中就能解決輕鬆這個問題:

#pragma mark - UITextFieldDelegate

- (BOOL)textFieldShouldReturn:(UITextField *)textField
{
    [textField resignFirstResponder];
    return NO;
}

第二種讓鍵盤消失的方法:就是viewDidLoad:方法的最後加入以下代碼:

- (void)viewDidLoad
{
    . . .

    UITapGestureRecognizer *gestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self.nameTextField action:@selector(resignFirstResponder)];
    gestureRecognizer.cancelsTouchesInView = NO;
    [self.view addGestureRecognizer:gestureRecognizer];
}

你在主界面建立了個點擊手勢。如今,當你點擊除文本框之外的地方的時候,這個手勢操做會給文本框發送"resignFirstResponder"消息,這個消息可使鍵盤消失。

注意,你須要將cancelsTouchesInView屬性設置爲NO,不然界面上的其它控件都不會有任何事件響應了,好比列表,按鈕。:

退出Host Screen界面


除了開始遊戲按鈕,其它的還都沒有事件。如今咱們要作的就是點擊Host Screen界面左下角的x按鈕,讓屏幕從新回到主界面。

這個按鈕是跟exitAction:方法綁定的,如今裏面仍是空的。點擊它應該可以關閉這個界面,你將用delegate來實現這些。在這個項目中有幾個viewController, 你將用delegate來控制它們的切換。

將下面的代碼添加到HostViewController.h,放在@interface這行以前:

@class HostViewController;

@protocol HostViewControllerDelegate <NSObject>

- (void)hostViewControllerDidCancel:(HostViewController *)controller;

@end

在@interface里加入這個新的屬性:

@property (nonatomic, weak) id <HostViewControllerDelegate> delegate;

屬性須要synthesize,所以將下面的代碼放在HostViewController.m中:

@synthesize delegate = _delegate;

最後,用下面的方法替換exitAction:方法:

- (IBAction)exitAction:(id)sender
{
    [self.delegate hostViewControllerDidCancel:self];
}

想法很明確了:在HostViewController中聲明一個delegate protocol。當用戶點擊了x按鈕時,HostViewController會告訴delegate,Host Game界面已經不須要了。而後delegate會履行本身的職責,關掉界面。

在這裏,MainViewController扮演着delegate的角色,固然,咱們還須要在MainViewController.h中添加<HostViewControllerDelegate>

@interface MainViewController : UIViewController <HostViewControllerDelegate>

MainViewController.m中將hostGameAction:方法改成:

- (IBAction)hostGameAction:(id)sender
{
    if (_buttonsEnabled)
    {
        [self performExitAnimationWithCompletionBlock:^(BOOL finished)
        {   
            HostViewController *controller = [[HostViewController alloc] initWithNibName:@"HostViewController" bundle:nil];
            controller.delegate = self;

            [self presentViewController:controller animated:NO completion:nil];
        }];
    }
}

如今,你已經將MainViewController設置爲HostViewController的delegate。最後,在MainViewController.m文件中實現delegate方法:

#pragma mark - HostViewControllerDelegate

- (void)hostViewControllerDidCancel:(HostViewController *)controller
{
    [self dismissViewControllerAnimated:NO completion:nil];
}

沒有使用動畫,簡單地關掉了HostViewController界面。可是因爲MainViewController的viewWillAppear方法被再次調用,卡片飛進來的動畫就被執行了一次。運行項目試試看。

注意:在調試時,當一個界面消失的時候,我想確保viewController被銷燬,因此我在viewController中加入dealloc方法,再次方法中輸出信息到控制檯。

- (void)dealloc
{
    #ifdef DEBUG
    NSLog(@"dealloc %@", self);
    #endif
}

即便你在項目中使用了ARC,項目仍然有可能有內存泄露的地方。雖然ARC是個很是好的內存管理工具,可是它沒法解決循環引用的問題。好比你有兩個對象,都有個強類型指針指向對方,這樣它們將永遠留在內存中。這就是我爲何在dealloc中輸出log,確保對象對象被銷燬的緣由,只是爲了擦亮本身的眼睛,把事情搞得更清楚。

如今,Host Game界面已經完成。在你創建想加入或者創建一局以前,你必須先進入遊戲界面。不然,其它設備找不到你的設備!

"Join Game"界面


這個界面跟Host Game界面很類似,可是鑑於它們那些不一樣之處,咱們足以有理由去建立一個新的類(不是繼承hostViewController)。因爲跟以前作的很相似,因此你能夠很快地完成這些。

建立一個UIViewController的子類JoinViewController。建立時不要帶有xib,由於我已經在你開始用的代碼裏提供了,將這個xib文件(在"Snap/en.lproj/"這裏)添加到項目中。

用這些代替JoinViewController.h文件的內容:

@class JoinViewController;

@protocol JoinViewControllerDelegate <NSObject>

- (void)joinViewControllerDidCancel:(JoinViewController *)controller;

@end

@interface JoinViewController : UIViewController <UITableViewDataSource, UITableViewDelegate, UITextFieldDelegate>

@property (nonatomic, weak) id <JoinViewControllerDelegate> delegate;

@end

就像當初對Host Game界面所作的那樣,用下面的代碼替換JoinViewController.m文件的內容:

#import "JoinViewController.h"
#import "UIFont+SnapAdditions.h"

@interface JoinViewController ()
@property (nonatomic, weak) IBOutlet UILabel *headingLabel;
@property (nonatomic, weak) IBOutlet UILabel *nameLabel;
@property (nonatomic, weak) IBOutlet UITextField *nameTextField;
@property (nonatomic, weak) IBOutlet UILabel *statusLabel;
@property (nonatomic, weak) IBOutlet UITableView *tableView;

@property (nonatomic, strong) IBOutlet UIView *waitView;
@property (nonatomic, weak) IBOutlet UILabel *waitLabel;
@end

@implementation JoinViewController

@synthesize delegate = _delegate;

@synthesize headingLabel = _headingLabel;
@synthesize nameLabel = _nameLabel;
@synthesize nameTextField = _nameTextField;
@synthesize statusLabel = _statusLabel;
@synthesize tableView = _tableView;

@synthesize waitView = _waitView;
@synthesize waitLabel = _waitLabel;

- (void)dealloc
{
    #ifdef DEBUG
    NSLog(@"dealloc %@", self);
    #endif
}

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.headingLabel.font = [UIFont rw_snapFontWithSize:24.0f];
    self.nameLabel.font = [UIFont rw_snapFontWithSize:16.0f];
    self.statusLabel.font = [UIFont rw_snapFontWithSize:16.0f];
    self.waitLabel.font = [UIFont rw_snapFontWithSize:18.0f];
    self.nameTextField.font = [UIFont rw_snapFontWithSize:20.0f];

    UITapGestureRecognizer *gestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self.nameTextField action:@selector(resignFirstResponder)];
    gestureRecognizer.cancelsTouchesInView = NO;
    [self.view addGestureRecognizer:gestureRecognizer];
}

- (void)viewDidUnload
{
    [super viewDidUnload];
    self.waitView = nil;
}

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
    return UIInterfaceOrientationIsLandscape(interfaceOrientation);
}

- (IBAction)exitAction:(id)sender
{
    [self.delegate joinViewControllerDidCancel:self];
}

#pragma mark - UITableViewDataSource

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return 0;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    return nil;
}

#pragma mark - UITextFieldDelegate

- (BOOL)textFieldShouldReturn:(UITextField *)textField
{
    [textField resignFirstResponder];
    return NO;
}

@end

除了waitView的綁定以外,這裏跟以前沒什麼特別的。注意,這個屬性是被聲明爲"strong",而不是像其它屬性同樣"weak",由於它在這個xib文件中屬於top-level視圖:

image

將這個屬性聲明爲strong是及其重要的,這樣能夠避免被銷燬。你不必對第一個視圖作這樣的操做,由於viewController內置的self.view屬性已經retain了。

當用戶點擊了列表中別人創建的遊戲房間名時,我麼在主頁面上方放置了另外一個視圖(就是畫面上顯示"Connecting..."的視圖)。這原本能夠用一個新的viewController來作,可是爲了簡單,咱們先這樣作了。

修改MainViewController.n文件:

#import "HostViewController.h"
#import "JoinViewController.h"

@interface MainViewController : UIViewController <HostViewControllerDelegate, JoinViewControllerDelegate>

@end

MainViewController.m中,用下面的方法替換JoinGameAction:

- (IBAction)joinGameAction:(id)sender
{
    if (_buttonsEnabled)
    {
        [self performExitAnimationWithCompletionBlock:^(BOOL finished)
        {
            JoinViewController *controller = [[JoinViewController alloc] initWithNibName:@"JoinViewController" bundle:nil];
            controller.delegate = self;

            [self presentViewController:controller animated:NO completion:nil];
        }];
    }
}

而且實現以下delegate方法:

#pragma mark - JoinViewControllerDelegate

- (void)joinViewControllerDidCancel:(JoinViewController *)controller
{
    [self dismissViewControllerAnimated:NO completion:nil];
}

如今咱們已經完成了Join Game界面。是時候添加配對邏輯了。

注意:當你寫多人遊戲(或者其它基於網絡通信的軟件)時,都有兩種架構選擇:client-server和peer-to-peer(這裏的意思是點對點方式)。儘管咱們可使用GameKit的"peer-to-peer"方式,可是此次咱們選擇client-server方式。一個玩家做爲服務器,其它玩家加進來。

image

在client-server這種模式下,server控制着全部事情而且決定卡片是否配對。client發送玩家的更新到server,server通知全部client更新,client之間並不進行直接數據交流。然而在真正讓點對點模式下,全部的client都是平等的,作相同的工做,可是你必定要確保全部的玩家看到一樣的事情,由於這種方式沒有server來控制。 
再次說一次,在這篇教程中使用的是client-server模式,這種模式須要一個玩家來做爲服務器。

牌型配對


如今你的Host Game和Join Game兩個界面基本上都能用了,如今你能夠添加卡片配對邏輯了。當一個玩家點擊Host Game界面上的按鈕時,他的設備會被別的玩家在Snap中搜索到。當其餘玩家進入Join Game界面時,可以看到不少server。

雖然GameKit的GKSession類爲這些作了不少,可是你仍然須要作不少工做。咱們建立了兩個類MatchmakingServer和MatchmakingCient來處理遊戲的邏輯,而不是把全部的邏輯都放在viewController裏。viewController承受東西太多的話,代碼看起來會很是的凌亂。這就是我爲何還要建立兩個新對象來管理設備間通信的緣由。

在建立這些新的類以前,你應該先把GameKit框架添加進項目中。在Target Summary界面,Linked Frameworks and Libraries裏,點擊+按鈕,在彈出的列表中選擇GameKit.framework加到項目中。

image

由於咱們要在不少文件中用到GameKit這個框架,因此咱們沒有像日常那樣在各個源代碼文件上方導入GameKit,而是在預編譯頭文件中導入GameKit框架,也就是Snap-Prefix.pch文件(在Supporting Files中)中的#ifdef __OBJC__部分加入下面這行代碼:

#import <GameKit/GameKit.h>

如今全部的文件中均可以用GameKit框架了。

你還有一件事要作,那就是在Info.plist文件中表示這個項目用了peer-to-peer功能,由於並非全部的設備(尤爲是第一代的iPhone和iPod Touch)都支持peer-to-peer功能。

打開Snap-Info.plist文件並在"Required device capabilities"下加入一個子項,並設置其值爲"peer-peer":

image

MatchmakingServer


建立一個新的NSObject的子類,命名爲MatchmakingServer。我建議把它放進一個新的組"Networking"裏。用下面的內容替換MatchmakingServer.h

@interface MatchmakingServer : NSObject <GKSessionDelegate>

@property (nonatomic, assign) int maxClients;
@property (nonatomic, strong, readonly) NSArray *connectedClients;
@property (nonatomic, strong, readonly) GKSession *session;

- (void)startAcceptingConnectionsForSessionID:(NSString *)sessionID;

@end

MatchmakingServer有一個鏈接其它client的列表,而且要有一個變量來控制同一時間鏈接進來的client數量。就是該遊戲每局有最多4我的玩家的限制,也就是隻能有3個玩家鏈接進來(算上本身正好是4個)。

MatchmakingServer還有個GKSession對象,來控制各個設備之間的網絡通信,MatchmakingServer還要遵照GKSessionDelegate協議,由於這樣GKSession能夠告訴它一些重要的事件。

如今,MatchmakingServer還只有一個方法,用來進行廣播服務和接收client的消息。很快,你就會往這個類里加不少東西。

用下面的代碼替換MatchmakingServer.m文件中的內容:

#import "MatchmakingServer.h"

@implementation MatchmakingServer
{
    NSMutableArray *_connectedClients;
}

@synthesize maxClients = _maxClients;
@synthesize session = _session;

- (void)startAcceptingConnectionsForSessionID:(NSString *)sessionID
{
    _connectedClients = [NSMutableArray arrayWithCapacity:self.maxClients];

    _session = [[GKSession alloc] initWithSessionID:sessionID displayName:nil sessionMode:GKSessionModeServer];
    _session.delegate = self;
    _session.available = YES;
}

- (NSArray *)connectedClients
{
    return _connectedClients;
}

#pragma mark - GKSessionDelegate

- (void)session:(GKSession *)session peer:(NSString *)peerID didChangeState:(GKPeerConnectionState)state
{
    #ifdef DEBUG
    NSLog(@"MatchmakingServer: peer %@ changed state %d", peerID, state);
    #endif
}

- (void)session:(GKSession *)session didReceiveConnectionRequestFromPeer:(NSString *)peerID
{
    #ifdef DEBUG
    NSLog(@"MatchmakingServer: connection request from peer %@", peerID);
    #endif
}

- (void)session:(GKSession *)session connectionWithPeerFailed:(NSString *)peerID withError:(NSError *)error
{
    #ifdef DEBUG
    NSLog(@"MatchmakingServer: connection with peer %@ failed %@", peerID, error);
    #endif
}

- (void)session:(GKSession *)session didFailWithError:(NSError *)error
{
    #ifdef DEBUG
    NSLog(@"MatchmakingServer: session failed %@", error);
    #endif
}

@end

這基本上是公式同樣的玩意兒,GKSessionDelegate方法除了在Xcode Debug面板上打出了一些log以外,沒有作任何事情。不過在startAcceptingConnectionsForSessionID方法中卻是有些新鮮的東西:

_session = [[GKSession alloc] initWithSessionID:sessionID displayName:nil sessionMode:GKSessionModeServer];
_session.delegate = self;
_session.available = YES;

你在這裏建立了GKSessin對象,而且設置好delegate在這裏管理。說詳細些就是隻對有效的service(以sessionID參數命名的)進行廣播服務,不會關心其它任何廣播一樣消息的設備。你告訴session,MatchmakingServer是它的delegate,而後設置了"availabel"屬性的值爲YES,這樣就能夠開啓廣播了。這就是你讓GameKit session所作的事情。

如今你要把MatchMakingServer導入到HostViewController.h文件中:

#import "MatchmakingServer.h"

添加一個MatchmakingServer對象做爲HostViewController的一個實例變量,以下:

@implementation HostViewController
{
    MatchmakingServer *_matchmakingServer;
}

添加以下方法到HostViewController.m文件中:

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];

    if (_matchmakingServer == nil)
    {
        _matchmakingServer = [[MatchmakingServer alloc] init];
        _matchmakingServer.maxClients = 3;
        [_matchmakingServer startAcceptingConnectionsForSessionID:SESSION_ID];

        self.nameTextField.placeholder = _matchmakingServer.session.displayName;
        [self.tableView reloadData];
    }
}

一旦Host Game界面出現,就會建立一個MatchmakingServer對象,而且告訴它開始接受鏈接。同時它會把你機器的名字做爲placeholder填入"Your Name"文本框中,若是你不輸入你的名字,那麼就用這個來做爲你在遊戲中的標示。

在你定義SESSION_ID以前,新的代碼是不會起做用的。不用太關心SESSION_ID的內容是什麼,只要server和client保持一樣地值就能夠了。GameKit將用這個值做爲惟一Bonjour標示。由於MatchmakingServer和MatchmakingClient都會用到這個SESSION_ID,因此最好仍是把它定義在prefix文件中吧。打開Snap-Prefix.pch而且把下面這行代碼放在文件的最後:

// The name of the GameKit session.
#define SESSION_ID @"Snap!"

運行項目,點擊Host Game界面上的按鈕。若是你是運行在模擬器中,你將會看到下面這個界面:

image

GKSession中displayName屬性的值是像這樣"com.hollance.Sanp355561232..."的一串字符串,若是你在你本身的設備上運行,上面顯示的將是你設備的名字,好比:"Joe's iPhone"或者你一開始給你設備設置的名字。

你如今已經有一個運行良好,能夠廣播"Snap!"服務的server了,可是尚未client鏈接進來。如今咱們就建立一個MatchmakingClient類來實現這些。

MatchingmakingClient


添加一個新的類MatchmakingClient,繼承NSObject,並把它放到Networking組。用下面的代碼替換MatchmakingClient.h文件的內容:

@interface MatchmakingClient : NSObject <GKSessionDelegate>

@property (nonatomic, strong, readonly) NSArray *availableServers;
@property (nonatomic, strong, readonly) GKSession *session;

- (void)startSearchingForServersWithSessionID:(NSString *)sessionID;

@end

就像是從MatchmakingServer這面鏡子映出來的同樣,可是這個列表裏不是client,而是server。用下面的代碼替換MatchmakingClient.m文件的內容:

#import "MatchmakingClient.h"

@implementation MatchmakingClient
{
    NSMutableArray *_availableServers;
}

@synthesize session = _session;

- (void)startSearchingForServersWithSessionID:(NSString *)sessionID
{
    _availableServers = [NSMutableArray arrayWithCapacity:10];

    _session = [[GKSession alloc] initWithSessionID:sessionID displayName:nil sessionMode:GKSessionModeClient];
    _session.delegate = self;
    _session.available = YES;
}

- (NSArray *)availableServers
{
    return _availableServers;
}

#pragma mark - GKSessionDelegate

- (void)session:(GKSession *)session peer:(NSString *)peerID didChangeState:(GKPeerConnectionState)state
{
    #ifdef DEBUG
    NSLog(@"MatchmakingClient: peer %@ changed state %d", peerID, state);
    #endif
}

- (void)session:(GKSession *)session didReceiveConnectionRequestFromPeer:(NSString *)peerID
{
    #ifdef DEBUG
    NSLog(@"MatchmakingClient: connection request from peer %@", peerID);
    #endif
}

- (void)session:(GKSession *)session connectionWithPeerFailed:(NSString *)peerID withError:(NSError *)error
{
    #ifdef DEBUG
    NSLog(@"MatchmakingClient: connection with peer %@ failed %@", peerID, error);
    #endif
}

- (void)session:(GKSession *)session didFailWithError:(NSError *)error
{
    #ifdef DEBUG
    NSLog(@"MatchmakingClient: session failed %@", error);
    #endif
}

@end

又一次,咱們給類搭了個框架,而後去填好這些方法。注意,你建立了個GKSessionModeClient模式的GKSession對象,所以它會尋找有效的server(不是本身廣播的服務)。

如今將新建立的類整合到JoinViewController中。這樣server和client才能鏈接。首先導入頭文件到JoinViewController.h中:

#import "MatchmakingClient.h"

而後添加一個實例變量到JoinViewController.m中:

@implementation JoinViewController
{
    MatchmakingClient *_matchmakingClient;
}

用下面的代碼實現viewDidAppear:方法:

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];

    if (_matchmakingClient == nil)
    {
        _matchmakingClient = [[MatchmakingClient alloc] init];
        [_matchmakingClient startSearchingForServersWithSessionID:SESSION_ID];

        self.nameTextField.placeholder = _matchmakingClient.session.displayName;
        [self.tableView reloadData];
    }
}

這時候,你能夠進行測試了。確保有兩臺以上帶有藍牙功能的設備,或者用本地Wi-Fi網絡,用你的模擬器和真機鏈接。一個設備點擊進入Host Game界面,另外一個點擊設備進入Join Game界面。

界面上什麼都沒有發生,可是debug窗口輸出了不少log:

server輸出內容:

Snap[3810:707] BTM: attaching to BTServer
Snap[3810:707] BTM: posting notification BluetoothAvailabilityChangedNotification
Snap[3810:707] BTM: received BT_LOCAL_DEVICE_CONNECTABILITY_CHANGED event
Snap[3810:707] BTM: posting notification BluetoothConnectabilityChangedNotification

這些都是GameKit發出的消息。client也能夠從GameKit發出消息,可是client輸出的是:

Snap[94530:1bb03] MatchmakingClient: peer 663723729 changed state 0

這個消息來自GKSessionDelegate的session:peer:didChangeState:方法,在你的MatchmakingClient類裏面。他告訴咱們ID爲"663723729"的小夥伴已經準備好了,也就是說,client偵測到了有效的server。

注意:peer ID是GameKit生成用來在一次會話中區別不一樣設備的標示。每次啓動遊戲,這個ID都會變的。很快你就會用到這些peer ID。

若是你有多臺設備,你就能夠建立多個server。但對於這篇教程,一個client只須要鏈接一個server就能夠了,可是他們能夠相互偵測到對方。試試吧!

到了這裏,該何去何從?


到如今爲止,全部的範例代碼都在這裏

恭喜,你如今有了一個漂亮的畫面,app中的按鈕動畫也很流暢,Host Game和Join Game界面也都基本實現。另外,也能夠用GameKit和Bonjour來廣播和偵測server了!

很是棒,可是很明顯,你要在屏幕上顯示搜索到的server,這樣用戶才能選擇你的server加入遊戲。這就是該系列教程在第二部分內容。

如何你對於本篇文件有任何問題或者評論,請在下面加入咱們的討論!

相關文章
相關標籤/搜索