Search Unity

Social Interface experiments

Discussion in 'Community Learning & Teaching' started by litebox, May 13, 2015.

  1. litebox

    litebox

    Joined:
    Aug 29, 2011
    Posts:
    158
    It's hard to imagine modern mobile game without social integration, global leaderboards and achievements. To keep pace with tendencies I decide to integrate Game Center and Play Services for iOS and Android versions of my game.

    I develop games at free time (it's my hobby), so I cast aside thoughts about paid plugins, like prime31. I choose interface Social, shipped with Unity. I was intrigued by this package: it has poor documentation and because of this I had two thoughts about it: interface is very simple in use or it's not suitable for using. So, it's time to investigate.

    First of all, Social interface has implementation only for iOS, and for Android - it just pure interface. This knowledge brings me to https://github.com/playgameservices/play-games-plugin-for-unity - it's a free Google's plugin for Android, it fills Social interface with desirable implementation. Plugin has a terrifying version 0.9, but it doesn't affect its workability. Other side it doesn't implement part of functionality, and we talk about it later.

    Full of energy and believe in success I prepare projects in iTunes Connect and Google Developer Console - at this point all is clear. Both platforms have very similar settings of leaderboards and achievements, and plenty of documentation.

    There is few moments I'd like to notice:
    Google Developer Console generates identifiers of leaderboards and achievements by itself, and in iTunes we can fill them ourself, so to keep things clear it's better to start from Google, and then fill information to iOS, just coping identifiers.
    When you work with Play Services in Google Developer Console, and when you add alpha/beta versions of game, Google emphatically offers make publishing of leaderboards and achievements - it's better to postpone this action to until release, because after publishing you cannot delete leaderboards and achievements, also you will not be able to modify important parameters, like count of steps for iterative achievements.

    I create "High Scores" leaderboards and minimal set of achievements (for Google this is five items) so, even if you don't intend to use achievements - you have to create them. Apple doesn't have this restriction, but because we already create few achievements it's not a big deal to copy them into iTunes Connect.

    Next, let's install plugin for Android, in Unity's menu select Assets/Import Package/Custom Package and add plugin into project. After successful import in Unity's menu appears Google Play Games submenu, select Android Setup..., enter app identifier, you can find it in section Game Services of Google Developer Console and plugin is ready to use.

    Now all is ready and we can write few lines of source code (C#) in Unity. First of all, we need to setup iOS/Android and authenticate:
    Code (CSharp):
    1. #if UNITY_ANDROID
    2. // Select the Google Play Games platform as our social platform implementation
    3. GooglePlayGames.PlayGamesPlatform.Activate();
    4. #endif
    5.  
    6. #if UNITY_IPHONE
    7. // by defaults when player gets achievement nothing happens, so we call this function to show standard iOS popup when achievement is completed by user
    8. UnityEngine.SocialPlatforms.GameCenter.GameCenterPlatform.ShowDefaultAchievementCompletionBanner(true);
    9. #endif
    10.  
    11. Social.localUser.Authenticate(onProcessAuthentication);
    12. // this function gets called when Authenticate completes, if the operation is successful, Social.localUser will contain data from the server
    13. private void onProcessAuthentication(bool success)
    14. {
    15.     Debug.Log("onProcessAuthentication: " + success);
    16. }
    After successful authentication we can work with leaderboards and achievements.

    Let's start from leaderboards. First of all I decide, that I need to get current player's highest scores from server - so we can compare server's highest scores with current scores and if player gets a new top, we popup dialog like "Congratulations! New Top: XXX". For this I write script, it creates leaderboard's table, sets filter for players (we need only our player) and gets highest scores if success:
    Code (CSharp):
    1. string[] userIds = new string[] { Social.localUser.id };
    2. highScoresBoard = Social.CreateLeaderboard();
    3. highScoresBoard.id = "LEADERBOARD_ID";
    4. highScoresBoard.SetUserFilter(userIds);
    5. highScoresBoard.LoadScores(onLeaderboardLoadComplete);
    6.  
    7. private void onLeaderboardLoadComplete(bool success)
    8. {
    9.     Debug.Log("onLeaderboardLoadComplete: " + success);
    10.     if (success)
    11.     {
    12.         long score = highScoresBoard.localUserScore.value;
    13.     }
    14. }
    Current progress send (sending of current highest scores can be even lower than previous one - we don't worry about this - in this case new data will be rejected by server):
    Code (CSharp):
    1. public void reportScore(long score)
    2. {
    3.     if (Social.localUser.authenticated)
    4.     {
    5.         Social.ReportScore(score, "LEADERBOARD_ID", onReportScore);
    6.     }
    7. }
    8. private void onReportScore(bool result)
    9. {
    10.     Debug.Log("onReportScore: " + success);
    11. }
    After test of this code I got a problem - it doesn't work at Android, because plugin has no implementation of few functions - this is a charm of version 0.9.
    But this is not a cause for frustration, because it isn't really necessary to get current top of player from server, it's sufficiently to keep it locally. The reason is, if player changes his smart phone, or removes our game and install it in future, it would be more agreeably again and again beat his own local top. Higher score from leaderboards will always keep bigger progress of player, and it could take mach time to beat it, so this can decrease motivation of player to play again. So I decide to orient on local top and add next script to authentication:
    Code (CSharp):
    1. public long highScore = 0;
    2.  
    3. private void onProcessAuthentication(bool success)
    4. {
    5.     Debug.Log("onProcessAuthentication: " + success);
    6.  
    7.     if (success)
    8.     {
    9.         if (PlayerPrefs.HasKey("high_score"))
    10.             highScore = (long)PlayerPrefs.GetInt("high_score");
    11.     }
    12. }
    Sending of progress to server has a few changes and looks like this:
    Code (CSharp):
    1. public void reportScore(long score)
    2. {
    3.     if (Social.localUser.authenticated)
    4.     {
    5.         if (score > highScore)
    6.         {
    7.             highScore = score;
    8.             PlayerPrefs.SetInt("high_score", (int)score);
    9.  
    10.             Social.ReportScore(score, "LEADERBOARD_ID", onReportScore);
    11.         }
    12.     }
    13. }
    This is how we save local player's top, so later we can use it to check if new scores highest than it.

    The last thing is to show standard leaderboards dialog, it could be done by calling function Social.ShowLeaderboardUI(). By default for Android it shows list of all leaderboards, even if we have the only one table ("High Scores" table in our case), this is not good and take one "extra click" for player, so we have to add few additional lines:
    Code (CSharp):
    1. #if UNITY_ANDROID
    2.     (Social.Active as GooglePlayGames.PlayGamesPlatform).SetDefaultLeaderboardForUI("LEADERBOARD_ID");
    3. #endif
    4. Social.ShowLeaderboardUI();
    Done with leaderboards and happy with results I start to implement achievements, and at this point big and unpleasant surprise is awaited for me, but about this we will talk later.

    Achievements can be of two kinds: "instantaneous" achievement and incremental achievement. First opens at once, for example, "launch the rocket" - as soon as player finds and launches one rocket - we decide that achievement complete at 100% and open it for player. Incremental achievements mean step by step accomplish in several phases, for example, achievement "Cherry Hunter" means collection of 15 cherries, while player collects cherries achievement will open little by little and after he collects all 15 fruits he completes it completely. These kind of achievements look more suitable for my game, so I add 5:



    After I begin to implement incremental achievements I face two problems:
    Differences in interaction between Android and iOS backends.
    We have to keep current achievement's progress, because every time we must send bigger progress, otherwise achievement will not increase.

    Google Play calculates percent of increment by itself, if we indicate 15 steps at Google Developer Console we can every time send to backend value of 1, and every time it will summarize units, while achieves 15, and at this point achievement will be completed.

    Apple Game Center places care about incremental progress to client, and waits from as gradual increment of progress in limits from 0 to 100 units (percents). So if we will be always sending 1, progress always will be 1%.

    So, in case of iOS we need to get current achievement's progress and save it. In this case we will have ability to send increased value every time. Also we have to keep on client count of steps (iterations) to be able to send correct incremental value of progress. For this purposes I create supplementary class:
    Code (CSharp):
    1. public class AchievementData
    2. {
    3.     public string id;
    4.     public int steps;
    5.     public AchievementData(string id, int steps)
    6.     {
    7.         this.id = id;
    8.         this.steps = steps;
    9.     }
    10. }
    And prepare data for my achievements (in fact this is a copy of data from Google Developer Console):
    Code (CSharp):
    1. // describe all achievements - their identifiers and steps to achieve
    2. public static readonly AchievementData cherryHunter = new AchievementData("ACHIEVEMENT_ID",  15);
    3. public static readonly AchievementData bananaHunter = new AchievementData("ACHIEVEMENT_ID",  25);
    4. public static readonly AchievementData strawberryHunter = new AchievementData("ACHIEVEMENT_ID",  50);
    5. public static readonly AchievementData rocketRider =     new AchievementData("ACHIEVEMENT_ID",  15);
    6. public static readonly AchievementData climberHero =    new AchievementData("ACHIEVEMENT_ID", 250);
    7. // array of all achievements
    8. private readonly AchievementData[] _achievements =
    9. {
    10.     cherryHunter,
    11.     bananaHunter,
    12.     strawberryHunter,
    13.     rocketRider,
    14.     climberHero
    15. };
    16.  
    17. // table of player's achievements, we get this data from backend
    18. private Dictionary<string, IAchievement> _achievementDict = new Dictionary<string, IAchievement>();
    Next script is necessary only for iOS:
    Code (CSharp):
    1. if (Application.platform == RuntimePlatform.IPhonePlayer)
    2. {
    3.     Social.LoadAchievements(onAchievementsLoadComplete);
    4. }
    5.  
    6. private void onAchievementsLoadComplete(IAchievement[] achievements)
    7. {
    8.     // if player has progress by achievement - place it in a table
    9.     foreach (IAchievement achievement in achievements)
    10.     {
    11.         _achievementDict.Add(achievement.id, achievement);
    12.     }
    13.    
    14.     // let's create all residuary achievements, player doesn't have progress by them yet
    15.     for (int i = 0; i < _achievements.Length; i++)
    16.     {
    17.         AchievementData achievementData = _achievements[i];
    18.  
    19.         if (_achievementDict.ContainsKey(achievementData.id) == false)
    20.         {
    21.             IAchievement achievement = Social.CreateAchievement();
    22.             achievement.id = achievementData.id;
    23.  
    24.             _achievementDict.Add(achievement.id, achievement);
    25.         }
    26.     }
    27. }
    Important remark: while player doesn't have any progress by achievements, list from backend will be empty, - this is not a bug, and the array contains only achievement with progress, greater than 0. Because of this after we get list of actual achievements we initialize all residuary achievements (with 0 progress), at this point we can work with all achievements by one scheme.

    Sending of progress by achievement has differences for both platforms:
    Code (CSharp):
    1. public void reportProgress(string id)
    2. {
    3.     if (Social.localUser.authenticated)
    4.     {
    5. #if UNITY_ANDROID
    6.         (Social.Active as GooglePlayGames.PlayGamesPlatform).IncrementAchievement(id, 1, onReportProgressComplete);
    7. #elif UNITY_IPHONE
    8.         IAchievement achievement = getAchievement(id);
    9.        
    10.         // normalize value in 0 - 100 interval
    11.         achievement.percentCompleted += 100.0 / getAchievementData(id).steps;
    12.        
    13.         achievement.ReportProgress(onReportProgressComplete);
    14. #endif
    15.     }
    16. }
    Here we used two supplementary functions:
    Code (CSharp):
    1. // ability to get data about achievement outside class scope
    2. public IAchievement getAchievement(string id)
    3. {
    4.     return _achievementDict[id];
    5. }
    6. // ability to get supplementary data by achievement, we need them to calculate correct progress for iOS, we specially keep them in client (array of all achievement's data)
    7. public AchievementData getAchievementData(string id)
    8. {
    9.     for (int i = 0; i < _achievements.Length; i++)
    10.     {
    11.         AchievementData achievementData = _achievements[i];
    12.         if (achievementData.id == id)
    13.             return achievementData;
    14.     }
    15.  
    16.     return null;
    17. }[code]
    18. To show standard achievements dialog we use next function:
    19. [code=CSharp]#if UNITY_ANDROID || UNITY_IPHONE
    20.     Social.ShowAchievementsUI();
    21. #endif
    22.  
    Supplementary function, could be useful while testing achievements for iOS, it returns list of all achievements (got from backend and created on client):
    Code (CSharp):
    1. override public string ToString()
    2. {
    3.     string result = "";
    4.  
    5.     foreach (KeyValuePair<string, IAchievement> pair in _achievementDict)
    6.     {
    7.         IAchievement achievement = pair.Value;
    8.         result += achievement.id + " " +
    9.             achievement.percentCompleted + " " +
    10.             achievement.completed + " " +
    11.             achievement.lastReportedDate + "\n";
    12.     }
    13.     return result;
    14. }
    At result, working with achievements for Android is simpler. In case of iOS you have to control more things on client side. This has only one plus - bigger flexibility for iOS, but we have to pay for this our time.

    Because I have to use 3d party plugin for Android, I start test new logic from it. Convinced that all is working, I decide to test this logic on iPad quickly and prepare game releases. And here a big and unpleasant surprise was waiting for me: sending of progress for iOS always returns false and mystical string:

    Looking for "ACHIEVEMENT_ID", cache count is 1.

    After I read forums and made several tests I understand, that this is a bug of Unity. Next morning I launch game again and notice that achievements are working. It looks like somehow Unity cannot send progress immediately, and cashes it on client. In some cases game is able to send this cashed progress later and after a while achievements get progress, but most of the time this is not happening.

    In this thread you can find solution how to work around problem with achievements progress:
    http://forum.unity3d.com/threads/problem-with-game-center-achievements.310817/

    If you would like to check how it works you can look here:
    Android: https://play.google.com/store/apps/details?id=com.litebox.CandyClimber
    iPhone, iPad, iPod: https://itunes.apple.com/app/r-climber/id975023004

    Conclusion: for achievements and leaderboards integration in Unity we have to use own or 3d party plugin.

    Thanks for reading.
     
    coder9401, eses, daterre and 3 others like this.
  2. paulrahme

    paulrahme

    Joined:
    Apr 25, 2012
    Posts:
    17
    Thank you for the article - very helpful cross-platform information.

    I am currently stuck on iOS with ReportProgress returning false, and the error "Achievement ID not found". VERY annoying.

    But I also need to implement the equivalent Google functions, so this article is a big help :)
     
  3. WhosTheBoss

    WhosTheBoss

    Joined:
    Jan 8, 2013
    Posts:
    64
    Excellent!

    I imported the Google Play Services plug-in into my Unity game.
    Hope it all works well.

    However, i'm getting an error from Unity saying:
    [Quote added to remove accidental smiley - mod]
     
    Last edited by a moderator: Nov 10, 2015
  4. Seal_I

    Seal_I

    Joined:
    Apr 19, 2013
    Posts:
    7
    WhosTheBoss, we have the same error and it seems that we have fixed it by downloading Google Play Services and Google Repository; we use Eclipce Android SDK Manager.
    (With GPS we have downloaded all packages that have Google Play in name, so if it won't help try others)

    But the is another problem:
    Error building Player: CommandInvokationFailure: Failed to re-package resources.
    Error: No resource found that matches the given name (at 'theme' with value '@StyLe/Theme.IAPTheme').

     
    Last edited: Nov 11, 2015
  5. yackeroeni

    yackeroeni

    Joined:
    Oct 28, 2015
    Posts:
    2
    I'm having the exact same problem as WhosTheBoss. Does anyone have a solution for this? Or is it maybe just a bug?
     
  6. jprocha101

    jprocha101

    Joined:
    Apr 8, 2015
    Posts:
    134
    @WhosTheBoss @Seal_I @yackeroeni

    I was having this issue with appcompat-v7 instead of play services. The problem is that it is looking for a newer version of the package than what you have downloaded to your Android SDK. In my scenario I had 23.0.1 installed and it was looking for 23.1.0 or greater. You probably have something less than 8.1. Updating the SDK then restarting Unity solved the issue.

    File path to appcompat-v7, you will need to go into gms instead of android:
    \sdk\extras\android\m2repository\com\android\support\appcompat-v7
     
    davidjheberle likes this.
  7. GunLengend

    GunLengend

    Joined:
    Sep 24, 2014
    Posts:
    54
    @Seal_I
    Hello there,
    I was facing same problem and i don't have any experience on it. Does anyone how to solve it ?
     
  8. waseem2bata

    waseem2bata

    Joined:
    Apr 17, 2014
    Posts:
    10
    ResolutionException: Cannot find candidate artifact for com.google.android.gms:play-services-games:8.1+
    Google.JarResolver.PlayServicesSupport.DependOn (System.String group, System.String artifact, System.String version)
    GooglePlayGames.BackgroundResolution.AddDependencies () (at Assets/GooglePlayGames/Editor/BackgroundResolution.cs:53)
    GooglePlayGames.BackgroundResolution..cctor () (at Assets/GooglePlayGames/Editor/BackgroundResolution.cs:45)
    Rethrow as TypeInitializationException: An exception was thrown by the type initializer for GooglePlayGames.BackgroundResolution
    System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor (RuntimeTypeHandle type) (at /Users/builduser/buildslave/mono-runtime-and-classlibs/build/mcs/class/corlib/System.Runtime.CompilerServices/RuntimeHelpers.cs:101)
    UnityEditor.EditorAssemblies.ProcessEditorInitializeOnLoad (System.Type type) (at C:/buildslave/unity/build/Editor/Mono/EditorAssemblies.cs:123)
    Rethrow as TargetInvocationException: Exception has been thrown by the target of an invocation.
    System.Reflection.MonoCMethod.Invoke (System.Object obj, BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[] parameters, System.Globalization.CultureInfo culture) (at /Users/builduser/buildslave/mono-runtime-and-classlibs/build/mcs/class/corlib/System.Reflection/MonoMethod.cs:519)
    System.Reflection.MonoCMethod.Invoke (BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[] parameters, System.Globalization.CultureInfo culture) (at /Users/builduser/buildslave/mono-runtime-and-classlibs/build/mcs/class/corlib/System.Reflection/MonoMethod.cs:528)
    System.Reflection.ConstructorInfo.Invoke (System.Object[] parameters) (at /Users/builduser/buildslave/mono-runtime-and-classlibs/build/mcs/class/corlib/System.Reflection/ConstructorInfo.cs:77)
    System.Activator.CreateInstance (System.Type type, Boolean nonPublic) (at /Users/builduser/buildslave/mono-runtime-and-classlibs/build/mcs/class/corlib/System/Activator.cs:372)
    System.Activator.CreateInstance (System.Type type) (at /Users/builduser/buildslave/mono-runtime-and-classlibs/build/mcs/class/corlib/System/Activator.cs:254)
    UnityEditor.AssetPostprocessingInternal.GetMeshProcessorVersions () (at C:/buildslave/unity/build/Editor/Mono/AssetPostprocessor.cs:145)
    UnityEditor.AssetPostprocessingInternal:GetMeshProcessorVersions()

    am getting errors like those may i know why?
     
  9. davidjheberle

    davidjheberle

    Joined:
    Apr 15, 2015
    Posts:
    1
    I was also running into the ResolutionException. Updating my Android Support Repository to 24.0.0 and my Google Repository to 23.0.0 solved it. I'm not sure which one did the trick. Thanks @jprocha101 for pointing in the right direction!
     
    jprocha101 likes this.
  10. Seal_I

    Seal_I

    Joined:
    Apr 19, 2013
    Posts:
    7
    TL;DR: we haven't fix problem yet

    Some investigation
    Environment params:
    • Android SDK tools - 24.4.1
    • Android SDK platform tools - 23.0.1
    • Android SDK Build tools - 23.0.2
    • Android support library - 23.1.1
    • Google Play Servives - 28
    • Google Repository - 23
    • SDK platform - 23
    • Unity 5.2.2
    Test 1: create empty project, import GooglePlayGamesPlugin-0.9.27.unitypackage, do setup for android... and it works;
    Test 2: create empty project, import GoogleMobileAds.unitypackage, copy google-play-services_lib to Plugins/Android (as it mention in official docs)... and it works;

    Test 3: take Test 2 project and import GooglePlayGamesPlugin-0.9.27.unitypackage, do setup for android
    FAIL
    Got building error " No resource found that matches the given name (at 'theme' with value '@StyLe/Theme.IAPTheme'). ",
    because Android Jar Dependencies (it seems that it does this) will remove google-play-services_lib from Plugins folder every build time;
    Commenting out string " <activityandroid:name="com.google.android.gms.ads.purchase.InAppPurchaseActivity" android:theme="@StyLe/Theme.IAPTheme"/> " is bad idea, because apk will build normally, but crash after launch.

    Still have no ideas, official google unitypackages are very disappointing...
    (We also need to add Analytics and TAGs... so it will be funny)
     
    Last edited: Nov 13, 2015
  11. Seal_I

    Seal_I

    Joined:
    Apr 19, 2013
    Posts:
    7
    @jprocha101 , can you provide some more information about appcompat-v7 ? it should be copied or replaced or something else?
     
  12. WhosTheBoss

    WhosTheBoss

    Joined:
    Jan 8, 2013
    Posts:
    64
    jprocha101 likes this.
  13. Seal_I

    Seal_I

    Joined:
    Apr 19, 2013
    Posts:
    7