Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Unity_в_действии_Джозеф_Хокинг_Рус.pdf
Скачиваний:
82
Добавлен:
21.06.2022
Размер:
26.33 Mб
Скачать

220      Глава 9. Подключение игры к Интернету

этого обновление будет происходить только после щелчка на кнопке. Установите ползунок Blend для скайбокса в центральное положение, чтобы получить среднюю освещенность сцены, и щелкните на кнопке Build в нижней части окна Lighting, чтобы запечь карты освещенности (к проекту будет добавлена папка Scene; трогать ее не нужно).

В начале работы сценарий инициализирует интенсивность света. Это начальное значение он сохраняет, считая его за «полную» интенсивность. Позднее интенсивность света появится в сценарии, когда нам потребуется приглушить свет.

Затем код на постоянную величину увеличивает значение каждого кадра и использует его для корректировки неба. А именно, вызывает в каждом кадре метод SetOvercast(), который инкапсулирует все внесенные в сцену изменения. Назначение метода SetFloat() я уже объяснял, так что снова мы на этом останавливаться не будем, последняя же строка меняет интенсивность света.

Теперь выполните воспроизведение сцены. Вы должны получить результат, показанный на рис. 9.2: через пару секунд солнечный день в сцене превратится в пасмурный и облачный.

Д а —

П а — а

Рис. 9.2. До и после: переход сцены от солнца к облачности

ВНИМАНИЕ  Неожиданной особенностью Unity является фиксация в материале изменения параметра Blend. После остановки игры Unity возвращает объекты сцены в исходное состояние, но ресурсы, добавленные непосредственно с вкладки Project (как материал для нашего скайбокса), изменяются насовсем. Такое происходит только в редакторе Unity (изменения не переносятся из игры в игру после развертывания игры за пределами редактора) и может привести к крайне неприятным ошибкам, если забыть о данной особенности.

Здорово наблюдать за переходом от солнца к облачности. Но все это является лишь подготовкой к решению основной задачи: синхронизации погоды в игре в соответствии с реальными погодными условиями. Для этого нам потребуется скачать сводку погоды из Интернета.

9.2. Скачивание сводки погоды из Интернета

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

9.2. Скачивание сводки погоды из Интернета      221

данных с помощью HTTP-запросов. В качестве бесплатного источника метеоданных я использую сайт OpenWeatherMap — перейдя по адресу http://openweathermap. org/api, можно воспользоваться прикладным программным интерфейсом (Application Programming Interface, API), который обеспечивает доступ к службе посредством кода, а не графического интерфейса.

ОПРЕДЕЛЕНИЕ  Веб-службой, или веб-API, называется подключенный к Интернету сервер, возвращающий данные по запросу. С технической точки зрения никакой разницы между API и вебузлом не существует; веб-узел является службой, возвращающей данные веб-странице, а браузеры интерпретируют HTML-данные, превращая их в видимые документы.

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

Поэтому мы создадим вспомогательный класс NetworkService, который позаботится о подключении к Интернету и HTTP-запросах. После чего класс WeatherManager сможет заставить класс NetworkService отправлять запросы и возвращать полученные ответы. Принцип функционирования такой структуры кода показан на рис. 9.3.

1. У -€‚ƒ„…ƒ†а

WeatherManager

2. Д-€‚ƒ„…ƒ† ’а€„а“”•ƒ„ €ƒ†“-€

 

 

„†ƒˆ‰Š„ а‹‹Œƒ.

NetworkService

 

 

• GetData()

–„‚†а“-„— ’а‚†–€

 

 

 

 

 

• OnResponse()

 

• HTTPRequest()

 

 

 

 

 

3. Сƒ†“-€ “–’“†аšаƒ„

 

 

IGameManager

-€‚ƒ„…ƒ†‰ HTTP-–„“ƒ„

 

 

 

 

 

 

 

 

 

 

Рис. 9.3. Структура кода, передающего данные по сети

Очевидно, что этот механизм будет работать только при наличии у класса WeatherManager доступа к объекту NetworkService. Для решения этой задачи мы создадим объект в сценарии Managers и в момент инициализации различных диспетчеров будем вставлять в них объект NetworkService. В результате ссылку на этот объект получит не только диспетчер WeatherManager, но и все диспетчеры, которые вы создадите после этого.

Воспроизводить архитектуру из главы 8 мы начнем с копирования сценариев

ManagerStatus и IGameManager (напоминаю, что IGameManager — это интерфейс, который должны реализовывать все диспетчеры, в то время как ManagerStatus — это перечисление, которым пользуется IGameManager). В сценарий IGameManager следует внести небольшие изменения, связанные с появлением класса NetworkService, поэтому создайте новый сценарий NetworkService (пока оставьте его пустым, код мы напишем позже), а затем отредактируйте сценарий IGameManager в соответствии с лис­ тингом 9.2.

222      Глава 9. Подключение игры к Интернету

