Importing spreadsheet data to Unity

I’d really like to see how we’d go about importing spreadsheet data into Unity via script. I’ve had a look around YouTube and maybe it’s not all that complicated but I think having a lecture on it at this point in the course would be really valuable, especially to see how we could work it into our existing Progression asset. Maybe there’s already a lecture on this sort of thing in the dialogue or inventory courses (I took a look at the titles but didn’t notice it at a glance) so apologies if that’s the case, but if not, consider this feedback :grin:

3 Likes

It didn’t end up in the course. There is a good utility for this with a tutorial here

1 Like

Hello Brian, can you reload the link please? i

Unfortunately, it appears that the link is dead.
Here’s a github link to a similar utility: https://github.com/mikito/unity-excel-importer

1 Like

I agree that being able to take the spreadsheet data and have it load in to Unity would be very handy. It can cut out the time of having to retype all the work you made in the sheet.

The scripts you make can have easy access to the file to load in the needed data such as in this course the information for the progression.

Then in the future lessons such as with questions, shops and inventory, this same idea could be used with adding additional spreadsheets for those different topics so things say separate and organized or keep things in one spreadsheet book with different sheets and having them named accordingly. Depending on your personal preference and what would keep things organized best for you and your game.

Would this be a lecture that we may be able to see in the future? So we may be able to learn how to create this setup ourselves?

I found this video really helpful and I can share my modified scripts if anybody wants.
How to load Excel Spreadsheet Data into Unity - YouTube

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

public class CVSLoader // Скрипт качает текст с таблицы
{    
    public static IEnumerator DownloadRawCvsTable(string sheetId, string pageId, Action<string> onSheetLoadedAction)
    {       
        string actualUrl = $"https://docs.google.com/spreadsheets/d/{sheetId}/export?format=csv&gid={pageId}";
        using (UnityWebRequest request = UnityWebRequest.Get(actualUrl))
        {
            yield return request.SendWebRequest();
            if (request.result == UnityWebRequest.Result.ConnectionError || request.result == UnityWebRequest.Result.ProtocolError ||
                request.result == UnityWebRequest.Result.DataProcessingError)
            {
                Debug.LogError(request.error);
            }
            else
            {              
                Debug.Log("Успешная загрузка");
                Debug.Log(request.downloadHandler.text);

                onSheetLoadedAction(request.downloadHandler.text);
            }

        }
        yield return null;
    }
}

I don’t use some methods in this class, it may be useful for others

using RPG.Stats;
using System;
using System.Collections.Generic;
using UnityEngine;

public class SheetProcessor  // Обрабатывает исходный текст
{
    private const char _cellSeporator = ','; // Разделитель ячеек в исходном тексте
    private const char _inCellSeporator = ';'; // Разделитель значений внутри одной ячейки
 
    /// <summary>
    /// Преобразуем сырой текст в двумерный массив строк<br/>
    /// a1,b1,c1,d1<br/>
    /// a2,b2,c2,d2<br/>
    /// a3,b3,c3,d3 
    /// </summary>
    /// <returns>Вернем массив с индексами - [номер строки, номер столбца]</returns>
    public static string[,] ProcessData(string cvsRawData) 
    {  
        SheetProcessor sheetProcessor = new SheetProcessor(); // Т.к. это статический метод то сначала создадим экземляр данного класса а потом вызывим у него нужный метод
        char lineEnding = sheetProcessor.GetPlatformSpecificLineEnd(); // Получить конец строки для конкретной платформы

        string[] rows = cvsRawData.Split(lineEnding); // Разделим на массив строк 
        string[] cells = rows[0].Split(_cellSeporator); // Разделим первую строку[0] на массив ячеек с помощью _cellSeporator и получим массив ячеек из которого узноем число столбцов

        string[,] gridDateArray = new string[rows.Length, cells.Length]; // Инициализируем двумерный массив определенного размера

        for (int rowNamber = 0; rowNamber < rows.Length; rowNamber++) // Переберем строки
        {
            cells = rows[rowNamber].Split(_cellSeporator); // Разделим строку на массив ячеек с помощью _cellSeporator 
            
            for (int columnNumber = 0; columnNumber< cells.Length; columnNumber++) // Перберем ячейки в данной строке
            {
                gridDateArray[rowNamber,columnNumber] = cells[columnNumber]; // Сохраним данные ячейки в двумерном массиве где [rowNamber,columnNumber] это будут индексы массива.
            }                 
        }        
        return gridDateArray;
    }

