Добавление рекордов с OAuth 2: Laravel Passport + Unity. Часть 2

Продолжение статьи про добавление рекордов из игры на сайт от конкретного пользователя. В первой части мы сделали страничку рекордов на Laravel и подготовили API для их добавления — как анонимным, так и авторизированным пользователем. В этой части будем дорабатывать готовую игру на Unity про Крысу на Стене, заходить за свой аккаунт и отправлять рекорды на сайт на Laravel с использованием токена авторизации.

 

Подготовка


В качестве примера предлагаю воспользоваться моим раннером про крысу с простейшим функционалом — крыса ползёт по стене, а сверху падают сковородки. Скачать проект для Unity 2017.1 можно с гитхаба. При желании можно использовать и любой другой проект, здесь рассматривается только принцип и один из вариантов его реализации.

Также в туториале используется готовый сайт на Laravel из первой части. Скачать его можно здесь. Чтобы сайт был доступен по адресу http://127.0.0.1:8000/, нужно воспользоваться командой:
 

php artisan serve


Откроем проект в Unity. Базовый игровой процесс выглядит следующим образом.

При нажатии на Play мы сможем управлять крысой, перемещаясь по стене в определенных границах и уклоняясь от падающих сковородок. Слева вверху идёт счётчик очков, внизу — остаток жизней. При нажатии на Esc отображается меню паузы — пустая панелька, на которую нам предстоит добавить форму авторизации. После окончания игру можно перезапустить кнопкой R.

Первым делом займемся добавлением анонимных рекордов.
 

Анонимные рекорды


Создадим новый скрипт в папке Scripts при помощи команды Create -> C# Script на панели Project. Назовем его WWWScore и откроем получившийся файл WWWScore.cs в используемом вами редакторе для Unity (Visual Studio, MonoDevelop).

Первым делом добавим поле для хранения адреса сервера. Укажем [SerializeField] для того, чтобы можно было изменять эту приватную переменную через панель Inspector в Unity.
 

[SerializeField]
private string serverURL = "http://127.0.0.1:8000/";


По-умолчанию адрес зададим тем же, что и у нашего сайта на Laravel. При желании его можно будет изменить.

Теперь перейдём к функции добавления рекорда от анонимного пользователя. Эта функция будет отправлять POST-запрос на сервер и дожидаться ответа. Как вариант обработки таких запросов, воспользуемся сопрограммой (Coroutine) для запуска функции параллельно. Функция для использования в сопрограмме будет выглядеть следующим образом:
 

public IEnumerator AddRecord(int score)
{
	WWWForm form = new WWWForm();
	form.AddField("score", score);

	WWW w = new WWW(serverURL + "api/anonymrecord", form);

	yield return w;

	if(!string.IsNullOrEmpty(w.error)) {
		Debug.Log(w.error);
	} else {
		Debug.Log("Рекорд добавлен!");
	}
}


Мы добавляем данные для POST-запроса (значение переменной score, которую мы будем передавать при вызове сопрограммы из класса GameController), формируем запрос по адресу http://127.0.0.1:8000/api/anonymrecord и ждем результата. Как только приходит ответ от сервера (или заканчивает срок ожидания запроса), в консоли будет выведено сообщение Рекорд добавлен!, или же информация об ошибке в случае неудачи.

Добавим скрипт WWWScore.cs объекту Game Controller через кнопку Add Component на панели Inspector, или же просто перетащив скрипт мышкой на объект.

Теперь отредактируем скрипт GameController.cs, добавив туда вызов сопрограммы.
 

void Update () {
	if (gameover){
		// Действия, выполняемые только один раз после конца игры до рестарта
		if (!gameoverStarted) {
			gameoverStarted = true; // Существующий код
			restartText.SetActive(true); // Существующий код
			// Отправляем рекорд
			StartCoroutine(GetComponent<WWWScore>().AddRecord(score));
		}
		// ...
	} else {
		// ...
	}
	// ...
}


Сопрограмма вызывается один раз в тот момент, когда игра была закончена — сразу после включения интерфейса рестарта игры. При нажатии на R сцена будет перезапущена, и можно будет опять дойти до конца игры, вызвав добавление рекорда.

Сохраним скрипт и проверим работу игры. Через некоторое время после окончания игры в консоли появится сообщение Рекорд добавлен!

Можно открыть табличку рекордов на сайте и убедиться в том, что запрос действительно был отправлен.

Анонимное добавление рекордов работает. Перейдём к авторизации.
 

Код авторизации


Добавим функцию авторизации Login(string email, string password) в WWWScore.cs, которую потом будем передавать сопрограмме. Аналогично функции добавления рекордов, она формирует POST-запрос к нашему сайту на Laravel, передавая в нём набор данных по адресу http://127.0.0.1:8000/oauth/token. Необходимый набор данных для авторизации мы рассматривали в первой части статьи.
 

