ANE与GameKit(五)实例

On November 26, 2011, in AIR, ANE - GameKit, iOS, by James

在这一篇教程中,我为大家提供了一个ANE与GameKit开发的小例子。我最初的设想是做一个完整的游戏,发布到苹果应用商店然后把游戏开源,但是游戏制做过程中,我对它的要求越来越高,越做越复杂,估计以我个人的力量要完成那款游戏怎么也要等到明年下半年。所以我决定做一个简单的实例让大家可以一目了然地了解与GameKit相关的主要代码。

这个例子是一个双人对战的红蓝棋,棋局的规则我没有做,只是实现了两个玩家通过Game Center或者蓝牙建立游戏连接,实现游戏中相互通讯的过程。

实例中包括三个项目:

GameKit – Flex Library Project
wuziqi.fla – Flash Professional Project
ANEGameKit – Cocoa Library Project

下面是开发环境和工具使用:

Flash Professional CS5.5 (编译工具)
Flash Builder 4.5.1
AIR 3.2 SDK
Xcode 4.2 + iOS SDK 5
开发环境 Mac 10.6.7
使用命令行直接调用ADT打包

如果你从未编译过ANE iOS项目,我建议你先看看这篇文章,然后下载实例:

GameCenterExample.zip

 

局域网多人游戏的核心类与功能

GameKit的第二个重要特性是为设备之间提供了局域网或者蓝牙的连接功能。与GKMatch类似,GameKit提供了GKSession类来在本地保存连接信息,GKSession中每个Peer的信息都存在一个GKPeer的实例中,各个玩家之间通过实现对GKSessionDelegate的接口来实现数据和状态的交互。

在请求连接之前需要给GKSession创建一个ID,只有按照相同的ID寻找的玩家才会相互看见对方,GameKit会采用应用的BundleID作为GKSession的默认ID,这个ID还大有用处,下文中会详细介绍。由于局域网连接不需要登陆,所以玩家的信息需要本地创建,每个Peer在公开自己的时候都需要显示一个display name,如果不指定这个名字,系统会默认使用设备的名字来代替。 下面就是公开搜寻其他玩家的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
FREObject requestPeerMatch(FREContext ctx,void* funcData, uint32_t argc, FREObject argv[]){
    const uint8_t* myName = nil;
    uint32_t len = -1;
    FREGetObjectAsUTF8(argv[0], &len, &myName);
 
    GKSession* session = [[GKSession alloc] initWithSessionID:nil 
                displayName:omyName
                sessionMode: GKSessionModePeer ];
    observer.gameSession = session;
 
    [session setDataReceiveHandler:observer withContext:nil];
    session.delegate = observer;
    session.available = YES;
 
    FREObject reVal;
    FRENewObjectFromUTF8((uint32_t)[session.peerID length], (const uint8_t*)[session.peerID UTF8String], &reVal);
    return reVal;
}

上面的代码中,GKSession:initWithSessionID:displayName:sessionMode是创建GKSession的方法,可以给initWithSessionID带一个参数来指定session id。由于GKSession不象GKMatchRequest可以指定一个玩家的最多和最少的数量,所以如果你希望建立一个特定的游戏人数,并且希望只有相同意愿的玩家才可以发现对方,便可以将这个游戏人数加在session id里。另外,你也可以将不同的应用版本置入这个id,保证游戏的各方使用的是相同的游戏版本。

———————–
如何设计局域网游戏的网络结构

使用GKSessionMode可以指定自己的身份,这个参数可以有三个值:GKSessionModeServer,GKSessionModeClient,GKSessionModePeer。

Server – Client 模式

如果你的游戏是一方创建,其他方加入的模式,创建方可以选择GKSessionModeServer,加入方可以选择GKSessionModeClient。在创建游戏的时候需要注意设置GKSession的available为YES,使设备可见。给GKSession对象添加一个Delegate回调函数用来实现后续的功能接口:

session: peer:didChangeState:(当Peer状态发生改变的时候触发)
常用的state值如下:
GKPeerStateAvailable 设备状态变为可用
GKPeerStateUnavailable 设备状态变为可用
GKPeerStateConnected 设备状态变为连接
GKPeerStateDisconnected 设备状态变为失去连接

