顯示具有 Corona SDK 標籤的文章。 顯示所有文章
顯示具有 Corona SDK 標籤的文章。 顯示所有文章

2015年11月17日 星期二

[Corona SDK] 如何支援多個廣告系統 - AdMob, iAds, Vungle

在如何加入廣告 - AdMob V2裡,
我們知道要如何加入AdMob Ads,
要加入iAds和Vungle的話,作法很類似,
在這裡我們討論如果要同時支援的話該如何處理,
也就是說,我們可以動態選擇 AdMob, iAds, Vungle,
不管是因為Fill Rate, eCPM或者其它原因...

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

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

Init the Ads
local adsProvider = {"adMob1","adMob2","vungle","iAds"} 
local ads = require( "ads" )
local adMobAdProvider = "admob"
local iAdsAdProvider = "iads"
local vungleAdProvider = "vungle"
local vungleAppId = "55e4xxxxxx"
local bannerAppID = "ca-app-pub-xxxxxx"
local interstitialAppID = "ca-app-pub-xxxxxx"
local appID = "com.gmail.MyCompany.App"
if ( isAndroid ) then
    bannerAppID = "ca-app-pub-xxxxx"
    interstitialAppID = "ca-app-xxxxxx"
    vungleAppId = "com.gmail.My_Company.App"
end
local lastAdsTypeShown = "none"
local currentAds = 1

local function adMobListener( event )
    local msg = event.response
    -- Quick debug message regarding the response from the library
    print( "Message from the adMob library: ", msg )
 if (event.type == "banner") then
     if ( event.isError ) then
         changeToNextAds()
     else
     end
 elseif(event.type == "interstitial") then
        if ( event.isError ) then
            changeToNextAds()
        elseif ( event.phase == "loaded" ) then
        elseif ( event.phase == "shown" ) then
        end
 end  
end

local function iAdsListener( event )
 local msg = event.response
 print("Message received from the iAds library: ", msg)
 if event.isError then
  changeToNextAds() 
 else
 end  
end

local function vungleListener( event )
   print("Message received from the vungleListener library: ", event.type)

   if ( event.type == "adStart" and event.isError ) then
     changeToNextAds()
   elseif ( event.type == "adEnd" ) then
      -- Ad was successfully shown and ended; hide the overlay so the app can resume.
   else
      print( "Received event", event.type )
   end
   return true
end

function adsInit()
 ads.init( adMobAdProvider, bannerAppID, adMobListener )
 if(isAndroid == false) then
   ads.init( iAdsAdProvider, appID, iAdsListener )    
 end
 ads.init( vungleAdProvider, vungleAppId, vungleListener )
end
我們需要針對AdMob,iAds和Vungle各自呼叫ads.init(),
因為callback回傳的參數不同,所以各自傳入不同callback function,
上面的程式裡,我們在callback function裡發現有錯誤時就換下一個Ads,
至於changeToNextAds()是什麼,看下一段

Change Ads Network
local function changeToNextAds()
 currentAds = currentAds + 1
 if(currentAds > 3) then
  currentAds = 1
 end
 if(adsProvider[currentAds] == "iAds") then
  ads:setCurrentProvider(iAdsAdProvider)
 elseif(adsProvider[currentAds] == "vungle") then
  ads:setCurrentProvider(vungleAdProvider)
 else
  ads:setCurrentProvider(adMobAdProvider)
 end
end
我們透過呼叫ads:setCurrentProvider()來設定新的Ads Network,

Show Ads
function showAds(type,px,py)
 if(adsProvider[currentAds] == "adMob") then
  if(type == "banner") then
   ads.show( type, { x=px, y=py, appId=bannerAppID} )
   return true
  elseif(type == "interstitial") then    
   if(ads.isLoaded(type))then
    ads.show( type, { x=px, y=py, appId=interstitialAppID } )
    return true
   else
    return false
   end 
  end 
 elseif(adsProvider[currentAds] == "vungle") then
  if (type == "interstitial" and ads.isAdAvailable() ) then
      ads.show( "interstitial" )
      return true
  else
   return false
  end   
 elseif(adsProvider[currentAds] == "iAds") then
  ads.show( type, { x=px, y=py} )
  return true
 end
 return false  
end

function loadAds()
 if(adsProvider[currentAds] == "adMob") then
  ads.load( "interstitial", { appId=interstitialAppID} )
 end
end 
Vungle沒有Banner Ads, 它自己會pre-load interstitial Ads,
iAds無法pre-load interstitial Ads,
上面的說明應該很完整,
基本上只要照抄就可以了

2015年11月16日 星期一

[Corona SDK] 如何加入Apple Game Center 和 Android Google Play Game Service

不管是Apple的Game Center,還是Android的Google Play Game Service,
基本上都是利用gameNetwork的功能即可,
實作不難,考慮同時支援Apple和Google,
先將程式碼貼上再來講解:
local gameNetwork = require( "gameNetwork" )
local playerName
local playerId

local leaderboardsListId=
{
 ["HighestScore"]=
 {
  ["Description"] = "Highest Score",
  ["Identifier"] = "com.gmail.mycom.demo.Leaderboards.HighestScore"
 }
}

local achievementsListId = 
{
 ["FinishLevel1"]=
 {
  ["Description"] = "Finish Level 1",
  ["Identifier"] = "com.gmail.mycom.demo.Achievements.FinishLevel1"
 },
 ["FinishLevel2"]=
 {
  ["Description"] = "Finish Level 2",
  ["Identifier"] = "com.gmail.mycom.demo.Achievements.FinishLevel2"
 }
}

