Давным-давно, в далеком трехмерном мире, герою-одиночке нужно было создать идеальную игру — это, кстати, вы.

Одна игра, чтобы править всеми. Единственная игра, которая могла бы избежать ловушек локальных настроек игрока, избежать головной боли, связанной с безопасным созданием пользователей и управлением ими, в то же время совместимой с данными и способной предоставить информацию для корректировок почти в реальном времени. Представляем вам Сэма и Фродо, Unity3D и Open Cider API.

Перво-наперво…

Open Cider API — это инструмент для упрощения разработки за счет абстрагирования управления пользователями в реализации, которая является прозрачной, ориентированной на пользователя и простой в использовании — это означает, что вам не нужно беспокоиться ни о чем другом, кроме вашего приложения.

В этой статье мы будем использовать API сводных данных Open Cider, который позволяет нам хранить часто используемые данные, такие как предпочтения игроков, конфигурации, рекорды и т. д.

Конечно, в основе лежит предположение, что вы начали свое путешествие с Unity3D. Хотя это путешествие и для Сэма, и для Фродо, весь Open Cider — это Сэм для уже существующего Фродо. Если вы не знакомы ни с одним из них, добавьте эту страницу в закладки, начните здесь — и вернитесь, когда закончите.

Сделав это, давайте пристегнемся, включим наш любимый плейлист и зажигаем.

Настройка сцены

Мы начнем с создания нового проекта 3D Unity — назовите его как хотите. В примере сцены мы собираемся создать куб с именем «Земля» и масштабировать его по осям X и Z до 40 единиц, что эквивалентно 40 метрам игрового пространства. Бьюсь об заклад, вы благодарите Бога, что обратили внимание на уроке геометрии прямо сейчас.

Далее мы собираемся добавить персонажа от первого лица, импортировав его из менеджера пакетов, например so. Небольшое замечание: если у вас его нет в диспетчере пакетов, вам может потребоваться получить его из магазина активов.

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

Поздравляю! Мы на полпути. Ну, почти. Нам нужен обратный отсчет от 60 секунд и несколько монет, чтобы создать игру поймай их всех.

Создайте папку Prefabs в папке Assets для размещения наших CoinPrefab, CountDownPrefab и GameControllerPrefab.

Начнем с CoinPrefab. Нажмите на + в левом верхнем углу и создайте в сцене пустой объект с именем CoinPrefab. Внутри этого объекта добавьте куб и переименуйте его в CoinGO для CoinGameObject. Очевидно, что вы можете использовать для этого настоящую модель монеты, и есть отличные платформы для получения бесплатных активов, но пока мы будем придерживаться обычного куба, масштабируемого до 0,3 единицы по всем трем осям. Затем мы добавим сценарий CoinController.cs, чтобы придать ему некоторое поведение.

using UnityEngine;

public class CoinController : MonoBehaviour
{
    private static float _rotationSpeed = 90f;

    private GameController _gameControllerRef;
    private GameObject _coinGameObject;


    void Awake()
    {
        //Get reference to Game Controller
        _gameControllerRef = GameObject.FindGameObjectWithTag("GameController")
        .GetComponent<GameController>();

        //Get reference to gameobject child in position 0
        _coinGameObject = transform.GetChild(0).gameObject;
        
        
        //throw game if refs are not present
        if (_coinGameObject == null || _gameControllerRef == null)
        Debug.LogError("Object ref(s) not Found!!!");

        //Tilt coin for rotation
        _coinGameObject.transform.Rotate(35f, 0f, 0f);
    }


    void FixedUpdate() {
        Spin();
    }

    void OnTriggerEnter(Collider other) {
        //Confirm that collider is player
        if (other.CompareTag("Player")) {
            _gameControllerRef.IncrementCoinsCollected();
            Destroy(this.gameObject);
        } //Else ignore...
    }

    private void Spin() {
        _coinGameObject.transform.Rotate(Vector3.up * Time.deltaTime * _rotationSpeed, Space.World);
    }
}

При этом у нас должна быть вращающаяся кубическая монета, которая увеличивает монеты, собранные в (еще не реализованном) скрипте GameController.cs.

