[原創]cocos2d-x + Lua接入iOS原生SDK的實現方案
相信很多朋友在使用cocos2d-x+lua開發遊戲時都遇到過接入iOS原生SDK的問題,比如常見的接應用內支付SDK,廣告SDK或是一些社交平台SDK等等,我也沒少接過這類SDK。這篇文章主要是對我做過項目中接入iOS原生SDK實現方案的一個總結,在這裡分享給大家,希望對自己和大家的開發工作都有幫助。
在展開正文之前,先做幾點說明:
1.我這裡說的iOS原生SDK是指那些完全用Objective-C語言開發,為原生iOS程序設計的SDK。swift很好很強大,不過我還沒用過,慚愧,不過語言終歸只是表達方式而已,解決問題的思路都是一樣的;
2.這裡假設遊戲的主要邏輯使用lua實現,對於主要邏輯使用C++實現的,用本文的思路一樣可行,並且設計上更簡單,接著往下看就知道了:)
3.本文以quick-cocos2d-x 2.1版本為例進行講解,主要因為這個是我之前做項目用得最多的一個版本,-x新版本變動比較大,但是,還是那句話,解決問題的思路是相同的。
-------------------正式開始的分割線-------------------
好了,我們正式開始。開門見山!
解決這種接入問題,實際上最主要是解決不同語言交互的問題,一旦跨過語言交互的障礙,剩下的事情就so easy!
由於涉及到Lua,C++,Objective-C三種語言,這個問題表面上看起來錯綜複雜,但其實只要冷靜地(=。=)梳理,我們便可以得到正確的思路。
因為我們遊戲的邏輯主要是用Lua實現的(前面已經做過假設),而SDK是用Objective-C實現,所以這裡我們需要解決Lua與Objective-C的交互問題,即最終希望達到的目標是,在Lua層面「調用」Objective-C的代碼(注意這裡的調用是加引號的,間接的調用),而當Objective-C層面收到SDK的回調,再通知Lua。我們知道,Lua並沒有簡單的方法直接和Objective-C交流,但是Lua可以通過Lua Binding和C/C++交流,而我們又知道,C++和Objective-C可以混編,即C++可以直接調用(這裡調用沒引號,是真的直接調用)Objective-C的代碼。想到這裡,思路就很明顯了,我們可以使用C++為Lua和Objective-C的交互充當橋樑,進而實現Lua到Objective-C的交互。
根據上面的分析,我們可以用如下圖表達我們的思路,我們這裡將語言交互的過程分成了4個小部分:
整個語言交互的過程可以總結為:Lua調用Lua Binding的C++接口,C++接口調用混編的Objective-C接口,而Objective-C通過block形式的回調,將結果通知給C++,C++通過Lua的C API將最終結果返回給Lua。這樣一趟下來,就完成了Lua與Objective-C的整個交互過程。
簡單的說一下這4部分:
1.Lua Binding
將C/C++接口導出給Lua調用的方法,由於篇幅的原因這裡就不展開了,具體可以參考Lua的文檔,以及網上其他地方的文章。
2.混編
Objective-C的一大優點就是可以和C與C++混編使用,就像同一個語言一樣共存在一個實現文件裡面。具體混編規則也不說了,這裡只提兩個小細節:
一,在XCode下混編的實現文件後綴是.mm,而不能是.cpp或者是.m;
二,混編的實現文件引用頭文件的地方,C++或者C的用#include,而Objective-C用#import,相互沒有影響。
3.Block回調
Block是Objective-C一個非常棒的特性,更棒的是在Block裡面還可以直接寫C++代碼:)具體想瞭解的可以看蘋果官方文檔。
其實在最初,我曾經嘗試過使用發送通知的方式來實現Objective-C對C++的回調,即Objective-C收到SDK回調,給C++部分發送附帶回調信息的通知,雖然cocos2d-x中有現成的NotificationCenter來幫助實現,但這種方式的一個顯而易見的弊端是大大增加了C++代碼和Objective-C代碼的耦合度,Objective-C部分也要混編C++調用C++的NotificationCenter發通知,C++部分也要混編Objective-C代碼,調用C++的NotificationCenter收通知,這種結構實在是有夠煩躁的。
相比之下,使用Block回調就乾淨利落太多,Objective-C這邊一切都是純粹的,它並不需要知道自己要被C++調用還是Objective-C調用,也不需要花很多精力在返回回調上,只需要干好自己的本職工作,然後在適當的時候調用Block就一切搞定。
4.Lua C API
Lua C API用於C/C++與Lua的交互,在cocos2d-x中這些C API已經被封裝成了更加易用的C++ Class API。這裡要提到的是,在用這套API調用Lua函數的時候,為了傳參,需要參數入棧的操作,這個入棧的順序影響到了Lua函數接受到參數的順序,不過好在規則很簡單:先入棧的參數排在前或者說是入棧順序和實參順序相同。舉例,如果C++這邊調用Lua函數func時,入棧的順序是A,B,C,那麼就是調用函數func(A,B,C)
-------------------漸入佳境的分割線-------------------
在整個語言交互的過程中,如果認為Lua是頂層,Objective-C是底層,那麼在實際遊戲中交互的過程就是一個自頂向下的過程,然而我們在實現各層級代碼的時候,需要自底向上完成,因為在頂層Lua代碼中的邏輯,是由底層Objective-C SDK的接口與功能決定的。即我們需要先根據SDK中原始的Objective-C的接口,做適合我們遊戲Objective-C封裝代理類,然後根據封裝結果實現C++的bridge接口,最後再實現Lua的對應邏輯。
根據以上分析,從層級的角度,設計如下:
除過Lua的邏輯,我們最重要需要實現的兩部分內容:C++ Bridge Class 和 Objective-C SDK Delegate Class。前者是起橋樑作用的接口類,原則上不做任何與遊戲邏輯相關的數據處理,而後者負責封裝原始的SDK接口,接收以及初步處理SDK回調數據。前者的實現依賴於後者的實現,而後者的實現又依賴於SDK。SDK取得的數據最終通過層層傳遞,交給Lua邏輯處理,最終保證對數據處理的遊戲邏輯儘可能多的放到Lua層中。
這樣設計的好處有很多,一方面,頂層的遊戲邏輯變動,不影響下層多語言交互代碼,另一方面,底層的SDK變動,如版本更新甚至更換,不影響上層遊戲邏輯,多層次結構有效地降低了複雜度,隔離了變化,對於頻繁的需求變更,這種結構也可以保證擴展的便利。
-------------------總結的分割線-------------------
綜上所述,解決接入iOS原生SDK的問題,主要需要4步:
1.根據SDK接口與功能實現Objective-C SDK Delegate Class;
2.根據Objective-C SDK Delegate Class實現對應的C++ Bridge Class;
3.根據C++ Bridge Class生成對應的Lua Binding代碼;
4.寫Lua層邏輯。
-------------------最後的分割線-------------------
好了,最後,又到了激動人心的上代碼的環節了:)下面就以某iOS第三方計費SDK為例,來說明下實現接入的步驟。
這個SDK只有一個頭文件GameBilling.h,主要使用到的方法和Protocol如下:(為了避免篇幅過長等原因,把註釋和不必要的代碼都刪掉了)。我用代碼註釋的方式說明了各方法的用途。
1 // 初始化計費SDK 2 + (GameBilling *)initializeGameBilling; 3 4 // 告訴SDK遊戲屏幕的Orientation,以便SDK展示正確的UI 5 - (void)setDialogOrientationMask:(UIInterfaceOrientationMask)orientationMask; 6 7 // 確認付費,顯示付費UI 8 - (void)doBillingWithUIAndBillingIndex:(NSString *)billingIndex isRepeated:(BOOL)isRepeated cpParam:(NSString*)cpParam; 9 10 // Delegate回調,告訴調用者付費是否成功等信息 11 @protocol GameBillingDelegate<NSObject> 12 @required 13 - (void)onBillingResult:(BillingResultType)resultCode billingIndex:(NSString *)index message:(NSString *)message; 14 @end
以上前兩個方法用於初始化SDK,並且和遊戲的邏輯沒什麼太大關係,所以我們把對他們的調用放在程序開始的位置,不必導出給Lua。第三個方法在用戶確認付費時使用,需要導出給Lua,當用戶在遊戲界面做相應操作時候調用。最後的delegate的回調,我們用前面提到的Objective-C SDK Delegate Class來接收,並作初步處理,再用Block傳給C++ Bridge Class.
好的,那我們先來完成Objective-C SDK Delegate Class。這裡這個Objective-C做成了個簡單的單例來使用,實際可能不需要這麼做。
先完成頭文件,這裡命名為CMGCIAPiOS.h,如下:
1 #import "GameBilling.h" 2 3 // 聲明Block 4 typedef void (^BillingResultCallback)(BOOL success, NSString *index,NSString *message); 5 6 @interface CMGCIAPiOS : NSObject<GameBillingDelegate> 7 { 8 GameBilling *_sdk; 9 NSString *_billingIndex; 10 BillingResultCallback _callback; 11 } 12 13 +(id)sharedInstance; 14 15 -(void)setDialogOrientationMask:(UIInterfaceOrientationMask)orientationMask; 16 17 -(void)doBillingWithUIAndBillingIndex:(NSString *)billingIndex 18 isRepeated:(BOOL)isRepeated 19 cpParam:(NSString*)cpParam 20 resultCallback:(BillingResultCallback)callback; 21 22 @end
應該很清楚,就不多做說明了。
下面是實現文件CMGCIAPiOS.m,如下:
1 #import "CMGCIAPiOS.h" 2 3 @implementation CMGCIAPiOS 4 5 static CMGCIAPiOS *_sharedInstance = nil; 6 7 + (id)sharedInstance 8 { 9 @synchronized(self) 10 { 11 if (_sharedInstance == nil) 12 { 13 _sharedInstance = [[CMGCIAPiOS alloc] init]; 14 } 15 } 16 return _sharedInstance; 17 } 18 19 -(id) init 20 { 21 if( (self = [super init]) ) 22 { 23 _sdk = [GameBilling initializeGameBilling]; 24 _sdk.delegate = self; 25 } 26 return self; 27 } 28 29 -(void)setDialogOrientationMask:(UIInterfaceOrientationMask)orientationMask 30 { 31 [_sdk setDialogOrientationMask:orientationMask]; 32 } 33 34 - (void)doBillingWithUIAndBillingIndex:(NSString *)billingIndex 35 isRepeated:(BOOL)isRepeated 36 cpParam:(NSString*)cpParam 37 resultCallback:(BillingResultCallback)callback 38 { 39 40 if (_callback != nil) 41 { 42 [_callback release]; 43 _callback = nil; 44 } 45 46 _callback = [callback copy]; // 注意要copy 47 48 [_sdk doBillingWithUIAndBillingIndex:billingIndex 49 isRepeated:isRepeated 50 cpParam:cpParam]; 51 } 52 53 #pragma mark - GameBillingDelegate 54 - (void)onBillingResult:(BillingResultType)resultCode 55 billingIndex:(NSString *)index 56 message:(NSString *)message 57 { 58 BOOL b = (resultCode == BillingResultType_PaySuccess || resultCode == BillingResultType_PaySuccess_Activated); 59 NSLog(@"billing = %@ %@ %@", b ? @"yes":@"no", index, message); 60 61 if (_callback != nil) 62 { 63 _callback(b,index,message); 64 65 // 調用完成就釋放掉 66 [_callback release]; 67 _callback = nil; 68 } 69 } 70 71 @end
可以看到對提到的幾個方法都做了封裝,並且接收了回調。
下面是C++ Bridge Class部分,頭文件CMGCIAP.h:
1 #include <iostream> 2 3 class CMGCIAP 4 { 5 public: 6 CMGCIAP(); 7 ~CMGCIAP(); 8 9 public: 10 static CMGCIAP *sharedInstance(); 11 12 bool init(); 13 14 void setDoBillingCallbackScriptHandler(int scriptHandler); // for lua callback 15 16 void doBillingWithUI(const char* billingIndex, 17 bool isRepeated, 18 const char* cpParam); 19 20 private: 21 22 int m_doBillingCallbackScriptHandler; 23 24 };
由於用cocos2d-x的tolua工具做Lua Binding的原因,我把設置Lua回調的方法單獨提出來了,如下:
1 void setDoBillingCallbackScriptHandler(int scriptHandler);
更好的做法是把這個scriptHandler放到下面這個函數中,這樣接口就可以和Objective的保持一致了。
1 void doBillingWithUI(const char* billingIndex, 2 bool isRepeated, 3 const char* cpParam);
不過也沒關係,獨立設置Lua回調函數也有更靈活的優點。
注意,C++ Bridge Class頭文件一定保持「純潔性」,做純粹的C++文件,不能出現Objective-C的任何代碼,否則就破壞了上面講到的層次結構。
下面是實現文件CMGCIAP.mm:
1 #include "CMGCIAP.h" 2 #include "cocos2d.h" 3 #include "script_support/CCScriptSupport.h" 4 5 #import "CMGCIAPiOS.h" 6 #import <Foundation/Foundation.h> 7 #import <UIKit/UIKit.h> 8 9 USING_NS_CC; 10 11 static CMGCIAP* s_sharedInstance = NULL; 12 13 CMGCIAP::CMGCIAP() 14 { 15 m_doBillingCallbackScriptHandler = 0; 16 } 17 18 CMGCIAP::~CMGCIAP() 19 { 20 21 } 22 23 CMGCIAP *CMGCIAP::sharedInstance() 24 { 25 if (s_sharedInstance == NULL) 26 { 27 s_sharedInstance = new CMGCIAP(); 28 } 29 30 return s_sharedInstance; 31 } 32 33 // init方法封裝了對SDK的初始化 34 bool CMGCIAP::init() 35 { 36 // 由於是豎屏的遊戲,所以這裡直接設置好了 37 [[CMGCIAPiOS sharedInstance] setDialogOrientationMask:UIInterfaceOrientationMaskPortrait]; 38 39 return true; 40 } 41 42 void CMGCIAP::setDoBillingCallbackScriptHandler(int scriptHandler) 43 { 44 m_doBillingCallbackScriptHandler = scriptHandler; 45 } 46 47 void CMGCIAP::doBillingWithUI(const char* billingIndex, 48 bool isRepeated, 49 const char* cpParam) 50 { 51 52 NSString *billingIndexString = [NSString stringWithUTF8String:billingIndex]; 53 54 NSString *cpParamString = [NSString stringWithUTF8String:cpParam]; 55 56 [[CMGCIAPiOS sharedInstance] doBillingWithUIAndBillingIndex:billingIndexString 57 isRepeated:isRepeated 58 cpParam:cpParamString 59 resultCallback:^(BOOL success, NSString *index,NSString *message){ 60 61 //通過Block將返回結果傳給Lua,Objective-C到C++的無縫連接:) 62 63 CCLuaStack *stack = CCLuaEngine::defaultEngine()->getLuaStack(); 64 stack->clean(); 65 stack->pushBoolean(success); 66 stack->pushString([index UTF8String]); 67 stack->pushString([message UTF8String]); 68 stack->executeFunctionByHandler(m_doBillingCallbackScriptHandler, 3); 69 70 }]; 71 }
好了,接下來只需要對C++ Bridge Class做Lua Binding,生成綁定文件,如果用tolua做綁定,綁定配置文件如下:
1 class CMGCIAP 2 { 3 static CMGCIAP *sharedInstance(); 4 5 void setDoBillingCallbackScriptHandler(LUA_FUNCTION nHandler); 6 7 void doBillingWithUI(const char* billingIndex, 8 bool isRepeated, 9 const char* cpParam); 10 }
OK,到這裡主要的編碼工作就完成了,記得要在程序的適當位置做好Lua Binding初始化工作。
如果一切順利,在以上工作完成後,在Lua裡面已經可以直接調用SDK的接口了,接下來的事情就靠你們了:)
呼~~~
文章到此也差不多了,整個思路和方案就是這些,如果有什麼地方不理解或者不明白,歡迎留言討論:)
謝謝收看!
轉載請註明出處,謝謝:)
沒有留言:
張貼留言