if(isAndroid) then
 leaderboardsListId=
 {
  ["HighestScore"]=
  {
   ["Description"] = "Highest Score",
   ["Identifier"] = "Cgkxxxxxxxx"
  }
 }

 achievementsListId = 
 {
  ["FinishLevel1"]=
  {
   ["Description"] = "Finish Level 1",
   ["Identifier"] = "Cgkxxxxxxx"
  },
  ["FinishLevel2"]=
  {
   ["Description"] = "Finish Level 2",
   ["Identifier"] = "Cgkxxxxxxxx"
  }
 }
end



function showLeaderboards( event )
 print("showLeaderboards()")
 if (playerId ~= nil) then
    if ( isAndroid ) then
       gameNetwork.show( "leaderboards" )
    else
       gameNetwork.show( "leaderboards", { leaderboard = {timeScope="AllTime"} } )
    end
 else
  gameNetworkSetup()
 end
    return true
end

local function postScoreSubmit( event )
   --whatever code you need following a score submission...
   return true
end

local function updateLeaderBoards(type, myScore)
 if (playerId == nil) then
  return
 end

 gameNetwork.request( "setHighScore",
 {
    localPlayerScore = { category=leaderboardsListId[type]["Identifier"], value=tonumber(myScore) },
    listener = postScoreSubmit
 } )
 print("gameNetwork.request( setHighScore) finished")
end

function showAchievements( event )
 print("showAchievements()")
 if (playerId ~= nil) then
    gameNetwork.show( "achievements" )
 else
  gameNetworkSetup()
 end
    return
end

local function achievementRequestCallback( event )
   
   return true
end

local function updateAchievement(type)
 if (playerId == nil) then
  return
 end
 gameNetwork.request( "unlockAchievement",
 {
    achievement = { identifier=achievementsListId[type]["Identifier"], percentComplete=100, showsCompletionBanner=true },
    listener = achievementRequestCallback
 } )
end

local function loadScoresCallback( event ) 
 if(event.data == nil) then
  print("event.data is nil")
  return
 end
 for i=1,25 do
  if(event.data[i] == nil) then
   break
  end
  print("event.data[",i,"].playerID:",event.data[i].playerID)
  print("event.data[",i,"].category:",event.data[i].category)
  if(event.data[i].playerID == playerId) then
   print("matched playID...........")
   break
  end
 end
end

local function loadLocalPlayerCallback( event ) 
 if(event.data == nil) then
  print("event.data is nil")
  return
 end

 playerId = event.data.playerID
 playerName = event.data.alias
 print("loadLocalPlayerCallback(),playerName:",playerName,",playerId:",playerId)
 if(playerId == nil) then
  return
 end
 myGameSettings.gameCerterHasEverLoggedIn = true
 saveData()   

 gameNetwork.request( "loadScores",
            {
                leaderboard =
                {
                    category = leaderboardsListId["HighestScore"]["Identifier"], 
                    playerScope = "Global",   -- Global, FriendsOnly
                    timeScope = "AllTime",    -- AllTime, Week, Today
                    --range = {1,5},
                    playerCentered = true,
                },
                listener = loadScoresCallback
            }) 
end

local function loadAchievementCallback( event )    
 if(event.data == nil) then
  print("event.data is nil")
  return
 end

 for i=1,#event.data do
  if(event.data[i] ~= nil) then
   print("event.data[",i,"].identifier:",event.data[i].identifier)
   print("event.data[",i,"].title:",event.data[i].title)
   print("event.data[",i,"].isCompleted:",event.data[i].isCompleted)
  end
 end 
end

local function gameNetworkLoginCallback( event )
 if(isAndroid==false) then
  if(event.data) then
   print("User has logged into iOS Game Center")
  else
   print("User has NOT logged into iOS Game Center")
   return 
  end
 end
 gameNetwork.request( "loadLocalPlayer", { listener=loadLocalPlayerCallback } )
 gameNetwork.request( "loadAchievements", { listener=loadAchievementCallback } )
 return true
end

local function gpgsInitCallback( event )
 if(event.data) then
  print("User has logged into Google App Play Center")
     gameNetwork.request( "login", { userInitiated=true, listener=gameNetworkLoginCallback } )
 else
  print("User has NOT logged into Google App Play Center")
 end
end

function gameNetworkSetup()
 if ( isAndroid ) then
  myGameSettings.gameCerterHasEverLoggedIn = false
  gameNetwork.init( "google", gpgsInitCallback )
 else
  gameNetwork.init( "gamecenter", gameNetworkLoginCallback )
 end
end
}
初始化
一般來說,我們可以在main.lua裡呼叫gameNetworkSetup(),
對於Android系統,在gpgsInitCallback()裡可以確認是否登錄成功
對於iOS系統,在gameNetworkLoginCallback()裡可以確認是否登錄成功,
至於後面的gameNetwork.request( "loadLocalPlayer", { listener=loadLocalPlayerCallback } ),
是用來確認目前登錄遊戲中心的使用者,有沒有呼叫它都可以,
gameNetwork.request( "loadAchievements", { listener=loadAchievementCallback } )則是用來回復使用者的紀錄, 例如使用曾經安裝過這個APP,移除它後又重新下載, 如果不想幫使用者回復紀錄,不執行也可以

顯示排行榜(leaderboard)
想要顯示排行榜,只要呼叫showLeaderboards()即可, 通常這個動作是在使用者按下某個按鈕時

