Unity3D脚本调用Objective C代码实现游戏内购买

2016年11月22日 15:23 0 点赞 0 评论 更新于 2025-11-21 20:50

一、Unity3D脚本调用OC代码的原理

OC与C是互通的,而C#可以通过DllImport的形式调用C代码,这就为Unity3D脚本调用OC代码搭建了沟通的桥梁,具体实现将在后续内容中详细介绍。

二、实现iOS内购买

本文将全面且详细地阐述从iOS购买到C#调用的整个过程。笔者首次在iOS平台编写内购代码,经过大量学习和反复测试,积累了一些经验。

(一)必须了解的第一个delegate:SKProductsRequestDelegate

从名称可以看出,SKProductsRequestDelegate中的方法用于请求产品信息。在购买流程中,首先需要通过iTunes上的产品ID查询产品信息,获取SKProduct对象后,再使用该对象进行购买。以下是获取产品信息的代码实现:

// 通过产品id数组获取产品的信息
-(void)getProductsInfo:(NSMutableArray *)productsIDArray
{
NSSet *productIdentifiers = [NSSet setWithArray:productsIDArray];
SKProductsRequest* productRequest = [[SKProductsRequest alloc]initWithProductIdentifiers:productIdentifiers];
productRequest.delegate = self;

if (self.skProductsRequest != nil) {
[self.skProductsRequest cancel];
self.skProductsRequest = nil;
}
self.skProductsRequest = productRequest;
[self.skProductsRequest start];
[productRequest release];
[productsIDArray release];
}

上述代码中,self.skProductsRequest是一个属性,在此不做详细说明。由于采用非ARC模式,因此需要进行release操作。productsIDArray是一个产品ID数组,其内容来自iTunes的产品ID,释放该数组是有原因的,后续代码会进行解释。调用start函数后,便开始了产品信息的查询。当有产品信息返回时,会调用以下两个方法中的一个:

#pragma mark -
#pragma mark - SKProductsRequestDelegate Methods

- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
{
// 购买时,获取到产品信息时的处理
// 如果拿到的产品列表为空,则按照购买失败处理
if (response.products == nil || response.products.count == 0) {
UnitySendMessage("Interface", "purchaseFailed", "productsRequest error");
}

// 获取到产品信息后,开始购买第一个产品。目前一次只能购买一个产品。
SKPayment *payment = [SKPayment paymentWithProduct:response.products[0]];
[[SKPaymentQueue defaultQueue] addPayment:payment];
}

- (void)request:(SKRequest *)request didFailWithError:(NSError *)error
{
// 目前进入游戏后如果产品信息获取失败,不做任何的处理
// 购买时,如果product info获取失败调用购买失败的处理函数
NSString *errorStr;
if (error != nil) {
errorStr = error.description;
} else {
errorStr = @"purchaseFailed";
}
UnitySendMessage("Interface", "purchaseFailed", [errorStr UTF8String]);
}

显然,第一个方法在请求成功时调用,第二个方法在请求失败时调用。UnitySendMessage函数是U3D的函数,需要注意的是,OC代码工程需要通过Unity3D IDE生成,这样才会有该函数,它是OC和U3D通信的重要方式。理论上,使用函数指针同样可以实现回调函数的调用,在C#中调用C时可以将delegate转换为函数指针。UnitySendMessage函数的第一个参数是GameObject名称,第二个参数是该GameObject下挂载的任何脚本中的一个函数名,第三个参数是函数的参数。

以下两句代码是购买操作的关键:

SKPayment *payment = [SKPayment paymentWithProduct:response.products[0]];
[[SKPaymentQueue defaultQueue] addPayment:payment];

这只是一个示例,代码中仅购买产品数组中的第一个产品。生成一个SKPayment对象,并将其添加到[SKPaymentQueue defaultQueue]中,此时会自动开始购买流程。为了获取购买过程的状态和最终结果,需要实现SKPaymentTransactionObserver中的方法。在类的init方法中添加以下代码:

[[SKPaymentQueue defaultQueue] addTransactionObserver:self];

(二)必须了解的第二个delegate:SKPaymentTransactionObserver

delegate中定义了一些方法,用于监控购买交易的整个过程。具体代码如下:

#pragma mark -
#pragma mark - SKPaymentTransactionObserver Methods
// 产品购买过程中的状态
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
{
for (SKPaymentTransaction *transaction in transactions) {
switch (transaction.transactionState) {
case SKPaymentTransactionStatePurchased:
[self completeTransaction:transaction];
break;
case SKPaymentTransactionStateFailed:
[self failedTransaction:transaction];
break;
case SKPaymentTransactionStateRestored:
[self restoreTransaction:transaction];
break;
default:
break;
}
}
}

