譯者: Lao Jiang | 原文做者: Matthijs Hollemans寫於2012/07/13 html
轉自朋友Tommy 的翻譯,本身只翻譯了這第三篇教程。編程
這篇文章爲iOS教程團隊成員 Matthijs Hollemans,他是一位經驗豐富iOS開發者和設計者。你能夠在 Google+和 Twitter找到他。服務器
歡迎回到咱們的Monster(怪物)系列教程的第七部分--建立一個多人紙牌遊戲,經過藍牙或Wi-Fi使用UIKit!網絡
若是你剛接觸這個系列教程,先點擊介紹.在那裏,你能夠看到一個遊戲的視頻,咱們會邀請您爲咱們的特殊的挑戰者!session
在系列教程第一篇,你建立了主菜單和主機的基礎內容,並添加了遊戲場景。app
在系列教程第二篇,你實現了鏈接/主機的遊戲邏輯,優雅的完成斷線處理。異步
在系列教程的第三篇,咱們將要實現客戶端和服務器相互通訊的功能。此外,咱們將建立咱們的遊戲的 model 類,這個遊戲的設置代碼,或更多。讓咱們回到遊戲建立中。編輯器
當玩家點擊遊戲界面上的「開始按鈕」承載回話時,這個遊戲開始。而後這個服務器會向客戶端發送消息--這些數據包經過藍牙或WiFi網絡傳輸-指導客戶端作好準備。函數
這些網絡數據包被「data receive handler」(數據接收處理)接收。你必須給GKSession一個處理任何傳入的包的對象。這有點像一個delegate,可是它不具備本身的@protocol(協議)。
小訣竅: 在比賽以前,這個服務器須要發送給客戶端一堆信息,但你並不真的想要讓JoinViewController繼承GKSession方法處理數據接收。遊戲的主要邏輯將被"Data Model"文件夾下的Game類處理,你想讓這個Game類繼承GKSession處理數據包的協議。所以,在客戶端上,只要客戶端鏈接到服務器,遊戲就開始。(若是對你來講沒有任何意義,它會很快顯現)。
添加如下方法到MatchmakingClient類的delegate protocol中:
- (void)matchmakingClient:(MatchmakingClient *)client didConnectToServer:(NSString *)peerID;
只要客戶端鏈接到服務器,你會調用此方法。添加以下代碼到MatchmakingClient.m類中的- (void)session:(GKSession )session peer:(NSString )peerID didChangeState:(GKPeerConnectionState)state{}中
// We're now connected to the server. case GKPeerStateConnected: if (_clientState == ClientStateConnecting) { _clientState = ClientStateConnected; [self.delegate matchmakingClient:self didConnectToServer:peerID]; } break;
該delegate(委託)方法應該由JoinViewController實施,因此它添加:
- (void)matchmakingClient:(MatchmakingClient *)client didConnectToServer:(NSString *)peerID { NSString *name = [self.nameTextField.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; if ([name length] == 0) name = _matchmakingClient.session.displayName; [self.delegate joinViewController:self startGameWithSession:_matchmakingClient.session playerName:name server:peerID]; }
這裏你第一次從文本框中(剔出任何空格)獲得玩家名稱。而後你調用一個新的委託方法讓MainViewController知道它不得不爲這個客戶端開始遊戲。
添加新的委託方法聲明 到JoinViewControllerDelegate
- (void)joinViewController:(JoinViewController *)controller startGameWithSession:(GKSession *)session playerName:(NSString *)name server:(NSString *)peerID;
請注意,你已經爲MainViewController添加了3個重要的數據塊,GKSession :新建遊戲類與服務器通訊時使用的;玩家名字;服務器的peerID(你能夠從GKSession對象中獲取這個服務器的peer ID,這個很容易)。
這個方法在 MainViewController.m中主要實現以下:
- (void)joinViewController:(JoinViewController *)controller startGameWithSession:(GKSession *)session playerName:(NSString *)name server:(NSString *)peerID { _performAnimations = NO; [self dismissViewControllerAnimated:NO completion:^ { _performAnimations = YES; [self startGameWithBlock:^(Game *game) { [game startClientGameWithSession:session playerName:name server:peerID]; }]; }]; }
這些是什麼意思?讓咱們從這個 _performAnimations 變量講起。這是一個新的實例變量,須要將其添加到源文件的頂部(MainViewController.h):
@implementation MainViewController { . . . BOOL _performAnimations; }
記得主屏幕上的很酷的動畫效果嗎?logo卡牌飛入屏幕和按鈕淡入效果。那個動畫在主屏幕上任什麼時候間可見出現,包括一個modally-presented(模態呈現)view controller(視圖控制器)關閉。
不過,當開始一個新的遊戲時,你不想在主場景作任何動畫。你要當即從Join Game screen.切換到實際遊戲畫面,全部動做發生。這個_performAnimations變量簡單控制着卡片飛動動畫是否發生。
設置_performAnimations默認值爲YES,在MainViewController.m文件中的initWithNibName設置:
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { if ((self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil])) { _performAnimations = YES; } return self; }
在viewWillAppear: 和 viewDidAppear: 方法中添加以下判斷:
- (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; if (_performAnimations) [self prepareForIntroAnimation]; } - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; if (_performAnimations) [self performIntroAnimation]; }
返回解釋先前添加的代碼。當startClientGameWithSession…方法被調用時候,你經過設置performAnimations爲NO來禁止這個動畫。而後你離開這個Join Game screen.,重設置performAnimations爲YES,並作下面操做:
[self startGameWithBlock:^(Game *game) { [game startClientGameWithSession:session playerName:name server:peerID]; }];
這可能須要更多的解釋。你要作一個新的遊戲類,做爲本場比賽的數據模型(data model),和一個GameViewController類來管理遊戲畫面。
開始一個新遊戲有三種方式:連服務器,一對一,或者在單人遊戲模式下。這三種類型的遊戲的主要設置是同樣的,除了這個遊戲對象如何初始化上面。startGameWithBlock: 處理着全部的共享細節,而且你傳給這個block的東西是特定類型的遊戲。
在這個例子中,由於你是客戶端,你在這個遊戲對象上調用startClientGameWithSession:playerName:server:讓它開始。可是在此以前,你必須先寫一些新的遊戲對象和GameViewController。
在startGameWithBlock:中加入
- (void)startGameWithBlock:(void (^)(Game *))block { GameViewController *gameViewController = [[GameViewController alloc] initWithNibName:@"GameViewController" bundle:nil]; gameViewController.delegate = self; [self presentViewController:gameViewController animated:NO completion:^ { Game *game = [[Game alloc] init]; gameViewController.game = game; game.delegate = gameViewController; block(game); }]; }
還未完成,可是你能夠看到什麼是應該作的:分配GameViewController,實現它,讓後分配遊戲對象。最後調用你的block作遊戲類型特定的初始化。
雖然這些文件不存在,先在MainViewController.h中導入它們。
#import "GameViewController.h"
在MainViewController.m:中導入:
#import "Game.h"
這以後,你要添加這個仍然是虛擬的 GameViewControllerDelegate到MainViewController’s @interface:
@interface MainViewController : UIViewController <HostViewControllerDelegate, JoinViewControllerDelegate, GameViewControllerDelegate>
如今添加一個新的Objective-C類到項目中,並繼承UIViewController,命名爲GameViewController。沒有Nib是必要的-它已經被第一部分的下載啓動代碼提供。從「Snap/en.lproj/」 文件夾下拖動 GameViewController.xib 到這個項目中中。用下面的內容替換GameViewController.h下的:
#import "Game.h" @class GameViewController; @protocol GameViewControllerDelegate <NSObject> - (void)gameViewController:(GameViewController *)controller didQuitWithReason:(QuitReason)reason; @end @interface GameViewController : UIViewController <UIAlertViewDelegate, GameDelegate> @property (nonatomic, weak) id <GameViewControllerDelegate> delegate; @property (nonatomic, strong) Game *game; @end
很是簡單。你聲明瞭一個新的委託協議GameViewControllerDelegate,其中只有一個方法用來讓MainViewController知道遊戲應該結束。這個GameViewController自身是這個委託的遊戲對象。許多委託隨處可見。
初版本的 GameViewController.xib,你用起來很是簡單。它在屏幕中只有一個exit按鈕和一個單獨的Label。
替代以下內容到 GameViewController.m中:
#import "GameViewController.h" #import "UIFont+SnapAdditions.h" @interface GameViewController () @property (nonatomic, weak) IBOutlet UILabel *centerLabel; @end @implementation GameViewController @synthesize delegate = _delegate; @synthesize game = _game; @synthesize centerLabel = _centerLabel; - (void)dealloc { #ifdef DEBUG NSLog(@"dealloc %@", self); #endif } - (void)viewDidLoad { [super viewDidLoad]; self.centerLabel.font = [UIFont rw_snapFontWithSize:18.0f]; } - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation { return UIInterfaceOrientationIsLandscape(interfaceOrientation); } #pragma mark - Actions - (IBAction)exitAction:(id)sender { [self.game quitGameWithReason:QuitReasonUserQuit]; } #pragma mark - GameDelegate - (void)game:(Game *)game didQuitWithReason:(QuitReason)reason { [self.delegate gameViewController:self didQuitWithReason:reason]; } @end
這裏沒有什麼太使人興奮的。exitAction:告訴退出遊戲對象,和遊戲對象經過調用game:didQuitWithReason:而響應。MainViewController爲GameViewController的委託,因此你應該實現gameViewController:didQuitGameWithReason::
- (void)gameViewController:(GameViewController *)controller didQuitWithReason:(QuitReason)reason { [self dismissViewControllerAnimated:NO completion:^ { if (reason == QuitReasonConnectionDropped) { [self showDisconnectedAlert]; } }]; }
這看起來也很是熟悉。你關閉遊戲畫面,若是有必要顯示一個警告彈出框。
這些措施,只是讓遊戲對象被實施。
正如我以前提到的,這個Game類是遊戲中主要數據模型對象。它也處理從GKSession傳入的網絡數據包。你要爲這個類創建一個基本的版原本讓這個遊戲開始。縱觀本系列的其他部分,你會擴大Game類,和GameViewController直到Snap全面完成。
添加到一個新的Objective-C類到這個項目中,繼承NSObject,命名爲Game。我建議你把Game.h和Game.m文件移動到一個新建組叫作「Data Model」.替換Game.h文件內容以下:
@class Game; @protocol GameDelegate <NSObject> - (void)game:(Game *)game didQuitWithReason:(QuitReason)reason; @end @interface Game : NSObject <GKSessionDelegate> @property (nonatomic, weak) id <GameDelegate> delegate; @property (nonatomic, assign) BOOL isServer; - (void)startClientGameWithSession:(GKSession *)session playerName:(NSString *)name server:(NSString *)peerID; - (void)quitGameWithReason:(QuitReason)reason; @end
這裏你聲明GameDelegate protocol,你已經在GameViewController中看到這個角色,和Game類。迄今爲止,你只爲這個類添加了startClientGameWithSession… 方法和一個委託和isServer屬性。
替換Game.m內容以下:
#import "Game.h" typedef enum { GameStateWaitingForSignIn, GameStateWaitingForReady, GameStateDealing, GameStatePlaying, GameStateGameOver, GameStateQuitting, } GameState; @implementation Game { GameState _state; GKSession *_session; NSString *_serverPeerID; NSString *_localPlayerName; } @synthesize delegate = _delegate; @synthesize isServer = _isServer; - (void)dealloc { #ifdef DEBUG NSLog(@"dealloc %@", self); #endif } #pragma mark - Game Logic - (void)startClientGameWithSession:(GKSession *)session playerName:(NSString *)name server:(NSString *)peerID { } - (void)quitGameWithReason:(QuitReason)reason { } #pragma mark - GKSessionDelegate - (void)session:(GKSession *)session peer:(NSString *)peerID didChangeState:(GKPeerConnectionState)state { #ifdef DEBUG NSLog(@"Game: peer %@ changed state %d", peerID, state); #endif } - (void)session:(GKSession *)session didReceiveConnectionRequestFromPeer:(NSString *)peerID { #ifdef DEBUG NSLog(@"Game: connection request from peer %@", peerID); #endif [session denyConnectionFromPeer:peerID]; } - (void)session:(GKSession *)session connectionWithPeerFailed:(NSString *)peerID withError:(NSError *)error { #ifdef DEBUG NSLog(@"Game: connection with peer %@ failed %@", peerID, error); #endif // Not used. } - (void)session:(GKSession *)session didFailWithError:(NSError *)error { #ifdef DEBUG NSLog(@"Game: session failed %@", error); #endif } #pragma mark - GKSession Data Receive Handler - (void)receiveData:(NSData *)data fromPeer:(NSString *)peerID inSession:(GKSession *)session context:(void *)context { #ifdef DEBUG NSLog(@"Game: receive data from peer: %@, data: %@, length: %d", peerID, data, [data length]); #endif } @end
這是最低限度的,你須要作的就是從新編譯。Game類中沒有哪一個方法是作任何有用的操做。還注意到你聲明瞭新的枚舉(enum),GameState,它包含遊戲所佔據的不一樣狀態。稍後更多關於遊戲的狀態。
再次運行該應用。當你客戶端鏈接到服務器時,你將簡要見到「Connecting…」信息(只要創建與服務器的鏈接,這一般只有幾分之一秒),而後應用程序切換到遊戲畫面。你不會注意到太大的區別,由於佈局保持大體相同的(目的),除了主要的label如今說「Center Label,」,而且它是白色的替代了綠色。
還要注意,客戶端從服務器的表視圖中消失。這是由於在客戶端上,關閉了JoinViewController,也就釋放了MatchmakingClient對象。這個對象是惟一一個保留GKSession對象的,因此纔會被釋放,以及只要你離開Join Game screen.鏈接當即打破。(你能夠驗證服務器的調試輸出;客戶端的狀態變爲3,這個是GKPeerStateDisconnected)。
你會解決這個問題。
替換Game.m’s startClientGameWithSession… 方法以下:
- (void)startClientGameWithSession:(GKSession *)session playerName:(NSString *)name server:(NSString *)peerID { self.isServer = NO; _session = session; _session.available = NO; _session.delegate = self; [_session setDataReceiveHandler:self withContext:nil]; _serverPeerID = peerID; _localPlayerName = name; _state = GameStateWaitingForSignIn; [self.delegate gameWaitingForServerReady:self]; }
這個方法控制GKSession 對象,並使「self」賦予delegate,即遊戲對象,稱爲新的GKSessionDelegate以及數據接收處理程序。(你已經添加了這些委託的方法,但它們如今仍是空的)。
你能夠複製服務器的 peer ID和玩家的名字到你本身的實例變量中,而後這隻遊戲狀態爲「waiting for sign-in.」 這意味着客戶端如今將等待服務器的特定訊息。最後,你告訴這個GameDelegate你已經準備好遊戲開始。這採用了一種新的委託方法,因此添加到Game.h, GameDelegate @protocol:
- (void)gameWaitingForServerReady:(Game *)game;
Game的委託是 GameViewController,因此你應該實現這個方法:
- (void)gameWaitingForServerReady:(Game *)game { self.centerLabel.text = NSLocalizedString(@"Waiting for game to start...", @"Status text: waiting for server"); }
這是它所作的一塊兒。它取代了中間label文字「Waiting for game to start…」。再次運行這個應用程序。將客戶端鏈接後,你應該看到這一點:
因爲你如今保持這個GKSession對象 在Join Game screen 關閉後還活着,服務器的表視圖中仍然顯示客戶端的設備名稱。
在這一點上還未爲客戶作太多東西,但你至少製做exit button 。在Game.m中替換以下方法:
- (void)quitGameWithReason:(QuitReason)reason { _state = GameStateQuitting; [_session disconnectFromAllPeers]; _session.delegate = nil; _session = nil; [self.delegate game:self didQuitWithReason:reason]; }
儘管Game在服務器上尚未真正開始,你仍然能夠退出。在服務器上看來就像是一個斷開,它會從其表視圖中刪除客戶。
你已經完成在GameViewController裏的game:didQuitWithReason: 方法,該方法能夠致使應用程序返回到主屏幕。由於你把一些NSLogging放到dealloc 方法中,你能夠在Xcode的debug輸出窗格中看看一切都被正確釋放。測試吧!
在服務器上開始遊戲,跟你剛纔作的沒有什麼太大的不一樣。從主機遊戲畫面點擊Start button。對此你應該建立一個遊戲對象,啓動GameViewController。
HostViewController有一個startAction: 方法與 Start button綁定.眼下此方法不起做用。將其替換爲:
- (IBAction)startAction:(id)sender { if (_matchmakingServer != nil && [_matchmakingServer connectedClientCount] > 0) { NSString *name = [self.nameTextField.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; if ([name length] == 0) name = _matchmakingServer.session.displayName; [_matchmakingServer stopAcceptingConnections]; [self.delegate hostViewController:self startGameWithSession:_matchmakingServer.session playerName:name clients:_matchmakingServer.connectedClients]; } }
這個Start button只會在一個有效MatchmakingServer的對象下工做(這是一般狀況下,除非沒有 Wi-Fi或Bluetooth),並至少有一個鏈接的客戶端-本身玩沒有樂趣!當這些條件都知足時,你可從 text field中獲得玩家的名字,告訴MatchmakingServer不要接收任何新客戶,並告訴委託 (MainViewController) ,它應該啓動一個服務器遊戲。
stopAcceptingConnections是新的,因此把它添加到MatchmakingServer.h:
- (void)stopAcceptingConnections;
添加到 MatchmakingServer.m:
- (void)stopAcceptingConnections { NSAssert(_serverState == ServerStateAcceptingConnections, @"Wrong state"); _serverState = ServerStateIgnoringNewConnections; _session.available = NO; }
這不像endSession,這不推倒GKSession對象。它只是移動MatchmakingServer到「ignoring new connections」 狀態,因此當 GKPeerStateConnected 或 GKPeerStateDisconnected發生回調時 它再也不接收新的鏈接。設置GKSession的可用屬性爲NO也意味着服務器的存在再也不廣播。
startAction: 也被稱爲一個新的委託方法,因此加其簽名到HostViewController.h:
- (void)hostViewController:(HostViewController *)controller startGameWithSession:(GKSession *)session playerName:(NSString *)name clients:(NSArray *)clients;
並在MainViewController.m文件中實現此方法:
- (void)hostViewController:(HostViewController *)controller startGameWithSession:(GKSession *)session playerName:(NSString *)name clients:(NSArray *)clients { _performAnimations = NO; [self dismissViewControllerAnimated:NO completion:^ { _performAnimations = YES; [self startGameWithBlock:^(Game *game) { [game startServerGameWithSession:session playerName:name clients:clients]; }]; }]; }
這與你爲客戶作的什麼很是類似的,除非你在Game 類中調用了一個新的方法。添加此方法到Game.h:
- (void)startServerGameWithSession:(GKSession *)session playerName:(NSString *)name clients:(NSArray *)clients;
也添加到Game.m:
- (void)startServerGameWithSession:(GKSession *)session playerName:(NSString *)name clients:(NSArray *)clients { self.isServer = YES; _session = session; _session.available = NO; _session.delegate = self; [_session setDataReceiveHandler:self withContext:nil]; _state = GameStateWaitingForSignIn; [self.delegate gameWaitingForClientsReady:self]; }
你作的客戶端遊戲有不少一樣地事情,除了你設置isServer 屬性爲YES,並你調用另外一個GameDelegate方法。添加這個方法到Game.h @protocol 中:
- (void)gameWaitingForClientsReady:(Game *)game;
在GameViewController.m中實施:
- (void)gameWaitingForClientsReady:(Game *)game { self.centerLabel.text = NSLocalizedString(@"Waiting for other players...", @"Status text: waiting for clients"); }
這就能夠了! 如今,當你在主機上按下啓動按鈕,HostViewController獲得謹慎解僱,和GameViewController出現:
如今你的應用程序能夠鏈接一個服務器(主機)到過個客戶端。可是這些設備只是坐等待主機開始遊戲的客戶端。玩家沒有什麼能夠作的,除了點擊退出按鈕。由於你已經實現了exitAction:,而且客戶端和服務器分享在 Game 和 GameViewController大部分的代碼,在服務端點擊退出按鈕應該結束遊戲。
注意:當你退出服務器時,客戶端可能須要幾秒鐘才認識到服務器已斷開。它也將繼續停留在「Waiting for game to start…」 屏幕,由於你尚未在Game類中實施任何斷線邏輯。 Disconnection logic(斷線的邏輯)被用來處理 MatchmakingServer 和 MatchmakingClient 類,但如今你已經開始遊戲,這些對象已經達到目的,並再也不使用。這個Game對象已經接管了GKSessionDelegate的職責。
這是個很好的時機來談論這個遊戲的數據模型。由於你使用的是UIKit(而不是Cocos2D 或 OpenGL),遊戲中使用Model-View-Controller (MVC) 模式結構是有道理的。
cocos2d遊戲的一種常見方法是引入該類別的CCSprite的子類,並把你的遊戲對象邏輯放到這個類中。在這裏你作的事情有點不一樣:你會把model, view, and view controller嚴格分離。
注意:它可能不會使全部的遊戲使用MVC而變得有意義,但它適合爲卡牌和棋牌遊戲。你能夠在model類中捕捉遊戲規則,從任何表示邏輯中分離。這樣作的好處,是讓你能夠輕鬆地單元測試這些遊戲規則,以確保它們始終是正確的,雖然你在本教程中會跳過。
Game是data model(數據模型)的一部分。它處理遊戲規則,已經在客戶機之間的網絡流量和服務器(它既是GKSession的委託又是數據接收處理者)。可是Game並非惟一的數據模型對象。這裏還有其它幾個:
你見過Game和GameViewController類,可是其餘類都是新的,你將在本教程的過程當中把它們添加到項目中。參加遊戲的玩家被Player對象表示。每位玩家都有成堆的卡,都是從Deck繪製出的。這些都是模型對象。
卡牌被CardView對象繪製在屏幕上。全部其它的視圖:UILabels ,UIButtons ,UIImageViews。對於網絡通訊,Game使用GKSession來發送和接收數據包對象,它表明在不一樣的設備之間的網絡發送的一個消息。
首先建立這個Player對象。在項目中添加一個新的Objective-C 類,繼承自NSObject,命名爲Player。因爲這是一個數據模型類,把它添加到數據模型組內。替換 Player.h內容以下:
typedef enum { PlayerPositionBottom, // the user PlayerPositionLeft, PlayerPositionTop, PlayerPositionRight } PlayerPosition; @interface Player : NSObject @property (nonatomic, assign) PlayerPosition position; @property (nonatomic, copy) NSString *name; @property (nonatomic, copy) NSString *peerID; @end
#import "Player.h" @implementation Player @synthesize position = _position; @synthesize name = _name; @synthesize peerID = _peerID; - (void)dealloc { #ifdef DEBUG NSLog(@"dealloc %@", self); #endif } - (NSString *)description { return [NSString stringWithFormat:@"%@ peerID = %@, name = %@, position = %d", [super description], self.peerID, self.name, self.position]; } @end
你如今保持它簡單。玩家的三個不一樣方式的屬性能夠識別每位玩家。 1. 經過它們的名字。這是你要展示在用戶面前的,但它不能保證是惟一的。這個名字是玩家在Host Game 或Join Game screens時輸入的名字(若是他們沒有輸入任何名字,你會在這裏使用設備名稱)。
這是不一樣的玩家如何看本身和其餘玩家坐在桌子周圍的位置:
遊戲對象如今已經進入其初始狀態,GameStateWaitingForSignIn,同時在客戶端和服務器。在「waiting for sign-in」狀態時,服務器將發送消息給全部的客戶端,要求他們迴應它們本地玩家的名字。
到目前爲止,服務器只知道哪些客戶端鏈接和它們的peer IDs和設備名稱,但它不知道任何關於用戶輸入Your Name」 輸入框的名稱。一旦服務器知道了每一個人的名字,它能夠告訴全部的客戶端的其餘玩家。
添加Player 到Game.h:
#import "Player.h"
在Game.m:中添加新的實例變量
@implementation Game { . . . NSMutableDictionary *_players; }
你將把Player對象裝到一個字典對象上。爲了使它容易地經過peer IDs看到玩家,你就會使用 peer ID 關鍵字。字典須要立刻分配,因此添加一個init方法到Game類中。
- (id)init { if ((self = [super init])) { _players = [NSMutableDictionary dictionaryWithCapacity:4]; } return self; }
在服務器上啓動遊戲的方法是 startServerGameWithSession:playerName:clients:,你已經在這裏作了一些東西設置遊戲。添加以下代碼到該方法底部:
- (void)startServerGameWithSession:(GKSession *)session playerName:(NSString *)name clients:(NSArray *)clients { . . . // Create the Player object for the server. Player *player = [[Player alloc] init]; player.name = name; player.peerID = _session.peerID; player.position = PlayerPositionBottom; [_players setObject:player forKey:player.peerID]; // Add a Player object for each client. int index = 0; for (NSString *peerID in clients) { Player *player = [[Player alloc] init]; player.peerID = peerID; [_players setObject:player forKey:player.peerID]; if (index == 0) player.position = ([clients count] == 1) ? PlayerPositionTop : PlayerPositionLeft; else if (index == 1) player.position = PlayerPositionTop; else player.position = PlayerPositionRight; index++; } }
首先,你爲服務器建立Player對象,並將其放置在屏幕位置的底部。而後你遍歷全部鏈接的客戶端peer IDs 數組,和爲它們建立Player對象。你以順時針方向的順序指定客戶端玩家的位置,這取決於總共有多少玩家。請注意,你不爲這些Plaer對象設置「name」屬性,由於在這一點上,你不知道客戶的名字。
如今,每一個客戶端都擁有一個Plaer對象,你能夠發送「sign-in」請求到每一個客戶端。每一個客戶端將與它們的名字異步響應。收到這樣的響應,你會看到那個客戶端的Player對象,並根據客戶端返回給你的內容設置它的「name」屬性的名稱。
GKSession有個方法叫作sendDataToAllPeers:withDataMode:error:將會發送NSData對象的內容到全部鏈接的peers(同齡人)。你可使用這種方法從服務器來發送一個單一的消息到全部客戶端。在這種狀況下,該消息是一個NSData對象,而且NSData對象裏面是什麼徹底取決於你。在Snap中!,全部的消息格式以下:
一個數據包至少是10個字節。這10個字節被稱爲 「header,」,和任何(可選)字節,能夠按照 「payload.」 (有效負荷)。不一樣類型的包具備不一樣的payloads,可是它們都具備相同的header 結構:
對於某些類型的數據包,也有多是更多的header(the payload)後面的數據。客戶端返回給服務器 「sign-in response」 包,例如,包含玩家名稱的一個 UTF-8 字符串。
這一切都好,但你想要在一個更好的界面下抽象這種底層東西。你要去建立一個Packet類,能夠在場景後面處理bits-and-bytes 。在項目中添加一個新的Objective-C 類,繼承自NSObject,命名爲Packet。爲了保持整潔,放置在「Networking」 組別中。
在 Packet.h 中替換以下內容:
typedef enum { PacketTypeSignInRequest = 0x64, // server to client PacketTypeSignInResponse, // client to server PacketTypeServerReady, // server to client PacketTypeClientReady, // client to server PacketTypeDealCards, // server to client PacketTypeClientDealtCards, // client to server PacketTypeActivatePlayer, // server to client PacketTypeClientTurnedCard, // client to server PacketTypePlayerShouldSnap, // client to server PacketTypePlayerCalledSnap, // server to client PacketTypeOtherClientQuit, // server to client PacketTypeServerQuit, // server to client PacketTypeClientQuit, // client to server } PacketType; @interface Packet : NSObject @property (nonatomic, assign) PacketType packetType; + (id)packetWithType:(PacketType)packetType; - (id)initWithType:(PacketType)packetType; - (NSData *)data; @end
上面枚舉類型包含了一個全部你發送和接收的不一樣信息類型的列表。在這一點,這個Packet類自己很是簡單:它有一個方便的構造函數和設置數據包類型的init方法。這個"data"方法返回一個有關這封郵件內容消息的新NSData對象。這個NSData對象是你經過GKSession發送到其餘設備的。
在Packet.m 中替換內容以下:
#import "Packet.h" #import "NSData+SnapAdditions.h" @implementation Packet @synthesize packetType = _packetType; + (id)packetWithType:(PacketType)packetType { return [[[self class] alloc] initWithType:packetType]; } - (id)initWithType:(PacketType)packetType { if ((self = [super init])) { self.packetType = packetType; } return self; } - (NSData *)data { NSMutableData *data = [[NSMutableData alloc] initWithCapacity:100]; [data rw_appendInt32:'SNAP']; // 0x534E4150 [data rw_appendInt32:0]; [data rw_appendInt16:self.packetType]; return data; } - (NSString *)description { return [NSString stringWithFormat:@"%@, type=%d", [super description], self.packetType]; } @end
這裏有趣的是這個data方法。它分配一個NSMutableData對象,讓後把它裏面放置兩個32位整數和一個16位整數。這是我前面提到的10個字節的header。第一部分是單詞「SNAP,」,第二部分是數據包數———時間你將保持爲0,第三部分是數據包類型。
從這些「rw_appendIntXX」 方法名稱來看,你已經能夠推斷它們來自一個類別。添加一個新的Objective-C類別文件到項目中。命名這個類別爲「SnapAdditions」並使其在NSData (不是NSMutableData!)。
你欺騙了一下,在這裏,其實是講類在NSMutableData。由於你稍後須要一個相似NSData類別,你把它們都放到同一個源文件下。在NSData+SnapAdditions.h 中替換內容以下:
@interface NSData (SnapAdditions) @end @interface NSMutableData (SnapAdditions) - (void)rw_appendInt32:(int)value; - (void)rw_appendInt16:(short)value; - (void)rw_appendInt8:(char)value; - (void)rw_appendString:(NSString *)string; @end
你如今留下了空的NSData類別,並增長了第二類NSMutableData、正如你所見,有添加大小不一樣整數的方法,以及添加一個NSString的方法。在NSData+SnapAdditions.m替換內容以下:
#import "NSData+SnapAdditions.h" @implementation NSData (SnapAdditions) @end @implementation NSMutableData (SnapAdditions) - (void)rw_appendInt32:(int)value { value = htonl(value); [self appendBytes:&value length:4]; } - (void)rw_appendInt16:(short)value { value = htons(value); [self appendBytes:&value length:2]; } - (void)rw_appendInt8:(char)value { [self appendBytes:&value length:1]; } - (void)rw_appendString:(NSString *)string { const char *cString = [string UTF8String]; [self appendBytes:cString length:strlen(cString) + 1]; } @end
這些方法都很是類似,但仔細看看 rw_appendInt32::
- (void)rw_appendInt32:(int)value { value = htonl(value); [self appendBytes:&value length:4]; }
在最後一行,你調用 [self appendBytes:length:] 以添加「value」 變量的內存長度,4個字節,NSMutableData對象。但在這以前,你使「value」調用htonl() 函數。這樣作是確保整型值老是以「network byte order,」表示,這剛好是大端。然而,處理器將運行這個程序, x86 和 ARM CPUs,使用小尾數法。
你能夠發送 「value」 變量的內存內容,但誰知道,一個新型號iPhone可能在將來使用不一樣的字節順序,而後一個設備發送和另外一個接收可能有不兼容的結構。
出於這個緣由,它老是一個好主意,當處理數據傳輸時決定一個特定的字節順序,網絡編程應該是大端。若是在發送以前,你只是簡單的爲32位整數調用htonl() 函數,和爲16位整數調用htons() 函數,那麼你應該始終ok.
另外注意的是rw_appendString:,首先轉換NSString爲UTF-8,而後把它添加到NSMutableData對象上,包括在結束時一個NUL字節以記念結束的字符串。
返回到Game類,在startServerGameWithSession…方法底部添加如下幾行:
- (void)startServerGameWithSession:(GKSession *)session playerName:(NSString *)name clients:(NSArray *)clients { . . . Packet *packet = [Packet packetWithType:PacketTypeSignInRequest]; [self sendPacketToAllClients:packet]; }
固然,這還不能編譯。首先添加所需的導入:
#import "Packet.h"
而後添加這個 sendPacketToAllClients: 方法:
#pragma mark - Networking - (void)sendPacketToAllClients:(Packet *)packet { GKSendDataMode dataMode = GKSendDataReliable; NSData *data = [packet data]; NSError *error; if (![_session sendDataToAllPeers:data withDataMode:dataMode error:&error]) { NSLog(@"Error sending data to clients: %@", error); } }
太好了,如今你能夠再次運行程序,並在服務器和客戶端之間發送一些消息。主持一個新的遊戲,加入一個或多個客戶短,並觀看調試輸出窗格中。在客戶端,它應該顯示這些:
Game: receive data from peer: 1995171355, data: <534e4150 00000000 0064>, length: 10
這個輸出來自GKSession 的data-receive-handler(數據接收處理程序)方法receiveData:fromPeer:inSession:context:。你還未在這個方法中作任何事情,但至少它會記錄它收到來自服務器的消息。實際的數據,是你用GKSession發送到服務器上的NSData對象,是這樣的:
534e4150 00000000 0064
這是10個字節長,可是以十六進制記數法表示。若是你的進制記數法有點生疏,下載 Hex Fiend或相似的十六進制編輯器,簡單地複製粘貼以上內容:
這代表數據包確實以字SNAP開始(Big Endian字節序,或0x534E4150),其後由四個0字節(你尚未使用的數據包數),而後是16位的數據包類型。對於可讀性的緣由,我給了第一個數據包類型,PacketTypeSignInRequest,值爲0×64 (你能夠在Packet.h文件下看到),因此它在十六進制數據下很容易發現。
酷,因此你組織發送一條信息給客戶端。如今客戶端有迴應。
我幾回提到 GKSession的 data-receive-handler (數據接收處理程序)方法。該方法的參數之一是一個新的NSData對象,由於它是從發送方收到的二進制消息內容。這種方法中你將NSData對象返過來到一個數據包種,看數據包什麼類型,並決定用它作什麼。
在Game.m中,替換以下方法:
- (void)receiveData:(NSData *)data fromPeer:(NSString *)peerID inSession:(GKSession *)session context:(void *)context { #ifdef DEBUG NSLog(@"Game: receive data from peer: %@, data: %@, length: %d", peerID, data, [data length]); #endif Packet *packet = [Packet packetWithData:data]; if (packet == nil) { NSLog(@"Invalid packet: %@", data); return; } [self clientReceivedPacket:packet]; }
你仍是傳入數據記錄,而後調用一個新的便捷構造Packet-packetWithData:- 轉變NSData到一個新的Packet對象。 這並非總起做用。例如,若是傳入數據不是以 ‘SNAP’ header開頭的,而後你打印一個錯誤。可是,若是你有一個有效的數據包對象,你傳遞給另外一個新的方法,clientReceivedPacket:。
首先爲Packet類添加方便的構造函數。添加新的方法到Packet.h中:
+ (id)packetWithData:(NSData *)data;
對於 Packet.m,首先在@implementation下面一行添加以下:
+ (id)packetWithData:(NSData *)data { if ([data length] < PACKET_HEADER_SIZE) { NSLog(@"Error: Packet too small"); return nil; } if ([data rw_int32AtOffset:0] != 'SNAP') { NSLog(@"Error: Packet has invalid header"); return nil; } int packetNumber = [data rw_int32AtOffset:4]; PacketType packetType = [data rw_int16AtOffset:8]; return [Packet packetWithType:packetType]; }
首先,你驗證的數據至少是10個字節。若是它是任何較小的,就有錯誤的。大多數的時候,你會在 「reliable」 模式下發送包,這意味着它們保證徹底相同的內容正如你發送的,因此你沒必要擔憂在傳輸過程當中任何位「falling over」(翻倒)。
可是不管如何,作一些合理性檢查是很好的,做爲防護性編程的一種形式。畢竟有人說,一些「流氓」的客戶端不會發送不一樣的信息以欺騙咱們?出於一樣的緣由,你檢查第一個32位整數真正的表明字SNAP。
注意:這個詞 ‘SNAP’ 可能看起來像字符串,但它不是。它也不是一個單一的字符,而是被稱爲四字符代碼,或 簡稱「fourcc」。不少網絡協議和文件格式都使用這樣的32位代碼,以代表本身的格式。爲了好玩,在 Hex Fiend中打開一些隨機的文件。你將會看到它們常常以一個fourcc開始。
Xcode不知道有關這些新的rw_intXXAtOffset:方法,因此將它們添加到NSData 類別。首先NSData+SnapAdditions.h:
@interface NSData (SnapAdditions) - (int)rw_int32AtOffset:(size_t)offset; - (short)rw_int16AtOffset:(size_t)offset; - (char)rw_int8AtOffset:(size_t)offset; - (NSString *)rw_stringAtOffset:(size_t)offset bytesRead:(size_t *)amount; @end
這時候你要添加方法到NSData,而不是NSMutableData。畢竟GKSession的data-receive-handler(數據接收處理程序)只接收一個不變的NSData對象。把方法自己放到NSData+SnapAdditions.m:
@implementation NSData (SnapAdditions) - (int)rw_int32AtOffset:(size_t)offset { const int *intBytes = (const int *)[self bytes]; return ntohl(intBytes[offset / 4]); } - (short)rw_int16AtOffset:(size_t)offset { const short *shortBytes = (const short *)[self bytes]; return ntohs(shortBytes[offset / 2]); } - (char)rw_int8AtOffset:(size_t)offset { const char *charBytes = (const char *)[self bytes]; return charBytes[offset]; } - (NSString *)rw_stringAtOffset:(size_t)offset bytesRead:(size_t *)amount { const char *charBytes = (const char *)[self bytes]; NSString *string = [NSString stringWithUTF8String:charBytes + offset]; *amount = strlen(charBytes + offset) + 1; return string; } @end
不像NSMutableData中的「append」方法同樣每次你調用它們時更新一個寫指針,這些讀方法不自動更新讀指針,這將是最方便的解決方案,但類別不能在類中添加新數據成員。
相反,你須要經過你想讀的字節偏移量。請注意,這些方法假設數據是網絡字節順序byte-order(大端),所以使用 ntohl() 和ntohs() 函數來將它們轉換回主機字節順序。
rw_stringAtOffset:bytesRead: 特別值得一提,由於它返回參數中是一個可讀的字節數。整型的方法,你已經知道你會讀多少個字節,但這個數字能夠是任何字符串。( bytesRead參數包含讀取的字節數,包括終止字符串NUL字節 )。
剩下在Game.m中實施 clientReceivedPacket:
- (void)clientReceivedPacket:(Packet *)packet { switch (packet.packetType) { case PacketTypeSignInRequest: if (_state == GameStateWaitingForSignIn) { _state = GameStateWaitingForReady; Packet *packet = [PacketSignInResponse packetWithPlayerName:_localPlayerName]; [self sendPacketToServer:packet]; } break; default: NSLog(@"Client received unexpected packet: %@", packet); break; } }
你只需看看數據包類型,而後決定如何處理它。對於PacketTypeSignInRequest-咱們目前擁有的惟一類型-你改變了遊戲狀態爲「waiting for ready,」,而後發送一個「sign-in response」包到服務器。
這將使用一個新的類,PacketSignInResponse,而不是僅僅分組。sign-in response 將包含額外的數據超出標準的10字節header,對於這個項目,你作這樣的packets繼承自Packet。
可是在此以前,首先添加endPacketToServer: 方法(在sendPacketToAllClients:下):
- (void)sendPacketToServer:(Packet *)packet { GKSendDataMode dataMode = GKSendDataReliable; NSData *data = [packet data]; NSError *error; if (![_session sendData:data toPeers:[NSArray arrayWithObject:_serverPeerID] withDataMode:dataMode error:&error]) { NSLog(@"Error sending data to server: %@", error); } }
它看起來跟 sendPacketToAllClients:很是類似,除非你沒有數據包發送到全部鏈接的peers(同齡人),但只是由一個_serverPeerID標識。
這樣作的緣由是,在場景後面,, Game Kit用來鏈接每個peer到其餘每個peer,所以,若是有兩個客戶端和一個服務器,客戶端不只鏈接到服務器,還彼此鏈接。對於這個遊戲,你不但願客戶端發送消息給對方,只發給服務器。(你能夠從GKSession的session:peer:didChangeState: callback中看到輸出「peer XXX changed state 2″)
如今你須要新的PacketSignInResponse類。在項目中添加新的 Objective-C類,繼承自Packet,命名爲:PacketSignInResponse。在新的.h文件中替換以下內容:
#import "Packet.h" @interface PacketSignInResponse : Packet @property (nonatomic, copy) NSString *playerName; + (id)packetWithPlayerName:(NSString *)playerName; @end
除了Packet超類的常規數據,這個特殊的數據包還包含一個忘記的名字。替換.m文件以下:
#import "PacketSignInResponse.h" #import "NSData+SnapAdditions.h" @implementation PacketSignInResponse @synthesize playerName = _playerName; + (id)packetWithPlayerName:(NSString *)playerName { return [[[self class] alloc] initWithPlayerName:playerName]; } - (id)initWithPlayerName:(NSString *)playerName { if ((self = [super initWithType:PacketTypeSignInResponse])) { self.playerName = playerName; } return self; } - (void)addPayloadToData:(NSMutableData *)data { [data rw_appendString:self.playerName]; } @end
這應該是至關簡單,但可能除了addPayloadToData:。這是一個新的方法使Packet的子類能夠重寫本身的數據到NSMutableData對象中。這裏你只需追加playerName屬性的字符串內容到這個數據對象。爲了使其起做用,你必須從父類調用此方法。
添加一個空的addPayloadToData: 方法到Packet.m:
- (void)addPayloadToData:(NSMutableData *)data { // base class does nothing }
並從 「data」 方法中調用它:
- (NSData *)data { NSMutableData *data = [[NSMutableData alloc] initWithCapacity:100]; [data rw_appendInt32:'SNAP']; // 0x534E4150 [data rw_appendInt32:0]; [data rw_appendInt16:self.packetType]; [self addPayloadToData:data]; return data; }
默認狀況下,addPayloadToData:不會作任何事情,可是子類能夠用它把本身的附件數據加到消息中。
還有一件事是須要作的是從新編譯一切,這是一個在 Game.m中的新的Packet子類:
#import "PacketSignInResponse.h"
編譯器仍然警報說:「the value stored in the local packetNumber variable is never read 」(in Packet’s packetWithData:))。一會ok的,你會很快作與該變量有關的東西。
如今編譯和運行應用程序,不管是在客戶端仍是服務器,並開始新的遊戲。跟之前同樣,客戶端應該受到 「sign-in」包(類型代碼0×64),但做爲響應,它如今應該想服務器發送一個數據包,包含它的玩家名字。服務器的調試輸出以下:
Game: receive data from peer: 1100677320, data: <534e4150 00000000 00656372 617a7920 6a6f6500>, length: 20
粘貼數據(全部在<>之間的)到Hex Fiend中弄清楚什麼被傳回到服務器中:
這個包類型如今從0×65替換成0×64(PacketTypeSignInResponse 替換 PacketTypeSignInRequest),高亮選中的位顯示的是這個特殊玩家的名字(「crazy joe」)包括NUL字節終止字符串的名稱。
注意:保持限制你的Game Kit傳輸大小是個好主意。蘋果建議1000字節或更少,雖然彷佛上限是87千字節左右。
若是你發送小於1000個字節,全部的數據能夠放入一個單一的TCP/ IP數據包,這將保證交互快捷。大數據將被接收者拆分和重組。Game Kit會爲你處理這些,它仍然是至關快的。但爲了得到最佳性能,保持低於1000字節。
你可能已經注意到,在服務器的調試輸出不只代表數據被接收,它也這樣說:
Client received unexpected packet: <Packet: 0x9294ba0>
這就是彷佛難以想象,考慮到你在服務器上,而不是客戶端。但服務器和客戶端共享了不少代碼也不是太奇怪,GKSession data-receive-handler(數據接收處理程序)也是相同的兩個。因此你必須在僅用於傳入服務器的消息與僅用於客戶端消息之間作出區分。
改變Game.m文件下的data-receive-handler 方法:
- (void)receiveData:(NSData *)data fromPeer:(NSString *)peerID inSession:(GKSession *)session context:(void *)context { #ifdef DEBUG NSLog(@"Game: receive data from peer: %@, data: %@, length: %d", peerID, data, [data length]); #endif Packet *packet = [Packet packetWithData:data]; if (packet == nil) { NSLog(@"Invalid packet: %@", data); return; } Player *player = [self playerWithPeerID:peerID]; if (self.isServer) [self serverReceivedPacket:packet fromPlayer:player]; else [self clientReceivedPacket:packet]; }
如今你作一個基於 isServer屬性的區分。對於服務器,重要的是要知道數據包來自哪一個玩家,因此你看到Player對象使用新的playerWithPeerID:方法基於 發送者 peer ID 。添加此方法到Game.m:
- (Player *)playerWithPeerID:(NSString *)peerID { return [_players objectForKey:peerID]; }
很是簡單,可是值得單獨放到一個方法中,由於你將在其它地方屢次使用。serverReceivedPacket:fromPlayer:也是新方法:
- (void)serverReceivedPacket:(Packet *)packet fromPlayer:(Player *)player { switch (packet.packetType) { case PacketTypeSignInResponse: if (_state == GameStateWaitingForSignIn) { player.name = ((PacketSignInResponse *)packet).playerName; NSLog(@"server received sign in from client '%@'", player.name); } break; default: NSLog(@"Server received unexpected packet: %@", packet); break; } }
在服務器上運行應用程序,看負擔得起多少客戶端。當遊戲開始時,服務器應該輸出:
server received sign in from client 'Crazy Joe' server received sign in from client 'Weird Al' ...and so on...
除非它不工做並以一個 「unrecognized selector sent to instance」 錯誤!崩潰
在上面的代碼中,你投Packet到一個PacketSignInResponse對象,由於那是客戶端發給你的內容,不是嗎?嗯,這不是真滴。這個客戶端只發送了一堆GKSession投入NSData對象的字節,你用packetWithData:方法把它放到一個Packet對象中。
你必須讓Packet聰明的建立,並當packet類型爲PacketTypeSignInResponse(即十六進制0×65)時返回一個PacketSignInResponse對象。在Packet.m中,更改packetWithData: 方法爲:
+ (id)packetWithData:(NSData *)data { if ([data length] < PACKET_HEADER_SIZE) { NSLog(@"Error: Packet too small"); return nil; } if ([data rw_int32AtOffset:0] != 'SNAP') { NSLog(@"Error: Packet has invalid header"); return nil; } int packetNumber = [data rw_int32AtOffset:4]; PacketType packetType = [data rw_int16AtOffset:8]; Packet *packet; switch (packetType) { case PacketTypeSignInRequest: packet = [Packet packetWithType:packetType]; break; case PacketTypeSignInResponse: packet = [PacketSignInResponse packetWithData:data]; break; default: NSLog(@"Error: Packet has invalid type"); return nil; } return packet; }
每一次你添加支持一個新的packet類型,你還須要添加一個case語句到這個方法中。不要忘了在Packet.m中導入:
#import "PacketSignInResponse.h"
請注意「sign-in response」 packet,你在PacketSignInResponse 上調用packetWithData:方法而不是常規的包自身代用。咱們的想法是在子類中覆蓋packetWithData的來讀取,特定數據包類型的數據 - 例如,玩家的名字。添加一些方法到PacketSignInResponse.m:中
+ (id)packetWithData:(NSData *)data { size_t count; NSString *playerName = [data rw_stringAtOffset:PACKET_HEADER_SIZE bytesRead:&count]; return [[self class] packetWithPlayerName:playerName]; }
在這裏,你簡單第看到玩家的名字,在NSData對象開始的10個字節中(被PACKET_HEADER_SIZE表示的不變變量)。而後你調用正規的方便分配和初始化對象的構造函數。
這裏惟一的問題是, PACKET_HEADER_SIZE是一個未知的符號。它在Packet.m中聲明,可是這對於其餘對象不可見,因此加一個前瞻性聲明到packet.h中:
const size_t PACKET_HEADER_SIZE;
如今一切都應該完成--運行!--再次。試試吧。服務器寫入了全部鏈接的客戶端的名稱。你已經實現了服務器和客戶端之間的雙向溝通!
這是到目前爲止全部教程系列的示例項目。
但願你沒有厭煩這些有關網絡的東西,由於在第4部分會有更多!當你準備好實施「遊戲客戶端和服務器之間的握手」時點擊進入,,並建立主遊戲畫面!
在此期間,關於這個系列的一部分,若是您有任何問題或意見,請加入論壇討論!