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

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

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


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



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



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



А сейчас перейдем к техническому аспекту интеграции Google таблиц с Unity. Игровой движок Unity предоставляет множество способов взаимодействия с интернет ресурсами, в том числе и с таблицами Google, одним из них является интеграция на базе standalone-приложения. Но основным вызовом в этом случае может быть использование движком .NET 2.0.

С 5 мая 2015 года Google прекратили поддержку устаревшего протокола авторизации OAuth 1.0, и все те приложения, которые не перешли на новый OAuth 2.0 перестали работать. Так вышло что разработчики бесплатных плагинов, которым я пользовался, Unity-GData и его обновленной версии Unity-Quicksheet об этом не позаботились, и мне ничего не оставалось как искать решение самому.

Поначалу мне казалось, что проблем с авторизацией на Unity и С# возникнуть не должно, потому что документация на эту тему исчерпывающая. Но оказалось не все так просто.

Во-первых, были заминки с самим процессом авторизации, так как некоторые параметры для запроса OAuth 2.0 в документации к Google Spreadsheets не были указаны, например “Access Type” и “Token Type”.

Во-вторых, в некоторых обновленных библеотеках .dll Google Data API, которые я импортировал из Google Data API SDK для mono, в Unity возникали ошибки при компиляции, тоже самое было и с рядом старых .dll из плагина Unity-GData. Пришлось комбинировать.

В-третьих нужно было еще добавить подходящую библиотеку Newtonsoft для работы с JSON запросами.

Перейдем к делу. Процесс интеграции Google таблиц с Unity можно разделить на несколько этапов.
  • Предварительная настройка аккаунта Google
  • Настройка доступа к Google Drive API
  • Получение и сохранение данных из Google таблиц

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

Предварительная настройка аккаунта Google

  • Входим в свой аккаунт.
  • Переходим по ссылке и создаем новый проект “Create new project”.
  • На боковой панели консоли разработчика в разделе “APIs & Auth” выбираем вкладку “APIs”.
  • Переходим в настройки “Drive API” в группе “Google Apps Api” и подключаем данный API к нашему приложению кнопкой “Enable API”.
  • В том же разделе “APIs & Auth” выбираем вкладку “Consent screen”. Вводим имя продукта “Product name” и по желанию указываем адрес домашней страницы, логотип и другие параметры.
  • Снова разделе “APIs & Auth” выбираем вкладку “Credentials”. Создаем новый Client ID для работы с Auth 2.0 — кнопка “Create client ID”. В диалоговом окне отмечаем “Application type” как “Installed application”, а “Installed application type”, как “Other”. Подтверждаем нажатием кнопки “Create Client ID”.
  • Мы получили значения Client ID, Client Secret, которые потребуются в дальнейшем.



Настройка доступа к Google Drive API

  • После того как вы импортировали unitypackage, скрипт “SpreadsheetEntity.cs”, должен автоматически начать свою работу при запуске Unity. Открываем скрипт для редактирования, и вводим информацию для авторизации.
  • Присваиваем переменным _ClientId и _ClientId значения Client ID и Client Secret полученные из консоли разработчика.
  • Запускаем Unity проект. Он автоматически переходит по ссылке, где после авторизации, можно получить Access Code для того, чтобы приложение могло успешно авторизоваться с Google.
  • Копируем Access Code и присваиваем его значение переменной _AccessCode в скрипте “SpreadsheetEntity.cs”.
  • После этого снова запускаем проект и получаем из лога консоли Unity значения OAuth 2.0 Access Token и Refresh Token, которые нужны нам для постоянного доступа к таблицам Google. Присваиваем эти значения переменным _AccessToken и _RefreshToken соответственно.

Приведу пример скрипта.
Тут находиться скрипт
using UnityEngine;
using UnityEditor;
using System.Collections;
using System.Collections.Generic;
using Google.GData.Client;
using Google.GData.Spreadsheets;

[InitializeOnLoad]
public class SpreadsheetEntity : MonoBehaviour 
{
	// enter Client ID and Client Secret values
	const string _ClientId = "";
	const string _ClientSecret = "";
	// enter Access Code after getting it from auth url
	const string _AccessCode = "";
	// enter Auth 2.0 Refresh Token and AccessToken after succesfully authorizing with Access Code
	const string _RefreshToken = "";
	const string _AccessToken = "";

	const string _SpreadsheetName = "";


	static SpreadsheetsService service;

	
	public static GOAuth2RequestFactory RefreshAuthenticate() 
	{
		OAuth2Parameters parameters = new OAuth2Parameters()
		{
			RefreshToken = _RefreshToken,
			AccessToken = _AccessToken,
			ClientId = _ClientId,
			ClientSecret = _ClientSecret,
			Scope = "https://www.googleapis.com/auth/drive https://spreadsheets.google.com/feeds",
			AccessType = "offline",
			TokenType = "refresh"
		};
		string authUrl = OAuthUtil.CreateOAuth2AuthorizationUrl(parameters);
		return new GOAuth2RequestFactory("spreadsheet", "MySpreadsheetIntegration-v1", parameters);
	}

