Adding Achievements to Your Corona Game or App Using OpenFeint & Papaya Mobile

Introduction

Achievements within games can help give incentives for players to learn how to play, give them confirmation they’re on the right track, and reward their accomplishments. They can also reward experimental behavior, create engagement for challenging or secret achievements, and can even create games within games. To paraphrase Jesse Schell, you can create the meta game, experiences that exist on top of other experiences.

Using one of the games I’m creating, this article will show you all the steps you need to do, plus Lua code, to get achievements in your Corona app or game via OpenFeint and Papaya Mobile. Additionally, I’ll cover mocking Achievements, as well as how to write code that’ll work for both OpenFeint & Papaya Mobile, and having them work offline.

Short Version

Log into OpenFeint.com (Login at the bottom):

Click the Features Tab:

Click “Achievements”:

Click “Add Achievement” button:

Fill our your achievement details and click the “Save Achievement” button:

Find your achievement in the list and copy the ID to the clipboard:

Implement in code:

gameNetwork.request("unlockAchievement", 1269182)

Done!

Start Anew?

If you don’t have an OpenFeint and/or Papaya Mobile account, there are a few steps you need to do first. You do not have to be complete with your game yet. OpenFeint & Papaya Mobile membership do not cost money.

  1. Get an OpenFeint Developer Account.
  2. Get a Papaya Mobile Developer Account.
  3. Register your Game with OpenFeint.
  4. Register your Game with Papaya Mobile.
  5. Copy your OpenFeint and/or Papaya Mobile secret info into your code.

The above takes like 5 minutes; it’s really easy. Both have great CMS’.

Remember, you cannot currently use both OpenFeint and Papaya in Corona, only one or the other. Here’s an example that initializes one or the other:

local mode = "papaya"
if mode == "papaya" then
	gameNetwork.init("papaya", "secrectCodeHere")
elseif mode == "openfeint" then
	gameNetwork.init("openfeint",
			"SecretCode",
			"SecretCodeHereToo",
			"JesterXL: Invaded Skies",
			"YetAnotherCode")
end

Other Notes

Beyond having to choose which service to use, you should make that gameNetwork.init() call as soon as possible. It’s an asynchronous call, and you have no way of knowing in code if it worked; there is no callback function (here be race conditions).

The user will see a message like so on the bottom if gameNetwork.init() worked for OpenFeint:

When a user unlocks an achievement, you’ll see the achievement icon, name, and how many points it was worth:

If they haven’t logged into OpenFeint yet, or the phone doesn’t remember their credentials, it may say a number instead of a name, like mine says “JesterXL”. Your game/app should provide a button to login to OpenFeint/PapayaMobile. Remember, you can also go directly to certain areas as well such as leader boards and high scores.

Logging In

If the user hasn’t signed up or isn’t logged in yet, they’ll see a screen like this when they do:

Once logged in:

And the achievements screen:

Caveats

Although I haven’t confirmed this with my own research of statistics, I have heard that user logins with non-insanely popular games are low. Really low. This means that many most users of your games do not log into these services. Additionally, you do not have access to the emails OpenFeint & Papaya Mobile are collecting. These can be valuable for both accurate analytics, as well as present & future marketing opportunities.

This also places a burden on you as a game developer to assume the user may not wish to login. You thus need to provide scores & achievements (assuming your supporting the bare minimum of both Android & iOS) as an option in your game/app design and not force it upon the user. At the same time you want to encourage users to do so for the social aspects & rewards so need to make it easy for them to do so.

From a coding perspective, you have a few challenges as well. First, as mentioned before, you don’t know if a gameNetwork.init() succeeded or failed. You also cannot “get” data from the services to know what the user’s high scores are, nor what achievements have currently been attained. OpenFeint for iOS has the ability to upload and download arbitrary blobs to get around this, but it doesn’t work on Android, and Papaya Mobile has no such equivalent. Finally, the user see’s none of these features if they do not log in… which cheapens the experience at the cost of your hard work integrating them. Worse, at the time of this writing, you can’t even test these features in the simulator, only on an actual device.

I’m intentionally ignoring the fact that you need to sync high scores & achievement info between both services if you intend to support both (as I found it easy to do so).

Mocking Achievements

I created a project called Corona Mock for gameNetwork on Github. I got so frustrated I couldn’t test my achievements on the simulator. I was as also concerned that if users never logged in, all my work would be for naught. I wanted a fallback GUI that’d work without a server, yet the same code would work for either gameNetwork as well as offline. Here’s a video of it in action working in the simulator:

I’ll show some code below that allows you to mock or “fake” achievements in the Simulator. It solves the following problems:

  1. You can test your code as you develop vs. having to deploy to a device every time. For achievements that require complexity to reach, this is a great time saver.
  2. You can write the same achievement code and it’ll work on either OpenFeint, Papaya Mobile, or whatever other services arrive in the future.
  3. You can optionally add better designs and use as your own achievement system if the user chooses not to sign up/login to OpenFeint/Papaya Mobile.

