Hi everyone
_ UPDATED: 03/10/2017 _
After going through this tutorial you will be able to implement Google Play Game Services (GPGS) with the following functionalities:
- Successful sign up/sign in
- Achievements
- Leaderboards
- Cloud syncing across devices
- License check
First activate the GPGS plugin from Corona Marketplace when you are signed in to coronalabs.
Then, set up you Google Play Game Services from your developer console. You can check out this official guide from Google.
NOTE: GPGS does not work for iOS as support has been removed for that platform.
Now comes the coding part, it may look long but it is really very easy to implement the GPGS plugin.
-build.settings
You get your googlePlayGamesAppId when you press the ADD NEW GAME to create a service for your new game in your developer console under Game services. Once you complete the procedure you should get the number under your game name. REMEMBER to create the game application at first then game services since game services needs to be linked to your application.
android = { usesPermissions = { "com.android.vending.CHECK\_LICENSE", }, googlePlayGamesAppId = "GET YOUR APP ID FROM DEV CONSOLE", plugins = { ["plugin.gpgs"] = { publisherId = "com.coronalabs", supportedPlatforms = {android = true} }, } }
-config.lua
To find the key for license go to your developer console then select the application from All application tab, then go to Development tools and press Services & APIs. Under Licensing & in-app billing you should get that long RSA public key.
application = { license = { google = { key = "YOUR APP ID FROM DEVELOPER CONSOLE", policy = "serverManaged", }, }, }
-main.lua
loadsaveis a module to save table and load table for Lua. You can save you game data easily all in one place.
local loadsave = require("loadsave") local gpgs = require( "plugin.gpgs" ) local licensing = require( "licensing" ) licensing.init( "google" ) local gpgsData = loadsave.loadTable( "gpgsData.json" ) if ( gpgsData == nil ) then gpgsData = {} gpgsData.userPref = "logged out" gpgsData.firstTime = true gpgsData.firstCheck = true loadsave.saveTable( gpgsData, "gpgsData.json" ) end -- gpgsData nil conditional END local function gpgsLoginListener( event ) gpgsData.userPref = event.phase loadsave.saveTable( gpgsData, "gpgsData.json" ) end local function gpgsInitListener( event ) if not event.isError then -- Try to automatically log in the user with displaying the login screen if (gpgsData.firstTime == true ) then gpgs.login( { userInitiated = true, listener = gpgsLoginListener } ) gpgsData.firstTime = false loadsave.saveTable( gpgsData, "gpgsData.json" ) else if (gpgsData.userPref == "logged in" ) then gpgs.login( { listener = gpgsLoginListener } ) elseif (gpgsData.userPref == "logged out") then -- DO NOTHING end -- userPref conditional END end -- gpgs.firstTime conditional END end -- event.iserror conditional END end -- gpgsInitListener func END local function licensingListener( event ) if not ( event.isVerified ) then -- Failed to verify app from the Google Play store; print a message gameSettings.googLicensed = false loadsave.saveTable( gameSettings, "gameSettings.json" ) local function onComplete( event ) if ( event.action == "clicked" ) then local i = event.index if ( i == 1 ) then -- Do nothing; dialog will simply dismiss local appStoreTarget = system.getInfo( "targetAppStore" ) if (appStoreTarget == "google") then local options = { supportedAndroidStores = { "google"} } native.showPopup( "appStore", options ) end -- appstore target conditional END end -- i==1 conditional END end -- event.action == clicked conditional END end -- onComplete func END native.showAlert( "Header", "Your message to your user when licensing fails or when app is not licensed", { "OK(button name)" }, onComplete ) if (gpgs.isConnected()) then gpgs.logout() gpgsData.userPref = "logged out" loadsave.saveTable( gpgsData, "gpgsData.json" ) end --logout user if logged in else gameSettings.googLicensed = true loadsave.saveTable( gameSettings, "gameSettings.json" ) gpgs.init( gpgsInitListener ) end -- not event.isverified conditional END end -- licensinglistener func END if (gameSettings.googLicensed == false or gameSettings.googLicensed == nil) then licensing.verify( licensingListener ) else gpgs.init( gpgsInitListener ) end -- licenseCheck if false END
-menuscreen.lua
inGameAnalytics is file name for saving game data. You can choose whatever name you choose for handling your game data. snapshotFileName variable name should be same across all the lua modules in your game.
local loadsave = require("loadsave") local gpgs = require( "plugin.gpgs" ) local gpgsLoggedInButton local gpgsLoggedOutButton local gpgsData local snapshotFileName = "YOU CAN NAME ANYTHING FOR SAVING GAME DATA" local function toggleGPGSToLogin( self, event ) if ( gpgs.isConnected() ) then gpgsLoggedOutButton.isVisible = false gpgsLoggedInButton.isVisible = true local tempInGameAnalytics = loadsave.loadTable( "inGameAnalytics.json" ) -- download snapshot local function gpgsSnapshotOpenForReadListener( event ) if not event.isError then local retrievedData = event.snapshot.contents.read() local inGameAnalytics, pos, msg = json.decode( retrievedData ) if not inGameAnalytics then toast.show('Decoding failed.' ..tostring(pos)..": "..tostring(msg) ) else if ( tempInGameAnalytics.totalGamePlayed \< inGameAnalytics.totalGamePlayed ) then -- save table from the gpgs server loadsave.saveTable( inGameAnalytics, "inGameAnalytics.json" ) else -- upload the offline gameplayed to GPGS server local inGameAnalytics = loadsave.loadTable( "inGameAnalytics.json" ) -- loading inGameAnalytics from phone -- save snapshots local function gpgsSnapshotOpenForSaveListener( event ) if not event.isError then event.snapshot.contents.write( json.encode(inGameAnalytics ) ) -- Write new data as a JSON string into the snapshot gpgs.snapshots.save({ snapshot = event.snapshot, description = "Precious", image = { filename = "SentioTapEmojiShare.png", baseDir = system.ResourceDirectory }, }) end end -- gpgsSnapshotOpenForSaveListener func END gpgs.snapshots.open({ -- Open the save slot filename = snapshotFileName, create = true, -- Create the snapshot if it's not found conflictPolicy = "most recently modified", listener = gpgsSnapshotOpenForSaveListener }) end -- (tempInGameAnalytics.totalGamePlayed \< inGameAnalytics.totalGamePlayed) conditional END end -- not inGameAnalytics conditional END else -- DO NOTHING end -- event.iserror conditional END end --gpgsSnapshotOpenForReadListener func END gpgs.snapshots.open({ filename = snapshotFileName, conflictPolicy = "most recently modified", listener = gpgsSnapshotOpenForReadListener }) end -- gpgs.isConnected() conditional END end -- toggleGPGSToLogin func END local function gpgsLoginListener( event ) gpgsData.userPref = event.phase loadsave.saveTable( gpgsData, "gpgsData.json" ) toggleGPGSToLogin() if event.isError then toast.show('Problem signing in') end -- event.isError conditional END end local function handleButton(event) if (event.target.id == "inGameLeaderboards" and event.phase == "ended") then if (gpgs.isConnected()) then gpgs.leaderboards.show() else gpgs.login( { userInitiated = true, listener = gpgsLoginListener } ) gpgs.leaderboards.show() end -- gpgs is connected check outer END elseif (event.target.id == "inGameAchievements" and event.phase == "ended") then if (gpgs.isConnected()) then gpgs.achievements.show() else gpgs.login( { userInitiated = true, listener = gpgsLoginListener } ) gpgs.achievements.show() end -- gpgs is connected check outer END end -- button selection conditional END end -- handlebutton func END gpgsData = loadsave.loadTable( "gpgsData.json" ) if ( gpgsData == nil ) then gpgsData = {} gpgsData.userPref = "logged out" gpgsData.firstTime = true gpgsData.firstCheck = true loadsave.saveTable( gpgsData, "gpgsData.json" ) end -- gpgsData nil conditional END local function googVerify () if ( ( gameSettings.googLicensed == true ) and (composer.getSceneName( "current" ) == "menuScreen" ) ) then local inGameLeaderboards = widget.newButton( { id = "inGameLeaderboards", sheet = mainMenuButtonsImageSheet, defaultFrame = 5, overFrame = 6, onEvent = handleButton } ) sceneGroup:insert(inGameLeaderboards) inGameLeaderboards.x = display.contentWidth \* 0.18 inGameLeaderboards.y = display.contentHeight \* 0.71 inGameLeaderboards:scale(display.contentWidth \* 0.0001, display.contentWidth \* 0.0001) transition.scaleTo( inGameLeaderboards, { xScale= display.contentWidth \* 0.0014, yScale= display.contentWidth \* 0.0014 ,time=100 } ) local inGameAchievements = widget.newButton( { id = "inGameAchievements", sheet = mainMenuButtonsImageSheet, defaultFrame = 1, overFrame = 2, onEvent = handleButton } ) sceneGroup:insert(inGameAchievements) inGameAchievements.x = display.contentWidth \* 0.82 inGameAchievements.y = display.contentHeight \* 0.71 inGameAchievements:scale(display.contentWidth \* 0.0001, display.contentWidth \* 0.0001) transition.scaleTo( inGameAchievements, { xScale= display.contentWidth \* 0.0014, yScale= display.contentWidth \* 0.0014 ,time=100 } ) local gpgsLoggedInWidth = mainMenuButtonsSheetInfo.sheet.frames[3].width local gpgsLoggedInHeight = mainMenuButtonsSheetInfo.sheet.frames[3].height gpgsLoggedInButton = display.newImageRect(mainMenuButtonsImageSheet, 3, gpgsLoggedInWidth, gpgsLoggedInHeight) gpgsLoggedInButton.isVisible = false gpgsLoggedInButton.x = display.contentWidth \* 0.50 gpgsLoggedInButton.y = display.contentHeight \* 0.70 sceneGroup:insert( gpgsLoggedInButton ) gpgsLoggedInButton:scale(display.contentWidth \* 0.0015, display.contentWidth \* 0.0015) local gpgsLoggedOutWidth = mainMenuButtonsSheetInfo.sheet.frames[4].width local gpgsLoggedOutHeight = mainMenuButtonsSheetInfo.sheet.frames[4].height gpgsLoggedOutButton = display.newImageRect(mainMenuButtonsImageSheet, 4, gpgsLoggedOutWidth, gpgsLoggedOutHeight) gpgsLoggedOutButton.isVisible = false gpgsLoggedOutButton.x = display.contentWidth \* 0.50 gpgsLoggedOutButton.y = display.contentHeight \* 0.70 sceneGroup:insert( gpgsLoggedOutButton ) gpgsLoggedOutButton:scale(display.contentWidth \* 0.0014, display.contentWidth \* 0.0014) local function gpgsDataSync (self, event) gpgsData = loadsave.loadTable( "gpgsData.json" ) if ( gpgs.isConnected() ) then gpgsLoggedInButton.isVisible = true gpgsData.userPref = "logged in" loadsave.saveTable( gpgsData, "gpgsData.json" ) local tempInGameAnalytics = loadsave.loadTable( "inGameAnalytics.json" ) -- download snapshot local function gpgsSnapshotOpenForReadListener( event ) if not event.isError then local retrievedData = event.snapshot.contents.read() local inGameAnalytics, pos, msg = json.decode( retrievedData ) if not inGameAnalytics then toast.show('Decoding failed.' ..tostring(pos)..": "..tostring(msg) ) else if ( tempInGameAnalytics.totalGamePlayed \< inGameAnalytics.totalGamePlayed ) then -- save table from the gpgs server loadsave.saveTable( inGameAnalytics, "inGameAnalytics.json" ) else -- upload the offline gameplayed to GPGS server local inGameAnalytics = loadsave.loadTable( "inGameAnalytics.json" ) -- loading inGameAnalytics from phone -- save snapshots local function gpgsSnapshotOpenForSaveListener( event ) if not event.isError then event.snapshot.contents.write( json.encode(inGameAnalytics ) ) -- Write new data as a JSON string into the snapshot gpgs.snapshots.save({ snapshot = event.snapshot, description = "Precious", image = { filename = "SentioTapEmojiShare.png", baseDir = system.ResourceDirectory }, }) end end -- gpgsSnapshotOpenForSaveListener func END gpgs.snapshots.open({ -- Open the save slot filename = snapshotFileName, create = true, -- Create the snapshot if it's not found conflictPolicy = "most recently modified", listener = gpgsSnapshotOpenForSaveListener }) end -- (tempInGameAnalytics.totalGamePlayed \< inGameAnalytics.totalGamePlayed) conditional END end -- not inGameAnalytics conditional END else -- DO NOTHING end -- event.iserror conditional END end --gpgsSnapshotOpenForReadListener func END gpgs.snapshots.open({ filename = snapshotFileName, conflictPolicy = "most recently modified", listener = gpgsSnapshotOpenForReadListener }) else gpgsLoggedOutButton.isVisible = true end -- gpgsData.userPref == "logged in" and gpgs.isConnected() conditional END outer end -- gpgsDataSync func END if ( gpgsData.firstCheck == false ) then if ( gpgs.isConnected() ) then gpgsLoggedInButton.isVisible = true gpgsDataSync() else gpgsLoggedOutButton.isVisible = true end else gpgsDataSync() gpgsData.firstCheck = false loadsave.saveTable( gpgsData, "gpgsData.json" ) end -- first time game opening check to prevent gpgs button overlap local function onGpgsLoginButtonTap( self, event ) if (gpgs.isConnected()) then --logout gpgs.logout() gpgsData.userPref = "logged out" loadsave.saveTable( gpgsData, "gpgsData.json" ) gpgsLoggedOutButton.isVisible = true gpgsLoggedInButton.isVisible = false else --login gpgs.login( { userInitiated = true, listener = gpgsLoginListener } ) end -- login or logout conditional END return true end -- onGpgsLoginButtonTap func END gpgsLoggedInButton:addEventListener( "tap", onGpgsLoginButtonTap ) gpgsLoggedOutButton:addEventListener( "tap", onGpgsLoginButtonTap ) end -- googLicensed END end -- googVerify func END googVerify() -- googVerify function call
-playscreen.lua
local loadsave = require("loadsave") local gpgs = require( "plugin.gpgs" ) local json = require( "json" ) scene:destroy if ( gpgs.isConnected() ) then gpgs.leaderboards.submit( {leaderboardId = "ID FROM DEV CONSOLE" , score = gamescore variable you want to upload } ) gpgs.achievements.increment( {achievementId = "ID FROM DEV CONSOLE", steps = 1, } ) if (A CONDITIONAL TO ENTER ) then gpgs.achievements.unlock( { achievementId = "ID FROM DEV CONSOLE", } ) end gpgs.achievements.increment( {achievementId = "ID FROM DEV CONSOLE", steps = ANY VARIABLE YOU WERE KEEPING TRACK OF, } ) -- save snapshots local snapshotFileName = "SAME NAME OF THE FILE NAME YOU WANT TO SAVE GAME DATA TO" local function gpgsSnapshotOpenForSaveListener( event ) if not event.isError then event.snapshot.contents.write( json.encode(inGameAnalytics ) ) -- Write new data as a JSON string into the snapshot gpgs.snapshots.save({ snapshot = event.snapshot, description = "Precious", playedTime = system.getTimer(), image = { filename = "IMAGE NAME WITH EXTENSION.png", baseDir = system.ResourceDirectory }, }) end end -- gpgsSnapshotOpenForSaveListener func END gpgs.snapshots.open({ -- Open the save slot filename = snapshotFileName, create = true, -- Create the snapshot if it's not found conflictPolicy = "most recently modified", listener = gpgsSnapshotOpenForSaveListener }) end -- gpgs is connected conditional END
-settings.lua
toast plugin allows you to display toast messages to the user. It needs to be activated from the Corona Marketplace to use it.
local loadsave = require("loadsave") local gpgs = require( "plugin.gpgs" ) local json = require( "json" ) local toast = require('plugin.toast') local gpgsData local snapshotFileName = "SAME NAME OF THE FILE GAME DATA IS BEING SAVED TO" local function gpgsLoginListener( event ) gpgsData.userPref = event.phase loadsave.saveTable( gpgsData, "gpgsData.json" ) if event.isError then toast.show('Problem signing in') end -- event.isError conditional END end local function handleButton (event) if (event.target.id == "cloudUpload" and event.phase == "ended") then -- save snapshots local function gpgsSnapshotOpenForSaveListener( event ) if not event.isError then event.snapshot.contents.write( json.encode(inGameAnalytics ) ) -- Write new data as a JSON string into the snapshot gpgs.snapshots.save({ snapshot = event.snapshot, description = "Precious", image = { filename = "IMAGE NAME WITH EXTENSION.png", baseDir = system.ResourceDirectory }, }) end end -- gpgsSnapshotOpenForSaveListener func END if (gpgs.isConnected()) then gpgs.snapshots.open({ -- Open the save slot filename = snapshotFileName, create = true, -- Create the snapshot if it's not found conflictPolicy = "most recently modified", listener = gpgsSnapshotOpenForSaveListener }) gpgs.snapshots.load( {reload = true, }) gpgs.snapshots.show( {title = "TITLE OF TO SHOW", disableAdd = true, }) else gpgs.login( { userInitiated = true, listener = gpgsLoginListener } ) gpgs.snapshots.open({ -- Open the save slot filename = snapshotFileName, create = true, -- Create the snapshot if it's not found conflictPolicy = "most recently modified", listener = gpgsSnapshotOpenForSaveListener }) gpgs.snapshots.load( {reload = true, }) gpgs.snapshots.show( {title = "TITLE TO SHOW", disableAdd = true, }) end -- gpgs is connected check outer END elseif (event.target.id == "cloudDownload" and event.phase == "ended") then -- download snapshot local function gpgsSnapshotOpenForReadListener( event ) if not event.isError then local retrievedData = event.snapshot.contents.read() local inGameAnalytics, pos, msg = json.decode( retrievedData ) if not inGameAnalytics then toast.show('Decoding failed.' ..tostring(pos)..": "..tostring(msg) ) else loadsave.saveTable( inGameAnalytics, "inGameAnalytics.json" ) end else -- DO NOTHING end -- event.iserror conditional END end --gpgsSnapshotOpenForReadListener func END if (gpgs.isConnected()) then gpgs.snapshots.open({ filename = snapshotFileName, conflictPolicy = "most recently modified", listener = gpgsSnapshotOpenForReadListener }) gpgs.snapshots.load( {reload = true, }) gpgs.snapshots.show( {title = "SentioTap Emoji", disableAdd = true, }) else gpgs.login( { userInitiated = true, listener = gpgsLoginListener } ) gpgs.snapshots.open({ filename = snapshotFileName, conflictPolicy = "most recently modified", listener = gpgsSnapshotOpenForReadListener }) gpgs.snapshots.load( {reload = true, }) gpgs.snapshots.show( {title = "SentioTap Emoji", disableAdd = true, }) end -- gpgs.isconnected END end -- button choosing conditional END end -- handleButton func END scene:show if (gameSettings.googLicensed == true) then local cloudUpload = widget.newButton( { id = "cloudUpload", sheet = settingButtonsImageSheet , defaultFrame = 3, overFrame = 4, label = translations["Save"][language], fontSize = 30, labelYOffset = 90, font = native.systemFont, labelColor = { default={ 0.9, 0, 0.42 }, over={ 0.9, 0, 0.42, 0.6 } }, onEvent = handleButton } ) -- Center the button sceneGroup:insert(cloudUpload) cloudUpload.x = display.contentWidth \* 0.20 cloudUpload.y = display.contentHeight \* 0.30 cloudUpload:scale(display.contentWidth \* 0.002, display.contentWidth \* 0.002) local cloudDownload = widget.newButton( { id = "cloudDownload", sheet = settingButtonsImageSheet , defaultFrame = 1, overFrame = 2, label = translations["Restore"][language], fontSize = 30, labelYOffset = 90, font = native.systemFont, labelColor = { default={ 0.9, 0, 0.42 }, over={ 0.9, 0, 0.42, 0.6 } }, onEvent = handleButton } ) -- Center the button sceneGroup:insert(cloudDownload) cloudDownload.x = display.contentWidth \* 0.78 cloudDownload.y = display.contentHeight \* 0.30 cloudDownload:scale(display.contentWidth \* 0.002, display.contentWidth \* 0.002) end -- googLicensed END