比如下面是用来判断设备可用的代码,当发现设备的时候会调用这个方法,然后创建一个XMLList,作为事件参数派发给Flash。Flash便可以在前端显示被发现的设备的信息,创建一个UI来等待用户点击加入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (void)session:(GKSession *)session peer:(NSString *)peerID didChangeState:(GKPeerConnectionState)state
{
    NSMutableString* retXML = [[NSMutableString alloc] initWithString:@"<p>"];
    [retXML appendFormat:@"<i>%@</i>", peerID];
    [retXML appendFormat:@"<a>%@</a>", [session displayNameForPeer:peerID]];
 
    switch (state)
    {    
        case GKPeerStateAvailable:
            [retXML appendFormat:@"<s>available</s>"];
            break;
        default:
            break;
    }
    [retXML appendFormat:@"</p>"];
    FREDispatchStatusEventAsync(g_ctx, (const uint8_t*)"player_status_changed",(const uint8_t*)[retXML UTF8String]);
}

如果用户选择了一条服务器并点击加入,则通过调用GKSession的connectToPeer方法发送连接请求:

1
2
3
4
5
6
7
8
9
10
FREObject joinServer(FREContext ctx,void* funcData, uint32_t argc, FREObject argv[]){
    const uint8_t* peerID = nil;
    uint32_t len = -1;
    FREGetObjectAsUTF8(argv[0], &len, &peerID);
 
    uint32_t timeInterval = 10000;
 
    [observer.gameSession connectToPeer:[NSString stringWithUTF8String:(const char *)peerID] withTimeout:timeInterval];
    return nil;
}

这时在服务器端会触发GKSessionDelegate的另一个接口方法:
session: didReceiveConnectionRequestFromPeer:

1
2
3
4
5
6
7
- (void)session:(GKSession *)session didReceiveConnectionRequestFromPeer:(NSString *)peerID{
    NSMutableString* retXML = [[NSMutableString alloc] initWithString:@"<p>"];
    [retXML appendFormat:@"<i>%@</i>", peerID];
    [retXML appendFormat:@"<a>%@</a>", [session displayNameForPeer:peerID]];
    [retXML appendFormat:@"</p>"];
    FREDispatchStatusEventAsync(g_ctx, (const uint8_t*)"received_client_request",(const uint8_t*)[retXML UTF8String]);
}

把这个事件派发给Flash后,Flash负责创建UI来提示用户是否接收对方的连接请求。如果选择接收,需要调用GKSession的acceptConnectionFromPeer方法,否则需要调用denyConnectionFromPeer:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
FREObject acceptPeer(FREContext ctx,void* funcData, uint32_t argc, FREObject argv[]){
    const uint8_t* peerID = nil;
    uint32_t len = -1;
    FREGetObjectAsUTF8(argv[0], &len, &peerID);
 
    [observer.gameSession acceptConnectionFromPeer:[NSString stringWithUTF8String:(const char *)peerID] error:nil];
 
    return nil;
}
 
FREObject denyPeer(FREContext ctx,void* funcData, uint32_t argc, FREObject argv[]){
    const uint8_t* peerID = nil;
    uint32_t len = -1;
    FREGetObjectAsUTF8(argv[0], &len, &peerID);
    [observer.gameSession denyConnectionFromPeer:[NSString stringWithUTF8String:(const char *)peerID]];
    return nil;
}

客户端同样通过session: peer:didChangeState:接口来获取结果,如果请求被接受了,就会收到GKPeerStateConnected状态,否则是GKPeerStateDisconnected。

Server-Client模式的好处是可以由Server端来控制游戏,包括游戏设置和开始游戏,游戏的数据也会通过Server在各个客户端实现同步。但是这种模式下,每个客户端都只和服务器端建立连接,一旦服务器掉线,则每个客户端都失去了连接。所以我不是很推荐用这种方法来构建多人游戏。

Peer Peer模式

如果两个设备都使用了GKSessionModeServer模式来寻找玩家,他们会互为Peer。这和使用GKSessionModePeer的模式是一样的。在更多情况下我们需要参与游戏的各方玩家都是平等的,即便有一个玩家掉线也不会影响到其他人,所以我推荐使用GKSessionModePeer的方式来构建网络。