#3 is key here. While #1 ensures you don’t go nuts, and #2 helps life be better, you want to ensure the user still has a good experience when they don’t opt-in to the above services.

There are 3 things I recommend for mocking achievements.

  • Create a constants variable to hold your achievements
  • Create an Achievements class to wrap the gameNetwork calls, hold game state on what achievements are unlocked, and save state when your game/app exits.
  • Create service classes to save score & achievement game state by reading/writing your Lua tables to JSON to/from the device

Constants

Here are 4 achievements in a constants file:

constants.achievements 							= {

	liftOff 		= {pid = 683, oid = 1269172, image = "achievement_Lift_Off.png", name="Lift Off", unlocked = false},
	firstBlood 		= {pid = 684, oid = 1269182, image = "achievement_First_Blood.png", name="First Blood", unlocked = false},
	dogFighter 		= {pid = 685, oid = 1269192, image = "achievement_Dogfighter.png", name="Dogfighter", unlocked = false},
	veteranPilot 		= {pid = 686, oid = 1269202, image = "achievement_Veteran_Pilot.png", name="Veteran Pilot", unlocked = false},
	zeeMissile		= {pid = -1,  oid = 1272282, image = nil, name="Zee Missile, It Does Nuffing!", unlocked = false}
}

Notice a few things. First, we designate the name of the achievement as the key in the table using camel-case. So, the “First Blood” achievement, we’ll refer to in Lua as constants.achievements.firstBlood. Also, the pid and oid; those are the “Papaya Mobile Achievement ID” and “OpenFeint Achievement ID”; same achievement, just different id depending on the service you’re using. The image is the same image you’ve uploaded to the OpenFeint and/or Papaya Mobile services, you’re just packaging it with your game as well just in case. The name is the nicely formatted name you’ll show the user. Finally, the unlocked variable means if the user has unlocked that achievement yet or not.

Cool, so now that you have your achievements defined. If the id’s ever change, or you decide to name it something else, your code doesn’t have to change because it’ll just reference the achievement table name. Let’s use them.

Achievements Wrapper

For a short example, you can see check out the Corona Mock for gameNetwork example project I have on Github. Otherwise, you can see the full, in-progress class in one of my games, but for now, I just want to show you 1 function in particular:

function AchievementsProxy:unlock(achievementVO)
	if achievementVO.unlocked == true then
		return true
	end

	achievementVO.unlocked = true

	if self.useMock == true then
		self.mock:showAchievement(achievementVO.image, achievementVO.name)
		return true
	end

	local idFieldName
	if self.mode == "openfeint" then
		idFieldName = "oid"
	elseif self.mode == "papayamobile" then
		idFieldName = "pid"
	end

	gameNetwork.request("unlockAchievement", achievementVO[idFieldName])
end

The achievementVO in this case is one of your achievements in your constants. Anywhere in your game/app you can call this Singleton function and unlock an achievement, whether mocking or on the device. Notice:

  • if the achievement is already unlocked, it aborts the function call
  • it unlocks the achievement
  • if you’re mocking achievements, it’ll show a fake OpenFeint/Papaya Mobile GUI so you can test in the simulator and see what gameNetwork.init() is called as well as when achievements are unlocked.
  • it determines what service your using and finds the appropriate id to send and makes the unlock achievement call

While useMock is true when running in the simulator and false when running on a device, you’re welcome to set true at any time. I’m just detecting the platform in the beginning and setting the value like so:

local platform = system.getInfo("platformName")
if platform == "Android" or platform == "iPhone OS" then
	AchievementsProxy.useMock = false
else
	AchievementsProxy.useMock = true
end

You can then litter your code with calls to the AchievementProxy singleton class, knowing it’ll work whether your in the simulator, or not, and optionally if the user has signed into a gameNetwork or not, like so:

AchievementsProxy:unlock(constants.achievements.veteranPilot)

Loading and Saving Game State

That’s all well and good, but when happens when the user quits your game and starts back up again? The constants are the same. No memory of what achievements were unlocked are actually saved. If the user logs into OpenFeint/Papaya Mobile, sure, their servers take care of all that… but what if the user never does? How do you make sure the user’s progress is remembered?

Adding this method to be called at the end of AchievementsProxy’ init method:

function AchievementsProxy:loadAchievements()
	local savedAchievements = LoadAchievementsService:new("savedAchievements.json")
	if savedAchievements == nil then
		return false
	end
	for savedAchievementName,savedAchievement in pairs(savedAchievements) do
		self.achievements[savedAchievementName] = savedAchievement.unlocked
	end
end

This function loads JSON from the phone, parses it into an achievements object (exactly like what’s in your constants file), and updates whether it’s been unlocked or not.

When you’re game/app shuts down, make sure to save them out, like so:

