2015年11月17日 星期二

[Corona SDK] How to support multiple Ads networks - AdMob, iAds, Vungle

InHow to add advertisement in my APP - using AdMob V2, we learn how to add AdMob Ads.
For iAds and Vungle, we can use the similar way to add them.
We just consider how to support multiple networks here.
That it, we want to select AdMobiAds or Vungle dynamically, no matter it is for Fill Rate, eCPM or any other reasons.

Add plugin
First, we need add plug-in in build.settings:
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",
        },
    },
}


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
We have to call ads.init() for AdMob,iAds and Vungle separately.
The parameters in their callback functions are different.
In above codes, we change to next Ads network when we get error.

Change Ads Network
We set new Ads network by function ads:setCurrentProvider().
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


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 does not have Banner Ads and it will pre-load interstitial Ads itself.
iAds cannot pre-load interstitial Ads,
The codes shown above are very completed. 
You can just copy them directly and make it work.

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

如何加入廣告 - AdMob V2裡,
我們知道要如何加入AdMob Ads,
要加入iAdsVungle的話,作法很類似,
在這裡我們討論如果要同時支援的話該如何處理,
也就是說,我們可以動態選擇 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,iAdsVungle各自呼叫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] How to add Apple Game Center and Android Google Play Game Service

For Apple Game Center or Android Google Play Game Service, 
we just need to utilize the function of gameNetwork.
It is not difficulty to implement them.
To consider Apple and Google at the same time, 
let's check the codes first and see how them work:
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
}
Init
In general, we can call gameNetworkSetup() in main.lua
For Android, we need to check if we have logged in successfully in gpgsInitCallback().
For iOS, we need to check if we have logged in successfully in gameNetworkLoginCallback().
The code gameNetwork.request( "loadLocalPlayer", { listener=loadLocalPlayerCallback } ) is to confirm the user who logs in.
You can go without it.
The code gameNetwork.request( "loadAchievements", { listener=loadAchievementCallback } ) is to recover the users's records.
For example, users may have ever installed this APP, remove this APP, and re-install it again.
If you think that you don't need to recover the records for them, you can go with it.

Show leaderboard
To show the leaderboard, we just need to call function showLeaderboards().
In general, we do it when user press some button

Show achievement
To show the leaderboard, we just need to call function showAchievements().
In general, we do it when user press some button.

Update leaderboard

To update leaderboard, we just need to call function updateLeaderBoards().
In general, wwe do it when user finish the game.

Update achievements 

To update leaderboard, we just need to call function updateAchievement().
In general, wwe do it when user finish some round.


Below are what differ between Apple Game Center and Android Google Play Game Service:
Apple Game Center
The data in leaderboardsListId{} and achievementsListId{}:
e.g. "com.gmail.mycom.demo.Leaderboards.HighestScore".
They are user defined, as long as they are  not duplicated.

Google Play Game Service
The data in leaderboardsListId{} and achievementsListId{}:
e.g. "Cgkxxxxxxxx".
They are defined by play store publish when you finished the setting in game center.
For Google Play Game Service, we also need to add the googlePlayGamesAppId in build.settings:
android =
    {
        usesPermissions =
        {
            "android.permission.INTERNET",
            "android.permission.ACCESS_NETWORK_STATE",
            "com.android.vending.BILLING",
        },
        googlePlayGamesAppId = "123456789012" 
    },
usesPermissions are not concerned for game service,
googlePlayGamesAppId is what you get when you link the APP in play store publish game center.

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

不管是AppleGame Center,還是AndroidGoogle Play Game Service
基本上都是利用gameNetwork的功能即可,
實作不難,考慮同時支援AppleGoogle
先將程式碼貼上再來講解:
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()即可, 通常這個動作是在使用者完成某個關卡時


以下是AppleGame Center,和AndroidGoogle 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的遊戲中心連結應用程式後系統給的編碼

[ 遊戲 ] 空戰 - 火力全開


https://www.facebook.com/Air-Fight-Infinite-Power-189988404668625/

你有無盡的火力來消滅敵人戰機。
小心接踵而來的敵機和巨量的子彈攻擊,
當你有危險時,不要遲疑,趕快發射炸彈來保護你自己。

如果你喜歡射擊遊戲,那你一定會喜歡這個APP。

[ Game ] Air Fight - Infinite Power



https://www.facebook.com/Air-Fight-Infinite-Power-189988404668625/