但是有一点需要考虑,GKSession支持最多16个玩家互为Peer,如果每一个玩家在搜索游戏时都会显示一大堆列表,而他需要和每一个Peer建立连接,同时还要接受其他每一个人的连接请求,那这一定是最糟糕的用户体验。

我推荐的方式是这样的,在寻找设备之前先确定这个游戏的参与玩家个数,带着这个意愿去寻找志同道合的玩家。只要发现匹配的设备就会自动加入,只要发现加入请求就会自动同意。这样玩家就会自动进入游戏,不需要点来点去。开发者需要考虑的是在互为Peer的玩家中选举一个“服务器”来负责创建游戏初始化数据,这一点和使用Game Center建立多人游戏遇到的情况一模一样。实际上使用Peer Peer的模式,Flash端可以用相同的代码来处理局域网和Game Center两种模式下的游戏连接体系,所以用这种模式更为合理。

图4,局域网连接的两种网络模式

———————–
标准界面PeerPicker的使用

GameKit为一种特定的情况准备了一个现成的标准UI,当只有两个设备都通过蓝牙用GKSessionModePeer的模式寻找设备的时候,可以显示一个GKPeerPickerController界面,实现寻找、发送请求以及接受连接的UI表现窗口。我个人认为这个功能更合适的是普通应用而非游戏类,比较适合双方建立连接后交换名片之类的。下面是实现GKPeerPickerController的代码:

1
2
3
GKPeerPickerController picker = [[GKPeerPickerController alloc] init];
picker.delegate = observer;
showModalViewController(picker)

用显示Leader board和GKMatchmaker同样的方式显示GKPeerPickerController,并且给它添加一个回调函数,实现对GKPeerPickerControllerDelegate的接口:
peerPickerController:didConnectPeer:toSession:

———————–
玩家数据的发送与接受

我强烈建议各位开发者使用自定义的界面来创建一个局域网的连接,这样会熟练掌握GKSession相关类的使用技巧。

当所有的玩家连接之后,就是处理玩家数据的交互了。GKSession创建数据交互的侦听的方法与GKMatch不同,并不是在Delegate中的一个接口,而是通过GKSession的setDataReceiveHandler:observer方法来实现的:

[session setDataReceiveHandler:observer withContext:nil];

我不是很理解为什么这里需要这样做,看起来和添加一个接口到Delegate没有什么区别。反正最终实现的方式可以是相同的。这里的handleReceivedData和之前Game Center多人游戏中使用的是同一个处理接收信息的函数。

1
2
3
4
5
- (void) receiveData:(NSData *)data fromPeer:(NSString *)peer inSession: (GKSession *)session context:(void *)context
{
    NSLog([NSString stringWithUTF8String:(const char*)[data bytes]]);
    handleReceivedData(data);
}
 

————————–
基于Game Center的多人游戏简介

Game Center的高级开发是建立多人实时联网的比赛。开发者可以选择使用Game Center提供的服务器来运行游戏,或者使用自己的服务器来作数据并发。由于我的案例是选择了前者,所以本文只对使用Game Center提供的服务器来介绍。

在Game Center中可以有选择地邀请好友,或者让服务器自己来寻找匹配的玩家。如果是邀请好友,首先两者必须在Game Center中是互为好友的关系,这样在邀请发出之后,好友就会收到一个Notification的邀请。一旦选择接受游戏,那么双方就会建立一个连接,当各方都成功连接之后,游戏就可以开始了。如果选择的是让Game Center自动寻找匹配玩家,服务器会对各个请求的发起者来自动配对,各方不一定是互为好友关系,只要他们的请求相同,比如参与游戏人数或者其他条件完全符合,就可以立即建立连接。通过Game Center服务器建立的比赛最多支持4个玩家同时在线,通过自己的服务器建立的比赛最多支持16个玩家同时在线。

图3 Game Center基于好友邀请的多人游戏模式

————————–
如何使用标准UI邀请朋友参加多人游戏

Game Center为游戏开发者提供了一个现成的邀请朋友界面。如下面的例子,通过GKMatchRequest类可以发送一个创建界面的请求,传递两个参数分别表示游戏需要的最少人数以及最多人数。FREGetObjectAsUint32可以将由Flash传递来的FREObject格式的参数转成Native的整型,寄存在变量min和max中。

