Продолжение статьи про добавление рекордов из игры на сайт от конкретного пользователя. В первой части мы сделали страничку рекордов на 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());
Напишем и эту функцию.
[Serializable]
public class TokenResponse
{
public string access_token;
}
public class WWWScore : MonoBehaviour {
// ...
public IEnumerator login(string email, string password)
{
WWWForm form = new WWWForm();
form.AddField("grant_type", "password");
form.AddField("client_id", "3");
form.AddField("client_secret", "W82LfjDg4DpN2gWlg8Y7eNIUrxkOcyPpA3BM0g3s");
form.AddField("username", email);
form.AddField("password", password);
form.AddField("scope", "*");
WWW w = new WWW(serverURL + "oauth/token", form);
yield return w;
if (!string.IsNullOrEmpty(w.error))
{
Debug.Log(w.error);
}
else
{
TokenResponse tokenResponse = JsonUtility.FromJson<TokenResponse>(w.text);
if (tokenResponse == null)
{
Debug.Log("Конвертирование не удалось!");
}
else
{
// Сохраняем токен в настройках
PlayerPrefs.SetString("Token", tokenResponse.access_token);
Debug.Log("Токен установлен!");
// Запрашиваем имя пользователя
StartCoroutine(getUserInfo());
}
}
}
}
Получение информации о пользователе
Нам нужно выполнить 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);
// Класс TokenResponse
// ...
[Serializable]
public class UserInfo
{
public string name;
}
public class WWWScore : MonoBehaviour {
// ...
// Функция login
// ...
public IEnumerator getUserInfo()
{
Dictionary<string, string> headers = new Dictionary<string, string>();
headers.Add("Authorization", "Bearer " + PlayerPrefs.GetString("Token"));
WWW w = new WWW(serverURL + "api/user", null, headers);
yield return w;
if (!string.IsNullOrEmpty(w.error))
{
Debug.Log(w.error);
}
else
{
UserInfo userInfo = JsonUtility.FromJson<UserInfo>(w.text);
if (userInfo == null)
{
Debug.Log("Конвертирование не удалось!");
}
else
{
// Сохраняем токен в настройках
PlayerPrefs.SetString("UserName", userInfo.name);
Debug.Log("Имя пользователя установлено!");
}
}
}
}
Изменения в коде добавления рекордов
Чтобы добавлять рекорды от авторизированного пользователя, мы немного изменим код функции 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);
}
public IEnumerator addRecord(int score)
{
WWWForm form = new WWWForm();
form.AddField("score", score);
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);
}
yield return w;
if(!string.IsNullOrEmpty(w.error)) {
Debug.Log(w.error);
} else {
Debug.Log("Рекорд добавлен!");
}
}
Код выхода
Чтобы выйти за пользователя из игры, необходимо удалить все данные о нем в настройках. В нашем случае у нас нет никаких других настроек, поэтому мы просто очищаем все настройки. Будьте аккуратнее с этим в своих проектах.
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 и поставить рекорд, он будет отправляться от авторизированного пользователя, а не от анонимного.
Это можно проверить, зайдя на страницу рекордов на сайте.
На этом мой первый туториал завершается. Буду рад ответить на вопросы по нему!
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[Serializable]
public class TokenResponse
{
public string access_token;
}
[Serializable]
public class UserInfo
{
public string name;
}
public class WWWScore : MonoBehaviour {
[SerializeField]
private string serverURL = "http://127.0.0.1:8000/";
public IEnumerator addRecord(int score)
{
WWWForm form = new WWWForm();
form.AddField("score", score);
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);
}
yield return w;
if(!string.IsNullOrEmpty(w.error)) {
Debug.Log(w.error);
} else {
Debug.Log("Рекорд добавлен!");
}
}
public IEnumerator login(string email, string password)
{
WWWForm form = new WWWForm();
form.AddField("grant_type", "password");
form.AddField("client_id", "3"); // Пример заполнения
form.AddField("client_secret", "W82LfjDg4DpN2gWlg8Y7eNIUrxkOcyPpA3BM0g3s"); // Пример заполнения
form.AddField("username", email);
form.AddField("password", password);
form.AddField("scope", "*");
WWW w = new WWW(serverURL + "oauth/token", form);
yield return w;
if (!string.IsNullOrEmpty(w.error))
{
Debug.Log(w.error);
}
else
{
TokenResponse tokenResponse = JsonUtility.FromJson<TokenResponse>(w.text);
if (tokenResponse == null)
{
Debug.Log("Конвертирование не удалось!");
}
else
{
// Сохраняем токен в настройках
PlayerPrefs.SetString("Token", tokenResponse.access_token);
Debug.Log("Токен установлен!");
// Запрашиваем имя пользователя
StartCoroutine(getUserInfo());
}
}
}
public IEnumerator getUserInfo()
{
Dictionary<string, string> headers = new Dictionary<string, string>();
headers.Add("Authorization", "Bearer " + PlayerPrefs.GetString("Token"));
WWW w = new WWW(serverURL + "api/user", null, headers);
yield return w;
if (!string.IsNullOrEmpty(w.error))
{
Debug.Log(w.error);
}
else
{
UserInfo userInfo = JsonUtility.FromJson<UserInfo>(w.text);
if (userInfo == null)
{
Debug.Log("Конвертирование не удалось!");
}
else
{
// Сохраняем токен в настройках
PlayerPrefs.SetString("UserName", userInfo.name);
Debug.Log("Имя пользователя установлено!");
}
}
}
public void logout()
{
PlayerPrefs.DeleteAll();
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
// Класс сковородки
[System.Serializable]
public class PanClass
{
// Префаб сковородки
public GameObject panObj;
// Пауза до начала падения сковородок
public float start;
// Пауза между сковородками
public float pause;
}
public class GameController : MonoBehaviour {
// Объект сковородки
public PanClass pan;
// Точка спавна
public Vector2 spawnValues;
// Объект с интерфейсом результата
public GameObject scoreText;
// Объект с интерфейсом рестарта игры
public GameObject restartText;
// Объект с интерфейсом панели паузы
public GameObject pausePanel;
// Время между повышениями результата
public float scoreRate = 1.0F;
// Значение, на которое повышается результат
public int scoreAdd = 10;
// Результат
public static int score;
// Признак завершения игры
public static bool gameover;
// Время до следующего результата
private float nextScore = 0.0F;
// Признак того, что единоразовые действия после конца игры были выполнены
private bool gameoverStarted;
// Объект авторизации
public GameObject loginObj;
// Объект выхода
public GameObject logoutObj;
// Поле E-mail
public GameObject inputFieldEmail;
// Поле Пароль
public GameObject inputFieldPassword;
// Надпись с именем пользователя
public GameObject userNameText;
void Start () {
// Инициализация значений (для рестарта)
gameover = false;
score = 0;
gameoverStarted = false;
// Запустить падение сковородок
StartCoroutine(panSpawn());
}
void FixedUpdate()
{
if (!gameover)
{
// Обновить результат
scoreText.GetComponent<Text>().text = score.ToString();
}
}
void Update () {
if (gameover){
// Действия, выполняемые только один раз после конца игры до рестарта
if (!gameoverStarted) {
gameoverStarted = true;
// Отобразить интерфейс рестарта
restartText.SetActive(true);
// Отправляем рекорд
StartCoroutine(GetComponent<WWWScore>().addRecord(score));
}
// Рестарт по R
if (Input.GetKey(KeyCode.R))
{
// Перезапуск сцены
SceneManager.LoadScene(0);
}
} else {
if (Input.GetKeyDown(KeyCode.Escape))
{
if (Time.timeScale != 0) {
// Поставить на паузу
Time.timeScale = 0;
pausePanel.SetActive(true);
} else {
// Снять с паузы
Time.timeScale = 1;
pausePanel.SetActive(false);
}
}
}
// Подсчет результата
if (!gameover && (Time.time > nextScore))
{
nextScore = Time.time + scoreRate;
score = score + scoreAdd;
}
setUserName();
setLoginVisible();
}
// Падение сковородки
IEnumerator panSpawn()
{
// Пауза до начала падения сковородок
yield return new WaitForSeconds(pan.start);
// Бесконечный цикл, до конца игры
while (!gameover)
{
// Генерировать крутящуюся сковородку в случайном месте на определенной высоте
Vector2 spawnPosition = new Vector2(Random.Range(-spawnValues.x, spawnValues.x), spawnValues.y);
Quaternion spawnRotation = Quaternion.identity;
Instantiate(pan.panObj, spawnPosition, spawnRotation);
yield return new WaitForSeconds(pan.pause);
}
}
// Авторизация
public void login()
{
var email = inputFieldEmail.GetComponent<InputField>().text;
var password = inputFieldPassword.GetComponent<InputField>().text;
StartCoroutine(GetComponent<WWWScore>().login(email, password));
}
// Выход
public void logout()
{
GetComponent<WWWScore>().logout();
}
// Поменять видимость формы авторизации
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 = "Аноним";
}
}
}
Первая часть
Готовый проект на Laravel
Базовый проект на Unity (ветка master
)
Готовый проект на Unity (ветка final
)
Автор: KuzyT