2015年6月21日 星期日

[遊戲] 迷宮賽車

Android Play Store:

iOS App Store:

一個結合速度和策略的全新型態遊戲,而且全部免費
高達80個免費關卡等你來挑戰.
只要滑動手指就能享受樂趣.
需要收集所有的旗子. 但是,過程中一直會有瘋狂火箭追逐著你.
當你改變車道時,要小心已經有炸彈在那等著你.
每關的挑戰都不同,你必須快速的做出反應,而且需要聰明的策略來通過那些障礙物.
移動炸彈,從天而降的飛碟,無敵的坦克...等等,你永遠不知道下一關又會跑出什麼新變化.





[Games] Crazy Maze Racing

Android Play Store:

iOS App Store:

A game combines speed with strategy.
Up to 80 rounds for you to challenge, and all are free.
Just slide your finger to control the car. 
Need to collect all flags within limited time. 
In the meanwhile, there always have crazy rockets chasing you.
Be careful of bombs when you change the lane.
Every round has various challenges. 
You need to respond quickly with smart strategy to get through obstacles in your way.
Walking bombs, fly UFO, unbeatable tanks..., you can never expect what will come out for next round.





2015年6月16日 星期二

[Corona SDK] How to implement In-App Purchases (IAP)?

It seems that In-App Purchases (IAP) feature is more popular nowadays.
That is, the App itself is free. 
However, if you want to get no-ads version, get all rounds, add some features or retrieve some weapons and so on, you will need to pay then.
This is what called In-App Purchases (IAP).

Below, we will consider about Android and iOS both.
Let's check the common codes shown below first, and discuss them later:
local store
local currentProductList = nil

local appleProductList = {
    {"com.gmail.myTest.DemoApp.GetFullVersion"}
}

local googleProductList = {
    "com.gmail.mytest.demo_app.getfullversion",
}
local function storeTransaction( event )
    local transaction = event.transaction

    if ( transaction.state == "purchased" ) then
     print("transaction.state == purchased")
        if(fullVersion == false) then
         fullVersion = true
     end
    elseif ( transaction.state == "restored" ) then
     --iOS only
     print("transaction.state == restored")
        print( "productIdentifier:", transaction.productIdentifier )
        print( "originalReceipt:", transaction.originalReceipt )
        print( "originalTransactionIdentifier:", transaction.originalIdentifier )
        print( "originalDate:", transaction.originalDate )
       if(fullVersion == false) then
         fullVersion = true
     end
    elseif ( transaction.state == "refunded" ) then
     --refunds are only supported by the Android
     print("transaction.state == refunded")        
        print( "productIdentifier:", transaction.productIdentifier )
        if(fullVersion == true) then
         fullVersion = false
     end
    elseif ( transaction.state == "consumed" ) then
     --consumed are only supported by the Android
     print("transaction.state == consumed")        
        print( "productIdentifier:", transaction.productIdentifier )
  if(fullVersion == true) then
   fullVersion = false
  end
  elseif event.transaction.state == "cancelled" then
   --For iOS, if user cancels the IAP action, it will come to here 
   print("transaction.state == cancelled")
  elseif event.transaction.state == "failed" then 
   --[[
   -1003: unknown
   -1005: user canclled,For Android, if user cancels the IAP action, it will come to here
  -7: already owned
  8: item not owned
   ]]
   print("transaction.state == failed")
   print("Transaction failed, type:"..tostring(event.transaction.errorType)..",-1005")
   print("Transaction failed, error:"..tostring(event.transaction.errorString)) 
    end    

    store.finishTransaction( event.transaction )
end


if ( isAndroid ) then
    store = require( "plugin.google.iap.v3" )
    currentProductList = googleProductList
    store.init( "google", storeTransaction )     
else
    store = require( "store" )
    currentProductList = appleProductList
    store.init( "apple", storeTransaction )
end 

--put the following codes somewhere, such as when user press some button 
if(store.canMakePurchases) then
 store.purchase( currentProductList[1] )
end

Android
First, you need to add your App in Play Store Publish.
After that, enter the need added App and then add the In-App products.
There are three types of IAP products for google IAP: managed, unmanaged and subscription.
Managed: It is for one-time shopping, e.g. changed from lite version to full version.
For this kind of products, Google will store the purchase information.
If user change phones or re-install the App, they can restore those managed products from Google server if they are using the same accounts.
Unmanaged: It is for those products which can be purchased again and again, e.g. weapons or blood
values of roles.
The Google server will not store the purchases records.
That is, the developers need to have some ways to store such information themselves.
Subscription: Let's not discuss it currently.

After that, we need to fill the IAP item id in googleProductList, as shown in above codes.

In Play Store Publish, go to the "Services & APIs" page and get the license key for this application.
It is a very long string. Just copy it and paste to the key field in config.lua:
application = 
{
 content = 
 { 
  width = 640,
  height = 960, 
  scale = "zoomStretch",
 },
 license =
    {
        google =
        {
            key = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
        },
    },
}


Next, we need to add the plugins and permissions in build.settings:
settings =
{
 plugins =
    {
        ["plugin.google.iap.v3"] =
        {
            publisherId = "com.coronalabs",
            supportedPlatforms = { android=true }
        },        
    },
 android =
    {
        usesPermissions =
        {
            "com.android.vending.BILLING",
        },
    },
}

Purchase
To purchase, we only need to call store.purchase( currentProductList[1] ).
However, we need to upload the built apk file to Play Store Publish first.
The version code of  uploaded apk file should be the same as what apk you want to test.
You can modify your codes and build many times, just remember to use the same version code.
We can upload to ALPHA testing or BETA testing.
If there is no apk file with same version code in Play Store Publish, you will get error message telling that the IAP function is not provided in this version.
In above codes, we check store.canMakePurchases before calling store.purchase( currentProductList[1] ). 
store.canMakePurchases is for iOS only. It will be true always for Android platform.

After purchasing successfully, the callback function storeTransaction will enter transaction.state == "purchased".
We can modify the related authorization there.