标准邀请游戏界面的类是GKMatchmakerViewController,从代码中可以看出,显示GKMatchmakerViewController的方法和显示Leader board是完全一样的。注意mmvc.matchmakerDelegate = observer这一行,采用Delegate来获取回调函数是iOS SDK中标准的程序模式,只要observer实现了GKMatchmakerViewControllerDelegate的接口,便可以充当侦听器获取用户在这个界面上的所有动作指令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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。开发者需要在游戏最初的时候尽早为游戏邀请添加侦听器,而且注册处理邀请的侦听应该加在登陆成功的事件中。所以这个流程应该是 游戏初始化->登陆->注册处理邀请侦听:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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预设了所有的被邀请方。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[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);
        }
    };

————————–
如何建立Match的连接

实现GKMatchmakerViewDelegate的接口matchmakerViewController:viewController didFindMatch:match,便可以侦听到玩家连接成功的事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
- (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,便可以准备开始游戏了:

1
2
3
4
5
6
7
8
- (void)match:(GKMatch *)match player:(NSString *)playerID didChangeState:(GKPlayerConnectionState)state
{
    if (!matchStarted && match.expectedPlayerCount == 0)
    {
        matchStarted = YES;
        initializeMatchPlayers();        
    }
}

开始游戏之前需要做的最后一个动作是获取所有游戏中的玩家信息,这是一个异步的过程,需要用侦听器来侦听信息获取的结果。下例就是initializeMatchPlayers的所有代码,非常通俗易懂,如果你还不是非常理解,请参考我在之前介绍过的如何实现AS与Native之间的数据交换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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方法,可以选择发送给特定的玩家或者所有的玩家。下面是一个发送给特定玩家的代码段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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:来接收来自特定玩家的信息。如下例:

1
2
3
4
5
6
7
- (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]);
}
 

Game Center简介

Game Center是Apple iOS平台的游戏平台,通过Game Center应用程序可以登陆进入平台进行游戏管理和好友管理,每一款游戏可以借助平台的服务器创建自己的排行和成绩榜,玩家也可以在Game Center平台中邀请好友参加某一款游戏,实现好友之间的多人对战。Game Center的功能和显示类库在GameKit框架中,集成了GameKit API的游戏,可以在游戏内部登陆Game Center服务器,实现等同于在平台应用内的操作。开发者可以按照游戏的风格自定义邀请好友、排行榜的显示界面,也可以调用GameKit的标准显示界面。

我个人认为Game Center更高的价值在于它提供了一套支持多人实时游戏的API,开发者可以依靠Game Center提供的服务器实现多人共享一个Match对象,完成多人在线游戏。也可以通过自己的服务器来完成更为复杂的在线游戏。

—————————
Game Center的核心类与功能

Game Center的类名都是以GK开头,比如玩家信息在GKPlayer和GKLocalPlayer里,排行榜的标准显示界面是GKLeaderboardViewController,排行榜的信息在GKLeaderboard里。比赛的信息在GKMatch里,申请比赛的标准窗口是GKMatchmakerViewController类,等等。这些在接下来的章节中会慢慢地介绍。

—————————
登陆Game Center

Game Center的用户系统与Apple的帐号是完全分开的,玩家可以自行创建若干个Game Center帐号。帐号可以在Game Center的应用中登陆,或者在运行含有GameKit API的游戏时,在需要登陆的时候通过弹出的登陆窗口来登陆。从开发者的角度来说,需要首先判断玩家的设备是否支持Game Center的功能,然后尽可能早的登陆Game Center。登陆后的账号信息会保存在一个静态的GKLocalPlayer实例中:

1
2
3
4
5
6
7
8
9
10
//获取静态实例localPlayer,然后使用authenticateWithCompletionHandler登陆
 GKLocalPlayer *localPlayer = [GKLocalPlayer localPlayer];
[localPlayer authenticateWithCompletionHandler:^(NSError *error) {
        GKLocalPlayer *lp = [GKLocalPlayer localPlayer];
        if(lp.isAuthenticated){
            //登陆成功,
        }else{
           //登陆失败
        }
    }];