    private Color ParseColor(string color)
    {
        color = color.Trim();
        Color result = default;
        if (_colors.ContainsKey(color))
        {
            result = _colors[color];
        }

        return result;
    }

    private Vector3 ParseVector3(string s)
    {
        string[] vectorComponents = s.Split(_inCellSeporator);
        if (vectorComponents.Length < 3)
        {
            Debug.Log("Can't parse Vector3. Wrong text format");
            return default;
        }

        float x = ParseFloat(vectorComponents[0]);
        float y = ParseFloat(vectorComponents[1]);
        float z = ParseFloat(vectorComponents[2]);
        return new Vector3(x, y, z);
    }

    public static int ParseInt(string s)
    {
        int result = -1;
        if (!int.TryParse(s, System.Globalization.NumberStyles.Integer, System.Globalization.CultureInfo.GetCultureInfo("en-US"), out result))
        {
            Debug.Log("Не удается разобрать int, неправильный текст");
        }

        return result;
    }

    public static CharacterClass ParseCharacterClass(string s)
    {     
        object character;
        if (!Enum.TryParse(typeof(CharacterClass), s, out character))
        {
            Debug.Log("Не удается разобрать CharacterClass, неправильный текст");
        }
        return (CharacterClass)character;
    }

    public static Stat ParseStat(string s)
    {
        object character;
        if (!Enum.TryParse(typeof(Stat), s, out character))
        {
            Debug.Log("Не удается разобрать Stat, неправильный текст");
        }
        return (Stat)character;
    }

    private float ParseFloat(string s)
    {
        float result = -1;
        if (!float.TryParse(s, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.GetCultureInfo("en-US"), out result))
        {
            Debug.Log("Can't pars float,wrong text ");
        }

        return result;
    }
      

    private char GetPlatformSpecificLineEnd() // Платформеная спецификация
    {
        char lineEnding = '\n'; // Для Windows & Android
#if UNITY_IOS
        lineEnding = '\r'; // Для IOS
#endif
        return lineEnding;
    }
}

Well, the last modified script “Progression”

using System.Collections.Generic;
using Unity.EditorCoroutines.Editor;
using UnityEngine;

namespace RPG.Stats
{
    [CreateAssetMenu(fileName = "Progression", menuName = "Stats/New Progression", order = 0)]
    public class Progression : ScriptableObject
    {   
        private const int _сharacterСolumnIndex = 0;
        private const int _statsСolumnIndex = 1;


        /// <summary>
        /// Идентификатор таблицы в Google таблицах.
        /// Допустим, ваша таблица имеет следующий URL-адрес https://docs.google.com/spreadsheets/d/1RWQJpSrdddz5TZS2YE-exPVPnWPmXQSBCOjAgCsz-9E/edit#gid=19912915
        /// В этом случае идентификатор таблицы равен _sheetId = "1RWQJpSrdddz5TZS2YE-exPVPnWPmXQSBCOjAgCsz-9E" и _pageId "19912915" (the gid parameter).
        /// </summary>

        [SerializeField] private string _sheetId;
        [SerializeField] private string _pageId;
        [SerializeField] private List<ProgressionCharacterClass> _characterClassList = null;

        private Dictionary<CharacterClass, Dictionary<Stat, float[]>> lookupTable = null; //хэшированная таблица поиска


        [ContextMenu("OpenSheet")]
        private void Open()
        {
            Application.OpenURL($"https://docs.google.com/spreadsheets/d/{_sheetId}/edit#gid={_pageId}");
        }

        [ContextMenu("Sync")]
        private void Sync() // Синхронизация данных
        {
            EditorCoroutineUtility.StartCoroutineOwnerless(CVSLoader.DownloadRawCvsTable(_sheetId, _pageId, CompleteTable));// Этот метод запускает EditorCoroutine без объекта-владельца.EditorCoroutine выполняется до тех пор, пока не завершится или не будет отменен с помощью StopCoroutine(EditorCoroutine).
        }

        private void CompleteTable(string rawCVSText) //Заполнение таблицы (Делегат) Вызывается когда закачается сырой исходный CVSText
        {
            string[,] gridDateArray = SheetProcessor.ProcessData(rawCVSText); // Получим преобразованный текст в двумерный массив

            // Инициализирум классы которые будем создавать и заполнять
            _characterClassList = new List<ProgressionCharacterClass>(); //Инициализируем список персонажей (1 колонка)
            ProgressionCharacterClass progressionCharacterClass = null;
            ProgressionStat progressionStat = null;

            int starTrowNamber = 1; // Будем переберать со 2й строки, 1-это шапка таблицы
            for (int rowNamber = starTrowNamber; rowNamber < gridDateArray.GetLength(0); rowNamber++) //переберем стоки // GetLength(0)- вернет длину первой части двумерного массива  (если [4,5] - то получим 4 )
            {
                CharacterClass character = SheetProcessor.ParseCharacterClass(gridDateArray[rowNamber, _сharacterСolumnIndex]); // Преобразуем данные из ячейки в CharacterClass
                Stat stat = SheetProcessor.ParseStat(gridDateArray[rowNamber, _statsСolumnIndex]);

                //Если список не пустой то - Сравним с последним персонажем в списке и если это другой персонаж то создадим его
                if (_characterClassList.Count == 0 || character != _characterClassList[_characterClassList.Count - 1].characterClass)
                {
                    //Создадим новый клас Персонажа ProgressionCharacterClass и заполним его внутрение поля
                    progressionCharacterClass = new ProgressionCharacterClass();
                    progressionCharacterClass.characterClass = character;
                    progressionCharacterClass.statList = new List<ProgressionStat>();

                    _characterClassList.Add(progressionCharacterClass); //Добавим созданный класс персонажа в список
                }

                //Если список не пустой то -  Сравним с последней статистикой в списке этого персонажа, и если это другой тип статистики то создадим её
                if (progressionCharacterClass.statList.Count == 0 || stat != progressionCharacterClass.statList[progressionCharacterClass.statList.Count - 1].stat)
                {
                    //Создадим новый класс статистики персонажа ProgressionStat и заполним его внутрение поля
                    progressionStat = new ProgressionStat();
                    progressionStat.stat = stat;
                    progressionStat.levelList = new List<float>();

                    progressionCharacterClass.statList.Add(progressionStat); //Добавим созданную статистику этого персонажа в список
                }

                int starColumnNamber = 2; // Будем переберать с 3ей колнки там храняться данные уровня  1 и 2 это характер и тип статистики
                for (int columnNumber = starColumnNamber; columnNumber < gridDateArray.GetLength(1); columnNumber++) // Перберем ячейки в данной строке
                {
                    int date = SheetProcessor.ParseInt(gridDateArray[rowNamber, columnNumber]);

                    progressionStat.levelList.Add(date); // Добавим полученную статистику в список статистик нашего персонажа
                }
            }           
        }

        public float GetStat(Stat stat, CharacterClass characterClass, int level)
        {
            BuildLookup();

            float[] levels = lookupTable[characterClass][stat]; //Вместо return lookupTable[characterClass][stat][level] сделаем промеж шаг, Создадим массив данных для полученной статистики и проверим

            if (levels.Length < level) // Если длина массива меньше переданного уровня значит этого уровня несуществует для данного персонажа
            {
                return 0;
            }

            return levels[level - 1]; // Вернем данные из нашего массива, соответствующие запрашиваемому уровню (level), [level - 1] - наш уровень начинается с 1 а не с 0 поэтому отнимем 1 что бы получить правильный элемент массива 
        }

        public int GetLevels(Stat stat, CharacterClass characterClass) // Получить количество доступных уровней
        {
            BuildLookup();

            float[] levels = lookupTable[characterClass][stat];
            return levels.Length;
        }

        private void BuildLookup() // Поиск по сборке
        {
            if (lookupTable != null) return; // Выходим если статистика уже загружена 

            lookupTable = new Dictionary<CharacterClass, Dictionary<Stat, float[]>>();
            foreach (ProgressionCharacterClass progressionClass in _characterClassList) // переберем всех персонажей
            {                
                var statLookupTable = new Dictionary<Stat, float[]>();

                foreach (ProgressionStat progressionStat in progressionClass.statList) // Переберем массив статистики (в нашем случае это массив enum Stat) нашего персонажа
                {                   
                    statLookupTable[progressionStat.stat] = progressionStat.levelList.ToArray(); // Сохраним в словарь (КЛЮЧ - название enum Stat, ЗНАЧЕНИЕ - массив данных для каждого уровня)
                }

                lookupTable[progressionClass.characterClass] = statLookupTable; // Сохраним в словарь (КЛЮЧ - название нашего персонажа, ЗНАЧЕНИЕ - словарь статистики
            }           
        }

        [System.Serializable]
        class ProgressionCharacterClass
        {
            public CharacterClass characterClass;
            public List<ProgressionStat> statList = null;            
        }

        [System.Serializable]
        class ProgressionStat
        {
            public Stat stat;
            public List<float> levelList;
        }
    }  
}

my table

1 Like

Privacy & Terms