Pull to refresh

JSON в Unity за 10 минут

Level of difficultyMedium
Reading time9 min
Views1.8K

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

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

P. S.

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

Сериализация и десериализация

Сериализация

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

Десериализация

Процесс десериализации полностью противоположен сериализации. Под этим процессом понимают преобразование JSON строки назад в объект. Это делается для того, чтобы распарсить данные и продолжить работать с ними в коде в виде объектов конкретных классов.

Вид файла JSON

Формат JSON славится тем, что он интуитивно понятен и удобен для чтения человеком. Ниже приведу пример простого JSON файла:

{
	"Name": "Alex",
	"Age": 20,
	"Sex": 0
}

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

public enum Sex
{
    Male,
    Female
}
public class Person
{
    public string  Name { get; set; }
    public int Age { get; set; } 
    public Sex Sex { get; set; }
}

Можно заметить, что JSON хранит данные в формате «ключ: значение». В ранее приведенном файле ключам соответствуют строки «Name», «Age» и «Sex», а их значениям - «Alex», 20 и 0 соответственно. В большинстве языков программирования есть множество структур данных, которые работают по тому же формату «ключ: значение». Такие структуры данных, кстати, называют «ассоциативными». В .NET примером ассоциативной структуры данных может послужить класс Dictionary<T>.

Unity тоже использует JSON для хранения определенных данных о проекте. Например, в папке Packages есть сразу два таких файла: manifest.json и packages-lock.json.

Первый файл (manifest.json) содержит список всех зависимостей проекта, которые загружает Unity Package Manager. Структура этого файла выглядит следующим образом:

{
  "dependencies": {
    "com.unity.collab-proxy": "2.8.2",
    "com.unity.feature.2d": "2.0.1",
    "com.unity.ide.rider": "3.0.31",
    "com.unity.ide.visualstudio": "2.0.22",
    "com.unity.inputsystem": "1.11.2",
    "com.unity.multiplayer.center": "1.0.0",
    "com.unity.render-pipelines.universal": "17.0.3",
    "com.unity.test-framework": "1.4.5",
    "com.unity.timeline": "1.8.7",
    ...
  }
}

Здесь хорошо читается версия пакета для Visual Studio (6 строка), видна версия пакета URP пайплайна (9 строка) и множество другой информации о зависимостях.

Основные классы для работы

В Unity существует два основных класса для работы с JSON: JsonUtility и JsonConvert. JsonUtility является членом API UnityEngine, JsonConvert - членом API Newtonsoft.Json.

У каждого из этих инструментов есть свои минусы и плюсы. В этой статье мы разберем работу с API Newtonsoft.Json, в частности с классом JsonConvert. Поговорим о преимуществах этого инструмента

JsonConvert: Плюсы перед JsonUtility:

  • Умение работать с массивами, коллекциями (List<T>, Dictionary<T>), а также кастомными структурами.

  • Поддержка аттрибутов, которые позволяют гибко управлять сериализацией.

  • Возможность без труда редактировать свойства уже сериализованного объекта, помещенного в строку.

  • Возможность переопределять механизмы сериализации и десериализации на основе своих потребностей. 

Начало работы

Поскольку класс JsonUtility принадлежит пакету UnityEngine, для начала работы с ним достаточно подключить пространство имен через:

using UnityEngine;

Однако пакет Newtonsoft.Json требует установки извне.

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

Итак, наш алгоритм действий следующий:

  1. Устанавливаем инструмент NuGet в Unity проект

  2. Через NuGet устанавливаем пакет Newtonsoft.Json

Установка NuGet

Установить NuGet для Unity можно через github по ссылке здесь. На странице менеджера пакетов будет расписано несколько способов, с использованием которых мы можем внедрить NuGet в наш проект. Лично я в этом вопросе всегда пользуюсь преимуществом Package Manager: возможностью установки пакетов через github ссылку.

рис. 1
рис. 1

Во всплывающем окне нам достаточно вставить следующую ссылку в текстовое поле:

https://github.com/GlitchEnzo/NuGetForUnity.git?path=/src/NuGetForUnity

На сегодняшний день эта ссылка актуальна для версий Unity 2019.3 или новее.

Установка Newtonsoft.Json

После установки NuGet для Unity, в верхнем меню эдитора мы можем заметить появление следующего раздела:

рис. 2
рис. 2

Нажатием «Manage NuGet Packages» мы попадем во всплывающее окно, где нам и предстоит найти нужный нам пакет для установки:

рис. 3
рис. 3

Ввиду своей популярности, нужный нам пакет с большой вероятностью будет в ряду первых. Нажатием «Install» мы завершаем цикл подготовки проекта для работы с JSON.

Практика