注意:如果在登陆成功之前使用GameKit的其他类,很可能会出现未知的错误,所以建议越早登陆越好。

—————————
通过ANE实现Native与ActionScript的基础通信

当用户登陆Game Center之后,本地玩家的用户信息将会存储在localPlayer对象中,包括玩家ID,玩家显示名称,好友列表等等。这些信息有的需要显示在Flash创建的前端界面中。作为ANE的开发者,需要掌握如何从OBJC中向Flash返回函数值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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){
            //OBJC中的字符串类型是NSString,格式如@"a"
            NSMutableString* retXML = [[NSMutableString alloc] initWithString:@"<p>"];
            [retXML appendFormat:@"<i>%@</i>",lp.playerID];
            [retXML appendFormat:@"<a>%@</a>",lp.alias];
            [retXML appendFormat:@"</p>"];
 
            //向Flash派发事件的参数字符串类型是uint8_t,在ActionScript中注册StatusEvent.STATUS事件的侦听器,
            //"authenticate_status_changed"会保存在StatusEvent.code里,retXML会保存在StatusEvent.level里。
            FREDispatchStatusEventAsync(g_ctx, (const uint8_t*)"authenticate_status_changed",(const uint8_t*)[retXML UTF8String]);
        }else{
            FREDispatchStatusEventAsync(g_ctx, (const uint8_t*)"authenticate_status_changed",(const uint8_t*)"");
        }
    }];
    return nil;
}

上面这个例子是一个FREFunction的函数体,也就是可以被Flash调用的FRE API(如果你对如何在Flash端调用Native函数还不是很清楚,我建议你先看看这篇文章)。例子里用一个retXML来保存玩家的基本信息,然后通过FREDispatchStatusEventAsync来派发给Flash。这是Native向AS的一个标准的事件派发实例。

下面是在FlashRuntimeExtension.h中对FREFunction的定义:

1
2
3
4
5
6
typedef FREObject (*FREFunction)(
        FREContext ctx,
	void*      functionData,
        uint32_t   argc,
        FREObject  argv[]
);

前三个参数在函数执行的时候基本不用考虑,我们只要注意第四个就可以了。Flash调用Native函数的时候,参数寄存在argv这个数组中,类型是FREObject。比如下面的这个例子,是从Flash端调用Native的alert窗口,并且显示标题和内容。例子的核心在于如何从argv中取出参数中的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
FREObject alert(FREContext ctx,void* funcData, uint32_t argc, FREObject argv[]){
    //先定义两个变量,用来寄存标题和内容。
    const uint8_t* title = nil;
    const uint8_t* msg = nil;
    uint32_t len = -1;
 
    //使用FREGetObjectAsUTF8,从argv中取出相应的参数值,然后存放到title和msg对应的指针中。
    //这是通过FRE API实现从FREObject中向Native变量赋值的典型形式。
    FREGetObjectAsUTF8(argv[0], &len, &title);
    FREGetObjectAsUTF8(argv[1], &len, &msg);
 
 
    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:[NSString stringWithUTF8String:(const char *)title]
                                                    message:[NSString stringWithUTF8String:(const char *)msg] delegate:nil 
                                          cancelButtonTitle:@"Cancel" 
                                          otherButtonTitles:nil,nil];
    [alert show];
 
    return nil;
}

其实不管是什么类型,字符串、数字、布尔职甚至数组、位图对象等等,只要是从Flash传递过来的,或者是传递给Flash去的,都必须定义为FREObject类型。比如下面这个例子是判断设备是否支持Game Center:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
FREObject isGCAvailable(FREContext ctx, void* funcData, uint32_t argc, FREObject argv[]) {
    BOOL localPlayerClassAvailable = (NSClassFromString(@"GKLocalPlayer")) != nil;
 
    // Game Center必须运行在iOS 4.1以上的环境。
    NSString *reqSysVer = @"4.1";
    NSString *currSysVer = [[UIDevice currentDevice] systemVersion];
    BOOL osVersionSupported = ([currSysVer compare:reqSysVer options:NSNumericSearch] != NSOrderedAscending);
 
    // 定义一个准备回传给Flash的类型。
    FREObject reVal;
    //通过FRE的FRENewObject... 来给FREObject赋值。&reVal是对reVal指针的操作,
    //可以将(localPlayerClassAvailable && osVersionSupported)的值作为ActionScript的布尔对象赋给变量reVal
    FRENewObjectFromBool((localPlayerClassAvailable && osVersionSupported),&reVal);
    return reVal;
};

