您的位置:首页 > 其它

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

2012-10-22 12:56 1181 查看
免责申明(必读!):本博客提供的所有教程的翻译原稿均来自于互联网,仅供学习交流之用,切勿进行商业传播。同时,转载时不要移除本申明。如产生任何纠纷,均与本博客所有人、发表该翻译稿之人无任何关系。谢谢合作!

原文链接地址: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 only if game is active

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 only player 1 checks for game over conditions

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<GCHelperDelegate>)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。如果你对此感兴趣的话,可以买我的

译者的话:本人水平有限,翻译不准的地方请不吝提出,谢谢!

原创文章,转载请注明: 转载自DEVDIV博客-History

本文链接地址: (译)如何使用GameCenter制作一个简单的多人游戏教程:第二部分
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