Теперь, когда есть настроенный динамический скайбокс, можно изменять его значение в соответствии со сводкой настоящей погоды. Для выполнения данной задачи будет использоваться архитектура диспетчеров. Мы создадим класс WeatherManager, инициализируемый центральным диспетчером диспетчеров. Этот класс будет отвечать за получение и сохранение метеорологических данных, взаимодействую с Интернетом.
Для взаимодействия с Интернетом мы создадим вспомогательный класс NetworkService, в котором осуществим подключение к Интернету и HTTP-запросы. После этого с помощью класса WeatherManager можно будет отправлять запросы и возвращать ответы через NetworkService. Принцип на рис. 1.
Для работы этого механизма WeatherManager должен иметь доступ к NetworkService. Поэтому мы создадим объект в сценарии Managers и в момент инициализации различных диспетчеров будем вставлять в них объект NetworkService. В результате ссылку на этот объект получит не только диспетчер WeatherManager, но и все диспетчеры, которые вы создадите после этого.
Воспроизведение арихитектуры начнем со сценариев ManagerStatus и IManager. IManager – это интерфейс, который должны реализовать все диспетчеры, в то время как ManagerStatus – это перечисление, которым пользуется IManager.
1) Создайте пустой скрипт NetworkService.
2) Создайте скрипт IManager с классом NetworkService:
public interface IManager { ManagerStatus status { get; } void Startup(NetworkService service); } |
3) Создайте скрипт WeatherManager:
using UnityEngine; using System.Collections; using System.Collections.Generic; public class WeatherManager: MonoBehaviour, IManager { public ManagerStatus status { get; private set; } // Сюда добавляется значение облачности private NetworkService _network; public void Startup(NetworkService service) { Debug.Log("Weather manager starting..."); _network = service; status = ManagerStatus.Started; } public float cloudValue { get; private set; } } |
Это начальный вариант сценария WeatherManager, необходимый IManager для реализации класса: здесь объявляется свойство интерфейса status и выполняется функция Startup(). Этот пустой каркас мы заполним позже.
|
4) Создайте скрипт Manager, инициализирующий сценарий WeatherManager:
using UnityEngine; using System.Collections; using System.Collections.Generic; [RequireComponent(typeof(WeatherManager))] public class Managers: MonoBehaviour { public static WeatherManager Weather { get; private set; } private List<IManager> _startSequence; void Awake() { Weather = GetComponent<WeatherManager>(); _startSequence = new List<IManager>(); _startSequence.Add(Weather); StartCoroutine(StartupManagers()); } private IEnumerator StartupManagers() { NetworkService network = new NetworkService(); foreach (IManager manager in _startSequence) { manager.Startup(network); } yield return null; int numModules = _startSequence.Count; int numReady = 0; while (numReady < numModules) { int lastReady = numReady; numReady = 0; foreach (IManager manager in _startSequence) { if (manager.status == ManagerStatus.Started) { numReady++; } } if (numReady > lastReady) Debug.Log("Progress: " + numReady + "/" + numModules); yield return null; } Debug.Log("All managers started up"); } } |
5) Создайте в сцене пустой объект, который будет играть роль диспетчера и присоедините к нему сценарии Managaers и WeatherManager. При корректной настройке он будет выводить на консоль сообщения о загрузке, чем его функция пока и ограничится.
6) Напишем скрипт NetworkService. В нем должен содержаться код реализации HTTP-запросов. Основным классом, сведения о котором потребуются, является WWW. В Unity этот класс обеспечивает взаимодействие с Интернетом. Создание экземпляра объекта WWW с использованием URL-адреса приводит к отправке запроса по этому адресу. Сопрограммы позволяют классу WWW ждать завершения запроса. Сопрограммами называются специальные функции, которые запускаются в фоновом режиме основной программы, в цикле выполняют код и возвращают результат в программу. Вместе с методом StartCorotine() используется ключевое слово yield, которое заставляло сопрограмму на время остановиться, вернуть управление программе, а в следующем кадре снова начать работу. В случае с классом WWW выполнение функции будет прервано до завершения сетевого запроса: сначала вы посылаете запрос, потом продолжаете выполнение остальной части программы, а через некоторое время получаете ответ.
|
using UnityEngine; using System.Collections; using System; public class NetworkService { private const string xmlApi = "https://api.openweathermap.org/data/2.5/weather?q=Minsk,by&mode=xml&appid=3b4d068225a7de36ec756e9a95e30b5b"; private bool IsResponseValid(WWW www) { if (www.error!= null) { Debug.Log("bad connection"); return false; } else if (string.IsNullOrEmpty(www.text)) { Debug.Log("bad data"); return false; } else { // все хорошо Debug.Log("Запрос удался!"); return true; } } private IEnumerator CallAPI(string url, Action<string> callback) { WWW www = new WWW(url); yield return www; if (!IsResponseValid(www)) yield break; callback(www.text); } public IEnumerator GetWeatherXML(Action<string> callback) { return CallAPI(xmlApi, callback); } } |
https://api.openweathermap.org/data/2.5/weather?q=Minsk,by&mode=xml&appid=3b4d068225a7de36ec756e9a95e30b5b – HTTP-запрос. Как можно заметить, данные получаем с сайта openweather.org. В качестве параметров посылаем: город (q=Minsk,by), в каком формате требуется ответ (mode=xml) и appid – бесплатный ключ, который можно получить при регистрации на этом сайте, без него запросы выполняться не будут.
Помещенный в сопрограмму метод GetWeatherXML() заставляет сценарий NetworkService создать HTTP-запрос. Обратите внимание, что в качестве типа возвращаемого значения указан тип IEnumerator, который объявляется во всех методах, используемых в сопрограмме.
|
Отсутствие ключевого слова yield в методе GetWeatherXML() на первый взгляд мо жет показаться странным. Ведь именно это ключевое слово останавливает выполнение сопрограммы, а значит, его наличие предполагается по умолчанию. Но дело в том, что у нас есть набор вставленных друг в друга методов. Если первый метод сопрограммы вызывает какой-то другой метод, который останавливается, выполнив только часть своего кода, остановка и возобновление работы сопрограммы будут осуществляться внутри второго метода. То есть в нашем случае ключевое слово yield в методе CallAPI() прерывает сопрограмму, начавшуюся в методе GetWeatherXML(). Этот принцип работы иллюстрирует рис. 2
В начале сопрограммы вызывается метод с параметром callback, который принадлежит типу Action. Тип Action является делегатом. Делегаты представляют собой ссылки на какой-то другой метод/функцию. Делегат позволяет сохранить функцию (указатель на нее) в переменной и передать эту переменную в качестве параметра другой функции. Т.е. делегаты позволяют передавать функции точно так же, как числа или строки, и вызывать их позже. Без делегатов мы могли бы вызывать функции только напрямую. Делегаты дают возможность сообщить коду о других методах, которые можно вызвать позже. Такое поведение требуется во многих случаях, особенно при реализации функций обратного вызова.
Обратным вызовом (callback) называется вызов функции, используемой для обмена данными с вызывающим объектом. Объект А может сообщить объекту Б об одном из своих методов. Позднее объект Б может вызвать этот метод для обмена данными с объектом А. В данном случае обратный вызов позволил нам, дождавшись завершения HTTP-запроса, переслать обратно полученный ответ. Код метода CallAPI() сначала отправляет HTTP-запрос, затем останавливается, ожидая завершения этого запроса, и наконец с помощью метода callback() возвращает полученный овтет.
Обратите внимание на синтаксис <>, используемый с ключевым словом Action; указанный в угловых скобках тип требуется параметрам, чтобы подойти к этому делегату. Другими словами, фнукция, на которую указывает делегат Action, должна иметь параметры указанного типа. В данном случае параметром является единственная строка, поэтмоу у метода обратного вызова должна быть прмерно такая сигтура: MethodName(string value).
Метод IsResponseValid() проверяет наличие ошибок в HTTP-ответе. Возможны ошибки двух типов: сбой запроса из-за проблем с интернет-подключением и некорректность возвращаемых данных. Константа const объявляется с URL-адресом, по которому код будет отправлять запросы.
7) Дополним скрипт WeatherManager, используя NetworkService:
… public void Startup(NetworkService service) { Debug.Log("Weather manager starting..."); _network = service; //начинаем загрузку данных из Интернета StartCoroutine(_network.GetWeatherXML(OnXMLDataLoaded)); //меняем состояние со Started на Initializing status = ManagerStatus.Initializing; } //метод обратного вызова сразу после загрузки данных public void OnXMLDataLoaded(string data) { Debug.Log("Выводим ответ:"+data); status = ManagerStatus.Started; } |
Были внесены изменения: запуск сопрограммы для скачивания данных из Интернета, задание другого состояния загрузки и определение метода обратного вызова для получения ответа. Все сложные аспекты работы с сопрограммами реализованы в скрипте NetworkService, поэтому сейчас вам остается только вызвать метод StartCoroutine(). Потом вы меняете состояние загрузки, так как на самом деле инициализация диспетчера не завершена, сначала он должен получить данные из Интернета.
Методы передачи данных по сети всегда нужно начинать с функции StartCoroutine(); обычный вызов в их случае неприменим. Вызов метода StartCoroutine() должен сопровождаться активацией. То есть нужно добавить скобки, а не просто указать имя функции. В нашем случае в качестве одного из параметров методу сопрограммы требуется функция обратного вызова, которую нам следует задать. Для обратного вызова мы воспользуемся функцией OnXMLDataLoaded(); обратите внимание, что ее параметр относится к типу string, что совпадает с объявлением Action в сценарии NetworkService. Пока что от функции обратного вызова нам много не требуется — она просто проверяет корректность полученных данных и выводит их на консоль. После этого последняя строчка функции меняет состояние загрузки диспетчера, уведомляя о том, что теперь он полностью загружен.
8) Запустите проект. При наличии хорошего интернет-подключения на консоли быстро появятся данные. Это всего лишь длинная строка, но отформатированная особым образом, что дает нам возможность воспользоваться содержащейся в ней информацией.
9) Парсинг текста в формате XML
Существующие в виде длинных строк данные обычно состоят из отдельных битов информации. Эти биты информации извлекаются методом синтаксического разбора, или, как его еще называют, парсинга. Парсингом (parsing) называется процесс анализа фрагментов кода и его разделения на отдельные фрагменты данных. Для парсинга строки она должна быть отформатирована таким образом, чтобы у вас (точнее, у кода-анализатора) была возможность идентификации отдельных фрагментов. Существует пара стандартных форматов передачи данных через Интернет; наиболее распространенным из них является XML. К счастью, Unity (точнее, Mono — встроенная в Unity среда разработки) предлагает функциональность для анализа XML-кода. Запрошенный нами прогноз погоды имеет формат XML, поэтому добавим в сценарий WeatherManager код, анализирующий ответ и извлекающий из него информацию об облачности.
Создайте скрипт Massenger:
using System; using System.Collections.Generic; using System.Linq; public enum MessengerMode { DONT_REQUIRE_LISTENER, REQUIRE_LISTENER, } static internal class MessengerInternal { readonly public static Dictionary<string, Delegate> eventTable = new Dictionary<string, Delegate>(); static public readonly MessengerMode DEFAULT_MODE = MessengerMode.REQUIRE_LISTENER; static public void AddListener(string eventType, Delegate callback) { MessengerInternal.OnListenerAdding(eventType, callback); eventTable[eventType] = Delegate.Combine(eventTable[eventType], callback); } static public void RemoveListener(string eventType, Delegate handler) { MessengerInternal.OnListenerRemoving(eventType, handler); eventTable[eventType] = Delegate.Remove(eventTable[eventType], handler); MessengerInternal.OnListenerRemoved(eventType); } static public T[] GetInvocationList<T>(string eventType) { Delegate d; if (eventTable.TryGetValue(eventType, out d)) { try { return d.GetInvocationList().Cast<T>().ToArray(); } catch { throw MessengerInternal.CreateBroadcastSignatureException(eventType); } } return null; } static public void OnListenerAdding(string eventType, Delegate listenerBeingAdded) { if (!eventTable.ContainsKey(eventType)) { eventTable.Add(eventType, null); } var d = eventTable[eventType]; if (d!= null && d.GetType()!= listenerBeingAdded.GetType()) { throw new ListenerException(string.Format("Attempting to add listener with inconsistent signature for event type {0}. Current listeners have type {1} and listener being added has type {2}", eventType, d.GetType().Name, listenerBeingAdded.GetType().Name)); } } static public void OnListenerRemoving(string eventType, Delegate listenerBeingRemoved) { if (eventTable.ContainsKey(eventType)) { var d = eventTable[eventType]; if (d == null) { throw new ListenerException(string.Format("Attempting to remove listener with for event type {0} but current listener is null.", eventType)); } else if (d.GetType()!= listenerBeingRemoved.GetType()) { throw new ListenerException(string.Format("Attempting to remove listener with inconsistent signature for event type {0}. Current listeners have type {1} and listener being removed has type {2}", eventType, d.GetType().Name, listenerBeingRemoved.GetType().Name)); } } else { throw new ListenerException(string.Format("Attempting to remove listener for type {0} but Messenger doesn't know about this event type.", eventType)); } } static public void OnListenerRemoved(string eventType) { if (eventTable[eventType] == null) { eventTable.Remove(eventType); } } static public void OnBroadcasting(string eventType, MessengerMode mode) { if (mode == MessengerMode.REQUIRE_LISTENER &&!eventTable.ContainsKey(eventType)) { throw new MessengerInternal.BroadcastException(string.Format("Broadcasting message {0} but no listener found.", eventType)); } } static public BroadcastException CreateBroadcastSignatureException(string eventType) { return new BroadcastException(string.Format("Broadcasting message {0} but listeners have a different signature than the broadcaster.", eventType)); } public class BroadcastException: Exception { public BroadcastException(string msg) : base(msg) { } } public class ListenerException: Exception { public ListenerException(string msg) : base(msg) { } } } // No parameters static public class Messenger { static public void AddListener(string eventType, Action handler) { MessengerInternal.AddListener(eventType, handler); } static public void AddListener<TReturn>(string eventType, Func<TReturn> handler) { MessengerInternal.AddListener(eventType, handler); } static public void RemoveListener(string eventType, Action handler) { MessengerInternal.RemoveListener(eventType, handler); } static public void RemoveListener<TReturn>(string eventType, Func<TReturn> handler) { MessengerInternal.RemoveListener(eventType, handler); } static public void Broadcast(string eventType) { Broadcast(eventType, MessengerInternal.DEFAULT_MODE); } static public void Broadcast<TReturn>(string eventType, Action<TReturn> returnCall) { Broadcast(eventType, returnCall, MessengerInternal.DEFAULT_MODE); } static public void Broadcast(string eventType, MessengerMode mode) { MessengerInternal.OnBroadcasting(eventType, mode); var invocationList = MessengerInternal.GetInvocationList<Action>(eventType); foreach (var callback in invocationList) callback.Invoke(); } static public void Broadcast<TReturn>(string eventType, Action<TReturn> returnCall, MessengerMode mode) { MessengerInternal.OnBroadcasting(eventType, mode); var invocationList = MessengerInternal.GetInvocationList<Func<TReturn>>(eventType); foreach (var result in invocationList.Select(del => del.Invoke()).Cast<TReturn>()) { returnCall.Invoke(result); } } } // One parameter static public class Messenger<T> { static public void AddListener(string eventType, Action<T> handler) { MessengerInternal.AddListener(eventType, handler); } static public void AddListener<TReturn>(string eventType, Func<T, TReturn> handler) { MessengerInternal.AddListener(eventType, handler); } static public void RemoveListener(string eventType, Action<T> handler) { MessengerInternal.RemoveListener(eventType, handler); } static public void RemoveListener<TReturn>(string eventType, Func<T, TReturn> handler) { MessengerInternal.RemoveListener(eventType, handler); } static public void Broadcast(string eventType, T arg1) { Broadcast(eventType, arg1, MessengerInternal.DEFAULT_MODE); } static public void Broadcast<TReturn>(string eventType, T arg1, Action<TReturn> returnCall) { Broadcast(eventType, arg1, returnCall, MessengerInternal.DEFAULT_MODE); } static public void Broadcast(string eventType, T arg1, MessengerMode mode) { MessengerInternal.OnBroadcasting(eventType, mode); var invocationList = MessengerInternal.GetInvocationList<Action<T>>(eventType); foreach (var callback in invocationList) callback.Invoke(arg1); } static public void Broadcast<TReturn>(string eventType, T arg1, Action<TReturn> returnCall, MessengerMode mode) { MessengerInternal.OnBroadcasting(eventType, mode); var invocationList = MessengerInternal.GetInvocationList<Func<T, TReturn>>(eventType); foreach (var result in invocationList.Select(del => del.Invoke(arg1)).Cast<TReturn>()) { returnCall.Invoke(result); } } } // Two parameters static public class Messenger<T, U> { static public void AddListener(string eventType, Action<T, U> handler) { MessengerInternal.AddListener(eventType, handler); } static public void AddListener<TReturn>(string eventType, Func<T, U, TReturn> handler) { MessengerInternal.AddListener(eventType, handler); } static public void RemoveListener(string eventType, Action<T, U> handler) { MessengerInternal.RemoveListener(eventType, handler); } static public void RemoveListener<TReturn>(string eventType, Func<T, U, TReturn> handler) { MessengerInternal.RemoveListener(eventType, handler); } static public void Broadcast(string eventType, T arg1, U arg2) { Broadcast(eventType, arg1, arg2, MessengerInternal.DEFAULT_MODE); } static public void Broadcast<TReturn>(string eventType, T arg1, U arg2, Action<TReturn> returnCall) { Broadcast(eventType, arg1, arg2, returnCall, MessengerInternal.DEFAULT_MODE); } static public void Broadcast(string eventType, T arg1, U arg2, MessengerMode mode) { MessengerInternal.OnBroadcasting(eventType, mode); var invocationList = MessengerInternal.GetInvocationList<Action<T, U>>(eventType); foreach (var callback in invocationList) callback.Invoke(arg1, arg2); } static public void Broadcast<TReturn>(string eventType, T arg1, U arg2, Action<TReturn> returnCall, MessengerMode mode) { MessengerInternal.OnBroadcasting(eventType, mode); var invocationList = MessengerInternal.GetInvocationList<Func<T, U, TReturn>>(eventType); foreach (var result in invocationList.Select(del => del.Invoke(arg1, arg2)).Cast<TReturn>()) { returnCall.Invoke(result); } } } // Three parameters static public class Messenger<T, U, V> { static public void AddListener(string eventType, Action<T, U, V> handler) { MessengerInternal.AddListener(eventType, handler); } static public void AddListener<TReturn>(string eventType, Func<T, U, V, TReturn> handler) { MessengerInternal.AddListener(eventType, handler); } static public void RemoveListener(string eventType, Action<T, U, V> handler) { MessengerInternal.RemoveListener(eventType, handler); } static public void RemoveListener<TReturn>(string eventType, Func<T, U, V, TReturn> handler) { MessengerInternal.RemoveListener(eventType, handler); } static public void Broadcast(string eventType, T arg1, U arg2, V arg3) { Broadcast(eventType, arg1, arg2, arg3, MessengerInternal.DEFAULT_MODE); } static public void Broadcast<TReturn>(string eventType, T arg1, U arg2, V arg3, Action<TReturn> returnCall) { Broadcast(eventType, arg1, arg2, arg3, returnCall, MessengerInternal.DEFAULT_MODE); } static public void Broadcast(string eventType, T arg1, U arg2, V arg3, MessengerMode mode) { MessengerInternal.OnBroadcasting(eventType, mode); var invocationList = MessengerInternal.GetInvocationList<Action<T, U, V>>(eventType); foreach (var callback in invocationList) callback.Invoke(arg1, arg2, arg3); } static public void Broadcast<TReturn>(string eventType, T arg1, U arg2, V arg3, Action<TReturn> returnCall, MessengerMode mode) { MessengerInternal.OnBroadcasting(eventType, mode); var invocationList = MessengerInternal.GetInvocationList<Func<T, U, V, TReturn>>(eventType); foreach (var result in invocationList.Select(del => del.Invoke(arg1, arg2, arg3)).Cast<TReturn>()) { returnCall.Invoke(result); } } } |
Он довольно длинный, но нас интересует только фрагмент, содержащий, к примеру, такие данные, как <clouds value="40" name="scattered clouds"/>.
Мы не только добавим код, анализирующий XML, но и воспользуемся системой сообщений. Дело в том, что нам нужно уведомить сцену о скачанных и проанализированных данных.
10) Создать сценарий с именем GameEvent
public static class GameEvent { public const string WEATHER_UPDATED = "WEATHER_UPDATED"; } |
Эта система сообщений дает нам замечательный несвязанный способ информировать остальную часть программы о событиях.
11) Теперь, когда у нас есть система сообщений, скорректируйте сценарий WeatherManager:
… using System.Xml;//обязательно добавьте необходимые инструкции using … public float cloudValue { get; private set; }//облачность редактируется внутренне, в остальных местах это свойство предназначено только для чтения … public void OnXMLDataLoaded(string data) { Debug.Log("Выводим данные:"+data); XmlDocument doc = new XmlDocument(); doc.LoadXml(data);//Разбиваем XML на структуру с возможностью поиска XmlNode root = doc.DocumentElement; XmlNode node = root.SelectSingleNode("clouds");//извлекаем из данных один //узел string value = node.Attributes["value"].Value; cloudValue = XmlConvert.ToInt32(value) / 100f;//преобразуем значение в число float в диапазоне от 0 до 1 Debug.Log("Value: " + cloudValue); Messenger.Broadcast(GameEvent.WEATHER_UPDATED);//рассылка сообщения status = ManagerStatus.Started;} |
Самые важные изменения появились внутри метода OnXMLDataLoaded(). Раньше он всего лишь выводил данные на консоль, позволяя нам убедиться в корректности их передачи. Теперь же мы добавили в метод команды, анализирующие XML-код. Мы начинаем с создания нового пустого XML-документа; он послужит контейнером для разбираемой XML-структуры. Следующая строка разбивает строку данных, превращая ее в структуру из XML-документа. После чего мы начинаем с корня XML-дерева, чтобы в последующем коде можно было выполнять поиск по этому дереву. На данном этапе в XML-структуре уже можно искать узлы, извлекая в итоге отдельные биты информации. Нас интересует только узел <clouds>. Первым делом мы ищем его в XML-документе, затем извлекаем из него атрибут value. Этот атрибут задает облачность в виде целого числа в диапазоне от 0 до 100, нам же для последующей корректировки вида сцены нужно число типа float в диапазоне от 0 до 1. Преобразование в данном случае осуществляется простой математической операцией. Наконец, после извлечения из полных данных информации об облачности мы рассылаем сообщение об обновлении погодных данных. Пока что никто этого сообщения не слышит, но издатель не обязан ничего знать о подписчиках (собственно, в этом и состоит смысл несвязанной системы рассылки сообщений). Позднее мы добавим в сцену подписчиков.
12) Как только из полученного ответа извлечено значение облачности, мы можем использовать его в методе SetOvercast() скрипта WeatherController. Строка данных в формате XML в конечном итоге превратилась в набор слов и чисел. И одно из таких чисел метод SetOvercast() принимает в качестве параметра. В первой части (без использования интернета) мы использовали число, изменявшееся в каждом кадре, теперь же вместо него нужно поставить значение, которое нам вернул сервис с сайта прогнозов погоды.
Наконец, изменим WeatherController:
using UnityEngine; using System.Collections; public class WeatherController: MonoBehaviour { [SerializeField] private Material sky; [SerializeField] private Light sun; private float _fullIntensity; private float _cloudValue = 0f; bool todark = true; bool realweather = false;//добавили флаг отображения погоды с Интернета private void Awake()//добавляем подписчиков на событие { Messenger.AddListener(GameEvent.WEATHER_UPDATED, OnWeatherUpdated); } void OnDestroy()//удалим подписчиков на событие { Messenger.RemoveListener(GameEvent.WEATHER_UPDATED, OnWeatherUpdated); } void Start() { _fullIntensity = sun.intensity; } private void OnWeatherUpdated() { Debug.Log("Реальная погода!: " + _cloudValue); //если нужно отобразить реальную погоду if(realweather) SetOvercast(Managers.Weather.cloudValue);//используем WeatherManager } void Update() { //если нужно отобразить циклическую погоду if (!realweather) { if (todark && _cloudValue < 1) _cloudValue +=.0005f; else if (todark) todark = false; if (!todark && _cloudValue > 0) _cloudValue -=.0005f; else if (!todark) todark = true; SetOvercast(_cloudValue); } } private void SetOvercast(float value) { sky.SetFloat("_Blend", value); sun.intensity = _fullIntensity - (_fullIntensity * value); } public void ChangeWeather(bool value)//функция, для смены режимов погоды { if (value) { realweather = true; SetOvercast(Managers.Weather.cloudValue); } else realweather = false; } } |
13) Создайте на сцене UI интерфейс с чек боксом, включающим погоду с Интернета. Настройте чекбокс так, чтобы OnValueChange(Boolean) вызывал функцию ChangeWeather(bool value) скрипта WeatherController. Примерно как на рис.
14) Запускаем проект и проверяем работу реальной, циклической погоды и переход между режимами.
Рисунок 4 пример проекта