图2 ActionScript与Native之间的数据通讯

—————————
标准Leader board的显示与移除

很多集成Game Center的游戏都只是为了显示玩家的得分以及排行,要实现这个功能,须要首先在iTunesConnect里启用Game Center并定义排行榜。这个部分的功能比较简单,作为技术文章我就不做具体的介绍了。

GameKit提供了一个现成的Leader board界面,调用这个界面即可以展示该应用的排行榜。下面就是如何调用这个标准的界面:

1
2
3
4
5
6
7
8
9
10
11
12
FREObject showLeaderBoard(FREContext ctx, void* funcData, uint32_t argc, FREObject argv[]) {
    GKLeaderboardViewController *leaderboardController = [[GKLeaderboardViewController alloc] init];
    if (leaderboardController != nil)
    {
        const uint8_t* category = nil;
        uint32_t len = -1;
        leaderboardController.leaderboardDelegate = observer;
        showModalViewController(leaderboardController);
    }else{
    }
    return nil;
}

iOS SDK中很多标准的界面都是继承了UIViewController,但是AIR应用并不是建立在UIViewController的架构之上的,所以如何在AIR的应用中显示UIViewController就是一个知识点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void showModalViewController(UIViewController* viewController){
    if(currentModalViewController!=nil){
        dismissModalViewController(currentModalViewController);
    }
 
    //找到AIR应用的最顶层View对象,一个UIWindow对象。
    UIWindow *window = [[UIApplication sharedApplication] keyWindow];
 
    //建立一个空的UIViewController对象,用来做显示Leader board的容器
    UIViewController *parentViewController = [[UIViewController alloc] init];
 
    //将容器加在AIR的root层View对象之上。
    [window addSubview:parentViewController.view];
 
    //用显示独占窗口的方式显示Leader board,因为presentModalViewController这个方法只存在于UIViewController类中,
    //这就是为什么使用一个容器来作为中间层。
    [parentViewController presentModalViewController:viewController animated:YES];
 
    currentModalViewController = viewController;
}

打开Leader board之后,须要给leader board窗口添加一个回调函数Delegate,用来接收关闭窗口的事件。这个Delegate类是GKLeaderboardViewControllerDelegate,接收关闭窗口的方法是leaderboardViewControllerDidFinish。在 OBJC中实现Delegate的定义,其实等同于ActionScript中对接口的实现。比如自定义一个MyDelegate类,在MyDelegate.h里须要用下面的代码进行声明:

1
2
3
4
@interface MyDelegate : NSObject <GKLeaderboardViewControllerDelegate>{
}
- (void)leaderboardViewControllerDidFinish:(GKLeaderboardViewController *)viewController;
@end

然后在MyDelegate.m里实现方法的函数体:

1
2
3
- (void)leaderboardViewControllerDidFinish:(GKLeaderboardViewController *)viewController{
    dismissModalViewController(viewController);
}

移除UIViewController的代码也非常易懂:

1
2
3
4
5
6
7
8
void dismissModalViewController(UIViewController* viewController){
    if(viewController !=nil){
        UIViewController *parentViewController = [viewController parentViewController];
        [parentViewController dismissModalViewControllerAnimated:NO];
        [parentViewController.view removeFromSuperview];
        currentModalViewController = nil;
    }
}

—————————
如何上传游戏得分

登陆用户只能上传自己的得分,一个应用可能包含了多个排行,每一个排行都有一个category的ID,在上传得分的时候必须要指明这个ID。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
FREObject reportScore(FREContext ctx,void* funcData, uint32_t argc, FREObject argv[]){
    const uint8_t* category = nil;
    uint32_t len = -1;
 
    if(FREGetObjectAsUTF8(argv[0], &len, &category) == FRE_OK){
        GKScore *scoreReporter = [[GKScore alloc] initWithCategory:[NSString stringWithUTF8String:(const char *) category]];
        scoreReporter.value = 1010;
        [scoreReporter reportScoreWithCompletionHandler:^(NSError *error) {
            if (error != nil)
            {
            }else{
            }
        }];
    }
    return nil;
}
 

