Game Center的高级开发是建立多人实时联网的比赛。开发者可以选择使用Game Center提供的服务器来运行游戏,或者使用自己的服务器来作数据并发。由于我的案例是选择了前者,所以本文只对使用Game Center提供的服务器来介绍。
在Game Center中可以有选择地邀请好友,或者让服务器自己来寻找匹配的玩家。如果是邀请好友,首先两者必须在Game Center中是互为好友的关系,这样在邀请发出之后,好友就会收到一个Notification的邀请。一旦选择接受游戏,那么双方就会建立一个连接,当各方都成功连接之后,游戏就可以开始了。如果选择的是让Game Center自动寻找匹配玩家,服务器会对各个请求的发起者来自动配对,各方不一定是互为好友关系,只要他们的请求相同,比如参与游戏人数或者其他条件完全符合,就可以立即建立连接。通过Game Center服务器建立的比赛最多支持4个玩家同时在线,通过自己的服务器建立的比赛最多支持16个玩家同时在线。
Game Center为游戏开发者提供了一个现成的邀请朋友界面。如下面的例子,通过GKMatchRequest类可以发送一个创建界面的请求,传递两个参数分别表示游戏需要的最少人数以及最多人数。FREGetObjectAsUint32可以将由Flash传递来的FREObject格式的参数转成Native的整型,寄存在变量min和max中。
标准邀请游戏界面的类是GKMatchmakerViewController,从代码中可以看出,显示GKMatchmakerViewController的方法和显示Leader board是完全一样的。注意mmvc.matchmakerDelegate = observer这一行,采用Delegate来获取回调函数是iOS SDK中标准的程序模式,只要observer实现了GKMatchmakerViewControllerDelegate的接口,便可以充当侦听器获取用户在这个界面上的所有动作指令。
FREObject requestMatchMaker(FREContext ctx,void* funcData, uint32_t argc, FREObject argv[]) { uint32_t min = 2; uint32_t max = 2; FREGetObjectAsUint32(argv[0], &min); FREGetObjectAsUint32(argv[1], &max); showMatchMaker(min,max); return nil; } void showMatchMaker(uint32_t min, uint32_t max){ GKMatchRequest *request = [[GKMatchRequest alloc] init]; request.minPlayers = min; request.maxPlayers = max; GKMatchmakerViewController *mmvc = [[GKMatchmakerViewController alloc] initWithMatchRequest:request]; mmvc.matchmakerDelegate = observer; showModalViewController(mmvc); [[GKMatchmaker sharedMatchmaker] queryActivityWithCompletionHandler:^(NSInteger activity, NSError *error) { if (error) { } else { } }]; }
如何设计接受朋友邀请
当玩家发送游戏邀请之后,被邀请的朋友会接收到一个Notification。开发者需要在游戏最初的时候尽早为游戏邀请添加侦听器,而且注册处理邀请的侦听应该加在登陆成功的事件中。所以这个流程应该是 游戏初始化->登陆->注册处理邀请侦听:
FREObject authenticate(FREContext ctx, void* funcData, uint32_t argc, FREObject argv[]) { GKLocalPlayer *localPlayer = [GKLocalPlayer localPlayer]; [localPlayer authenticateWithCompletionHandler:^(NSError *error) { GKLocalPlayer *lp = [GKLocalPlayer localPlayer]; if(lp.isAuthenticated){ NSMutableString* retXML = [[NSMutableString alloc] initWithString:@"<p>"]; [retXML appendFormat:@"<i>%@</i>",lp.playerID]; [retXML appendFormat:@"<a>%@</a>",lp.alias]; [retXML appendFormat:@"</p>"]; FREDispatchStatusEventAsync(g_ctx, (const uint8_t*)"authenticate_status_changed",(const uint8_t*)[retXML UTF8String]); handleInvitation(); }else{ } }]; return nil; }
上面代码中的handleInvitation就是对邀请的处理,一旦检测到有邀请,便启动GKMatchmakerViewController界面与邀请方进行连接。inviteHandler会同时检测到两种情况,一个是被邀请方检测到来自邀请方的游戏请求,这个时候侦听器的GKInvite变量acceptedInvite不为空,使用这个变量来初始化GKMatchmakerViewController可以实现被邀请方加入游戏的UI界面;还有一种情况是邀请方不是在游戏内部而是通过Game Center应用平台发送的邀请,这时邀请方也会自动打开游戏应用,这种情况下inviteHandler会检测到一个NSArray数组playersToInvite,如果这个值不为空,则需要显示邀请方的邀请界面,其显示代码与上面介绍的显示邀请界面代码是很类似的,只不过使用playersToInvite预设了所有的被邀请方。
[GKMatchmaker sharedMatchmaker].inviteHandler = ^(GKInvite *acceptedInvite, NSArray *playersToInvite) { if (acceptedInvite) { GKMatchmakerViewController *mmvc = [[GKMatchmakerViewController alloc] initWithInvite:acceptedInvite]; mmvc.matchmakerDelegate = observer; showModalViewController(mmvc); } else if (playersToInvite) { GKMatchRequest *request = [[GKMatchRequest alloc] init]; request.minPlayers = 2; request.maxPlayers = 4; request.playersToInvite = playersToInvite; GKMatchmakerViewController *mmvc = [[GKMatchmakerViewController alloc] initWithMatchRequest:request]; mmvc.matchmakerDelegate = observer; showModalViewController(mmvc); } };
实现GKMatchmakerViewDelegate的接口matchmakerViewController:viewController didFindMatch:match,便可以侦听到玩家连接成功的事件;
- (void)matchmakerViewController:(GKMatchmakerViewController *)viewController didFindMatch:(GKMatch *)match { dismissModalViewController(viewController); self.myMatch = match; self.myMatch.delegate = self; FREDispatchStatusEventAsync(g_ctx, (const uint8_t*)"request_match_complete", (const uint8_t*)""); if (!matchStarted && match.expectedPlayerCount == 0) { matchStarted = YES; initializeMatchPlayers(); } }
我来简单解释一下上面代码中的信息。Game Center为各方玩家之间建立一个Peer 2 Peer的连接,每个玩家都需要与另外的任何一个玩家连接,每次连接成功之后都会调用上面的这个方法。服务器为每位成功连接的玩家指配一个GKMatch对象,对象中包括需要等待的玩家人数以及每位与自己成功连接的玩家信息。开发者需要把这个GKMatch对象作为全局变量寄存下来,并且为它添加一个回调Delegate,集成GKMatchDelegate的所有接口,用来实现多人游戏后续的动作比如数据交换和玩家状态检测。此时创建游戏的标准界面 GKMatchmakerViewController 的使命已经完成,使用dismissModelViewController来移除它。
实现GKMatchDelegate的接口match:player:didChangeState可以继续检测玩家的连接状况,一旦需要等待的玩家个数为0,便可以准备开始游戏了:
- (void)match:(GKMatch *)match player:(NSString *)playerID didChangeState:(GKPlayerConnectionState)state { if (!matchStarted && match.expectedPlayerCount == 0) { matchStarted = YES; initializeMatchPlayers(); } }
开始游戏之前需要做的最后一个动作是获取所有游戏中的玩家信息,这是一个异步的过程,需要用侦听器来侦听信息获取的结果。下例就是initializeMatchPlayers的所有代码,非常通俗易懂,如果你还不是非常理解,请参考我在之前介绍过的如何实现AS与Native之间的数据交换.
void initializeMatchPlayers(){ [GKPlayer loadPlayersForIdentifiers:[observer.myMatch playerIDs] withCompletionHandler:^(NSArray *players, NSError *error) { if(error){ //Handler load player info error; }else{ NSMutableString* retXML = [[NSMutableString alloc] initWithString:@"<m>"]; for(GKPlayer *player in players){ [retXML appendFormat:@"<p>"]; [retXML appendFormat:@"<i>%@</i>",player.playerID]; [retXML appendFormat:@"<a>%@</a>",player.alias]; [retXML appendFormat:@"</p>"]; }; [retXML appendFormat:@"</m>"]; FREDispatchStatusEventAsync(g_ctx, (const uint8_t*)"match_players_initialized", (const uint8_t*)[retXML UTF8String]); }; }]; }
如何设计游戏的网络结构以及实现数据的传递
Game Center提供的连接服务是一种玩家之间两两相连的Peer Peer连接方式,这种方式的优势在于数据的交互可以不用经过服务器直达玩家,但劣势是没有服务器端进行同步,这在稍微复杂一点的多人游戏中就会出现问题。举个简单的例子,比如一个游戏里在初始化的时候需要往场景中随机创建若干个物品,在网游中这些信息都是在服务器端创建和保存的。如何在Peer Peer连接中实现同样的功能?我推荐的方式是选举一个Peer作为服务器,由他来负责所有客观数据的运算和同步,其他Peer作为客户端,到这个“服务器”上去验证,这样就能保证数据的同步。当然,如果充当“服务器”的玩家掉线了,剩下的Peer们需要再次选举一个服务器角色。选举服务器的规则可以由开发者自行定义,可以选举玩家中设备性能最高的,或者选举网络状况最好的,或者干脆就按照玩家的ID来排序,谁排在前面谁来当服务器。
一旦服务器被选举出来之后,他要做的第一件事就是将参与游戏的玩家数据以及游戏的初始化数据同步给所有人。然后大家便可以在客户端设置同样的游戏场景、数据以及玩家信息。
玩家之间发送数据用的是GKMatch的sendData和sendDataToAllPlayers方法,可以选择发送给特定的玩家或者所有的玩家。下面是一个发送给特定玩家的代码段:
FREObject sendDataToGCPlayers(FREContext ctx,void* funcData, uint32_t argc, FREObject argv[]){ //创建msg来寄存发送的信息 const uint8_t* msg = nil; uint32_t len = -1; //将信息寄存在msg中 FREGetObjectAsUTF8(argv[1], &len, &msg); const char* datachar = (const char*) msg; //创建playerIDs来寄存接收玩家列表 const uint8_t* playerIDs = nil; uint32_t plen = -1; //将玩家列表寄存在playerIDs中 FREGetObjectAsUTF8(argv[0], &plen, &playerIDs); //转换玩家列表到字符串playerIDStr中,并创建数组players NSString *playerIDStr = [[NSString alloc] initWithString:[NSString stringWithUTF8String:(const char*)playerIDs]]; NSArray *players = [playerIDStr componentsSeparatedByString:@","]; //将发送的信息保存在一个NSData中 NSData *packet = [NSData dataWithBytes:datachar length:strlen(datachar)]; //使用GKMatch的sendData方法将信息发送给指定的玩家们 [observer.myMatch sendData:packet toPlayers:players withDataMode:GKSendDataUnreliable error:nil]; return nil; }
上例中的withDataMode是发送的方式,GameKit有两种数据发送的方式:GKSendDataUnreliable和GKSendDataReliable,前者只发送一次,不保证接收方能收到数据,一般用在实时同步的游戏中;后者相对比较稳定,可以保证数据按一定的顺序发送到对方,一般可以用在回合制的游戏中。但是我在实际的编程中发现,GKSendDataReliable也并非一定能保证数据的可靠发送,数据发送丢失的情况时有发生。我的解决方案是在ActionScript里建立一个Router类实现握手,用这个类对接收方持续发送,每次接收方收到信息,都会发送一个回执。只要给每条信息都编号,就可以保证接收方有选择地执行接收到的信息,忽略重复发送的信息,以及等待尚未到达的信息。对发送方来说,只要接到某条信息的回执或者接到该玩家掉线的信息,便可以停止信息的发送。这样就可以使用GKSendDataUnreliable来频繁发送,每次发送的数据包都比较小。实践证明这样的方式很可靠,而且数据量并不大。
玩家可以通过GKMatchDelegate的接口match:didReceiveData: fromPlayer:来接收来自特定玩家的信息。如下例:
- (void)match:(GKMatch *)match didReceiveData:(NSData *)data fromPlayer:(NSString *)playerID{ handleReceivedData(data); } void handleReceivedData(NSData *data){ NSString *datastr = [NSString stringWithUTF8String:[data bytes]]; FREDispatchStatusEventAsync(g_ctx, (const uint8_t*)"received_data_from",(const uint8_t*)[datastr UTF8String]); }