2013年11月11日 星期一

[Android] In-app Billing(App內付款) 概念 (1/2)

http://blog.tonycube.com/2013/02/android-in-app-billingapp-12.html


以下文章極大部份翻譯自官方文件,我也是 IAB 的初學者,所以如果有錯誤歡迎指正。

關於 In-app Billing API

應用程式(App)使用 API 透過裝置中內建的 Google Play app 來存取 In-app Billing 服務,Google Play app 會在你的 App 及 Google Play server 間傳送計費的請求及回應,你的 App 不會直接和 Google Play server 溝通,相反的,你的 App 是透過 IPC(Interprocess communication) 傳送付費的請求給 Google Play app 然後接收從 Google Play app 傳回的結果。你的 App 不用管理任何網路連線的事。


In-app Billing 只能在透過 Google Play 發佈的應用程式上執行。要完成 App 內付款的流程,Google Play app 必須能透過網路存取 Google Play server。

目前 Google Play 支援兩個版本的 In-app Billing API,V3及V2。

Version 3 (官方建議使用):

  • 使用較容易的方式從 Google Play 取得產品資料。
  • 訂購資訊在購買完成時同步傳送到裝置上。
  • 所有的購買行為是受管理的,意即 Google Play 持續追蹤使用者的內購商品擁有權。使用者不能擁有多個內購商品的副本,在任何時間點只能有一個商品。
  • 購買的商品可以是消耗品,當用完時,該商品會被顯示為 "unowned" 狀態,這時候可以透過 Google Play 再次購買。
Version 2 :
  • 購買請求是透過單一 API 介面(sendBillingRequest)。
  • Google Play 的回應是非同步的,以廣播意圖的形式回應。
  • 沒有可消耗品的商品型式,必須自行處理。
  • 支援訂閱式及不受管理的內購商品,及受管理的內購商品。
兩個版本的相容性,Version 3 支援 Android 2.2 或更新的版本,Version 2 提供類似的相容性,細節請參考 Version Notes


內購商品 (In-app Products)

在 App 中的內購商品為數位商品,透過 App 賣給使用者,像是遊戲幣、應用程式的升級或解除功能限制及新的內容等等。你只能使用 In-app Billing 販賣數位內容,不能賣實體商品、實體服務或任何需寄送才能取得的商品。不像付費型 App ,使用者可以在購買後的"試用時間"內由使用者自己取消購買;當使用者付費購買內購商品時,是沒有退款的機制讓使用者自己取消購買(但可寫信給開發者,由開發者決定是否取消該次付款)。

Google Play 沒有提供任何形式的遞送內容的機制,開發者必須自行傳遞數位商品給購買的使用者。內購式商品永遠明確的只和一款 App 產生關聯,也就是說,一個 App 不能購買由另一個 App 所發佈的內購式商品,即使是由同一個開發者所販賣的。

商品類型

開發者可以在 Google Play Developer Console (開發者控制台)中定義要販售的商品。類型有:

  • managed in-app products:託管式商品。
  • subscriptions:訂閱的內容。
  • unmanaged in-app products:非託管式商品。
託管的意思是指由 Google Play 掌握及追蹤在你的 App 中,單一使用者所購買的內購商品的所有權,非託管的意思就是由開發者自己做這件事。


Google Play 購物流程

對使用者來說,買 App 的購物流程和買內購式商品的流程是一樣的。開發者必須有成為 Google Checkout Merchant (Checkout商家)才能使用 In-app Billing 的服務。

當使用者開始購買一項商品,你的 App 會送出一個指定商品的付款請求,接著 Google Play 會控管整個交易的結帳細節,包含請求及驗證付款格式並且處理金流。當結帳流程完成後,Google Play 會送出購買明細給你的 App ,像是訂單號碼、訂購日期和時間以及付款金額,你的 App 完全不用控制整個交易流程,全部交由 Google Play 處理。

重要:官方強烈建議開發者在應用程式發佈前,使用混淆器來混淆程式碼,這是基於安全考量。


In-app Billing Version 3 API

特點:
  • 同步式的購買流程
  • 可追蹤消耗性商品
  • 本地端暫存內購商品資料
開發者使用"開發者控制台"定義商品的類型、SKU、價格及說明等等。


內購式商品(Purchasing Items)購買流程:


(圖片來源:http://developer.android.com/google/play/billing/api.html#purchase)

1. App 送出 isBillingSupported 的請求,向 Google Play 確認目前這個 In-app Billing API 版本是否支援付款。

2. 當 App 被啟動或使用者登入,就是檢查使用者擁有哪些購買項目的時候。要查詢使用者購買過哪些內購式商品,可以送出 getPurchases 請求。如果請求成功,會回傳一個 Bundle,包含已購買商品的ID清單、個人的購買明細清單及為購買簽名的清單。

3. 通常來說,開發者會想要告知使用者有哪些商品可以購買,要查詢開發者在 Google Play 定義的可內購式商品的細節,App 可以送出 getSkuDetails 的請求,開發者必須在查詢請求中指定產品ID清單。查詢成功則回傳一個 Bundle,包含相關的產品細節,像是價格、標題、說明及購買類型。

4. 假如某個內購式商品尚未屬於使用者,開發者就可以對使用者顯示購買提示(例如購買按鈕),當使用者按下按鈕送出購買請求,App 就會送出 getBuyIntent ,指定該項商品的ID及其他參數來購買。開發者應該在"開發者控制台"建立一個新的內購式商品時記下商品ID。

  1. Google Play 回傳一個 Bundle 內含一個 PendingIntent 可以讓使用者開始付款結帳的界面。
  2. App 會呼叫 startIntentSenderForResult 來啟動這個 PendingIntent。
  3. 當這個結帳流程結束(指使用者購買成功或取消購買),Google Play 會回傳一個 Intent 到 App 的 onActivityResult 方法,結果碼(result code)會表示這次的購買是成功的或被取消,在回傳的 Intent 中包含了購買商品的資訊,有一個由 Google Play 產生的 purchaseToken 字串,可用來識別這次的交易。

消耗性商品購買流程:


(圖片來源:http://developer.android.com/google/play/billing/api.html#consume)

在 Version 3 裡,內購式商品都是被託管的,也就是說,所有的內購商品的所有權都是受 Google Play 管理,App 可以在需要的時候查詢使用者的購物資訊。當使用成功購買一項商品時,購買記錄會被儲存在 Google Play,當一項商品被購買後,它就會成為"被擁有(owned)"狀態,當處於"被擁有"狀態時,不能再度在 Google Play 中重覆購買。必須對被使用者擁有的商品送出消除的請求,才能再度在 Google Play 中購買該項商品,消除該商品會讓狀態恢復到"未被擁有"的狀態,前一次的購物資料將被丟棄。

要讓使用者重新取回商品的擁有權,可以讓 App 對 Google Play 呼叫 getPurchases。App 可以透過呼叫 consumePurchase 送出一個消除的請求,在請求的參數中,必須指定一個唯一的 purchaseToken 字串,這個字串是當初在 Google Play 完成購買時所給予的,然後 Google Play 會回傳狀態碼說明是否消除成功。

開發者要決定內購式商品為消耗性或非消耗性,兩者的說明:

  • Non-consumable Items(非消耗性商品):只能在 App 中購買一次,並且提供永久的使用,該商品會和 Google 帳號相關聯。
  • Consumable Items(可消耗性商品):能在 App 中在該商品用完時再次購買,例如遊戲中的藥水。


在應用程式中管理消耗性商品的基本流程如下:

  1. 呼叫 getBuyIntent 來啟動付款。
  2. 從 Google Play 取得 Bundle ,說明付款是否成功。
  3. 假如成功,呼叫 consumePurchase 來消費這次的購買。
  4. 從 Google Play 取得回傳碼,說明消費是否順利完成。
  5. 假如成功,就在 App 中提供該項商品。
之後,當使用者啟動或登入你的 App,你應該確認使用者是否擁有任何已購買的商品。以下的建議是在使用消耗性商品時的 App 啟動流程:
  1. 送出 getPurchases 查詢使用者擁有的商品。
  2. 如果有任何可消費的商品,呼叫 consumePurchase 來消費該商品,這步驟是必要的,因為 App 可能已完成購買某項商品的訂單,但是在 App 送出消費請求前中止或離線了。
  3. 從 Google Play 取得回應碼,說明消費是否成功。
  4. 假如成功,就在 App 中提供該商品。

本地快取

因為 Google Play 客戶端現在會快取內購資訊在本地裝置上,所以開發者可以使用 Version 3 API 頻繁的查詢購買資訊,例如,呼叫 getPurchases 。不像前一個版本的 API ,很多 Version 3 API 的呼叫會透過在快取中查找替代透過網路連線到 Google Play 的方式,這很明顯的提升的 API 的回應速度。


總結整理


  • 如果是第一次使用IAB,就使用V3 API,比V2好用易上手。
  • V3需透過Google Play app來傳送購買訊息,所以必須使用實機測試,除非模擬器上有Google Play app(即Play 商店)
  • 瞭解您的商品屬於一次性購買或屬於消耗品,選擇正確的商品販賣類型。

參考資料:
Google Play In-app Billing

Eclipse 編譯 Cocos2d-x 的程式碼

http://fecbob.pixnet.net/blog/post/38509887-cocos2d-x%E8%B7%A8android%26ios%E5%B9%B3%E8%87%BA%E9%96%8B%E7%99%BC%E5%85%A5%E9%96%80

1017440  
假如你可以使用和cocos2d相似的api來開發android上面的遊戲,那豈不是更爽?這樣,你就可以毫不費勁地擴大你的遊戲的市場份額(android的使用者數量巨大啊!)

好吧,確實存在這麼一個遊戲框架,她就是cocos2d-x!cocos2d-x採用c++把cocos2d的api重新移植了一遍,,除了一些語法細節上面的差異之外,你可以使用幾乎一樣的api來編寫遊戲。正是由於它是c++寫的,所以可以跨多個平臺,從ios到android、windows、再到linux、bada等!

在這篇教程裡面,你將學習到,如何使用cocos2d-x和c++來編寫一個能夠同時在ios和android設備上面跑的「HelloWorld」程式。

而且,過兩天,我會再出一篇教程,向大家展示一個具體的遊戲例子---一個跨平臺的太空射擊遊戲!

好了,帶上你的iPhone和Android設備,讓我們開始吧!

Hello,Cocos2D-X for iOS!

在繼續之前,確保你先下載最新版本的cocos2d-x。

隨便在你的硬碟的某個位置上面解壓就可以了。注意,這個解壓出來的資料夾的完整路徑名,我們之後會用一個統一的別名$COCOS2DX_HOME來指代。

現在,你已經下載到源碼了,讓我們來安裝專案範本吧!你可以先cd到$COCOS2DX_HOME這個目錄(譯者:指你剛剛解壓縮代碼的存放路徑)下面去。在安裝範本之前,先退出Xcode,然後打開一個終端,並輸入下面的命令:

cd$COCOS2DX_HOME

sudo./install-templates-xcode.sh-u

注意:在上面所示的代碼中,你不能直接輸入$COCOS2DX_HOME,而要輸入實際的路徑。如果你直接輸入了$COCOS2DX_HOME是不會有任何反應的。當然,你可以參考我寫的《如何在macos下面組態集成ios和android遊戲教程》,裡面有講到環境變數的配置。大概方法就是打開~/.bash_profile檔,然後用export來定義環境變數就ok了。

你可能會被要求輸入管理員密碼(加了-u應該是不需要輸入密碼的,不加-u就需要輸入),之後的過程就非常簡單了。在範本安裝好之後,啟動xcode,然後創建一個新的專案。這時,你可以在專案範本對話方塊中看到有cocos2d-x的範本了。不用擔心,cocos2d-x的範本與cocos2d的範本並不會有衝突,因此,你還是可以創建cocos2d的專案。

讓我們來試一下新範本吧!打開Xcode,然後創建一個新的工程,選擇iOS\cocos2d-x\cocos2dx範本,如下圖所示:
1017442 

把工程例句為Cocos2DxFirstIosSample,然後保存。

現在,編譯並運行,你將會看到cocos2d-x的hellowolrd程式跑起來了,如下圖所示:
1017444 

非常簡單,不是嗎?這時,你可以看看xcode裡面的檔,特別是Classes\HelloWorldScene.H和Classes\HelloWorldScene.cpp。如果你也熟悉cocos2d的話,那麼裡面的代碼看起來會覺得非常熟悉---幾乎完全一樣的cocos2dAPI,只是語言是c++而已!

配置cocos2d-x的eclipse開發環境

現在我們已經完成了Xcode4的helloworld程式了,是時候看看如何使用eclipse來配置android的cocos2d-x開發環境了。

這篇教程假設你有一個可以進行Android開發的標準開發環境。如果你還沒有的話,可以參考這篇教程來配置,它裡面提供了詳細的過程,教你如何一步步配置好一個標準的eclipseandroid開發環境。

但是,請等一下。一個標準的eclipseandroid開發環境是專為java開發設計的,而cocos2d-x卻是基於c++來做開發的!

不用擔心,eclipse已經有10年的歷史了,它有許多外掛程式可以支援其它語言的開發,這當然就包括c/C++語言的外掛程式啦。先讓我們來安裝這個外掛程式吧。打開eclipse,然後執行以下步驟:

從EclipseIDE的主工具列上面,選擇Help/InstallNewSoftware。

打開WorkWith核取方塊,並從中選擇一個包含你的eclipse版本名稱的項(如果是最新版的eclipse的話,就選擇indigo)

在外掛程式樹裡面找到ProgrammingLanguages(只有當你複選中「Groupitemsbycategory」時,你才可以看到外掛程式樹。)並打開它.

選擇CDT外掛程式,然後安裝下列元件。(但 是,請注意,你如果直接就點擊C/C++DevelopmentTools的話,你是得不到下圖所示的樣子的,你還需要在 「MobileDevelopment」裡面去查找相關的項)(譯者:我沒找了,直接就選擇了C/C++DevelopmentTools就Next安裝 了)
1017446 

點擊next來完成嚮導,然後等待元件下載並安裝。現在,你可以使用eclipse來開發C/C++專案了!

Set up the Android NDK(Native Development Toolkit)

本來,android開發只能採用java,而且現在市場上面大部分的app也是採用java寫的。

然而,現在你還可以通過NativeDevelopmentToolkit(NDK),使用C/C++來編寫android程式。這種程式設計方式是由Google在2009年6月份引入的,它允許一些元件採用C/C++來編寫,然後通過標準的JAVANativeInterface(JNI)來調用。

安裝NDK的過程非常簡單:

下載最新版本的NDK。here(注意選擇MacOSX平臺).

解壓tar.bz2到任意目錄.之後,我會使用$NDKROOT來指定這個解壓縮後的目錄。

NDK給Android開發帶來了全套的C/C++編譯工具集,可以使用GCC4.4.3來編譯來連結並且可以構建即時安裝的APK包。

有了這套編譯工具集,我們就可以在eclipse裡面集成一些外部的C/C++庫(比如cocos2d-x)。這些庫被編譯成動態庫,然後通過JNI(JAVANativeInterface)與JAVAAndroid體系程式進行交互。

這些編譯工具集可以採用下面兩種方式使用:

獨立模式:直接在你的makefile中使用arm-linux-androideabi-g++.這種方式會增加你的專案的複雜性和可維護性。我推薦你不要使用這種方式。

整合模式:使用$NDKROOT/ndk-buildshell工具,它是一種高度可定制的makefile,專門為NDK庫所設計的。這也是我們這篇教程所採用的方式。
1017448 

解釋JNI和NDK可能會花費大量時間,而且也超出了本教程的討論範圍。現在網路上有許多關於JNI的資源。這裡還推薦一本書,它也討論了JNI這個主題。

如果你需要更多關於NDK的資訊,這裡推薦一本非常好的書,它裡面涵蓋了使用C/C++進行NDK開發,書名是:AndroidNativeDevelopmentKitBeginner’sGuide。這本書覆蓋了NDK程式設計的方方面面,可謂是「從入門到精通」,而且NDK本身也帶有非常詳細的文檔,在$NDKROOT/docs路徑下面。

Hello,Cocos2D-X for Android!

現在,讓我們在Andriod平臺上面來開發一個「Hello,World」程式吧,就像我們之前在ios平臺上的程式一樣。

我們需要通過命令列來完成工作,因為目前還沒有在EclipseIDE裡面集成cocos2d-x的範本。

在$COCOS2DX_HOME目錄下面包含一個shell腳本,叫做create-android-project.sh,我們可能通過它來創建android專案。但是,在運行腳本之前,我們需要在指令檔的頂部做一些修改:

#setenvironmentparamters

NDK_ROOT_LOCAL="/home/laschweinski/android/android-ndk-r5"

ANDROID_SDK_ROOT_LOCAL="/home/laschweinski/android/android-sdk-linux_86"

修改上面這些行,把NDK_ROOT_LOCAL指向你安裝AndroidNDK($NDKROOT)的位置,同時把ANDROID_SDK_ROOT_LOCAL指向你安裝AndroidSDK位置。(譯者:這裡其實可以不用修改,讀者可以打開這個sh腳本去看看為什麼。如果之前看過我的配置教程,裡面有配置一個全域的NDK_ROOT和ANDROID_SDK_ROOT。這個腳本在判斷有全域的環境變數存在的時候,會直接替代NDK_ROOT_LOCAL和ANDROID_SDK_ROOT_LOCAL。)

現在,可以運行create-android-project.sh腳本了,然後你會接收到一系列的提示輸入。我們將一個個向您解釋一下:

第一個提示要求你「Inputpackagepath」。這個包名會給後面的java代碼使用。你可以使用你的功能變數名稱反過來寫,類似寫ios的bundleID。比如com.yourdomain.samplecocos2dxandroid,記住實際輸入的時候要替換掉「com.yourdomain」。

接下來,你會得到一系列可用的AndroidAPI和它們的id號。這個具體取決於你的機器上面安裝的AndriodAPI的情況。

最後,你需要提供專案的名稱,在這裡取名為samplecocos2dxandroid:

命令列的輸出大致如下:

bash-$./create-android-project.sh

Inputpackagepath.Forexample:org.cocos2dx.example

org.jymc.samplecocos2dxandroid

...

AvailableAndroidtargets:

----------

...

----------

id:9or"GoogleInc.:GoogleAPIs:15"

Name:GoogleAPIs

Type:Add-On

Vendor:GoogleInc.

Revision:1

Description:Android+GoogleAPIs

BasedonAndroid4.0.3(APIlevel15)

...

inputtargetid:

9

inputyourprojectname:

samplecocos2dxandroid

Createdprojectdirectory:/Users/jymen/development/cocos2dx/samplecocos2dxandroid

...

Addedfile/Users/jymen/development/cocos2dx/samplecocos2dxandroid/AndroidManifest.xml

Addedfile/Users/jymen/development/cocos2dx/samplecocos2dxandroid/build.xml

Addedfile/Users/jymen/development/cocos2dx/samplecocos2dxandroid/proguard.cfg

bash-$

注意「Createdprojectdirectory:」這一行是指令檔最後輸出來的,這個輸出的路徑也就是你的Andriod專案被創建好的路徑。我這裡指的是/Users/jymen/development/cocos2dx/samplecocos2dxandroid,如上圖所示:

Note:Donottrytomovetheprojectfromthatlocationtoadifferentlocation.Atleastoneofthescriptswe’llworkwithinthenextsectionwillnotworkifyoudo.

構建工程

這裡有兩個步驟來構建專案----首先通過命令列腳本編譯c++代碼,然後通過ecipse來編譯java代碼。

為了編譯c++代碼,我們需要切換到$PROJECT_HOME/android資料夾下面去,然後在終端裡面輸入下列命令:

./build_native.sh

你應該會看到下面類似的輸出:

Gdbserver:[arm-linux-androideabi-4.4.3]libs/armeabi/gdbserver

Gdbsetup:libs/armeabi/gdb.setup

Compile++thumb:cocos2d<=CCConfiguration.cpp

Compile++thumb:cocos2d<=CCDrawingPrimitives.cpp

:

:

:

Compile++thumb:cocos2d<=CCTileMapAtlas.cpp

Compile++thumb:cocos2d<=CCTouchDispatcher.cpp

Compile++thumb:cocos2d<=CCTouchHandler.cpp

Prebuilt:libstlport_static.a<=/sources/cxx-stl/stlport/libs/armeabi/

SharedLibrary:libcocos2d.so

Install:libcocos2d.so=>libs/armeabi/libcocos2d.so

Compile++thumb:cocosdenshion<=SimpleAudioEngine.cpp

Compile++thumb:cocosdenshion<=SimpleAudioEngineJni.cpp

SharedLibrary:libcocosdenshion.so

Install:libcocosdenshion.so=>libs/armeabi/libcocosdenshion.so

Compile++thumb:game_logic<=AppDelegate.cpp

Compile++thumb:game_logic<=HelloWorldScene.cpp

SharedLibrary:libgame_logic.so

Install:libgame_logic.so=>libs/armeabi/libgame_logic.so

Compile++thumb:game<=main.cpp

SharedLibrary:libgame.so

Install:libgame.so=>libs/armeabi/libgame.so

這裡就是在編譯cocos2d-x庫和你的專案裡面的c++檔。

為了編譯java代碼,我們需要創建一個eclipse工程---這可比命令列要方便多了。:)

打開Eclipse,然後他吃軟飯File\New \Other,選擇Android\AndroidProject,然後點擊Next,在ProjectName裡面輸入 samplecocos2dxandroid,然後選擇Createprojectfromexistingsource,然後流覽 到$PROJECT_HOME/android資料夾,如下圖所示:
10174410 

點擊Next,選擇一個Androidversiontotarget(這裡選擇的是3.2,你可以選擇任何一個)
10174412 

點擊Finish,好了,現在你有一個工程了!
10174414 

在左邊的樹上右擊專案,選擇RunAs\AndroidApplication,然後AVD(AndroidVirtualDevice)就會啟動,然後就會跑出來Hello,Cocos2D-X專案了!
10174416 

注意:如果程式沒有啟動的話,你需要按照提示去創建一個AVD來測試。更多的細節,請參考GettingStartedwithAndroidDevelopment這篇教程。

恭喜你,你現在有一個「Hello,World」專案可以同時跑在ios和android上面了!

在Eclipse裡面定義一個結合java/c++的工程

通過命令腳本來編譯c++代碼,完了之後用eclipse來編譯java代碼,這種來回切換的做法非常笨重。如果全部可以用eclipse來完成的話,那麼生活會更簡單一些。

幸運的是,我們可以告訴eclipse,我們的android專案是一個集成java/c++的跨語言專案!

為此,我們在左邊的專案樹上選擇samplecocos2dxandroid,然後從功能表裡面選擇File\New\Other,然後從對話方塊裡面選擇C/C++\ConverttoaC/C++Project,如下所示:
10174418 

點擊Next。在下一個螢幕裡面,選中你的專案,然後選擇c++選項按鈕,然後為Projecttype指定Makefileproject\OtherToolchain,如下所示:

10174420 
點擊完成。這時eclipse會詢問你是否打開C/C++視圖,當對話方塊出現的時候,選擇NO。

接下來的步驟就是修改專案設置,指定如何運行我們的make命令。在左邊的專案樹上,右擊samplecocos2dxandroid工程,選擇Properties,然後選擇C/C++build

10174422 
取消「Usedefaultbuildcommand」核取方塊,然後在build文字方塊中輸入下面的命令:

bash${workspace_loc:/samplecocos2dxandroid}/build_native.shNDK_DEBUG=1V=1

點擊Apply,然後點OK。

回到ecipse,從主功能表中選擇Project\BuildAll,這時你可以在ecipse的主控台輸出中看到c++make在運行。
10174424 

我們仍然有一些警告需要解決。想知道我是什麼意思嗎,你打開jni/helloworld/main.cpp,這裡你會看到一系列的警告,如下所示:
10174426 

這些警告之所以會出現,是因為我們沒有配置正確的c++包含路徑。為了解決它,我們右擊samplecocos2dxandroid工程,然後選擇Properties\C/C++General\PathandSymbols\GNUC++。

選擇Add...按鈕來選擇下面的目錄,之後點擊Apply和OK。

$(NDKROOT)/platforms/android-9/arch-arm/usr/include

$(COCOS2DX_HOME)/cocos2dx/include

注意:不用忘了使用實際的路徑來替換掉$(NDKROOT)和$(COCOS2DX_HOME)。
10174428 

點擊Apply,這時會提示你是否重建索引。直接點擊yes繼續就可以了。

現在,你再看看main.cpp,你會看到大部分警告已經消失了。
10174430 

但是,AppDelegate.h檔還是找不到。這是因為AppDelegate.h是在$PROJECT_HOME\Classes資料夾下面。這個資料夾對我們來說非常重要,因為它包含了我們工程裡面的可移植的c++類,比如HelloWorldScene.cpp等。

當我們創建eclipse專案的時候,我們必須選擇$PROJECT_HOME\android資料夾,因為eclipse需要一個AndroidManifest.xml檔。但是,這樣的話,我們的工程就不包括關鍵的「Classes」資料夾了,這也是為什麼我們會得到這麼多警告的原因。

讓我們來修正它吧。右擊samplecocos2dxandroid專案,選擇Properties\C/C++General\PathsandSymbols\Sourcelocation。點擊LinkFolder,然後複選中Linktoafolderinthefilesystem,接著流覽到$PROJECT_HOME資料夾,並指向Classes目錄,最後點Apply和OK。

10174432 
你現在可以在檔樹裡面看到Classes目錄了,而且在main.cpp裡面的#include「AppDelegate.h」警告也應該消失了。

Eclipse工程裡面還會有許多警告,但是,這是因為eclipse對於解析c++標頭檔的能力並不強大。為了消除這些警告,我們又需要設置一下專案設置。(右鍵點工程,然後選擇Properties),把CodeAnalysis部分的warnings關閉,如下圖所示:
10174434 

然後點擊Apply和OK,這時,你就得到一個工程可以在eclipse裡面進行編輯了。

What About the JAVA Code?

這時你在eclipse裡面再找找看,你會在src和gen目錄下面找到一些java代碼,你肯定很好奇這些代碼是幹嗎用的。

Andriod開發採用的主要語言是java,這個專案範本已經為我們創建了一些java框架代碼,用來載入c++編寫好的動態庫。

一般情況下,你是不需要修改這些java代碼的,但是,還是讓我們來看看src\com\xxx\samplecosos2dxandroid\samplecocos2dandroid.java這個類。

這個類包含了主AndroidActivity類,當應用程式啟動的時候會載入。它首先通過NDK來請求編譯好的c++動態庫。(如果採用的是最新版的coocs2dx的話,下面只會載入一個game庫)

10174436 
由於類是從Cocos2dxActivity所派生,在後臺cocos2d-x會把控制權移交給c++的AppDelegate的initInstance方法來處理,最後applicationDidFinishLAunching方法也就被執行起來了。

因此,再重申一遍,java代碼只是一些包裝代碼,大部分情況下我們都不要去修改它,因為我們的遊戲邏輯是由c++寫的。

Sotorepeat,theJAVAcodejustcontainswrappercodethatyouwillrarelyneedtochange,sinceallthegame’slogicwillbeinsidetheC++code.

連接andriod和ios專案結構

現在,我們有兩個cocos2d-x的專案了,一個是ios下面的,一個是andriod下面的。我們的目標是讓兩個工程共用同樣的目錄,這樣就可以使用同樣的c++檔了。

我們兩個專案裡面,都有一個資料夾叫做「Classes」,它包含了我們的可移植的遊戲代碼(如下圖:android的在左邊,ios的在右邊)
10174438  
如果你仔細看看eclipse和xcode裡面的Classes目錄,你會發現這些c++檔完全相等。因此,我們需要做的就是讓兩個專案指向同一個資料夾就ok了!

為了實現這個目標,在xcode裡面打開Cocos2DxFirstIosSample專案。選擇Classes分組,然後點擊Delete,選擇「MovetoTrash」;

接下來,右鍵ProjectNavigator,選擇「NewGroup」,然後重命名為Classes。點鍵Classes,點擊AddFiles。然後選擇$PROJECT_HOME\Classes目錄,同時確保「Copyitemsintodestinationgroup’sfolder」沒有被選中!
10174440 

然後clean並rebuildxcode專案,並運行一下,確保一切ok。

恭喜,現在基礎配置全部弄好了!你可以試著在eclipse裡面修改,xcode這邊運行,或者xcode裡面修改,eclipse裡面運行了。

什麼時候使用Xcode,什麼時候使用Eclipse?

現在,你有兩個專案了,那麼問題馬上就出來了,我們到底什麼時候該使用哪一個呢?

使用cocos2d-x通常的開發策略是,首先在ios上面通過xcode來測試,確認沒問題後,再通過eclipse來測試。只需要在心裡記住下面幾點就行:

經常測試.開發完一個小功能之後,就應該在andriod上面測試看行不行。這樣的話,就不至於遇到問題找不到了。

在多個設備上面測試:現在市場上面有大量的android設備,而且它們的差別很細微,所以,你要盡可能多地測試多種不同型號的設備。

當然,你也可以先在andriod上面測試,然後再在ios上面測試。但是這樣肯定會降低你的生產率。因為xcode比eclipse反應速度更快,而ios模擬器比android的模擬器的啟動速度也要快一些。

調試技巧

在ios上面調試cocos2d-x和cocos2d的方法差不多,這裡也沒什麼多講的。

然而,對於Android,這裡有幾點需要注意下。

當使用NDK來調試的時候,一個「服務端的」gdb和gdbcoNtext被ndk-buildshell腳本塞到APk裡面去了。這個遠端的gdb調試器可以與任何gdb相容的用戶端進行通信。

Eclipse裡面最好的gdbagent就是NVidiadebugmanagerplugin,但是安裝這個外掛程式不在本教程的討論範圍之內。

注意:如果你想安裝NVidiaDebugManagerplugin,你首先到這個頁面下載完整的TegraAndroid開發包,即便你可能現在並不需要它。然後,安裝TegraDeveloper,它會提示你安裝哪些項,然後你可以選擇NVidiaDebugManagerplugin這個外掛程式了。當你安裝完之後,它並沒有包外掛程式安裝到eclipse裡面去,而是把zip檔放在你的硬碟的某個位置,你可以參考這個pdf文檔來進行安裝。

我發現在一些物理設備(比如SamsumgGalaxyS),remotedebugger老是啟動不了,但是使用AVD就可以正常啟動。

為了啟動一個debug會話,你可以選擇你的專案,然後右擊DebugAsAndroidNDKapplication,如下圖所示:

10174442 
請注意,只有你安裝了NVidiadebugmanagerplugin,你才會在右擊的時候看到AndroidNDKApplication這個選項。

你可以在一行代碼上面按兩下來設置一個中斷點,如下所示:
10174444 

當中斷點到達的時候,它將會停留在中斷點所在行:
10174446 

悲催的是,使用gdb來調試android有時候並不管用,所以,你可能需要添加一些額外的tracing系統。想知道更多的資訊,請聯繫我,我將與大家分享這些tips。

何去何從?

恭喜你,你現在可以使用cocos2d-x來開發iphone和android下面的遊戲了。

請耐心等待我的第二篇教程吧,它將帶你一起開發一個跨平臺的太空射擊遊戲,當然是使用cocos2d-x啦!

原文連結位址:HTTP://www.raywenderlich.com/11283/cocos2d-x-for-ios-and-android-getting-started


2013年11月7日 星期四

五子棋AI演算法 Java实现

http://blog.csdn.net/kindazrael/article/details/7993713


五子棋AI算法 也算是一个典型的游戏AI算法,一些棋类的AI算法都可以参考实现,下面是Java实现代码
棋盘抽象接口

import java.util.List;


public interface IChessboard {
 //取得棋盘最大横坐标
 public int getMaxX();
 //最大纵坐标
 public int getMaxY();
 //取得当前所有空白点,这些点才可以下棋
 public List<Point> getFreePoints();
}

棋子类实现

//棋子类
public class Point {
 // 这了性能,设成公有
 public int x;
 public int y;
 

 public int getX() {
  return x;
 }

 public Point setX(int x) {
  this.x = x;
  return this;
 }

 public int getY() {
  return y;
 }

 public Point setY(int y) {
  this.y = y;
  return this;
 }

 public Point(int x, int y) {
  this.x = x;
  this.y = y;
 }


 @Override
 public int hashCode() {
  return x + y;
 }

 @Override
 public boolean equals(Object obj) {
  if (this == obj)
   return true;
  Point other = (Point) obj;
  if (x != other.x)
   return false;
  if (y != other.y)
   return false;
  return true;
 }

}
玩家抽象接口
import java.util.List;

public interface IPlayer {
 //下一步棋子,传入对手已经下的棋子集合
 public void run(List<Point> enemyPoints, Point point);

 public boolean hasWin();
 
 public void setChessboard(IChessboard chessboard);
 
 public List<Point> getMyPoints();
}
玩家基础抽象类


import java.util.ArrayList;
import java.util.List;

public abstract class BasePlayer implements IPlayer {
 //我已下的棋子
 protected List<Point> myPoints = new ArrayList<Point>(200);
 //棋盘
 protected IChessboard chessboard;
 //棋盘最大横坐标和纵标,
 protected int maxX;
 protected int maxY;
 
 //所有空白棋子
 protected List<Point> allFreePoints;

 @Override
 public final List<Point> getMyPoints() {
  return myPoints;
 }

 @Override
 public void setChessboard(IChessboard chessboard) {
  this.chessboard = chessboard;
  allFreePoints = chessboard.getFreePoints();
  maxX = chessboard.getMaxX();
  maxY = chessboard.getMaxY();
  myPoints.clear();
 }
 
 private final Point temp = new Point(0, 0);
 //我是否是否赢了
 public final boolean hasWin(){
  if(myPoints.size()<5){
   return false;
  }
  Point point = myPoints.get(myPoints.size()-1);
  int count = 1;
  int x=point.getX(),y=point.getY();
  //横向--
  temp.setX(x).setY(y);
  while (myPoints.contains(temp.setX(temp.getX()-1)) && temp.getX()>=0 && count<5) {
   count ++;
  }
  if(count>=5){
   return true;
  }
  temp.setX(x).setY(y);
  while (myPoints.contains(temp.setX(temp.getX()+1)) && temp.getX()<maxX && count<5) {
   count ++;
  }
  if(count>=5){
   return true;
  }
  //纵向|
  count = 1;
  temp.setX(x).setY(y);
  while (myPoints.contains(temp.setY(temp.getY()-1)) && temp.getY()>=0) {
   count ++;
  }
  if(count>=5){
   return true;
  }
  temp.setX(x).setY(y);
  while (myPoints.contains(temp.setY(temp.getY()+1)) && temp.getY()<maxY && count<5) {
   count ++;
  }
  if(count>=5){
   return true;
  }
  //正斜向 /
  count =1;
  temp.setX(x).setY(y);
  while (myPoints.contains(temp.setX(temp.getX()-1).setY(temp.getY()+1)) && temp.getX()>=0 && temp.getY()<maxY) {
   count ++;
  }
  if(count>=5){
   return true;
  }
  temp.setX(x).setY(y);
  while (myPoints.contains(temp.setX(temp.getX()+1).setY(temp.getY()-1)) && temp.getX()<maxX && temp.getY()>=0 && count<6) {
   count ++;
  }
  if(count>=5){
   return true;
  }
  //反斜 \
  count = 1;
  temp.setX(x).setY(y);
  while (myPoints.contains(temp.setX(temp.getX()-1).setY(temp.getY()-1)) && temp.getX()>=0 && temp.getY()>=0) {
   count ++;
  }
  if(count>=5){
   return true;
  }
  temp.setX(x).setY(y);
  while (myPoints.contains(temp.setX(temp.getX()+1).setY(temp.getY()+1)) && temp.getX()<maxX && temp.getY()<maxY && count<5) {
   count ++;
  }
  if(count>=5){
   return true;
  }
  return false;
 }
}
电脑AI类实现


import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

//算法核心类,算法的主体思想分三个步骤,
//第一步:根据双方的当前的形势循环地假设性的分别给自己和对方下一子(在某个范围内下子),并判断此棋子能带来的形势上的变化,如能不能冲4,能不能形成我方或敌方双3等,
//第二步:根据上一步结果,组合每一步棋子所带来的所有结果(如某一步棋子可能形成我方1个活3,1个冲4(我叫它半活4)等),包括敌方和我方的。
//第三步:根据用户给的规则对上一步结果进行排序,并选子(有进攻形、防守形规则)
public class BaseComputerAi extends BasePlayer {

 // 四个方向,横- 、纵| 、正斜/ 、反斜\
 private static final int HENG = 0;
 private static final int ZHONG = 1;
 private static final int ZHENG_XIE = 2;
 private static final int FAN_XIE = 3;
 //往前往后
 private static final boolean FORWARD = true;
 private static final boolean BACKWARD = false;
 
 
 
 //标示分析结果当前点位是两头通(ALIVE)还是只有一头通(HALF_ALIVE),封死的棋子分析过程自动屏蔽,不作为待选棋子
 private static final int ALIVE = 1;
 private static final int HALF_ALIVE = 0;
 //private static final int DEAD = -1;
 
 //计算范围,太大的范围会有性能问题
 private class CalcuteRange{
  int xStart,yStart,xStop,yStop;
  private CalcuteRange(int xStart, int yStart, int xStop, int yStop) {
   this.xStart = xStart;
   this.yStart = yStart;
   this.xStop = xStop;
   this.yStop = yStop;
  }
 }
 
 
 //限定电脑计算范围,如果整个棋盘计算,性能太差,目前是根据所有已下的棋子的边界值加RANGE_STEP值形成,目前为1
 private static final int RANGE_STEP = 1;
 CalcuteRange currentRange = new CalcuteRange(0, 0, 0, 0);
 private void initRange(List<Point> comuters, List<Point> humans){
  currentRange.xStart = humans.get(0).getX()-RANGE_STEP;
  currentRange.yStart = humans.get(0).getY()-RANGE_STEP;
  currentRange.xStop = humans.get(0).getX()+RANGE_STEP;
  currentRange.yStop = humans.get(0).getY()+RANGE_STEP;
  for (Point point : humans) {
   if(point.getX()-RANGE_STEP<currentRange.xStart){
    currentRange.xStart = point.getX()-RANGE_STEP;
   }else if(point.getX()+RANGE_STEP>currentRange.xStop){
    currentRange.xStop = point.getX()+RANGE_STEP;
   }
   if(point.getY()-RANGE_STEP<currentRange.yStart){
    currentRange.yStart = point.getY()-RANGE_STEP;
   }else if(point.getY()+RANGE_STEP>currentRange.yStop){
    currentRange.yStop = point.getY()+RANGE_STEP;
   }
  }
  for (Point point : comuters) {
   if(point.getX()-RANGE_STEP<currentRange.xStart){
    currentRange.xStart = point.getX()-RANGE_STEP;
   }else if(point.getX()+RANGE_STEP>currentRange.xStop){
    currentRange.xStop = point.getX()+RANGE_STEP;
   }
   if(point.getY()-RANGE_STEP<currentRange.yStart){
    currentRange.yStart = point.getY()-RANGE_STEP;
   }else if(point.getY()+RANGE_STEP>currentRange.yStop){
    currentRange.yStop = point.getY()+RANGE_STEP;
   }
  }
  
  //如果范围扩大后超过了棋盘,则等于棋盘
  currentRange.xStart=currentRange.xStart<0?0:currentRange.xStart;
  currentRange.yStart=currentRange.yStart<0?0:currentRange.yStart;
  currentRange.xStop=currentRange.xStop>=maxX?maxX-1:currentRange.xStop;
  currentRange.yStop=currentRange.yStop>=maxY?maxY-1:currentRange.yStop;
 }

 // 分析当前形式的入口方法,分析总共分三个步骤,第三步骤可由子类干预以作难度控制
 private Point doAnalysis(List<Point> comuters, List<Point> humans) {
  if(humans.size()==1){//第一步
   return getFirstPoint(humans);
  }
  
  //初始化计算范围
  initRange(comuters, humans);
  
  //清除以前的结果
  initAnalysisResults();
  // 开始分析,扫描所有空白点,形成第一次分析结果
  Point bestPoint = doFirstAnalysis(comuters, humans);
  if(bestPoint!=null){
   //System.out.println("这个棋子最重要,只能下这个棋子");
   return bestPoint;
  }
  // 分析第一次结果,找到自己的最佳点位
  bestPoint = doComputerSencondAnalysis(computerFirstResults,computerSencodResults);
  if(bestPoint!=null){
   //System.out.println("快要赢了,就下这个棋子");
   return bestPoint;
  }
  computerFirstResults.clear();
  System.gc();
  // 分析第一次结果,找到敌人的最佳点位
  bestPoint = doHumanSencondAnalysis(humanFirstResults,humanSencodResults);
  if(bestPoint!=null){
   //System.out.println("再不下这个棋子就输了");
   return bestPoint;
  }
  humanFirstResults.clear();
  System.gc();
  //没找到绝杀点,第三次结果分析
  return doThirdAnalysis();
 }
 

 //下第一步棋子,不需要复杂的计算,根据人类第一步棋子X值减1完成
 private Point getFirstPoint(List<Point> humans) {
  Point point = humans.get(0);
  if(point.getX()==0 || point.getY()==0 || point.getX()==maxX && point.getY()==maxY)
   return new Point(maxX/2, maxY/2);
  else{
   return new Point(point.getX()-1,point.getY());
  }
 }

// private int debugx,debugy;//用于DEBUG

 // 开始分析,扫描所有空白点,形成第一次分析结果
 private Point doFirstAnalysis(List<Point> comuters, List<Point> humans){
  int size = allFreePoints.size();
  Point computerPoint = null;
  Point humanPoint = null;
  int x,y;
  FirstAnalysisResult firstAnalysisResult;
  for (int i = 0; i < size; i++) {
   computerPoint = allFreePoints.get(i);
   //先把X、Y坐标记下来,因为在分析过程中会改变原来的对象
   x = computerPoint.getX();
   y = computerPoint.getY();
   if(x<currentRange.xStart || x>currentRange.xStop || y<currentRange.yStart || y>currentRange.yStop){
    continue;
   }
   
//   if(x==debugx && y==debugy){
//    System.out.println("sssssssssssss");
//   }
   
   //尝试在此位置上下一个棋子,并分析在“横向”这个方向上我方可形成的状态,如活4,活3,半活4,活2等所有状态
   firstAnalysisResult = tryAndCountResult(comuters,humans, computerPoint, HENG);
   computerPoint.setX(x).setY(y);//回复点位的原值,以供下次分析
   if(firstAnalysisResult!=null){//无返回结果此方向上不可能达到五个棋子,
    if(firstAnalysisResult.count==5)//等于5表示在此点上下棋子即可连成5个,胜利了,不再往下进行分析
     return computerPoint;
    //记录第一次分析结果
    addToFirstAnalysisResult(firstAnalysisResult,computerFirstResults);
   }
   
   //在“纵向”这个方向上重复上面的步骤
   firstAnalysisResult = tryAndCountResult(comuters,humans, computerPoint, ZHONG);
   computerPoint.setX(x).setY(y);
   if(firstAnalysisResult!=null){//死棋,不下
    if(firstAnalysisResult.count==5)
     return computerPoint;
    
    addToFirstAnalysisResult(firstAnalysisResult,computerFirstResults);
   }
   
   //正斜向
   firstAnalysisResult = tryAndCountResult(comuters,humans, computerPoint, ZHENG_XIE);
   computerPoint.setX(x).setY(y);
   if(firstAnalysisResult!=null){//死棋,不下
    if(firstAnalysisResult.count==5)
     return computerPoint;
    
    addToFirstAnalysisResult(firstAnalysisResult,computerFirstResults);
   }
   
   //反斜向
   firstAnalysisResult = tryAndCountResult(comuters,humans, computerPoint, FAN_XIE);
   computerPoint.setX(x).setY(y);
   if(firstAnalysisResult!=null){//死棋,不下
    if(firstAnalysisResult.count==5)
     return computerPoint;
    
    addToFirstAnalysisResult(firstAnalysisResult,computerFirstResults);
   }
   
   //在“横向”上分析此棋子可在敌方形成如何状态,如敌方的活3、半活4等
   firstAnalysisResult = tryAndCountResult(humans,comuters, computerPoint, HENG);
   computerPoint.setX(x).setY(y);
   if(firstAnalysisResult!=null){//死棋,不下
    if(firstAnalysisResult.count==5)
     humanPoint = computerPoint;
    
    addToFirstAnalysisResult(firstAnalysisResult,humanFirstResults);
   }
   
   //“纵向”
   firstAnalysisResult = tryAndCountResult(humans,comuters, computerPoint, ZHONG);
   computerPoint.setX(x).setY(y);
   if(firstAnalysisResult!=null){//死棋,不下
    if(firstAnalysisResult.count==5)
     humanPoint = computerPoint;
    
    addToFirstAnalysisResult(firstAnalysisResult,humanFirstResults);
   }
   
   //“正斜”
   firstAnalysisResult = tryAndCountResult(humans,comuters, computerPoint, ZHENG_XIE);
   computerPoint.setX(x).setY(y);
   if(firstAnalysisResult!=null){//死棋,不下
    if(firstAnalysisResult.count==5)
     humanPoint = computerPoint;
    
    addToFirstAnalysisResult(firstAnalysisResult,humanFirstResults);
   }
   
   //“反斜”
   firstAnalysisResult = tryAndCountResult(humans,comuters, computerPoint, FAN_XIE);
   computerPoint.setX(x).setY(y);
   if(firstAnalysisResult!=null){//死棋,不下
    if(firstAnalysisResult.count==5)
     humanPoint = computerPoint;
    
    addToFirstAnalysisResult(firstAnalysisResult,humanFirstResults);
   }
  }
  //如果没有绝杀棋子,第一次分析不需要返回结果
  return humanPoint;
 }
 
 //第二次分析,分析第一次形成的结果,第一次分析结果会把一步棋在四个方向上可形成的结果生成最多四个FirstAnalysisResult对象(敌我各四)
 //这里要把这四个对象组合成一个SencondAnalysisResult对象,
 private Point doComputerSencondAnalysis(Map<Point,List<FirstAnalysisResult>> firstResults,List<SencondAnalysisResult> sencodResults) {
  List<FirstAnalysisResult> list = null;
  SencondAnalysisResult sr = null;
  for (Point p : firstResults.keySet()) {
   sr = new SencondAnalysisResult(p);
   list = firstResults.get(p);
   for (FirstAnalysisResult result : list) {
    if(result.count==4){
     if(result.aliveState==ALIVE){//经过前面的过滤,双方都排除了绝杀棋,有活4就下这一步了,再下一步就赢了
      return result.point;//如果有绝杀,第一轮已返回,在此轮活4已经是好的棋子,直接返回,不再往下分析
     }else{
      sr.halfAlive4 ++;
      computer4HalfAlives.add(sr);
     }
    }else if(result.count==3){
     if(result.aliveState==ALIVE){
      sr.alive3++;
      if(sr.alive3==1){
       computer3Alives.add(sr);
      }else{
       computerDouble3Alives.add(sr);
      }
     }else{
      sr.halfAlive3++;
      computer3HalfAlives.add(sr);
     }
    }else{//半活2在第一阶段已被排除,不再处理
     sr.alive2++;
     if(sr.alive2==1){
      computer2Alives.add(sr);
     }else{
      computerDouble2Alives.add(sr);
     }
    }
   }
   sencodResults.add(sr);
  }
  //没有找到活4
  return null;
 }
 
 //这个方法和上面的基本一样,但为了性能,少作几次判断,将人类和电脑的分开了
 private Point doHumanSencondAnalysis(Map<Point,List<FirstAnalysisResult>> firstResults,List<SencondAnalysisResult> sencodResults) {
  List<FirstAnalysisResult> list = null;
  SencondAnalysisResult sr = null;
  for (Point p : firstResults.keySet()) {
   sr = new SencondAnalysisResult(p);
   list = firstResults.get(p);
   for (FirstAnalysisResult result : list) {
    if(result.count==4){
     if(result.aliveState==ALIVE){
      human4Alives.add(sr);
     }else{
      sr.halfAlive4 ++;
      human4HalfAlives.add(sr);
     }
    }else if(result.count==3){
     if(result.aliveState==ALIVE){
      sr.alive3++;
      if(sr.alive3==1){
       human3Alives.add(sr);
      }else{
       humanDouble3Alives.add(sr);
      }
     }else{
      sr.halfAlive3++;
      human3HalfAlives.add(sr);
     }
    }else{
     sr.alive2++;
     if(sr.alive2==1){
      human2Alives.add(sr);
     }else{
      humanDouble2Alives.add(sr);
     }
    }
   }
   sencodResults.add(sr);
  }
  //没有找到活4
  return null;
 }
 
 private void sleep(int miniSecond){
  try {
   Thread.sleep(miniSecond);
  } catch (InterruptedException e) {
  }
 }
 
 
 //第三次分析,双方都不可以制造活4,找双活3棋子,不行就找半活4,再不行就找单活3,双活2
 private Point doThirdAnalysis() {
  if(!computer4HalfAlives.isEmpty()){
   return computer4HalfAlives.get(0).point;
  }
  System.gc();
  sleep(300);
  Collections.sort(computerSencodResults);
  System.gc();
  
  //即将单活4,且我没有半活4以上的,只能堵
  Point mostBest = getBestPoint(human4Alives, computerSencodResults);
  if(mostBest!=null)
   return mostBest;
  
  Collections.sort(humanSencodResults);
  System.gc();
  
  mostBest = getBestPoint();
  if(mostBest!=null)
   return mostBest;
  
  //拿出各自排第一的,谁好就下谁
  return computerSencodResults.get(0).point;
 }
 
 //子类实现这个方法,并改变其顺序可以实现防守为主还是猛攻
 protected Point getBestPoint(){
  //即将单活4,且我没有半活4以上的,只能堵
  Point mostBest = getBestPoint(computerDouble3Alives, humanSencodResults);
  if(mostBest!=null)
   return mostBest;
  
  mostBest = getBestPoint(computer3Alives, humanSencodResults);
  if(mostBest!=null)
   return mostBest;
  
  mostBest = getBestPoint(humanDouble3Alives, computerSencodResults);
  if(mostBest!=null)
   return mostBest;
  
  mostBest = getBestPoint(human3Alives, computerSencodResults);
  if(mostBest!=null)
   return mostBest;

  mostBest = getBestPoint(computerDouble2Alives, humanSencodResults);
  if(mostBest!=null)
   return mostBest;
  
  mostBest = getBestPoint(computer2Alives, humanSencodResults);
  if(mostBest!=null)
   return mostBest;
  
  mostBest = getBestPoint(computer3HalfAlives, humanSencodResults);
  if(mostBest!=null)
   return mostBest;
  
  mostBest = getBestPoint(human4HalfAlives, computerSencodResults);
  if(mostBest!=null)
   return mostBest;
  
  mostBest = getBestPoint(humanDouble2Alives, computerSencodResults);
  if(mostBest!=null)
   return mostBest;
  
  mostBest = getBestPoint(human2Alives, computerSencodResults);
  if(mostBest!=null)
   return mostBest;
  
  mostBest = getBestPoint(human3HalfAlives, computerSencodResults);
  return mostBest;
 }
 
 
 //第三次分析的最后一步,第二次结果已经过排序,在此可以从前面选出最好的棋子
 protected Point getBestPoint(List<SencondAnalysisResult> myBest,List<SencondAnalysisResult> yourSencodResults){
  if(!myBest.isEmpty()){
   if(myBest.size()>1){
    for (SencondAnalysisResult your : yourSencodResults) {
     if(myBest.contains(your)){
      return your.point;
     }
    }
    return myBest.get(0).point;
   }else{
    return myBest.get(0).point;
   }
  }
  return null;
 }
 
 
 //第一次分析结果
 private final Map<Point,List<FirstAnalysisResult>> computerFirstResults = new HashMap<Point,List<FirstAnalysisResult>>();
 private final Map<Point,List<FirstAnalysisResult>> humanFirstResults = new HashMap<Point,List<FirstAnalysisResult>>();
 //第二次总结果
 protected final List<SencondAnalysisResult> computerSencodResults = new ArrayList<SencondAnalysisResult>();
 protected final List<SencondAnalysisResult> humanSencodResults = new ArrayList<SencondAnalysisResult>();
 //第二次分结果,电脑
 protected final List<SencondAnalysisResult> computer4HalfAlives = new ArrayList<SencondAnalysisResult>(2);
 protected final List<SencondAnalysisResult> computerDouble3Alives = new ArrayList<SencondAnalysisResult>(4);
 protected final List<SencondAnalysisResult> computer3Alives = new ArrayList<SencondAnalysisResult>(5);
 protected final List<SencondAnalysisResult> computerDouble2Alives = new ArrayList<SencondAnalysisResult>();
 protected final List<SencondAnalysisResult> computer2Alives = new ArrayList<SencondAnalysisResult>();
 protected final List<SencondAnalysisResult> computer3HalfAlives = new ArrayList<SencondAnalysisResult>();
 
 //第二次分结果,人类
 protected final List<SencondAnalysisResult> human4Alives = new ArrayList<SencondAnalysisResult>(2);
 protected final List<SencondAnalysisResult> human4HalfAlives = new ArrayList<SencondAnalysisResult>(5);
 protected final List<SencondAnalysisResult> humanDouble3Alives = new ArrayList<SencondAnalysisResult>(2);
 protected final List<SencondAnalysisResult> human3Alives = new ArrayList<SencondAnalysisResult>(10);
 protected final List<SencondAnalysisResult> humanDouble2Alives = new ArrayList<SencondAnalysisResult>(3);
 protected final List<SencondAnalysisResult> human2Alives = new ArrayList<SencondAnalysisResult>();
 protected final List<SencondAnalysisResult> human3HalfAlives = new ArrayList<SencondAnalysisResult>();
 
 //第一次分析前清空上一步棋子的分析结果
 private void initAnalysisResults(){
  computerFirstResults.clear();
  humanFirstResults.clear();
  //第二次总结果
  computerSencodResults.clear();
  humanSencodResults.clear();
  //第二次分结果
  computer4HalfAlives.clear();
  computerDouble3Alives.clear();
  computer3Alives.clear();
  computerDouble2Alives.clear();
  computer2Alives.clear();
  computer3HalfAlives.clear();
  
  //第二次分结果,人类
  human4Alives.clear();
  human4HalfAlives.clear();
  humanDouble3Alives.clear();
  human3Alives.clear();
  humanDouble2Alives.clear();
  human2Alives.clear();
  human3HalfAlives.clear();
  System.gc();
 }
 
 //加入到第一次分析结果中
 private void addToFirstAnalysisResult(FirstAnalysisResult result,Map<Point,List<FirstAnalysisResult>> dest){
  if(dest.containsKey(result.point)){
   dest.get(result.point).add(result);
  }else{
   List<FirstAnalysisResult> list = new ArrayList<FirstAnalysisResult>(1);
   list.add(result);
   dest.put(result.point, list);
  }
 }
 
 
 //第一次分析结果类
 private class FirstAnalysisResult{
  //连续数
  int count;
  //点位
  Point point;
  //方向
  int direction;
  //状态
  int aliveState;
  private FirstAnalysisResult(int count, Point point, int direction) {
   this(count, point, direction, ALIVE);
  }
  
  private FirstAnalysisResult(int count, Point point, int direction,int aliveState) {
   this.count = count;
   this.point = point;
   this.direction = direction;
   this.aliveState = aliveState;
  }
  

  
  private FirstAnalysisResult init(Point point,int direction,int aliveState){
   this.count = 1;
   this.point = point;
   this.direction = direction;
   this.aliveState = aliveState;
   return this;
  }
  
  private FirstAnalysisResult cloneMe(){
   return new FirstAnalysisResult(count, point, direction,aliveState);
  }
  
 }
 
 //第二次分析结果类
 class SencondAnalysisResult implements Comparable<SencondAnalysisResult>{
  int alive4 = 0;
  //活3数量
  int alive3 = 0;
  //半活4,一头封的
  int halfAlive4 = 0;
  //半活3,一头封的
  int halfAlive3 = 0;
  //活2数量
  int alive2 = 0;
  //点位
  Point point;
  
  @Override
  public int hashCode() {
   final int prime = 31;
   int result = 1;
   result = prime * result + ((point == null) ? 0 : point.hashCode());
   return result;
  }
  @Override
  public boolean equals(Object obj) {
   SencondAnalysisResult other = (SencondAnalysisResult) obj;
   if (point == null) {
    if (other.point != null)
     return false;
   } else if (!point.equals(other.point))
    return false;
   return true;
  }

  private SencondAnalysisResult(Point point) {
   this.point = point;
  }
  
  
  //第三次分析时,对第二次分析结果进行排序,此为排序回调函数
  @Override
  public int compareTo(SencondAnalysisResult another) {
   return compareTowResult(this, another);
  }
   
 }
 
 //返加-1则第一个参数优先,1则第二个参数优先,0则按原来顺序
 private int compareTowResult(SencondAnalysisResult oneResult,SencondAnalysisResult another){
  if(oneResult.alive4>another.alive4){
   return -1;
  }
  if(oneResult.alive4<another.alive4){
   return 1;
  }
  if(oneResult.halfAlive4>another.halfAlive4){
   return -1;
  }
  if(oneResult.halfAlive4<another.halfAlive4){
   return 1;
  }
  if(oneResult.alive3>another.alive3){
   return -1;
  }
  if(oneResult.alive3<another.alive3){
   return 1;
  }
  if(oneResult.alive2>another.alive2){
   return -1;
  }
  if(oneResult.alive2<another.alive2){
   return 1;
  }
  if(oneResult.halfAlive3>another.halfAlive3){
   return -1;
  }
  if(oneResult.halfAlive3>another.halfAlive3){
   return 1;
  }
  return 0;
 }
 
 
 //一个临时对象,供第一次分析时临时存放分析结果使用,如果分析出有活1以上(不含)的结果,则调用其cloneMe方法获得结果,否则抛弃此结果
 private final FirstAnalysisResult far = new FirstAnalysisResult(1, null, HENG);
 // 分析如果在当前位下一子,会形成某个方向上多少个子,参数:当前己方已下的所有点,当前要假设的点,需要判断的方向
 private FirstAnalysisResult tryAndCountResult(List<Point> myPoints,List<Point> enemyPoints, Point point,int direction) {
  int x = point.getX();
  int y = point.getY();
  FirstAnalysisResult fr = null;
  
  int maxCountOnThisDirection = maxCountOnThisDirection(point, enemyPoints, direction, 1);
  if(maxCountOnThisDirection<5){
   //无意义的棋子
   return null;//此方向不足五个空位,已排除己方已下的棋子
  }else if(maxCountOnThisDirection==5){
   //半死状态,当是一头通
   fr = far.init(point, direction,HALF_ALIVE);
  }else{
   //两头皆通
   fr = far.init(point, direction,ALIVE);
  }
  
  //在前和后的方向上计算一次
  countPoint(myPoints,enemyPoints,point.setX(x).setY(y),fr,direction,FORWARD);
  countPoint(myPoints,enemyPoints,point.setX(x).setY(y),fr,direction,BACKWARD);
  
  
  if(fr.count<=1 || (fr.count==2 && fr.aliveState==HALF_ALIVE)){//活1,半活2及其以下结果,抛弃
   return null;
  }
  //返回复制的结果
  return fr.cloneMe();
 }
 
 //棋子出了墙
 private boolean isOutSideOfWall(Point point,int direction){
  if(direction==HENG){
   return point.getX()<0 || point.getX()>=maxX;//最大的X和Y值均在墙外所以用等号
  }else if(direction==ZHONG){
   return point.getY()<0 || point.getY()>=maxY;
  }else{//这里可能有问题
   return point.getX()<0 || point.getY()<0 || point.getX()>=maxX || point.getY()>=maxY;
  }
 }
 
 private Point pointToNext(Point point,int direction,boolean forward){
  switch (direction) {
   case HENG:
    if(forward)
     point.x++;
    else
     point.x--;
    break;
   case ZHONG:
    if(forward)
     point.y++;
    else
     point.y--;
    break;
   case ZHENG_XIE:
    if(forward){
     point.x++;
     point.y--;
    }else{
     point.x--;
     point.y++;
    }
    break;
   case FAN_XIE:
    if(forward){
     point.x++;
     point.y++;
    }else{
     point.x--;
     point.y--;
    }
    break;
  }
  return point;
 }
 
 //在某个方向(八个中的一个)可下多少棋子,这个方法是第一分析中的核心方法
 private void countPoint(List<Point> myPoints, List<Point> enemyPoints, Point point, FirstAnalysisResult fr,int direction,boolean forward) {
  if(myPoints.contains(pointToNext(point,direction,forward))){
   fr.count ++;
   if(myPoints.contains(pointToNext(point,direction,forward))){
    fr.count ++;
    if(myPoints.contains(pointToNext(point,direction,forward))){
     fr.count ++;
     if(myPoints.contains(pointToNext(point,direction,forward))){
      fr.count ++;
     }else if(enemyPoints.contains(point) || isOutSideOfWall(point,direction)){
      fr.aliveState=HALF_ALIVE;
     }
    }else if(enemyPoints.contains(point) || isOutSideOfWall(point,direction)){
     fr.aliveState=HALF_ALIVE;
    }
   }else if(enemyPoints.contains(point) || isOutSideOfWall(point,direction)){
    fr.aliveState=HALF_ALIVE;
   }
  }else if(enemyPoints.contains(point) || isOutSideOfWall(point,direction)){
   fr.aliveState=HALF_ALIVE;
  }
 }
 
 

 //在某个方向上是否还能下到满五个棋子
 private int maxCountOnThisDirection(Point point,List<Point> enemyPoints,int direction,int count){
  int x=point.getX(),y=point.getY();
  switch (direction) {
  //横向
  case HENG:
   while (!enemyPoints.contains(point.setX(point.getX()-1)) && point.getX()>=0 && count<6) {
    count ++;
   }
   point.setX(x);
   while (!enemyPoints.contains(point.setX(point.getX()+1)) && point.getX()<maxX && count<6) {
    count ++;
   }
   break;
  //纵向
  case ZHONG:
   while (!enemyPoints.contains(point.setY(point.getY()-1)) && point.getY()>=0) {
    count ++;
   }
   point.setY(y);
   while (!enemyPoints.contains(point.setY(point.getY()+1)) && point.getY()<maxY && count<6) {
    count ++;
   }
   break;
  //正斜向 /
  case ZHENG_XIE:
   while (!enemyPoints.contains(point.setX(point.getX()-1).setY(point.getY()+1)) && point.getX()>=0 && point.getY()<maxY) {
    count ++;
   }
   point.setX(x).setY(y);
   while (!enemyPoints.contains(point.setX(point.getX()+1).setY(point.getY()-1)) && point.getX()<maxX && point.getY()>=0 && count<6) {
    count ++;
   }
   break;
  //反斜 /
  case FAN_XIE:
   while (!enemyPoints.contains(point.setX(point.getX()-1).setY(point.getY()-1)) && point.getX()>=0 && point.getY()>=0) {
    count ++;
   }
   point.setX(x).setY(y);
   while (!enemyPoints.contains(point.setX(point.getX()+1).setY(point.getY()+1)) && point.getX()<maxX && point.getY()<maxY && count<6) {
    count ++;
   }
   break;
  }
  return count;
 }
 
 //下棋子,对外接口
 @Override
 public void run(List<Point> humans,Point p) {
  //把人类下的最后一步棋子去除
  allFreePoints.remove(humans.get(humans.size()-1));
  //电脑可以下的一步棋子
  Point result = doAnalysis(myPoints, humans);
  //去除电脑下的棋子
  allFreePoints.remove(result);
  //加入到电脑棋子中,下棋了
  myPoints.add(result);
 }
}
人类玩家实现起来就非常简单


import java.util.List;

public class HumanPlayer extends BasePlayer {

 @Override
 public void run(List<Point> enemyPoints,Point p) {
  getMyPoints().add(p);
  allFreePoints.remove(p);
 }
}

总结:虽然是Java写的但算法已被抽象可以方便的修改成各种平台的实现。


2013年11月5日 星期二

Particle System Tutorial on Android

http://code.google.com/p/bayninestudios/source/checkout
http://www.bayninestudios.com/2010/04/particle-system-tutorial-on-android/


Particle systems are a way to add effects to a game, like an explosion, fire, or water fountain. They usually don’t interact with each other or the world around them, to improve performance. Even a simple system can have 50 particles which are a lot of polygons to do for an effect. First, you’ll want to start a new Android project and paste in the simplest GLSurfaceView application from the Google Android Blog. This is a great starting point for any OpenGL app on Android, I use it all the time when starting a new OpenGL project. It really helped me get up and running quickly so I could start learning more about OpenGL rather than struggle with learning Android and OpenGL at the same time.
First, we need to make a particle object.
package com.bayninestudios.particlesystemdemo;

import java.util.Random;

public class Particle {

    // location
    public float x;
    public float y;
    public float z;

    public Particle(Random gen)
    {
        this.x = gen.nextFloat();
        this.y = gen.nextFloat();
        this.z = gen.nextFloat();
    }
}
Simple particle with an x,y,z coordinate and assigned a random value between 0 and 1. The local variables are public, rather than using getters and setters for performance reasons. Now this particle needs to belong in a system.
package com.bayninestudios.particlesystemdemo;

import com.bayninestudios.particlesystemdemo.Particle;

public class ParticleSystem {
    private Particle[] mParticles;
    private int PARTICLECOUNT = 20;
 
    public ParticleSystem() {
        mParticles = new Particle[PARTICLECOUNT];

        Random gen = new Random(System.currentTimeMillis());
        for (int i=0; i < PARTICLECOUNT; i++) {
            mParticles[i] = new Particle(gen);
        }
    }
}
Simple. Now we have to draw everything. Rather than have a draw routine for each particle, with an assigned texture and vertexes, I have one object that gets allocated by the system and drawn. For each draw, you'll need to do the appropriate translates for the position of the particle. The ParticleSystem class will need a vertex buffer and index buffer to define the triangle to draw. I've also added two helper methods to convert float or short arrays to native order buffers. In previous Android versions I just used FloatBuffer.wrap(float[]) but newer versions require the native order buffers.
    // for use to draw the particle
    private FloatBuffer mVertexBuffer;
    private ShortBuffer mIndexBuffer;
 
    public ParticleSystem() {
        mParticles = new Particle[PARTICLECOUNT];

        Random gen = new Random(System.currentTimeMillis());
        for (int i=0; i < PARTICLECOUNT; i++) {
            mParticles[i] = new Particle(gen);
        }

        float[] coords = {
                -0.1f,0.0f,0.0f,
                0.1f,0.0f,0.0f,
                0.0f,0.0f,0.1f};
        short[] icoords = {0,1,2};

        mVertexBuffer = makeFloatBuffer(coords);
        mIndexBuffer = makeShortBuffer(icoords);
    }

    // use to make native order buffers
    private FloatBuffer makeFloatBuffer(float[] arr) {
        ByteBuffer bb = ByteBuffer.allocateDirect(arr.length*4);
        bb.order(ByteOrder.nativeOrder());
        FloatBuffer fb = bb.asFloatBuffer();
        fb.put(arr);
        fb.position(0);
        return fb;
    }

    // use to make native order buffers
    private ShortBuffer makeShortBuffer(short[] arr) {
        ByteBuffer bb = ByteBuffer.allocateDirect(arr.length*4);
        bb.order(ByteOrder.nativeOrder());
        ShortBuffer ib = bb.asShortBuffer();
        ib.put(arr);
        ib.position(0);
        return ib;
    }
Also ParticleSystem will need a draw routine. This routine setups the vertex buffer to use, the color, translates the draw to the location of the particle, and draws it.
    public void draw(GL10 gl) {
        gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mVertexBuffer);
        gl.glColor4f(1f, 1f, 1f, 1f);
        for (int i = 0; i < PARTICLECOUNT; i++) {
            gl.glPushMatrix();
            gl.glTranslatef(mParticles[i].x, mParticles[i].y, mParticles[i].z);
            gl.glDrawElements(GL10.GL_TRIANGLES, 3, GL10.GL_UNSIGNED_SHORT, mIndexBuffer);
            gl.glPopMatrix();
        }
    }
Finally, in the main ParticleSystemDemo class, create a new ParticleSystem, initialize the GL perspective and call the draw routine.
class ClearRenderer implements GLSurfaceView.Renderer {
    private ParticleSystem mParticleSystem;
 
    public ClearRenderer() {
        mParticleSystem = new ParticleSystem();
    }

    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        GLU.gluPerspective(gl, 15.0f, 80.0f/48.0f, 1, 100);
        GLU.gluLookAt(gl, 0f, -10f, 15f, 0.5f, 0.0f, 0f, 0.0f, 1.0f, 1.0f);
    }

    public void onSurfaceChanged(GL10 gl, int w, int h) {
        gl.glViewport(0, 0, w, h);
    }

    public void onDrawFrame(GL10 gl) {
     gl.glClearColor(0, 0, .5f, 1.0f);
        gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
        gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
        mParticleSystem.draw(gl);
    }
}
After all of this you should have a blue background with a bunch of triangle particles in the middle of the screen. They don't move yet but that will be for part two of this. There are other improvements to make, possibly having the system assign the initial location of the particle. Randomizing the color. The particle size is hardcoded, and could be dynamic if needed. Texture mapping the particles. Moving the particle drawing to a different class, that way there can be multiple particle models for the same system.
The most recent code can be found at Bay Nine Studios on Google Code.

8 thoughts on “Particle System Tutorial on Android”  




2013年11月4日 星期一

使GLSurfaceview透明 可见背景图片

blog.csdn.net/cc_lq/article/details/6629659

以下是从网上搜到的方法,我的程序是要给一个activity加背景图片,activity上有一个GLSurfaceview,只有使GLSurfaceview透明之后才能看到背景图片。
参考:http://www.bangchui.org/read.php?tid=14982
http://www.bangchui.org/read.php?tid=16672

如何实现一个透明的GLSurfaceview,Surfaceview与GLSurfaceview原理类似。