You have infinite power to clear the enemies.

Be careful of consecutive enemy fighters and massive bullets.
Don't hesitate to shoot the bomb when you are in danger.

If you like shooting game, you will love this app.

[ 遊戲 ] 桌上撞盤


https://www.facebook.com/Table-Discs-1477277249248292


這是一個有趣的新型態遊戲。

第一眼看起有點像撞球中的開侖,
但是它其實更為簡單且好玩。

你不需要有什麼開超的撞球技巧,
只要將白盤射出,讓它撞到紅盤即可。

當然,這中間一定會有許多的障礙來阻撓你...

[ Game ] Table Discs


https://www.facebook.com/Table-Discs-1477277249248292


This is an interesting game in new type.
It look like Carom at first sight.
However, it is easier and has more fun.

You don't need to have good skills for billiards game.
Just shoot the white disc and let it collide with red disc.

Of course, there will have some obstacles to interfere with your movement.

[ 遊戲 ] 推推樂



https://www.facebook.com/pages/Push-Discs/1632278527059052

想要運動您的手指嗎?
"推推樂"是一個簡單好玩但又充滿挑戰性的遊戲。
它支援Google遊戲中心。
快邀請您的朋友一同來挑戰它。

[ Game ] Push Discs


https://www.facebook.com/pages/Push-Discs/1632278527059052

Want to exercise your finger?
"Push Discs" is a simple but full of challenging game.
It supports Google Play Game Services.
Invite your friends to challenge it together.

[ 遊戲 ] 單字矩陣無限玩



https://www.facebook.com/Infinite-Word-Matrix-3-letters-874519139268720/

你認識很多3個字母的單字嗎?

這太簡單了吧?要拼出3個字的單字實在沒什麼難度,
不過,其實這會比你想像中的困難,
因為3個字的單字比你想像中的少,
再者,有些字又很相似,
例如 "ria","rib","rid","rif","rig","rim","rip"....
我們可能會認為 "rie", "rit"等等也都是有意義的單字,
但...其實不然。

這個APP不只是個好玩的拼字遊戲,也是一個幫助你練習和覆習拼字的工具。

[ Game ] Infinite Word Matrix


https://www.facebook.com/Infinite-Word-Matrix-3-letters-874519139268720/


How many words in 3 letters do you know?

You may think that it is easy to spell out the words in 3 letters.
However, the number of words in 3 letters is less than you expect.
Furthermore, many words are similar except for one letter difference.
For example, "ria","rib","rid","rif","rig","rim","rip"....
We may think that there should have words such as "rie", "rit".
But...you get the wrong answers.


This APP is not only a game for fun, it is also a tester, reminder for you to improve your spell skill.

[ 遊戲 ] 撲克陣列



https://www.facebook.com/Infinite-Poker-Matrix-1591171817815298

一個玩不停的撲克遊戲。
你只要點選任何5張牌即可。

牌面越大,你得到的金幣就會越多。
不過,當你在選擇牌時,你必須同時考慮下一次的5張牌應該是什麼。
這是一個完全免費的遊戲。

[ Game ] Infinite Poker Matrix


https://www.facebook.com/Infinite-Poker-Matrix-1591171817815298

An endless poker game.
Just pick any five cards.
The better combination, the more coins you get.
However, when you pick current 5 cards, you should also consider that what will the next 5 cards be.
This is a totally free game.

[ 遊戲 ] 快樂兔遊隧道


https://www.facebook.com/Happy-Bunny-Tunnel-Surf-1477746165886686


快樂兔正在隧道內快樂的遊玩。
胡蘿蔔仍然是她的最愛。

快樂兔正在隧道內快樂的遊玩。
胡蘿蔔仍然是她的最愛。

[ Game ] Happy Bunny - Tunnel Surf


https://www.facebook.com/Happy-Bunny-Tunnel-Surf-1477746165886686


Happy Bunny is playing in the tunnel.
Carrot is still her favorite.

She can jump...
She can change tunnel...
She can also do these two thing at the same time.

The is a brand new type game.
We believe that you can enjoy playing it for a long time.

[ 遊戲 ] 快樂兔動動腦


https://www.facebook.com/pages/Happy-Bunny-Rack-Brain/1482173925439162


你曾經擠破腦袋也記不住某些事?

它真是一個惡夢啊...
老是記了就忘,
再記...還是忘...
有什麼簡單的方法可以改善嗎?