Далее мы создадим CountdownPrefab.

Откройте экземпляр NestedParent_Unpack в иерархии, затем нажмите UI_Canvas_StarterAssetInputs_Joysticks. Это уже созданный пользовательский интерфейс, предоставленный Unity вместе с активом от первого лица. Там мы создадим пустой объект, который назовем CountdownPrefab, и добавим TextMeshPro к объекту, который также переименуем в CountdownText.

Установите Pos X TextMesh на -700 и Pos Y на 300. Мы также выровняем его по центру, используя основные настройки. Наконец, добавьте к этому функциональность, создав скрипт на префабе с именем CountdownController.cs и добавив следующее:

using UnityEngine;
using TMPro;

public class CountdownController : MonoBehaviour
{
    public int Seconds = 0, Minutes = 2;

    private bool _isValid = false;
    private GameController _gameControllerRef;
    private TextMeshProUGUI _timerText;


    /* Get Reference for Game Controller & TextMeshProUGUI */
    void Awake()
    {
        _gameControllerRef = GameObject.FindGameObjectWithTag("GameController")
        .GetComponent<GameController>();

        _timerText = GetComponentInChildren<TextMeshProUGUI>();

        if (_timerText == null || _gameControllerRef == null) {
            Debug.LogError("Error Finding Game References");
            Debug.Break();
        }

        //Poll every second...
        InvokeRepeating("Poll", 1f, 1f);
    }

    void Poll()
    {
        CountDown();
        TimeFormatting();
    }


    private void CountDown() {
        if (!_isValid) {
            if (Seconds == 0) {
                if (Minutes == 0) {
                    _isValid = true;
                    _gameControllerRef.SayGameOver();
                } else {
                    Seconds = 59;
                    Minutes -= 1;
                }
            } else {
                 Seconds -= 1;
            }
        }
    }

    private void TimeFormatting() {
        string minuteText = "", secondText = "";

        if (Minutes < 10) minuteText = "0" + Minutes; else minuteText = Minutes.ToString();

        if (Seconds < 10) secondText = "0" + Seconds; else secondText = Seconds.ToString();

        _timerText.text = $"{minuteText}:{secondText}";
    }
}

Теперь мы переходим к последней части — GameController. Дизайн предназначен для того, чтобы отдельные компоненты взаимодействовали с GameController, который затем принимает решение об общем состоянии игры на основе предоставленной информации. Мы можем видеть примеры этого в двух вызовах функций Game Controller Ref.

Здесь мы просто создадим GameControllerPrefab в Hierarchy, изменим Tag на GameController в Inspector и добавим скрипт GameController.cs к объекту, а затем сделаем его префабом.

P.S: Чтобы создать префабы, перетащите объекты из Hierarchy в папку Asset.

Теперь для этого нам нужны две функции, а именно: IncrementCoinsCollected() и SayGameOver().

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

[HideInInspector] public int CoinsCollected = 0;

private bool _isGameOver = false;

public void IncrementCoinsCollected() {
    if (!_isGameOver)
    CoinsCollected += 1;
}

Затем переходим ко второй функции. Здесь мы устанавливаем переменную _isGameOver в значение true. Мы также сейчас представим Open Cider API. Здесь мы отправим CoinsCollected с помощью API сводных данных. Это выглядит так:

/* POST Summary Data */
[Serializable]
public class SummaryDataPostRequest{
    public string token;
    public string metric0;
    public int metric1;
    public float metric2;
    public int metric3;
    public float metric4;
}

[Serializable]
public class SummaryDataPostResponse{
    public string status;
    public string message;
}