	static void Auth()
	{
		GOAuth2RequestFactory requestFactory = RefreshAuthenticate();
		
		service = new SpreadsheetsService("MySpreadsheetIntegration-v1");  
		service.RequestFactory = requestFactory;
	}
	

	// Use this for initialization
	static SpreadsheetEntity(){
		if (_RefreshToken == "" && _AccessToken == "")
		{
			Init();
			return;
		}
		
		Auth();
		
		Google.GData.Spreadsheets.SpreadsheetQuery query = new Google.GData.Spreadsheets.SpreadsheetQuery();
		
		// Make a request to the API and get all spreadsheets.
		SpreadsheetFeed feed = service.Query(query);
		
		if (feed.Entries.Count == 0)
		{
			Debug.Log("There are no spreadsheets in your docs.");
			return;
		}
		
		AccessSpreadsheet(feed);
	}

	// access spreadsheet data
	static void AccessSpreadsheet(SpreadsheetFeed feed)
	{

		string name = _SpreadsheetName;
		SpreadsheetEntry spreadsheet = null;

		foreach (AtomEntry sf in feed.Entries)
		{
			if (sf.Title.Text == name)
			{
				spreadsheet = (SpreadsheetEntry)sf;
			}
		}

		if (spreadsheet == null)
		{
			Debug.Log("There is no such spreadsheet with such title in your docs.");
			return;
		}

		
		// Get the first worksheet of the first spreadsheet.
		WorksheetFeed wsFeed = spreadsheet.Worksheets;
		WorksheetEntry worksheet = (WorksheetEntry)wsFeed.Entries[0];
		
		// Define the URL to request the list feed of the worksheet.
		AtomLink listFeedLink = worksheet.Links.FindService(GDataSpreadsheetsNameTable.ListRel, null);
		
		// Fetch the list feed of the worksheet.
		ListQuery listQuery = new ListQuery(listFeedLink.HRef.ToString());
		ListFeed listFeed = service.Query(listQuery);


		foreach (ListEntry row in listFeed.Entries)
		{
			//access spreadsheet data here
		}


	}
	
	static void Init()
	{
		
		////////////////////////////////////////////////////////////////////////////
		// STEP 1: Configure how to perform OAuth 2.0
		////////////////////////////////////////////////////////////////////////////

		if (_ClientId == "" && _ClientSecret == "")
		{
			Debug.Log("Please paste Client ID and Client Secret");
			return;
		}

		string CLIENT_ID = _ClientId;

		string CLIENT_SECRET = _ClientSecret;

		string SCOPE = "https://www.googleapis.com/auth/drive https://spreadsheets.google.com/feeds https://docs.google.com/feeds";

		string REDIRECT_URI = "urn:ietf:wg:oauth:2.0:oob";
		
		string TOKEN_TYPE = "refresh";
		
		////////////////////////////////////////////////////////////////////////////
		// STEP 2: Set up the OAuth 2.0 object
		////////////////////////////////////////////////////////////////////////////
		
		// OAuth2Parameters holds all the parameters related to OAuth 2.0.
		OAuth2Parameters parameters = new OAuth2Parameters();

		parameters.ClientId = CLIENT_ID;

		parameters.ClientSecret = CLIENT_SECRET;

		parameters.RedirectUri = REDIRECT_URI;
		
		////////////////////////////////////////////////////////////////////////////
		// STEP 3: Get the Authorization URL
		////////////////////////////////////////////////////////////////////////////

		parameters.Scope = SCOPE;
		
		parameters.AccessType = "offline"; // IMPORTANT and was missing in the original
		
		parameters.TokenType = TOKEN_TYPE; // IMPORTANT and was missing in the original

		// Authorization url.

		string authorizationUrl = OAuthUtil.CreateOAuth2AuthorizationUrl(parameters);
		Debug.Log(authorizationUrl);
		Debug.Log("Please visit the URL above to authorize your OAuth "
		          + "request token.  Once that is complete, type in your access code to "
		          + "continue...");

		parameters.AccessCode = _AccessCode;

		if (parameters.AccessCode == "")
		{
			Application.OpenURL(authorizationUrl);
			return;
		}
		////////////////////////////////////////////////////////////////////////////
		// STEP 4: Get the Access Token
		////////////////////////////////////////////////////////////////////////////

		OAuthUtil.GetAccessToken(parameters);
		string accessToken = parameters.AccessToken;
		string refreshToken = parameters.RefreshToken;
		Debug.Log("OAuth Access Token: " + accessToken + "\n");
		Debug.Log("OAuth Refresh Token: " + refreshToken + "\n");
	
	}
	
}


Отмечу некоторую особенность: этот скрипт выполняется при запуске редактора Unity, это очень удобно, потому что все данные уже будут на месте перед выполнением основного кода приложения, подробнее здесь.

После того как вы выполнили все необходимые шаги, проект готов для работы с Google таблицами.

Получение и хранение данных с Google таблиц

У Google процесс работы с таблицами хорошо описан в выше упомянутой документации для разработчиков. Но для ясности приведу небольшой пример. Для получения данных, я использую запросы на основе списков (list-based feeds), а для хранения — файлы XML. Более подробную информацию по работе с XML в Unity вы можете найти здесь.

Пример кода
// modified AccessSpreadsheet Method
void AccessSpreadsheet(SpreadsheetFeed feed)
        {
 
                string name = _SpreadsheetName;
                SpreadsheetEntry spreadsheet = null;
 
                foreach (AtomEntry sf in feed.Entries)
                {
                        if (sf.Title.Text == name)
                        {
                                spreadsheet = (SpreadsheetEntry)sf;
                        }
                }
 
                if (spreadsheet == null)
                {
                        Debug.Log("There is no such spreadsheet with such title in your docs.");
                        return;
                }
 
               
                // Get the first worksheet of the first spreadsheet.
                WorksheetFeed wsFeed = spreadsheet.Worksheets;
                WorksheetEntry worksheet = (WorksheetEntry)wsFeed.Entries[0];
               
                // Define the URL to request the list feed of the worksheet.
                AtomLink listFeedLink = worksheet.Links.FindService(GDataSpreadsheetsNameTable.ListRel, null);
               
                // Fetch the list feed of the worksheet.
                ListQuery listQuery = new ListQuery(listFeedLink.HRef.ToString());
                ListFeed listFeed = service.Query(listQuery);
               
                //create list to add dynamic data
                List<TestEntity> testEntities = new List<TestEntity>();
 
                foreach (ListEntry row in listFeed.Entries)
                {
                        TestEntity entity = new TestEntity();
                        entity.name = row.Elements[0].Value;
                        entity.number = int.Parse(row.Elements[1].Value); //use Parse method to get int value
                        Debug.Log("Element: " + entity.name + ", " + entity.number.ToString());
                        testEntities.Add(entity);
                }
 
                TestContainer container = new TestContainer(testEntities.ToArray());
 
                container.Save("test.xml");
 
        }
 
// classes for xml serialization
public class TestEntity {
 
        public string name;
        public int number;
       
        public TestEntity(){
                name = "default";
                number = 0;
        }
}
 
[XmlRoot("TestCollection")]
public class TestContainer {
        [XmlArray("TestEntities")]
        [XmlArrayItem("testEntity")]
        public TestEntity[] testEntities;// = new skinEntity[];
       
        public TestContainer(){
        }
       
        public TestContainer(TestEntity[] arch){
                testEntities = arch;
        }
       
        public void Save(string path)
        {
                var serializer = new XmlSerializer(typeof(TestContainer));
                using(var stream = new FileStream(path, FileMode.Create))
                {
                        serializer.Serialize(stream, this);
                }
        }
       
        public static TestContainer Load(string path)
        {
                var serializer = new XmlSerializer(typeof(TestContainer));
                using(var stream = new FileStream(path, FileMode.Open))
                {
                        return serializer.Deserialize(stream) as TestContainer;
                }
        }
}


Стоит еще добавить, что скрипт работает только в редакторе, использовать функционал Google таблиц на устройстве нельзя, по крайней с теми версиями плагинов, которые я нашел, это связано с проблемами совместимости части библиотек с некоторыми платформами. Предложенные мной библиотеки в unitypackage не будут компилироваться ни для одной из платформы, кроме редактора. Если Вам все-таки потребуется использовать таблицы на устройстве, то в 5 версии Unity можно выбрать платформу, которую должен поддерживает плагин, с помощью Plugin Inspector, а в более ранних версиях — поместить плагин в нужную папку, подробнее здесь.

Итак, электронные таблицы являются незаменимым инструментов в процессе разработки игр. Они помогают оптимизировать рутинную работу и наладить взаимодействие всей команды, визуализировать огромные массивы данных и использовать математику для их расчета. Интеграция Google таблиц с Unity — процесс несложный, но он требует немного знаний и усилий

Вот ссылка на GitHub проекта.

Спасибо за внимание, надеюсь статья была Вам полезна.

Комментарии (5)


  1. soulburner
    18.07.2015 03:08
    +1

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


  1. slonopotamus
    18.07.2015 15:26
    +4

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


    1. imbeat
      20.07.2015 13:27

      Могу предположить, что в системе контроля версий можно хранить скрипт генерирующий таблицу. Но, по-моему, это из разряда «по воробьям из пушки».


    1. deepnavy Автор
      20.07.2015 13:49

      Очень хороший вопрос. Почитав здесь немного про ревизии версий при работе с google документами, стало понятно что привязать определенную версию документа к нужной версии программы можно — сохранить метаданные файла, а когда программа будет запрашивать все ревизии файлов найти нужную по метаданным и с ней уже работать. Но заставить работать именно Drive API для .NET на Unity мне не хватило пока нервов.


    1. beststream
      14.08.2015 00:48

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