// 购买完成的处理
- (void)completeTransaction:(SKPaymentTransaction *)transaction
{
// 通知u3d购买成功,返回product ID
UnitySendMessage("Interface", "purchaseSuccessful",[[NSString stringWithFormat:@"%@,%@",transaction.payment.productIdentifier,transaction.transactionIdentifier] UTF8String]);
[[SKPaymentQueue defaultQueue] finishTransaction: transaction];
}

// 购买失败的处理
- (void)failedTransaction:(SKPaymentTransaction *)transaction
{
// 通知u3d购买失败
// 错误信息为transaction.error.code
// SKErrorUnknown,
// SKErrorClientInvalid, // client is not allowed to issue the request, etc.
// SKErrorPaymentCancelled, // user cancelled the request, etc.
// SKErrorPaymentInvalid, // purchase identifier was invalid, etc.
// SKErrorPaymentNotAllowed, // this device is not allowed to make the payment
// SKErrorStoreProductNotAvailable, // Product is not available in the current storefront
UnitySendMessage("Interface", "purchaseFailed", [[NSString stringWithFormat:@"%d",transaction.error.code] UTF8String]);
[[SKPaymentQueue defaultQueue] finishTransaction: transaction];
}

// 购买到永久有效物品的处理
- (void)restoreTransaction:(SKPaymentTransaction *)transaction
{
// 通知u3d购买成功
UnitySendMessage("Interface", "purchaseSuccessful",[[NSString stringWithFormat:@"%@,%@",transaction.payment.productIdentifier,transaction.transactionIdentifier] UTF8String]);
[[SKPaymentQueue defaultQueue] finishTransaction: transaction];
}

核心方法是- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions,在一次购买过程中该方法会被多次调用,我们主要关注购买成功和失败的状态,针对这两种状态分别进行专门的函数处理。处理函数较为简单,在此不再赘述。

在交易过程中,需要注意以下情况:如果用户在游戏内点击购买后,立即将游戏置于后台或退出游戏(APP进程被杀死),系统仍会弹出购买询问框。无论最终是否购买,此时我们的代码可能无法执行。不过,经过反复测试,当应用回到前台或再次启动后,updatedTransactions函数仍会运行。初步推测,苹果在云端记录了APP是否结束本次交易,而结束一次交易的方法是调用[[SKPaymentQueue defaultQueue] finishTransaction: transaction];,因此建议在交易完成的处理函数中添加该处理。

另外,还有一个- (void)paymentQueue:(SKPaymentQueue *)queue removedTransactions:(NSArray *)transactions函数,该函数会在finishTransaction函数调用后执行。Apple建议:“Your application does not typically need to implement this method but might implement it to update its own user interface to reflect that a transaction has been completed.” 因此,如无特殊需求,无需实现该方法。

最后,不要忘记在dealloc函数中执行以下代码:

[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];

至此,iOS内购部分完成。

三、U3D脚本调用OC函数

前文已经解释了C#脚本调用OC函数的原理,下面来看具体的代码实现。首先,需要在OC代码中实现C函数的声明和实现:

#pragma mark -
#pragma mark - C Methods Imp

// 函数定义
#ifdef __cplusplus
extern "C" {
#endif
extern void getProductsInfo(char ** productsIDArray, int count);
#ifdef __cplusplus
}
#endif

// 函数实现
#ifdef __cplusplus
extern "C" {
#endif
static AEPurchaseManager *aePurchaseManager;

void getProductsInfo(char ** productsIDArray, int count)
{
if (aePurchaseManager == NULL)
{
aePurchaseManager = [[AEPurchaseManager alloc] init];
}

NSMutableArray* array = [[NSMutableArray alloc] init];

for (int i = 0; i < count; i++)
{
[array addObject: [NSString stringWithCString: productsIDArray[i] encoding:NSASCIIStringEncoding]];
}
[aePurchaseManager getProductsInfo:array ];
}
#ifdef __cplusplus
}
#endif

OC的优势在于可以直接与C进行混合编程,这在上述代码中得到了很好的体现。在函数声明中,需要注意不能使用OC中的类型,例如NSString。以下是在C#脚本中的实现:

[DllImport("__Internal")]
private static extern void getProductsInfo(string[] productsIDArray, int count);

只需声明函数即可,需要注意的是,OC的.h.m文件需要放置在Unity工程中。另外,生成iOS工程文件后,还需将这些文件添加到iOS工程中,因为最终在iOS上生成游戏安装包的是Xcode。

关于OC代码文件的补充说明

实际上,前文所述的将OC代码文件放置在Unity工程和iOS工程中的方法,笔者并未采用。我使用的方法是在Unity工程中创建Plugins文件夹,再在其中创建子文件夹iOS,具体原因可参考http://wiki.unity3d.com/index.php/Special_Folder_Names_in_your_Assets_Folder 。这样,在生成Xcode工程时,OC代码文件会自动包含在Xcode工程中。

作者信息

孟子菇凉

孟子菇凉

共发布了 3994 篇文章