ANE与GameKit(一)概览

On November 26, 2011, in ANE - GameKit, by James

下周是2011的After MAX China大会,我将给大家介绍一下ANE在iOS上的高级开发,包括IAP和GameKit两个部分。本系列的文章会重点围绕GameKit的两个重要功能-Game Center和Peer to Peer来进行详细的技术解说。希望这个系列的教程能够让广大的Flash移动开发者解渴,并且帮助大家在iOS上创作出更多优秀的作品。本文还将提供一个GameKit的案例,并免费开放源码。这也许是我作为Adobe Evangelist为各位贡献的最后一篇长篇教程,但作为一名Flash技术爱好者,我会继续为大家分享开发中的经验,和各位继续交流。谢谢大家的支持!

GameKit是iOS SDK的产品制做框架之一。三个核心内容包括交互游戏平台Game Center,Peer 2 Peer 设备通讯功能以及In-Game Voice。本文主要介绍的是前两个,Game Center和Peer 2 Peer。考虑到GameKit的开发过程有很大比例是在Native里开发的,所以本系列也会着重介绍一些Objective-C的代码,为了方便大家的理解,我会适当地做一些基础 OBJC 的语法介绍,纯属个人理解与学习的结果,希望OBJC的高手能够给予帮助和指正。

图1,GameKit的三个主要功能

本系列教程可以展开为如下几个部分:

(一)概览
ANE的基础
开发工具与准备事项

(二)Game Center的基础开发以及基于Leader board的操作
Game Center简介
Game Center的核心类与功能
通过ANE实现Game Center与ActionScript的通信
标准Leader board的显示与移除
如何上传游戏得分

(三)基于Game Center服务器的多人游戏(Match)
基于Game Center的多人游戏简介
如何使用标准UI邀请朋友参加多人游戏
如何设计接受朋友邀请
如何建立Match的连接
如何设计游戏的网络结构以及实现数据的传递

(四)基于局域网连接的多人游戏
局域网多人游戏的核心类与功能
如何设计局域网游戏的网络结构
标准界面PeerPicker的使用
玩家数据的发送与接受

(五)实例

-------------------------------------------
下面进入正文:

—————————
ANE的基础

ANE,全称AIR Native Extension,是AIR在3.0版本开始具备的特性,功能顾名思义,就是针对原生操作系统的扩展。再进一步地解释,就是说可以将ActionScript语言创建的类库与原生语言创建的类库打包编译成一整套双向通信的类库,实现Flash技术利用操作系统层面的资源实现更多的功能。这个我已经在前面的教程中详细介绍过了,有不理解的朋友请往前翻阅。

ANE这个概念包括几个部分,Native类库,FRE扩展API,以及ActionScript类库。FRE 也就是FlashRuntimeExtension API,是一套用Native语言创建的专门用来解析和构造ActionScript对象的API,也是Native类和AS类之间通信的中间件,通过FRE可以在AS端实现Native方法的直接调用和参数传递,而在Native端可以实现函数值的同步返回,或者事件异步派发,还可以针对AS传递过来的BitmapData和ByteArray对象进行内存指针操作,甚至可以创建ActionScript的基础类。利用ANE的技术,可以兼具Flash在前端的表现和交互优势,和Native在底层的运算以及高权限,来创作更具交互性更具用户体验的高性能应用程序。

—————————
开发工具与准备事项

开发ANE的过程比较复杂,但如果掌握了其中的规则也是非常得心应手的。
针对本文将要介绍的 iOS 系统,开发者需要具备Apple iOS平台开发者权限,Xcode开发工具,iOS SDK 4+,以及Flash开发工具, Flash Profession,或者Flash Builder。对于Flash Pro和Flash Builder没有具体的版本要求,但需要能安装和编译AIR 3.0 SDK。

关于如何获得Apple iOS平台开发者权限,请参考我之前的 Flash开发iOS应用全攻略 系列教程。

关于如何搭建ANE的开发环境,请参考我之前的 ANE开发iOS应用内付费(IAP)全教程