private IEnumerator UploadScore() {
    var request = new SummaryDataPostRequest();
    
    request.token   = _token;
    request.metric0 = "Some player config value here maybe";
    request.metric1 = CoinsCollected;
    /*We don't care about the rest */
    request.metric2 = 0;
    request.metric3 = 0;
    request.metric4 = 0;

    var json = JsonUtility.ToJson(request);

    var uwr = new UnityWebRequest(_url, "POST");
    byte[] jsonToSend = new System.Text.UTF8Encoding().GetBytes(json);
    uwr.uploadHandler = (UploadHandler)new UploadHandlerRaw(jsonToSend);
    uwr.downloadHandler = (DownloadHandler)new DownloadHandlerBuffer();
    uwr.SetRequestHeader("Content-Type", "application/json");

    //Send the request then wait here until it returns
    yield return uwr.SendWebRequest();

    if (uwr.result == UnityWebRequest.Result.ConnectionError)
    {
        Debug.Log("Error While Sending: " + uwr.error);
    }
    else
    {
        var response = JsonUtility.FromJson<SummaryDataPostResponse>(uwr.downloadHandler.text);
        Debug.Log("Received: " + response.message);
    }
}

Мы создали объект запроса POST и сериализовали его с помощью JsonUtility. Затем мы отправили запрос и десериализовали ответ в объект Response. Для получения дополнительной информации об объектах запроса и ответа для Open Cider API посетите страницу документации.

Окончательный объект Game Controller выглядит так:

using System;
using System.Collections;
using UnityEngine;
using UnityEngine.Networking;

public class GameController : MonoBehaviour
{
    [HideInInspector] public int CoinsCollected = 0;

    private bool _isGameOver = false;

    private static string _url  = "https://api.opencider.com/v1/user/query/summary-data";
    private static string _token = "SERVICE_USER_TOKEN";

    
    #region Events
    public void IncrementCoinsCollected() {
        if (!_isGameOver)
        CoinsCollected += 1;
    }

    public void SayGameOver() {
        _isGameOver = true;
        Debug.LogWarning("Posting Score to Open Cider. Score: " + CoinsCollected);
        StartCoroutine(UploadScore());
    }
    #endregion


    /* POST Summary Data */
    [Serializable]
    public class SummaryDataPostRequest{
        public string token;
        public string metric0;
        public int metric1;
        public float metric2;
        public int metric3;
        public float metric4;
    }

    [Serializable]
    public class SummaryDataPostResponse{
        public string status;
        public string message;
    }

    private IEnumerator UploadScore() {
        var request = new SummaryDataPostRequest();
        
        request.token   = _token;
        request.metric0 = "Some player config value here maybe";
        request.metric1 = CoinsCollected;
        /*We don't care about the rest */
        request.metric2 = 0;
        request.metric3 = 0;
        request.metric4 = 0;

        var json = JsonUtility.ToJson(request);

        var uwr = new UnityWebRequest(_url, "POST");
        byte[] jsonToSend = new System.Text.UTF8Encoding().GetBytes(json);
        uwr.uploadHandler = (UploadHandler)new UploadHandlerRaw(jsonToSend);
        uwr.downloadHandler = (DownloadHandler)new DownloadHandlerBuffer();
        uwr.SetRequestHeader("Content-Type", "application/json");

        //Send the request then wait here until it returns
        yield return uwr.SendWebRequest();

        if (uwr.result == UnityWebRequest.Result.ConnectionError)
        {
            Debug.Log("Error While Sending: " + uwr.error);
        }
        else
        {
            var response = JsonUtility.FromJson<SummaryDataPostResponse>(uwr.downloadHandler.text);
            Debug.Log("Received: " + response.message);
        }
    }
}

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

Поздравляем!

Очевидно, что это только введение, но ясно, что с меньшим количеством строк кода нам больше не нужно беспокоиться о базах данных и системах управления пользователями без ущерба для качества наших систем.

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

В нашем примере мы сохранили только собранные монеты, но с другими предоставленными метриками мы могли бы, например, сохранить время, в течение которого они были собраны. Единственным ограничением здесь является воображение.

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

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

Вы также получаете несколько бесплатных кредитов для тестирования при регистрации.

Краткое содержание

Мы рассмотрели, как использовать Open Cider API, чтобы сосредоточиться на создании наиболее важных частей нашего приложения. Платформа предоставляет несколько других преимуществ, таких как хранилище только для добавления, а также предоставление конечным пользователям полномочий в отношении того, как службы получают доступ к их данным, тем самым укрепляя доверие ваших пользователей.

Код, используемый в этой статье, можно найти на GitHub: https://github.com/open-cider/tax-collector-game-example.

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