轉自朋友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
這篇文章是由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的兒童遊戲。這就是你最後完成遊戲的畫面。
什麼,你還不清楚遊戲的規則!好吧,玩這個遊戲,須要2到4我的,使用52張牌,經過卡片配對的方式來贏牌,你的目標就是贏得全部牌。
在每輪開始前,都會從新洗牌,而後順時針依次發牌,直到牌發完爲止。這些牌都會正面朝下襬在玩家面前。
玩家順時針依次翻牌。若是輪到你了,翻過你最上方的牌,若是你看到翻開的牌中有能和你的牌造成配對的時候,快速大喊一聲"snap",則匹配成功。兩個張牌具備相同的值,就能匹配,好比兩張王,無論大王仍是小王。
最快速喊出"snap!"而且兩張牌確實匹配的那個玩家贏得這兩張牌,而後將這兩張牌放入本身正面朝下的那些牌中。直到一個玩家贏得了全部牌。若是玩家喊出了"snap!",可是並無可匹配的牌,那麼他要給其餘每一個玩家一張牌做爲懲罰。
這是一個經過藍牙或Wi-Fi鏈接的多人遊戲,你將使用GameKit框架來實現它。這裏只用到了GameKit的peer-to-peer鏈接的特性,並無使用Game Center相關知識,其實這個教程主要使用的只有一個類:GKsession。
在該教程的第一部分,你將學到如何鏈接玩家的設備,讓這些設備可使用Snap經過藍牙或Wi-Fi傳遞信息。玩這個遊戲呢,總要有我的先創建遊戲,做爲遊戲的"服務器",其餘玩家做爲"客戶端",加到這個已經建好的服務器中來。
這個遊戲的流程大致是下面這個樣子:
上面這張圖就是遊戲的主屏幕,打開遊戲玩家首先看到的畫面。如何你想玩一局,你能夠創建遊戲,讓其餘玩家加進來,或者加進別人創建的遊戲,還能夠一我的玩單機模式。
這個"Host Game"畫面列出了已經加進來的玩家,點擊start按鈕開始遊戲;從這一刻起,其餘沒有加進來的玩家就不能在進入到該局遊戲了。在玩遊戲前,一般玩家都約定好了誰來創建遊戲,而後其餘玩家加入。
"Join Game"畫面很像Host Game畫面,惟一的差異就是這個界面誒有開始按鈕,這個tableview列出了能夠加入的遊戲(列表裏頗有可能列了多個遊戲)。選擇一個你想加入的,而後等待主機點擊他畫面上的開始按鈕。
game screen畫面展現的是玩家們都圍着桌子坐下,桌子上拍着各自牌面朝上和朝下的牌。點擊屏幕右下角的按鈕能夠發出"snap!"的響聲(玩遊戲時,你不必讓滿屋子都是那響聲)。當有玩家按下"snap"按鈕時,該玩家暱稱旁邊會出現一個氣泡。
爲了節省你時間,我已經建好了一個帶有圖片資源和一些nib文件的項目,在這裏下載源代碼,用Xcode打開Snap.xcodeproj。
若是你看了源代碼,你會發現,項目裏只有一個view controller,MainViewController。運行項目,你會發現界面很是簡單。
這個界面有5個UIImageView對象,組成一個logo(S,N,A,P和大王),還有3個UIButton。你能夠在這些imageView上作些簡單動畫,讓其更加生動一些,哦,不過你最好先把這些按鈕弄好看些。
你下載的文件還有一個叫作Action_Man.ttf的文件。這是你將在項目中用到的字體文件,他將代替系統的標準字體Helvetica或者你iPhone的內置字體。若是你在Mac下雙擊這個文件,這個文件將會在字體庫中被打開:
若是你問我,問什麼要用這種字體,由於我以爲這種字體看起來更能讓人興奮。但不幸的是這種字體不能裝在Mac上,也就是意味着不能在Interface Builder中使用,你必須在代碼裏面進行控制。然而,首先,你須要告訴UIKit有這種字體,這樣app才能加載它。
在Xcode中打開Snap-Info.plist文件,添加一行,key選中"Fonts provided by application",值爲數組類型。把第一項設置成那個字體文件的名字Action_Man.ttf :
你還須要將那個TTF字體文件加到項目中去,將文件拖拽至Supporting Files下:
注意,你須要確保Add to Targets這一項是選中的,要否則,字體文件是不會被包含在項目中的。
如今你能夠想下面這樣給你的button和label設置字體了:
UIFont *font = [UIFont fontWithName:@"Action Man" size:16.0f]; someLabel.font = font;
爲了不代碼重複,咱們建立個類別。打開File菜單,選擇New->File…選項,而後選擇"Objcect-C category"模板。建立一個"UIFont"類的類別"SnapAdditions":
這樣將會建立兩個文件,UIFont+SnapAdditions.h
和UIFont+SnapAddtions.m
。爲了保持項目結構整潔,我將這兩個文件加進了剛建立的Categories組中。
將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]; }
如今運行項目。按鈕上應該顯示了新的字體:
若是你看到的仍然是老的字體,那麼在檢查一下Action_Man.ttf是否放入了項目中。選中文件,確保在Target membership這一項是選中的(在Xcode窗口右邊的Inspector面板中):
注意:在使用任何字體的時候都要仔細閱讀它的的許可證書,字體文件是受版權保護的,若是你想把它做爲你項目中的一部分一塊兒發佈,每每是要收取費用的, 但幸運的是Action Man字體是能夠無償使用和發佈的。
如今這些按鈕看起來好多了,可是一個好的按鈕還要有個邊框,咱們用一些拉伸的圖片來給按鈕加上邊框。這些圖片已經加在項目裏了,叫作Button.png和ButtonPressed.png。
由於這幾個不一樣的畫面都須要一樣樣式的按鈕,因此你最好把這個自定義樣式的功能放到類別中去。
給項目添加一個新的類別,叫作"SnapAdditions",可是此次類別是加在UIButton
類上。這樣就又建立了兩個文件,UIButton+SnapAddtions.h
和UIButton+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]; }
再次運行項目,如今按鈕是這個樣子的了:
如今,你應該讓主畫面活躍一些。在遊戲啓動的時候,讓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,哈哈。
僅僅這些動畫還不夠完美。我想,當卡片飛向目標位置時,讓按鈕漸漸出來,這樣效果會更好。將下面方法中的幾行代碼放在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是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
來建立設備間的鏈接。就像下面這個樣子。
GKPeerPickerController
的使用很簡單,可是隻能兩臺設備鏈接。可是Snap!須要同時四我的一塊兒玩,因此經過這篇教程來教你經過本身寫代碼實現多人鏈接。
在這部分,你將添加一個"Host Game"界面。這個界面容許玩家創建一個房間,讓其餘玩家加進來。當你完成的時候,應該是這個樣子:
這裏有個列出了鏈接到當局遊戲的玩家的列表,一個開始按鈕,還有個能夠輸入玩家暱稱的文本框(默認裏面是你機器的名字)。
添加一個UIViewController
的子類,命名爲HostViewController
。建立時不要選中"With XIB for user interface"選項。這個界面上的基本控件已經在一個xib文件中擺放好了,你能夠在"Snap/en.lproj"文件夾中找到這個xib文件。把HostViewController.xib加到項目中。這個xib文件打開後是下面這個樣子:
這些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)。運行項目看看效果吧。
如今還有點東西要改進。這裏有個讓用戶輸入名字的文本框,當用戶點擊時,在界面的最上方會彈出個鍵盤。這個鍵盤蓋住了半個屏幕,關鍵是如今尚未辦法讓它下去。
第一種讓鍵盤消失的方法:就是用那個又大又藍帶有"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界面左下角的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界面已經完成。在你創建想加入或者創建一局以前,你必須先進入遊戲界面。不然,其它設備找不到你的設備!
這個界面跟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視圖:
將這個屬性聲明爲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方式。一個玩家做爲服務器,其它玩家加進來。
在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加到項目中。
由於咱們要在不少文件中用到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":
建立一個新的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界面上的按鈕。若是你是運行在模擬器中,你將會看到下面這個界面:
GKSession中displayName屬性的值是像這樣"com.hollance.Sanp355561232..."的一串字符串,若是你在你本身的設備上運行,上面顯示的將是你設備的名字,好比:"Joe's iPhone"或者你一開始給你設備設置的名字。
你如今已經有一個運行良好,能夠廣播"Snap!"服務的server了,可是尚未client鏈接進來。如今咱們就建立一個MatchmakingClient類來實現這些。
添加一個新的類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加入遊戲。這就是該系列教程在第二部分內容。
如何你對於本篇文件有任何問題或者評論,請在下面加入咱們的討論!