Листинг 9.2. Добавление в сценарий IGameManager класса NetworkService

public interface IGameManager { ManagerStatus status {get;}

void Startup(NetworkService service); ¬ Функция Startup теперь принимает один параметр — вставленный объект.

}

Далее создадим сценарий WeatherManager для реализации нашего слегка отредактированного интерфейса. Добавьте в новый сценарий содержимое следующего листинга.

Листинг 9.3. Начальный код сценария WeatherManager

using UnityEngine;

using System.Collections;

using System.Collections.Generic;

public class WeatherManager : MonoBehaviour, IGameManager { public ManagerStatus status {get; private set;}

// Сюда добавляется значение облачности (см. листинг 9.8) private NetworkService _network;

public void Startup(NetworkService service) { Debug.Log("Weather manager starting...");

_network = service; ¬ Сохранение вставленного объекта NetworkService.

status = ManagerStatus.Started;

}

}

Этот начальный вариант сценария WeatherManager не выполняет никаких действий. Пока это всего лишь минимальное количество кода, необходимое сценарию IGameManager для реализации класса: здесь объявляется свойство интерфейса status и выполняется функция Startup(). Этот пустой каркас мы заполним в следующих разделах. А пока скопируйте сценарий Managers из главы 8 и отредактируйте его таким образом, чтобы он запускал сценарий WeatherManager (как показано в следующем листинге).

Листинг 9.4. Сценарий Managers, инициализирующий сценарий 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<IGameManager> _startSequence;

void Awake() {

Weather = GetComponent<WeatherManager>();

9.2. Скачивание сводки погоды из Интернета      223

_startSequence = new List<IGameManager>(); _startSequence.Add(Weather); StartCoroutine(StartupManagers());

}

 

private IEnumerator StartupManagers() {

Создаются экземпляры объекта NetworkService

NetworkService network = new NetworkService(); ¬

для вставки во все диспетчеры.

foreach (IGameManager 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 (IGameManager 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");

}

}

Это всё, что нужно для архитектуры, заданной сценарием Managers. Как и в предыдущей главе, создайте в сцене пустой объект, который будет играть роль диспетчера,

иприсоедините к нему сценарии Managers и WeatherManager. При корректной настройке он будет выводить на консоль сообщения о загрузке, чем его функция пока

иограничится.

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

9.2.1. Запрос веб-данных через сопрограмму

Сценарий NetworkService пока пуст, и в него нужно ввести код реализации HTTPзапросов. Основным классом, сведения о котором вам потребуются, является WWW. В Unity этот класс обеспечивает взаимодействие с Интернетом. Создание экземпляра объекта WWW с использованием URL-адреса приводит к отправке запроса по этому адресу.

224      Глава 9. Подключение игры к Интернету

Сопрограммы позволяют классу WWW ждать завершения запроса. В первый раз сопрограммы упоминаются в главе 3, где мы использовали их для остановки кода на определенный период времени. Напомню определение: сопрограммами называются специальные функции, которые запускаются в фоновом режиме основной программы, в цикле выполняют код и возвращают результат в программу. Вместе с методом StartCoroutine() мы использовали ключевое слово yield, которое заставляло сопрограмму на время остановиться, вернуть управление программе, а в следующем кадре снова начать свою работу.

В главе 3 сопрограмма возвращала метод WaitForSeconds(), что останавливало работу функции на указанное количество секунд. В случае с классом WWW выполнение функции будет прервано до завершения сетевого запроса. Работа программы в данном случае напоминает асинхронные Ajax-вызовы в веб-приложениях: сначала вы посылаете запрос, потом продолжаете выполнение остальной части программы, а через некоторое время получаете ответ.

Это была теория, давайте, наконец, писать код!

Первым делом откройте сценарий NetworkService и замените присутствующий там шаблон содержимым следующего листинга.

Листинг 9.5. Выполнение HTTP-запросов в сценарии NetworkService

using UnityEngine;

using System.Collections; using System;

public class NetworkService {

private const string xmlApi = ¬ URL-адрес для отправки запроса.

"http://api.openweathermap.org/data/2.5/weather?q=Chicago,us&mode=xml";

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 { // все хорошо return true;

}

}

private IEnumerator CallAPI(string url, Action<string> callback) { WWW www = new WWW(url); ¬ HTTP-запрос, отправленный путем создания веб-объекта. yield return www; ¬ Пауза в процессе скачивания.

if (!IsResponseValid(www))

yield break; ¬ Прерывание сопрограммы в случае ошибки. callback(www.text); ¬ Делегат может быть вызван так же, как и исходная функция.

9.2. Скачивание сводки погоды из Интернета      225

}

public IEnumerator GetWeatherXML(Action<string> callback) {

return CallAPI(xmlApi, callback); ¬ Каскад ключевых слов yield в вызывающих друг друга методах

сопрограммы.

}

}