顯示任務(achievement)
想要顯示排行榜,只要呼叫showAchievements()即可, 通常這個動作是在使用者按下某個按鈕時

更新排行榜 想要更新排行榜,只要呼叫updateLeaderBoards()即可, 通常這個動作是在使用者完成遊戲時

更新任務 想要更新排行榜,只要呼叫updateAchievement()即可, 通常這個動作是在使用者完成某個關卡時


以下是Apple的Game Center,和Android的Google Play Game Service稍有不同的地方
Apple Game Center
leaderboardsListId{}和achievementsListId{}裡的資料, 例如"com.gmail.mycom.demo.Leaderboards.HighestScore", 這是使用者自己決定的編碼,然後在iTunes Connect裡設定即可, 不重覆就行

Google Play Game Service
leaderboardsListId{}和achievementsListId{}裡的資料,
例如"Cgkxxxxxxxx", 這是使用者在play store publish的遊戲中心設定後系統給的編碼, 另外,對於Google Play Game Service, 我們還需要在build.settings增加:
android =
    {
        usesPermissions =
        {
            "android.permission.INTERNET",
            "android.permission.ACCESS_NETWORK_STATE",
            "com.android.vending.BILLING",
        },
        googlePlayGamesAppId = "123456789012" 
    },
usesPermissions無所謂,
googlePlayGamesAppId是在play store publish的遊戲中心連結應用程式後系統給的編碼

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).

底下會同時考慮Android和iOS系統,
我們先將需要的共同程式部份貼出,然後再分別討論:
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.canMakePurchases是iOS才有作用,
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 storeTransaction的transaction.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 storeTransaction的transaction.state == "restored"
Apple規定如果你的App有非消耗性項目(non-consumable products)的IAP功能,
那就必須提供回復的按鈕,
等使用者按了之後,再執行回復的動作,
為什麼程式不能自動幫它回復呢?
因為你程式執行回復的動作,會跳出要使用者輸入帳號密碼的畫面,
如果你的程式是自動回復,突然跳出輸入密碼的畫面會讓使用者會覺得莫名其妙,
前面提過,已經購買過了,再購買也不用花錢,也不會像Google會得到錯誤訊息,
那就請使用者再按一次購買就好了啊,為什麼要再弄個回復的按鈕,
因為使用者已經購買過了,按購買的按鈕會讓使用者覺得需要再花一次錢,
不用錢的提示訊息是程式真的執行購買動作後才會跳出,



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





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

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

首先,您必須到AdMob去申請一個帳號,
然後新增你要加入廣告的APP,
細節這邊就先不提了,
原則上,AdMob有二個樣式,
一個是橫幅(banner),一個是插頁(interstitial),
如果你是同時要支援Android和iOS平台,
那各別都要申請,
也就是說,你總共會需要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
上面的bannerAppID和interstitialAppID,就是你去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] 如何讓您你的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] 如何讓您你的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] 如何支援多國語言

如果我們只要支援一種語言,
那程式就很簡單,例如:
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 )

2015年5月12日 星期二

[Corona SDK] 如何儲存和取出遊戲的設定資料?

要儲存資料,可以用file:write()寫到指定的檔案,
之後再用file:read()將它們讀出來,
不過,一般遊戲設定的資料通常都是以table方式來紀錄,
而file:write()或file:read() 卻是以string為input/output對象,
所以我們還另外需要json來幫忙將做table和string的轉換:
local json = require("json")

function saveTable(myTable, filename)
    local path = system.pathForFile( filename, system.DocumentsDirectory)
    local file = io.open(path, "w")
    if file then
        local sTable = json.encode(myTable)--encode the table to string
        file:write( sTable )
        io.close( file )
        return true
    else
        return false
    end
end

function loadTable(filename)
    local path = system.pathForFile( filename, system.DocumentsDirectory)
    local contents = ""
    local myTable = {}
    local file = io.open( path, "r" )
    if file then         
         local sTable = file:read( "*a" )-- read the entire contents of the file
         myTable = json.decode(sTable)--decode the string back to table
         io.close( file )
         file = nil
         return myTable 
    end
    return nil
end
接下來,在程式中要如何使用呢?
gameSettingsFileName = "mygamesettings.json"
currentSettingsVersion = 3

myGameSettings = loadTable("mygamesettings.json")
if(myGameSettings == nil or myGameSettings.version == nil or myGameSettings.version ~= currentSettingsVersion) then
        -- the default settings
 myGameSettings = {}
 myGameSettings.version = currentSettingsVersion
 myGameSettings.level = 2
 myGameSettings.speed = 5 
 saveTable(myGameSettings, gameSettingsFileName)
end

myGameSettings.level = myGameSettings.level+1
saveTable(myGameSettings, gameSettingsFileName)

2015年4月30日 星期四

[Corona SDK] 如何設計自己風格的Segmented Control?

Segmented Control是widget裡相當好用的一種,
假設你有4個items選項讓使用者設定,
用以下範例即可:
display.setStatusBar( display.HiddenStatusBar )
local widget = require( "widget" )
-- Listen for segmented control events      
local function onSegmentPress( event )
    local target = event.target
    print( "Segment Label is:", target.segmentLabel )
    print( "Segment Number is:", target.segmentNumber )
end

-- Create a default segmented control
local segmentedControl = widget.newSegmentedControl
{
    left = 36,
    top = 150,
    segmentWidth = 24,
    segments = { "1", "2", "3", "4" },
    defaultSegment = 2,
    onPress = onSegmentPress
}
得到的結果:

不過,你會發現,每個item的寛度可以透過segmentWidth來設定,
那高度呢?好像無法改變耶...
或者,我們想要有自己特別的風格呢? 

首先,你必須準備好一張圖,當成ImageSheet的來源,
它裡面要含有7張圖形,分別是item未選取的左框/中框/右框, 以及選到時的左框/中框/右框,最後一張是item中間的分隔線,
圖形內容如下:
程式改寫如下:
display.setStatusBar( display.HiddenStatusBar )
local widget = require( "widget" )
-- Listen for segmented control events      
local function onSegmentPress( event )
    local target = event.target
    print( "Segment Label is:", target.segmentLabel )
    print( "Segment Number is:", target.segmentNumber )
end

local options = {
     frames = 
     {
         { x=0, y=0, width=24, height=20 },
         { x=26, y=0, width=24, height=20 },
         { x=52, y=0, width=24, height=20 },
         { x=78, y=0, width=24, height=20 },
         { x=104, y=0, width=24, height=20 },
         { x=130, y=0, width=24, height=20 },
         { x=156, y=0, width=1, height=20 }
     },
     sheetContentWidth = 157,
     sheetContentHeight = 20
 }
 local segmentSheet = graphics.newImageSheet( "imageSheet.png", options )
 local segmentedControl = widget.newSegmentedControl
 {
     left = 100,
     top = 100,     

     sheet = segmentSheet,
     leftSegmentFrame = 1,
     middleSegmentFrame = 2,
     rightSegmentFrame = 3,
     leftSegmentSelectedFrame = 4,
     middleSegmentSelectedFrame = 5,
     rightSegmentSelectedFrame = 6,
     segmentFrameWidth = 24,
     segmentFrameHeight = 20,

     dividerFrame = 7,
     dividerFrameWidth = 1,
     dividerFrameHeight = 20,

     labelSize=12,
  labelColor = { default={ 0.5, 0.5, 0.5 }, over={ 1, 1, 1 } },
     segmentWidth = 24,
     segments = { "1", "2", "3", "4", "5"},
     defaultSegment = seg,
     onPress = onSegmentPress
 } 
看一下執行結果:
好像不太對, 有二個問題,
一個是item 1和item 2,同時highlight了,
另一個是divider的高度,好像是系統預的,不是我們設定的20, 

第一個問題,可以修改frames裡第4個frame (leftSegmentSelectedFrame)的寬度,
我們將它減掉divider的寬度:
local options = {
     frames = 
     {
         { x=0, y=0, width=24, height=20 },
         { x=26, y=0, width=24, height=20 },
         { x=52, y=0, width=24, height=20 },
         { x=78, y=0, width=23, height=20 },
         { x=104, y=0, width=24, height=20 },
         { x=130, y=0, width=24, height=20 },
         { x=156, y=0, width=1, height=20 }
     },
     sheetContentWidth = 157,
     sheetContentHeight = 20
 }
也就說,讓它寛度比segmentWidth小即可, 

第二個問題,可以將segmentedControl移除再加一次即可:
local segmentedControl = widget.newSegmentedControl
 {
     left = 100,
     top = 100,     

     sheet = segmentSheet,
     leftSegmentFrame = 1,
     middleSegmentFrame = 2,
     rightSegmentFrame = 3,
     leftSegmentSelectedFrame = 4,
     middleSegmentSelectedFrame = 5,
     rightSegmentSelectedFrame = 6,
     segmentFrameWidth = 24,
     segmentFrameHeight = 20,

     dividerFrame = 7,
     dividerFrameWidth = 1,
     dividerFrameHeight = 20,

     labelSize=12,
  labelColor = { default={ 0.5, 0.5, 0.5 }, over={ 1, 1, 1 } },
     segmentWidth = 24,
     segments = { "1", "2", "3", "4", "5"},
     defaultSegment = seg,
     onPress = onSegmentPress
 } 
 segmentedControl:removeSelf( )
 local segmentedControl = widget.newSegmentedControl
 {
     left = 100,
     top = 100,     

     sheet = segmentSheet,
     leftSegmentFrame = 1,
     middleSegmentFrame = 2,
     rightSegmentFrame = 3,
     leftSegmentSelectedFrame = 4,
     middleSegmentSelectedFrame = 5,
     rightSegmentSelectedFrame = 6,
     segmentFrameWidth = 24,
     segmentFrameHeight = 20,

     dividerFrame = 7,
     dividerFrameWidth = 1,
     dividerFrameHeight = 20,

     labelSize=12,
  labelColor = { default={ 0.5, 0.5, 0.5 }, over={ 1, 1, 1 } },
     segmentWidth = 24,
     segments = { "1", "2", "3", "4", "5"},
     defaultSegment = seg,
     onPress = onSegmentPress
 } 
最後結果:
上面的方法都只是workaround...

2015年4月29日 星期三

[Corona SDK] 如何決定各個物體間的碰撞發生與否?

在[Corona SDK] 如何偵測物體碰撞 - 利用Global Collision裡,我們會發現所有的物體都會互相碰撞,
我們可以在onGlobalCollision()去決定要如何處理碰撞,
假設我們希望fruit1和fruit2不會互相碰撞,要怎麼處理呢?
此時我們就必須增加filter的參數,如以下範例:
local main = display.newImage( "main.png", 160, 240 )
local mainCollisionFilter = { categoryBits=2, maskBits=1023 } 
physics.addBody( main, { density = 1.0, friction = 0.3, bounce = 0.2 , filter=mainCollisionFilter} )
main.myName = "main"

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

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