JsonConvert предоставляет нам два основных метода для работы: SerializeObject() и DeserializeObject<T>(). Исходя из названий, можно сделать выводы, что первый метод отвечает за сериализацию объекта, второй - за десериализацию. Классическое применение этих двух методов в коде может выглядеть следующим образом:

// Процесс сериализации объекта data в строку json
string json = JsonConvert.SerializeObject(data);

// Процесс десериализации  из строки в объект data
T data = JsonConvert.DeserializeObject<T>(json);

Теперь предлагаю применить в коде полученные базовые знания: применить сериализацию и десериализацию, а далее посмотреть на результаты операций.

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

public class Coordinates
{
    public int X { get; set; }
    public int Y { get; set; }
    public int Z { get; set; }
}

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

Теперь создаем экземпляр этого класса, сериализуем его в строку, а далее записываем в файл:

private void Save()
{
    /*
     * Создаем экземпляр класса и вручную задаем значения полям.
     * (В реальном примере положение игрока могло бы передаваться,
     * например, через параметры метода Load)
    */
    Coordinates lastPosition = new()
    {
        X = 10,
        Y = 15,
        Z = -28
    };

    // Строка будет содержать сериализованный JSON объект
    string json = JsonConvert.SerializeObject(lastPosition);

    // Ключ отражает название создаваемого файла
    string key = "LastPosition.json";

    // Строка содержит путь, по которому будет храниться файл
    string path = Path.Combine(Application.persistentDataPath, key);

    // Создаем новый файл и через метод Write записываем сериализованный JSON объект
    using (StreamWriter writer = new(path))
    {
        writer.Write(json);
    }
}

Application.persistentDataPath - это строковое свойство API UnityEngine, которое содержит ссылку на специальную директорию, создаваемую автоматически для каждого Unity проекта. Найти ее можно по пути здесь:

«C:/Users/<Имя_Пользователя>/AppData/LocalLow/<Имя_Компании>/<Имя_Приложения>/».

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

Теперь нам осталось вызвать метод Load на старте проекта, а далее обратиться по ранее указанному пути в поисках файла LastPosition.json. Структура найденного файла имеет следующий вид:

{
	"X": 10,
	"Y": 15,
	"Z": -28
}

Отлично, с сериализацией разобрались. Теперь десериализуем:

private void Load()
{
    // Название файла
    string key = "LastPosition.json";

    // Путь к файлу
    string path = Path.Combine(Application.persistentDataPath, key);

    using (StreamReader reader = new(path))
    {
        string json = reader.ReadToEnd();

        // Десериализация с указанием конкретного типа
        Coordinates lastPosition = JsonConvert.DeserializeObject<Coordinates>(json);

        // Выводим все координаты для проверки
        Debug.Log(lastPosition.X);
        Debug.Log(lastPosition.Y);
        Debug.Log(lastPosition.Z);
    }
}

В результате вызова этого метода мы получаем следующие выводы в консоль:

рис. 4
рис. 4

Значения полностью соответствуют тем, которые мы указывали сериализуемому объекту, что говорит о успешной десериализации строки. Теперь мы можем продолжать работать с положением игрока в коде так, как нам нужно.

Вложенные типы

Мы уже видели, как выглядит сериализованная строка простого объекта в классе, где все свойства лежат на одном уровне. Однако что, если мы сериализуем объект класса, который содержит в себе объект еще одного класса? Наша задача увидеть, как JSON представляет такие классы. Вернемся к классу Person и чуть его дополним:

public enum Sex
{
    Male,
    Female
}
public class Person
{
    public string  Name { get; set; }
    public int Age { get; set; } 
    public Sex Sex { get; set; }
    public Address HomeAddress { get; set; } // Новое свойство класса
}

// Новый класс
public class Address
{
    public string City { get; set; }
    public string Street { get; set; }
    public int HouseNumber { get; set; }
}

Как мы можем заметить, класс теперь содержит не три свойства, а четыре, где четвертое свойство - это единственное свойство ссылочного типа. Задача такова: нам нужно сериализовать объект класса Person, а его сериализация будет означать и сериализацию объекта HomeAddress класса Address.

Благо JsonConvert удачно справляется с задачами сериализации и десериализации со вложенными объектами, поэтому общая структура кода с методом Save останется неизменной:

private void Save()
{
    // Создаем объект класса person и инициализируем все свойства
    Person person = new()
    {
        Name = "Katrine",
        Age = 19,
        Sex = Sex.Female,
        HomeAddress = new()
        {
            City = "Ottawa",
            Street = "Wellington Street",
            HouseNumber = 71
        }
    };

    // Меняем сериализуемый объект, передавая person на сериализацию
    string json = JsonConvert.SerializeObject(person);

    // Меняем название для создания нового файла
    string key = "Person.json";

    string path = Path.Combine(Application.persistentDataPath, key);

    using (StreamWriter writer = new(path))
    {
        writer.Write(json);
    }
}

