修改cocos2d-x提供的build_native.sh

在cocos2d-x專案中要編譯native code時會用到build_native.sh來做編譯,這個Bash腳本會使用NDK的ndk-build

隨著專案越來越複雜有些需求需要特別去處理,以我自己的狀況來講,須要分為Debug與Release兩個版本以及分為國際版與韓國版,上述兩個部分都可以經由修改build_native.sh來達到,下面會分成幾個部分來說:

先列出幾個教學網站
http://www.ibm.com/developerworks/cn/linux/l-bash-parameters.html
http://linuxconfig.org/Bash_scripting_Tutorial#18-redirections
http://linux.vbird.org/linux_basic/0340bashshell-scripts.php
我都是有需要在去查

基本上這個部份我是在版本2.0.3加的 但是最新的2.1.3應該也可用

選項修改

原本build_native.sh中的選項部分的程式碼如下
其實要不是因為要改這個部份我還不知道這個腳本有選項可以用

while getopts "sh" OPTION; do  
case "$OPTION" in  
s)  
buildexternalsfromsource=1  
;;  
h)  
usage  
exit 0  
;;  
esac  
done  

上面的寫法主要是處理”-s”這種型態的選項,也就是說如果今天的的指令是長下面這樣:
bash build_native.sh -s xxx
就只會處理-s的部分。

原本是想說就再加一個選項,結果並不如我想的這樣簡單,基本上先看到下面跟ndk-build有關的程式碼如下

"$NDK_ROOT"/ndk-build -C "$APP_ANDROID_ROOT" $* \
NDK_MODULE_PATH=${COCOS2DX_ROOT}:${COCOS2DX_ROOT}/cocos2dx/platform/third_party/android/source"  

重點在於$*代表該腳本的所有參數,如果今天我用了一個ndk-build已經有的參數就會影響ndk-build的行為,如果用了一個沒有的又會說是一個invalid option。

此時有兩個做法 一個是把 $*拿掉,但是這樣有一個缺點,原本可以下如下指令
bash build_native.sh clean,也就是ndk-build clean,就會變成不能用。

另一個做法就是在選項的部分作修改,以下是我的改法:

for p in $*
do
     echo "$p"
     case $p in
          "kr")
               echo "force build kr"
               forcebuildkr=1
               #Can not pass this parameter to next call bash
               ;;
          "debug")
               echo "get debug"
               debugflag=1
               #Can not pass this parameter to next call bash
               ;;
          "-s")
               buildexternalsfromsource=1
               param="$param$p "
               ;;
          "-h")
               usage
               exit 0
               ;;
          *)
               param="$param$p "
               ;;
     esac
done  

這是另一種選項的做法,以上面提到的bash build_native.sh -s xxx,在這裡就會全都處理到,又由於我不想全部選項都傳到ndk-build,所以該傳的我就存在$param,再把$*改為$param即可,此時我已經可以透過選項來選擇我要怎樣的版本。

利用Application.mk分不同的版本

在最早的時候,我在分debug與release版的時候是直接去android.mk中修改CPPFLAG,後來覺得實在是太麻煩要改來改去,就採取我現在使用的方法

ndk-build有一個選項:ndk-build NDK_APPLICATION_MK=,可以讓你自己指定要使用的Application.mk,所以我將不同狀況需要的CPPFLAG設定寫到不同的Application.mk檔中,再依照選項不同設定不同的數值去決定到底要用哪一個,大概如下:

if [[ "$debugflag" ]]; then
     echo "debug on!!"
     if [[ "$forcebuildkr" ]]; then
          echo "force build kr!!"
          usedebugappmk="${APP_ANDROID_ROOT}/jni/Application_debug_kr.mk"
     else
          echo "build global!!"
          usedebugappmk="${APP_ANDROID_ROOT}/jni/Application_debug.mk"
     fi
else
     echo "debug off!!"
     if [[ "$forcebuildkr" ]]; then
          echo "force build kr!!"
          usedebugappmk="${APP_ANDROID_ROOT}/jni/Application_kr.mk"
     else
          echo "build global!!"
          usedebugappmk="${APP_ANDROID_ROOT}/jni/Application.mk"
     fi
fi
"$NDK_ROOT"/ndk-build NDK_APPLICATION_MK="$usedebugappmk" -C "$APP_ANDROID_ROOT" $param \
"NDK_MODULE_PATH=${COCOS2DX_ROOT}:${COCOS2DX_ROOT}/cocos2dx/platform/third_party/android/prebuilt"

控制資源

