📜 ⬆️ ⬇️

Unity3d: Experiments with the Social Interface

A modern mobile game is difficult to imagine without social integration, common leaderboards and achievements (achievements). In order to keep up with the trends, I decided to integrate Game Center and Play Services for iOS and Android versions of my game.

Since I am developing a game in my spare time as a hobby, then thoughts about buying plug-ins, for example, prime31, were dropped immediately. The choice fell on the interface Social, which is part of Unity. The intrigue is felt around this package: the practical lack of reference information leads to two thoughts: either the interface is very simple, or not suitable for use. So, it's time to figure it out.

First of all, it turned out that this interface has an implementation only for iOS, and for Android it is, in fact, a pure interface.

The reluctance to buy plugins and the desire to add a table of records led me here: https://github.com/playgameservices/play-games-plugin-for-unity . This is a free Android plugin from Google that fills the Social interface with a vibrant implementation and keeps the wallet's thickness at the same level. The plugin has a frightening version of 0.9, but this does not affect its performance, but there is no part of the functional, which will be discussed further.
')
Determined and faith in success, I began to prepare projects in iTunes Connect and Google Developer Console - at this stage there are no problems, both platforms have almost identical settings of the tables of records and achievements, and an abundance of background information does not go astray.

There are a couple of points worth paying attention to:

Google Developer Console generates IDs of achievements and leaderboards itself, and in iTunes Connect they need to be set independently, therefore for greater compatibility of the future code it is convenient to start from Google, and then customize the project under iOS in the same way, copying the same identifiers.

When working with Play Services in the Google Developer Console, as well as adding alpha / beta versions of the game, Google strongly suggests making “publishing” achievements and leaderboards - you shouldn’t agree to this until the release itself, because after “publishing” you lose the ability to delete achievements and high score tables, as well as edit such important parameters as the number of steps necessary to perform iterative achievements.

I created High Scores leaderboards and a minimal set of achievements (for Google, these are five positions), so even if you are not going to use them, you have to squeeze something out of yourself. Apple has no such limit, but once the achievements have been created - there is nothing difficult to copy them.

Next, install the plugin for Android. In the Unity menu, select the Assets / Import Package / Custom Package and deploy the plugin to your project. After the successful import, the Google Play Games item appears in the menu, select the Android Setup sub-item ..., enter the application ID, which can be found in the Game Services section of the Google Developer Console and we get a plug-in ready for use.

