📜 ⬆️ ⬇️

Simple leaderboard on Unity3D with facebook

After participating in Ludum Dare 31, we had a game in which we could compete with friends and we decided to add a leaderboard to it, with authorization through Facebook. What difficulties may arise and how to make similar in your game read under the cut.

image

Facebook


The first thing we did was connect the Facebook SDK. It can be downloaded for free from the Asset Store . It should be noted that the SDK was written for a long time and is not actively updated. In particular, for compatibility with Unity 4.6, you need to fix something. Open the FB.cs file and change it there in line 411 UNITY_4_5 to UNITY_4_6 . Will work now. Well, or rewrite this define to the correct one, so that it works on both 4.5 and 4.6 and all subsequent ones.

Next you need to create and configure the application on https://developers.facebook.com . After that, you get an App ID, which you enter in the settings through the inspector.
')


Next, you need to initialize Facebook to Unity. To do this, call the function FB.Init () . The documentation says that it needs to be called once and only once when you first start the game. And here may be the first difficulty. The fact is that you need to transfer 2 callbacks to it. At the end of the initialization and the folding of the game library. The question arises where to call this function. If you have the whole game on the same stage and this scene never reboots, then there are no problems. Just call some GameController in Awake () .

Otherwise, it is better to do either static functions or singleton at once. We used this implementation here. It turned out quite simple.

public class SocialController : Singleton<SocialController> { public void Awake() { FB.Init(FacebookInited, OnHideUnity); } private void FacebookInited() { Debug.Log("FacebookInited"); if (FB.IsLoggedIn) { Debug.Log("Already logged in"); OnLoggedIn(); } } private void OnHideUnity(bool isGameShown) { Debug.Log("OnHideUnity"); if (!isGameShown) { GameController gameController = FindObjectOfType<GameController>(); if (gameController != null) { gameController.SetPause(); } } } ... } 

After that, you can call some function SocialController, and it will automatically be created. On any stage and will not be destroyed when moving on scenes. In the OnHideUnity () function, you can pause the game if the gameplay is active now, and in FacebookInited () you can check if the user is not already logged in (this happens when the game restarts). If the user has not logged in yet, then he should be given this opportunity. To do this, we added a button that is shown in the game if the user is not logged in (this can be checked with FB.IsLoggedIn ).

  public void LoginToFaceBook() { if (!FB.IsLoggedIn) { FB.Login("user_friends", LoginCallback); } } 

The necessary permissions for the game are transferred to the FB.Login () function. And here the question arises, what permissions do we need? And it depends on what we want from Facebook. Initially, we wanted to use the Facebook Scores API for the leaderboard. It was created specifically for leaderboards in games, and is positioned as something very simple. And at first it seemed so. We can easily get both our points and our friends' points, and at the same time a sorted list. However, upon further study, everything was not so simple. Firstly, you can store only 1 number for each user there. So only 1 leaderboard and only among its friends.

But the worst thing is that in order to update its own Score, the application must get permission to publish_actions . And this permission implies the ability for the user to write to the tape and much more. And your application must be reviewed to be able to request this permission from the user. And the user may not give it to you. The result is very difficult, and the possibilities are minimal. So that decision had to be abandoned.
What we need in this case from Facebook:


Based on this, we create a list of permissions in the FB.Login () function. We now need only user_friends .
Having logged in, you can request the information we need:

  void OnLoggedIn() { Debug.Log("Logged in. ID: " + FB.UserId); FB.API("/me?fields=name,friends", Facebook.HttpMethod.GET, FacebookCallback); } void FacebookCallback(FBResult result) { if (result.Error != null) { return; } string get_data = result.Text; var dict = Json.Deserialize(get_data) as IDictionary; _userName = dict["name"].ToString(); friends = Util.DeserializeJSONFriends(result.Text); GotUser(); GetBestScoresFriends(); } 

The function Util.DeserializeJSONFriends () is taken from the official example and is available here . As a result, in the _userName variable we will have the player's name, and in friends the list of his friends.

Parse


The next step is to save points and a list of players. Since we refused the Facebook Score API, we will need a server. The easiest way to get it is to use Parse . It has a library specifically for Unity and is available here . However, the library was obviously not created specifically for Unity, but simply the .NET version was taken, which would cause some difficulties.

Setting up a Parse doesn’t cause much difficulty, since the official guide is written quite well. I note only that Parse Initialize Behavior should be added to the new object, and not to the game controller, since with it the object will not be destroyed when the scene is reloaded.

Parse provides great opportunities for developers, but what can we need? The first thing we thought to use is ParseUser - a user in Parse terminology. You can create a new one or update an existing one. They are needed in order to unify users of different types and link different profiles of the same user. Say, if a user first logged in to you via email, and then decided to specify another Facebook account. Then you can add information about the player's FB account to his profile. However, having tinker a bit with ParseUser, we realized that we don’t really need them, so we will not use them further.

What we will definitely need is ParseObject. Each such object is essentially a record in the data table. In which table is given the name when creating the object. Accordingly, write new ParseObject ("DataTable") - get a new entry in the DataTable table after calling the Save () method. It turns out a simple algorithm for leaderboard. We are looking for an entry in the table with the current user, if not found, create a new one. In any case, we will have ParseObject with the current user. We write the player's name in it, his record and save.

  private void GotUser() { var query = ParseObject.GetQuery("GameScore") .WhereEqualTo("playerFacebookID", FB.UserId); query.FindAsync().ContinueWith(t => { IEnumerable<ParseObject> result = t.Result; if (!result.Any()) { Debug.Log("UserScoreParseObject not found. Create one!"); _userScoreParseObject = new ParseObject("GameScore"); _userScoreParseObject["score"] = 0; if (string.IsNullOrEmpty(_userName)) _userScoreParseObject["playerName"] = "Player"; else _userScoreParseObject["playerName"] = _userName; _userScoreParseObject["playerFacebookID"] = FB.UserId; _userScoreParseObject.SaveAsync(); } else { Debug.Log("Found score on Parse!"); _userScoreParseObject = result.ElementAt(0); int score = _userScoreParseObject.Get<int>("score"); if (score > GameController.BestScore) { GameController.BestScore = score; GameController.UpdateBestScore = true; } } }); } public void SaveScore(int score) { if (_userScoreParseObject == null) return; Debug.Log("Save new score on Parse! " + score); int oldScore = _userScoreParseObject.Get<int>("score"); if (score > oldScore) { _userScoreParseObject["score"] = score; _userScoreParseObject.SaveAsync(); } } 

GameController.BestScore and GameController.UpdateBestScore are static fields of the GameController class. As I said, Parse was not originally designed for Unity, so the logic of its work is somewhat inconvenient. Instead of the usual Unity programmers Corutin, the Task system class is used here. It works asynchronously and the contents of ContinueWith () will be called in another thread. If you try to call some MonoBehaviour method, you will get an error. Static fields are not the most beautiful way to get around this problem, but in our case it helped. If someone from habr-users will tell in comments a normal way to return to the main flow of the application (maybe like Looper from android programming) - I will be grateful.

It remains only to get a list of the best players among all and among friends. To do this, just make a correct request to Parse.

  public void GetBestScoresOverall() { var query = ParseObject.GetQuery("GameScore") .OrderByDescending("score") .Limit(5); query.FindAsync().ContinueWith(t => { IEnumerable<ParseObject> result = t.Result; string leaderboardString = ""; foreach (ParseObject parseObject in result) { leaderboardString += parseObject.Get<string>("playerName"); leaderboardString += " - "; leaderboardString += parseObject.Get<int>("score").ToString(); leaderboardString += "\n"; } GameController.overallLeaderboardString = leaderboardString; }); } public void GetBestScoresFriends() { if (friends != null && friends.Any()) { List<string> friendIds = new List<string>(); foreach (Dictionary<string, object> friend in friends) { friendIds.Add((string)friend["id"]); } if (friendIds.Any()) { string regexp = FB.UserId; for (int i = 0; i < friendIds.Count; i++) { regexp += "|"; regexp += friendIds[i]; } var queryFriends = ParseObject.GetQuery("GameScore") .OrderByDescending("score") .WhereMatches("playerFacebookID", regexp, "") .Limit(5); queryFriends.FindAsync().ContinueWith(t => { IEnumerable<ParseObject> result = t.Result; string leaderboardString = ""; foreach (ParseObject parseObject in result) { leaderboardString += parseObject.Get<string>("playerName"); leaderboardString += " - "; leaderboardString += parseObject.Get<int>("score").ToString(); leaderboardString += "\n"; } GameController.friendsLeaderboardString = leaderboardString; }); } } } 

If everything is more or less clear with the function of obtaining a global leaderboard, then writing a friend selection function may not be trivial. In this case, we create a regular expression that selects us and our friends from the table. Not only does the documentation for WhereMatches () function work slowly, the regular expression can also be quite long (depending on the number of friends playing this game too). I think this method is not suitable for any large projects, but for a small game it works fine and did not cause problems. However, I will be grateful if someone describes how in this case it is necessary to act "according to the mind."

Bonus


After all this, we got a working leaderboard with authorization through Facebook. And since we already have authorization, why not make it possible to share the result with friends. The great thing is that if you do not do this automatically, and offer the user a standard dialogue, you will not need special permissions.

  public void ShareResults() { string socialText = string.Format("I scored {0} in Sentinel. Can you beat it?", GameController.BestScore); FB.Feed( link: "https://apps.facebook.com/306586236197672", linkName: "Sentinel", linkCaption: "Sentinel @ LudumDare#31", linkDescription: socialText, picture: "https://www.dropbox.com/s/nmo2z079w90vnf0/icon.png?dl=1", callback: LogCallback ); } 

Thank you for reading to the end. Make good games and push players to compete with leaderboards.

Who cares how it ultimately works - the final result . And here you can play the original version.

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


All Articles