一般cocos2d-x專案的資源檔都放在Resource資料夾下,後來我遇到了某種情況是某些檔案只在android狀況下改變,所以我另外開個一個資料夾Resource_android,在原本的# copy resources腳本下,添加了一段處理android特有資源的程式:

# copy ver android
for file in "$APP_ROOT"/Resource_android/*
do
if [ -d "$file" ]; then
    cp -rf "$file" "$APP_ANDROID_ROOT"/assets
fi
if [ -f "$file" ]; then
    cp "$file" "$APP_ANDROID_ROOT"/assets
fi
done

另外也可以將腳本寫在別的.sh中在去呼叫如下

if [[ "$debugflag" -ne 1 ]]; then
     echo "delete .lua .bat .exe"
     if [ -e "del_lua.sh" ]; then
          ./del_lua.sh
     fi
fi

專案在做release版時會將lua檔全部刪除,這部分是寫在另一個.sh中。

JNI中是否需要自行呼叫DeleteLocalRef

這是一個被問到我一時之間答不出來的問題,後來去找了一下

JNI電子書籍
http://192.9.162.55/docs/books/jni/
http://www.soi.city.ac.uk/~kloukin/IN2P3/material/jni.pdf

下面是相關說明
引自 http://192.9.162.55/docs/books/jni/html/refs.html

A local reference is valid only within the dynamic context of the native method that creates it, and only within that one invocation of the native method. All local references created during the execution of a native method will be freed once the native method returns.

There are two ways to invalidate a local reference. As explained before, the virtual machine automatically frees all local references created during the execution of a native method after the native method returns. In addition, programmers may explicitly manage the lifetime of local references using JNI functions such as DeleteLocalRef.

Why do you want to delete local references explicitly if the virtual machine automatically frees them after native methods return? A local reference keeps the referenced object from being garbage collected until the local reference is invalidated. The DeleteLocalRef call in MyNewString, for example, allows the intermediate array object, elemArr, to be garbage collected immediately. Otherwise the virtual machine will only be able to free the elemArr object after the native method that calls MyNewString (such as C.f above) returns.

基本上就是說

  • Local Reference只在其創建方法的dynamic context內有效。
  • 兩個狀況下Local Reference會失效:使用DeleteLocalRef,其創建方法返回時。
  • 既然返回時會失效為啥要呼叫DeleteLocalRef,在於呼叫後就立刻允許VM回收而非等到方法返回之後。

個人感覺,這跟java中將用完物件的ref設為null的感覺很像。

Android設定寄送夾帶附件的E-mail時附件無法使用

問題起因

基本上使用常見的方法如下

Intent i = new Intent(Intent.ACTION_SEND);
i.setType("image/jpeg");
i.putExtra(Intent.EXTRA_EMAIL  , new String[]{sendTarget});
i.putExtra(Intent.EXTRA_SUBJECT, sendSubject);
i.putExtra(Intent.EXTRA_TEXT   , sendText);
i.putExtra(Intent.EXTRA_STREAM, Uri.parse("file://"+ filePath));
try {
     curActivity.startActivity(Intent.createChooser(i, "Please select Email client"));
} catch (android.content.ActivityNotFoundException ex) {
    Toast.makeText(curActivity, "There are no email clients installed.", Toast.LENGTH_SHORT).show();
}

問題出於我給的是Internal Storage的路徑,所以基本上當其它的應用程式要取用檔案時有有存取權限問題

解決方式

copy一份到External Storage再用copy檔的路徑即可

File externalPicPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
File dstFile = new File(externalPicPath, "picture.png");
try{
    FileInputStream in = new FileInputStream(filePath);
    FileOutputStream out = new FileOutputStream(dstFile);
    byte[] buf = new byte[1024];
    int len;
    while ((len = in.read(buf)) > 0) {
        out.write(buf, 0, len);
    }
    in.close();
    out.close();
}
catch(IOException e)
{
    Toast.makeText(curActivity, "I/O Error.", Toast.LENGTH_SHORT).show();
     return;
}

Unity練習:使用Texture Packer自製簡易2D Sprite物件

這是一個基本的練習,記載練習中學到的一些東西,基本上公司專案是使用NGUI這個plugin。
檔案放在 https://github.com/hsienwei/Unity_2D_Sprite_Test
呈現畫面大概如下

Texture Packer與JSON Parser

Texture Packer是一個功能很完整的工具軟體,可以讓你把一些分散的圖包成一個大圖,可以節省記憶體與加快讀取速度,在以前使用cocos2d-x時就常常在用,現在他也提供Unity 3D的格式,但以這個格式匯出的檔案基本上是一個JSON格式的檔案,只是副檔名存成txt,我想是因為Unity的TextAsset只能是.txt的檔案,另外在格式當中另外有JSON(Array)與JSON(Hash)都是JSON格式,主要差在Frame的資料是用array存或者是object存,Unity 3D的格式跟JSON(Hash)的內容是一樣的。

確定為JSON格式後就是選擇parser,試了兩個現有的library(Asset store中也有但我沒試)

http://wiki.unity3d.com/index.php/JSONObject
這一個在讀Texture Packer的檔案時會有點問題,看了一下居然是無法處理空格囧,基本上要處理的話在JSONObject.cs中Line61加一行 str = str.Replace(" ", ""); 就可以解決,但是後來想想還是算了。

http://wiki.unity3d.com/index.php?title=UnityLitJSON
這個是基於一個C#的JSON Library : LitJSON去做的,看了一下這個相關文件比較多而且比較便於使用。
他提供了兩個用法:

  • Mapping JSON to objects
  • Readers and Writers

我用的是Mapping JSON to objects,比較接近我以前的經驗。
下面是LitJSON一些相關資料
http://www.cnblogs.com/peiandsky/archive/2012/04/20/2459219.html 簡單介紹使用方法
http://litjson.sourceforge.net/doc/manual.html 官方說明

基本上上面的一些步驟有參考下面這兩篇
http://blog.csdn.net/midashao/article/details/8220868 Unity3D之结合TexturePacker使用显示贴图
但我用的不是JSON(Array) 且我用的LitJSON不是原版的,似乎有被加工處理
http://tpathuis.tumblr.com/post/42501893370/texturepacker這一篇是Texture Packer中Unity 3D分類的Turtorial,這裡有一些Texture Packer的設定,我用的設定比較貼近這個,沒有開Allow rotation,但我有開Trim Mode。

設定好後publish,就可以產出兩個檔案 一個txt檔一個png檔,將這兩個檔案放到unity project 中asset/resources資料夾中之後使用。
(註記一下如果要用Resources.Load()去讀檔案一定要放在名稱為”resources”的資料夾中)

建立Mesh物件

2D Sprite由一個平面組成基本上只要兩個三角面,但Unity的內建的Plane有200個三角面,實在是多太多了,所以有必要自己弄一個面數較少的來使用。
要建立Mesh資料,可以使用Mesh Filter這個component,然後再設定Mesh的vertices, uv與triangles,我的設定內容如下連結:
https://github.com/hsienwei/Unity_2D_Sprite_Test/blob/master/Assets/script/Plane.cs
我的做法是依照frame的資訊去設定vertices的寬高,跟NGUI的做法上不太一樣,NGUI的設定基本上寬高都是1*1,藉由調整Scale去調整為該frame的寬高,我的做法是設定vertices的位置為該frame的寬高,調整scale的話就是調整縮放比例。
至於uv的部分就是將座標值轉成0-1之間的數值,另外由於座標系統不一樣,必須將y軸做調整。

另外要注意因為每個atlas都是一張圖,所以在atlas物件中有一個material欄位是用來對各個sprite物件設定renderer的material,這裡主要是因為想要減少drawcall,下面是相關資料的連結:
http://docs.unity3d.com/Documentation/Manual/DrawCallBatching.html
Dynamic Batching的條件中有一項是使用相同的Material,所以我從之前各個sprite都自己new material物件改為在atlas讀檔時new material,在sprite設定時再參照到atlas的material,由於sprite的頂點夠少,所以unity會自動幫我們處理batching。
(如果要自己使用batching script,在Import Package->script 匯入Standard Assets/Scripts/Utility Scripts內的兩個script)。

editor

這個練習我寫了兩個主要的script,一個是atlas,另一個是Plane,這兩個我在做法上有點不同。
首先是atlas,他的畫面與部分程式碼如下

主要設定 TextAsset這個欄位,下面的一些資訊會自動生成(我這裡有一點bug,設定好TextAsset 後需要play一次下面的資訊才會出現),基本上這邊我只有使用最簡單的方式,也就是在程式的部分設定為public,這些值就會出現在Inspector介面中供調整,TextAset以下的其實可以不要出現在Inspector,但為了方便看相關資訊所以讓他出現。

下面的是Plane這個sprite

這一個主要是使用custom editor,自己寫一個editor的類別去處理這個script在Inspector中顯示的GUI介面,程式碼如下
https://github.com/hsienwei/Unity_2D_Sprite_Test/blob/master/Assets/editor/PlaneEditor.cs

可以參考一下editor類別的說明 http://docs.unity3d.com/Documentation/ScriptReference/Editor.html
還有官方文件http://docs.unity3d.com/Documentation/Components/gui-ExtendingEditor.html
相關教程 http://www.youtube.com/watch?v=WlGwBmM-dfA

自己記錄的幾個點

  • 要用custom editor 要在前面加上 [CustomEditor(typeof(CLASS))]
  • 繼承 Editor
  • 要放在editor資料夾
  • 可以使用OnEnable, OnInspectorGUI兩個方法,OnEnable會在該GUI出現時執行一次,OnInspectorGUI會執行數次(我還不確定次數的根據)
  • 如果改了Scene裡的物件屬性有時候不會及時更新,可呼叫SceneView.RepaintAll();
  • 可用GUI.changed去判定是否有變更過屬性
  • 基本上你在GUI修改數值是不會被存起來的(play時或play完就會回到原樣),需要用EditorUtility.SetDirty告訴Unity存起來
  • 使用target存取目標物件
  • 可以使用SerializedObject輔助,修改target時SerializedObject取property會是改變前的數值(相關討論 http://answers.unity3d.com/questions/43611/oninspectorgui-using-the-default-object-selection.html)

大概就是這樣,這個練習應該還有一些bug,但基本上我學了不少

cocos2d-x 2.0.3 跨平台專案經驗

注意線程安全

不要經pthread開線程(Native code)還使用JNI呼叫Java Code
基本上是線程安全問題
如果這樣做很容易出問題
有遇過下面的狀況

  1. 字串被卡掉
  2. JNI Call找不到jclass
  3. JNI Call找到jclass,但仍然出錯,加上程式是多執行緒,除錯十分困難

基本上後來都採取pthread傳訊息,schedule再處理的模式

map erase 的使用

map的erase方法不會回傳刪除後下一個元素的iterator(vector會)
曾經遇到一個狀況如下:

for (itMap = _mapData.begin(); itMap != _mapData.end(); ++itMap)
{
    if (...)
    {
        _mapData.erase(itMap);
    }
}

這樣的用法是有問題的
在iOS運行正常,但在Android有可能造成無限迴圈

正確的方式應該如下

for (itMap = _mapData.begin(); itMap != _mapData.end(); )
{
    if (...)
    {  //移除
        _mapData.erase(itMap++);
    }
    else
    {  //不移除
        ++itMap;
    }
}

cocos2d-x文字寬度不同平台差異

CCLabalTTF iOS版跟Android版的中文字(字型黑體)在設定相同字型大小時,實際顯示大小有不同,如果設定寬度交由cocos2d-x去斷行基本上沒有什麼問題,但是如果因為文句美觀而自行設定斷行時,就要注意兩平台寬度不同造成顯示效果的不同。

Android的CCRenderTexture

這個類別在Android常常出狀況,基本上下面這兩個我都遇到過(2.0.3剛出的時候)。
http://www.cocos2d-x.org/news/75 2.0.4修正
http://www.cocos2d-x.org/issues/1544 2.1.1修正

現在看來是都修好了,只是那時候剛換2.0.3的時候真的很慘,不確定何時會修好,就算確定也不能等,所以android用到的部分幾乎都要再另寫一個版本,使用的人在Android版最好多加測試,因為cocos2d-x的開發者也不是能夠測到所有裝置,須仰賴整體社群的幫忙。

cocos2d-x 預設libcurl 不支援SSL

cocos2d-x 中有提供libcurl 的這個URL連線程式庫,不過內建的沒有支援ssl,基於專案的需求所以要更換為有SSL功能的。
原本想要自己編,不過看了一下官方的建議做法,覺得實在太麻煩,後來發現下面這個網址:
https://github.com/dumganhar/libcurl-build
這應該算cocos2d-x官方提供的,可以直接使用。

版本號的設定

一開始iOS的版本號設定是以x.x.x這樣的形式去處理的。
遊戲移植到android後,由於需要在某些版本快速修正bug後出一個新版本,所以在android有些版本versionName是像x.x.x.y這樣。
後來iOS版由於某些原因需要以某版為基礎修正後再出一版,版本號原本是想要跟android一樣,但後來發現iOS版本的專案設定中,版本的設定分為version與build,version的設定只能x.x.x,build則無限制。
因為在這個專案中的版本號會影響到一些跟server取資料時的判定,所以版本號需要相同。
iOS跳號後android也需要跟進,所以在這一點需要注意。