Now everything is ready to write a couple of lines of code (C #) in Unity. First of all, you need to make preliminary settings for iOS and Android, as well as log in:

#if UNITY_ANDROID //   Google Play Games,     Android, //    Social    GooglePlayGames.PlayGamesPlatform.Activate(); #endif #if UNITY_IPHONE //       iOS   ,             UnityEngine.SocialPlatforms.GameCenter.GameCenterPlatform.ShowDefaultAchievementCompletionBanner(true); #endif Social.localUser.Authenticate(onProcessAuthentication); //  ,    //    , Social.localUser     private void onProcessAuthentication(bool success) { Debug.Log("onProcessAuthentication: " + success); } 

After successful authorization, we can work with leaderboards and achievements.

When working with leaderboards, I decided that I first need to get the current player record - this is necessary so that you can compare the old record with the new one and if the player reaches the new top, display the message “Congratulations! New Top: XXX. To do this, I wrote the following code, which creates a table, sets a filter for players for which we need data (only our player), and gets the current player record in case of success:

 string[] userIds = new string[] { Social.localUser.id }; highScoresBoard = Social.CreateLeaderboard(); highScoresBoard.id = "LEADERBOARD_ID"; highScoresBoard.SetUserFilter(userIds); highScoresBoard.LoadScores(onLeaderboardLoadComplete); private void onLeaderboardLoadComplete(bool success) { Debug.Log("onLeaderboardLoadComplete: " + success); if (success) { long score = highScoresBoard.localUserScore.value; } } 

Sending the current progress is as follows (and we don’t necessarily care that the new result may be less than the old one - in this case, the data will be discarded by the server):

 public void reportScore(long score) { if (Social.localUser.authenticated) { Social.ReportScore(score, "LEADERBOARD_ID", onReportScore); } } private void onReportScore(bool result) { Debug.Log("onReportScore: " + success); } 

After testing this code, there was a problem - it does not work under Android, because In the plugin there is no implementation of this function - here it is, the beauty of version 0.9.

But this is not a reason for frustration, having thought it over, I came to the conclusion that there is no need to get the current player record, it is enough to store it locally. The fact is that if a player changes the device, or simply deletes your game and returns to it after some time, it will be more pleasant for him to beat his own local record again and again. The record in the leaderboards will always contain the maximum progress of the player, and to beat him the player can take a long time, which ultimately can reduce the player's motivation. So I decided to abandon the global record, and added the following code to authorization:

 public long highScore = 0; private void onProcessAuthentication(bool success) { Debug.Log("onProcessAuthentication: " + success); if (success) { if (PlayerPrefs.HasKey("high_score")) highScore = (long)PlayerPrefs.GetInt("high_score"); } } 

Sending progress to the server took the form:
 public void reportScore(long score) { if (Social.localUser.authenticated) { if (score > highScore) { highScore = score; PlayerPrefs.SetInt("high_score", (int)score); Social.ReportScore(score, "LEADERBOARD_ID", onReportScore); } } } 

Thus, we memorize the local record of the player, which can then be used to test the achievement of a new record.

It remains to display the standard leaderboard dialog, which can be done using the Social.ShowLeaderboardUI () function. By default, Android displays a list of all leaderboards, even if you have one (the “High Scores” table), this is not very nice and requires too much choice from the player, so I had to add this code:

 #if UNITY_ANDROID (Social.Active as GooglePlayGames.PlayGamesPlatform).SetDefaultLeaderboardForUI("LEADERBOARD_ID"); #endif Social.ShowLeaderboardUI(); 

Having dealt with the record tables and satisfied with the result, I started to implement the achievements, and then a big and unpleasant surprise awaited me, but let's take it in order.

Achievements are of two types: “one-way” (achievement) and incremental (incremental achievement). The first ones imply achievement from one time, for example, “launch a rocket” - as soon as a player finds and launches one rocket, we consider that the achievement is 100% complete and open to the player. Incremental achievements mean step-by-step execution in several stages, for example, the achievement “Cherry Hunter” implies collecting 15 cherries, in the process of which the player will gradually open the achievement, and after collecting all 15 cherries he will receive it completely. Such achievements seemed to me more relevant in my game; For starters, I added 5 achievements:



Starting the implementation of incremental achievements, I encountered two problems:
- Difference in interaction with Android and iOS servers;
- It is necessary to keep the current progress to achieve in order to send an increased value each time, otherwise the achievement will not grow.

The difference in the interaction is that Google Play calculates the percentage increment of achievement itself, specifying the number of steps in the Google Developer Console 15, we can send the value 1 to the server each time, and the server logic will add units until it is typed 15 and the achievement will not be open.

Apple Game Center shifts the concern for incrementing progress to customer logic, and expects us to gradually increase progress from 0 to 100 units (percent). Therefore, if we send him 1 constantly, progress will always be 1%.

So, in the case of iOS, we need to get the current progress of achievements and save it so that we can send an increased value in the future. And also we need to store on the client the number of steps (iterations) in order to send the correct increment of progress. For these purposes, I created a helper class:

 public class AchievementData { public string id; public int steps; public AchievementData(string id, int steps) { this.id = id; this.steps = steps; } } 

And I prepared the data on my achievements (in fact, this is a copy of the data that I entered in the Google Developer Console for Android):

 //     -    -    public static readonly AchievementData cherryHunter = new AchievementData("ACHIEVEMENT_ID", 15); public static readonly AchievementData bananaHunter = new AchievementData("ACHIEVEMENT_ID", 25); public static readonly AchievementData strawberryHunter = new AchievementData("ACHIEVEMENT_ID", 50); public static readonly AchievementData rocketRider = new AchievementData("ACHIEVEMENT_ID", 15); public static readonly AchievementData climberHero = new AchievementData("ACHIEVEMENT_ID", 250); //     private readonly AchievementData[] _achievements = { cherryHunter, bananaHunter, strawberryHunter, rocketRider, climberHero }; //   ,       private Dictionary<string, IAchievement> _achievementDict = new Dictionary<string, IAchievement>();      iOS: if (Application.platform == RuntimePlatform.IPhonePlayer) { Social.LoadAchievements(onAchievementsLoadComplete); } private void onAchievementsLoadComplete(IAchievement[] achievements) { //    ,        foreach (IAchievement achievement in achievements) { _achievementDict.Add(achievement.id, achievement); } //   ,        for (int i = 0; i < _achievements.Length; i++) { AchievementData achievementData = _achievements[i]; if (_achievementDict.ContainsKey(achievementData.id) == false) { IAchievement achievement = Social.CreateAchievement(); achievement.id = achievementData.id; _achievementDict.Add(achievement.id, achievement); } } } 

It is important to note that while there is no progress on achievements, an empty list will arrive - this is not a bug, in this array only achievements on which the player already has progress greater than 0 come, therefore after receiving the list of available achievements, we fill in the gaps on the remaining achievements (with progress 0), to continue to work with all the achievements on the same principle.

Dispatching progress to achieve different for both platforms:
 public void reportProgress(string id) { if (Social.localUser.authenticated) { #if UNITY_ANDROID (Social.Active as GooglePlayGames.PlayGamesPlatform).IncrementAchievement(id, 1, onReportProgressComplete); #elif UNITY_IPHONE IAchievement achievement = getAchievement(id); //     0 - 100 achievement.percentCompleted += 100.0 / getAchievementData(id).steps; achievement.ReportProgress(onReportProgressComplete); #endif } } 

It uses two auxiliary functions:

 //         public IAchievement getAchievement(string id) { return _achievementDict[id]; } //      ,        iOS        (   ) public AchievementData getAchievementData(string id) { for (int i = 0; i < _achievements.Length; i++) { AchievementData achievementData = _achievements[i]; if (achievementData.id == id) return achievementData; } return null; } 

To display the standard achievement dialog, use the function:

 #if UNITY_ANDROID || UNITY_IPHONE Social.ShowAchievementsUI(); #endif 

An auxiliary function that may come in handy when testing achievements for iOS returns the entire list of achievements (received from the server + created on the client):
 override public string ToString() { string result = ""; foreach (KeyValuePair<string, IAchievement> pair in _achievementDict) { IAchievement achievement = pair.Value; result += achievement.id + " " + achievement.percentCompleted + " " + achievement.completed + " " + achievement.lastReportedDate + "\n"; } return result; } 

To summarize, work with achievements for Android is easier. In the case of iOS, you need the most control on the client side. This is only one plus - more flexibility for iOS, for which you have to pay time costs.

Since I had to use a third-party plugin for Android, I began to check the written logic from it. After making sure that everything works, I decided to quickly check the logic on the iPad and prepare releases for the game. And here I was waiting for the very unpleasant surprise that came up when you least expected it: the function of sending progress for iOS constantly returned false and the mysterious string:

Looking for "ACHIEVEMENT_ID", cache count is 1.

After reading the forums and having a lot of experimentation, I realized that the achievements under iOS do not shine for me, and that this is some kind of Unity or Game Center bug. The next morning, being in a very nasty mood, I launched the game on an iPad and was surprised to find that achievements are being correctly processed. In the evening, the situation repeated again. Upon reflection, I came to the conclusion that the problem may be related to this: sandbox transactions have a much lower priority than games in the store, so during the "rush hour", when it is day in America, almost no progress on achievement is performed, but if try to update the progress of achievement, when it is deep night in America, and the Apple servers “rest” in the cool night of California night, almost all the achievements are processed. And the message “Looking for„ ACHIEVEMENT_ID “, cache count is 1.” means that it is not possible to send progress at this time, and Unity caches progress towards reaching it locally. This progress will not be lost, and will go to the server when it is possible to communicate with it.

This theory is opposed by the fact that developers using the prime31 plugin for these purposes do not experience such "delays", and that the most likely problem is in Unity. I decided to take a chance and give out the game with achievements in such a “hanging” consisted to test my theory.

After a week and a half of waiting, the game appeared in the store. Having tested leaderboards and achievements, I found that in production they work as mysteriously as in the sandbox. It seems that Unity caches progress on achievements to some kind of “critical mass”, and then synchronizes them at one moment. This result is not satisfactory.

To sum up: to integrate achievements and leaderboards in Unity, you can't do without your own or third-party plug-in, but integration takes some time, the lion's part of which goes to “fighting windmills” and is not as simple as we would like.

Source: https://habr.com/ru/post/256951/


All Articles