Это уже всеми нами знакомый код с косметическими изменениями. Теперь вызовем наш метод на старте игры, найдем файл в нужной директории и посмотрим на его сигнатуру. Появившийся файл Person.json имеет следующее содержимое:

{
  "Name": "Katrine",
  "Age": 19,
  "Sex": 1,
  "HomeAddress":
  {
    "City": "Ottawa",
    "Street": "Wellington Street",
    "HouseNumber": 71
  }
}

Как мы видим, у нас появился дополнительный «уровень» в json строке, который объясняется наличием объекта ссылочного типа внутри сериализуемого экземпляра класса.

Основные атрибуты

Теперь разберем основные атрибуты, которые есть в Newtonsoft.Json и которые позволяют более гибко работать с JSON.

  • [JsonProperty] - позволяет указать полю уникальный ключ, отличный от названия сериализуемой переменной.

Пример использования:

public class Person
{
    // В .json файле поле сохраниться с ключом "n", вместо ключа "Name"
    [JsonProperty(PropertyName = "n")]
    public string  Name { get; set; }
  
    public int Age { get; set; } 
    public Sex Sex { get; set; }
    public Address HomeAddress { get; set; }
}
  • [JsonIgnore] - позволяет указать поле, которое сериализовать не нужно.

Пример использования:

public class Person
{
    [JsonIgnore]
    public string  Name { get; set; }

    public int Age { get; set; } 
    public Sex Sex { get; set; }
    public Address HomeAddress { get; set; }
}
  • [JsonRequired] - позволяет указать поле, которое в обязательном порядке должно быть сериализовано, иначе выбрасывается ошибка.

Пример использования:

public class Person
{
    [JsonRequired]
    public string  Name { get; set; }

    public int Age { get; set; } 
    public Sex Sex { get; set; }
    public Address HomeAddress { get; set; }
}

Дополнительные особенности Newtonsoft.Json

Пакет Newtonsoft.Json предоставляет и другие инструменты, которые обеспечивают полноценную и удобную работу с JSON. Например, пакет предоставляет нам класс JObject.

JObject

JObject - это что-то около промежуточного состояния между уже сериализованным в строку объектом и привычным .NET экземпляром.

Предположим, что у нас есть класс, состоящий из десяти свойств. Мы сериализовали объект этого класса, поместили в файл, а далее в коде у нас появляется необходимость изменить одно из десяти свойств в JSON файле. Вариант с тем, чтобы десериализовать файл, внести изменение одному свойству, а далее снова сериализовать - не лучший вариант.

Здесь нам на помощь и приходит класс JObject.

Основные методы и свойства JObject:

// Преобразует JSON строку в объект типа JObject
JObject obj = JObject.Parse(string json);

// Преобразует .NET объект в JObject
JObject netObj = JObject.FromObject(object obj);

/* Позволяет получать отдельные свойства из JSON строки
* в формате JToken, обращаясь к ним по ключу */
JToken x = obj["X"];

// Добавляет новое свойство к объекту
obj.Add("W", 10);

// Удаляет свойство по ключу
obj.Remove("W");

Здесь стоит добавить важное замечание, связанное с тем, что с использованием индексатора, как на 9 строке, обращаться к вложенным объектам классов не выйдет, поэтому для таких целей существует отдельный метод в JObject:

// Извлекает свойство в виде токена по ключу из вложенных классов
JToken cityToken = jObj.SelectToken("City");

В перечне основных методов для работы с JObject вы наверняка заметили мелькающий класс JToken. Что же он из себя представляет?

JToken

JToken представляет собой базовый класс для JObject. Более того, он является базовым классом еще для таких структур данных, как JArray, JValue и JProperty.

Каждый из этих классов предназначен для работы с JSON свойствами в разных форматах:

  • JObject - хранит JSON объект ( Например, { "Name": "Alex" } )

  • JArray - хранит JSON массив ( Например, [ 1, 2, 3 ] )

  • JValue - хранит JSON примитив в формате значения ( Например, 40, true или null )

  • JProperty - хранит свойство внутри JObject

Заключение

JSON - это мощный инструмент, который хорошо подходит для работы с данными в Unity. В этой статье мы разобрали ключевые аспекты работы с JSON, используя библиотеку Newtonsoft.Json. Для более углубленного понимания можно попрактиковаться в работе с сериализацией коллекций, а также переопределения процессов сериализации и десериализации с использованием абстрактного класса JsonConverter<T>.

Tags:
Hubs:
+1
Comments8

Articles