Leaderboard and rating aplication

Hi I am learning programming and i made very simple game. Now I want to make leaderboard on it and something like rating of players of apliccation. This could be local leaderboard or global I really dont know how to start making it.If it would be difficult global, local would be enough. I was looking for any posts and videos but nothing help me. I need to log in Google PlayGame Services?. The next thing is that i made 5 buttons which have value 1-5 which means how user rate aplication. How i can save that votes? Really thanks for all answers. I m newbie in programming so I would love really simple way to make it. Thank you all for reply

There are various approaches to this.

  1. If you just care about the local player on the local device, you can have a table of high scores and save them locally on the device. An great example of this is in Chapter 6 of the Getting Started Guide (https://docs.coronalabs.com/guide/programming/06/index.html). This keeps everything in house, but it’s only for people playing on that device. If you want your app to support multiple users on the same device, you would also need a way to identify the different players which could be as simple as a Player Name.

  2. The next step is to build an online leaderboard where all people playing your app on the many different devices all report their high scores. There is a service DreamIO that offers a free leaderboard service.  http://dreamlo.com/ You can use our network.request() API’s and easily store results and retrieve them and format your own high scores. One of our community developers has built a plugin to assist with using DreamIO. Check it out at:  https://marketplace.coronalabs.com/plugin/dreamlo  Many online services like this come and go. It’s hard to predict how stable it will be. The nice advantage to this is that both Android, iOS, AppleTV, Windows, Android and macOS apps (any platform we can build for) will share a single leaderboard.

  3. GameCenter, Google Play Game Services, Amazon GameCircle. All three of these support leaderboards, but the are by device. In other words your iOS users will have their leaderboards and Android will have their leaderboards and they don’t interchange. Also, these are only for iOS, Google based Android devices and Amazon based Android devices. We don’t have GameCenter support for tvOS and macOS. On Windows we have a plugin for Steam that supports leaderboards, but you have to get your app through their greenlighting process first. If you distribute your apps outside of the Steam eco-system, we don’t have a leaderboard package.

  4. Create your own online service. If you have any PHP/MySQL or .NET experience, you can whip up your own leaderboard service like DreamIO except it’s yours. As long as you maintain your webhost or dedicated virtual host you can run your own servers. Then from Corona it’s just a matter of making the various network.request() API calls to talk to  your server to report high scores and get a list of high scores.

Rating your app could be done with your own servers, but it won’t have much value to you. You want to get people to rate you on the various stores (iTunes, Google Play, Amazon) because the more good ratings you have the higher you will rank in search which translates to more installs.  Look at the native.showPopup() API call. There is an option there that will load the store page for your app so you can send people to directly rate you where it counts!

Rob

great reply Rob, thank you very much. I just made local leaderboard. Could you give me some advice how can I add any name to score ? I made fieldText and made variable from it but how to connect this two things ? I made it like in example which you show me

Can you post the code where you’re creating the native.newTextField()?   Please copy/paste it by clicking on the blue <> button in the editor bar with Bold, Italic etc.

Rob

local composer = require( "composer" ) local scene = composer.newScene() klik2 =audio.loadSound("klik2.mp3"); -- ----------------------------------------------------------------------------------- -- Code outside of the scene event functions below will only be executed ONCE unless -- the scene is removed entirely (not recycled) via "composer.removeScene()" -- ----------------------------------------------------------------------------------- -- ----------------------------------------------------------------------------------- -- Scene event functions -- ----------------------------------------------------------------------------------- name=default -- create() function scene:create( event ) local sceneGroup = self.view local defaultField display.setDefault( "background", 80/255 ) local widget = require( "widget" ) local function textListener( event ) if ( event.phase == "began" ) then -- User begins editing "defaultField" elseif ( event.phase == "ended" or event.phase == "submitted" ) then -- Output resulting text from "defaultField" print( event.target.text ) name=event.target.text elseif ( event.phase == "editing" ) then print( event.target.text ) name=event.target.text end end text1 = display.newText( "choose your name", 160, 50 , native.systemFontBold, 20 ) text1:setFillColor( 1, 180/255, 110/255 ) sceneGroup:insert(text1) function accept(event) if (event.phase == "ended") or (event.phase =="moved") then local alert = native.showAlert( "Player name", "hello ".. name , { "OK"}, onComplete ) composer.setVariable( "score", score ) composer.gotoScene( "highscores", { time=800, effect="crossFade" } ) end end local button1 = widget.newButton { onEvent = buttonHandler1, shape = "roundedRect", label = "accept", width = 200, height = 40, cornerRadius = 20, fillColor = { default={1,0,0}, over={1,0.1,0.7,0.4} }, } button1.x = 150; button1.y = 360 button1:addEventListener( "touch" , accept); sceneGroup:insert(button1) -- Create text field defaultField = native.newTextField( 150, 150, 180, 30 ) sceneGroup:insert(defaultField) defaultField:addEventListener( "userInput", textListener ) end -- show() function scene:show( event ) local sceneGroup = self.view local phase = event.phase if ( phase == "will" ) then -- Code here runs when the scene is still off screen (but is about to come on screen) elseif ( phase == "did" ) then -- Code here runs when the scene is entirely on screen end end -- hide() function scene:hide( event ) local sceneGroup = self.view local phase = event.phase if ( phase == "will" ) then -- Code here runs when the scene is on screen (but is about to go off screen) elseif ( phase == "did" ) then -- Code here runs immediately after the scene goes entirely off screen end end -- destroy() function scene:destroy( event ) local sceneGroup = self.view -- Code here runs prior to the removal of scene's view end -- ----------------------------------------------------------------------------------- -- Scene event function listeners -- ----------------------------------------------------------------------------------- scene:addEventListener( "create", scene ) scene:addEventListener( "show", scene ) scene:addEventListener( "hide", scene ) scene:addEventListener( "destroy", scene ) -- ----------------------------------------------------------------------------------- return scene

have you any idea Rob ?

I thought I responded to this.

You have a few minor issues with the code above, but none of it should prevent this from working. You’re using global variables which isn’t the best thing and one of them you’re naming “name” which is pretty generic.  But the biggest thing is this scene just captures the name and sticks it into a global variable.

function accept(event) if (event.phase == "ended") or (event.phase =="moved") then local alert = native.showAlert( "Player name", "hello ".. name , { "OK"}, onComplete ) composer.setVariable( "score", score ) composer.gotoScene( "highscores", { time=800, effect="crossFade" } ) end end 

In this block of code you don’t want to check for moved. Instead you probably should just use “ended”. Using “moved” will generate multiple alerts and multiple attempts to go to the high score scene. Are you seeing the name populate this alert?

If you localize the name variable, you can share it with the highscores scene using setVariable like you are with score.

Then when you get to your highscore scene, name being a global will be there unless it’s getting stomped on somewhere else.  Also humor me and remove this line:

name=default

which is right above the scene:create() function.  This line isn’t doing anything more than setting name to nil.  (default was never defined).

Rob

Thank you i fixed problems which you said me but still i dont know how to connect my score with name of the player. I need to make second table which will be connected with scoretable ? Do you have any idea ? I want only to end this leaderboard and i will be very happy. Thanks for reply.

local composer = require( "composer" ) local widget = require( "widget" ) local scene = composer.newScene() local json = require( "json" ) local scoresTable = {} local filePath = system.pathForFile( "scores.json", system.DocumentsDirectory ) local function loadScores() local file = io.open( filePath, "r" ) if file then local contents = file:read( "\*a" ) io.close( file ) scoresTable = json.decode( contents ) end if ( scoresTable == nil or #scoresTable == 0 ) then scoresTable = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } end end local function saveScores() for i = #scoresTable, 11, -1 do table.remove( scoresTable, i ) end local file = io.open( filePath, "w" ) if file then file:write( json.encode( scoresTable ) ) io.close( file ) end end -- create() function scene:create( event ) local sceneGroup = self.view -- Load the previous scores loadScores() -- Insert the saved score from the last game into the table, then reset it table.insert( scoresTable, composer.getVariable( "koniec" ) ) composer.setVariable( "koniec", 0 ) -- Sort the table entries from highest to lowest local function compare( a, b ) return a \> b end table.sort( scoresTable, compare ) -- Save the scores saveScores() local highScoresHeader = display.newText("Ranking", 100, 0, native.systemFont, 44 ) sceneGroup:insert(highScoresHeader) for i = 1, 10 do if ( scoresTable[i] ) then local yPos = 20 + ( i \* 36 ) local rankNum = display.newText( sceneGroup, i .. ")", display.contentCenterX-100, yPos, native.systemFont, 26 ) rankNum:setFillColor( 0.8 ) rankNum.anchorX = 1 local thisScore = display.newText( sceneGroup, scoresTable[i], display.contentCenterX-80, yPos, native.systemFont, 26 ) thisScore.anchorX = 0 local playerScore = display.newText( sceneGroup, name, display.contentCenterX-40, yPos, native.systemFont, 26 ) thisScore.anchorX = 0 end end print ( table.concat(scoresTable, ", ") ) function zmiana1(event) if (event.phase == "ended") or (event.phase =="moved") then audio.play(klik2); composer.gotoScene( "main1", { time=800, effect="crossFade" } ) end end local button1 = widget.newButton { onEvent = buttonHandler1, shape = "roundedRect", label = "menu", width = 200, height = 40, cornerRadius = 20, fillColor = { default={1,0,0}, over={1,0.1,0.7,0.4} }, } button1.x = 150; button1.y = 450 button1:addEventListener( "touch" , zmiana1); sceneGroup:insert(button1) end -- show() function scene:show( event ) local sceneGroup = self.view local phase = event.phase if ( phase == "will" ) then -- composer.removeScene( "nazwa" ) -- Code here runs when the scene is still off screen (but is about to come on screen) elseif ( phase == "did" ) then composer.removeScene( "nazwa" ) -- Code here runs when the scene is entirely on screen end end -- hide() function scene:hide( event ) local sceneGroup = self.view local phase = event.phase if ( phase == "will" ) then -- Code here runs when the scene is on screen (but is about to go off screen) elseif ( phase == "did" ) then -- Code here runs immediately after the scene goes entirely off screen end end -- destroy() function scene:destroy( event ) local sceneGroup = self.view -- Code here runs prior to the removal of scene's view end -- ----------------------------------------------------------------------------------- -- Scene event function listeners -- ----------------------------------------------------------------------------------- scene:addEventListener( "create", scene ) scene:addEventListener( "show", scene ) scene:addEventListener( "hide", scene ) scene:addEventListener( "destroy", scene ) -- ----------------------------------------------------------------------------------- return scene

You can’t use a simple array of numbers any more. Instead you probably should change your table from:

 if ( scoresTable == nil or #scoresTable == 0 ) then scoresTable = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } end

to:

if ( scoresTable == nil or #scoresTable == 0 ) then scoresTable = {} scoresTable[1] = { name="", score = 0 } scoresTable[2] = { name="", score = 0 } scoresTable[3] = { name="", score = 0 } scoresTable[4] = { name="", score = 0 } scoresTable[5] = { name="", score = 0 } scoresTable[6] = { name="", score = 0 } scoresTable[7] = { name="", score = 0 } scoresTable[8] = { name="", score = 0 } scoresTable[9] = { name="", score = 0 } scoresTable[10] = { name="", score = 0 } end

Now you have gone from each entry just being a number to each entry being a data record with multiple values. There are quite a few other changes needed so bear with me. Next you have to change how you save the score to the table from:

table.insert( scoresTable, composer.getVariable( "koniec" ) )

to

table.insert( scoresTable, { score = composer.getVariable( "koniec" ), name = composer.getVariable( "name" ) } )

assuming you’re using the composer variable “name” to hold the name.

Next you have to change the sort function which is expecting just numbers from:

 -- Sort the table entries from highest to lowest local function compare( a, b ) return a \> b end

to:

 -- Sort the table entries from highest to lowest local function compare( a, b ) return a.score \> b.score end

Of course you will have to update your output to include the name as well as getting the name and score from the score table since it’s now an array of sub-tables. That’s too much code for me to work out for you. After all you’re doing this to learn!

Finally the code I typed above is untested and I may have typos.

Rob

There are various approaches to this.

  1. If you just care about the local player on the local device, you can have a table of high scores and save them locally on the device. An great example of this is in Chapter 6 of the Getting Started Guide (https://docs.coronalabs.com/guide/programming/06/index.html). This keeps everything in house, but it’s only for people playing on that device. If you want your app to support multiple users on the same device, you would also need a way to identify the different players which could be as simple as a Player Name.

  2. The next step is to build an online leaderboard where all people playing your app on the many different devices all report their high scores. There is a service DreamIO that offers a free leaderboard service.  http://dreamlo.com/ You can use our network.request() API’s and easily store results and retrieve them and format your own high scores. One of our community developers has built a plugin to assist with using DreamIO. Check it out at:  https://marketplace.coronalabs.com/plugin/dreamlo  Many online services like this come and go. It’s hard to predict how stable it will be. The nice advantage to this is that both Android, iOS, AppleTV, Windows, Android and macOS apps (any platform we can build for) will share a single leaderboard.

  3. GameCenter, Google Play Game Services, Amazon GameCircle. All three of these support leaderboards, but the are by device. In other words your iOS users will have their leaderboards and Android will have their leaderboards and they don’t interchange. Also, these are only for iOS, Google based Android devices and Amazon based Android devices. We don’t have GameCenter support for tvOS and macOS. On Windows we have a plugin for Steam that supports leaderboards, but you have to get your app through their greenlighting process first. If you distribute your apps outside of the Steam eco-system, we don’t have a leaderboard package.

  4. Create your own online service. If you have any PHP/MySQL or .NET experience, you can whip up your own leaderboard service like DreamIO except it’s yours. As long as you maintain your webhost or dedicated virtual host you can run your own servers. Then from Corona it’s just a matter of making the various network.request() API calls to talk to  your server to report high scores and get a list of high scores.

Rating your app could be done with your own servers, but it won’t have much value to you. You want to get people to rate you on the various stores (iTunes, Google Play, Amazon) because the more good ratings you have the higher you will rank in search which translates to more installs.  Look at the native.showPopup() API call. There is an option there that will load the store page for your app so you can send people to directly rate you where it counts!

Rob

great reply Rob, thank you very much. I just made local leaderboard. Could you give me some advice how can I add any name to score ? I made fieldText and made variable from it but how to connect this two things ? I made it like in example which you show me

Can you post the code where you’re creating the native.newTextField()?   Please copy/paste it by clicking on the blue <> button in the editor bar with Bold, Italic etc.

Rob

local composer = require( "composer" ) local scene = composer.newScene() klik2 =audio.loadSound("klik2.mp3"); -- ----------------------------------------------------------------------------------- -- Code outside of the scene event functions below will only be executed ONCE unless -- the scene is removed entirely (not recycled) via "composer.removeScene()" -- ----------------------------------------------------------------------------------- -- ----------------------------------------------------------------------------------- -- Scene event functions -- ----------------------------------------------------------------------------------- name=default -- create() function scene:create( event ) local sceneGroup = self.view local defaultField display.setDefault( "background", 80/255 ) local widget = require( "widget" ) local function textListener( event ) if ( event.phase == "began" ) then -- User begins editing "defaultField" elseif ( event.phase == "ended" or event.phase == "submitted" ) then -- Output resulting text from "defaultField" print( event.target.text ) name=event.target.text elseif ( event.phase == "editing" ) then print( event.target.text ) name=event.target.text end end text1 = display.newText( "choose your name", 160, 50 , native.systemFontBold, 20 ) text1:setFillColor( 1, 180/255, 110/255 ) sceneGroup:insert(text1) function accept(event) if (event.phase == "ended") or (event.phase =="moved") then local alert = native.showAlert( "Player name", "hello ".. name , { "OK"}, onComplete ) composer.setVariable( "score", score ) composer.gotoScene( "highscores", { time=800, effect="crossFade" } ) end end local button1 = widget.newButton { onEvent = buttonHandler1, shape = "roundedRect", label = "accept", width = 200, height = 40, cornerRadius = 20, fillColor = { default={1,0,0}, over={1,0.1,0.7,0.4} }, } button1.x = 150; button1.y = 360 button1:addEventListener( "touch" , accept); sceneGroup:insert(button1) -- Create text field defaultField = native.newTextField( 150, 150, 180, 30 ) sceneGroup:insert(defaultField) defaultField:addEventListener( "userInput", textListener ) end -- show() function scene:show( event ) local sceneGroup = self.view local phase = event.phase if ( phase == "will" ) then -- Code here runs when the scene is still off screen (but is about to come on screen) elseif ( phase == "did" ) then -- Code here runs when the scene is entirely on screen end end -- hide() function scene:hide( event ) local sceneGroup = self.view local phase = event.phase if ( phase == "will" ) then -- Code here runs when the scene is on screen (but is about to go off screen) elseif ( phase == "did" ) then -- Code here runs immediately after the scene goes entirely off screen end end -- destroy() function scene:destroy( event ) local sceneGroup = self.view -- Code here runs prior to the removal of scene's view end -- ----------------------------------------------------------------------------------- -- Scene event function listeners -- ----------------------------------------------------------------------------------- scene:addEventListener( "create", scene ) scene:addEventListener( "show", scene ) scene:addEventListener( "hide", scene ) scene:addEventListener( "destroy", scene ) -- ----------------------------------------------------------------------------------- return scene

have you any idea Rob ?

I thought I responded to this.

You have a few minor issues with the code above, but none of it should prevent this from working. You’re using global variables which isn’t the best thing and one of them you’re naming “name” which is pretty generic.  But the biggest thing is this scene just captures the name and sticks it into a global variable.

function accept(event) if (event.phase == "ended") or (event.phase =="moved") then local alert = native.showAlert( "Player name", "hello ".. name , { "OK"}, onComplete ) composer.setVariable( "score", score ) composer.gotoScene( "highscores", { time=800, effect="crossFade" } ) end end 

In this block of code you don’t want to check for moved. Instead you probably should just use “ended”. Using “moved” will generate multiple alerts and multiple attempts to go to the high score scene. Are you seeing the name populate this alert?

If you localize the name variable, you can share it with the highscores scene using setVariable like you are with score.

Then when you get to your highscore scene, name being a global will be there unless it’s getting stomped on somewhere else.  Also humor me and remove this line:

name=default

which is right above the scene:create() function.  This line isn’t doing anything more than setting name to nil.  (default was never defined).

Rob

Thank you i fixed problems which you said me but still i dont know how to connect my score with name of the player. I need to make second table which will be connected with scoretable ? Do you have any idea ? I want only to end this leaderboard and i will be very happy. Thanks for reply.

local composer = require( "composer" ) local widget = require( "widget" ) local scene = composer.newScene() local json = require( "json" ) local scoresTable = {} local filePath = system.pathForFile( "scores.json", system.DocumentsDirectory ) local function loadScores() local file = io.open( filePath, "r" ) if file then local contents = file:read( "\*a" ) io.close( file ) scoresTable = json.decode( contents ) end if ( scoresTable == nil or #scoresTable == 0 ) then scoresTable = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } end end local function saveScores() for i = #scoresTable, 11, -1 do table.remove( scoresTable, i ) end local file = io.open( filePath, "w" ) if file then file:write( json.encode( scoresTable ) ) io.close( file ) end end -- create() function scene:create( event ) local sceneGroup = self.view -- Load the previous scores loadScores() -- Insert the saved score from the last game into the table, then reset it table.insert( scoresTable, composer.getVariable( "koniec" ) ) composer.setVariable( "koniec", 0 ) -- Sort the table entries from highest to lowest local function compare( a, b ) return a \> b end table.sort( scoresTable, compare ) -- Save the scores saveScores() local highScoresHeader = display.newText("Ranking", 100, 0, native.systemFont, 44 ) sceneGroup:insert(highScoresHeader) for i = 1, 10 do if ( scoresTable[i] ) then local yPos = 20 + ( i \* 36 ) local rankNum = display.newText( sceneGroup, i .. ")", display.contentCenterX-100, yPos, native.systemFont, 26 ) rankNum:setFillColor( 0.8 ) rankNum.anchorX = 1 local thisScore = display.newText( sceneGroup, scoresTable[i], display.contentCenterX-80, yPos, native.systemFont, 26 ) thisScore.anchorX = 0 local playerScore = display.newText( sceneGroup, name, display.contentCenterX-40, yPos, native.systemFont, 26 ) thisScore.anchorX = 0 end end print ( table.concat(scoresTable, ", ") ) function zmiana1(event) if (event.phase == "ended") or (event.phase =="moved") then audio.play(klik2); composer.gotoScene( "main1", { time=800, effect="crossFade" } ) end end local button1 = widget.newButton { onEvent = buttonHandler1, shape = "roundedRect", label = "menu", width = 200, height = 40, cornerRadius = 20, fillColor = { default={1,0,0}, over={1,0.1,0.7,0.4} }, } button1.x = 150; button1.y = 450 button1:addEventListener( "touch" , zmiana1); sceneGroup:insert(button1) end -- show() function scene:show( event ) local sceneGroup = self.view local phase = event.phase if ( phase == "will" ) then -- composer.removeScene( "nazwa" ) -- Code here runs when the scene is still off screen (but is about to come on screen) elseif ( phase == "did" ) then composer.removeScene( "nazwa" ) -- Code here runs when the scene is entirely on screen end end -- hide() function scene:hide( event ) local sceneGroup = self.view local phase = event.phase if ( phase == "will" ) then -- Code here runs when the scene is on screen (but is about to go off screen) elseif ( phase == "did" ) then -- Code here runs immediately after the scene goes entirely off screen end end -- destroy() function scene:destroy( event ) local sceneGroup = self.view -- Code here runs prior to the removal of scene's view end -- ----------------------------------------------------------------------------------- -- Scene event function listeners -- ----------------------------------------------------------------------------------- scene:addEventListener( "create", scene ) scene:addEventListener( "show", scene ) scene:addEventListener( "hide", scene ) scene:addEventListener( "destroy", scene ) -- ----------------------------------------------------------------------------------- return scene

You can’t use a simple array of numbers any more. Instead you probably should change your table from:

 if ( scoresTable == nil or #scoresTable == 0 ) then scoresTable = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } end

to:

if ( scoresTable == nil or #scoresTable == 0 ) then scoresTable = {} scoresTable[1] = { name="", score = 0 } scoresTable[2] = { name="", score = 0 } scoresTable[3] = { name="", score = 0 } scoresTable[4] = { name="", score = 0 } scoresTable[5] = { name="", score = 0 } scoresTable[6] = { name="", score = 0 } scoresTable[7] = { name="", score = 0 } scoresTable[8] = { name="", score = 0 } scoresTable[9] = { name="", score = 0 } scoresTable[10] = { name="", score = 0 } end

Now you have gone from each entry just being a number to each entry being a data record with multiple values. There are quite a few other changes needed so bear with me. Next you have to change how you save the score to the table from:

table.insert( scoresTable, composer.getVariable( "koniec" ) )

to

table.insert( scoresTable, { score = composer.getVariable( "koniec" ), name = composer.getVariable( "name" ) } )

assuming you’re using the composer variable “name” to hold the name.

Next you have to change the sort function which is expecting just numbers from:

 -- Sort the table entries from highest to lowest local function compare( a, b ) return a \> b end

to:

 -- Sort the table entries from highest to lowest local function compare( a, b ) return a.score \> b.score end

Of course you will have to update your output to include the name as well as getting the name and score from the score table since it’s now an array of sub-tables. That’s too much code for me to work out for you. After all you’re doing this to learn!

Finally the code I typed above is untested and I may have typos.

Rob