Test
We are not allow to test IAP function with the same account.
We need to get another account for testing.
If the account in your phone is the same as your developer account, you need to back to factory default and set new account.

We can go to Play Store Publish, find the setting page and then add the test account.
When you purchase your IAP products with this testing account, it will not spend you any money.

During our test, we are not sure if the problems are caused by wrong coding, or by wrong setting in Play Store Publish.
Google suggest us that we should do what called "Static Responses" test first.
That is, we can replace the items in googleProductList to be following items:
"android.test.purchased"
"android.test.canceled"
"android.test.refunded"
"android.test.item_unavailable"
These items are defined by Google.
We don't need to set anything IAP items in Play Store Publish in advance.
We don't need to upload our apk file either.
We'd better use them to test our IAP related codes first.

Refund
Google allow user to perform refund after purchasing.
Basically, we just need change the related authorization in  transaction.state == "refunded" of callback function storeTransaction.

Comsuming items
This is Google specific feature.
If your item is managed type, you may need to re-purchase after you modify some error codes.
We can consume the purchased record in Google server:
store.consumePurchase( currentProductList[1], storeTransaction )
The callback function storeTransaction should enter transaction.state == "consumed" then.
After that, we can perform store.purchase( currentProductList[1] ) again.
If you don't do store.consumePurchase(), you will get the error message in transaction.state == "failed", which tells you that the item is already owned.

Restore
If users have ever bought the managed products, they may install your App in different devices or re-install your App with the same account.
We need to restore the related authorization by following method:
store.restore( )
After calling it, the callback function storeTransaction will enter transaction.state == "purchased".
Google does not ask when or how we should do restore.
We can perform restore automatically or add a restore button.
If user did not buy it before, nothing will happen after calling store.restore( ) .
As mentioned above, doing purchase again will get error message "already owned", instead of entering transaction.state == "purchased" or transaction.state == "restored" 
We have to call store.restore( ) somewhere.

iOS
You have to visit iTunes Connect to add your new App.
We can add IAP products after adding new App successfully.
There are five types of IAP products provide by Apple.
Consumable products are similar with unmanaged products mentioned above.
Non-Consumable products are similar with managed products mentioned above.

Similarly, we need to fill the IAP item id in appleProductList of our codes.
Note that appleProductList is in table format, which is different from googleProductList.

Purchase
To purchase, we need call store.purchase( currentProductList[1] ).
We have to check store.canMakePurchases because iOS devices have a setting that disables purchasing.
After purchasing successfully, the callback function storeTransaction will enter transaction.state == "purchased".
We can modify the related authorization there.

Test
Our App has not been published to Store.
When we do IAP test, we will get the error message telling that the App cannot connect with iTunes Store.
To test it, we need to setup the sandbox environment.
Visit iTunes Connect, go to the "Manage Users" page and add the test counts.
The added accounts cannot be any accounts which have already existed.
That is, you can use those accounts in your phones or tablets.
We have to create a new Apple id for testing.
A suggested way is to combine your original account with test1, test2, and so on.
For example, if your phone Apple id is "ABC", then you can add test account "ABC+test1", "ABC+test2"....
Apple does not offer the function for eliminating purchased record, like store.consumePurchase() for Android we mentioned before.
If your products are non-consumable products, you will need to create many test accounts.
For Apple, if we do store.purchase() for the item which has been purchased, it will tell you that this item has been purchased and you can get it free for one more time.
It will not return error message as Google platform.
After purchasing, the callback function storeTransaction will enter transaction.state == "purchased" as well.
Anyway, we'd better test with new account which has never bought any items before.

Restore
If users have ever bought the managed products, they may install your App in different devices or re-install your App with the same account.
We need to restore the related authorization by following method:
store.restore( )
After calling it, the callback function storeTransaction will enter transaction.state == "restored".
Apple ask that if your App has non-consumable products inside, then you need to provide the restore button.
That is, we should perform restore after user triggering it.
We cannot restore it automatically.
Why?
Because if we perform restore automatically, it will pop up the window for asking password.
Users may feel strange for showing up such menu automatically.
As we mentioned in last paragraph, if we perform purchase for the purchased products, we can restore the purchased items too. It is free.
Why do we need the restore button? 
Can we just ask user to press the purchase button again?
We do need the restore button.
Users may feel that they need to pay again for pressing purchase button.
The message about getting the products again for free is shown up after you press the purchase button.


For Android Play Store Publish or iOS iTunes Connect, you need to wait for a while to take effect after you set up a new IAP product.
It may take few hours sometimes.






2015年6月12日 星期五

[Corona SDK] 如何偵測物體碰撞 - 利用Global Collision

偵測碰撞可以用Local Collision或Global Collision.
以下是利用Global Collision的方法:
local main = display.newImage( "mainRole.png", 160, 240 )
physics.addBody( main, { density = 1.0, friction = 0.3, bounce = 0.2 } )
main.myName = "mainRole"

local fruit1 = display.newImage( "fruit.png", 100, 120 )
physics.addBody( fruit1, { density = 1.0, friction = 0.3, bounce = 0.2 } )
fruit1.myName = "fruit1"

local fruit2 = display.newImage( "fruit.png", 300, 220 )
physics.addBody( fruit2, { density = 1.0, friction = 0.3, bounce = 0.2 } )
fruit2.myName = "fruit2"

local function onGlobalCollision( event )

    if ( event.phase == "began" ) then        
        if((event.object1.myName=="mainRole" and event.object2.myName=="fruit1") or (event.object1.myName=="fruit1 and event.object2.myName=="mainRole")) then
         print( "began: " .. event.object1.myName .. " and " .. event.object2.myName )         
         if(fruit1 ~= nil) then
          print("Remove fruit1")
          fruit1:removeSelf( )
    fruit1 = nil
         end
        elseif((event.object1.myName=="mainRole" and event.object2.myName=="fruit2") or (event.object1.myName=="fruit2" and event.object2.myName=="mainRole")) then
         print( "began: " .. event.object1.myName .. " and " .. event.object2.myName )         
         if(fruit2 ~= nil) then
          print("Remove fruit2")
          fruit2:removeSelf( )
    fruit2 = nil
         end
        end

    elseif ( event.phase == "ended" ) then
        print( "ended: " .. event.object1.myName .. " and " .. event.object2.myName )
    end
end

Runtime:addEventListener( "collision", onGlobalCollision )
我們替每個物體都加上了專屬的名稱,
在碰撞發生時,就可以利用名稱來分辨是那二個物體,

不過,以上的程式執行時,有時會遇到Runtime error,
執行fruit1:removeSelf( )時,它會告訴你fruit1是nil,
怪了,我們明明有檢查if(fruit1 ~= nil),
fruit2一樣有類似的情形,
從log來看,有時print( "began: " .. event.object1.myName .. " and " .. event.object2.myName )會執行好幾次,然後才執行print("Remove fruit1"),
看來,這個function是multi-entry
multi-entry但又缺少critical section的保護機制,然免會出問題,
要解決這個問題,就是把remove的動作移到其它地方去,如下面用法:
local removeFruit1 = false
local removeFruit2 = false
local main = display.newImage( "mainRole.png", 160, 240 )
physics.addBody( main, { density = 1.0, friction = 0.3, bounce = 0.2 } )
main.myName = "mainRole"

local fruit1 = display.newImage( "fruit.png", 100, 120 )
physics.addBody( fruit1, { density = 1.0, friction = 0.3, bounce = 0.2 } )
fruit1.myName = "fruit1"

local fruit2 = display.newImage( "fruit.png", 300, 220 )
physics.addBody( fruit2, { density = 1.0, friction = 0.3, bounce = 0.2 } )
fruit2.myName = "fruit2"

local function onGlobalCollision( event )

    if ( event.phase == "began" ) then        
        if((event.object1.myName=="mainRole" and event.object2.myName=="fruit1") or (event.object1.myName=="fruit1 and event.object2.myName=="mainRole")) then
         print( "began: " .. event.object1.myName .. " and " .. event.object2.myName )         
         removeFruit1 = true
        elseif((event.object1.myName=="mainRole" and event.object2.myName=="fruit2") or (event.object1.myName=="fruit2" and event.object2.myName=="mainRole")) then
         print( "began: " .. event.object1.myName .. " and " .. event.object2.myName )         
         removeFruit2 = true
        end

    elseif ( event.phase == "ended" ) then
        print( "ended: " .. event.object1.myName .. " and " .. event.object2.myName )
    end
end

Runtime:addEventListener( "collision", onGlobalCollision )

local function removeAction()
 if(removeFruit1) then
   if(fruit1 ~= nil) then
      print("Remove fruit1")
      fruit1:removeSelf( )
      fruit1 = nil
      removeFruit1 = false
   end
 end
 if(removeFruit2) then
   if(fruit2 ~= nil) then
     print("Remove fruit2")
     fruit2:removeSelf( )
     fruit2 = nil
     removeFruit2 = false
   end
 end 
end
 
timer.performWithDelay( 50, removeAction,-1 )

2015年6月11日 星期四

[Corona SDK] 如何加入程式內購In-App Purchases (IAP)?

現在好像越來越流行In-App Purchases的功能,
也就是程式是免費的,
但是如果你想要無廣告,取得所有關卡,增加某些功能,或者購買某些武器等等,
此時你就需要花錢購買,這就是所謂的In-App Purchases (IAP).

底下會同時考慮AndroidiOS系統,
我們先將需要的共同程式部份貼出,然後再分別討論:
local store
local currentProductList = nil

local appleProductList = {
    {"com.gmail.myTest.DemoApp.GetFullVersion"}
}

local googleProductList = {
    "com.gmail.mytest.demo_app.getfullversion",
}
local function storeTransaction( event )
    local transaction = event.transaction

    if ( transaction.state == "purchased" ) then
     print("transaction.state == purchased")
        if(fullVersion == false) then
         fullVersion = true
     end
    elseif ( transaction.state == "restored" ) then
     --iOS only
     print("transaction.state == restored")
        print( "productIdentifier:", transaction.productIdentifier )
        print( "originalReceipt:", transaction.originalReceipt )
        print( "originalTransactionIdentifier:", transaction.originalIdentifier )
        print( "originalDate:", transaction.originalDate )
       if(fullVersion == false) then
         fullVersion = true
     end
    elseif ( transaction.state == "refunded" ) then
     --refunds are only supported by the Android
     print("transaction.state == refunded")        
        print( "productIdentifier:", transaction.productIdentifier )
        if(fullVersion == true) then
         fullVersion = false
     end
    elseif ( transaction.state == "consumed" ) then
     --consumed are only supported by the Android
     print("transaction.state == consumed")        
        print( "productIdentifier:", transaction.productIdentifier )
  if(fullVersion == true) then
   fullVersion = false
  end
  elseif event.transaction.state == "cancelled" then
   --For iOS, if user cancels the IAP action, it will come to here 
   print("transaction.state == cancelled")
  elseif event.transaction.state == "failed" then 
   --[[
   -1003: unknown
   -1005: user canclled,For Android, if user cancels the IAP action, it will come to here
  -7: already owned
  8: item not owned
   ]]
   print("transaction.state == failed")
   print("Transaction failed, type:"..tostring(event.transaction.errorType)..",-1005")
   print("Transaction failed, error:"..tostring(event.transaction.errorString)) 
    end    

    store.finishTransaction( event.transaction )
end


if ( isAndroid ) then
    store = require( "plugin.google.iap.v3" )
    currentProductList = googleProductList
    store.init( "google", storeTransaction )     
else
    store = require( "store" )
    currentProductList = appleProductList
    store.init( "apple", storeTransaction )
end 

--put the following codes somewhere, such as when user press some button 
if(store.canMakePurchases) then
 store.purchase( currentProductList[1] )
end

Android
首先你必須到Play Store Publish內新增你的APP,
成功後,再進入到那個APP項目內新增應用程式內商品,
IAP種類可以分為管理的(managed)未管理的(unmanaged)定閱的(subscription)
管理的,是指一次的,例如從試用版變成完成版之類的,
如果你的IAP是這種種類的,使用者購買後,Google會將購買紀錄存下來,如果使用者換機或者移除程式再重新安裝程式,可以將已購買部份回復。
未管理的,通常是指可以重複購買的,例如武器或主角的血值等等,
Google不會將購買紀錄存下來,
也就是說,如果使用者換了手機或者移除程式再重新安裝程式,
這部份的相關紀錄,需要開發者另外有方法將其紀錄下來,
定閱的,這部份就先不討論。

完成後,把那個項目id填入程式裡的googleProductList內,

Play Store Publish內同一個APP裡,有一個服務和API項目,
點進去,裡面有一個授權金鑰,
它會是一段字串的格式,將它貼到config.lua內的key欄位:
application = 
{
 content = 
 { 
  width = 640,
  height = 960, 
  scale = "zoomStretch",
 },
 license =
    {
        google =
        {
            key = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
        },
    },
}
上面的過程,只要你可以申請成功即可,不用經過審查,<br />

再來,我們還需要在build.settings裡新增plugins和權限:
settings =
{
 plugins =
    {
        ["plugin.google.iap.v3"] =
        {
            publisherId = "com.coronalabs",
            supportedPlatforms = { android=true }
        },        
    },
 android =
    {
        usesPermissions =
        {
            "com.android.vending.BILLING",
        },
    },
}

購買Purchase
要購買時,我們只要呼叫store.purchase( currentProductList[1] )即可,
不過,在實測前,我們需要將build好的apk檔上傳,
因為我們還沒測試好,所以上傳到ALPHA或BETA階段都可以,
重點就是要先上傳,不然實測時會得到"這一版的應用程式未提供Google Play結帳功能"之類的錯誤訊息,
之前是不用先上傳apk就可以測試,但現在規則改了,
上面的範例會先判斷store.canMakePurchasesiOS才有作用,
Android平台的話都會是true,
購買成功之後,在callback function storeTransaction裡會進入transaction.state == "purchased"
我們在裡面再將相關的權限打開即可,

測試Test
因為Google規定不能用自己的帳號來測試,所以你必須再弄個新帳號,
如果你的手機和你開發用的帳號是一樣的,
那就必須回到出廠設定,然後再設成新帳號,

我們可以到Play Store Publish 的設定畫面裡新增測試用Gmail帳戶,
也就是說,用這些帳戶購買你的IAP商品時,都不會被扣錢,
如果你沒有先去設定測試用帳戶,一樣可執行測試,
只是要真的花錢就是了,
當然,花了錢還是可以透過下面會提到的退貨機制來退錢,

另外,一開始測試有問題是,我們會不確定是因為程式有問題,
還是因為我們在Play Store Publish上面的設定有問題,
所以Google建議我們在測試IAP時,最好是先做所謂的"Static Responses"測試,
也就是將上面的googleProductList內項目換成
"android.test.purchased"
"android.test.canceled"
"android.test.refunded"
"android.test.item_unavailable"
等等,
這些項目google內定的,
你不用到Play Store Publish內先設定好,
也不用將apk先上傳,
它們可以測試程式內有關於IAP的處理流程,
程式加入IAP功能時,建議最好先做這樣的測試。

退貨Refund
Google允許使用者可以退貨,使用者可能會在電子錢包執行退貨動作,
原則上,我們只要在callback function storeTransaction裡的transaction.state == "refunded"做一些權限的改變即可

清除購買項目Comsuming items
這是Google特有的,
如果你的項目是管理的(managed),你可能測試之後發現有問題,
如果你修改程式之後想要再測一次,該怎麼處理呢?
因為你已經購買過了,我們可以用comsuming的方式將購買紀錄消除掉,
然後再進行購買試試
方法如下:
store.consumePurchase( currentProductList[1], storeTransaction )
執行上面的動作後,callback function storeTransaction會進入transaction.state == "consumed"表示紀錄已消除,然後你可以再執行store.purchase( currentProductList[1] )
如果你沒有執行清除就直接再購買一次,那會出現錯誤"already owned",
callback function storeTransaction裡會進入transaction.state == "failed"

回復
如果使用者曾經買過管理的(managed)的商品,
使用者可能移除程式再安裝,或者同樣的帳號安裝在不同的機器呢?
我們必須將相關的權限回復,
程式可呼叫:
store.restore( )
呼叫之後,會跑到callback function storeTransactiontransaction.state == "purchased"
Google沒有規定何時該執行回復,
你可以程式執行時自動回復,或者弄個按鈕讓使用者啓動回復,
如果使用者沒有買過,那執行回復不會有任何反應,
如上一段所說,已經買過了,
使用者再買一次時會得到錯誤訊息,而不是直接變成回復,
所以程式裡一定要有執行回復的動作


iOS
你必須先到iTunes Connect內新增你的APP,
成功後,再進入到那個APP項目內新增App內購買項目,
Apple提供5種購買項目種類,
一般我們最常用的大概就是消耗性項目(consumable products),和非消耗性項目(non-consumable products)
消耗性項目,和Google的未管理項目一樣,
非消耗性項目,和Google的管理項目一樣,
請參考前面Android部份的說明,

接著,我們把那個項目id填入程式裡的的appleProductList內,
注意, appleProductList內要用table格式,和Android用的格式不同,
上面的過程,只要你可以申請成功即可,不用經過審查,

購買Purchase
要購買時,我們只要呼叫store.purchase( currentProductList[1] )即可,
上面的範例會先判斷store.canMakePurchases是因為在iOS裝置可以將IAP功能鎖住,
主要是為了怕小孩誤按之類的,
購買成功之後,在callback function storeTransaction裡會進入transaction.state == "purchased"
我們在裡面再將相關的權限打開即可,

測試Test
因為App還未上架,在實機上測試IAP功能時,
我們會得到無法連接iTunes Store的錯誤訊息,
要測試,我們必須先iTunes Connect的"使用者和職能"裡面新增沙箱技術測試人員,
新增的帳號不能是現在已經存在的,什麼意思呢?
也就是說,你不能把你手機或平板的帳號設成測試人員帳號,
我們必須新增一個Apple帳號,這點和前面Android的設定測試人員不同,
因為這個新增帳號我們只拿來測試,所以資料可以亂寫,
一個簡單方法就是用你既有的帳號加上test1,test2之類的,
例如,你手機的帳號是"ABC",那就新增帳號"ABC+test1", "ABC+test2"...

你會需要新增很多測試帳號,
因為如果你的IAP商品是非消耗性項目(non-consumable products)
只要買過一次,系統就會紀錄,
Apple沒有像Google有消除購買項目的功能,
當然你再執行購買時,它會告訴你已經買過,可以再免費取得一次,
成功後,在callback function storeTransaction裡也是會進入transaction.state == "purchased"
它並不會像Android平台會進入transaction.state == "failed"
不過,如果你要模擬新使用者的狀態,那還是再新增一個測試帳

回復
如果使用者曾經買過非消耗性項目(non-consumable products)的商品,
使用者可能移除程式再安裝,或者同樣的帳號安裝在不同的機器呢?
我們必須將相關的權限回復,
程式可呼叫:
store.restore( )
呼叫之後,會跑到callback function storeTransactiontransaction.state == "restored"
Apple規定如果你的App有非消耗性項目(non-consumable products)的IAP功能,
那就必須提供回復的按鈕,
等使用者按了之後,再執行回復的動作,
為什麼程式不能自動幫它回復呢?
因為你程式執行回復的動作,會跳出要使用者輸入帳號密碼的畫面,
如果你的程式是自動回復,突然跳出輸入密碼的畫面會讓使用者會覺得莫名其妙,
前面提過,已經購買過了,再購買也不用花錢,也不會像Google會得到錯誤訊息,
那就請使用者再按一次購買就好了啊,為什麼要再弄個回復的按鈕,
因為使用者已經購買過了,按購買的按鈕會讓使用者覺得需要再花一次錢,
不用錢的提示訊息是程式真的執行購買動作後才會跳出,



不管Android的Play Store Publish還是iOS的iTunes Connect
設定新的IAP商品後,都需要一段時間才會生效,
有時可能要等幾個小時,
沒有任何地方告訴你已經可以開始測試,
如果你一直測試有問題,又找不出原因,那就隔天再試,
如果是Android,如前面提的,你可以先做"Static Responses"看看





[Corona SDK] How to add advertisement in my APP - using AdMob V2

We may want to integrate advertising bar or page in our APP.
Let's take AdMob as our example.

First, you need to visit AdMob to apply for an account.
After that, you can add the APP which you want to has Ads.
I will skip the detailed steps for that.
Basically, there have two types of Ads for AdMob.
One is called banner, and the other one is interstitial.
If your APP will be for Android and iOS platforms both, you need to apply for the Ads Unit ID separately.
That is, you will get 4 Ads Unit IDs.
Let's check the coding part.

Add plugin
First, we need to add plugin in build.settings:
settings =
{
 plugins =
    {
        ["plugin.google.play.services"] =
        {
            publisherId = "com.coronalabs"
        },        
    },
 android =
    {

        usesPermissions =
        {
            "android.permission.INTERNET",
            "android.permission.ACCESS_NETWORK_STATE",
        },
    },
}
As you can see, we also need to add access right for Android system.

Show the Ads
To show the Ads, we just need to program as follow:
local ads = require( "ads" )
local bannerAppID = "ca-app-pub-3001435268155896/xxxxxxxxxx"  --for your iOS banner
local interstitialAppID = "ca-app-pub-3001435268155896/xxxxxxxxxx"  --for your iOS interstitial
if ( system.getInfo("platformName") == "Android" ) then
    bannerAppID = "ca-app-pub-3001435268155896/xxxxxxxxxx"  --for your Android banner
    interstitialAppID = "ca-app-pub-3001435268155896/xxxxxxxxxx"  --for your Android interstitial
end

local adProvider = "admob"

local function adListener( event )
    local msg = event.response
    print( "Message from the ads library: ", msg )
 if (event.type == "banner") then
     if ( event.isError ) then
         print( "Error, no bannser ad received", msg )
     else
         print( "Got one banner AD" )
     end
 elseif(event.type == "interstitial") then
        if ( event.isError ) then
            print( "Error, no interstitial ad received", msg )
        elseif ( event.phase == "loaded" ) then
            print("interstitial ads got...")
        elseif ( event.phase == "shown" ) then
            print("interstitial has been shown and closed") 
        end
 end
end

ads.init( adProvider, bannerAppID, adListener )

ads.show( "banner", { x=0, y=0, appId=bannerAppID} )--show banner ads

ads.show( "interstitial", { x=0, y=0, appId=interstitialAppID} )--show banner ads

ads.hide() -- hide the ads
The bannerAppID and interstitialAppID are the Unit ID that  you get from AdMob websit.

We only need to call ads.init() one time.
You can pass in any type of appId.
In ads.show(), we pass in the Ads type and related appId then.

Banner Ads
ads.show( "banner", { x=0, y=0, appId=bannerAppID} ) can show the banner Ads.
The parameters {x=0, y=0} mean than we what to locate the banner in the top side.
You can decide whether to show it, as long as it will not affect the process of game.
If you want to have Ads, you'd better consider the best location for it when you are doing UI layout.

In general, we will put it in the top side or bottom side.
The reserved width should be screen-wide.
However, what about the height?
There is so called Smart Banners for AdMob.
When it is showing in phones, its height will be 32 for Landscape mode, and will be 50 for Portrait mode.
When it is showing in tablets, its height will be 90.
However, I did get banner with height in one of my phones.
Just in case, we can reserve 90 or even 100 height if possible.
It will be suitable for all cased.

Interstitial Ads 
For Interstitial Ads, it will occupy the whole screen.
It doesn't matter for what values of parameters x and y you pass in.
You should not show Interstitial Ads too frequent.
Otherwise, the user will feel that it is annoying.

The loading of Interstitial Ads may take a while.
We'd better load it in advance before showing it, as shown below:
ads.load( "interstitial", { appId=interstitialAppID } ) --load the "interstitial" ads in advance
Once it has been loaded, it will show up immediately when we call ads.show()
Before showing, we need to check if it has been loaded.
if(ads.isLoaded("interstitial"))then
 ads.show( "interstitial", { x=0, y=0, appId=interstitialAppID } )
end
Please note that once we call ads.show(), even we call ads.show( "banner", { x=0, y=0, appId=bannerAppID} ) to show the banner ads, ads.isLoaded("interstitial") will be false.
That is, the pre-loaded data will be cleared.
We need to call ads.load( "interstitial", { appId=interstitialAppID } ), ads.isLoaded("interstitial") again after we calling ads.show().
However, for Android system, once we have ever called ads.load( "interstitial", { appId=interstitialAppID } ), it will reload the data automatically.
That is, ads.isLoaded("interstitial") will be true later automatically after it has got the Interstitial Ads.
For iOS system, it will not reload it automatically. Not sure the reason.

The Ads function can only work on real devices.
We'd better add some judgement for above codes.
That is, adding system.getInfo( "environment" ) ~= "simulator" in our codes.

[Corona SDK] 如何加入廣告 - AdMob V2

很多人開發的APP都是免費的,
為了獲利,難免都會加作廣告,
我們就以很多人常用的AdMob來當範例。

首先,您必須到AdMob去申請一個帳號,
然後新增你要加入廣告的APP,
細節這邊就先不提了,
原則上,AdMob有二個樣式,
一個是橫幅(banner),一個是插頁(interstitial),
如果你是同時要支援AndroidiOS平台,
那各別都要申請,
也就是說,你總共會需要4個廣告編號。

接著我們來看程式部份:

Add plugin
首先,我們需要在build.settings加入plug-in:
settings =
{
 plugins =
    {
        ["plugin.google.play.services"] =
        {
            publisherId = "com.coronalabs"
        },        
    },
 android =
    {

        usesPermissions =
        {
            "android.permission.INTERNET",
            "android.permission.ACCESS_NETWORK_STATE",
        },
    },
}
另外,Android需要增加網路存取的權限

Show the Ads
要顯示廣告,只要按照以下寫法即可
local ads = require( "ads" )
local bannerAppID = "ca-app-pub-3001435268155896/xxxxxxxxxx"  --for your iOS banner
local interstitialAppID = "ca-app-pub-3001435268155896/xxxxxxxxxx"  --for your iOS interstitial
if ( system.getInfo("platformName") == "Android" ) then
    bannerAppID = "ca-app-pub-3001435268155896/xxxxxxxxxx"  --for your Android banner
    interstitialAppID = "ca-app-pub-3001435268155896/xxxxxxxxxx"  --for your Android interstitial
end

local adProvider = "admob"

local function adListener( event )
    local msg = event.response
    print( "Message from the ads library: ", msg )
 if (event.type == "banner") then
     if ( event.isError ) then
         print( "Error, no bannser ad received", msg )
     else
         print( "Got one banner AD" )
     end
 elseif(event.type == "interstitial") then
        if ( event.isError ) then
            print( "Error, no interstitial ad received", msg )
        elseif ( event.phase == "loaded" ) then
            print("interstitial ads got...")
        elseif ( event.phase == "shown" ) then
            print("interstitial has been shown and closed") 
        end
 end
end

ads.init( adProvider, bannerAppID, adListener )

ads.show( "banner", { x=0, y=0, appId=bannerAppID} )--show banner ads

ads.show( "interstitial", { x=0, y=0, appId=interstitialAppID} )--show banner ads

ads.hide() -- hide the ads
上面的bannerAppIDinterstitialAppID,就是你去AdMob申請得到的廣告unit id,

ads.init()只要做一次即可,
之後在ads.show()裡再帶入想要顯示的ads型態和對應的appId即可,

橫幅廣告Banner Ads
ads.show( "banner", { x=0, y=0, appId=bannerAppID} )可以顯示橫幅的廣告,
上面的參數裡,x=0, y=0表示要顯示在最上方,位置你可以自己決定,
所以程式的UI配置要先決定, 以不影響遊戲進行就好,
大部份不是最上方就最下方,
然後預留給它的寬度是整個螢幕寛度,
那高度呢?要預留多高才行?
AdMob有所謂的Smart Banners
手機上面, 在landscape模式時,高度是32,
在portrait模式時,高度是50,
平板上面,高度就是90,
不過,我有遇過手機上得到的banner高度是70左右,
為了保險起見,預留90或者100就能適合各種情形 

插頁廣告Interstitial Ads 
插頁廣告會佔用全螢幕,所以ads.show()裡傳入的x,y值就沒有差別,
插頁廣告最好不要太多,不然使用者可能會覺得很煩,
另外,插頁廣告下載需要一段時間,
所以我們最好是先預載到手機或平板上,如下面的方法:
ads.load( "interstitial", { appId=interstitialAppID } ) --load the "interstitial" ads in advance
這個動作可以程式一啓動就先做,
要顯示廣告時,先判斷是否已下載回來了:
if(ads.isLoaded("interstitial"))then
 ads.show( "interstitial", { x=0, y=0, appId=interstitialAppID } )
end
要注意的是,如果你呼叫過ads.show()
即使是ads.show( "banner", { x=0, y=0, appId=bannerAppID} )
ads.isLoaded("interstitial")都會變成false
在Android平台上,只要曾經呼叫過ads.load( "interstitial", { appId=interstitialAppID } ),
之後呼叫ads.show()後,雖然ads.isLoaded("interstitial")會被清為false
但是它會自已在reload,
可是在iOS平台,每次都必須呼叫ads.load( "interstitial", { appId=interstitialAppID } ), ads.isLoaded("interstitial")才會再度為true,

Ads只有在實機上才有用,
所以,上面的程式,最好是可以加上判斷,
也就是說system.getInfo( "environment" ) ~= "simulator"時才執行

2015年6月3日 星期三

[Corona SDK] How to localize your APP title - Android part

In [Corona SDK] How to localize your APP title - iOS part, we have learn how to localize the APP title for iOS system.
For Android APP, such function is available in Enterprise version.
However, I think most people are using free version.
So..how should we do?

The Android apk file is just a compressed format.
First, we need to prepare two tools, one is apktool, the other is zipalign.
These two tools should be found in Android SDK.
If you don't have, please search them on internet.

We can put the apk file and related tools in the same folder.

myAndroidApp.apk is the target apk we want to modify.
For convenience, I create some bat files.
Let's look at their contents.
(The following actions are done in Windows environment)

apktool.bat
if "%PATH_BASE%" == "" set PATH_BASE=%PATH%
set PATH=%CD%;%PATH_BASE%;
java -jar -Duser.language=en "%~dp0\apktool.jar" %1 %2 %3 %4 %5 %6 %7 %8 %9
unpack.bat
apktool d %1
pack.bat
apktool b %1 -o %2
sign.bat
jarsigner -verbose -sigalg MD5withRSA -digestalg SHA1 -keystore "my.keystore" %1 yourPassword
zipalign -v 4 %1 zip-%1 
del %1 
ren zip-%1 %1
Remember to change yourPassword as yours in sign.bat

1. In command shell, input unpack "myAndroidApp.apk"
It will unpack myAndroidApp.apk to the folder myAndroidApp
2. Enter the folder myAndroidApp\res\values, if the file strings.xml is there, open it.
If not, create a new one. 
Modify its content as:
<resources>
    <string name="appTitle">Your Title</string>
</resources>
3. If we want to add other languages, say French.
We need to create a new folder values-fr under myAndroidApp\res.
Again, create strings.xml under folder values-fr.
The content of  strings.xml is the same as what we do in step, except for that Your Title should be translated into French or any other titles you want.
4. For supporting a new language, we just repeat step 3, except for that the folder names are different.
For example, values-ja is for Japanese, while values-it is for Italian.
5. Return to the folder myAndroidApp, open file AndroidManifest.xml.
Search android:label="xxx" and change it to be android:label="@string/appTitle".
There should have two places. Both of them need to be modified.
Someone said that FileContentProvider should be modified as well.
However, if you do it, you will get run time error when you start the  APP.
6. In command shell, input  pack myAndroidApp  newApp.apk
7. In command shell, input  sign newApp.apk

Done...




[Corona SDK] 如何讓您你的APP名稱也支援多國語言 - Android篇

[Corona SDK] 如何讓您你的APP名稱也支援多國語言 - iOS篇裡我們提到如何將iOS APP的名稱顯示成不同的語言,
Android APP的話,要Enterprise版本的Corona SDK才能支援,
不過,我想大部份人可能都只是使用免費版,
那要如何解決呢?

android的apk檔,基本上只是一個壓縮檔,
所以,我們可以稍微動一下手腳,

首先,你要準備二個工具,
一個是apktool,一個是zipalign,
這二個工具在Android SDK裡會有,如果你手上沒有,就自己上網找一下,

我們將build出來的apk檔和所需要的工具都放在同一個目錄以方便操作,
myAndroidApp.apk是要修改的原始apk檔,
為了方便,所以我又造出了幾個bat檔,
我們先來看看各個bat檔的內容
(我是在Windows作業系統操作的)
apktool.bat
if "%PATH_BASE%" == "" set PATH_BASE=%PATH%
set PATH=%CD%;%PATH_BASE%;
java -jar -Duser.language=en "%~dp0\apktool.jar" %1 %2 %3 %4 %5 %6 %7 %8 %9
unpack.bat
apktool d %1
pack.bat
apktool b %1 -o %2
sign.bat
jarsigner -verbose -sigalg MD5withRSA -digestalg SHA1 -keystore "my.keystore" %1 yourPassword
zipalign -v 4 %1 zip-%1 
del %1 
ren zip-%1 %1
sign.bat裡的yourPassword記得改成你自己的

1. 在命令視窗裡輸入 unpack "myAndroidApp.apk"
它會將myAndroidApp.apk解壓縮到目錄myAndroidApp
2. 進入myAndroidApp\res\values,如果裡面已有strings.xml,打開它來編輯,
如果沒有,新增一個,將其內容修改如下:
<resources>
    <string name="appTitle">Your Title</string>
</resources>
3. 如果想要增加其它語言,例如繁體中文,
那就在myAndroidApp\res下面新增一個目錄values-zh-rTW
在目錄values-zh-rTW裡面同樣新增strings.xml
內容和上面的一樣,只不過Your Title要改成中文的名稱
4. 要增加幾個語言,步驟3就重複做幾次,只是新增的目錄不同,
例如日文是values-ja,義大利文是values-it等等
5. 回到目錄myAndroidApp下面,打開AndroidManifest.xml
尋找android:label="xxx",將它改成android:label="@string/appTitle",
應該會有二個地方,全部都要改,
另外,國外網站有人說FileContentProvider也要修改,那是不對的,
如果你修改它,程式執行時會得到錯誤訊息
6. 在命令視窗裡輸入 pack myAndroidApp  newApp.apk
7. 在命令視窗裡輸入 sign newApp.apk
完成



2015年6月2日 星期二

[Corona SDK] How to localize your APP title - iOS part

In [Corona SDK] How to support multi-language (localize your APP),
we know how to support multi-languages.
However, the method mentioned above only worked when the APP has been started.
How about the APP title?
Can we solve this problem before the APP start?

Let's check how should we do for iOS first....
Assume that we want to support English and Spanish.
We need to modify build.settings as follow:
iphone =
{
 plist =
 {
     CFBundleLocalizations={
            "en",
            "es",
      },
      CFBundleDisplayName = "myAppTitle",
      CFBundleName = "myAppTitle"",
    }
 }
After that, we need to add some folders for those languages we want to support.
For example, we will add folder "en.lproj" and "es.lproj" for our case.
In the folders we just added, we all need to add  one file "InfoPlist.strings",
In  "InfoPlist.strings", we need to add:
"CFBundleDisplayName"="myAppTitle";
"CFBundleName"="myAppTitle";
The above example is for "en.lproj".
Other languages are similar.

[Corona SDK] 如何讓您你的APP名稱也支援多國語言 - iOS篇

[Corona SDK] 如何支援多國語言裡,
我們知道如何支援多國語言,
不過,裡面的方法是APP啓動之後,
那APP的名稱呢?
在APP啓動之前,我們如何解決這個問題呢?

我們先來看看iOS系統要如何處理....
假設我們要支援英文,繁體中文和簡體中文,
build.settings裡我們先修改如下:
iphone =
{
 plist =
 {
     CFBundleLocalizations={
            "en",
            "zh-Hans",
            "zh-Hant",
      },
      CFBundleDisplayName = "myAppTitle",
      CFBundleName = "myAppTitle"",
    }
 }
然後,我們再根據想要支援的語言新增幾個目錄,
如以上範例,我們新增了"en.lproj","zh-Hant.lproj","zh-Hans.lproj"

如果要增加其它語言,方法類似,
在剛剛增加的目錄裡,我們都新增了一個檔案"InfoPlist.strings"
"InfoPlist.strings"裡的內容如下:
"CFBundleDisplayName"="myAppTitle";
"CFBundleName"="myAppTitle";
上面是"en.lproj"裡的,
如果是"zh-Hant.lproj"裡的"InfoPlist.strings",會是:
"CFBundleDisplayName"="我的名稱";
"CFBundleName"="我的名稱";
其它語言的方法類似

[Corona SDK] How to support multi-language (localize your APP)

If we only want to support one language, it will be quite simple for programming.
We can code it  as:
display.newText("hello", 200, 200, native.systemFontBold, 36 )

But, what if we want to support more than two languages?

First, we need to get the information of languages for current settings in user phones.
For Android system, we may use system.getPreference( "locale", "language" ) to get such information.
For iOS, we may use system.getPreference( "ui", "language" ).
However, it will be complicated for Chinese.
There have so-called Simplified Chinese and Traditional Chinese.
To overcome such problem, you can reference the following codes:
language = "en"
isAndroid = false
if(system.getInfo("platformName") == "Android") then
 isAndroid = true
end
if(isAndroid) then
 language = system.getPreference( "locale", "language" )
else
 language = system.getPreference( "ui", "language" )
end

local localeLanguage = system.getPreference( "locale", "language" ):upper()
local localeCountry = system.getPreference( "locale", "country" ):upper()
local uiLanguage = system.getPreference( "ui", "language" ):upper()

if(localeLanguage == "ZH-HANT" or localeLanguage == "ZH_HANT" or uiLanguage == "ZH-HANT" or uiLanguage == "ZH_HANT") then
 language = "zh-Hant"
elseif(localeLanguage == "ZH-HANS" or localeLanguage == "ZH_HANS" or uiLanguage == "ZH-HANS" or uiLanguage == "ZH_HANS") then
 language = "zh-Hans"
elseif(localeLanguage == "中文" or localeLanguage == "ZH" or uiLanguage == "中文" or uiLanguage == "ZH") then
 if(localeCountry == "TW") then
  language = "zh-Hant"
 else
  language = "zh-Hans"
 end
end
After getting the language setting, we need to take care of display problem.
We can add a new multi-language translation file, such as translations.lua as show below:
local translations =
{
    ["hello"] =
    {
        ["en"] = "hello",        
        ["zh-Hant"] = "哈囉",
        ["zh-Hans"] = "哈啰",
        ["fr"] = "Salut",
        ["de"] = "Hallo",

    },      
}

return translations
When we want to display the text, we can code it as:
translations = require("translations")
display.newText( translations["hello"][language], 200, 200, native.systemFontBold, 36 )

[Corona SDK] 如何支援多國語言

如果我們只要支援一種語言,
那程式就很簡單,例如:
display.newText("哈囉", 200, 200, native.systemFontBold, 36 )

但,如果想要支援二種以上語言呢?

首先,我們要知道目前使用者手機設定的語言是什麼,
英文,法文,日文等等還比較容易,
但中文有繁體和簡體,加上手機又有Android和iOS二大系統(Window Phone就先跳過吧^^),
Android系統可能要用system.getPreference( "locale", "language" )比較適合,
iOS可能用system.getPreference( "ui", "language" )
將各種可能情形總結,可參考以下的範例:
language = "en"
isAndroid = false
if(system.getInfo("platformName") == "Android") then
 isAndroid = true
end
if(isAndroid) then
 language = system.getPreference( "locale", "language" )
else
 language = system.getPreference( "ui", "language" )
end

local localeLanguage = system.getPreference( "locale", "language" ):upper()
local localeCountry = system.getPreference( "locale", "country" ):upper()
local uiLanguage = system.getPreference( "ui", "language" ):upper()

if(localeLanguage == "ZH-HANT" or localeLanguage == "ZH_HANT" or uiLanguage == "ZH-HANT" or uiLanguage == "ZH_HANT") then
 language = "zh-Hant"
elseif(localeLanguage == "ZH-HANS" or localeLanguage == "ZH_HANS" or uiLanguage == "ZH-HANS" or uiLanguage == "ZH_HANS") then
 language = "zh-Hans"
elseif(localeLanguage == "中文" or localeLanguage == "ZH" or uiLanguage == "中文" or uiLanguage == "ZH") then
 if(localeCountry == "TW") then
  language = "zh-Hant"
 else
  language = "zh-Hans"
 end
end
取得語言設定後,再來就是顯示的問題,
我們可以新增一個多國語言的翻譯字串檔案,
例如translations.lua,裡面的內容如下:
local translations =
{
    ["hello"] =
    {
        ["en"] = "hello",        
        ["zh-Hant"] = "哈囉",
        ["zh-Hans"] = "哈啰",
        ["fr"] = "Salut",
        ["de"] = "Hallo",

    },      
}

return translations
顯示字串時,我們可以改寫如下:
translations = require("translations")
display.newText( translations["hello"][language], 200, 200, native.systemFontBold, 36 )