Надеюсь, вы помните объяснение особенностей данного проекта: диспетчер WeatherManager заставит сценарий NetworkService извлечь нужные данные. Фактически, весь этот код пока не запускается; вы написали код, который позднее будет вызван сценарием WeatherManager. Объяснять смысл этого листинга я начну снизу.

Включенные друг в друга методы сопрограммы

Помещенный в сопрограмму метод GetWeatherXML() заставляет сценарий Network­ Service создать HTTP-запрос. Обратите внимание, что в качестве типа возвращаемого значения указан тип IEnumerator, который объявляется во всех методах, используемых в сопрограмме.

Отсутствие ключевого слова yield в методе GetWeatherXML() на первый взгляд может показаться странным. Ведь именно это ключевое слово останавливает выполнение сопрограммы, а значит, его наличие предполагается по умолчанию. Но дело

втом, что у нас есть набор вставленных друг в друга методов. Если первый метод сопрограммы вызывает какой-то другой метод, который останавливается, выполнив только часть своего кода, остановка и возобновление работы сопрограммы будут осуществляться внутри второго метода. То есть в нашем случае ключевое слово yield

вметоде CallAPI() прерывает сопрограмму, начавшуюся в методе GetWeatherXML(). Этот принцип работы иллюстрирует рис. 9.4.

 

1. Д†‡ˆ‰Š‹‰Œ Žа‡Šа’“”‰Š ‡“•–—•

 

 

WeatherManager

‡˜Ž™аŠš ŽаˆŒ˜‡ (ˆ•Š‰› Žаˆ•‡œа

NetworkService

 

 

‡˜ˆŒ˜žŒа››Ÿ)

 

2. В ‡“•–—‰ ˜™†§ ›‰Š˜™

• GetData() {

 

• HTTPRequest() {

 

’ŸŽŸ’а‰Š ™Œ•ž˜¨

StartCoroutine()

 

CallAPI()

}

 

}

 

• OnResponse()

 

• CallAPI() {

3. С˜ˆŒ˜žŒа››а

 

 

yield WWW

 

4. С“•–—а ’˜Ž’Œа£а‰Š

callback()

˜‡Šа§а’“†’а‰Š‡”, ™˜¨™”

 

™˜ œ“«‹‰’˜ž˜ ‡“˜’а yield

 

HTTP-˜Š’‰Š ™†‡ˆ‰Š‹‰Œ•

}

 

’˜ ’Š˜Œ˜› ›‰Š˜™‰

 

 

 

Рис. 9.4. Схема работы сопрограммы, управляющей передачей данных по сети

Следующей потенциально непонятной вещью является параметр callback типа

Action.

Как работает обратный вызов

В начале сопрограммы вызывается метод с параметром callback, который принадлежит типу Action. Но что это за тип?

226      Глава 9. Подключение игры к Интернету

ОПРЕДЕЛЕНИЕ  Тип Action является делегатом (в C# есть ряд объяснений этому понятию, но я изложу самый простой). Делегаты представляют собой ссылки на какой-то другой метод/функцию. Делегат позволяет сохранить функцию (или, точнее, указатель на нее) в переменной и передать эту переменную в качестве параметра другой функции.

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

ОПРЕДЕЛЕНИЕ  Обратным вызовом (callback) называется вызов функции, используемой для обмена данными с вызывающим объектом. Объект A может сообщить объекту B об одном из своих методов. Позднее объект B может вызвать этот метод для обмена данными с объектом A.

Например, в данном случае обратный вызов позволил нам, дождавшись завершения HTTP-запроса, переслать обратно полученный ответ. Код метода CallAPI() сначала отправляет HTTP-запрос, затем останавливается, ожидая завершения этого запроса, и наконец с помощью метода callback() возвращает полученный ответ.

Обратите внимание на синтаксис <>, используемый с ключевым словом Action; указанный в угловых скобках тип требуется параметрам, чтобы подойти к этому делегату. Другими словами, функция, на которую указывает делегат Action, должна иметь параметры указанного типа. В данном случае параметром является единственная строка, поэтому у метода обратного вызова должна быть примерно такая сигнатура:

MethodName(string value)

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

Остаток листинга 9.5 достаточно очевиден. Метод IsResponseValid() проверяет наличие ошибок в HTTP-ответе. Возможны ошибки двух типов: сбой запроса из-за проблем с интернет-подключением и некорректность возвращенных данных. Константа const объявляется с URL-адресом, по которому код будет отправлять запросы. (Если хотите, можете поменять этот URL-адрес для получения сведений о погоде из другого места.)

Применение кода передачи данных по сети

Этот сценарий включает в себя код NetworkService. Воспользуемся им в сценарии WeatherManager — следующий листинг демонстрирует дополнения, которые туда нужно внести.

Листинг 9.6. Добавление в сценарий WeatherManager кода для работы с NetworkService

...

public void Startup(NetworkService service) { Debug.Log("Weather manager starting...");