您的位置:首页 > 其它

(译)如何使用GameCenter制作一个简单的多人游戏教程:第二部分

2011-06-27 00:33 1146 查看

(译)如何使用GameCenter制作一个简单的多人游戏教程:第二部分

on 2011-6-24 in 博文摘选 | 0 Comment

免责申明(必读!):本博客提供的所有教程的翻译原稿均来自于互联网,仅供学习交流之用,切勿进行商业传播。同时,转载时不要移除本申明。如产生任何纠纷,均与本博客所有人、发表该翻译稿之人无任何关系。谢谢合作!
原文链接地址:http://www.raywenderlich.com/3325/how-to-make-a-simple-multiplayer-game-with-game-center-tutorial-part-22
教程截图:



这是本系列教程的第二部分,主要是内容是关于如何使用Game Center matchmaking来制作一个简单的联机游戏。
在上篇教程中,你学会了如何为你的app激活Game Center,还有如何使用内置的GKMatchmakerViewController来查找玩家。
在这篇教程中,你将学会如何查找玩家别名,如何在游戏里面收发数据以及如何支持玩家邀请功能。
在最后,你将会得到一个完整的,但是非常简单的网络游戏,使用的技术就是cocos2d和Game Center。你可以和你的朋友一起玩!
如果你还没有上个教程的工程的话,你可以先在这里下载。

网络代码策略:挑选玩家

在选择玩家之前,让我们先讨论一下我们将要采纳的网络代码相关策略,同时还有如何挑选玩家的策略。
在这个游戏里面,我们有两个玩家:玩家1(狗)和玩家2(小孩)。问题是,我们怎么决定谁充当狗的角色,谁充当小孩的角色?(呵呵,老外这里有点搞笑了,我也照样翻译了)。
策略就是,我们将会在两个玩家游戏启动的时候生成随机数,并且把这个数发给对方。如果哪方的数大,那么就当玩家1,另外一方自然就是玩家2.
但是,极少数情况下面,可能会生成两个一样的随机数,那样的话,我们只好再重试一遍罗。
不管哪一个玩家是palyer1,他都会获得一些“特权”。首先,player1这一方开始游戏的时候,给另一方发送一个消息。同时,player1还负责检查游戏什么时候结束,并且负责发送消息给另一方,告知”游戏结束了“。
换句话说,”player1“将充当服务器的角色。它是整个游戏中拥有最终发言权的那位!
网络代码策略:玩家别名(player alias)
因为我们随机决定哪个玩家是dog,哪个玩家是kid,所以我们需要有一种方式可以知道玩家具体属性哪个游戏角色。
对于这个游戏来说,我们将会在游戏角色的上面显示玩家的别名,这样的话,就可以区别出谁是谁了。
如果你还不知道玩家别名是什么,其实就是你在建立Game Center帐号的时候取的nickname啦。当一个match完成的时候,你并不会自动获得它,你需要手动地调用一个方法,然后Game Center会把名字返回给你。

网络代码策略:游戏状态

编写网络相关代码的一个挑战就是,程序执行的顺序可能会和你预期的不一样。
比如,一方可能已经完成match的初始化工作了,然后开始发送随机数给另一端,但是,这时候,可能另一端并没有完成match的初始化啊!
因此,如果我们不够仔细的话,我们可以会遇到一些与时间相关的很奇怪的问题。一个比较好的解决方法就是,追踪每一端的游戏的当前状态。
下面一张图可以解释Cat Race这个游戏需要有哪些状态:



让我们一个状态一个状态来看:

Waiting for Match: 游戏当前等待一个match被连接,同时查找对象玩家的别名。如果这两个都完成了的话,再检测是否从另一端收到随机数,然后再跳到等待游戏开始状态。

Waiting for Random #:(#号是number的意思)游戏里面有一个match和玩家别名,但是仍然需要从另一端那里接收到一个随机数。

Waiting for Start: 游戏等待另一方开始游戏,本例中就是对应等待player2开始游戏。

Active: 游戏正在进行中---还没有发现任何赢的玩家。每一个时刻,当一个玩家移动的时候,他都会给另一方发送一个消息,告诉他我在移动了。

Done: 游戏结束 (player 1 给player2发送一个消息说,游戏结束了). 这时,再没有别的消息会发送了,然后两端就重新开始游戏。

好,现在你头脑中有一个概念了,让我们一步步来实现吧!

查找玩家别名