WWWForm form = new WWWForm();
form.AddField("grant_type", "password");
form.AddField("client_id", "<Client ID>");
form.AddField("client_secret", "<Client Secret>");
form.AddField("username", email); // Параметр функции
form.AddField("password", password); // Параметр функции
form.AddField("scope", "*");


После получения результата запроса необходимо преобразовать данные из json. Это можно сделать с помощью JsonUtility, преобразовав json в объект. Опишем класс объекта в том же файле WWWScore.cs до описания класса WWWScore.
 

[Serializable]
public class TokenResponse
{
    public string access_token;
}


Как мы помним, в получаемом объекте json будут 4 поля, но нам нужно только поле access_token, его мы и описываем в классе. Теперь можно добавить само конвертирование json в объект.
 

TokenResponse tokenResponse = JsonUtility.FromJson<TokenResponse>(w.text);


После получения токена авторизации нам нужно сохранить его. Для простоты воспользуемся классом PlayerPrefs, предназначенном как раз для сохранения пользовательских настроек.
 

PlayerPrefs.SetString("Token", tokenResponse.access_token);


После того, как мы сохранили токен, можно воспользоваться им для добавления рекорда от этого пользователя. Но перед этим мы можем также запросить информацию о текущем пользователе, чтобы отобразить в игре, за какого пользователя осуществлен вход. Для этого вызываем сопрограмму с соответствующей функцией, которой пока ещё нет.
 

StartCoroutine(GetUserInfo());


Напишем и эту функцию.

 
Полный код функции Login

Получение информации о пользователе


Нам нужно выполнить GET-запрос по адресу http://127.0.0.1:8000/api/user, прописав в Headers запроса данные авторизации и не передавая при этом никаких других данных в запросе (null).
 

Dictionary<string, string> headers = new Dictionary<string, string>();
headers.Add("Authorization", "Bearer " + PlayerPrefs.GetString("Token"));
WWW w = new WWW(serverURL + "api/user", null, headers);


Аналогично прошлой функции, в качестве ответа мы получаем json, для разбора которого нужно создать отдельный класс с единственным нужным нам полем из всей структуры json — именем.
 

[Serializable]
public class UserInfo
{
    public string name;
}


Конвертируем json в объект этого класса.
 

UserInfo userInfo = JsonUtility.FromJson<UserInfo>(w.text);


Сохраняем имя пользователя в настройках.
 

PlayerPrefs.SetString("UserName", userInfo.name);
 
Полный код функции GetUserInfo

Изменения в коде добавления рекордов


Чтобы добавлять рекорды от авторизированного пользователя, мы немного изменим код функции AddRecord(int score). Добавим проверку, заполнен ли токен авторизации в настройках, и если да — будем добавлять его в Headers аналогично тому, как это было при получении информации о пользователе, с тем лишь отличием, что мы всё ещё передаём рекорд в данных POST-запроса.
 

WWW w;
if (PlayerPrefs.HasKey("Token"))
{
	Dictionary<string, string> headers = new Dictionary<string, string>();
	byte[] rawData = form.data;
	headers.Add("Authorization", "Bearer " + PlayerPrefs.GetString("Token"));
	w = new WWW(serverURL + "api/record", rawData, headers);
} else {
	w = new WWW(serverURL + "api/anonymrecord", form);
}
 
Полный код изменённой функции AddRecord

Код выхода


Чтобы выйти за пользователя из игры, необходимо удалить все данные о нем в настройках. В нашем случае у нас нет никаких других настроек, поэтому мы просто очищаем все настройки. Будьте аккуратнее с этим в своих проектах.
 

public void Logout()
{
	PlayerPrefs.DeleteAll();
}


 

Основной контроллер


Теперь подготовим основной контроллер игры (GameController.cs) для работы с авторизацией пользователя. Нам будут нужны объекты с панелью авторизации loginObj и панелью выхода logoutObj, чтобы можно было переключать их. На панели авторизации будут поля ввода для электронного адреса (inputFieldEmail) и для пароля (inputFieldPassword). Также нам будет нужна надпись userNameText для отображения имени пользователя, который зашел за свой аккаунт.
 

// Объект авторизации
public GameObject loginObj;
// Объект выхода
public GameObject logoutObj;
// Поле E-mail
public GameObject inputFieldEmail;
// Поле Пароль
public GameObject inputFieldPassword;
// Надпись с именем пользователя
public GameObject userNameText;


Для авторизации мы создадим функцию Login(), которая будет вызываться по клику на кнопке Войти, считывать адрес электронной почты с паролем и вызывать сопрограмму с одноименной функцией из WWWScore.cs.
 

