- PVSM.RU - https://www.pvsm.ru -
После участия в Ludum Dare 31 у нас появилась игра, в которой можно соревноваться с друзьями и мы решили добавить к ней лидерборд, с авторизацией через Facebook. Какие сложности могут возникнуть и как сделать подобный в своей игре читайте под катом.
Первое что мы сделали — подключили Facebook SDK. Его можно скачать бесплатно с Asset Store [1]. Стоит отметить, что SDK написан достаточно давно и не слишком активно обновляется. В частности для совместимости с Unity 4.6 необходимо кое что поправить. Открываем файл FB.cs и там меняем в 411 строке UNITY_4_5 на UNITY_4_6. Будет работать теперь. Ну или перепишите этот дефайн на правильный, чтобы работал и на 4.5, и на 4.6 и на всех последующих тоже.
Далее нужно создать и настроить приложение на https://developers.facebook.com [2]. После этого вы получаете App ID, который вписываете в настройки через инспектор.
Далее необходимо инициализировать Facebook в Unity. Для этого необходимо вызвать функцию FB.Init().В документации сказано, что ее нужно вызвать один и только один раз при первом запуске игры. И вот тут могут возникнуть первые сложности. Дело в том, что в нее нужно передать 2 колбека. На окончание инициализации и на сворачивание игры библиотекой. Возникает вопрос где эту функцию вызвать. Если у вас вся игра на одной сцене и эта сцена никогда не перезагружается, то проблем, нет. Просто вызываете в Awake() какого-нибудь GameController-а.
В противном случае лучше делать либо статические функции, либо сразу синглтон. Мы использовали вот эту [3] его реализацию. Получилось достаточно просто.
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();
}
}
}
...
}
После этого можно вызвать какую-нибудь функцию SocialController-а и он автоматически создастся. На любой сцене и не будет уничтожен при переходе по сценам. В функции OnHideUnity() можно поставить игру на паузу, если сейчас идет активный геймплей, а в FacebookInited() можно проверить не залогинен ли пользователь уже(такое бывает при перезапусках игры). Если же пользователь еще не логинился, то эту возможность ему нужно дать. Для этого мы добавили кнопку, которая показана в игре, если пользователь не залогинен(это можно проверить с FB.IsLoggedIn).
public void LoginToFaceBook()
{
if (!FB.IsLoggedIn)
{
FB.Login("user_friends", LoginCallback);
}
}
В функцию FB.Login() передаются необходимые игре разрешения. И вот тут возникает вопрос, а какие разрешения нам нужны? И зависит это от того, что мы хотим от фейсбука. Изначально мы хотели для лидерборда использовать Facebook Scores API [4]. Создано оно специально для лидербордов в играх, причем позиционируется как нечто очень простое. И по началу оно таким и показалось. Мы можем легко получить как свои очки, так и очки своих друзей, причем сразу же отсортированный список. Однако при дальнейшем изучении все оказалось не так просто. Во первых хранить там можно лишь 1 число для каждого пользователя. Так что лидерборд только 1 и только среди своих друзей.
Но хуже всего, что для обновления собственного Score приложение должно получить разрешение publish_actions. А это разрешение подразумевает возможность и пользователю в ленту писать и много чего еще. А еще ваше приложение должно пройти ревью, чтобы иметь возможность просить это разрешение у пользователя. А пользователь может вам его еще и не дать. В итоге получается очень сложно, а возможности минимальны. Так что от такого решения пришлось отказаться.
Что же нам нужно в таком случае от фейсбука:
Исходя из этого и формируем список разрешений в функции FB.Login(). Нам сейчас нужно только user_friends.
Залогинившись, можно запросить необходимую нам информацию:
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();
}
Функция Util.DeserializeJSONFriends() взята из официального примера и доступна вот здесь [5]. В итоге в переменной _userName у нас будет имя игрока, а в friends — список его друзей.
Следующий шаг — сохранение очков и списка игроков. Так как от Facebook Score API мы отказались, то нам потребуется сервер. Самый простой способ его получить — использовать Parse [6]. У него есть библиотека специально для Unity и доступна здесь [7]. Впрочем, библиотека явно создавалась не специально для Unity, а была взята просто .NET версия, что еще вызовет определенные трудности.
Настройка Parse больших сложностей не вызывает, благо официальный гайд [8] написан достаточно хорошо. Отмечу лишь, что Parse Initialize Behaviour стоит добавлять именно к новому объекту, а не к геймконтроллеру, так как с ним объект не будет уничтожаться при перезагрузке сцены.
Parse предоставляет разработчикам большие возможности, но что же может понадобиться нам? Первое что мы думали использовать — это ParseUser — пользователь в терминологии Parse. Его можно создать нового или обновлять существующего. Нужны они для того чтобы унифицировать пользователей разных типов и связывать разные профили одного пользователя. Скажем, если пользователь сначала логинился к вам через email, а потом решил указать еще и аккаунт Facebook. Тогда вы можете добавить информацию о FB аккаунте игрока в его профиль. Однако, повозившись немного с ParseUser мы поняли, что они нам не особо то и нужны, так что дальше мы их оспользовать не будем.
А вот что нам точно понадобится, так это ParseObject. Каждый такой объект по сути — это запись в таблице данных. В какой таблице задается названием при создании объекта. Соответственно пишите new ParseObject(«DataTable») — получите новую запись в таблице DataTable после того как вызовите метод Save(). Получается простой алгоритм для лидерборда. Ищем в таблице запись с текущим пользователем, если не нашли создаем новую. В любом случае у нас будет ParseObject с текущим пользователем. Записываем в него имя игрока, его рекорд и сохраняем.
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 и GameController.UpdateBestScore — статические поля класса GameController. Как я уже говорил, Parse изначально не проектировался для Unity, поэтому и логика его работы несколько неудобна. Вместо привычных для Unity программистов корутин здесь используется системный класс Task. Работает он асинхронно и содержимое ContinueWith() у вас будет вызвано в другом потоке. При попытке вызвать какой-то метод MonoBehaviour — получите ошибку. Статические поля — не самый красивый способ обойти эту проблему, но в нашем случае это помогло. Если кто-то из хабра-пользователей расскажет в комментариях нормальный способ вернуться в главный поток приложения(может быть наподобие Looper [9]-а из андроид-программирования) — буду благодарен.
Осталось лишь получить список лучших игроков среди всех и среди друзей. Для этого достаточно лишь составить правильный запрос к 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;
});
}
}
}
Если с функцией получения глобального лидерборда все более-менее понятно, то вот написание функции выбора друзей может быть не тривиально. В данном случае мы составляем регулярное выражение, которое выбирает из таблицы нас и наших друзей. Мало того, что функция WhereMatches() по документации может работать медленно, так еще и регулярное выражение может получиться достаточно длинным(зависит от количества друзей, тоже играющих в эту игру). Думаю, этот способ не подойдет для сколько-нибудь крупных проектов, но для небольшой игры пока работает замечательно и проблем не вызывало. Впрочем, буду благодарен, если кто то опишет как в этом случае надо поступать «по уму».
После всего этого мы получили рабочий лидерборд с авторизацией через Facebook. А раз уж у нас уже есть авторизация, то почему бы не сделать возможность поделиться результатом с друзьями. Самое классное, что если не делать это автоматически, а предлагать пользователю стандартный диалог, то вам не потребуются специальные разрешения.
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
);
}
Спасибо, что дочитали до конца. Делайте хорошие игры и подталкивайте игроков к соревнованию с лидербордами.
Кому интересно как это в итоге работает — итоговый результат [10]. А вот здесь [11] можно поиграть в изначальную версию.
Автор: Gasparfx
Источник [12]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/facebook/77314
Ссылки в тексте:
[1] Asset Store: https://www.assetstore.unity3d.com/en/#!/content/10989
[2] https://developers.facebook.com: https://developers.facebook.com
[3] эту: http://wiki.unity3d.com/index.php/Singleton
[4] Facebook Scores API: https://developers.facebook.com/docs/games/scores?locale=ru_RU
[5] здесь: https://github.com/fbsamples/friendsmash-unity/blob/master/friendsmash_advanced/Assets/Scripts/Util.cs
[6] Parse: https://www.parse.com/
[7] здесь: https://www.parse.com/docs/downloads
[8] гайд: https://www.parse.com/apps/quickstart#parse_data/unity
[9] Looper: http://developer.android.com/reference/android/os/Looper.html
[10] итоговый результат: https://apps.facebook.com/306586236197672
[11] здесь: http://ludumdare.com/compo/ludum-dare-31/?action=preview&uid=46923
[12] Источник: http://habrahabr.ru/post/245985/
Нажмите здесь для печати.