Чтение из json в Unity 5.6
Здравствуй, читатель!
В этой статье я хочу поделиться своим опытом решения проблемы передачи данных из файла в unity, но обо всем по порядку.
Задумал я написать прототип игры по собственному концепту, который разработал во время прохождения курса геймдизайнера, да добавить в нее редактор персонажа.
И хотелось мне, чтобы в редакторе были кнопки, соответствующие деталям персонажа (корпус, оружие), а дублировать кнопки руками и менять у каждой все переменные — не хотелось. Тогда то и задумался над вводом данных из файла.
Графическая составляющая
Первым делом набросал в Фотошопе разметку интерфейса:

Далее разметил это в unity:

После чего сделал префабы для кнопок категорий и кнопок конкретных частей.
Подготовка данных к чтению
Перед написанием кода необходимо определиться с параметрами, которые мы будем считывать из файла. Для категорий это всего два параметра: имя и код категории.
Код
{
	"name": "Ноги",
	"code": "legs"
}
  
Для частей игрока параметров больше и их структура сложнее: имя, категория, точка опоры, точки крепления.
На этом моменте уже можно набросать примерный json-файлик с описанием отдельных объектов.
Код
{
	"name": "leg1",
	"category": "legs",
	"anchorPoint": {
		"x": 0,
		"y": 0
	},
	"attachPoints": {
		"attachPointsArray": [
			{
				"x": 0,
				"y": 0
			}
		]
	}
}
  
Но создавать по файлику на каждую кнопку как-то неразумно. Нам нужен массив!
Код
{ "categories":
	[
		{
			"name": "Ноги",
			"code": "legs"
		},
		{
			"name": "Корпус",
			"code": "body"
		}
	]
}
  
Cам массив должен быть параметром некоего внешнего объекта.
 
Подготовка необходимых классов
И вот тут начинается веселье. C# строго типирован, и, как оказалось, вы не можете просто так разобрать json-строку в динамический объект, из которого потом будете забирать нужные данные. Нееет. Вам необходимо описать формат того, что будет считывать Unity из файла.
Здесь на помощь приходит вид/тип/семейство классов под тэгом Serializable. В unity 5 такие классы описываются следующим образом:
Код
using System.Collections.Generic;
[System.Serializable]
public class Category {
    public string name;
    public string code;
}
  
Класс! Этого достаточно, чтобы считать из json-строки один экземпляр. Как же быть с массивом? Нужно создать сериализуемый класс, содержащий один параметр — массив объектов нужного класса. (Ваш кэп)
Код
[System.Serializable]
public class CategoryList
{
    public List<Category> categories;
}
  
Теперь то всё? Теперь все — можем считать и разобрать наш json, содержащий массив описаний кнопок категорий.
Названия параметров, перечисленных в json, должны совпадать с параметрами сериализуемого класса.
 
Под спойлером описание с примером чуть более сложной структуры объекта.
using System.Collections.Generic;
[System.Serializable]
public class AnchorPoint
{
    public int x;
    public int y;
}
[System.Serializable]
public class AttachPoint
{
    public int x;
    public int y;
}
[System.Serializable]
public class AttachPointList
{
    public List<AttachPoint> attachPointsArray;
}
[System.Serializable]
public class PlayerPart
{
    public string name;
    public string category;
    public AnchorPoint anchorPoint;
    public AttachPointList attachPoints;
}
[System.Serializable]
public class PlayerPartsList
{
    public List<PlayerPart> parts;
}
  
Переходим к считыванию!
Для чтения и создания кнопок я создал класс UIinitializer, в который нужно передать шаблоны кнопок и соответствующие им контейнеры.
Код
using UnityEngine;
using System.IO; //нужно для чтения файлов
public class UIinitializer : MonoBehaviour {
    public GameObject categoryArea;
    public GameObject categoryPrefab;
    public GameObject selectPartArea;
    public GameObject partButtonPrefab;
    private string categoriesJson = "Assets/Configs/UI/categories.json";
    private string objectsJson = "Assets/Configs/PlayerParts.json";
    private CategoryList catList;
    private PlayerPartsList partsList;
    private Sprite[] objectSprites;
    // Use this for initialization
    void Start () {
       
    }
	
    // Update is called once per frame
    void Update () {
		
    }
}
  
Далее я написал два метода: чтение json из переданного файла и создание кнопок из префаба.
Код
private void readCategories()
{
    StreamReader strRead = new StreamReader(categoriesJson);
    string json = strRead.ReadToEnd();
    catList = JsonUtility.FromJson<CategoryList>(json);
}
private void createCategoriesButtons()
{
    int i = 0;
    foreach (Category cat in catList.categories)
    {
        GameObject button = Instantiate(categoryPrefab, categoryArea.transform);
        CategorySelectorScript buttonScript = button.GetComponent<CategorySelectorScript>();
        
        i++;
    }
}
  
Пишем скрипт, который будет управлять самой кнопкой, определяем в нем методы для установки параметров.
Код
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
public class CategorySelectorScript : MonoBehaviour {
    public string categoryName;
    private Text textField;
    private string categoryCode;
    private List<GameObject> linkedItems; //массив связанных 
	// Use this for initialization
	void Awake () {
        textField = this.GetComponentInChildren<Text>();
	}
	
	// Update is called once per frame
	void Update () {
		
	}
    public void PickCategory()
    {
        
        foreach(GameObject item in linkedItems)
        {
            item.SetActive(true);
        }
    }
    public void AddObjectToList(GameObject obj)
    {
        linkedItems.Add(obj);
    }
    public void SetName(string name)
    {
        categoryName = name;
        textField.text = categoryName;
    }
    public void SetCode(string code)
    {
        categoryCode = code;
    }
}
  
Добавляем передачу параметров и установку координат в наш класс-создатель:
Код
//после строки CategorySelectorScript buttonScript = button.GetComponent<CategorySelectorScript>();
button.GetComponent<RectTransform>().anchoredPosition = new Vector2(0, -i * offset + first_offset);
buttonScript.SetName(cat.name);
buttonScript.SetCode(cat.code);
  
Готово! Мы великолепны, теперь после загрузки сцены игра считает json, создает и размещает кнопки без нашего участия.
Под спойлером код для создания кнопок в нижней части.
Полный листинг инициализирующего скрипта
using UnityEngine;
using System;
using System.IO;
public class UIinitializer : MonoBehaviour {
    public GameObject categoryArea;
    public GameObject categoryPrefab;
    public GameObject selectPartArea;
    public GameObject partButtonPrefab;
    public int offset = 20;
    public int first_offset = -14;
    private string categoriesJson = "Assets/Configs/UI/categories.json";
    private string objectsJson = "Assets/Configs/PlayerParts.json";
    private CategoryList catList;
    private PlayerPartsList partsList;
    //для подгрузки спрайта из атласа
    private Sprite[] objectSprites;
    // Use this for initialization
    void Start () {
        objectSprites = Resources.LoadAll<Sprite>("Sprites/objectsAtlas");
        readCategories();
        //считываем объекты
        readObjects();
        createCategoriesButtons();
    }
	
	// Update is called once per frame
	void Update () {
		
	}
    private void createCategoriesButtons()
    {
        int i = 0;
        foreach (Category cat in catList.categories)
        {
            GameObject button = Instantiate(categoryPrefab, categoryArea.transform);
            CategorySelectorScript buttonScript = button.GetComponent<CategorySelectorScript>();
            button.GetComponent<RectTransform>().anchoredPosition = new Vector2(0, -i * offset + first_offset);
            buttonScript.SetName(cat.name);
            buttonScript.SetCode(cat.code);
            //добавили этот цикл для создания кнопок в нижней части экрана
            foreach (PlayerPart part in partsList.parts)
            {
                if (cat.code == part.category)
                {
                    GameObject partButton = Instantiate(partButtonPrefab, selectPartArea.transform);
                    PlayerPartButton partButtonScript = partButton.GetComponent<PlayerPartButton>();
                    Sprite newSprite = getSpriteByName(part.name);
                    partButtonScript.SetConfig(part.name, part.anchorPoint, part.attachPoints, newSprite);
                }
            }
            i++;
        }
    }
    private void readCategories()
    {
        StreamReader strRead = new StreamReader(categoriesJson);
        string json = strRead.ReadToEnd();
        catList = JsonUtility.FromJson<CategoryList>(json);
    }
    private void readObjects()
    {
        StreamReader strRead = new StreamReader(objectsJson);
        string json = strRead.ReadToEnd();
        partsList = JsonUtility.FromJson<PlayerPartsList>(json);
    }
    //получаем спрайт из атласа по имени
    private Sprite getSpriteByName(string sprName)
    {
        foreach (Sprite spr in objectSprites)
        {
            if (spr.name == sprName) return spr;
        }
        return new Sprite();
    }
}
Описание контроллера кнопок в нижней части:
using UnityEngine;
using UnityEngine.UI;
public class PlayerPartButton: MonoBehaviour {
    private AnchorPoint anchor;
    private AttachPointList attachPointList;
    private string spriteName;
    private Image sprite;
	// Use this for initialization
	void Awake () {
        sprite = this.GetComponent<Image>();
	}
	
	// Update is called once per frame
	void Update () {
		
	}
    public void SetConfig(string newName, AnchorPoint newAnchor, AttachPointList newAtList, Sprite newSprite)
    {
        spriteName = newName;
        anchor = newAnchor;
        attachPointList = newAtList;
        sprite.sprite = newSprite;
    }
}
  
Резюмируя, алгоритм для работы с данными из конфигов следующий:
- Продумать нужные параметры
 
- Создать сериализуемые классы с этими параметрами, если планируете получать массивы — классы с параметром-массивом классов.
 
- Получить json в коде
 
- Присвоить считанные данные нужным переменным/выполнить какие-то действия
 
Надеюсь, данная статья была вам полезна. Когда я сам с этим разбирался — мне не хватило такого руководства.
А пока я пишу следующий билд, можете подписаться на цель в смартпрогрессе, поделиться опытом и мнением здесь или в комментариях к цели. 
Также предлагаю прямо в браузере поиграть в мою первую игру на unity и рассказать о своих результатах (мой рекорд — 120 очков, кто больше?). Буду рад обратной связи, до новых статей!