public void Login()
{
	var email = inputFieldEmail.GetComponent<InputField>().text;
	var password = inputFieldPassword.GetComponent<InputField>().text;
	StartCoroutine(GetComponent<WWWScore>().Login(email, password));
}


Функция выхода очень проста — она будет вызываться по клику на кнопке Выйти и вызывать одноименную функцию из WWWScore.cs без каких-либо параметров.
 

public void Logout()
{
	GetComponent<WWWScore>().Logout();
}


Для переключения видимости панелей авторизации и выхода мы будем проверять, сохранена ли соответствующая настройка в PlayerPrefs и в зависимости от этого отображать нужную панель.
 

public void SetLoginVisible()
{
	if (PlayerPrefs.HasKey("Token"))
	{
		loginObj.SetActive(false);
		logoutObj.SetActive(true);
	}
	else
	{
		loginObj.SetActive(true);
		logoutObj.SetActive(false);
	}
}


Аналогично, для отображения имени пользователя проверяем настройку имени и если её нет, пишем Аноним.
 

public void SetUserName()
{
	if (PlayerPrefs.HasKey("UserName"))
	{
		userNameText.GetComponent<Text>().text = PlayerPrefs.GetString("UserName");
	} else
	{
		userNameText.GetComponent<Text>().text = "Аноним";
	}
}


Последние две функции следует вызывать только при изменении соответствующих настроек (и в процессе инициализации), но в рамках этого туториала можно делать это и в функции Update():
 

void Update () {
	// ...

	// Подсчет результата
	// ...
	
	SetUserName();
	SetLoginVisible();
}


Теперь переходим к визуальной составляющей.
 

Интерфейс авторизации


Добавим интерфейс авторизации. Поставим галочку Enable панельке Pause, вложенной в объект Canvas. Создадим новый пустой объект (Create Empty), назовём его Login и поместим внутрь панели Pause, на одном уровне с Title (надпись Пауза). Добавим ему компонент Graphic Raycaster (для корректной работы со вложенными элементами).

В этот объект Login добавим два поля ввода, InputFieldEmail и InputFieldPassword (UI -> Input Field), поменяв им текст плейсхолдера для наглядности. Компоненту Input Field у объекта InputFieldEmail сменим тип данных в поле Content Type на Email Address, а у объекта InputFieldPassword — на Password. Добавим кнопку ButtonLogin (UI -> Button) в этот же объект Login. Интерфейс будет выглядеть примерно так (если поиграться со шрифтами и размером компонентов).

Привяжем созданную ранее функцию к событию клика по кнопке ButtonLogin. У компонента Button на панели Inspector нажмём на плюсик у события On Click (), выберем Editor and Runtime из списка (для корректной работы в процессе отладки) и перетянем туда объект Game Controller (мышкой или же выбрав его при клике на кружок выбора у поля объекта). В появившемся после этого выпадающем меню выберем компонент GameController и функцию Login() в нём.

Снимем галочку Enable у объекта Login — его отображение регулируется в GameController.cs.
 

Интерфейс выхода


Создадим новый объект Logout аналогично объекту Login (не забыв про компонент Graphic Raycaster) вложенным в Pause. Добавим объекту Logout только кнопку ButtonLogout. Аналогично прошлой кнопке, привяжем к событию клика функцию Logout() компонента GameController одноименного объекта.

Снимем галочку Enable у объекта Logout и у самой панели Pause.
 

Отображение имени пользователя


Добавим текстовый элемент User (UI -> Text) в главный Canvas до элемента Pause, написав в нём Аноним (либо оставив пустым, т.к. надпись будет назначаться в GameController.cs) и поместив в верхний правый угол. Здесь будет отображаться имя авторизированного пользователя.

Назначение объектов контроллеру


Выберем объект GameController. На панели Inspector у компонента Game Controller есть несколько пустых полей, которые мы добавляли в коде ранее. Назначьте им соответствующие объекты, перетащив мышкой из панели Hierarchy или выбрав из списка после нажатия на кружок выбора у поля.

Тестирование


Мы подошли к заключительной части — проверки, что всё работает так, как надо. Запустим игру и нажмём на Esc. Перед нами откроется панель авторизации. Наберём данные зарегистрированного на сайте пользователя (в прошлой статье мы использовали habr@habrahabr.ru / habrahabr).

Нажмём на кнопку Войти. В случае успеха через некоторое время панель авторизации пользователя сменится на панель выхода, оставив только соответствующую кнопку, а вместо Аноним справа вверху будет написано Habr — имя пользователя с сайта.

Теперь, если снова нажать на Esc и поставить рекорд, он будет отправляться от авторизированного пользователя, а не от анонимного.

Это можно проверить, зайдя на страницу рекордов на сайте.

На этом мой первый туториал завершается. Буду рад ответить на вопросы по нему!

 
Полный код WWWScore.cs
 
Полный код GameController.cs