打开GCHelper.h,然后作如下修改:
// Add inside @interface NSMutableDictionary *playersDict; // Add after @interface @property (retain) NSMutableDictionary *playersDict;
这里定义了一个实例变量和相应的属性。我们使用一个字典类,这样可以方便地基于每个玩家的唯一的id来查找 GKPlayer数据(这里面包含了玩家别名信息)。
然后,打开GCHelper.m文件并作如下修改:
// Add to top of file @synthesize playersDict; // Add new method after authenticationChanged - (void)lookupPlayers { NSLog(@"Looking up %d players...", match.playerIDs.count); [GKPlayer loadPlayersForIdentifiers:match.playerIDs withCompletionHandler:^(NSArray *players, NSError *error) { if (error != nil) { NSLog(@"Error retrieving player info: %@", error.localizedDescription); matchStarted = NO; [delegate matchEnded]; } else { // Populate players dict self.playersDict = [NSMutableDictionary dictionaryWithCapacity:players.count]; for (GKPlayer *player in players) { NSLog(@"Found player: %@", player.alias); [playersDict setObject:player forKey:player.playerID]; } // Notify delegate match can begin matchStarted = YES; [delegate matchStarted]; } }]; } // Add inside matchmakerViewController:didFindMatch, right after @"Ready to start match!": [self lookupPlayers]; // Add inside match:playerdidChangeState, right after @"Ready to start match!": [self lookupPlayers];
主要的查找逻辑都在函数 lookupPlayers里面。这个函数在match准备好之后被调用,它会查找在一个match中的所有玩家的信息(除了本地玩家信息),因为我们可以通过 GKLocalPlayer单例类来查找本地玩家。
Game Center会在一个match中,为每一个玩家返回一个GKPlayer对象。为了使得后面使用更方便,我们把每一个GKPlayer对象放到字典里面,使用player id作为key。
最后,把match标记为已经开始了。然后调用游戏的delegate方法来开始整个游戏。
但是,在我们继续讲下去之前,测试一下!把两个设备都编译并运行一下,这一次,你再看一下控制台输出,你将会看到查找玩家及其别名的过程:
CatRace[16918:207] Authentication changed: player authenticated. CatRace[16918:207] Player connected! CatRace[16918:207] Ready to start match! CatRace[16918:207] Looking up 1 players... CatRace[16918:207] Found player: Vickipsq CatRace[16918:207] Match started

添加网络代码

你已经建立好match了,同时还拥有每个玩家的名字,因此,现在你要开始学习这个项目真正有用的东西了---添加网络代码!
你需要做的第一件事情就是,基于我们前面画的图,定义一些新的游戏状态。打开HelloWorldLayer.h,然后修改GameState结构体,具体如下:
typedef enum { kGameStateWaitingForMatch = 0, kGameStateWaitingForRandomNumber, kGameStateWaitingForStart, kGameStateActive, kGameStateDone } GameState;
你也需要添加一个游戏结束的新的原因---断开连接。因此,修改EndReason枚举类型,如下所示:
typedef enum { kEndReasonWin, kEndReasonLose, kEndReasonDisconnect } EndReason;
接下来,你需要为每个消息定义相应的结构体类型。因此,在HelloWorldLayer.h里面添加下面的代码:
typedef enum { kMessageTypeRandomNumber = 0, kMessageTypeGameBegin, kMessageTypeMove, kMessageTypeGameOver } MessageType; typedef struct { MessageType messageType; } Message; typedef struct { Message message; uint32_t randomNumber; } MessageRandomNumber; typedef struct { Message message; } MessageGameBegin; typedef struct { Message message; } MessageMove; typedef struct { Message message; BOOL player1Won; } MessageGameOver;
注意,每一个消息开头都有一个消息类型---这样做的目的就是你可以通过比较相应的字段,来判别消息是什么类型的。
最后,在HelloWorldLayer类中添加一些实例变量:
uint32_t ourRandom; BOOL receivedRandom; NSString *otherPlayerID;
这里将追踪我们为每台设备产生的随机数,是否我们已经从另一端接收到了这个随机数,同时,我们还存储了玩家id的引用。
好了,现在,让我们来实现网络代码吧。打开HelloWorldLayer.m,然后修改matchStartd方法,如下所示:
- (void)matchStarted { CCLOG(@"Match started"); if (receivedRandom) { [self setGameState:kGameStateWaitingForStart]; } else { [self setGameState:kGameStateWaitingForRandomNumber]; } [self sendRandomNumber]; [self tryStartGame]; }
因此,当match启动的时候,我们判断一下,是否已经收到另一端的随机数了,然后相应地设置游戏状态。然后我们调用发送随机数函数并开始游戏。
让我们开始实现 sendRandomNumber函数。在HelloWorldLayer.m中作如下修改:
// Add at bottom of init, anbd comment out previous call to setGameState ourRandom = arc4random(); [self setGameState:kGameStateWaitingForMatch]; // Add these new methods to the top of the file - (void)sendData:(NSData *)data { NSError *error; BOOL success = [[GCHelper sharedInstance].match sendDataToAllPlayers:data withDataMode:GKMatchSendDataReliable error:&error]; if (!success) { CCLOG(@"Error sending init packet"); [self matchEnded]; } } - (void)sendRandomNumber { MessageRandomNumber message; message.message.messageType = kMessageTypeRandomNumber; message.randomNumber = ourRandom; NSData *data = [NSData dataWithBytes:&message length:sizeof(MessageRandomNumber)]; [self sendData:data]; }
sendRandomNumber创建一个新的 MessageRandomNumber结构体,设置结构体的 randomNumber域,然后把此结构体转换成DSData并发送给另一端。
sendData调用GCHelper的 sendDataToAllPlayers方法来给match对象里的所有玩家发送消息。
接下来,我们实现 tryStartGame方法。在HelloWorldLayer.m作如下修改:
// Add right after sendRandomNumber - (void)sendGameBegin { MessageGameBegin message; message.message.messageType = kMessageTypeGameBegin; NSData *data = [NSData dataWithBytes:&message length:sizeof(MessageGameBegin)]; [self sendData:data]; } // Add right after update method - (void)tryStartGame { if (isPlayer1 && gameState == kGameStateWaitingForStart) { [self setGameState:kGameStateActive]; [self sendGameBegin]; } }
这个也非常简单---如果玩家1(就是有特权并且充当服务器的一端)和游戏都准备好了,那么就设置游戏状态为active,并且发送一个 MessageGameBegin消息给另一方。
好,现在让我们编写一些代码来处理这些接收到的消息。修改 match:didReceiveData:fromPlayer方法,如下所示:
- (void)match:(GKMatch *)match didReceiveData:(NSData *)data fromPlayer:(NSString *)playerID { // Store away other player ID for later if (otherPlayerID == nil) { otherPlayerID = [playerID retain]; } Message *message = (Message *) [data bytes]; if (message->messageType == kMessageTypeRandomNumber) { MessageRandomNumber * messageInit = (MessageRandomNumber *) [data bytes]; CCLOG(@"Received random number: %ud, ours %ud", messageInit->randomNumber, ourRandom); bool tie = false; if (messageInit->randomNumber == ourRandom) { CCLOG(@"TIE!"); tie = true; ourRandom = arc4random(); [self sendRandomNumber]; } else if (ourRandom > messageInit->randomNumber) { CCLOG(@"We are player 1"); isPlayer1 = YES; } else { CCLOG(@"We are player 2"); isPlayer1 = NO; } if (!tie) { receivedRandom = YES; if (gameState == kGameStateWaitingForRandomNumber) { [self setGameState:kGameStateWaitingForStart]; } [self tryStartGame]; } } else if (message->messageType == kMessageTypeGameBegin) { [self setGameState:kGameStateActive]; } else if (message->messageType == kMessageTypeMove) { CCLOG(@"Received move"); if (isPlayer1) { [player2 moveForward]; } else { [player1 moveForward]; } } else if (message->messageType == kMessageTypeGameOver) { MessageGameOver * messageGameOver = (MessageGameOver *) [data bytes]; CCLOG(@"Received game over with player 1 won: %d", messageGameOver->player1Won); if (messageGameOver->player1Won) { [self endScene:kEndReasonLose]; } else { [self endScene:kEndReasonWin]; } } }
这个方法把接收到的消息转换为我们定义的Message类型,然后我们就可以根据Message结构中的类型来作相应的处理了。

对于 MessageRandomNumber 这种消息,我们主要基于两个玩家所产生的随机数来进行比较,决定谁是1,谁是2,进而决定谁充当服务器的角色。同时,还有相应的状态改变。

对于 MessageGameBegin 这种消息,它仅仅是把游戏状态切换到active,这意味着玩家1将会发送一个消息给玩家2.

对于 MessageMove 这种消息, 它会使另一方向前移动一点点。

对于 MessageGameOver 这种消息, 它会基于游戏结束时的状态,以一种合适地方式来结束游戏。

好了,现在你差不多完成了大部分游戏逻辑了,只是,还有一些细节部分没有完成。接下来,我们在HelloWorldLayer.m中作如下修改:
// Modify setGameState as follows // Adds debug labels for extra states - (void)setGameState:(GameState)state { gameState = state; if (gameState == kGameStateWaitingForMatch) { [debugLabel setString:@"Waiting for match"]; } else if (gameState == kGameStateWaitingForRandomNumber) { [debugLabel setString:@"Waiting for rand #"]; } else if (gameState == kGameStateWaitingForStart) { [debugLabel setString:@"Waiting for start"]; } else if (gameState == kGameStateActive) { [debugLabel setString:@"Active"]; } else if (gameState == kGameStateDone) { [debugLabel setString:@"Done"]; } } // Add new methods after sendGameBegin // Adds methods to send move and game over messages - (void)sendMove { MessageMove message; message.message.messageType = kMessageTypeMove; NSData *data = [NSData dataWithBytes:&message length:sizeof(MessageMove)]; [self sendData:data]; } - (void)sendGameOver:(BOOL)player1Won { MessageGameOver message; message.message.messageType = kMessageTypeGameOver; message.player1Won = player1Won; NSData *data = [NSData dataWithBytes:&message length:sizeof(MessageGameOver)]; [self sendData:data]; } // Add to beginning of ccTouchesBegan:withEvent // Sends move message to other side when user taps, but _disibledevent=> if (gameState != kGameStateActive) return; [self sendMove]; // Add to end of endScene: // If the game ends and it's player 1, sends a message to the other side if (isPlayer1) { if (endReason == kEndReasonWin) { [self sendGameOver:true]; } else if (endReason == kEndReasonLose) { [self sendGameOver:false]; } } // Add to beginning of update: // Makes it so _disibledevent=> if (!isPlayer1) return; // Add at bottom of matchEnded // Disconnects match and ends level [[GCHelper sharedInstance].match disconnect]; [GCHelper sharedInstance].match = nil; [self endScene:kEndReasonDisconnect]; // Add inside dealloc // Releases variable initialized earlier [otherPlayerID release]; otherPlayerID = nil;
上面的代码其实非常简单(你仔细阅读一下注释就知道它完成了哪些功能),所以,我们在这里就不在啰嗦了。
恩,写了好多代码了,不过可以跑起来!呵呵,编译并运行,两台设备都要,这时你有一个完整的赛跑游戏啦!



显示玩家的名字

目前为止,你已经有一个可以玩的游戏了,但是,你并不能分辨游戏里的角色身份,因为他们是随机决定的。
因此,让我们为每一个游戏角色定义玩家别名,这样子就可以区分开来了。打开HelloWorldLayer.h,添加下面的代码:
CCLabelBMFont *player1Label; CCLabelBMFont *player2Label;
然后,打开HelloWorldLayer.m,然后在 tryStartGame方法后面添加一个新的方法,具体如下:
- (void)setupStringsWithOtherPlayerId:(NSString *)playerID { if (isPlayer1) { player1Label = [CCLabelBMFont labelWithString:[GKLocalPlayer localPlayer].alias fntFile:@"Arial.fnt"]; [self addChild:player1Label]; GKPlayer *player = [[GCHelper sharedInstance].playersDict objectForKey:playerID]; player2Label = [CCLabelBMFont labelWithString:player.alias fntFile:@"Arial.fnt"]; [self addChild:player2Label]; } else { player2Label = [CCLabelBMFont labelWithString:[GKLocalPlayer localPlayer].alias fntFile:@"Arial.fnt"]; [self addChild:player2Label]; GKPlayer *player = [[GCHelper sharedInstance].playersDict objectForKey:playerID]; player1Label = [CCLabelBMFont labelWithString:player.alias fntFile:@"Arial.fnt"]; [self addChild:player1Label]; } }
这里创建了两个 CCLabelBMFonts,每个游戏角色一个label。然后获得本地玩家的别名,我们可以使用GKLocalPlayer来获取。对于另外一个玩家,我们需要查找GCHelper的字典里的GKPlayer对象。
接下来,在HelloWorldLayer.m作如下修改:
// Inside if statement for tryStartGame [self setupStringsWithOtherPlayerId:otherPlayerID]; // Inside if statement for match:didReceiveData, kMessageTypeGameBegin case [self setupStringsWithOtherPlayerId:otherPlayerID]; // At beginning of update method player1Label.position = player1.position; player2Label.position = player2.position;
基本上,当游戏切找到Active状态的时候,我们肯定已经接收到玩家的名字了,而这时游戏已经快开始了,是时候初始化label了。
在update方法中,每一帧,我们根据玩家的位置来更新label的位置。
你可能会觉得这样实现有一点点奇怪--为什么不把label当作一个精灵的孩子呢?好吧,我们实际上不能这样做,因为,我们的精灵是加到一个batchNode里面的,而batchNode里面添加的孩子只能是ccsprite的子类,并且这个子类的孩子也要是ccsprite的子类)。因此,我们才这样实现的。
在两台设备上编译并运行代码,现在,你可以在每个游戏角色的上面看到玩家的别名了。



支持邀请功能

我们已经有一个非常好的,可以跑的游戏了,但是,让我们再添加一些更酷、更好玩的功能吧--支持邀请功能!
你可能已经注意到了,当matchmaker视图控制器出现的时候,它有一个选项,可以让你邀请你的朋友一起来玩。但是,目前为止,这个功能还不能用。因为我们还没有写任何代码,但是,实际上这个功能是非常容易实现的。
打开GCHelper.h,然后作如下修改:(代码添加位置,请注意看注释!)
// Add inside GCHelperDelegate - (void)inviteReceived; // Add inside @interface GKInvite *pendingInvite; NSArray *pendingPlayersToInvite; // Add after @interface @property (retain) GKInvite *pendingInvite; @property (retain) NSArray *pendingPlayersToInvite;
这里创建了一些实例变量,并定义了相应的属性。同时往 GCHelperDelegate 协议里面添加了一个新的方法,可以在邀请被接收到的时候被通告。
接下来,回到GCHelper.m,然后作如下修改:
// At top of file @synthesize pendingInvite; @synthesize pendingPlayersToInvite; // In authenticationChanged callback, right after userAuthenticated = TRUE [GKMatchmaker sharedMatchmaker].inviteHandler = ^(GKInvite *acceptedInvite, NSArray *playersToInvite) { NSLog(@"Received invite"); self.pendingInvite = acceptedInvite; self.pendingPlayersToInvite = playersToInvite; [delegate inviteReceived]; };
在Game Center里面,想让邀请功能跑起来的话,你需要提供一个回调函数,当invite被接收到的时候来回调之。你也应该尽可能的在你的游戏启动起来的时候就注册这样一个block函数---推荐的时机,是刚好在用户被认证之后。
对于这个游戏,这个回调函数会保存邀请信息,然后通知delegate对象。而delegate对象会重新切回cocos2d场景,然后通过调用 findMatchWithMinPlayers方法来查找一个match。---我们将会修改这个函数,并且把任何将到到达的邀请信息考虑进去。
因此,修改 findMatchWithMinPlayers方法,具体如下所示:
- (void)findMatchWithMinPlayers:(int)minPlayers maxPlayers:(int)maxPlayers viewController:(UIViewController *)viewController delegate:(id)theDelegate { if (!gameCenterAvailable) return; matchStarted = NO; self.match = nil; self.presentingViewController = viewController; delegate = theDelegate; if (pendingInvite != nil) { [presentingViewController dismissModalViewControllerAnimated:NO]; GKMatchmakerViewController *mmvc = [[[GKMatchmakerViewController alloc] initWithInvite:pendingInvite] autorelease]; mmvc.matchmakerDelegate = self; [presentingViewController presentModalViewController:mmvc animated:YES]; self.pendingInvite = nil; self.pendingPlayersToInvite = nil; } else { [presentingViewController dismissModalViewControllerAnimated:NO]; GKMatchRequest *request = [[[GKMatchRequest alloc] init] autorelease]; request.minPlayers = minPlayers; request.maxPlayers = maxPlayers; request.playersToInvite = pendingPlayersToInvite; GKMatchmakerViewController *mmvc = [[[GKMatchmakerViewController alloc] initWithMatchRequest:request] autorelease]; mmvc.matchmakerDelegate = self; [presentingViewController presentModalViewController:mmvc animated:YES]; self.pendingInvite = nil; self.pendingPlayersToInvite = nil; } }
当创建 GKMatchmakerViewController的时候,这里和之前的做法非常之类似,除了这里使用pendingInvite和 pendingPlayersToInvite值。
最后,回到HelloWorldLayer.m并实现 inviteReceived方法,如下所示:
- (void)inviteReceived { [self restartTapped:nil]; }
大功告成!在两台设备上编译并运行工程,当一个设备跑起来后,使用 GKMatchmakerViewController发送一个邀请给另一方。这时另一方将会收到邀请通知,这个通知看起来应该如下图所示:



点击”Accept“,然后在原来的设备上点“Play now”,这时,你又可以和之前一样玩这个游戏了。但是,这次,你的对手是你邀请的朋友呀!:)

何去何从?

这里有本教程的完整源代码。
如果你正想往你的游戏中添加Game Center的功能,你可能想使用Leaderboards和Achievements。如果你对此感兴趣的话,可以买我的书。
译者的话:本人水平有限,翻译不准的地方请不吝提出,谢谢!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