當然可以,
我們可以藉由這個遊戲來活絡我們的大腦,
它會加強所謂的右腦圖像記憶。

記住,不要把它當成訓練,
用玩遊戲的輕鬆心情去完成它,
無形中,你會發現你的瞬間記憶變強了,
讓我們一起試試吧!

[ Game ] Happy Bunny - Rack Brain


https://www.facebook.com/pages/Happy-Bunny-Rack-Brain/1482173925439162

Have you ever racked your brain for trying to remember something?

It's a nightmare for most of us.
Can we improve our memory skill in easy way?

Yes, we can.
Let's activate our brain just by playing this game.
It can strengthen your ability of brain imaging memory.
Try it now!

[ 遊戲 ] https://www.facebook.com/Happy-Bunny-Dodge-Game-124867114526322


https://www.facebook.com/Happy-Bunny-Dodge-Game-124867114526322

好玩的快樂兔將會讓你一玩就停不下來。

當你玩這個遊戲時,你會很驚訝時間怎麼會過得這麼快。
它是一款無止盡的遊戲,
只能你能,你想玩多久就玩多久。
準備好了嗎?來和我們一起玩吧....

[ Game ] Happy Bunny - Dodge Game


https://www.facebook.com/Happy-Bunny-Dodge-Game-124867114526322

The interesting game "Happy Bunny Go" will let you never stop playing.
You will be surprised that time passes so fast when you play this game.
It is an endless game.
You can play as long as you can.
Ready? Come play with us...

[ 遊戲] 我是直升機駕駛高手 - 城市冒險

https://www.facebook.com/Fly-Helicopter-City-Adventure-1503841246594029

你可以駕駛直升機通過不同的地形?

就這樣嗎?
當然不止....
你需要盡可能避開敵人的直升機,為了您的安全。
與此同時,噴氣戰鬥機正以可怕的速度接近你。
隨時注意轟炸機,否則你會被空投的炸彈炸飛。
地上的高射砲會一直瞄準你,無論你如何移動。

請記住,盡可能收集硬幣,越多越好。
這些金幣可以換成續關鑰匙。

[ Game ] Fly Helicopter-City Adventure

 https://www.facebook.com/Fly-Helicopter-City-Adventure-1503841246594029

Can you fly the helicopter to pass different kinds of terrain?

Is that all?
Of course not ....
You need to avoid the enemy helicopters as far as possible, for your safety.
At the same time, the jet planes are approaching you in terrible speed.
Watch out the bomber any time, or you will be attacked by the air bomb.
The antiaircraft gun is always aiming at you no matter how you move.

Remember to collect the coins as more as possible.
They can be exchanged for key, which will save your life.

[遊戲] 圓盤保齡球


https://www.facebook.com/Discs-Bowling-879320752188039/


這是一個用圓盤來玩的保齡球遊戲

它的規則和一般的保齡球玩法一樣,
不過,你可以更簡單的控制它,包括方向,力道,移動的弧度等等。
有二種挑戰,標準賽和障礙賽。
找個休間時間,好好來享受這個遊戲的樂趣吧...

[Game] Discs Bowling


https://www.facebook.com/Discs-Bowling-879320752188039/


This is a bowling game played with discs.
The rule is the same as typical bowling game.
However, you can control it in easier method, including direction, strength, moving curve....and so on.
There have two types of challenges, Standard Race and Obstacle Race.
Take your time and enjoy this game....

[Game] Crazy Racing - Keep Speeding


https://www.facebook.com/Crazy-Racing-Keep-Speeding-1691066911114797


Crazy racing is coming again...
The is a racing game without speed limitation.
As the speed increasing, you will have less and less time to respond.
You will feel excited if you like the crazy speed.

Remember to collect the coins as more as possible.
They can be exchanged for key, which will save your life.

[遊戲] 瘋狂賽車 - 超越極速




瘋狂賽車又來了...
這是一個沒有速度極限的賽車遊戲。

隨著速度越來越快,你將需要更快的反應。
如果你喜歡這種瘋狂的速度感,本遊戲會讓你感到興奮。

請記住,盡可能收集硬幣,越多越好。
這些金幣可以換成續關鑰匙。

[遊戲] 瘋狂直昇機 - 城市戰爭





這一次,你的直昇機已裝上武器...

你有機關槍可以用極為強大的速度射出子彈,
只要你適時的補充,這些子彈是打不完的。