function AchievementsProxy:saveAchievements()
	SaveAchievementsService:new("savedAchievements.json", self.achievements)
end

All these service classes do is read & write JSON (a text format) to your phone. Johnathan Beebe has a good tutorial on working with JSON in a blog post that I basically copied most of my code from so nothing fancy here. The LoadAchievementsService class is just a wrapper around the function Jonathan gave:

require "json"

LoadLevelService = {}
LoadLevelService.loader = nil

function LoadLevelService:new(jsonFileName)

	local loader

	if LoadLevelService.loader == nil then
		LoadLevelService.loader = {}

		loader = LoadLevelService.loader

		-- jsonFile() loads json file & returns contents as a string
		function loader:jsonFile( filename, base )
			-- set default base dir if none specified
			if not base then base = system.ResourceDirectory; end

			-- create a file path for corona i/o
			local path = system.pathForFile( filename, base )

			-- will hold contents of file
			local contents

			-- io.open opens a file at path. returns nil if no file found
			local file = io.open( path, "r" )
			if file then
			   -- read all contents of file into a string
			   contents = file:read( "*a" )
			   io.close( file )	-- close the file after using it
			end

			return contents
		end
	else
		loader = LoadLevelService.loader
	end

	local jsonString = loader:jsonFile(jsonFileName)
	local level = json.decode(jsonString)

	return level
end

return LoadLevelService

I’ll leave the SaveAchievementsService class as a reader exercise.

Application State

That’s it. As you can see upon loading, you load the current state of the game from a saved JSON file on the phone, if any. Any time the user unlocks an achievement, it’ll update the achievement’s unlocked property to true. When the game/app ends, it’ll save that state to the phone in a JSON text file. When the game/app boots back up, it’ll read that file, update the in RAM achievements to what they were when the user last used it.

This ensures if the user does choose to sign up/log into OpenFeint/Papaya Mobile, they still get a good user experience using your achievements. You’ll just need to make a better design for my Mocking class, and perhaps make a list screen that shows the current achievements they’ve unlocked.

Flash Developer?

OpenFeint, Papaya Mobile, Urban Airship, and others clearly don’t care about Flash, doesn’t even seem on their radar. While Adobe is making a big gaming push as of late with their gaming center and recent Flash Player 11 features, these services don’t even offer API’s to easily use them. I even remember OpenFeint about a year ago have a private beta for their REST API… which I can no longer find anywhere.

We’ll have to wait for some enterprising dev to wire them up and expose them via Native Extensions.

I do know SuperRewards has some form of Flash and API support.

Anyway, if you’re a Flash Dev and feeling left out, check out Scoreoid; it was made for Flash.

Conclusions

Achievements add guidance, are a progress indicator, and allow recognition. Their an incentive for learning, confirmation the player is on the right track, and reward accomplishment. While services like OpenFeint & Papaya Mobile make it easy to create & code them in your Corona SDK game or app, based on the metrics I’ve heard, you should remember while you should make it easy for users to sign in, don’t force it upon them. With a little abstraction you can ensure achievements still work in your game and only optionally use those services if your user wishes to do so while reducing the amount of work you need to do.

Achievement Unlocked: You read this whole thing.

6 Replies to “Adding Achievements to Your Corona Game or App Using OpenFeint & Papaya Mobile”

  1. JesterXL,

    I’m the marketing manager @Papaya. Thanks for creating such a detailed step by step walkthrough. I’d love to chat with you or put you in touch with our Game Integration Engineer to discuss how we can improve. I’m also curious to see if you have tried adding any of our other features like challenges, recommend a game, chat rooms, or notifications? Shoot me over an email when you get a chance. Our goal is to make life as easy as possible for developers.

    Thanks,

    Justin
    justinmauldin@papayamobile.com

    1. I love y’alls web interface. So far, no problems. I’ve just been doing scores & achievements to ensure parity between both Android and iOS. I thought about diving into Android specific features, though, so when I get time to play after RIA Unleashed, I’ll give ’em a whirl and let you know.

      I will say I have the same problems with OpenFeint that I have with Papaya and that’s the whole black box thing. As a software developer who constantly utilizes web API’s, it’s a little strange that almost all of Corona’s API’s are write only; I can’t read anything to update the client and do a custom GUI. I get why, and maybe that’s just how it is and I need to deal.

      Regarding improvement, I can only answer with 1 request: Support for Adobe AIR.

      I can write a more formalized, and helpful-to-a-developer response in email this weekend. Thanks for taking the time to come here!

  2. Wow, thanks for such a greatly detailed step-by-step guide for integrating these leaderboard options into Corona!! My buddy and I were JUST talking about getting the ball rolling on integrating OpenFeint yesterday so this is a timely help!!

    Best,
    Mario

  3. Thanks for link and info about Scoroied, I would add that Scoreoid was not just made for Flash but is truly cross platform. The goal allowing everyone to use it including emerging markets like HTML5 and TV’s.

Comments are closed.