Real-time дебаг Android и iOS приложений на Unity

в 14:16, , рубрики: android, debug, game development, iOS, unity3d, метки: , , ,

Когда разработка проекта идёт на компьютере, проблемы просмотра лога не встаёт. Но когда начинаешь разрабатывать на планшете, то всё становится гораздо печальнее. На iPad надо зайти через iTunes, найти свой лог, и только тогда можно посмотреть его содержимое. На Android всё ещё сложнее и неудобнее. Зачастую хочется видеть сообщения в real-time. Разработчики движка добавили много разных полезных вещей в профайлер, но к сожалению забыли о простой консоли, чтобы переключившись на её вкладку разработчик мог смотреть сообщения от своей игры и отправлять какие-то консольные команды прямо с компьютера на планшет.
Это упущение мы сейчас и будем исправлять.

Если кратко, мы создадим таблицу в Google Docs, и скрипт, который будет при вызове добавлять туда значение. И создадим скрипт в движке, который будет слушать команды типа Debug.Log() и отправлять их нашему скрипту. Дел на 10 минут, но удобство повышается значительно. Если у вас в проекте реализована консоль, то можно сделать чтобы скрипт отправлял отклик (скажем содержимое написанного вами в какой-то ячейке таблицы), можно это передавать в качестве команды в консоль.
Работает всё довольно быстро, у меня интернет 50 mbit, сообщения появляются практически мгновенно. Таблица Google обновляется сама, рефрешить не надо.

Сначала простейший вариант, для понимания сути.

1. Настройка серверной части.

Для серверной части нужен аккаунт Google, потому что создавать таблицу мы будем в Google Drive.

Создайте Spreadsheet.

В меню создайте скрипт (Tools -> Script Editor -> Script as Web App).

Удалите созданный темплейт и скопируйте следующий код:

function doGet(e)
{
  var sheet = SpreadsheetApp.openById("0Ap2rwVyty8rZdElOTFhlTG9BSEZGY29BZDlpQWdMR2c");
  var n = sheet.getLastRow() + 1;
  sheet.getRange("A"+n).setValue( new Date() );
  sheet.getRange("B"+n).setValue(e.parameter.p);
  return ContentService.createTextOutput("OK");
}

!!! В коде id 0Ap2rwVyty8rZdElOTFhlTG9BSEZGY29BZDlpQWdMR2c надо заменить на id вашей страницы, его можно взять из URL созданного вами Spreadsheet.

При вызове этого скрипта он будет брать созданную вами таблицу, и добавлять в последнюю строку время и значение, которое пришло по запросу GET с ключом p.

Сохраните скрипт под именем, к примеру, RemoteLogger.

Теперь надо создать версию (File -> Manage versions -> Save new version).

Теперь мы можем опебликовать его (Publish -> Deploy as Web App) с такими настройками:
!!! Execute the app as: me
!!! Who has access to the app: Anyone, even anonymous

После нажатия Deploy Google даёт нам URL такого вида:
script.google.com/macros/s/AKfycbyubkC0TQbyrRYvVD9nYezvxuDkya0Asbvff67EUK0G0oqFndws/exec
при его запуске в созданной вами таблице появится строка со временем запуска и значением undefined, если в конце дописать ?p=test
script.google.com/macros/s/AKfycbyubkC0TQbyrRYvVD9nYezvxuDkya0Asbvff67EUK0G0oqFndws/exec?p=test
то в таблице появится строка со словом «test»

Можно считать, что серверная часть работает нормально.

2. Клиентский Unity-скрипт.

Создайте скрипт, к примеру RemoteLogger.cs и поместите туда следующий код:

using UnityEngine;
using System.Collections;
using System.IO;

public class RemoteLogger : MonoBehaviour {

	string url = "https://script.google.com/macros/s/AKfycbyubkC0TQbyrRYvVD9nYezvxuDkya0Asbvff67EUK0G0oqFndws/exec"; // Your URL copy here
	
	// Use this for initialization
	void Start () {
		Debug.Log("Hello world!");
	}
	
	// Update is called once per frame
	void Update () {
	}
	
	//get method here http://forum.antichat.ru/showthread.php?t=290347
	string UrlEncode(string instring)
	{
	    StringReader strRdr = new StringReader(instring);
	    StringWriter strWtr = new StringWriter();
	    int charValue = strRdr.Read();
	    while (charValue != -1)
	    {
	        if (((charValue >= 48) && (charValue <= 57)) // 0-9
	        || ((charValue >= 65)  && (charValue <= 90)) // A-Z
	        || ((charValue >= 97)  && (charValue <= 122))) // a-z
	        {
	            strWtr.Write((char) charValue);
	        }
	        else if (charValue == 32) // Space
	        {
	            strWtr.Write("+");
	        }
	        else
	        {
	            strWtr.Write("%{0:x2}", charValue);
	        }
	        charValue = strRdr.Read();
	    }
	    return strWtr.ToString();
	}
	