你有飛彈可以用來擊落轟炸機,或用來消滅地上的坦克車,
當然,你也可以用飛彈來清除任何擋在你前面的障礙物。

還有一個所謂的無敵模式,
在這模式下,你是不受任何敵方的武器所攻擊,也可穿越任何的障礙物..

讓我們一起來戰鬥吧,盡情享受殺敵的快感.....

[Game] Crazy Helicopter - City War





This time, your helicopter is armed.

You have machine gun to shoot the bullets in powerful speed.
The bullets will not run out of as long as you refill them in time.

You have missiles to shoot down the bomber or to destroy the tank.
Of course, you can also use missiles to clear any obstacles in front of you.

When you are in super mode, you are invincible and have endless bullets and missiles.
Let's fight together......

2015年7月28日 星期二

[C#] Android, iOS App icon generator

When we develop Android or iOS App, we need to design the icon the our App as well.
The annoying problem is that we need to have different resolutions of icons.

There have many free website which can help to us to do that.
However, some websites need to have account to start generating.
Some websites may have slow response time.

So...I decide to write a program using C# for such purpose.
At the same time, I think that there may have many people have similar requirement like me.
I would like to share my program as below:

I suggest that you can design a icon with the resolution of 1024x1024.
That you can open it with generateAppIcon and generate files with different resolutions you need.


By the way, we often use rounded corner in our icon.
In Android, you can design your rounded corner.
The system will show the icon as you design.

However, in iOS, the system will generate the rounded corner itself.
The radius of rounded corner may change for different system version.
I would like to suggest that you can design a icon without rounded corner and let the system to generate the rounded corner itself.

[C#] Android, iOS App icon產生器

當我們開發Android,iOS的App時,需要製作App的icon,
比較麻煩是,我們需要不同解析度的icon,
網路上有許多免費的網站可以幫忙產生不同的解析度,
不過,有的網站需要申請帳號,
有的網站處理速度很慢,

所以,只好自己用C#寫一個來用,
我想可能很多人都有這樣的需求,
所以就分享出來,
下載網址如下:

建議先製作一個1024x1024大小的icon,
然後丟到generateAppIcon去產生不同的解析度,
Launch Images是app還沒開始前會先載入的畫面,
很多人會放公司的logo之類的,
它的大小最大是1242x2208, 1536x2048等等,
最好是製作一個高解析的圖當成原圖,

附帶一提,
很多App icon都會有圓角的設計,
Android,你可以設計自己想要的圓角,
它會按照你產生的icon來顯示,

iOS,系統會自己截取你的icon來產生圓角,
而且它的圓角也可能因為不同版本會改變,
除非你的icon和圓角半徑有關,
不然,建議你可以產生一個沒有圓角的icon,
讓系統自己來產生圓角

[C#] When we use PictureBox to load the picture file,the picture file will be locked?

We often use PictureBox to load the picture file, like below:
try
{
 var srcImage = Image.FromFile("test.jpg");
 myPictureBox.Image = srcImage;
}
catch (System.OutOfMemoryException)
{
 MessageBox.Show("Invalid picture format.");
}
However, you will find that you cannot move or rename this picture file.
That is this picture file is locked.

What if we wish that it will not be locked?
We can solve this problem be using FileStream, as below:
try
{
 FileStream stream = new FileStream(openFileDialog1.FileName, FileMode.Open);
 var srcImage = Image.FromStream(stream);
 stream.Close();
 myPictureBox.Image = srcImage;
}
catch (System.ArgumentException)
{
 MessageBox.Show("Invalid picture format.");
}

[C#] 圖片檔案載入PictureBox 控制項時,影像檔案會被鎖住?

我們常常會利用PictureBox來載入影像檔案,
例如以下的用法:
try
{
 var srcImage = Image.FromFile("test.jpg");
 myPictureBox.Image = srcImage;
}
catch (System.OutOfMemoryException)
{
 MessageBox.Show("Invalid picture format.");
}
不過,你會發現此時你無法再對這個做移動或者更名等動作,
因為這個檔案已經被鎖住了,

如果我們不希望它被鎖住呢?
我們可以藉用FileStream來解決,如下:
try
{
 FileStream stream = new FileStream(openFileDialog1.FileName, FileMode.Open);
 var srcImage = Image.FromStream(stream);
 stream.Close();
 myPictureBox.Image = srcImage;
}
catch (System.ArgumentException)
{
 MessageBox.Show("Invalid picture format.");
}

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"看看