local fruit3 = display.newImage( "fruit.png", 300, 220 )
physics.addBody( fruit3, { density = 1.0, friction = 0.3, bounce = 0.2 } )
fruit3.myName = "fruit3"
其中,categoryBits是用來設定物體的類別,原則上每個物體都會不同,這樣才能區分,
因為要配合後面的maskBits,所以categoryBits設定也是以bit為操作,
當然,不同物體也可以設定成同一個categoryBits值,如果你希望它們的行為是類似的話,
categoryBits也可以是多個bits為1,例如7,
最後的行為都是看和maskBits執行mask的結果,
將上面範例整理成圖表方式:
fruit3並沒有設定filter,它的預設值就會變成{ categoryBits=1, maskBits=1023 },
也就是說,它會和任何物體碰撞,
如果有些物體有設定filter,有些物體沒有設定,
那麼,那些有設定filter的categoryBits,最好是從2開始,才能區分,

另外,範例中,雖然fruit2的maskBits是6,也就是說,它希望能和main以及fruit1碰撞,
但是因為fruit1的maskBits為2,fruit1不希望和fruit2碰撞,
mask執行是所有設定值執行AND的結果,所以fruit1和fruit2並不會發生碰撞

2015年4月28日 星期二

[Corona SDK] 如何偵測在螢幕雙擊?

如果要偵測使用者是否有觸控到螢幕,
使用Tap Detection或Touch Detection都可以,
但如果想偵測雙擊的動作,那使用tap detection會簡單些,
範例如下:
local function buttonListener( event )

    if ( event.numTaps == 1 ) then
        print( "single-tapped" )
        runSingleTappedAction()
    elseif ( event.numTaps == 2 ) then
        print( "double-tapped" )
        runDoubleTappedAction()
    else
        return true
    end
end

local myButton = display.newRect( 160, 240, 120, 60 )
myButton:addEventListener( "tap", buttonListener )
雙擊時,一定是先發生單擊事件,然後才是雙擊事件,
也就是說,會執行runSingleTappedAction(),然後才是runDoubleTappedAction(),
如果雙擊時,不想執行runSingleTappedAction(),
那就必需等一段時間確認是單擊或雙擊再執行,
如以下用法:
local tapCount=0
local timeId = nil
function runTapAction( event )
 if ( tapCount == 1 ) then
     print( "single-tapped" )   
     runSingleTappedAction() 
 elseif ( tapCount == 2 ) then
     print( "double-tapped" )
     runDoubleTappedAction()
 end
end

local function buttonListener( event )
 tapCount = event.numTaps
 if(timeId ~= nil) then
   timer.cancel( timeId )
 end
 timeId = timer.performWithDelay( 200, runTapAction,1 )
end

local myButton = display.newRect( 160, 240, 120, 60 )
myButton:addEventListener( "tap", buttonListener )
當然,以上的作法會犠牲單擊的反應速度

2015年4月23日 星期四

[Corona SDK] 如何呼叫其它lua檔案的function?

Corona SDK APIs本身就是根據功能的不同分類,
例如display.* , os.* 等等,
我們如果有類似功能的function,也可以將它們擺在同一個lua檔,
那要如何從其它檔案呼叫它們呢?

假設我們有一個檔案GameScenario.lua,用來專門設定遊戲關卡的佈局:
gameScenario = {}

local function configureGameScenario(round)
 print("configureGameScenario():"..round)
end--local function configureGameScenario()

gameScenario.configureGameScenario = configureGameScenario
return gameScenario
configureGameScenario()會根據傳進來的參數round來決定如何設置,
你可以從其它檔案呼叫它:
local gameScenario = require("GameScenario")
gameScenario.configureGameScenario(1)
上面的用法就是利用lua強大的table功能

2015年4月22日 星期三

[Corona SDK] 如何重新載目前的scene

如果要改變場景,我們可以呼叫composer.gotoScene( xxx ),
但,如果是要重新載入目前的場景呢?
我們一樣可以呼叫composer.gotoScene( xxx ),其中xxx只要傳入目前場景的名稱即可,

不過,你會發現重新載入時,
scene:create()並沒有被呼叫到,
而是直接跳到scene:show(),
當然,你可以把scene:create()應該做的事移到scene:show()內去,
但這樣會變成scene:create()其實是多餘的,

之前可以透過storyboard.reloadScene()來重新載入,
目前新的Corona SDK已經不支援了,
要如何處理呢?

有一個簡單的方法,就是建一個暫時的轉換場景,
先呼叫它,然後再轉換回來:
local options =
{
 effect = "fade",
 time = 0,
 params =
 {
  myData = 1234
 }
}
composer.gotoScene( "RestartDummy", options )
參數time可以設為0,表示立刻轉換,
params是我們想要傳給"RestartDummy"的參數,
"RestartDummy.lua"部份:
local composer = require( "composer" )
local scene = composer.newScene()

function scene:create( event ) 
 print( "((create scene RestartDummy's view))" )
 local params = event.params
 print(params.myData)
end --function scene:create( event )

function scene:show( event ) 
 local phase = event.phase 
 if "did" == phase then 
  print( "((show scene RestartDummy's view))" ) 
  composer.removeScene( xxx ) 
  composer.gotoScene( "xxx", "fade", 0 )  
 end 