 	void SendLog(string mes) {
		string t_url = url + "?p=" + UrlEncode(mes);
		WWW www = new WWW(t_url);
		StartCoroutine(WaitForRequest(www));
	}

	IEnumerator WaitForRequest(WWW www)
	{
		yield return www;
		// check for errors
		if (www.error == null) {
			//OK
		} else {
			//Error
		}
	}
	
	void OnEnable() {
        Application.RegisterLogCallback(HandleLog);
    }
    
	void OnDisable() {
        Application.RegisterLogCallback(null);
    }
    
	void HandleLog(string logString, string stackTrace, LogType type) {
		SendLog(logString);
    }	
}

!!! Не забудьте поменять значение url на адрес вашего скрипта.

Этот скрипт слушает лог, берёт сообщение, кодирует для передачи по методу GET и отсылает его нашему скрипту, чтобы он добавил его в таблицу.

После добавления к GameObject и запуска, на открытой в вашем браузере таблице должно появиться «Hello world!» (таблица обновляется автоматически). Если этого не произошло, то вероятно вы забыли поменять id таблицы, url или при публикации скрипта настроили права неправильно.

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

Сначала исправляем скрипт на сервере:

function doGet(e)
{
  var sheet = SpreadsheetApp.openById("0Ap2rwVyty8rZdElOTFhlTG9BSEZGY29BZDlpQWdMR2c");
  var n = sheet.getLastRow() + 1;
  sheet.getRange("A"+n).setValue( new Date() );

  for (var j = 0; j < 10; j++) {
    if (e.parameter["p" + j] != null) {
      sheet.getRange("B"+n).setValue(e.parameter["p" + j]);
      n++;
    }
  }
  return ContentService.createTextOutput("OK");
}

То есть перебираются параметры от p0 до p9, если есть значение — то оно добавляется.

Теперь код класса RemoteLogger.cs

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.IO;

public class RemoteLogger : MonoBehaviour {

	string url = "https://script.google.com/macros/s/AKfycbx6q4xPM-5S7FJn1QFHHd9eXhF4yXvaguawc-yEoxU6ETFrZmU/exec";
	bool sema = false;
	Queue<string> log = new Queue<string>();
	
	// Use this for initialization
	void Awake () {
		DontDestroyOnLoad(gameObject);
	}
	
	// Use this for initialization
	void Start () {
		Debug.Log("Hello world!");
	}
	
	// Update is called once per frame
	void Update () {
		if (!sema && log.Count > 0) {
			SendLog();
		}
	}
	
	//get method here http://forum.antichat.ru/showthread.php?t=290347
	string UrlEncode(string instring)
	{
	    StringReader strRdr = new StringReader(instring);
	    StringWriter strWtr = new StringWriter();
	    int charValue = strRdr.Read();
	    while (charValue != -1)
	    {
	        if (((charValue >= 48) && (charValue <= 57)) // 0-9
	        || ((charValue >= 65)  && (charValue <= 90)) // A-Z
	        || ((charValue >= 97)  && (charValue <= 122))) // a-z
	        {
	            strWtr.Write((char) charValue);
	        }
	        else if (charValue == 32) // Space
	        {
	            strWtr.Write("+");
	        }
	        else
	        {
	            strWtr.Write("%{0:x2}", charValue);
	        }
	        charValue = strRdr.Read();
	    }
	    return strWtr.ToString();
	}
	
 	void SendLog() {
		if (sema) return;
		sema = true;
		int count = log.Count > 10 ? 10 : log.Count;
		string t_url = url + "?p0=" + UrlEncode(log.Dequeue());
		for (int i = 1; i < count; i++) {
			t_url += "&p" + i + "=" + UrlEncode(log.Dequeue());
		}
		WWW www = new WWW(t_url);
		StartCoroutine(WaitForRequest(www));
	}

	IEnumerator WaitForRequest(WWW www)
	{
		yield return www;
		// check for errors
		if (www.error == null) {
			//OK
		} else {
			//Error
		}
		sema = false;
	}
	
	void OnEnable() {
        Application.RegisterLogCallback(HandleLog);
    }
    
	void OnDisable() {
        Application.RegisterLogCallback(null);
    }
    
	void HandleLog(string logString, string stackTrace, LogType type) {
		log.Enqueue(logString);
    }	
}

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

TODO:

В зависимости от ваших нужд, можно преедевать в таблицу что это за сообщение error/warning/message и подсвечивать его соответсвенно, как это сделано в Unity.
Можно передавать стек, если вам важно видеть, откуда было оно отправлено.
Теоретически можно использовать построение графиков.
Можно сделать обратную связь, и передвать какие-то команды в игру, там уже слушая отклик скрипта и передавая его в консоль. Добавляется легко.
Можно консоль не делать, а добавить пункты меню, к примеру «Make screenshot», «Enable cheats», и отправлять их.

В общем, есть возможности для доработки и расширения. Делитесь вашими идеями и их реализациями, возможно они кому-нибудь пригодятся. :)

PS: Большое спасибо автору этой статьи из песочницы.

Автор: Igor_Sib

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js