end

function scene:hide( event ) 
 local phase = event.phase 
 if "will" == phase then 
  print( "((hiding scene RestartDummy's view))" )  
 end 
end

function scene:destroy( event )
 print( "((destroying scene RestartDummy's view))" )
end


-- Listener setup
scene:addEventListener( "create", scene )
scene:addEventListener( "show", scene )
scene:addEventListener( "hide", scene )
scene:addEventListener( "destroy", scene )

return scene

2015年4月21日 星期二

[Corona SDK] 如何設定button的字型大小和顏色?

要增加一個button,只要利用widget的newButton即可,
如下面範例:
local widget = require( "widget" )
local button1Press = function( event )
 print("button1 pressed")
end

local button1 = widget.newButton
{
 defaultFile = "buttonYellow.png",
 overFile = "buttonYellowOver.png",
 label = "botton 1",     
 onPress = button1Press,
}
defaultFile是用來設定預設的按鈕圖案,
overFile是用來設定使用者按下按鈕時按鈕的顯示圖案,
按鈕上的文字是"botton 1", 可是它的大小和顏色是預設的,
如果想要改變,可以參考以下範例:
local widget = require( "widget" )
local button1Press = function( event )
  print("button1 pressed")
end

local button1 = widget.newButton
{
  defaultFile = "buttonYellow.png",
  overFile = "buttonYellowOver.png",
  label = "botton 1", 
  font = native.systemFont,
  fontSize = 20,
  emboss = true,
  labelColor = { default = { 0.5, 1, 1 }, over = { 0.5, 0.5, 0.5} },    
  onPress = button1Press,
}

2015年4月16日 星期四

[Corona SDK] 如何取得某一目錄下的所有檔案列表

如果想要取得某一目錄下的所有檔案列表,
只要利用LuaFileSystem即可,
對於要秀圖時,例如相片瀏覧程式,
這樣的功能很方便,
範例如下:
dir = "/myDir"
for file in lfs.dir(dir) do
    if (file ~= '.' and file ~= '..') then
        print(file)
    end
end
目錄或檔案都會列出,
下面的範例是列出目前目錄下的所有檔案和目錄,以及子目錄下的所有檔案列表:
dir = "."
for file in lfs.dir(dir) do
    if (file ~= '.' and file ~= '..') then
     print(file)
        if(lfs.attributes(file,"mode") == "directory") then        
         for subFile in lfs.dir(file) do
          if (subFile ~= '.' and subFile ~= '..') then
          print(file.."/"..subFile)
          end
         end
        end
    end
end

2015年4月13日 星期一

[Corona SDK] 如何利用physics.addBody() 參數裡outline來達成不規則物件的碰撞

底下是一個簡單的物理碰撞範例:
local physics = require( "physics" )
physics.start()

local sky = display.newImage( "bkg_clouds.png", 160, 195 )

local ground = display.newImage( "ground.png", 160, 445 )

physics.addBody( ground, "static", { friction=0.5, bounce=0.3 } )

local crate = display.newImage( "crate.png", 180, -50 )
crate.rotation = 5

physics.addBody( crate, { density=3.0, friction=0.5, bounce=0.3 } )
其中,用到的圖形如下:
bkg_clouds.png

crate.png

ground.png
碰撞之後結果如下:
不過,現實世界裡,很多物件不是如此方方正正,
假設我們把crate.png和gound.png修改如下:
crate.png

ground.png
再跑一次看看,執行結果如下:
碰撞的過程和結果都我們修改後的圖形無關,
執行仍是按照原來矩形形狀的碰撞結果???

其實在physics.addBody()參數裡,我們可以指定物體的形狀,
參數可以是矩形,圓形或多角形等等,
在這裡,我們可以直接用原來圖形的形狀來當參數,
也就是借用graphics.newOutline()取得的外觀,
修改程式如下:
local physics = require( "physics" )
physics.start()

local sky = display.newImage( "bkg_clouds.png", 160, 195 )

local ground = display.newImage( "ground.png", 160, 445 )
local ground_outline = graphics.newOutline( 2, "ground.png" )
physics.addBody( ground, "static", { friction=0.5, bounce=0.3,outline=ground_outline } )


local crate = display.newImage( "crate.png", 180, -50 )
crate.rotation = 5
local image_outline = graphics.newOutline( 2, "crate.png" )
physics.addBody( crate, { density=0.2, friction=0.5, bounce=0.3,outline=image_outline} )
graphics.newOutline()裡第一個參數是用來設定外觀的精細度,
再看一次執行結果:

2015年4月9日 星期四

[Corona SDK] 如何利用Composer來切換場景

我們的程式通常會有幾個不同功能的畫面,
例如,設定相關畫面..遊戲畫面..說明畫面等等,
我們可以把同一個畫面相關程式放在同一個"xx.lua"檔裡,
然後利用Composer來切換不同的畫面(場景Scene),

The Template of Scene
每個Scene檔裡,都會有4個基本的function
local composer = require( "composer" )
local scene = composer.newScene()

---------------------------------------------------------------------------------

function scene:create( event )
 local sceneGroup = self.view

 -- Called when the scene's view does not exist.
 -- 
 -- INSERT code here to initialize the scene
 -- e.g. add display objects to 'sceneGroup', add touch listeners, etc.
end

function scene:show( event )
 local sceneGroup = self.view
 local phase = event.phase
 
 if phase == "will" then
  -- Called when the scene is still off screen and is about to move on screen
 elseif phase == "did" then
  -- Called when the scene is now on screen
  -- 
  -- INSERT code here to make the scene come alive
  -- e.g. start timers, begin animation, play audio, etc.
 end 
end

function scene:hide( event )
 local sceneGroup = self.view
 local phase = event.phase
 
 if event.phase == "will" then
  -- Called when the scene is on screen and is about to move off screen
  --
  -- INSERT code here to pause the scene
  -- e.g. stop timers, stop animation, unload sounds, etc.)
 elseif phase == "did" then
  -- Called when the scene is now off screen
 end 
end


function scene:destroy( event )
 local sceneGroup = self.view
 
 -- Called prior to the removal of scene's "view" (sceneGroup)
 -- 
 -- INSERT code here to cleanup the scene
 -- e.g. remove display objects, remove touch listeners, save state, etc.
end

---------------------------------------------------------------------------------

-- Listener setup
scene:addEventListener( "create", scene )
scene:addEventListener( "show", scene )
scene:addEventListener( "hide", scene )
scene:addEventListener( "destroy", scene )

---------------------------------------------------------------------------------

return scene
create()是切換到此場景時先被呼叫的地方,我們可以這裡加入button等場景需要的物件以及相對應的function,
show()是場景要開始真正運作的進入點,我們在這裡加入需要的執行動作,例如播放音樂等等,
hide()是場景要被切換時會呼叫,也就是要離開場景時,
destroy()是場景被remove時會呼叫

main.lua
我們也可以在main.lua裡加入display相關物件,
在這裡加入的物件,不管如何切換場景,它們都會出現在畫面上,
可以視它們為global objects
-- hide device status bar
display.setStatusBar( display.HiddenStatusBar )

-- require controller module
local composer = require "composer"

local widget = require "widget"

local function runTab1(event )
 composer.gotoScene( "scene1", "fade", 400 )
end

local function runTab2(event )
 composer.gotoScene( "scene2", "fade", 400 )
end

local widget = require( "widget" )

 -- Create buttons table for the tabBar
 local tabButtons = 
 {
  {
   label = "tab1",
   id = "tab1",
   size = 40,
   onPress = runTab1, 
   selected = true
  },
  {
   label = "tab2",
   id = "tab2",
   size = 40,
    onPress = runTab2
  }
 }

 -- Create tabBar at bottom of the screen
 local tabBar = widget.newTabBar
 {
  buttons = tabButtons
 }
 tabBar.y = display.contentHeight - (tabBar.height/2) 
 print(display.contentHeight)
 runTab1()
在這裡我們加入一個tabbar,
不管在那個場景,我們都可以這個tabbar切換場景,
composer.gotoScene( "scene1", "fade", 400 )表示要切到"scene1", 它需要有一個對應的"scene1.lua",

scene1.lua
local composer = require( "composer" )
local scene = composer.newScene()
local widget = require "widget"
---------------------------------------------------------------------------------
-- BEGINNING OF YOUR IMPLEMENTATION
---------------------------------------------------------------------------------

local image, text1, text2, text3, memTimer

-- Function to handle button events
local function handleButtonEvent( event )

    if ( "ended" == event.phase ) then
        composer.gotoScene( "scene2", "fade", 400 )
    end
end

-- Called when the scene's view does not exist:
function scene:create( event )
 local sceneGroup = self.view
 text1 = display.newText( "Scene 1", 0, 0, native.systemFontBold, 24 )
 text1:setFillColor( 255 )
 text1.x, text1.y = display.contentWidth * 0.5, 50
 sceneGroup:insert( text1 )
 -- Create the widget
 local button1 = widget.newButton
 {
     left = 100,
     top = 320,
     id = "button1",
     label = "Goto Scene 2",
     onEvent = handleButtonEvent
 } 
 sceneGroup:insert( button1 )
 print( "\n1: create event")
end

function scene:show( event ) 
 local phase = event.phase 
 if "did" == phase then 
  print( "1: show event, phase did" )   
  composer.removeScene( "scene2" ) 
 end 
end

function scene:hide( event ) 
 local phase = event.phase 
 if "will" == phase then 
  print( "1: hide event, phase will" ) 
 end 
end

function scene:destroy( event )
 print( "((destroying scene 1's view))" )
end

---------------------------------------------------------------------------------

-- Listener setup
scene:addEventListener( "create", scene )
scene:addEventListener( "show", scene )
scene:addEventListener( "hide", scene )
scene:addEventListener( "destroy", scene )

---------------------------------------------------------------------------------

return scene
在scene1裡我們加入了一個button,除了用原本的tabbar, 我們也可以利這個button來切換到scene2

scene2.lua
local composer = require( "composer" )
local scene = composer.newScene()
local widget = require "widget"
local image, text1, text2, text3, memTimer
-- Function to handle button events
local function handleButtonEvent( event )

    if ( "ended" == event.phase ) then        
        composer.gotoScene( "scene1", "fade", 400 )
    end
end

function scene:create( event )
 local sceneGroup = self.view
 text1 = display.newText( "Scene 2", 0, 0, native.systemFontBold, 24 )
 text1:setFillColor( 255 )
 text1.x, text1.y = display.contentWidth * 0.5, 50
 sceneGroup:insert( text1 )
 local button1 = widget.newButton
 {
     left = 100,
     top = 320,
     id = "button1",
     label = "Goto Scene 1",
     onEvent = handleButtonEvent
 } 
 sceneGroup:insert( button1 ) 
 print( "\n2: create event" )
end

function scene:show( event ) 
 local phase = event.phase 
 if "did" == phase then 
  print( "2: show event, phase did" )
  composer.removeScene( "scene1" ) 
 end
 
end

function scene:hide( event ) 
 local phase = event.phase
 if "will" == phase then 
  print( "2: hide event, phase will" )
 end
end

function scene:destroy( event ) 
 print( "2: destroying scene 2's view" )
end

---------------------------------------------------------------------------------

-- Listener setup
scene:addEventListener( "create", scene )
scene:addEventListener( "show", scene )
scene:addEventListener( "hide", scene )
scene:addEventListener( "destroy", scene )

---------------------------------------------------------------------------------

return scene
scene2和scene1類似,
當我們呼叫composer.gotoScene( "scene2", "fade", 400 )時,
scene1的hide()會先被呼叫,
然後是scene2的create(),再來是scene2的show(),
在scene2的show()裡面我們執行composer.removeScene( "scene1" ),
此時scene1的destroy()會被呼叫

2015年4月8日 星期三

[Corona SDK] 如何取得local IP和Public Internet IP?

如果我們是透過某個AP連上網路,
AP會給你的裝置一個local IP,
要取得這個IP,可利用下面方式:
local getIP = function()
    local s = socket.connect("www.google.com", 80)--need to have internet connection
    local ip, _ = s:getsockname()
    print( "myIP:", ip)
    text1 = display.newText( "Local IP:"..ip, display.contentCenterX, 40, native.systemFontBold, 24 )
    text1:setFillColor( 1,1,0 )
    return ip
end

getIP()
上面的方式需要在有連網的情形下執行,
它是連到某個開放網站,例如"www.google.com",
當然,你也可以連到其它網站,

因為是取得local IP,有時會希望在沒有連上網路時也能取得這樣的資訊,
此時可使用下面的方式:
local getIP = function()
    local s = socket.udp()  --creates a UDP object
    s:setpeername( "1.1.1.1", 80 )--No need to have internet connection
    local ip, _ = s:getsockname()
    print( "myIP:", ip)
    text1 = display.newText( "Local IP:"..ip, display.contentCenterX, 0, native.systemFontBold, 24 )
    text1:setFillColor( 1,1,0 )
    return ip
end

getIP()
我們建立一個UDP連線,透過這樣的方式來取得自己的IP,
例子中,"1.1.1.1"可以是任何IP,
因為我們並沒有真的要傳送資料,

那...如果要取得真正對外的網路IP呢?
此時我們可利用一些提供這種服務的網站,
例如以下的方式:
function findPublicIPAddress()
    
    local getipScriptURL = "http://myip.dnsomatic.com/"
    local DeviceIP
    
    function ipListener(event)
        if not event.isError and event.response ~= "" then
            DeviceIP = event.response 
            print("Public DeviceIP:"..DeviceIP)
            text1 = display.newText( "Public IP:"..DeviceIP, display.contentCenterX, 80, native.systemFontBold, 24 )
            text1:setFillColor( 0,1,1 )
        end
    end
    
    network.request(getipScriptURL,"GET",ipListener) 
    
end
 
findPublicIPAddress()
當然,上面的例子必須在有連網的情形下執行

2015年4月7日 星期二

[Corona SDK] 如何使用widget.newTabBar 建立tab bar?

Tab在視窗操作中算是很實用的方式,
在手機遊戲程式裡,Tab倒是少人用,
不過,如果是有點工具類性質的操作畫面,
個人覺得用上Tab可以使畫面更加簡潔,
操作起來也方便快速...

要建立一個tab很簡單,只要使用widget.newTabBar即可,
如下面只要幾行即可:
display.setStatusBar( display.HiddenStatusBar )
local widget = require( "widget" )

local function runTab1(event )
 display.remove( text2 )
 text1 = display.newText( "Tab 1", 100, 100, native.systemFont, 40 )
end

local function runTab2(event )
 display.remove( text1 )
 text2 = display.newText( "Tab 2", 100, 100, native.systemFont, 40 )
end


 -- Create buttons table for the tabBar
 local tabButtons = 
 {
  {
   label = "tab1",
   id = "tab1",
   size = 40,
   onPress = runTab1, 
   selected = true
  },
  {
   label = "tab2",
   id = "tab2",
   size = 40,
   onPress = runTab2
  }
 }

 -- Create tabBar at bottom of the screen
 local tabBar = widget.newTabBar
 {
  buttons = tabButtons
 }

 tabBar.y = display.contentHeight - (tabBar.height/2) 
要使用widget,需要宣告require( "widget" ),
在widget.newTabBar裡帶入buttons = tabButton即可建立tab bar,
上面的範例裡,我們建立二個tab 按鈕,
在tabButtons裡的onPress 可帶入按下tab按鈕後想執行的動作,
下面是執行結果:
另外,如果希望tab按鈕可以多一個變化,
我們也可以把圖形放在上面,例如:
local tabButtons = 
 {
  {
   label = "tab1",
   id = "tab1",
   size = 40,
   onPress = runTab1, 
   selected = true
  },
  {
   label = "tab2",
   id = "tab2",
   size = 40,
   width = 232,
   height = 40,
         defaultFile = "1.png",
         overFile = "2.png",
   onPress = runTab2
  }
 }
上面的例子裡,tab2預設的圖形是1.png,選到時的圖形是2.png,
需注意此時width和height的參數必須指定,
執行如果如下: