Вы находитесь на странице: 1из 352

2nd Edition

ББК 32.973.2-018
УДК 004.42
Х70

Хокинг Джозеф
Х70 Unity в действии. Мультиплатформенная разработка на C#. 2-е межд. изд. — СПб.:
Питер, 2019. — 352 с.: ил. — (Серия «Для профессионалов»).
ISBN 978-5-4461-0816-9
Второе издание знаменитого бестселлера «Unity в действии» было полностью переработано, чтобы
познакомить вас с новыми подходами и идеями, позволяющими максимально эффективно использовать
Unity для разработки игр. Больше внимания уделено проектированию двумерных игр, фундаментальные
концепции которых читатель может применить на практике и построить сложный двумерный платформер.
Эту книгу можно смело назвать введением в Unity для профессиональных программистов. Джозеф Хокинг
дает людям, имеющим опыт разработки, всю необходимую информацию, которая поможет быстро освоить
новый инструмент и приступить к созданию новых игр. А учиться лучше всего на конкретных проектах
и практических заданиях.
Unity зачастую представляют как набор компонентов, не требующих программирования, что в корне
неверно. Для создания успешной игры необходимо многое: великолепная работа художника, програм-
мистские навыки, интересная история, увлекательный геймплей, дружная и слаженная работа команды
разработчиков. А еще нельзя забывать про безупречную визуализацию и качественную работу на всех
платформах — от игровых консолей до мобильных телефонов. Unity объединяет мощный движок, воз-
можности профессионального программирования и творчества дизайнеров, позволяя воплотить в жизнь
самые невероятные и амбициозные проекты.
Осваивайте Unity и быстрее приступайте к созданию собственных игр!
16+ (В соответствии с Федеральным законом от 29 декабря 2010 г. № 436-ФЗ.)

ББК 32.973.2-018
УДК 004.42

Права на издание получены по соглашению с Manning. Все права защищены. Никакая часть данной книги не может
быть воспроизведена в какой бы то ни было форме без письменного разрешения владельцев авторских прав.

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

ISBN 978-1617294969 англ. © 2018 by Manning Publications Co. All rights reserved.
ISBN 978-5-4461-0816-9 © Перевод на русский язык ООО Издательство «Питер», 2019
© Издание на русском языке, оформление ООО Издательство «Питер», 2019
© Серия «Для профессионалов», 2019
© Джозеф Хокинг, 2018
Оглавление

Предисловие.................................................................................................... 8

Введение......................................................................................................... 9

Благодарности............................................................................................... 10

О книге.......................................................................................................... 11
Структура книги............................................................................................................................................. 12
Условные обозначения, требования и доступные для скачивания данные............................... 13
Форум для обсуждения книги................................................................................................................... 13
Об авторе........................................................................................................................................................... 14
Иллюстрация на обложке........................................................................................................................... 14

Часть I. ПЕРВЫЕ ШАГИ................................................................................15


Глава 1. Знакомство с Unity............................................................................ 16
1.1. Достоинства Unity................................................................................................................................. 17
1.2. Работа с Unity.......................................................................................................................................... 22
1.3. Подготовка к программированию в Unity..................................................................................... 27
Заключение...................................................................................................................................................... 33

Глава 2. Создание 3D-ролика.......................................................................... 34


2.1. Подготовка…............................................................................................................................................. 35
2.2. Начало проекта: размещение объектов........................................................................................... 38
2.3. Двигаем объекты: сценарий, активирующий преобразования............................................... 43
2.4. Компонент сценария для осмотра сцены: MouseLook............................................................... 47
2.5. Компонент для клавиатурного ввода.............................................................................................. 54
Заключение...................................................................................................................................................... 59

Глава 3. Добавляем в игру врагов и снаряды..................................................... 60


3.1. Стрельба путем бросания лучей........................................................................................................ 61
3.2. Создаем активные цели........................................................................................................................ 67
3.3. Базовый искусственный интеллект для перемещения по сцене........................................... 70
3.4. Увеличение количества врагов.......................................................................................................... 74
3.5. Стрельба путем создания экземпляров.......................................................................................... 78
Заключение...................................................................................................................................................... 83
6    Оглавление

Глава 4. Работа с графикой............................................................................. 84


4.1. Основные сведения о графических ресурсах............................................................................... 84
4.2. Создание геометрической модели сцены....................................................................................... 87
4.3. Наложение текстур................................................................................................................................ 90
4.4. Создание неба с помощью текстур................................................................................................... 95
4.5. Собственные трехмерные модели..................................................................................................... 99
4.6. Системы частиц..................................................................................................................................... 103
Заключение.................................................................................................................................................... 108

Часть II. ОСВАИВАЕМСЯ............................................................................109


Глава 5. Двумерная игра Memory....................................................................110
5.1. Подготовка к работе с двумерной графикой............................................................................... 111
5.2. Создание карт и превращение их в интерактивные объекты............................................... 116
5.3. Отображение набора карт.................................................................................................................. 118
5.4. Совпадения и подсчет очков............................................................................................................ 124
5.5. Кнопка Restart....................................................................................................................................... 129
Заключение.................................................................................................................................................... 132

Глава 6. Базовый двумерный платформер.......................................................133


6.1. Создание графических ресурсов..................................................................................................... 134
6.2. Смещение персонажа вправо и влево............................................................................................ 136
6.3. Анимация спрайтов............................................................................................................................. 139
6.4. Прыжки.................................................................................................................................................... 142
6.5. Дополнительные возможности для платформера.................................................................... 145
Заключение.................................................................................................................................................... 151

Глава 7. Двумерный GUI для трехмерной игры................................................152


7.1. Перед тем как писать код…................................................................................................................ 153
7.2. Настройка GUI...................................................................................................................................... 156
7.3. Программирование интерактивного UI....................................................................................... 161
7.4. Обновление игры в ответ на события........................................................................................... 169
Заключение.................................................................................................................................................... 173

Глава 8. Игра от третьего лица: перемещения и анимация игрока......................174


8.1. Корректировка положения камеры............................................................................................... 176
8.2. Элементы управления движением, связанные с камерой...................................................... 182
8.3. Прыжки.................................................................................................................................................... 186
8.4. Анимация персонажа.......................................................................................................................... 192
Заключение.................................................................................................................................................... 200

Глава 9. Интерактивные устройства и элементы...............................................201


Оглавление    7

9.1. Двери и другие устройства................................................................................................................ 202


9.2. Взаимодействие с объектами через столкновение.................................................................... 206
9.3. Управление данными инвентаризации и состоянием игры.................................................. 212
9.4. Интерфейс для использования и подготовки элементов....................................................... 220
Заключение.................................................................................................................................................... 226

Часть III. УВЕРЕННЫЙ ФИНИШ................................................................227


Глава 10. Подключение к интернету................................................................228
10.1. Натурная сцена................................................................................................................................... 230
10.2. Скачивание метеорологических данных................................................................................... 233
10.3. Рекламный щит................................................................................................................................... 245
10.4. Отправка данных на веб-сервер.................................................................................................... 251
Заключение.................................................................................................................................................... 254

Глава 11. Звуковые эффекты и музыка...........................................................255


11.1. Импорт звуковых эффектов........................................................................................................... 256
11.2. Звуковые эффекты............................................................................................................................ 259
11.3. Интерфейс управления звуком..................................................................................................... 263
11.4. Фоновая музыка................................................................................................................................. 269
Заключение.................................................................................................................................................... 278

Глава 12. Объединение фрагментов в готовую игру..........................................279


12.1. Изменение назначения проектов для получения ролевого боевика................................ 280
12.2. Разработка общей игровой структуры........................................................................................ 295
12.3. Продвижение по уровням............................................................................................................... 303
Заключение.................................................................................................................................................... 309

Глава 13. Развертывание игр на устройствах игроков.......................................310


13.1. Приложения для настольных компьютеров: Windows, Mac и Linux............................... 312
13.2. Создание игр для интернета........................................................................................................... 317
13.3. Сборки для мобильных устройств: iOS и Android................................................................. 321
Заключение.................................................................................................................................................... 335

Послесловие.................................................................................................336
Проектирование игр.................................................................................................................................... 337
Продвижение вашей игры......................................................................................................................... 338

Приложения..................................................................................................339
Приложение А. Перемещение по сцене и клавиатурные комбинации..................................... 339
Приложение Б. Внешние инструменты, используемые вместе с Unity................................... 341
Приложение В. Моделирование скамейки в программе Blender................................................ 344
Предисловие

Созданием игр я занялся в 1982 году. Это было непросто: интернета в то время не было,
доступные ресурсы ограничивались небольшим количеством по большей части ужас-
ных книг и журналов с интересными, но запутанными фрагментами кода, а игровых
движков попросту не существовало! Написание кода для игр было гонкой с огромным
количеством препятствий.
Как я завидую тебе, читатель, держащий в руках эту глубокоинформативную книгу!
Инструмент под названием Unity многим дал возможность заняться программиро-
ванием игр. В данном случае достигнут идеальный баланс между мощностью про-
фессионального игрового движка и его доступностью, так ценимой начинающими.
Но доступность в данном случае возникает только при правильном обучении. Мне
довелось поработать в цирковой труппе, которой руководил фокусник. Он был столь
любезен, что взял меня на работу и помог стать хорошим артистом. «Стоя на сцене, —
говорил он, — ты даешь обещание. Ты обещаешь, что не будешь понапрасну тратить
время зрителей».
В этой книге мне больше всего нравится практическая часть. Джо Хокинг не тратит
ваше время понапрасну и быстро переходит к написанию кода, причем не каких-то
бессмысленных фрагментов, а кода, который вы можете понять и использовать в сво-
их целях. Ведь он знает, что вы не просто хотите прочитать эту книгу и проверить,
как работают приведенные им примеры, — вашей целью является программирование
собственных игр.
И благодаря его указаниям вы научитесь это делать быстрее, чем можно было
ожидать. Следуйте тропой, которую проложил для вас Джо, но как только ощутите
в себе силы, не колеблясь уходите с нее и прокладывайте собственную дорогу. Сразу
переходите к наиболее интересным вам темам, экспериментируйте, будьте смелым
и храбрым! Почувствовав, что заблудились, вы в любой момент сможете вернуться
к тексту книги.
Впрочем, довольно вступительных слов — вас с нетерпением ждет карьера разработ-
чика игр! Запомните этот день, ведь именно он изменит вашу жизнь. Именно сегодня
вы начали создавать игры.
Из первого издания
Джесси Шелл (Jesse Schell),
руководитель фирмы Schell Games,
автор книги Art of Game Design
www.amazon.com/Art-Game-Design-Lenses-Second/dp/1466598646/
Введение

Программированием игр я занимаюсь уже много лет, но с Unity познакомился отно-


сительно недавно. Разработку игр я осваивал во времена, когда такого инструмента
попросту не существовало; его первая версия появилась только в 2005 году. С самого
начала это было многообещающее средство разработки, но всеобщее признание оно
получило только после выхода нескольких версий. В частности, такие платформы, как
iOS и Android (называемые «мобильными»), появились не так давно, и именно они
повлияли на рост популярности Unity.
Изначально я смотрел на Unity как на диковинку, интересный инструмент разработ-
ки, за развитием которого имеет смысл понаблюдать. В то время я писал игры для
настольных компьютеров и сайтов, выполняя проекты для широкого круга клиентов.
Я пользовался такими инструментами, как Blitz3D и Flash, великолепно подходящи-
ми для программирования, но ограниченными в ряде других аспектов. Но по мере их
устаревания я стал искать более совершенные средства разработки игр.
Я начал экспериментировать с Unity версии 3 и полностью переключился на этот ин-
струмент, будучи разработчиком в компании Synapse Games. Изначально я занимался
интернет-играми, но в итоге перешел на разработку игр для мобильных устройств. А затем
все вернулось на круги своя, так как инструмент Unity позволял выполнять разверты-
вание одного и того же базового кода как на мобильных устройствах, так и в интернете!
Я всегда понимал важность передачи знаний, поэтому последние годы занялся пре-
подаванием такой темы, как разработка игр. Во многом это обусловлено примерами,
которые подавали мне мои наставники и учителя. Кстати, возможно, об одном из них
вы слышали, потому что это потрясающий человек — Рэнди Пауш (Randy Pausch),
незадолго до своей кончины выступивший с Последней общественной лекцией. Я препо-
давал в нескольких школах и всегда хотел написать книгу, посвященную разработке игр.
Эта книга во многом напоминает другую, о которой я мечтал во времена, когда толь-
ко начал осваивать Unity. К многочисленным достоинствам Unity можно отнести
огромное количество обучающих ресурсов, но, как правило, они представляют собой
наборы отдельных фрагментов (ссылки на сценарии или не связанные друг с другом
уроки), соответственно, поиск нужного материала затруднен. Мне же всегда хотелось
получить книгу, объединяющую все необходимые знания, представленные в четкой
и логически связанной манере, поэтому я написал такую книгу для вас. Моя целевая
аудитория — это люди, уже имеющие навыки программирования, но не обладающие
опытом работы с Unity и, возможно, никогда не занимавшиеся разработкой игр. Вы-
бор проектов отражает мой опыт наработки навыков и уверенности в себе, который
я в свое время получил, выполняя различные заказы в достаточно быстром темпе.
Приступая к изучению разработки игр с помощью Unity, вы начинаете захватываю-
щее приключение. Для меня этот процесс был связан с необходимостью не вешать
нос перед лицом многочисленных препятствий. У вас же есть преимущество в виде
единого логически согласованного ресурса — этой книги!
Благодарности

Я хотел бы поблагодарить издательство Manning Publications, предоставившее мне


возможность написать эту книгу. В этом мне помогли редакторы, с которыми я работал,
в том числе Робин де Йон (Robin de Jongh) и особенно Дэн Махари (Dan Maharry).
Именно взаимодействие с ними сделало книгу намного лучше. Мои искренние бла-
годарности и многим другим людям, сотрудничавшим со мной в процессе написания
и издания.
От пристального внимания рецензентов на всем протяжении работы над книгой она
только выиграла. Спасибо Алексу Лукасу (Alex Lucas), Крейгу Хоффману (Craig
Hoffman), Дэну Кэйсенджару (Dan Kacenjar), Джошуа Фредерику (Joshua Frederick),
Люке Кампобассо (Luca Campobasso), Марку Элстону (Mark Elston), Филиппу Таффе-
ту (Philip Taffet), Рене ван ден Бергу (René van den Berg), Серджио Арбео Родригесу
(Sergio Arbeo Rodríguez), Шайло Моррису (Shiloh Morris), Виктору М. Пересу (Victor
M. Perez), Кристоферу Хаупту (Christopher Haupt), Клаудио Казейро (Claudio Caseiro),
Давиду Торрибиа Иниго (David Torribia Iñigo), Дину Цальтасу (Dean Tsaltas), Эрику
Вильямсу (Eric Williams), Ники Бакнеру (Nickie Buckner), Робину Дьюсону ( Robin
Dewson), Сергею Евсикову и Тане Вильке (Tanya Wilke). Отдельная благодарность
техническому редактору Скотту Шоси (Scott Chaussee) и техническому корректору
Кристоферу Хаупту (Christopher Haupt), а также Рене ван ден Бергу (René van den
Berg) и Шайло Моррису (Shiloh Morris), сыгравшим свою роль в выходе второй
редакции книги. Также хотелось бы поблагодарить Джесси Шелл (Jesse Schell) за
предисловие к книге.
Затем я хотел бы выразить свою признательность людям, помощь которых сделала
мой опыт работы с Unity крайне плодотворным. Разумеется, этот список начинается
с создавшей Unity (игровой движок) компании Unity Technologies. Чувство глубокой
благодарности я испытываю и к сообществу gamedev.stackexchange.com. Этот сайт конт­
роля качества я посещал практически ежедневно, учась у других и отвечая на чужие
вопросы. Самый же сильный толчок к работе с Unity я получил от своего руководи-
теля в фирме Synapse Games Алекса Рива (Alex Reeve). Мои коллеги показали мне
множество приемов и методов, которые я постоянно применяю при написании кода.
Наконец, я хотел бы поблагодарить мою жену Виргинию за ту поддержку, которую
она мне оказывала во время написания книги. До начала работы я понятия не имел,
насколько подобные проекты переворачивают твою жизнь, влияя на все ее аспекты.
Спасибо ей за ее любовь и помощь.
О книге

Второе издание книги Unity в действии посвящено программированию игр с помощью


Unity. Эту книгу можно считать введением в Unity для опытных программистов. Цель
ее крайне проста: научить людей, имеющих опыт программирования, но ни разу не
сталкивавшихся с Unity, разрабатывать игры с помощью этого инструмента.
Учить разработке лучше всего на примерах проектов, заставляя обучающихся выпол-
нять практические задания, и именно такой подход используется в данном случае. Темы
представлены как этапы построения отдельных игр, и я настоятельно рекомендую вам
в процессе знакомства с книгой заняться разработкой этих игр при помощи Unity. Мы
рассмотрим ряд проектов, каждому из которых посвящено несколько глав. Бывают
книги, целиком посвященные одному крупному проекту, но такой подход исключает
возможность чтения с середины, если информация в первых главах покажется вам
неинтересной.
В этой книге более строго, чем в большинстве других изданий (особенно предна-
значенных для начинающих), изложен материал, касающийся программирования.
Приложение Unity зачастую представляют как набор компонентов, не требующих
программирования, что в корне неверно, так как не дает знаний, без которых невоз-
можно производство коммерчески успешных продуктов. Если вы пока не имеете
навыков программирования, советую сначала их приобрести и только после этого
приступать к чтению.
Выбор языка программирования не имеет особого значения; все примеры в  книге
написаны на C#, но они легко переводятся на другие языки. Первая половина книги
в изрядной степени посвящена знакомству с новыми понятиями, и первые шаги по
разработке игры с помощью Unity намеренно описаны со всей возможной тщатель-
ностью, но затем повествование ускоряется, давая читателям возможность выполнять
проекты в различных игровых жанрах. Завершает книгу описание развертывания игр
на различных платформах, но в целом мы не будем делать упор на этом аспекте, так
как Unity не зависит от платформы.
Что касается прочих аспектов разработки игр, излишне широкий охват различных
художественных дисциплин привел бы к сокращению объема представленного в книге
конкретного материала по Unity и в значительной степени относился бы к внешним по
отношению к Unity программам (например, программам создания анимации). Поэтому
обсуждение художественных дисциплин сводится к тем аспектам, которые имеют непо-
средственное отношение к Unity или должны быть известны всем разработчикам игр.
Впрочем, одно из приложений посвящено моделированию нестандартных объектов.
12    О книге

Структура книги
Глава 1 знакомит с Unity — межплатформенной средой разработки игр. Вы освоите
базовую систему компонентов, лежащую в основе Unity, а также научитесь писать
и выполнять базовые сценарии.
В главе 2 мы перейдем к написанию программы, демонстрирующей движение в трех-
мерном пространстве, попутно рассмотрев такие темы, как ввод с помощью мыши
и клавиатуры. Детально объясняется определение положения объектов в трехмерном
пространстве и операции их поворота.
В главе 3 мы превратим демонстрационную программу в шутер от первого лица, про-
демонстрировав метод испускания луча и основы искусственного интеллекта. Испу-
скание луча (мы создаем в сцене линию и смотрим, с чем она пересечется) требуется
во всех вариантах игр.
Глава 4 посвящена импорту и созданию игровых ресурсов. Это единственная глава
в книге, в которой код не играет центральной роли, так как каждому проекту требуются
(базовые) модели и текстуры.
Глава 5 научит вас создавать в Unity двумерные игры. Хотя изначально этот инстру-
мент предназначался исключительно для создания трехмерной графики, сейчас в нем
прекрасно поддерживается двумерная графика.
В главе 6 продолжается объяснение принципов создания двумерных игр на примере
платформера. В частности, мы реализуем элементы управления, имитацию физической
среды и анимацию для персонажа.
Глава 7 знакомит с новейшей GUI-функциональностью в Unity. Пользовательский
интерфейс требуется всем играм, а последние версии Unity могут похвастаться улуч-
шенной системой создания пользовательского интерфейса.
В главе 8 мы создадим еще одну программу, демонстрирующую движение в трехмерном
пространстве, однако на этот раз с точки зрения стороннего наблюдателя. Реализация
элементов управления третьим лицом даст вам представление о ключевых матема-
тических операциях в трехмерном пространстве, кроме того, вы научитесь работать
с анимированными персонажами.
Глава 9 покажет способы реализации интерактивных устройств и элементов в игре.
У игрока будет ряд способов применения этих устройств, в том числе прямым каса-
нием, прикосновением к пусковым устройствам внутри игры или нажатием кнопки
контроллера.
Глава 10 учит взаимодействию со Всемирной паутиной. Вы узнаете, как отправить
и получить данные с помощью стандартных технологий, таких как HTTP-запросы на
получение с сервера XML-данных.
В главе 11 вы научитесь добавлять в игры звук. В Unity замечательно поддерживаются
как короткие звуковые эффекты, так и долгие музыкальные фонограммы; оба варианта
звукового сопровождения критически важны почти для всех видеоигр.
В главе 12 мы соберем воедино фрагменты из различных глав, чтобы получить в итоге
одну игру. Кроме того, вы научитесь программировать элементы управления, манипу-
ляция которыми осуществляется с помощью мыши, и сохранять игру.
Форум для обсуждения книги   13

Глава 13 демонстрирует процесс создания итогового приложения с его последую-


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

Условные обозначения, требования и доступные


для скачивания данные
Весь код в этой книге, как в листингах, так и в представленных фрагментах, набран
вот таким моноширинным шрифтом, позволяющим отличить код от остального текста.
Большинство листингов снабжено примечаниями, отмечающими ключевые понятия,
кроме того, в тексте периодически встречаются маркированные списки с дополнитель-
ными сведениями по поводу кода. Код отформатирован при помощи переносов строки
и аккуратных отступов в соответствии с шириной страницы.
Единственной программой, которая вам потребуется, является Unity; в книге исполь-
зуется версия Unity 2017.1, которая являлась самой последней на момент написания
этого текста. В некоторых главах время от времени обсуждаются другие программы,
но это всегда дополнительная информация, не оказывающая решающего влияния на
изучение основного материала.

ВНИМАНИЕ  В Unity-проектах сохраняется информация о том, в какой версии программы они


были созданы, и при попытке открыть их в другой версии выводят предупреждение. Если, открыв
относящиеся к этой книге материалы, скачанные из интернета, вы увидите такое предупреждение,
просто щелкните на кнопке Continue.

Встречающиеся в книге фрагменты кода в общем случае демонстрируют добавления


и изменения, вносимые в существующие файлы; если это не первое появление файла
с кодом, не следует заменять файл соответствующим листингом. Можно просто ска-
чать рабочий проект целиком, но обучение пойдет намного быстрее, если вы будете
набирать все листинги вручную, используя примеры из проекта только для сравнения.
Весь код доступен для скачивания на сайте издателя по адресу www.manning.com/books/
unity-in-action-second-edition, а также на сайте GitHub (https://github.com/jhocking/uia-2e).

Форум для обсуждения книги


На странице https://forums.manning.com/forums/unity-in-action-second-edition вы найдете
информацию о том, как попасть на форум после регистрации, на какую помощь вы
можете рассчитывать, а также о правилах поведения на форуме.
Издательство Manning взяло на себя обязательство по предоставлению места, где
читатели могут конструктивно пообщаться как друг с другом, так и с автором книги.
Но оно не может гарантировать присутствия на форуме автора, участие которого
в обсуждениях является добровольным (и неоплачиваемым). Мы надеемся, что вы
14    О книге

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

Об авторе
Джозеф Хокинг живет в Чикаго и занимается разработкой про-
граммного обеспечения для интерактивных сред. Он работает
в фирме InContext Solutions. Первое издание данной книги было
написано в период его работы в компании Synapse Games. Кроме
того, он преподавал разработку игр в Университете Иллинойса,
в Чикагском институте искусств и в колледже Колумбия. Его
сайт www.newarteest.com.

Иллюстрация на обложке
Книгу Unity в действии, 2-е издание украшает иллюстрация, подписанная как «Платье
церемониймейстера Великого господина». Великим господином называли султана
Османской империи. Иллюстрация взята из четырехтомника «Коллекция костюмов
разных народов, античных и современных» Томаса Джеффериса, опубликованного
в Лондоне между 1757 и 1772 годами. На титульной странице указано, что это вы-
полненные вручную каллиграфические цветные гравюры, обработанные гуммиараби-
ком. Томас Джефферис (1719–1771) носил звание «Географ короля Георга III». Этот
английский картограф был ведущим поставщиком карт того времени. Он гравировал
и печатал множество карт для нужд правительства и других официальных органов.
Выпустил он и множество коммерческих карт и атласов, в частности карт Северной
Америки. Он интересовался и одеждой народов, населяющих разные земли, и собрал
блестящую коллекцию различных платьев, описав ее в четырех томах.
В конце XVIII века очарование чужих земель и путешествий ради удовольствия было
относительно новым явлением, и подобные коллекции были весьма популярны, так
как позволяли получить представление о том, как выглядят жители других стран.
Разнообразие собранных Джефферисом рисунков свидетельствует о яркой индивиду-
альности и уникальности народов мира, живших более 200 лет назад. С тех пор стиль
одежды сильно изменился, исчезли характеризовавшие различные области и страны
различия. Сейчас трудно распознать по одежде даже жителей различных континентов.
Если взглянуть на это с оптимистической точки зрения, культурное и внешнее многооб-
разие было принесено в жертву более насыщенной личной жизни, более неординарной
и интересной интеллектуальной и технической деятельности.
Сейчас, когда одну техническую книгу сложно отличить от другой, издательство
Manning украшает обложки своих книг изображениями, порожденными богатой ва-
риативностью жизненного уклада народов двухвековой давности, продлевая жизнь
рисункам Джеффериса.
Часть I
ПЕРВЫЕ ШАГИ
Настало время знакомства с Unity. Ничего страшного, если вы ничего не
знаете о нем! Я подробно опишу Unity и научу основам программирования
игр с его помощью. В финале вы получите инструкцию по разработке про-
стой игры. Первый проект познакомит вас с различными техниками, давая
представление о том, как выглядит рабочий процесс.
Итак, начнем!
1 Знакомство с Unity

33 Почему следует выбрать Unity.


33 Как работает редактор Unity.
33 Как выглядит программирование в Unity.
33 В чем разница языков C# и JavaScript.

Возможно, вы, как и  я, мысленно давным-давно разрабатываете видеоигру. Но


перей­ти из лагеря игроков в лагерь создателей игр непросто. За последние годы
появилось множество инструментов разработки игр, и мы обсудим один из самых
новых и мощных представителей этого семейства. Приложение Unity — это про-
фессиональный игровой движок, с помощью которого создаются видеоигры для
различных платформ. Им ежедневно пользуются опытные разработчики, и вместе
с тем это один из наиболее доступных инструментов для новичков. Еще недавно
решение научиться программированию игр (особенно трехмерных) наталкивалось
на множество серьезных препятствий, но приложение Unity сильно облегчило
жизнь новичкам.
Раз вы читаете эту книгу, значит, интересуетесь компьютерными технологиями и либо
разрабатывали игры с помощью других инструментов, либо создавали программное
обеспечение других типов, например приложения для рабочего стола или веб-сайты.
Создание видеоигр в своей основе не отличается от написания любого другого ПО; по
большей части различия проявляются в количественной плоскости. К примеру, игра
намного интерактивнее большинства веб-сайтов, а значит, потребуется код другого
типа. Но в обоих случаях будут применяться сходные навыки и процессы. Тем, кто
преодолел первое препятствие на пути к карьере разработчика игр, то есть изучил ос-
новы создания ПО, нужно сделать следующий шаг: выбрать инструмент и приобрести
специализированные навыки программирования. В этом смысле приложение Unity
представляет собой замечательную среду разработки.
1.1. Достоинства Unity   17

ПРЕДУПРЕЖДЕНИЕ ПО ПОВОДУ ТЕРМИНОЛОГИИ_ ______________________


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

Первым делом зайдите на сайт www.unity3d.com и скачайте среду разработки. Я поль-


зовался версией Unity 2017.1, то есть наиболее новой на момент написания книги.
Адрес сайта подчеркивает, что изначально приложение Unity предназначалось для
создания трехмерных игр. Их поддержка по-прежнему остается приоритетной, но
теперь в Unity замечательно разрабатываются и двумерные игры. В платной версии
программы доступны расширенные функциональные возможности, но базовая версия
распространяется бесплатно. Все приведенные в книге примеры прекрасно работают
в бесплатной версии, так что покупать Unity  Pro не потребуется. Платная версия
отличается от бесплатной уже упоминавшимися расширенными функциональными
возможностями (рассмотрение которых выходит за рамки данной книги) и коммер-
ческими условиями лицензирования.

1.1. Достоинства Unity


Внимательно рассмотрим данное в начале главы определение: приложение Unity — это
профессиональный игровой движок, позволяющий создавать видеоигры для различ-
ных платформ. Это достаточно точный ответ на вопрос «Что такое Unity?». Но что
конкретно он означает? И чем примечателен инструмент Unity?

1.1.1. Преимущества Unity


Любой игровой движок предоставляет множество функциональных возможностей,
которые задействуются в различных играх. Реализованная на конкретном движке
игра получает все функциональные возможности, к которым добавляются ее соб-
ственные игровые ресурсы и код игрового сценария. Приложение Unity предлагает
моделирование физических сред, карты нормалей, преграждение окружающего света
в экранном пространстве (Screen Space Ambient Occlusion, SSAO), динамические
тени… список можно продолжать долго. Подобные наборы функциональных воз-
можностей есть во многих игровых движках, но Unity обладает двумя основными
преимуществами над другими передовыми инструментами разработки игр. Это
крайне производительный визуальный рабочий процесс и сильная межплатфор-
менная поддержка.
Визуальный рабочий процесс — достаточно уникальная вещь, выделяющая Unity из
большинства сред разработки игр. Альтернативные инструменты разработки зачастую
представляют собой набор разрозненных фрагментов, требующих контроля, а в не-
которых случаях библиотеки, для работы с которой нужно настраивать собственную
18    Глава 1. Знакомство с Unity

интегрированную среду разработки (Integrated Development Environment, IDE), це-


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

ПРИМЕЧАНИЕ  В других инструментах, оснащенных центральным визуальным редактором,


возможность написания сценариев зачастую поддерживается ограниченно и недостаточно гибко,
но приложение Unity лишено этого недостатка. Все, что создается для Unity, в конечном счете про-
ходит через визуальный редактор, но основной интерфейс при этом включает в себя множество
связанных проектов с запускаемым в игровом движке пользовательским кодом. Это своего рода
аналог связывания классов в настройках проекта для таких интегрированных сред разработки, как
Visual Studio или Eclipse. Поэтому опытным программистам не стоит пренебрегать Unity, считая
это приложение чисто визуальным инструментом создания игр с  ограниченной возможностью
программирования!

Редактор особенно удобен для процессов с последовательным улучшением, например


циклов создания прототипов или тестирования. Даже после запуска игры остается
возможность модифицировать в нем объекты и двигать элементы сцены. Настраивать
можно и сам редактор. Для этого применяются сценарии, добавляющие к интерфейсу
новые функциональные особенности и элементы меню.
Дополнением к производительности, которую обеспечивает редактор, служит сильная
межплатформенная поддержка набора инструментов Unity. В данном случае это сло-
восочетание подразумевает не только места развертывания (игру можно развернуть на
персональном компьютере, в интернете, на мобильном устройстве или на консоли), но
и инструменты разработки (игры создаются на машинах, работающих под управлением
как Windows, так и Mac OS). Эта независимость от платформы явилась результатом
того, что изначально приложение Unity предназначалось исключительно для ком-
пьютеров Mac, а позднее было перенесено на машины с операционными системами
семейства Windows. Первая версия появилась в 2005 году, а к настоящему моменту
вышли уже пять основных версий (с множеством небольших, но частых обновлений).
Изначально разработка и развертка поддерживались только для машин Mac, но через
несколько месяцев вышло обновление, позволяющее работать и на машинах с Windows.
В следующих версиях добавлялись все новые платформы развертывания, например
межплатформенный веб-плеер в 2006-м, iPhone в 2008-м, Android в 2010-м и даже такие
игровые консоли, как Xbox и PlayStation. Позднее появилась возможность развертки
в WebGL — новом фреймворке для трехмерной графики в веб-браузерах. Немногие
игровые движки поддерживают такое количество целевых платформ развертывания,
и ни в одном из них развертка на разных платформах не осуществляется настолько
просто.
Дополнением к этим основным достоинствам идет и третье, менее бросающееся в глаза
преимущество в виде модульной системы компонентов, которая используется для
конструирования игровых объектов. «Компоненты» в такой системе представляют со-
бой комбинируемые пакеты функциональных элементов, поэтому объекты создаются
1.1. Достоинства Unity   19

как наборы компонентов, а не как жесткая иерархия классов. В результате получается
альтернативный (и обычно более гибкий) подход к объектно-ориентированному
программированию, в котором игровые объекты создаются путем объединения, а не
наследования. Оба подхода схематично показаны на рис. 1.1.

Наследование Система компонентов

Враг Мобильный Мобильный Стационарный


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

Рис. 1.1. Сравнение наследования с компонентной системой

В компонентной системе объект существует в горизонтальной иерархии, поэтому


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

1.1.2. Недостатки, о которых нужно знать


Многочисленные преимущества приложения Unity превращают его в замечательное
средство разработки игр, но было бы упущением не упомянуть о его недостатках.
В частности, затруднения может вызвать нетипичное сочетание визуального редактора
со сложным кодом, несмотря на всю его эффективность в рамках компонентной систе-
мы Unity. В сложных сценах из виду могут потеряться присоединенные компоненты.
Разумеется, существует функция поиска, но она могла бы быть и более надежной; по-
рой все равно возникают ситуации, когда для поиска связанных сценариев приходится
20    Глава 1. Знакомство с Unity

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

ПРИМЕЧАНИЕ  Раньше существенным недостатком была сложность работы с системами контроля


версий (такими, как Subversion, Git и Mercurial), но в более поздних версиях Unity все прекрасно
работает. В Сети можно наткнуться на устаревшие сведения, в которых утверждается, что Unity не
работает с системами контроля версий, но на более новых ресурсах есть указания, какие папки и файлы
проекта следует поместить в репозиторий. Начать лучше всего с чтения документации https://docs.
unity3d.com/ru/current/Manual/ExternalVersionControlSystemSupport.html. Или обратите внимание на файл
.gitignore в хранилище GitHub (http://mng.bz/g7nl).

Третий недостаток связан с шаблонами создания экземпляров (prefabs). Эта концепция


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

1.1.3. Примеры игр, созданных в Unity


Итак, вы познакомились с сильными и слабыми сторонами Unity, но, возможно, пока
до конца не уверены в том, что этот инструмент позволяет получать первоклассные
результаты. Зайдите в галерею Unity на странице http://unity3d.com/ru/showcase/gallery
и полюбуйтесь постоянно обновляемым списком сотен игр и симуляций, созданных
этим инструментом. Я, в свою очередь, приведу небольшой список игр, демонстриру-
ющих разные жанры и платформы развертывания.

Игры для рабочего стола (WINDOWS, MAC, LINUX)


Редактор работает на одной платформе с приложением, поэтому зачастую проще всего
развернуть игру на машине с операционной системой семейства Windows или Mac.
Примеры игр для рабочего стола в различных жанрах:
 Guns of Icarus Online (рис. 1.2)  — игра от первого лица в  жанре многопользова-
тельского симулятора боевого дирижабля, созданная независимым разработчи-
ком Muse Games.
 Gone Home (рис. 1.3) — квест от первого лица, разработанный независимой студи-
ей Fullbright Company.
1.1. Достоинства Unity   21

Рис. 1.2. Guns of Icarus Online Рис. 1.3. Gone Home

Игры для мобильных устройств (IOS, ANDROID)


Приложение Unity также позволяет развертывать игры на мобильных платформах,
таких как iOS (смартфонах iPhone и планшетах iPad) и Android (смартфонах и план-
шетах). Примеры мобильных игр в различных жанрах:
 Lara Croft GO (рис. 1.4)  — трехмерная головоломка, разработанная компанией
Square Enix.
 INKS (рис. 1.5) — двумерная головоломка от студии State of Play.

Рис. 1.4. Lara Croft GO Рис. 1.5. INKS

 Tyrant Unleashed (рис. 1.6)  — коллекционная карточная игра, созданная студией


Synapse Games.

Рис. 1.6. Tyrant Unleashed


22    Глава 1. Знакомство с Unity

Игры для консолей (PlayStation, Xbox, Switch)


С помощью Unity можно разворачивать игры на игровых консолях, хотя для этого
требуется лицензия от Sony, Microsoft или Nintendo. Межплатформенная развертка
позволяет избежать лицензионных ограничений, поэтому консольные игры часто до-
ступны и на обычных компьютерах. Примеры таких игр в различных жанрах:
 Yooka-Laylee (рис. 1.7) — трехмерная игра в жанре платформер от британской сту-
дии Playtonic Games.
 Shadow Tactics (рис. 1.8) — симулятор военной тактики в режиме реального време-
ни, созданный немецкой студией Mimimi Productions.

Рис. 1.7. Yooka-Laylee Рис. 1.8. Shadow Tactics

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

1.2. Работа с Unity


Выше уже много было сказано о том, как выгоден встроенный в Unity визуальный
редактор с точки зрения производительности, пришло время познакомиться с  его
интерфейсом и узнать, как он работает. Если на вашем компьютере еще нет приложе-
ния, скачайте его со страницы http://unity3d.com/ru/get-unity и установите (обязательно
установите флажок Example Project, если установщик его сбросит). После этого запустите
Unity, и мы приступим к изучению интерфейса.
Для наглядности откройте входящий в комплект поставки проект; при установке новой
копии этот проект должен предлагаться автоматически, но можно выбрать в меню File
команду Open Project и открыть его вручную. Он находится в пользовательской папке
общего доступа, адрес которой в операционных системах семейства Windows выглядит
1.2. Работа с Unity   23

примерно так: C:\Users\Public\Documents\Unity Projects\, а в Mac OS — примерно так: Users/


Shared/Unity/. Если заодно потребуется открыть пример сцены, дважды щелкните на
файле Car (на рис. 1.9 видно, что такие файлы в Unity обозначаются символом куба).
В диспетчере файлов, расположенном в нижней части редактора, значок этого файла
будет находиться по адресу SampleScenes/Scenes/. Вы должны получить экран, показан-
ный на рис. 1.9.

Верхнюю часть целиком занимает


Вкладки Scene и Game панель инструментов. Слева кнопки С правой стороны располагается
предназначены для просмотра для осмотра и перемещения объектов, панель, отображающая информацию
трехмерных сцен и воспроизведения а в центре располагается кнопка Play о выделенном в данный момент
игр соответственно
объекте

Панель Hierarchy отображает


текстовый список всех объектов
сцены и их взаимосвязей.
Для связывания объектов нужно
перетащить их на данную панель

Вкладки Project и Console


предназначены для просмотра
всех файлов проекта
и сообщений, касающихся кода
программы соответственно

Выберите папку справа и дважды щелкните


на значке сцены Car

Рис. 1.9. Части интерфейса Unity

Интерфейс Unity состоит из следующих частей: вкладка Scene, вкладка Game, панель
инструментов, вкладка Hierarchy, панель Inspector, вкладки Project и Console. Каждая часть
имеет собственное предназначение, но все они играют важную роль в цикле создания
игры:
 Просмотр файлов выполняется на вкладке Project.
 Объекты трехмерной сцены просматриваются на вкладке Scene.
 Панель инструментов предоставляет элементы управления сценой.
 На вкладке Hierarchy путем перетаскивания можно менять связь между объектами.
 Панель Inspector отображает сведения о выделенных объектах и показывает свя-
занный с ними код.
 Тестирование результатов осуществляется на вкладке Game, а сообщения об ошиб-
ках появляются на вкладке Console.
24    Глава 1. Знакомство с Unity

Это компоновка по умолчанию; все доступные представления помещены на вкладки,


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

1.2.1. Вкладка Scene, вкладка Game и панель инструментов


Наиболее заметная часть интерфейса — расположенная в центре вкладка Scene. Имен-
но здесь мы можем видеть, как выглядит мир игры, и перемещать объекты по сцене.
Сеточные объекты в сцены, как им и положено, представлены в виде сеток. Можно
наблюдать и ряд других объектов, представленных различными значками и цветны-
ми линиями. Это камеры, источники света, источники звука, области столкновений
и т. п. Разумеется, эта картинка отличается от того, что будет показываться в процессе
игры, — рассматривать сцену можно, не ограниваясь игровым представлением.

ОПРЕДЕЛЕНИЕ  Сеточным объектом (mesh object) называется объект, видимый в трехмерном


пространстве. Он создается из набора соединенных друг с другом линий и форм, формирующих сетку.

Игровое представление отображается на вкладке Game, расположенной рядом с вклад-


кой Scene (переход с вкладки на вкладку осуществляется в верхнем левом углу области
отображения). Интерфейс содержит и другие элементы, сконструированные подобным
образом; для изменения отображаемого ими содержимого достаточно перейти на дру-
гую вкладку. После запуска игры начинает отображаться игровое представление. При
этом переходить на вкладку Game не нужно, переключение выполняется автоматически.

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

Для запуска игры достаточно нажать кнопку Play над вкладкой Scene. Вся верхняя
часть интерфейса занята так называемой панелью инструментов (Toolbar), и кнопка Play
находится как раз в центре этой панели. На рис. 1.10 из всего интерфейса редактора
для наглядности оставлена только панель инструментов с расположенными под ней
вкладками Scene/Game.
На левой стороне панели инструментов находятся кнопки навигации и преобразо-
вания объектов. Они позволяют рассматривать сцену с разных сторон и перемещать
объекты. Попрактикуйтесь в их применении, так как основные действия, выполня-
емые в визуальном редакторе Unity, — это просмотр сцен и перемещение объектов
(они настолько важны, что им будет посвящен отдельный раздел). Правую сторону
панели инструментов занимают раскрывающиеся меню с  перечнем компоновок
и слоев. Я уже упоминал о гибкости и возможностях настройки интерфейса Unity.
Поэтому в нем и существует меню Layouts, позволяющее переходить от одного вари-
анта компоновки к другому. Меню Layers относится к расширенным функциональ-
ным возможностям, которые мы пока рассматривать не будем (о слоях пойдет речь
в следующих главах).
1.2. Работа с Unity   25

Средства отображения элементов сцены Воспроизведение


(например, кнопка отображения игры Панель инструментов
источников света)

Инстру-
мент
Rect

Инструмент
Scale
Инструмент
Rotate
Инструмент Источник света
Translate
Инструмент
Navigate Сеточный объект
scene

Рис. 1.10. Усеченный вариант редактора, демонстрирующий только панель инструментов


и вкладки Scene и Game

1.2.2. Работа с мышью и клавиатурой


Навигация в сценах осуществляется с помощью мыши и набора клавиш-модифика-
торов, влияющих на результат манипуляций с мышью. Главные операции — это пере-
мещение (move), облет (orbit) и масштабирование (zoom). Действия, необходимые
для совершения каждой из этих операций, зависят от типа мыши, и их описание вы
найдете в приложении A. Но в основном они сводятся к щелчкам и перетаскиванию
при нажатых и удерживаемых клавишах Ctrl и Alt (или Option на компьютерах Mac). По-
тренируйтесь в манипулировании объектами сцены, чтобы понять, как выполняются
перемещение, облет и масштабирование.

СОВЕТ  Хотя работать в Unity можно и с двухкнопочной мышью, рекомендую приобрести мышь
с тремя кнопками (не сомневайтесь, в Mac OS X она тоже работает).

Преобразование объектов также осуществляется посредством трех вышеупомянутых


операций. Более того, каждому типу навигации соответствует собственное преобразо-
вание: перенос (translate), поворот (rotate) и изменение размеров (scale). Рисунок 1.11
демонстрирует эти преобразования на примере куба.
Выделенный объект сцены можно двигать (или, если брать более точный термин,
переносить), вращать и указывать его размер. Если рассмотреть процесс навигации
в сцене с этой точки зрения, перемещение будет соответствовать переносу камеры, об-
лет — повороту камеры, а масштабирование — изменению размеров камеры. Переход
между этими операциями осуществляется не только кнопками панели инструментов,
но и нажатием клавиш  W, E и  R. При входе в режим преобразования у выделенного
объекта появляются цветные стрелки или окружности. Это габаритный контейнер
преобразования (transform gizmo), перетаскивание которого меняет вид объекта.
26    Глава 1. Знакомство с Unity

Перенос Поворот Масштабирование

Рис. 1.11. Три варианта преобразования: перенос, поворот и масштабирование


(более светлые линии обозначают исходное состояние объекта)

Рядом с кнопками преобразований находится кнопка инструмента Rect, позволяющая


перейти к работе с двумерной графикой и объединяющая в себе операции переноса,
поворота и масштабирования. В трехмерном пространстве за каждую из этих операций
отвечает свой инструмент, но в двумерном пространстве они объединены, так как у нас
становится меньше на одно измерение. В Unity существует также набор клавиатурных
комбинаций для ускорения выполнения различных операций. Они перечислены в при-
ложении A. А теперь посмотрим на остальные фрагменты интерфейса.

1.2.3. Вкладка Hierarchy и панель Inspector


На левой стороне экрана примостилась вкладка Hierarchy, в то время как правую заняла
панель Inspector (рис. 1.12). Вкладка Hierarchy содержит список всех объектов сцены
в виде древовидной структуры, ветки которой вложены друг в друга в соответствии
с иерархическими связями между этими объектами. По сути, эта вкладка позволяет
выделять объекты по именам, избавляя от необходимости искать в сцене нужный объ-
ект, чтобы выделить его щелчком. Иерархические связи объединяют объекты в группы.
Визуально это оформлено в виде папок и дает возможность за одну операцию пере-
местить целую группу объектов.

Рис. 1.12. Вкладка Hierarchy и панель Inspector


1.3. Подготовка к программированию в Unity   27

На панели Inspector отображаются данные выделенного объекта. Выделите любой объ-


ект и обратите внимание, как изменится вид панели Inspector. Отображаемая информа-
ция по большей части представляет собой список компонентов, причем вы можете как
добавлять к объектам новые компоненты, так и удалять существующие. Все игровые
объекты содержат по крайней мере один компонент — Transform, поэтому на панели
Inspector всегда будут отображаться хотя бы сведения о положении и ориентации вы-
деленного объекта. У многих объектов вы увидите целые списки компонентов, в том
числе связанные с этими объектами сценарии.

1.2.4. Вкладки Project и Console


Нижнюю часть экрана занимают показанные на рис. 1.13 вкладки Project и  Console.
В данном случае мы видим пример такой же организации элементов интерфейса,
как и у вкладок Scene и View, что легко позволяет переходить от одного представле-
ния к другому. На вкладке Project отображаются все ресурсы проекта (графические
фрагменты, код и т. п.). В левой части этой вкладки находится список папок проекта;
при выделении папки справа появляются находящиеся в ней файлы. По сути, это
такой же список, как и на вкладке Hierarchy, но если эта вкладка показывает перечень
объектов сцены, то на вкладке Project представлены файлы, не включенные ни в одну
конкретную сцену (в том числе и сами файлы сцен — сохраненная сцена появляется
на вкладке Project!).

Рис. 1.13. Вкладки Project и Console

СОВЕТ  Вкладка Project дублирует содержимое папки Assets на диске, но в общем случае удалять
или перемещать файлы непосредственно в этой папке не рекомендуется. Выполняйте эти операции
на вкладке Project, а о синхронизации с папкой Assets позаботится Unity.

Вкладка Console представляет собой место вывода связанных с кодом сообщений.


Иногда это намеренно вставленные в программу сообщения отладчика, иногда Unity
посылает сообщения об ошибке, обнаружив неполадку в написанном сценарии.

1.3. Подготовка к программированию в Unity


Посмотрим, как в Unity выглядит процесс программирования. Компоновка игровых
ресурсов происходит в визуальном редакторе, но для обеспечения интерактивности
игры требуется управляющий ресурсами код. В приложении Unity поддерживается
ряд языков программирования, в частности JavaScript и C#. Каждый вариант имеет
свои достоинства и недостатки, но в этой книге все примеры даны на языке C#.
28    Глава 1. Знакомство с Unity

ПОЧЕМУ C#, А НЕ JAVASCRIPT?______________________________________


Весь код в книге написан на языке C#, так как он имеет ряд преимуществ над языком JavaScript
и куда меньше недостатков, особенно с точки зрения профессионального разработчика (именно
этим языком я пользуюсь для работы).
Одним из преимуществ является строгая типизация языка C#, отсутствующая в JavaScript. Среди
опытных программистов существуют разные точки зрения по поводу того, является ли динами-
ческая проверка типов оптимальным подходом, например, к веб-разработке, но при написании
программ для определенных игровых платформ (таких, как iOS) зачастую выгодна, а порой даже
требуется статическая типизация. В Unity даже добавлена директива #pragma, принудительно
обеспечивающая статическую проверку типов в JavaScript. Технически такое вполне допустимо,
но при этом нарушается один из основных принципов функционирования JavaScript. Поэтому
лучше изначально выбрать язык со строгой типизацией.
Это всего лишь один пример того, чем отличается язык JavaScript в Unity. Во многом он напоминает
JavaScript в веб-браузерах, но в его функционировании есть ряд зависящих от контекста отличий.
Многие разработчики называют версию для Unity именем UnityScript, которое указывает на сходство,
но одновременно и на отличие от JavaScript. Именно это состояние «аналогичный, но отличающий-
ся» становится проблемой как при попытках применить общие знания языка JavaScript в контексте
Unity, так и при попытках применять на стороне знания, полученные в процессе работы в Unity.
По этой причине в Unity понемногу прекращается поддержка JavaScript/UnityScript. Вот перевод
соответствующей статьи из блога: https://devtribe.ru/p/unity/unityscripts-is-depricating. Оригинал
статьи находится по адресу http://mng.bz/B9au.

Попробуем написать и запустить код. Откройте Unity и щелкните на ссылке New


в стандартном окне. Или выберите в меню File команду New Project, если приложение
Unity уже запущено. Откроется окно диалога New Project, предназначенное для создания
нового проекта. Укажите имя проекта и место, куда бы вы хотели его сохранить. Проект
Unity представляет собой обычную папку с файлами различных ресурсов и настроек,
поэтому его можно сохранять где угодно. Щелкните на кнопке Create Project, и Unity
ненадолго исчезнет, чтобы создать папку проекта.
ВНИМАНИЕ  Проекты Unity запоминают, в какой версии программы они создавались, и при по-
пытке открыть их в другой версии появляется предупреждение. Иногда на это можно не обращать
внимания (к примеру, если такое предупреждение появится при открытии файлов с  примерами
к  данной книге, просто игнорируйте его), но бывают и  ситуации, когда перед открытием проекта
имеет смысл сделать его резервную копию.

ВНИМАНИЕ  При открытии файлов с примерами к книге может появиться и вот такое сообще-
ние: Rebuilding Library because the asset database could not be found! Оно связано с  папкой Library
открываемого проекта; эта папка содержит файлы, генерируемые Unity и используемые в процессе
работы, но редактор не считает нужным загружать их по умолчанию.

Когда Unity появится снова, вы увидите пустую сцену. Давайте посмотрим, как в Unity
происходит запуск программ.

1.3.1. Запуск кода: компоненты сценария


Для запуска кода в Unity первым делом нужно связать файлы с кодом с каким-ли-
бо объектом сцены. Это часть компонентной системы, о которой говорилось выше;
игровые объекты создаются как наборы компонентов, и в каждый такой набор может
входить исполняемый сценарий.
1.3. Подготовка к программированию в Unity   29

ПРИМЕЧАНИЕ  В терминологии Unity файлы с кодом принято называть сценариями, используя


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

По этому описанию вы, наверное, уже догадались, что сценарии в Unity представляют
собой компоненты. Но следует отметить, что в эту группу попадают не все сценарии,
а только наследующие от класса MonoBehaviour, базового класса компонентов-сцена-
риев. Этот класс определяет способ присоединения компонентов к игровым объектам.
Наследование от этого класса дает пару автоматически запускаемых методов (показан-
ных в листинге 1.1), которые вы можете переопределить. Это метод Start(), вызывае-
мый при активации объекта (она, как правило, наступает после загрузки содержащего
объект уровня), и вызываемый в каждом кадре метод Update(). Соответственно, код
будет запущен, если вставить его в эти предустановленные методы.

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

Листинг 1.1. Шаблон кода для базового компонента сценария


using UnityEngine; Добавляет пространства имен для классов Unity и Mono.
using System.Collections;
using System.Collections.Generic;

public class HelloWorld : MonoBehaviour { Синтаксис для задания наследования.


void Start() {
// однократное действие Сюда добавляется однократно выполняемый код.
}

void Update() {
// действие, выполняемое в каждом кадре Сюда добавляется код,
} запускаемый в каждом кадре.
}

Именно так выглядит файл в момент создания нового сценария на языке C#: мини-
мальный шаблонный код, определяющий корректный компонент Unity. Существует
шаблон сценария, скрытый в недрах Unity. При создании нового сценария приложение
копирует этот шаблон и переименовывает класс в соответствии с именем файла (в моем
случае это HelloWorld.cs). Добавляются и пустые оболочки методов Start() и Update(),
так как именно из этих методов чаще всего вызывается пользовательский код.
Чтобы создать сценарий, откройте меню Assets, наведите указатель мыши на строчку
Create и выберите на открывшейся дополнительной панели команду C# Script. Обрати-
те внимание, что как в меню Assets, так и в меню GameObjects есть варианты команды
30    Глава 1. Знакомство с Unity

Create, но это разные вещи. Альтернативным способом доступа к нужному нам меню
является щелчок правой кнопкой мыши в произвольной точке вкладки Project. Введите
имя нового сценария, например HelloWorld. Чуть позже, на рис. 1.15 вы увидите, что
можно перетащить файл сценария мышью на произвольный объект сцены. Дважды
щелкните на значке сценария, и он автоматически откроется в программе MonoDevelop,
о которой мы поговорим ниже.

1.3.2. Программа MonoDevelop, межплатформенная IDE


Программирование осуществляется не внутри Unity, код существует в виде отдельных
файлов, местоположение которых вы сообщаете приложению. Файлы сценариев могут
создаваться в самом приложении, но в любом случае потребуется текстовый редактор
или IDE, где для изначально пустых файлов будет писаться код. В комплекте с Unity
поставляется приложение MonoDevelop как межплатформенная интегрированная
среда разработки (IDE) для языка C# с открытым исходным кодом (оно показано на
рис. 1.14). Более подробную информацию можно получить на сайте www.monodevelop.
com, но пользоваться при этом следует версией, идущей в комплекте с Unity, а не
скачанным с этого сайта приложением, так как в базовую программу был внесен ряд
изменений для лучшей интеграции с Unity.

Файлы сценариев открываются на вкладках в основной


Не трогайте кнопку Run в программе MonoDevelop;
области просмотра. Допустимо открывать одновременно
для запуска кода пользуйтесь кнопкой Play в Unity
несколько файлов

Панель Solution
отображает все файлы
сценариев в проекте

Панель Document Outline


по умолчанию может
отсутствовать. Выберите
в меню View команду Pads
и в открывшемся списке
вариант Document Outline,
а затем перетащите панель
на удобное для вас место

Рис. 1.14. Фрагменты интерфейса программы MonoDevelop

ПРИМЕЧАНИЕ  Программа MonoDevelop объединяет файлы в группы, называемые решениями


(solution). Приложение Unity автоматически генерирует решение, содержащее все файлы сценариев,
поэтому, как правило, об этом можно вообще не думать.

Так как язык C# изначально разрабатывался компанией Microsoft, возможно, вам


интересно, можно ли для программирования в Unity пользоваться Visual Studio. Да,
можно. Для этого существуют инструменты, отвечающие за поддержку (в частности, за
1.3. Подготовка к программированию в Unity   31

корректную работу отладки и точек останова). Чтобы убедиться в наличии этих инстру-
ментов, проверьте, содержит ли меню Debug команду Attach Unity Debugger. В случае его
отсутствия запустите установщик Visual Studio, внесите изменения в установленную
версию и найдите модуль разработки игр Unity.
Я, как правило, использую программу MonoDevelop, но если вы уже привыкли про-
граммировать в Visual Studio, просто продолжайте работать с этой средой. Вы легко
сможете выполнять все практические задания из книги (после вводной главы упоми-
нать о IDE мы больше не будем). Однако привязка рабочего процесса к Windows ан-
нулирует одно из самых важных преимуществ Unity. Язык C# разработан инженерами
компании Microsoft, и поэтому изначально применялся только в Windows на платфор-
ме .NET Framework, однако он давно превратился в открытый стандарт языка и для
него появился важный межплатформенный фреймворк: Mono. В Unity фреймворк
Mono используется как основа для программирования, и именно среда MonoDevelop
обеспечивает межплатформенные возможности всего процесса разработки.

ВНИМАНИЕ  Интегрированная среда разработки MonoDevelop шла в комплекте с Unity 2017.1, но,


согласно статье в блоге Unity http://mng.bz/9HR8, начиная с версии Unity 2018.1, ситуация изменится.

Все время помните, что в программах MonoDevelop и Visual Studio код только пишется,
но не запускается. В данном случае IDE — это всего лишь хорошо оснащенный тексто-
вый редактор, а воспроизводить код следует нажатием кнопки Play в программе Unity.

1.3.3. Вывод на консоль: Hello World!


Итак, в нашем проекте появился пустой сценарий, но пока отсутствует объект, к кото-
рому этот сценарий можно было бы присоединить. Вспомните рис. 1.1, демонстриру-
ющий работу системы компонентов; любой сценарий — это тоже компонент, поэтому
его нужно добавить к какому-то объекту.
Выберите в меню GameObject команду Create Empty, и на вкладке Hierarchy появится
пустой объект с именем GameObject. Перетащите значок сценария с вкладки Project на
вкладку Hierarchy и положите на строчку GameObject. Как показано на рис. 1.15, если
присоединение сценария является допустимой операцией, компонент подсвечивается.
Если вы отпустите кнопку мыши, сценарий будет связан с объектом GameObject. Чтобы
удостовериться в успешном исходе операции, выделите объект и посмотрите на панель
Inspector. Там должны появиться два компонента: Transform — базовый компонент поло-
жения/ориентации/масштаба, присутствующий у всех объектов, который невозможно
удалить, а сразу под ним — сценарий.

ПРИМЕЧАНИЕ  В конце концов вы привыкнете перетаскивать объекты и  класть их на другие


объекты. Этим способом в Unity создается огромное количество различных связей, а не только при-
вязка сценариев к объектам.

Вид панели Inspector после связывания сценария с объектом показан на рис. 1.16.


Сценарий уже фигурирует в качестве компонента и при воспроизведении сцены нач-
нет исполняться. Но никаких внешних проявлений вы пока не заметите, так как код
сценария еще не написан. Давайте устраним этот пробел.
32    Глава 1. Знакомство с Unity

Перетащите сценарий
с вкладки Project
на вкладку Hierarchy
и опустите на объект GameObject

Рис. 1.15. Связывание сценария с объектом GameObject

Рис. 1.16. Связанный сценарий на панели Inspector

Откройте сценарий в Visual Studio, чтобы отредактировать листинг 1.1. Знакомство


с новой средой программирования всегда начинается с вывода на экран текста «Hello
World!». Поэтому добавьте в метод Start() строчку, показанную в следующем лис­тинге.

Листинг 1.2. Сообщение для вывода на консоль


void Start() {
Debug.Log("Hello World!"); Добавляем команду регистрации.
}

Команда Debug.Log() выводит сообщение на вкладку Console. Строка с этой командой


вставляется в метод Start(), потому что, как упоминалось выше, данный метод одно-
кратно вызывается после активации объекта. Другими словами, после нажатия кнопки
Play в редакторе метод Start() будет вызван всего один раз. Добавив в сценарий коман-
ду регистрации, обязательно сохраните его и нажмите кнопку Play в программе Unity.
Откройте вкладку Console. Там появится сообщение «Hello World!». Поздравляю, вы
Заключение   33

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

ЭТАПЫ НАПИСАНИЯ СЦЕНАРИЯ HELLO WORLD!_________________________


Кратко перечислим действия, выполнявшиеся при чтении последних страниц:
• Создание нового проекта.
• Создание нового сценария на языке C#.
• Создание пустого объекта GameObject.
• Перетаскивание сценария на этот объект.
• Добавление к сценарию команды регистрации.
• Нажатие кнопки Play!

Теперь сцену можно сохранить; появится файл с расширением .unity и со значком
Unity. Этот файл представляет собой снимок всего, что есть в игре в данный момент,
что позволяет в дальнейшем легко загрузить сцену в программу. В данном случае в со-
хранении нет особого смысла (у нас всего один пустой объект GameObject). Но если
вы не сохраните сцену и захотите вернуться к ней в будущем, она окажется пустой.

ОШИБКИ В СЦЕНАРИИ_____________________________________________
Чтобы посмотреть, каким образом Unity указывает на ошибки, специально сделайте опечатку
в сценарии HelloWorld. Даже лишняя скобка приведет к появлению на вкладке Console сообще-
ния об ошибке:

Содержащий ошибку Описание ошибки


сценарий Местоположение ошибки
в сценарии (строка, символ)

Заключение
 Unity — это мультиплатформенный инструмент разработки.
 Визуальный редактор Unity состоит из набора фрагментов, работающих в связке
друг с другом.
 Сценарии присоединяются к объектам в виде компонентов.
 Код сценариев пишется в программе MonoDevelop или Visual Studio.
2 Создание 3D-ролика

33 Знакомство с трехмерным координатным пространством.


33 Размещение в сцене игрока.
33 Создание сценария перемещения объектов.
33 Реализация элементов управления персонажем в игре от первого лица.

Глава 1 завершилась традиционным способом знакомства с новыми средствами


программирования — написанием программы «Hello World!». Пришло время по-
грузиться в более сложный Unity-проект, содержащий как средства взаимодействия
с пользователем, так и графику. Вы поместите в сцену набор объектов и напишете
код, позволяющий игроку перемещаться в этой сцене. По сути, это будет аналог игры
Doom без монстров (примерно такой, как на рис. 2.1). Визуальный редактор Unity по-
зволяет новым пользователям сразу приступить к сборке трехмерного прототипа без
предварительного написания шаблонного кода (для таких вещей, как инициализация
трехмерного представления или установка цикла визуализации).

Рис. 2.1. Снимок экрана трехмерной демоверсии (по сути, это Doom без монстров)
2.1. Подготовка…   35

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

ПРИМЕЧАНИЕ  Проект этого (и всех разделов) можно загрузить с  веб-сайта книги. Откройте
проект в Unity, затем откройте Scene, чтобы запустить его. Пока вы учитесь, я рекомендую вам само-
стоятельно набирать весь код и использовать загруженный образец кода для справки. Адрес веб-сайта:
www.manning.com/books/unity-in-action-second-edition.

2.1. Подготовка…
Инструмент Unity дает новичкам возможность сразу приступить к работе, но перед
тем, как заняться созданием сцены, оговорим пару аспектов. Даже при наличии такого
гибкого инструмента, как Unity, следует четко представлять себе, что именно вы хотите
получить в результате своих действий. Кроме того, нужно хорошо представлять себе,
как функционирует трехмерная система координат, в противном случае вы запутаетесь
при первой же попытке вставки в сцену объекта.

2.1.1. Планирование проекта


Перед тем как приступить к программированию, всегда нужно остановиться и ответить
на вопрос «Что я собираюсь построить?». Проектирование игр — весьма обширная тема,
которой посвящено множество толстых книг. К счастью, в данном случае для разработки
базового учебного проекта достаточно мысленно представлять структуру будущей сце-
ны. Первые проекты будут несложными, чтобы лишние детали не мешали вам изучать
принципы программирования, а задумываться о разработке более высокоуровневого
проекта можно (и нужно!) только после того, как вы освоите основы создания игр.
Вашим первым проектом станет создание сцены из шутера от первого лица — мы будем
обозначать этот тип игр аббревиатурой FPS (First-Person Shooter). Вы по­строите ком-
нату, по которой можно перемещаться, при этом игроки будут наблю­дать окружающий
мир с точки зрения игрового персонажа. Управлять этим персонажем игрок сможет
посредством мыши и клавиатуры. Все интересные, но сложные элементы готовой игры
мы пока отбросим, чтобы сконцентрироваться на основной задаче — перемещениях
в трехмерном пространстве. Рисунок 2.2 иллюстрирует сценарий проекта, по сути,
представляющий собой придуманный мной перечень действий:
1. Разработка комнаты: создание пола, внешних и внутренних стен.
2. Размещение источников света и камеры.
3. Создание объекта-игрока (в том числе и присоединение камеры к его верхней ча­
сти).
4. Написание сценариев перемещения: повороты при помощи мыши и перемещения
при помощи клавиатуры.
Пусть столь внушительный сценарий вас не пугает! Кажется, что вам предстоит
большая работа, но Unity делает ее очень простой. Разделы, посвященные сценариям
перемещения, так велики только потому, что мы подробно рассматриваем каждую
36    Глава 2. Создание 3D-ролика

1. Обустройство комнаты.
Сначала создаем пол,
затем — внешние стены, 2. Игрок должен видеть комнату.
потом — внутренние Расположите в ней несколько
стены источников света и поместите
камеру, которая станет
«глазами» игрока

3. Создайте примитивную фигуру,


которая будет представлять собой игрока.
Поместите наверх этого объекта камеру
4. Напишите сценарии перемещения таким образом, чтобы она перемещалась
игрока. Сначала напишите код вместе с объектом
поворотов с помощью мыши, затем —
код перемещений с помощью
клавиатуры
Рис. 2.2. Сценарий трехмерного демонстрационного ролика

деталь, чтобы полностью понять принципы программирования. Мы начинаем с игры


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

2.1.2. Трехмерное координатное пространство


Сформулированный нами для начала простой план включает в себя три аспекта: игро-
вое пространство, средства наблюдения, элементы управления. Для их реализации
требуется понимание того, каким образом в трехмерных симуляциях задаются поло-
жение и перемещение объектов. Те, кто раньше никогда не сталкивался с трехмерной
графикой, вполне могут этого не знать.
Все сводится к числам, указывающим положение точки в пространстве. Сопоставле-
ние этих чисел с пространством происходит через оси системы координат. На уроках
математики в школе вы, скорее всего, видели показанные на рис. 2.3 оси X и Y и даже
пользовались ими для присваивания координат различным точкам на листе бумаги.
Это так называемая прямоугольная, или декартова, система координат.
Две оси дают вам двумерные координаты. Это случай, когда все точки лежат в одной
плоскости. Трехмерное пространство задается уже тремя координатными осями. Так
как ось X располагается на странице горизонтально, а ось Y — вертикально, третья
ось должна как бы «протыкать» страницу, располагаясь перпендикулярно осям X и Y.
Рисунок 2.4 демонстрирует оси X, Y и Z для трехмерного координатного пространства.
У всех элементов, обладающих определенным положением в сцене (у игрока, у стен
и т. п.), будут координаты XYZ.
На вкладке Scene в Unity вы видите значок трех осей, а на панели Inspector можно указать
три числа, задающих положение объекта. Координаты в трехмерном простран­стве вы
2.1. Подготовка…   37

будете использовать не только при написании кода, определяющего местоположение


объектов, но и для указания смещений как значений сдвига вдоль каждой из осей.

Вертикальная ось Координаты, определяющие положение


(обычно обозначается буквой Y) точки. Числа соответствуют расстоянию
по каждой из осей: (X, Y)

(6, 5)

Горизонтальная ось
(обычно обозначается буквой X)

Рис. 2.3. Координаты по осям X и Y определяют положение точки на плоскости

Вертикальная ось
(обозначена буквой Y) (6, 7, 5)

Если двумерные координаты


состоят из двух цифр,
по одной вдоль каждой оси,
Ось Z перпендикулярна странице то трехмерные — из трех: (X, Y, Z)
и как бы «протыкает» ее насквозь

Горизонтальная ось
(обозначена буквой X)

Рис. 2.4. Координаты по осям X, Y и Z определяют точку в трехмерном пространстве

ПРАВАЯ И ЛЕВАЯ СИСТЕМЫ КООРДИНАТ______________________________


Положительное и отрицательное направления каждой оси выбираются произвольным образом,
в  обоих случаях система координат прекрасно работает. Главное, используя инструмент для
обработки трехмерной графики (инструмент анимации, инструмент разработки и т. п.), всегда
выбирать одну и ту же систему координат.
Впрочем, практически всегда ось X указывает вправо, а ось Y — вверх; разница между инстру-
ментами в основном состоит в том, что где-то ось Z выходит из страницы, а где-то входит в нее.
Эти два варианта называют «правой» и «левой» системами координат. Как показано на рисунке
на с. 38, если большой палец расположить вдоль оси X, а указательный — вдоль оси Y, средний
палец задаст направление оси Z.
38    Глава 2. Создание 3D-ролика

ось Y
Левая система
Правая система
координат
ось Z координат

ось X

У левой и правой руки ось Z ориентирована в разных направлениях

В Unity, как и во многих других приложениях для работы с компьютерной графикой, используется
левая система координат. Однако существует множество инструментов, в которых применяется
правая система координат (например, OpenGL). Помните об этом, чтобы не растеряться, стол-
кнувшись с другими направлениями координатных осей.

Теперь, когда у вас есть не только план действий, но и представление о том, как с по­
мощью координат задать положение объекта в трехмерном пространстве, приступим
к работе над сценой.

2.2. Начало проекта: размещение объектов


Итак, начнем с создания объектов и размещения их в сцене. Первыми будут статические
объекты — пол и стены. Затем выберем место для источников света и камеры. Последним
создадим игрока — это будет объект, к которому вы добавите сценарии, перемещающие
его по сцене. Рисунок 2.5 демонстрирует вид редактора после завершения работы.

Направленные и точечные
источники света в сцене

Камера располагается
поверх игрока;
расположенные
под углом белые линии
показывают
ее угол обзора

Игрок, которого будет представлять базовый объект-капсула

Рис. 2.5. Сцена в редакторе с полом, стенами, источниками света, камерой и игроком
2.2. Начало проекта: размещение объектов   39

В главе 1 демонстрировался способ создания нового проекта в Unity. Именно это мы


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

2.2.1. Декорации: пол, внешние и внутренние стены


В расположенном в верхней части экрана меню GameObject наведите указатель мыши на
строчку 3D Object, чтобы открыть дополнительное меню. Выберите в нем вариант Cube,
так как для нашей сцены требуется куб (позднее вы поработаете и с другими фигура-
ми, такими как Sphere и Capsule). Отредактируйте положение и масштаб появившегося
в сцене куба, а также его имя таким образом, чтобы получить пол; значения, которые
следует присвоить параметрам этого объекта на панели Inspector, показаны на рис. 2.6
(для превращения куба в пол его нужно растянуть).

1. Вверху вводится имя объекта.


Например, присвоим объекту,
2. Передвиньте и отмасштабируйте служащему полом, имя «Floor»
куб, чтобы создать пол.
В разной степени растянутый
по разным осям объект теряет
кубическую форму
Это остальные компоненты объекта
При этом мы его слегка опускаем
Cube, которые на данном этапе
вниз для компенсации высоты,
следует оставить без изменений.
но объект остается центрированным
В эту группу попадают
компоненты: Mesh Filter
(определяющий геометрию
объекта), Mesh Renderer
(определяющий материал
объекта) и Box Collider
(позволяющий объекту принимать
участие в столкновениях
во время движения)

Рис. 2.6. Панель Inspector для пола

ПРИМЕЧАНИЕ  Единицы измерения положения могут выбираться произвольным образом;


главное, чтобы они использовались для всех элементов сцены. Чаще всего в качестве единицы из­
мерения фигурирует метр. Сам я  предпочитаю пользоваться именно метрами, но бывают случаи,
когда я выбираю футы. Хотя мне доводилось видеть людей, которые считали, что размеры в сцене
измеряются в дюймах!

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


Можно каждый раз задействовать новый куб, а можно копировать и вставлять суще-
ствующий объект, указывая стандартные сокращения. Двигайте, поворачивайте
40    Глава 2. Создание 3D-ролика

и масштабируйте стены, чтобы получить показанный на рис. 2.5 периметр. Экспери-


ментируйте с различными значениями (например, 1, 4, 50 для полей Scale) или вос-
пользуйтесь инструментами преобразований, с которыми вы познакомились в раз-
деле 1.2.2 (напоминаю, что перемещения и  повороты в трехмерном пространстве
называют преобразованиями).

СОВЕТ  Не забудьте про средства навигации, позволяющие рассматривать сцену под разными угла-
ми и менять ее масштаб, например, имитируя взгляд с высоты птичьего полета. При этом нажатие
клавиши F вернет вас к просмотру выделенного в данный момент объекта.

Точные значения преобразований для стен будут зависеть от


того, каким образом вы повернете и отмасштабируете исход-
ные объекты Cube, подогнав их размеры и положение, а также
от способа их связывания на вкладке Hierarchy. К примеру, на
рис. 2.7 демонстрируется ситуация, когда все стены явля-
ются потомками пустого корневого объекта. Содержимое
вкладки Hierarchy в этом случае имеет упорядоченный вид.
Если вы предпочитаете просто скопировать рабочие значе-
ния, скачайте пример проекта и возьмите все данные оттуда.
Рис. 2.7. Панель Hierarchy,
показывающая, что стены
и пол являются потомками
СОВЕТ  Связи между объектами устанавливаются простым пере­
таскиванием объектов друг на друга на вкладке Hierarchy. Объект, к ко-
пустого объекта
торому присоединены другие объекты, называется предком (parent);
объекты, присоединенные к другим объектам, называются потомками
(children). Перемещение (поворот или масштабирование) родительского объекта сопровождается
аналогичным преобразова­нием всех его потомков.

СОВЕТ  Для систематизации объектов сцены подобным образом применяются пустые игровые объ-
екты. Связывание видимых объектов с корневым позволяет сворачивать списки объектов на вкладке
Hierarchy. Но помните, что перед этой операцией следует расположить пустой корневой объект в точке
с координатами 0, 0, 0, чтобы в дальнейшем избежать проблем с позиционированием.

ЧТО ТАКОЕ GAMEOBJECT?_ _________________________________________


Все объекты сцены представляют собой экземпляры класса GameObject аналогично тому, как
все компоненты сценариев наследуют от класса MonoBehaviour. Этот факт становится более
наглядным, если пустому объекту присвоить имя GameObject. Впрочем, даже если этот объект
будет называться Floor, Camera или Player, суть дела не изменится.
На самом деле GameObject представляет собой всего лишь контейнер для набора компонентов.
Его основным назначением является обеспечение некоего объекта, к  которому можно присо-
единять класс MonoBehaviour. Как все это будет выглядеть в сцене, зависит от добавленных
к объекту GameObject компонентов. К примеру, куб получается добавлением компонента Cube,
сфера — добавлением компонента Sphere, и т. п.

Завершив работу над внешними стенами, приступайте к созданию внутренних. Рас-


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

2.2.2. Источники света и камеры


Как правило, трехмерные сцены освещаются направленным источником света, к кото-
рому добавляется набор точечных осветителей. Начать имеет смысл с направленного
источника. Он может по умолчанию присутствовать в сцене, но если его нет, наведите
указатель мыши на строку Light в меню GameObject и в открывшемся дополнительном
меню выберите вариант Directional Light.

ТИПЫ ОСВЕТИТЕЛЕЙ_ _____________________________________________


Существуют различные типы осветителей, разными способами проецирующие световые лучи. Три
основных типа: точечный источник, прожектор и направленный источник.
Все лучи точечного источника (point light) начинаются в одной точке и распространяются во всех
направлениях. В реальном мире таким осветителем является лампочка. Яркость света увеличи-
вается по мере приближения к источнику за счет концентрации лучей.
Лучи прожектора (spot light) также исходят из одной точки, но распространяются в  пределах
ограниченного конуса. В текущем проекте мы с прожекторами работать не будем, но осветители
данного типа повсеместно используются для подсвечивания отдельных частей уровня.
Лучи направленного источника света (directional light) распространяются равномерно и парал-
лельно друг другу, одинаково освещая все элементы сцены. Это аналог солнца.

Испускаемый направленным осветителем свет не зависит от местоположения источ-


ника, значение имеет только его ориентация, поэтому его можно поместить в произ-
вольную точку сцены. Я рекомендую установить его над комнатой, чтобы он выглядел
как солнце и не мешал вам при работе с остальными фрагментами сцены. Поверните
источник света и посмотрите, как это повлияет на освещенность комнаты; для полу-
чения нужного эффекта я рекомендую слегка повернуть его относительно осей X и Y.
На панели Inspector вы найдете параметр Intensity (рис. 2.8). Как следует из его названия,
он управляет яркостью света. Если бы данный направленный осветитель был в сцене
единственным, его яркость имело бы смысл увеличить, но, так как мы добавим несколь-
ко точечных источников света, его можно оставить тусклым. Например, присвойте
параметру Intensity значение 0.6.

Остальные настройки мы пока трогать


не будем. К ним относятся цвет света, Здесь указывается
отбрасываемые тени и даже яркость осветителя,
проецирование силуэта (вспомните значение 0 соответ-
прожектор с нарисованной летучей ствует полной
мышью из фильма «Бэтмен») темноте

Рис. 2.8. Настройки направленного источника света на панели Inspector

Теперь командой уже знакомого вам меню создайте несколько точечных осветителей,
поместив их в темные места комнаты. Вам нужно, чтобы все было как следует освещено.
Источников должно быть не слишком много (иначе пострадает производительность),
вполне достаточно по одному осветителю в каждом углу (я предлагаю поднять их
к верхней кромке стен) и одного, расположенного высоко над сценой (например, со
значением 18 координаты Y). Это придаст освещению комнаты некое разнообразие.
42    Глава 2. Создание 3D-ролика

Обратите внимание, что у точечных источников света на панели Inspector появляется


дополнительный параметр Range (рис. 2.9). Он управляет дальностью распространения
лучей. Если направленный источник света равномерно освещает всю сцену, то яркость
точечного источника уменьшается по мере удаления от него. Поэтому для стоящего на
полу источника этот параметр может быть равен 18, а для поднятого к верхней кромке
стены его необходимо увеличить до 40, иначе он не сможет осветить всю комнату.

Здесь указывается диапазон


действия световых лучей
За исключением в тех же единицах,
параметра Range все что и преобразования
остальные настройки перемещения и масштабирования.
точечного источника (Ошибку «realtime not supported»,
света такие же, если таковая появится, можно
как у направленного проигнорировать или выбрать
в списке Baking вариант Mixed)

Рис. 2.9. Настройки точечного источника света на панели Inspector

Чтобы игрок мог наблюдать за происходящим, требуется еще один тип объекта —
камера. Впрочем, пустая сцена содержит основную камеру, которой мы можем вос­
пользоваться. Если вам когда-нибудь потребуется создать дополнительные камеры
(например, для разделения экрана в многопользовательских играх), выберите команду
Camera в меню GameObject. Нашу же камеру мы расположим поверх игрока, чтобы на-
блюдать сцену его глазами.

2.2.3. Коллайдер и точка наблюдения игрока


В этом проекте игрока будет представлять обычный примитив. В  меню GameObject
(напоминаю, что для открытия этого дополнительного меню нужно навести указатель
мыши на строку 3D Object) выберите вариант Capsule. Появится цилиндрическая фигура
со скругленными концами — это и есть наш игрок. Сместите объект вверх, сделав его
координату Y равной 1.1 (половина высоты объекта плюс еще немного, чтобы избежать
перекрывания с полом). Теперь наш игрок может произвольным образом перемещать-
ся вдоль осей X и Z при условии, что он остается внутри комнаты и не касается стен.
Присвойте объекту имя Player.
На панели Inspector вы увидите, что этому объекту назначен капсульный коллайдер.
Это очевидный вариант для объекта Capsule, точно так же, как объект Cube по умолча-
нию обладает коллайдером Box. Но так как наша капсула будет представлять игрока,
ее компоненты должны слегка отличаться от компонентов большинства объектов.
Капсульный коллайдер мы удалим. Для этого нужно щелкнуть на значке с изобра-
жением шестерни справа от имени компонента, как показано на рис. 2.10. Откроется
меню, в котором в числе прочих вы найдете и команду Remove Component. Коллайдер
выглядит как окружающая объект зеленая сетка, поэтому после удаления компонен­та
вы обнаружите, что она исчезла.
Вместо капсульного коллайдера мы назначим объекту контроллер персонажа. В ниж-
ней части панели Inspector вы найдете кнопку Add Component. Щелчок на ней открывает
меню с перечнем типов доступных компонентов. В разделе Physics и находится нужная
2.3. Двигаем объекты: сценарий, активирующий преобразования   43

Щелчок на этом значке


открывает меню
с командой Remove Component

Рис. 2.10. Удаление компонента на панели Inspector

нам строка Character Controller. Как несложно догадаться, именно этот компонент по-
зволит объекту вести себя как персонаж.
Для завершения настройки игрока осталось сделать всего один шаг — присоединить
к нему камеру. Как уже упоминалось в разделе, посвященном созданию пола и стен, вы
можете перетаскивать объекты и размещать их друг на друге на вкладке Hierarchy. Про-
делайте эту операцию, перетащив камеру на капсулу, чтобы присоединить ее к игроку.
Затем расположите ее таким образом, чтобы она соответствовала глазам игрока (я пред-
лагаю указать координаты 0, 0.5, 0). При необходимости верните координатам преоб-
разования поворота значения 0, 0, 0 (если вы вращали капсулу, они будут различаться).
Итак, в сцене присутствуют все необходимые объекты. Осталось написать код пере-
мещения игрока.

2.3. Двигаем объекты: сценарий, активирующий


преобразования
Чтобы заставить игрока перемещаться по сцене, нам потребуются сценарии движения,
которые будут присоединены к игроку. Напоминаю, что компонентами называются
модульные фрагменты функциональности, добавляемые к объектам, поэтому сцена-
рии тоже можно считать своего рода компонентами. Именно они будут реагировать
на клавиатурный ввод и манипуляции мышью, но для начала мы заставим игрока
поворачиваться на месте. Это продемонстрирует вам, как применять преобразования
в коде. Надеюсь, вы помните, что преобразования бывают трех видов — перемещение,
поворот и масштабирование; вращение объекта на месте соответствует преобразованию
поворота. Но вы должны знать об этой задаче еще кое-что кроме того, что она «сводится
к изменению ориентации объекта».

2.3.1. Схема программирования движения


Анимация объекта (например, его вращение) сводится к смещению его в каждом кадре
на небольшое расстояние и к последующему многократному воспроизведению всех
кадров. Само по себе преобразование происходит мгновенно, в отличие от движения,
растянутого во времени. Но последовательное применение набора преобразований
вызывает визуальное перемещение объекта, совсем как набор рисунков в кинеографе.
Этот принцип проиллюстрирован на рис. 2.11.
44    Глава 2. Создание 3D-ролика

Кадр 1 Кадр 2 Кадр 3 Кадр 4

Поворот куба Поворот куба Поворот куба


на 15° на 15° на 15°

Рис. 2.11. Возникновение движения: циклический процесс преобразования


статичных изображений

Напомню вам, что компоненты сценария снабжены запускающимся в каждом кадре


методом Update(). Поэтому, чтобы заставить куб вращаться вокруг своей оси, следует
добавить в метод Update() код, поворачивающий его на небольшой угол. Этот код будет
запускаться в каждом кадре снова и снова. Не правда ли, все очень просто?

2.3.2. Написание кода


Применим рассмотренную концепцию на практике. Создайте новый сценарий C#
(напоминаю, что команда находится в дополнительном меню Create, доступ к кото-
рому осуществляется через меню Assets), присвойте ему имя Spin и напишите код из
следующего листинга (не забудьте после ввода листинга сохранить файл!).

Листинг 2.1. Приводим объект во вращение


using UnityEngine;
using System.Collections;

public class Spin : MonoBehaviour { Объявление общедоступной


переменной для скорости вращения.
public float speed = 3.0f;

void Update() {
transform.Rotate(0, speed, 0); Поместите сюда команду Rotate, чтобы
} она запускалась в каждом кадре.
}

Для добавления компонента сценария к игроку перетащите сценарий с вкладки Project


на строку Player на вкладке Hierarchy. Щелкните на кнопке Play, и вы увидите, как все
придет во вращение — вы написали код, заставляющий объект двигаться! В основном
этот код состоит из шаблона нового сценария, к которому добавлены две строки. По-
смотрим, что именно они делают.
Прежде всего, появилась переменная для скорости, добавленная в верхнюю часть
определения класса. Превращение скорости вращения в переменную произошло по
2.3. Двигаем объекты: сценарий, активирующий преобразования   45

двум причинам. Первая — это соблюдение стандартного правила программирования:


«никаких магических чисел». Вторая же причина связана со способом отображения
общедоступных переменных в Unity. Познакомимся с ним.

СОВЕТ  Общедоступные переменные появляются на панели Inspector, что позволяет редактировать


значения уже добавленных к игровому объекту компонентов. Это называется «сериализацией» зна­
чения, так как Unity сохраняет модифицированное состояние переменной.

Рисунок 2.12 показывает вид компонента сценария на панели Inspector. Можно ввести
новое значение, и сценарий будет использовать его вместо указанного в коде. Таким
способом удобно редактировать параметры компонентов различных объектов непо-
средственно в визуальном редакторе, вместо того чтобы заниматься правкой кода.

Рис. 2.12. На панели Inspector присутствует объявленная в сценарии


общедоступная переменная

Вторая строка, на которую следует обратить внимание в  листинге 2.1, касается


метода Rotate(). Он вставлен в метод Update(), а значит, выполняется в каждом
кадре. Так как метод Rotate() принадлежит классу Transform, он вызывается при
помощи точечной нотации через компонент преобразования объекта (как и в боль-
шинстве объектно-ориентированных языков, если вы введете transform, это будет
восприниматься как this.transform). В рассматриваемом случае преобразование
сводится к повороту на speed градусов в каждом кадре, что в итоге даст нам плавное
вращение. Но почему параметры метода Rotate() указаны как (0, speed, 0), а не,
допустим, (speed, 0, 0)?
Вспомните, что в трехмерном пространстве есть три оси: X, Y и Z. Интуитивно понятно,
каким образом они связаны с местоположением объекта и его перемещениями, но,
кроме того, их применяют для описания преобразования поворота. Сходным образом
описывают вращение в воздухоплавании, поэтому работающие с трехмер­ной графикой
программисты зачастую пользуются терминами из механики полета: тангаж (pitch),
рысканье (yaw) и крен (roll). Значение этих терминов иллюстрирует рис. 2.13: тангаж
означает вращение вокруг оси X, рысканье — вращение вокруг оси Y, крен — вращение
вокруг оси Z.
Возможность описывать вращение вокруг осей X, Y и Z означает три параметра для
метода Rotate(). Так как нам требуется только вращение вокруг своей оси без накло-
нов вперед и назад, меняться должна только координата Y, координатам же X и Z мы
просто присвоим значение 0. Надеюсь, вы уже поняли, что получится, если изменить
параметры на (speed, 0, 0) и запустить воспроизведение сцены; попробуйте реали-
зовать это на практике!
Существует еще одна тонкость, связанная с вращением и осями координат, реализо­
ванная в виде необязательного четвертого параметра метода Rotate().
46    Глава 2. Создание 3D-ролика

Крен
Тангаж

Рысканье

Рис. 2.13. Тангаж, рысканье и крен самолета

2.3.3. Локальные и глобальные координаты


По умолчанию метод Rotate() работает с так называемыми локальными координатами,
хотя вы также можете использовать глобальную систему координат. Вы сообщаете
методу, какой именно системой координат хотите воспользоваться, при помощи чет-
вертого, необязательного, параметра, который может принимать два значения: Space.
Self и Space.World. Например:
Rotate(0, speed, 0, Space.World)

Вернитесь к описанию трехмерного координатного пространства и хорошенько поду-


майте над следующими вопросами: где находится точка (0, 0, 0)? как выбирается по-
ложительное направление оси X? может ли перемещаться сама координатная система?
Оказывается, у каждого объекта есть собственная точка начала координат и собствен-
ные положительные направления осей. И эта координатная система перемещается
вместе с объектом. В таких случаях говорят о локальных координатах. При этом трех-
мерная сцена как целое обладает собственной точкой начала координат и собствен-
ными направлениями осей, причем такая координатная система всегда статична. Это
так называемые глобальные координаты. Соответственно, указывая вариант системы
координат для метода Rotate(), вы сообщаете, относительно каких осей X, Y и Z нужно
выполнить преобразование поворота (рис. 2.14).
Новичкам в области трехмерной графики эта концепция может показаться сложной
для понимания. Различные оси показаны на рис. 2.14 (обратите внимание, что, к при-
меру, понятие «лево» для самолета отличается от понятия «лево» для сцены в целом),
но разницу между локальными и глобальными координатами проще всего понять на
примере.
Для начала выделите игрока и слегка его наклоните (например, повернув относи-
тельно оси X на 30°). Это приведет к отключению используемых по умолчанию ло-
кальных координат, и повороты относительно глобальных и локальных координат
2.4. Компонент сценария для осмотра сцены: MouseLook   47

Оси глобальной системы


координат

Оси локальной системы координат

Обратите внимание, что эти оси


выровнены относительно
наклонившегося объекта,
но при этом выравнивание
относительно осей глобальной
системы координат отсутствует

Рис. 2.14. Оси локальной и глобальной систем координат

начнут выглядеть по-разному. Теперь попробуйте запустить сценарий Spin как с добав-
ленным параметром Space.World, так и без него; если вам сложно понять, что именно
происходит, удалите назначенный игроку компонент вращения и начните вращать
расположенный перед игроком наклоненный куб. Вы увидите, что оси вращения за-
висят от выбранной системы координат.

2.4. Компонент сценария для осмотра сцены:


MouseLook
Теперь нужно заставить преобразование поворота реагировать на ввод с помощью
мыши. В данном случае подразумевается вращение объекта, к которому присоединен
сценарий, то есть игрока. Задача будет решаться в несколько этапов путем постепенного
добавления персонажу двигательных возможностей. Сначала мы заставим игрока по-
ворачиваться только из стороны в сторону, а затем — только вверх и вниз. В конечном
счете игрок научится смотреть во всех направлениях (поворачиваясь одновременно
в горизонтальной и вертикальной плоскостях). Такое поведение называют слежением
за мышью (mouse-look).
Так как у нас предполагается три типа поведения при повороте (по горизонтали, по
вертикали и комбинированный), начнем мы с написания фреймворка, поддержива-
ющего все эти типы. Создайте новый сценарий на C# с именем MouseLook и добавьте
в него код из следующего листинга.

Листинг 2.2. Фреймворк MouseLook с перечислением для преобразования поворота


using UnityEngine; Объявляем структуру данных enum, которая
using System.Collections; будет сопоставлять имена с параметрами.

public class MouseLook : MonoBehaviour {


public enum RotationAxes {
MouseXAndY = 0, Объявляем обще-
MouseX = 1, доступную переменную,
MouseY = 2 которая появится
} в редакторе Unity.
public RotationAxes axes = RotationAxes.MouseXAndY;
using UnityEngine; Объявляем структуру данных enum, которая
using System.Collections; будет сопоставлять имена с параметрами.
48    Глава 2. Создание 3D-ролика
public class MouseLook : MonoBehaviour {
public enum RotationAxes {
MouseXAndY = 0, Объявляем обще-
MouseX = 1, доступную переменную,
MouseY = 2 которая появится
} в редакторе Unity.
public RotationAxes axes = RotationAxes.MouseXAndY;

Сюда поместим код


void Update() { для вращения только
if (axes == RotationAxes.MouseX) { по горизонтали.
// это поворот в горизонтальной плоскости
} Сюда поместим код
else if (axes == RotationAxes.MouseY) { для вращения только
// это поворот в вертикальной плоскости по вертикали.
}
Сюда поместим код для комбинированного
else {
вращения.
// это комбинированный поворот
}
}
}

Обратите внимание, что именно перечисление позволяет сценарию MouseLook выбирать,


каким образом — в горизонтальной или вертикальной плоскости — будет выполняться
поворот. Определив структуру данных enum, вы получите возможность за­давать значе-
ния по имени, вместо того чтобы указывать числа и пытаться запомнить, что означает
каждое из них. (Означает ли ноль вращение по горизонтали? Или такому повороту
соответствует единица?) Если затем объявить общедоступную переменную, типизи-
рованную как перечисление, она появится на панели Inspector в виде раскрывающегося
меню, удобного для выбора параметров (рис. 2.15).

Рис. 2.15. Панель Inspector отображает сопоставленные перечислениям общедоступные переменные


в виде раскрывающихся меню

Удалите компонент Spin (тем же способом, каким ранее удалялся капсульный кол-
лайдер) и вместо него присоедините к игроку новый сценарий. В процессе работы над
кодом для перехода от одной ветки кода к другой пользуйтесь меню Axes. По очереди
выбирая горизонтальное/вертикальное вращение, вы сможете написать код для каждой
ветки условной конструкции.

2.4.1. Горизонтальное вращение, следящее за указателем мыши


Первая и наиболее простая ветка соответствует вращению в горизонтальной плоскости.
Для начала напишем уже знакомую вам команду из листинга 2.1, заставляющую объ-
ект поворачиваться. Не забудьте объявить общедоступную переменную для скорости
вращения; сделайте это после объявления осей, но до метода Update(), назвав новую
переменную sensitivityHor, так как слово «speed» имеет слишком общий смысл
и не позволит отличать варианты поворота друг от друга. Увеличьте значение этой
2.4. Компонент сценария для осмотра сцены: MouseLook   49

переменной до 9, так как совсем скоро она начнет масштабироваться. Исправленный


код приведен в следующем листинге.
Листинг 2.3. Поворот по горизонтали, пока не связанный с движениями указателя
Курсивом выделен код, который
уже присутствует в сценарии;
...
здесь он приведен для удобства.
public RotationAxes axes = RotationAxes.MouseXAndY;

public float sensitivityHor = 9.0f; Объявляем переменную для скорости вращения.

void Update() { Сюда мы поместим команду


if (axes == RotationAxes.MouseX) { Rotate, чтобы она запускалась
transform.Rotate(0, sensitivityHor, 0); в каждом кадре.
}
...

Если запустить сценарий, сцена, как и раньше, начнет вращаться, но намного быстрее,
потому что теперь скорость вращения вокруг оси Y равна 9, а не 3. Теперь нам нужно
сделать так, чтобы преобразование возникало в ответ на движения указателя мыши,
поэтому создадим новый метод: Input.GetAxis(). Класс Input обладает множеством
методов для обработки информации, поступающей с устройств ввода (таких, как
мышь). В частности, метод GetAxis() возвращает числа, связанные с движениями
указателя мыши (положительные или отрицательные в зависимости от направления
движения). Он принимает в качестве параметра имя нужной оси. А горизонтальная
ось у нас называется Mouse X.
Если умножить скорость вращения на координату оси, объект начнет поворачиваться
вслед за указателем мыши. Скорость масштабируется в соответствии с перемещениями
указателя, уменьшаясь до нуля и даже меняя направление. Новый вид команды Rotate
показан в следующем листинге.
Листинг 2.4. Команда Rotate, реагирующая на движения указателя мыши
...
transform.Rotate(0, Input.GetAxis("Mouse X") * sensitivityHor, 0);
... Метод GetAxis() получает данные, вводимые с помощью мыши.

Щелкните на кнопке Play и подвигайте мышь в разные стороны. Объект начнет пово-
рачиваться вправо и влево вслед за указателем. Видите, как здорово! Теперь нужно
заставить игрока вращаться еще и в вертикальной плоскости.

2.4.2. Поворот по вертикали с ограничениями


Вращение в горизонтальной плоскости было реализовано у нас с помощью метода
Rotate(), но для поворота по вертикали мы воспользуемся другим способом. Дело
в том, что указанный метод при всем его удобстве в осуществлении преобразований
недостаточно гибок. Он применим только для неограниченного приращения угла пово-
рота, что в нашей ситуации подходит только для вращения в горизонтальной плоскости.
В случае же поворота по вертикали требуется задать предел наклона вниз и вверх. Код
этого преобразования для сценария MouseLook представлен в следующем листинге.
50    Глава 2. Создание 3D-ролика

Листинг 2.5. Поворот в вертикальной плоскости для сценария MouseLook


... Объявляем переменные, задающие
public float sensitivityHor = 9.0f; поворот в вертикальной плоскости.
public float sensitivityVert = 9.0f;
Объявляем закрытую
public float minimumVert = -45.0f;
переменную для угла
public float maximumVert = 45.0f;
поворота по вертикали.
private float _rotationX = 0; Увеличиваем угол поворота
по вертикали в соответствии
void Update() { с перемещениями указателя
мыши.
if (axes == RotationAxes.MouseX) {
transform.Rotate(0, Input.GetAxis("Mouse X") * sensitivityHor, 0);
}
else if (axes == RotationAxes.MouseY) {
_rotationX -= Input.GetAxis("Mouse Y") * sensitivityVert;
_rotationX = Mathf.Clamp(_rotationX, minimumVert, maximumVert);
float rotationY = transform.localEulerAngles.y;

transform.localEulerAngles = new Vector3(_rotationX, rotationY, 0);


} Создаем новый вектор
... из сохраненных значений поворота.

Сохраняем одинаковый угол поворота Фиксируем угол поворота по вертикали


вокруг оси Y (то есть вращение в диапазоне, заданном минимальным
в горизонтальной плоскости отсутствует). и максимальным значениями.

Выбираем в меню Axes компонента MouseLook вращение по вертикали и воспроизводим


новый сценарий. Теперь сцена вместо поворотов из стороны в сторону будет вслед
за движениями указателя мыши наклоняться вверх и вниз. При этом углы поворота
ограничиваются указанными вами пределами.
Этот код знакомит вас с рядом новых понятий, которые следует объяснить более под-
робно. Во-первых, теперь мы не пользуемся методом Rotate(), поэтому нам требуется
еще одна переменная (она называется _rotationX, так как вертикальное вращение
происходит вокруг оси X), предназначенная для сохранения угла поворота. Метод
Rotate() просто увеличивает угол поворота, в то время как на этот раз мы его задаем
в явном виде. Другими словами, если в предыдущем листинге мы просили «добавить
5 к текущему значению угла», то теперь мы «присваиваем углу поворота значение 30».
Разумеется, нам и в этом случае нужно увеличивать угол поворота, именно поэтому
в коде присутствует оператор -=. То есть мы вычитаем значение из угла поворота,
а не присваиваем это значение углу. Так как методом Rotate() мы не пользуемся,
манипулировать углом поворота можно разными способами, а не только увеличивая
его. Этот параметр умножается на Input.GetAxis(), совсем как в коде для вращения
в горизонтальной плоскости, просто на этот раз мы выясняем значение Mouse Y, ведь
нас интересует вертикальная ось.
Следующая строка также посвящена манипуляциям углом поворота. Метод Mathf.
Clamp() позволяет ограничить этот параметр максимальным и  минимальным
значе­ниями. Эти предельные значения задаются объявленными общедоступными
2.4. Компонент сценария для осмотра сцены: MouseLook   51

переменными, которые гарантируют поворот сцены вверх и вниз всего на 45 градусов.


Метод Clamp() работает не только с преобразованиями поворота. Он, в принципе,
используется для сохранения числового значения переменной в заданных пределах.
В экспериментальных целях превратите строку с методом Clamp() в комментарий; вы
увидите, как объект начнет свободно вращаться в вертикальной плоскости! Понятно,
что нам совсем не обязательно смотреть на сцену, перевернутую с ног на голову; от-
сюда и появляются ограничения.
Так как угловое свойство преобразования выражается переменной Vector3, мы должны
создать новую переменную с углом поворота, которая будет передаваться в конструк-
тор. Метод Rotate() этот процесс автоматизировал, увеличив угол поворота и затем
создав новый вектор.

ОПРЕДЕЛЕНИЕ  Вектором (vector) называется набор чисел, сохраняемых как единое целое.
К примеру, Vector3 состоит из трех чисел (помеченных как x, y, z).

ВНИМАНИЕ  Мы создаем новый вектор Vector3, вместо того чтобы поменять значения у суще­
ствующего, так как в случае преобразований эти значения предназначены только для чтения. Это
крайне распространенная ошибка.

УГЛЫ ЭЙЛЕРА И КВАТЕРНИОНЫ_ ____________________________________


Возможно, вам интересно, почему свойство называется localEulerAngles, а не localRotation.
Для ответа на этот вопрос вам нужно познакомиться с концепцией, называемой кватернионами
(quaternions).
Кватернионы представляют собой математическую конструкцию для представления вращения
объектов. Они отличаются от углов Эйлера, то есть используемого нами подхода с осями X, Y,
Z. Помните обсуждение тангажа, рысканья и крена? В этом случае вращения были представлены
с помощью углов Эйлера. Кватернионы… совсем другие. Объяснить их природу сложно, так как
для этого нужно углубиться в запутанные дебри высшей математики, включающие в себя дви-
жение через четыре измерения. Если вам требуется детальное объяснение, попробуйте прочесть
документ http://wat.gamedev.ru/articles/quaternions.
Несколько проще объяснить, почему кватернионы используются для представления вращений: они
более равномерно реализуют интерполяцию между углами поворота (то есть переход от одного
промежуточного значения к другому).
Свойство localRotation выражено через кватернионы, а не через углы Эйлера. При этом в Unity
существует и свойство, выражаемое через углы Эйлера, благодаря которому понять, как именно
происходит процесс поворота, становится проще; преобразование одного варианта значений
в другой и обратно делается автоматически. Инструмент Unity выполняет все сложные вычисления
в фоновом режиме, избавляя вас от необходимости проделывать их вручную.

Осталось написать код для последнего вида вращения, происходящего одновременно


в горизонтальной и вертикальной плоскостях.

2.4.3. Одновременные горизонтальное и вертикальное вращения


В последнем фрагменте кода метод Rotate() также не применяется, так как нам снова
требуется ограничить диапазон углов поворота по вертикали. Это означает, что гори-
зонтальный поворот тоже нужно вычислять вручную. Напоминаю, что метод Rotate()
автоматизировал процесс приращения угла поворота.
52    Глава 2. Создание 3D-ролика

Листинг 2.6. Сценарий MouseLook, поворачивающий объект одновременно


по горизонтали и по вертикали
... Значение delta — это величина
else { изменения угла поворота.
_rotationX -= Input.GetAxis("Mouse Y") * sensitivityVert;
_rotationX = Mathf.Clamp(_rotationX, minimumVert, maximumVert);

float delta = Input.GetAxis("Mouse X") * sensitivityHor;


float rotationY = transform.localEulerAngles.y + delta;

transform.localEulerAngles = new Vector3(_rotationX, rotationY, 0);


} Приращение угла поворота
... через значение delta.

Первая пара строк, посвященная переменной _rotationX, полностью аналогична таким


же строкам из предыдущего сценария. Просто помните, что вращение в верти­кальной
плоскости происходит вокруг оси X объекта. А так как вращение в горизонтальной
плоскости больше не реализуется через метод Rotate(), появились строки с перемен-
ными delta и rotationY. Дельта — это распространенное математическое обозначение
«величины изменения», поэтому наше вычисление переменной delta дает величину, на
которую следует поменять угол поворота. Затем она добавляется к текущему значению
этого угла для придания объекту желаемой ориентации.
В конце оба угла наклона — по вертикали и по горизонтали — используются для созда­
ния нового вектора, который назначается угловому свойству компонента Transform.

ОТКЛЮЧАЕМ ВЛИЯНИЕ СРЕДЫ НА ИГРОКА_ ___________________________


Хотя для данного проекта это не имеет особого значения, в большинстве современных игр от
первого лица на все элементы сцены действует модель физической среды. Она заставляет объ-
екты отскакивать и опрокидываться; такое поведение подходит для большинства объектов, но
вращение нашего игрока должно контролироваться исключительно мышью, не попадая под
влияние смоделированной физической среды.
По этой причине в сценариях, где присутствует ввод с помощью мыши, к компоненту Rigidbody
игрока обычно добавляют свойство freezeRotation. Поместите в  сценарий MouseLook вот
такой метод Start():
...
void Start() {
Rigidbody body = GetComponent<Rigidbody>();
if (body != null) Проверяем, существует ли этот компонент.
body.freezeRotation = true;
}
...
Rigidbody — это дополнительный компонент, которым может обладать объект. Моделируе-
мая физическая среда влияет на этот компонент и управляет объектами, к которым он присо-
единен.

На случай, если вы запутались и не знаете, куда вставить очередной фрагмент кода,
привожу полный текст сценария. Кроме того, вы можете скачать пример проекта.
2.4. Компонент сценария для осмотра сцены: MouseLook   53

Листинг 2.7. Окончательный вид сценария MouseLook


using UnityEngine;
using System.Collections;

public class MouseLook : MonoBehaviour {


public enum RotationAxes {
MouseXAndY = 0,
MouseX = 1,
MouseY = 2
}
public RotationAxes axes = RotationAxes.MouseXAndY;

public float sensitivityHor = 9.0f;


public float sensitivityVert = 9.0f;

public float minimumVert = -45.0f;


public float maximumVert = 45.0f;

private float _rotationX = 0;

void Start() {
Rigidbody body = GetComponent<Rigidbody>();
if (body != null)
body.freezeRotation = true;
}

void Update() {
if (axes == RotationAxes.MouseX) {
transform.Rotate(0, Input.GetAxis("Mouse X") * sensitivityHor, 0);
}
else if (axes == RotationAxes.MouseY) {
_rotationX -= Input.GetAxis("Mouse Y") * sensitivityVert;
_rotationX = Mathf.Clamp(_rotationX, minimumVert, maximumVert);

float rotationY = transform.localEulerAngles.y;

transform.localEulerAngles = new Vector3(_rotationX, rotationY, 0);


}
else {
_rotationX -= Input.GetAxis("Mouse Y") * sensitivityVert;
_rotationX = Mathf.Clamp(_rotationX, minimumVert, maximumVert);

float delta = Input.GetAxis("Mouse X") * sensitivityHor;


float rotationY = transform.localEulerAngles.y + delta;

transform.localEulerAngles = new Vector3(_rotationX, rotationY, 0);


}
}
}

После запуска этого сценария вы получите возможность смотреть во всех направлени-


ях, просто перемещая указатель мыши. Великолепно! Но вы все еще стоите на месте,
как орудийная башня. В следующем разделе игрок начнет перемещаться по сцене.
54    Глава 2. Создание 3D-ролика

2.5. Компонент для клавиатурного ввода


Возможность смотреть по сторонам в ответ на перемещения указателя мыши относится
к важным фрагментам элементов управления персонажем в играх от первого лица, но
это только половина дела. Кроме этого, игрок должен перемещаться по сцене в ответ
на клавиатурный ввод. Поэтому в дополнение к компоненту для элемента управления
мышью напишем компонент для клавиатурного ввода; создайте новый компонент
C# script с именем FPSInput и присоедините его к игроку (в дополнение к сценарию
MouseLook). При этом мы на время ограничим компонент MouseLook только горизон-
тальным вращением.

СОВЕТ  Рассматриваемые нами элементы управления клавиатурным вводом и вводом с помо­щью


мыши помещены в отдельные сценарии. Вы вовсе не обязаны структурировать код подобным образом
и вполне можете поместить все в единый сценарий «элементов управления игроком». Но, как прави-
ло, модульные системы (такие, как в Unity) стремятся к максимальной гибкости, и, соот­ветственно,
эффективнее всего моделировать функциональность как набор отдельных небольших компонентов.

Код, который мы писали, влиял только на ориентацию объекта, теперь же мы будем


менять только его местоположение. Как показано в листинге 2.8, мы берем код враще-
ния и вводим его в сценарий FPSInput, заменяя метод Rotate() методом Translate().
После щелчка на кнопке Play сцена начнет скользить вверх, а не вращаться, как раньше.
Попытайтесь поменять значения параметров и посмотреть, как изменится движение (на-
пример, после того, как вы поменяете местами первое и второе числа); после некоторого
количества экспериментов можно будет перейти к добавлению клавиатурного ввода.

Листинг 2.8. Код вращения из первого листинга с парой небольших изменений


using UnityEngine;
using System.Collections; Необязательный элемент
на случай, если вы захотите
public class FPSInput : MonoBehaviour { увеличить скорость.
public float speed = 6.0f;
void Update() {
transform.Translate(0, speed, 0); Меняем метод Rotate() на метод Translate().
}
}

2.5.1. Реакция на нажатие клавиш


Код перемещения в ответ на нажатия клавиш (показанный в листинге 2.9) аналогичен
коду вращения в ответ на движение указателя мыши. Мы снова используем метод
GetAxis(), причем аналогичным образом.

Листинг 2.9. Изменение положения в ответ на нажатие клавиш


...
void Update() {
float deltaX = Input.GetAxis("Horizontal") * speed; "Horizontal" и "Vertical" — это
float deltaZ = Input.GetAxis("Vertical") * speed; дополнительные имена для
transform.Translate(deltaX, 0, deltaZ); сопоставления с клавиатурой.
}
2.5. Компонент для клавиатурного ввода   55

Как и раньше, значения, задаваемые методом GetAxis(), умножаются на скорость,


давая в итоге величину смещения. Но если раньше в ответ на запрос оси мы получали
ответ «Mouse имя оси», то теперь это значение Horizontal или Vertical. Это абстрактные
представления для параметров ввода в Unity; если в меню Edit навести указатель
мыши на строку Project Settings и выбрать в открывшемся меню команду Input, откро-
ется список абстрактных имен ввода и соответствующих им элементов управления.
Клавиши с указывающими влево/вправо стрелками и с буквами A/D соответствуют
имени Horizontal, в то время как клавиши со стрелками, указываю­щими вверх/вниз,
и с буквами W/S соответствуют имени Vertical.
Обратите внимание, что значения перемещения меняются по координатам X и Z. Вы
могли заметить в процессе экспериментов с методом Translate(), что изменение ко-
ординаты X соответствует движению из стороны в сторону, в то время как изменение
координаты Z означает движение вперед и назад.
Вставив в сценарий этот новый код перемещения, вы получите возможность двигать
объект по сцене нажатием клавиш со стрелками или клавиш с буквами WASD. Это стан-
дартная ситуация для большинства игр от первого лица. Сценарий, управляю­щий пере-
мещениями, практически готов, осталось только внести в него некоторые изменения.

2.5.2. Независимая от скорости работы компьютера


скорость перемещений
Пока вы запускаете код только на собственном компьютере, этот аспект незаметен, но
на разных машинах игрок будет перемещаться с разной скоростью. Ведь более мощ-
ные компьютеры быстрее обрабатывают код и графику. В настоящее время ско­рость
перемещения вашего игрока привязана к скорости работы компьютера. Это называ-
ется зависимостью от кадровой частоты (frame rate dependent), так как код движения
зависит от частоты кадров игры.
Например, представьте, что вы запускаете деморолик на двух компьютерах, один из
которых отображает на мониторе 30 кадров в секунду, а второй 60 кадров. Это означает,
что на втором компьютере метод Update() будет вызываться в два раза чаще, двигая
объ­ект с одной и той же скоростью 6 при каждом вызове. В итоге при частоте 30 кадров
в секунду скорость движения составит 180 единиц в секунду, в то время как частота
60 кадров в секунду обеспечит скорость перемещения в 360 единиц в секунду. Такая
вариативность неприемлема для большинства игр.
Решить эту проблему позволяет такое редактирование кода, в результате которого мы
получим независимость от кадровой частоты (frame rate independent). То есть ско-
рость перемещения перестанет зависеть от частоты кадров игры. Это обеспечивается
сменой скорости в соответствии с частотой кадров. Она должна уменьшаться или уве-
личиваться в зависимости от того, насколько быстро работает компьютер. Достигнуть
такого результата нам поможет умножение скорости на переменную deltaTime, как
показано в следующем листинге.
Листинг 2.10. Движение с независимостью от кадровой частоты благодаря
переменной deltaTime
...
void Update() {
56    Глава 2. Создание 3D-ролика

float deltaX = Input.GetAxis("Horizontal") * speed;


float deltaZ = Input.GetAxis("Vertical") * speed;
transform.Translate(deltaX * Time.deltaTime, 0, deltaZ * Time.deltaTime);
}
...

Это простое изменение. Класс Time обладает рядом свойств и методов, позволяющих
регулировать время. К таким свойствам относится и deltaTime. Так как мы знаем, что
дельта указывает на величину изменения, переменная deltaTime означает величину
изменения во времени. А конкретно — это время между кадрами. Его величина за­висит
от частоты кадров (например, при частоте 30 кадров в секунду deltaTime со­ставляет
1/30 секунды). Поэтому умножение скорости на эту переменную приведет к масшта-
бированию скорости на различных компьютерах.
Теперь движение нашего персонажа одинаково на всех машинах. Но сценарий еще не
завершен, ведь, перемещаясь по комнате, мы можем проходить сквозь стены.

2.5.3. Компонент CharacterController для распознавания


столкновений
Назначенные объекту напрямую преобразования не затрагивают такой аспект, как
распознавание столкновений. В результате персонаж начинает ходить сквозь стены.
Поэтому нам требуется компонент CharacterController. Именно он обеспечит есте-
ственность перемещений нашего персонажа. Напомню, что в момент настройки игрока
мы присоединяли этот компонент, поэтому теперь остается воспользоваться им в коде
сценария FPSInput (листинг 2.11).

Листинг 2.11. Перемещение компонента CharacterController вместо


компонента Transform
Переменная для ссылки
... на компонент CharacterController.
private CharacterController _charController;

void Start() {
_charController = GetComponent<CharacterController>(); Доступ к другим компонен-
} там, присоединенным
к этому же объекту.
void Update() {
float deltaX = Input.GetAxis("Horizontal") * speed; Ограничим движение по диа-
float deltaZ = Input.GetAxis("Vertical") * speed; гонали той же скоростью,
Vector3 movement = new Vector3(deltaX, 0, deltaZ); что и движение параллельно
movement = Vector3.ClampMagnitude(movement, speed); осям.
Преобразуем вектор движения
movement *= Time.deltaTime; от локальных к глобальным
movement = transform.TransformDirection(movement); координатам.
_charController.Move(movement);
Заставим этот вектор перемещать
} компонент CharacterController.
...

В этом фрагменте кода появляется несколько новых концепций. Прежде всего, это
переменная для ссылки на компонент CharacterController. Она просто создает локаль-
ную ссылку на объект (объект в коде — не путайте его с объектами сцены); ссылаться
2.5. Компонент для клавиатурного ввода   57

на этот экземпляр компонента могут разные сценарии. Изначально эта переменная


пуста, поэтому, прежде чем ею пользоваться, следует указать объект, на который вы
будете ссылаться с ее помощью. Именно здесь появляется метод GetComponent(); он
возвращает другие компоненты, присоединенные к тому же объекту GameObject. Вместо
передачи параметра в скобках мы воспользовались синтаксисом C# и определили тип
в угловых скобках <>.
Теперь, когда у нас появилась ссылка на компонент CharacterController, можно
вызвать для этого контроллера метод Move(). Мы передаем этому методу вектор тем
же способом, которым код вращения в зависимости от указателя мыши использовал
вектор для значений поворота. А аналогично тому, как мы ограничивали значения
поворота, мы задействуем метод Vector3.ClampMagnitude(), чтобы ограничить модуль
вектора скоростью движения; мы берем именно этот метод, потому что в противном
случае движение по диагонали происходило бы быстрее движения вдоль координатных
осей (нарисуйте катеты и гипотенузу прямоугольного треугольника).
Но вектор движения обладает еще одной особенностью, связанной с выбором системы
координат. Вы уже сталкивались с ней при обсуждении преобразования поворота. На-
пример, мы создадим вектор, перемещающий объект влево. Но может оказаться, что
у игрока это направление не совпадает с направлением координатных осей. То есть мы
говорим о локальном, а не о глобальном пространстве. Поэтому методу Move() следует
передавать вектор движения, определенный в глобальном пространстве, а значит, нам
требуется преобразование от локальных к глобальным координатам. Математически
это крайне сложная операция, но, к счастью для нас, об этом позаботится Unity. Нам же
достаточно вызвать метод TransformDirection(), чтобы, как следует из его названия,
преобразовать направление.
ОПРЕДЕЛЕНИЕ  Слово «transform», используемое в качестве глагола, означает преобразование
от одного координатного пространства к другому (если вы не помните, что такое координатное про-
странство, перечитайте раздел 2.3.3). Не следует путать его с аналогичным существительным, которое
означает как компонент Transform, так и изменение положения объектов в сцене. В данном случае
значения терминов перекрываются, так как в основе лежит одна и та же концепция.

Запустите код. Если это еще не сделано, установите для компонента MouseLook вра-
щение одновременно по горизонтали и вертикали. Вы можете без ограничений про-
сматривать пространство вокруг себя и летать, нажимая соответствующие клавиши.
Это здорово, если вам требуется игрок, умеющий летать. Но как сделать, чтобы он
перемещался исключительно по земле?

2.5.4. Ходить, а не летать


Теперь, когда распознавание столкновений работает, в сценарий можно добавить силу
тяжести, чтобы игрок стоял на полу. Объявим переменную gravity и воспользуемся
ее значением для оси Y, как показано в следующем листинге.

Листинг 2.12. Добавление силы тяжести в код движения


...
public float gravity = -9.8f;
...
58    Глава 2. Создание 3D-ролика

void Update() {
...
movement = Vector3.ClampMagnitude(movement, speed);
movement.y = gravity; Используем значение переменной gravity вместо нуля.
...

Теперь на игрока действует постоянная сила, тянущая его вниз. К сожалению, она не
всегда направлена строго вниз, так как изображающий игрока объект может накло-
няться вниз и вверх, следуя за движениями указателя мыши. К счастью, у нас есть все,
чтобы исправить этот недочет, достаточно слегка поменять настройки компонента по
отношению к игроку. Первым делом ограничьте компонент MouseLook только горизон-
тальным вращением. Затем добавьте этот компонент к камере и ограничьте его только
вертикальным вращением. В результате на перемещения указателя мыши у вас будут
реагировать два разных объекта!
Так как игрок теперь поворачивается только в горизонтальной плоскости, решилась
проблема с наклоном вектора силы тяжести. При этом объект камеры является до-
черним по отношению к игроку (помните, как мы установили эту связь на вкладке
Hierarchy?), поэтому, обладая способностью поворачиваться в вертикальной плоско­сти
независимо от игрока, она вращается по горизонтали вслед за ним.

СОВЕРШЕНСТВОВАНИЕ ГОТОВОГО СЦЕНАРИЯ__________________________


Воспользуйтесь методом RequireComponent(), чтобы проверить, присоединены ли остальные тре-
буемые сценарию компоненты. Иногда такие компоненты являются необязательными (то есть можно
сказать, что «если этот компонент присоединен, тогда…»), но бывают ситуации, когда без каких-то
компонентов сценарий работать не будет. Поэтому добавьте в верхнюю часть сценария этот метод,
чтобы обеспечить выполнение зависимостей, указав требуемый компонент в качестве параметра.
Кроме того, можно вставить в верхнюю часть сценария метод AddComponentMenu(), который
добавит сценарий в меню компонентов в редакторе Unity. Достаточно указать имя элемента меню,
и вы получите возможность выбирать данный сценарий в списке, открываемом щелчком на кнопке
Add Component в нижней части панели Inspector. Очень удобно!
Код добавления обоих методов выглядит примерно так:
using UnityEngine;
using System.Collections;

[RequireComponent(typeof(CharacterController))]
[AddComponentMenu("Control Script/FPS Input")]
public class FPSInput : MonoBehaviour {

Листинг 2.13 демонстрирует полностью готовый сценарий. В дополнение к слегка


скорректированной настройке компонентов игрока мы добавили игроку возможность
ходить по комнате. Несмотря на наличие переменной gravity, вы легко можете заста-
вить игрока летать, введя в поле Gravity на панели Inspector значение 0.

Листинг 2.13. Готовый сценарий FPSInput


using UnityEngine;
using System.Collections;

[RequireComponent(typeof(CharacterController))]
Заключение   59

[AddComponentMenu("Control Script/FPS Input")]


public class FPSInput : MonoBehaviour {
public float speed = 6.0f;
public float gravity = -9.8f;

private CharacterController _charController;

void Start() {
_charController = GetComponent<CharacterController>();
}

void Update() {
float deltaX = Input.GetAxis("Horizontal") * speed;
float deltaZ = Input.GetAxis("Vertical") * speed;
Vector3 movement = new Vector3(deltaX, 0, deltaZ);
movement = Vector3.ClampMagnitude(movement, speed);

movement.y = gravity;

movement *= Time.deltaTime;
movement = transform.TransformDirection(movement);
_charController.Move(movement);
}
}

Поздравляю вас с созданием 3D-проекта! В этой главе вы получили большое коли-


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

Заключение
 Трехмерное координатное пространство определяется осями X, Y и Z.
 Сцену создают помещенные в комнату объекты и источники света.
 Игрока в сцене от первого лица, по сути, представляет камера.
 Код движения в каждом кадре циклически повторяет небольшие преобразования.
 Элементы управления персонажем в  игре от первого лица состоят из указателя
мыши, отвечающего за вращение, и клавиатуры, отвечающей за перемещения.
3 Добавляем в игру врагов
и снаряды

33 Как игрок и его враги получают возможность целиться и стрелять.


33 Попадания и реакция на них.
33 Заставляем врагов перемещаться.
33 Порождение новых объектов сцены.

Ролик, демонстрирующий перемещения объекта, который вы создали в предыдущей


главе, выглядит здорово, но до полноценной игры ему далеко. Давайте попробуем пре-
вратить его в шутер от первого лица. Для этого нам не хватает возможности стрелять,
а также врагов, которых требуется поразить. Начнем мы с написания сценариев, пре-
вращающих нашего игрока в стрелка. После этого мы заполним сцену врагами, добавив
в числе прочего код, позволяющий бесцельно перемещаться по сцене и реагировать на
попадание. В заключение мы дадим врагам возможность отстреливаться, кидая в игрока
огненные шары. Написанные в главе 2 сценарии при этом редактироваться не будут;
мы просто добавим в проект новые сценарии с дополнительными функциональными
возможностями.
Я выбрал для этого проекта шутер от первого лица по двум причинам. Во-первых,
подобные игры популярны, ведь людям нравится стрелять. Во-вторых, я учел набор
приемов, с которыми вы можете ознакомиться в процессе работы. Создание такой
игры дает возможность изучить несколько фундаментальных концепций трехмерного
моделирования. Например, вы узнаете, что такое бросание луча (raycasting). Подробно
особенности этого приема я объясню чуть позже, а пока достаточно информации о том,
что он позволяет решать множество различных задач в трехмерном моделировании
и применяется во множестве ситуаций, из которых интуитивно наиболее понятной
является реализация стрельбы.
Создание блуждающих целей, которые требуется поразить, дает нам замечательный
повод исследовать код контролируемых компьютером персонажей, а также техники
3.1. Стрельба путем бросания лучей   61

отправки сообщений и порождения объектов. Более того, поведение блуждающих


целей — это еще один аспект, в моделировании которого нам пригодится прием ис-
пускания луча, так что мы познакомимся с еще одним вариантом его применения.
Также широко применим демонстрируемый в рамках этого проекта подход к рассылке
сообщений. В следующих главах вы встретите другие варианты применения указан-
ных приемов. Впрочем, даже в рамках одной главы они используются в различных
ситуациях.
По сути дела, мы добавляем в проект по одной функциональной возможности за раз,
причем на каждом этапе в игру можно играть, но всегда остается ощущение, что каких-
то элементов не хватает. Вот план нашей будущей работы:
1. Написать код, позволяющий игроку стрелять.
2. Создать статичные цели, реагирующие на попадание.
3. Заставить цели перемещаться по сцене.
4. Вызвать автоматическое появление новых блуждающих целей.
5. Дать возможность целям/врагам кидать в игрока огненные шары.

ПРИМЕЧАНИЕ  Проект, которым мы будем заниматься в этой главе, предполагает наличие де-
монстрационного ролика с перемещающимся по сцене персонажем. Этот ролик создавался в главе 2,
но если вы ее пропустили, скачайте соответствующие файлы с сайта книги.

3.1. Стрельба путем бросания лучей


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

3.1.1. Что такое бросание лучей?


Название приема подразумевает, что вам предстоит бросать лучи. Но что именно
в данном случае подразумевается под словом луч?

ОПРЕДЕЛЕНИЕ  Лучом (ray) в сцене называется воображаемая, невидимая линия, начинающаяся


в некоторой точке и распространяющаяся в определенном направлении.

Прием бросания луча иллюстрирует рис. 3.1. Вы формируете луч и затем определяете,
с чем он пересекается. Подумайте, что происходит, когда вы стреляете из пистолета:
пуля вылетает из точки, в которой находится пистолет, и летит по прямой вперед, пока
не столкнется с каким-нибудь препятствием. Луч можно сравнить с путем пули, а бро-
сание луча аналогично выстрелу из пистолета и наблюдению за тем, куда попадет пуля.
Думаю, очевидно, что реализация метода бросания лучей зачастую требует сложных
математических расчетов. Трудно не только просчитать пересечение линии с трехмер-
ной плоскостью, но и сделать это для всех сеточных объектов в сцене (напоминаю, что
62    Глава 3. Добавляем в игру врагов и снаряды

сеточным объектом называется трехмерный видимый объект, сконструированный из


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

Луч, проходящий через


трехмерную сцену

Источник луча
(представьте себе пистолет)

Точка пересечения,
то есть место столкновения
луча с препятствием
Рис. 3.1. Луч представляет собой воображаемую линию,
и при бросании луча выясняется, с чем этот луч пересекается

В текущем проекте ответом на второй вопрос (почему возникает луч) является ими-
тация выпускания пули. В шутерах от первого лица луч, как правило, начинается из
места, где располагается камера, и распространяется по центру ее поля зрения. Иными
словами, вы проверяете наличие объекта непосредственно перед камерой — в Unity
есть соответствующие команды. Рассмотрим их более подробно.

3.1.2. Имитация стрельбы командой ScreenPointToRay


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

ОПРЕДЕЛЕНИЕ  Выбор с помощью мыши (mouse picking) означает действие по выбору в трех-
мерной сцене точки, непосредственно попадающей под указатель мыши.

Эта операция в Unity осуществляется методом ScreenPointToRay(). Происходящее


иллюстрирует рис. 3.2. Метод создает луч, начинающийся с камеры, и проецирует его
по линии, проходящей через указанные экранные координаты. Обычно при выборе
с помощью мыши используются координаты указателя, но в шутерах от первого лица
эту роль играет центр экрана. Появившийся луч передается методу Physics. Raycast(),
который и выполняет его «бросание».
Напишем код, использующий только что рассмотренные нами методы. Создайте в ре-
дакторе Unity новый компонент C# script и присоедините его к камере (а не к объекту,
представляющему игрока). Добавьте в него код следующего листинга.
3.1. Стрельба путем бросания лучей   63

В листинге следует обратить внимание на несколько моментов. Во-первых, компонент


Camera вызывается в методе Start() совсем как компонент CharacterController в пре-
дыдущей главе. Остальная часть кода помещена в метод Update(), так как положение
мыши нам требуется проверять снова и снова. Метод Input.GetMouseButtonDown()
в зависимости от того, нажимается ли кнопка мыши, возвращает значения true и false.
Помещение этой команды в условную инструкцию означает, что указанный код вы-
полняется только после щелчка кнопкой мыши. Мы хотим, чтобы выстрел возникал
по щелчку мыши, именно поэтому условная инструкция проверяет ее состояние.

Источником луча Экран (то есть окно камеры


является камера, в трехмерной сцене)
аналогично
упоминавшемуся ранее
пистолету

Луч от камеры проходит


через эту точку экрана

Рис. 3.2. Метод ScreenPointToRay() проецирует луч от камеры


через указанные экранные координаты

Листинг 3.1. Сценарий RayShooter, присоединяемый к камере


using UnityEngine;
using System.Collections;

public class RayShooter : MonoBehaviour {


private Camera _camera;
Доступ к другим компонентам,
void Start() {
присоединенным к этому же объекту.
_camera = GetComponent();
}
Реакция на нажатие
кнопки мыши. Середина экрана —
void Update() { это половина его
if (Input.GetMouseButtonDown(0)) { ширины и высоты.
Vector3 point = new Vector3( Создание в этой
_camera.pixelWidth/2, _camera.pixelHeight/2, 0); точке луча методом
ScreenPointToRay().
Ray ray = _camera.ScreenPointToRay(point);
RaycastHit hit;
if (Physics.Raycast(ray, out hit)) {
Debug.Log("Hit " + hit.point);
}
Загружаем координаты точки, Испущенный луч заполняет
}
в которую попал луч. информацией переменную,
} на которую имеется ссылка.
}
64    Глава 3. Добавляем в игру врагов и снаряды

Вектор создается, чтобы определить для луча экранные координаты (напоминаю,


что вектором называются несколько связанных друг с другом чисел, хранимых как
единое целое). Параметры pixelWidth и pixelHeight дают нам размер экрана. Определить
его центр можно, разделив эти значения пополам. Хотя координаты экрана являются
двумерными, то есть у нас есть только размеры по вертикали и горизонтали, а глубина
отсутствует, вектор Vector3 все равно создается, так как метод ScreenPointToRay()
требует данных этого типа (возможно, потому, что расчет луча включает в себя опе-
рации с трехмерными векторами). Вызванный для этого набора координат метод
ScreenPointToRay() дает нам объект Ray (программный, а не игровой объект; эти два
объекта часто путают).
Затем луч передается в метод Raycast(), причем это не единственный передаваемый
в этот метод объект. Есть также структура данных RaycastHit; она представляет собой
набор информации о пересечении луча, в том числе о точке, в которой возник луч, и об
объекте, с которым он столкнулся. Используемый в данном случае синтаксис языка C#
гарантирует, что структура данных, с которой работает команда, является тем же объ-
ектом, существующим вне команды, в противоположность ситуациям, когда в разных
областях действия функции используются разные копии объекта.
В конце мы вызываем метод Physics.Raycast(), который проверяет место пересечения
рассматриваемого луча, собирает информацию об этом пересечении и возвращает зна-
чение true в случае столкновения луча с препятствием. Так как возвращаемое значение
принадлежит типу Boolean, метод можно поместить в инструкцию проверки условия,
совсем как метод Input.GetMouseButtonDown() чуть раньше.
Пока что на пересечения код реагирует консольным сообщением с координатами точки,
в которой луч столкнулся с препятствием (значения X, Y, Z мы обсуждали в главе 2).
Но понять, в каком именно месте это произошло, или показать, где находится центр
экрана (то есть место, через которое проходит луч), практически нереально. Поэтому
давайте добавим в сцену визуальные индикаторы.

3.1.3. Добавление визуальных индикаторов


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

ОПРЕДЕЛЕНИЕ  Сопрограммы (coroutines) в Unity выполняются параллельно программе в тече-


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

Начнем с добавления индикаторов в точки попадания. Готовый сценарий показан


в листинге 3.2. Подвигайтесь по сцене, стреляя; индикаторы в виде сфер выглядят
довольно забавно!
3.1. Стрельба путем бросания лучей   65

Сфера указывает место


попадания в стену

Точка прицеливания
в центре экрана

Рис. 3.3. Многократные выстрелы после добавления индикаторов прицеливания и попаданий

Листинг 3.2. Сценарий RayShooter после добавления индикаторных сфер


using UnityEngine;
using System.Collections;

public class RayShooter : MonoBehaviour {


private Camera _camera;

void Start() {
_camera = GetComponent();
Эта функция по большей части содержит
}
знакомый нам код бросания луча
из листинга 3.1.
void Update() {
if (Input.GetMouseButtonDown(0)) {
Vector3 point = new Vector3(
_camera.pixelWidth/2, _camera.pixelHeight/2, 0);
Ray ray = _camera.ScreenPointToRay(point);
RaycastHit hit; Запуск сопрограммы
в ответ на попадание.
if (Physics.Raycast(ray, out hit)) {
StartCoroutine(SphereIndicator(hit.point));
}
}
Сопрограммы пользуются
}
функциями IEnumerator.
private IEnumerator SphereIndicator(Vector3 pos) {
GameObject sphere = GameObject.CreatePrimitive(PrimitiveType.Sphere);
sphere.transform.position = pos;

yield return new WaitForSeconds(1);


Ключевое слово yield
указывает сопрограмме,
Destroy(sphere); Удаляем этот GameObject когда следует остановиться.
} и очищаем память.
}

Из нового у нас появился метод SphereIndicator() и однострочная модификация су-


ществующего метода Update(). Новый метод создает сферу в указанной точке сцены
и через секунду удаляет ее. Вызов метода SphereIndicator() внутри кода испускания
луча гарантирует появление визуальных индикаторов точно в местах попадания.
66    Глава 3. Добавляем в игру врагов и снаряды

Данная функция определена с помощью интерфейса IEnumerator, связанного с кон-


цепцией сопрограмм.
С технической точки зрения сопрограммы не асинхронны (асинхронные операции не
останавливают выполнение остальной части кода; представьте, к примеру, скачивание
изображения в сценарии веб-сайта), но продуманное применение перечислений в Unity
заставляет сопрограммы вести себя аналогично асинхронным функциям. Секретным
компонентом сопрограммы является ключевое слово yield, временно прерывающее
ее работу, возвращающее управление основной программе и в следующем кадре воз-
обновляющее сопрограмму с прерванной точки. В результате создается впечатление,
что сопрограммы работают в фоновом режиме.
Как следует из ее имени, функция StartCoroutine() запускает сопрограмму. После
этого она начинает работать до завершения выполнения, периодически делая паузы.
Обратите внимание на небольшую, но важную деталь. Переданный в StartCoroutine()
метод имеет набор скобок, следующий за именем. Такой синтаксис означает, что вы не
передаете имя функции, а вызываете ее. И эта функция работает, пока не встретится
команда yield. После этого ее выполнение на время прервется.
Функция SphereIndicator() создает сферу в определенной точке, останавливается
после инструкции yield и после возобновления сопрограммы удаляет сферу. Про-
должительность паузы контролируется значением, которое возвращается в  момент
появления инструкции yield. В сопрограммах допустимо несколько типов возвра-
щаемых значений, но проще всего в явном виде вернуть время ожидания. Возвращая
WaitForSeconds(1), мы заставляем сопрограмму остановить работу на одну секунду.
Создание сферы, секундная остановка и разрушение сферы — такая последователь-
ность дает нам временный визуальный индикатор.
Код для таких индикаторов был дан в листинге 3.2. Но нам требуется также точка
прицеливания в центре экрана. Она создается в следующем листинге.

Листинг 3.3. Визуальный индикатор для точки прицеливания


...
void Start() {
_camera = GetComponent();

Cursor.lockState = CursorLockMode.Locked; Скрываем указатель мыши


Cursor.visible = false; в центре экрана.
}

void OnGUI() {
int size = 12;
float posX = _camera.pixelWidth/2 - size/4; Команда GUI.Label()
отображает на экране
float posY = _camera.pixelHeight/2 - size/2;
символ.
GUI.Label(new Rect(posX, posY, size, size), "*");
}
...

Еще один новый метод, добавленный в класс RayShooter, называется OnGUI(). В Unity
возможна как базовая, так и усовершенствованная система пользовательского интер-
фейса (UI). Так как базовая система обладает множеством ограничений, в следующих
3.2. Создаем активные цели   67

главах мы построим более гибкий, усовершенствованный пользовательский интерфейс,


но пока нам проще отображать точку в центре экрана средствами базового интерфейса.
Любой сценарий класса MonoBehaviour автоматически реагирует как на методы Start()
и Update(), так и на функцию OnGUI(). Эта функция запускается в каждом кадре после
визуализации трехмерной сцены, прорисовывая поверх сцены дополнительные эле-
менты (представьте себе бумажные этикетки, наклеенные на нарисованный пейзаж).

ОПРЕДЕЛЕНИЕ  Визуализацией (rendering) называется работа компьютера по прорисовыва-


нию пикселов трехмерной сцены. Хотя сцена задается с  помощью координат X, Y и  Z, монитор
отображает двумерную сетку цветных пикселов. Соответственно, для показа трехмерной сцены
компьютер должен рассчитать цвет всех пикселов двумерной сетки; работа этого алгоритма и на-
зывается визуализацией.

Код внутри функции OnGUI() определяет двумерные координаты для отображения


(слегка смещенные с учетом размера метки) и затем вызывает метод GUI.Label(). Этот
метод отображает текстовую метку; так как переданная ему строка состоит из символа
звездочки (*), именно он появляется в центре экрана. С ним прицеливаться в нашей
будущей игре станет намного проще! Кроме того, листинг 3.3 добавляет в метод Start()
настройки указателя мыши, а именно возможность управлять его видимостью и воз-
можность блокировки. В принципе, сценарий прекрасно работает и без этих настроек,
но они делают элементы управления более удобными в использовании. Указатель
мыши все время будет располагаться в центре экрана, а чтобы не заслонять обзор, мы
сделаем его невидимым. Он будет появляться только при нажатии клавиши Esc.

ВНИМАНИЕ  Помните, что вы в любой момент можете снять блокировку с указателя мыши, нажав
клавишу Esc. При заблокированном указателе щелкнуть на кнопке Play и остановить игру невозможно.

Итак, код, управляющий поведением игрока, готов. Фактически мы завершили работу


над взаимодействиями игрока со сценой, но у нас все еще отсутствуют цели.

3.2. Создаем активные цели


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

3.2.1. Определяем точку попадания


Первым делом нам нужен объект, который послужит мишенью. Создайте куб (выбрав
в меню GameObject команду 3D Object, а затем вариант Cube) и поменяйте его размер по
вертикали, введя в поле Y для преобразования Scale значение 2. В полях X и Z оставьте
значение 1. Поместите новый объект в точку с координатами 0, 1, 0, чтобы он стоял
на полу в центре комнаты, и присвойте ему имя Enemy. Создайте сценарий с именем
ReactiveTarget и присоедините его к объекту. Скоро мы напишем для этого сценария код,
но пока оставьте его как есть; мы создали этот файл, так как без него код из следующего
68    Глава 3. Добавляем в игру врагов и снаряды

листинга компилироваться не будет. Вернитесь к сценарию RayShooter.cs и отредакти-


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

Листинг 3.4. Распознавание попаданий в цель


...
Получаем объект, в который
if (Physics.Raycast(ray, out hit)) {
попал луч.
GameObject hitObject = hit.transform.gameObject;
ReactiveTarget target = hitObject.GetComponent<ReactiveTarget>();
if (target != null) { Проверяем наличие у этого объекта
Debug.Log("Target hit"); компонента ReactiveTarget.
} else {
StartCoroutine(SphereIndicator(hit.point));
}
}
...

Обратите внимание, что вы получаете объект, с которым пересекся луч, совсем так же,
как получали координаты для сферических индикаторов. С технической точки зрения
вам возвращается вовсе не игровой объект, а попавший под удар компонент Transform.
Далее вы получаете доступ к объекту gameObject как к свойству класса transform.
Затем к этому объекту применяется метод GetComponent(), проверяющий, является
ли он активной целью (то есть присоединен ли к нему сценарий ReactiveTarget).
Как вы уже видели раньше, этот метод возвращает компоненты определенного типа,
присоединенные к объекту GameObject. Если таковые отсутствуют, не возвращается
ничего. Поэтому остается проверить, было ли возвращено значение null, и написать
два варианта кода, которые будут запускаться в зависимости от результата проверки.
Если пораженным объектом оказывается активная цель, вместо запуска сопрограммы
для сферических индикаторов код отображает отладочное сообщение. Теперь нам
нужно проинформировать целевой объект о попадании, чтобы он смог отреагировать
на это событие.

3.2.2. Уведомляем цель о попадании


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

Листинг 3.5. Отправка сообщения целевому объекту


...
if (target != null) {
target.ReactToHit(); Вызов метода для мишени вместо генерации отладочного сообщения.
} else {
StartCoroutine(SphereIndicator(hit.point));
}
...

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


нам нужно написать. Введите в сценарий ReactiveTarget код следующего листинга.
При попадании мишень будет опрокидываться и исчезать, как показано на рис. 3.4.
3.2. Создаем активные цели   69

Рис. 3.4. Целевой объект, падающий после попадания

Листинг 3.6. Сценарий ReactiveTarget, реализующий смерть врага при попадании


using UnityEngine;
using System.Collections;

public class ReactiveTarget : MonoBehaviour {

public void ReactToHit() { Метод, вызванный сценарием стрельбы.


StartCoroutine(Die());
}

private IEnumerator Die() { Опрокидываем врага, ждем 1,5 секунды и уничтожаем его.
this.transform.Rotate(-75, 0, 0);

yield return new WaitForSeconds(1.5f);

Destroy(this.gameObject); Объект может уничтожать сам


} себя точно так же, как любой
} другой объект.

Большая часть кода должна быть вам уже знакома по предыдущим сценариям, поэто-
му рассматривать его мы будем совсем кратко. Первым делом мы определяем метод
ReactToHit(), так как именно он вызывается в сценарии стрельбы. Этот метод запускает
сопрограмму, аналогичную коду для сферических индикаторов, но на этот раз она при-
звана манипулировать объектом этого же сценария, а не создавать отдельный объект.
Такие выражения, как this.gameObject, относятся к объекту GameObject, к которому
присоединен данный сценарий (ключевое слово this указывать необязательно, то есть
можно ссылаться просто на gameObject).
Первая строка сопрограммы заставляет мишень опрокинуться. Как обсуждалось в гла-
ве 2, преобразование вращения можно определить как угол поворота относительно
каждой из трех осей, X, Y и Z. Так как объект не должен поворачиваться из стороны
в сторону, оставьте координатам Y и Z значение 0, а угол поворота назначьте координате X.

ПРИМЕЧАНИЕ  Преобразование происходит мгновенно, но, возможно, вы предпочитаете видеть,


как опрокидываются мишени. После более детального знакомства с методами моделирования, воз-
можно, вы захотите воспользоваться анимацией по начальной и конечной точкам (tweening), позво-
ляющей смоделировать плавное движение объектов.
70    Глава 3. Добавляем в игру врагов и снаряды

Во второй строке метода фигурирует ключевое слово yield, останавливающее выпол-


нение сопрограммы и возвращающее количество секунд, через которое она возобновит
свою работу. В последней строке функции игровой объект уничтожается. Функция
Destroy(this.gameObject) вызывается после времени ожидания точно так же, как
раньше мы вызывали код Destroy(sphere).
ВНИМАНИЕ  Аргумент функции Destroy() в  данном случае должен выглядеть как this.
gameObject, а не просто this! Не путайте эти два случая; само по себе ключевое слово this отно-
сится к компоненту данного сценария, в то время как this.gameObject означает объект, к которому
присоединен данный сценарий.

Теперь наша мишень реагирует на попадание! Но больше она ничего не делает. Давайте
добавим ей дополнительные модели поведения, чтобы превратить ее в настоящего
врага.

3.3. Базовый искусственный интеллект


для перемещения по сцене
Статичная цель не очень интересна, поэтому давайте напишем код, который заставит
врагов перемещаться по сцене. Такой код является одним из простейших примеров ис-
кусственного интеллекта (Artificial Intelligence, AI). Этот термин относится к сущностям,
поведение которых контролируется компьютером. В данном случае такой сущностью
служит враг в нашей игре, но в реальной жизни это может быть, например, робот.

3.3.1. Диаграмма работы базового искусственного интеллекта


Существует множество подходов к реализации искусственного интеллекта (это одна
из основных областей исследований ученых, работающих в области теории вычисли-
тельных машин и систем), но для наших целей подойдет достаточно простой вариант.
По мере накопления опыта и усложнения создаваемых вами игр вы, скорее всего, за-
хотите познакомиться с другими способами реализации AI. Но пока мы ограничимся
процессом, показанным на рис. 3.5.
Шаг 1: Небольшое смещение Шаг 2: Бросание луча вперед Шаг 3: Отход от препятствия
вперед для обнаружения препятствий

Шаг 4: Визуализация кадра,


возвращение к шагу 1
Рис. 3.5. Базовый искусственный интеллект: циклический процесс движения вперед
с обходом препятствий
3.3. Базовый искусственный интеллект для перемещения по сцене   71

В каждом кадре код искусственного интеллекта сканирует окружающее пространство,


чтобы определить свое дальнейшее поведение. Если на пути появляется препятствие,
враг меняет направление движения. При этом независимо от поворотов он неуклонно
двигается вперед. В итоге враг будет в разных направлениях ходить по комнате, не-
прерывно перемещаясь вперед и поворачиваясь, чтобы не сталкиваться со стенами.
Сам код будет иметь знакомый вам вид, так как движение врагов вызывают те же
коман­ды, что и движение игрока. Кроме того, в коде вы снова увидите метод бросания
луча, но уже в другом контексте.

3.3.2. «Поиск» препятствий методом бросания лучей


Как вы узнали во введении к данной главе, бросание луча представляет собой прием,
позволяющий решать различные задачи трехмерного моделирования. Первым делом
на ум приходит имитация выстрелов, но, кроме того, этот прием позволяет сканировать
окружающее пространство. А так как в данном случае нам требуется решить именно
эту задачу, код испускания лучей попадет в наш код для AI.
Раньше мы создавали луч, который брал начало из камеры, так как именно она слу-
жит глазами игрока. На этот раз луч будет начинаться в месте расположения врага.
В первом случае луч проходил через центр экрана, теперь же он будет распространяться
перед персонажем, как показано на рис. 3.6. Затем аналогично тому, как код стрельбы
использовал информацию из структуры RaycastHit, чтобы определить, поражена ли
какая-нибудь цель и где она находится, код AI задействует эту же информацию, чтобы
определить наличие препятствия по ходу движения и расстояние до этого препятствия.

В каждом кадре персонаж AI испускает перед собой луч, чтобы


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

Рис. 3.6. Обнаружение препятствий методом бросания луча

Основным различием между лучом, бросаемым в случае выстрела, и лучом в коде AI


является радиус распознаваемого пространства. При стрельбе мы обходились бесконеч-
но тонким лучом, в то время как у луча для AI будет большое сечение. Соответственно,
мы воспользуемся методом SphereCast() вместо метода Raycast(). Все дело в том, что
72    Глава 3. Добавляем в игру врагов и снаряды

пули имеют маленький размер, в то время как, проверяя наличие препятствий перед
персонажем, мы должны учитывать ширину самого персонажа.
Создайте сценарий с именем WanderingAI и присоедините его к целевому объекту
(вместе со сценарием ReactiveTarget). Введите в него код из следующего листинга.
Запустите код, и вы увидите, как враг перемещается по комнате; при этом вы можете
выстрелить в него, и он среагирует на попадание так же, как и раньше.

Листинг 3.7. Базовый сценарий WanderingAI


using UnityEngine;
using System.Collections;
Значения для скорости движения
ublic class WanderingAI : MonoBehaviour { и расстояния, с которого начинается
public float speed = 3.0f; реакция на препятствие.
public float obstacleRange = 5.0f;
Непрерывно движемся
void Update() { вперед в каждом кадре,
transform.Translate(0, 0, speed * Time.deltaTime); несмотря на повороты.

Ray ray = new Ray(transform.position, transform.forward);


RaycastHit hit;
if (Physics.SphereCast(ray, 0.75f, out hit)) { Бросаем луч с описанной
if (hit.distance < obstacleRange) { вокруг него окружностью.
float angle = Random.Range(-110, 110);
transform.Rotate(0, angle, 0);
} Луч находится в том же положении
и нацеливается в том же
} Поворот с наполовину случайным
направлении, что и персонаж.
} выбором нового направления.
}

В листинге появилась пара новых переменных. Одна — для скорости движения врага,
а вторая — для расстояния, на котором враг начинает реагировать на препятствие. За-
тем мы добавили внутрь метода Update() метод Translate(), обеспечив непрерывное
движение вперед (в том числе воспользовавшись переменной deltaTime для движения,
не зависящего от частоты кадров). Кроме того, в метод Update() помещен код броса-
ния луча, во многом напоминающий написанный нами ранее сценарий поражения
целей. В данном случае прием бросания луча применяется для осмотра сцены, а не
для стрельбы. Луч создается на базе положения врага и направления его движения,
а не на базе камеры.
Как уже упоминалось, для расчета траектории луча применяется метод Physics.
SphereCast(), в качестве параметра принимающий радиус окружности, в пределах ко-
торой будут распознаваться пересечения. Во всех же прочих отношениях он аналогичен
методу Physics.Raycast(). Сходство наблюдается в способе получения информации
о столкновении луча, способе проверки пересечений и применении свойства distance,
гарантирующего, что враг среагирует только тогда, когда приблизится к препятствию.
Как только враг окажется перед стеной, код поменяет направление его движения напо-
ловину случайным образом. Я использую словосочетание «наполовину случайным»,
так как значения ограничены минимумом и максимумом, имеющими смысл в данной
ситуации. Мы прибегаем к методу Random.Range(), которым Unity позволяет получить
3.3. Базовый искусственный интеллект для перемещения по сцене   73

случайное значение в указанном диапазоне. В нашем случае ограничения немного


превышают величины поворота влево и вправо, что дает персонажу возможность раз-
вернуться на достаточный угол, чтобы избежать препятствий.

3.3.3. Слежение за состоянием персонажа


Текущее поведение врага имеет один недостаток. Движение вперед продолжается
даже после попадания в него пули. Ведь метод Translate() запускается в каждом
кадре вне зависимости от обстоятельств. Внесем в код небольшие изменения, по-
зволяющие следить за тем, жив персонаж или мертв. Говоря техническим языком, мы
хотим отслеживать «живое» состояние персонажа. Код, по-разному реагирующий на
разные состояния, представляет собой паттерн, распространенный во многих областях
программирования, а не только в AI. Более сложные реализации этого паттерна на-
зываются конечными автоматами.

ОПРЕДЕЛЕНИЕ  Конечным автоматом (Finite State Machine, FSM) называется структура кода,
в  которой отслеживается текущее состояние объекта, существуют четко определенные переходы
между состояниями и код ведет себя по-разному в зависимости от состояния.

Разумеется, речь о настоящей реализации конечного автомата не идет, но нет ничего


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

Листинг 3.8. Сценарий WanderingAI после добавления «живого» состояния


...
private bool _alive; Логическая переменная для слежения за состоянием персонажа.

void Start() {
_alive = true; Инициализация этой переменной.
}

void Update() {
if (_alive) { Движение начинается только в случае живого персонажа.
transform.Translate(0, 0, speed * Time.deltaTime);
...
}
}

public void SetAlive(bool alive) { Открытый метод, позволяющий


_alive = alive; внешнему коду воздействовать
} на «живое» состояние.
...

Теперь сценарий ReactiveTarget может сообщить сценарию WanderingAI, в каком со-


стоянии находится враг.
74    Глава 3. Добавляем в игру врагов и снаряды

Листинг 3.9. Сценарий ReactiveTarget сообщает сценарию WanderingAI, когда наступает


смерть
...
public void ReactToHit() {
WanderingAI behavior = GetComponent();
if (behavior != null) { Проверяем, присоединен ли к персонажу
behavior.SetAlive(false); сценарий WanderingAI; он может
} и отсутствовать.
StartCoroutine(Die());
}
...

СТРУКТУРА КОДА ДЛЯ AI___________________________________________


Приведенный в этой главе код для AI помещен в один класс, так что изучить и понять его до-
статочно просто. Такая структура кода совершенно нормальна для простого искусственного
интеллекта, поэтому не бойтесь, что вы сделали что-то не так и что на самом деле требуется
более сложная структура. Для усложненных реализаций искусственного интеллекта (например,
в игре с широким диапазоном интеллектуальных персонажей) облегчить разработку AI поможет
более надежная структура кода.
Как упоминалось в  главе 1 при сравнении компонентной структуры с  наследованием, иногда
фрагменты кода для AI имеет смысл помещать в  отдельные сценарии. Это позволит сочетать
и комбинировать компоненты, генерируя уникальное поведение для каждого персонажа. Поду-
майте, в чем ваши персонажи одинаковы, а чем различаются. Именно эти различия помогут вам
при разработке архитектуры кода. К примеру, если в игре есть враги, сломя голову несущиеся на
персонажа, и враги, тихо подкрадывающиеся в тени, имеет смысл создать для них разные ком-
поненты перемещения и написать отдельные сценарии для перемещения бегом и перемещения
крадучись.
Точная структура кода для AI зависит от особенностей конкретной игры; единственного «правиль-
ного» варианта в данном случае не существует. Средства Unity позволят вам легко спроектировать
гибкую архитектуру.

3.4. Увеличение количества врагов


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

3.4.1. Что такое шаблон экземпляров?


Шаблоны экземпляров предлагают гибкий подход к визуальному определению инте-
рактивных объектов. В двух словах, это полностью сформированный игровой объект
(с уже присоединенными и настроенными компонентами), существующий не внутри
конкретной сцены, а в виде ресурса, который может быть скопирован в любую сцену.
Копирование может осуществляться вручную, чтобы гарантировать идентичность
объектов-врагов (или других объектов) в каждой сцене. Но куда важнее то, что эти
шаблоны могут порождаться кодом; поместить копии объектов в сцену можно не только
вручную в визуальном редакторе, но и с помощью команд сценария.
3.4. Увеличение количества врагов   75

ОПРЕДЕЛЕНИЕ  Ресурсом (asset) называется любой файл, отображаемый на вкладке Project;


это могут быть двумерные изображения, трехмерные модели, файлы с кодом, сцены и т. п. Термин
«ресурс» вскользь упоминался в  главе  1, но до текущего момента мы не акцентировали на нем
внимание.

Термин экземпляр (instance) относится также к создаваемым на основе класса объ-


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

3.4.2. Создание шаблона врага


Конструирование шаблона начинается с создания объекта. Так как мы планируем
получить шаблон из объекта-врага, этот шаг уже сделан. Теперь нужно перетащить
строку с названием этого объекта с вкладки Hierarchy на вкладку Project, как показано
на рис. 3.7. Объект автоматически сохранится в качестве шаблона. На панели Hierarchy
его имя будет выделено синим цветом, означающим, что теперь он связан с шаблоном
экземпляров. Редактирование этого шаблона (например, добавление компонентов) осу-
ществляется посредством редактирования объекта сцены, после чего в меню GameObject
выбирается команда Apply Changes To Prefab. Но сейчас данный объект в сцене уже не
нужен (мы собираемся порождать новые шаблоны, а не пользоваться уже имеющимся
экземпляром), поэтому его следует удалить.

Для создания шаблона перетащите


объект с вкладки Hierarchy
на вкладку Project

Рис. 3.7. Процесс получения шаблона экземпляров

ВНИМАНИЕ  Интерфейс для работы с шаблонами экземпляров не слишкомудобен, а соотношения


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

Теперь у нас есть шаблон для заполнения сцены врагами, поэтому давайте напишем
код, создающий его экземпляры.
76    Глава 3. Добавляем в игру врагов и снаряды

3.4.3. Экземпляры невидимого компонента SceneController


Хотя сам по себе шаблон в сцене отсутствует, нам нужен некий объект, к которому
будет присоединяться код, порождающий врагов. Поэтому мы создадим пустой игро-
вой объект и добавим к нему сценарий, при этом сам объект останется невидимым.

СОВЕТ  Использование пустого объекта GameObject для присоединения к  нему компонен­


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

Выберите в меню GameObject команду Create Empty и присвойте новому объекту имя
Controller. Убедитесь, что он находится в точке с координатами 0, 0, 0 (с технической
точки зрения местоположение этого объекта не имеет значения, так как его все равно не
видно, но, поместив его в начало координат, вы облегчите себе жизнь, если в будущем
решите использовать его в цепочке наследования). Создайте сценарий SceneController,
показанный в следующем листинге.

Листинг 3.10. Сценарий SceneController, порождающий экземпляры врагов


using UnityEngine; Сериализованная
using System.Collections; переменная для связи
с объектом-шаблоном.
public class SceneController : MonoBehaviour {
[SerializeField] private GameObject enemyPrefab;
private GameObject _enemy; Закрытая переменная для слеже-
Порождаем нового ния за экземпляром врага в сцене.
Void Update() {
врага, только если враги
в сцене отсутствуют.
if (_enemy == null) {
_enemy = Instantiate(enemyPrefab) as GameObject; Метод, копирующий
_enemy.transform.position = new Vector3(0, 1, 0); объект-шаблон.
float angle = Random.Range(0, 360);
_enemy.transform.Rotate(0, angle, 0);
}
}
}

Присоедините этот сценарий к объекту-контроллеру, и на панели Inspector появится


поле для шаблона врага с именем Enemy Prefab. Оно работает аналогично общедоступ-
ным переменным, но есть и важное отличие.

ВНИМАНИЕ  Для ссылки на объекты в  редакторе Unity я  рекомендую закрытые переменные


с  атрибутом SerializeField, так как нам нужно отобразить поле новой переменной на панели
Inspector, но при этом не хотелось бы, чтобы ее значение могли менять другие сценарии. Как объяс-
нялось в главе 2, открытые переменные отображаются на панели Inspector по умолчанию (другими
словами, их сериализацией занимается Unity), поэтому в большинстве руководств и примеров для
всех сериализованных значений фигурируют общедоступные переменные. Но эти переменные могут
быть модифицированы другими сценариями (ведь они являются общедоступными); в большинстве
же случаев редактирование значений должно разрешаться только через панель Inspector.
3.4. Увеличение количества врагов   77

Перетащите шаблон врага с вкладки Project на пустое поле переменной; при наведении
на него указателя мыши вы должны увидеть, как поле подсвечивается, демонстрируя
допустимость присоединения объекта (рис. 3.8). После присоединения к шаблону врага
сценария SceneController воспроизведите сцену, чтобы посмотреть, как работает код.
Враг, как и раньше, будет возникать в центре комнаты, но если вы его застрелите, на
его месте появится новый враг. Это намного лучше, чем единственный враг, который
умирает навсегда!
СОВЕТ  Перетаскивание объектов на поля переменных панели Inspector — весьма удобный прием,
используемый в  самых разных сценариях. В  данном случае мы связали шаблон со сценарием, но
можно связывать между собой объекты сцены и даже отдельные компоненты (так как код в этом
конкретном компоненте должен вызывать открытые методы). В следующих главах мы еще не раз
воспользуемся этим приемом.

Перетащите шаблон со вкладки Project


на поле соответствующей переменной
панели Inspector

Рис. 3.8. Процедура соединения шаблона со сценарием

Центральной частью сценария является метод Instantiate() , поэтому обратите


внимание на содержащую его строку. Созданные экземпляры шаблона появляются
в сцене. По умолчанию метод Instantiate() возвращает новый объект обобщенного
типа Object, который сам по себе практически бесполезен, поэтому его следует об-
рабатывать как объект GameObject. В языке C# приведение типов осуществляется
при помощи ключевого слова as (указывается исходный объект, ключевое слово as
и желаемый новый тип).
Полученный экземпляр сохраняется в закрытой переменной _enemy типа GameObject
(снова напоминаю о разнице между шаблоном экземпляра и самим экземпляром; пере-
менная enemyPrefab хранит в себе шаблон, в то время как переменная _enemy — экзем-
пляр этого шаблона). Инструкция if, проверяющая сохраненный объект, гарантирует,
что метод Instantiate() будет вызван только при пустой переменной _enemy (или на
языке кода — при равенстве этой переменной значению null). Изначально эта пере-
менная пуста, соответственно, код создания экземпляра запускается в самом начале
сеанса. Возвращенный методом Instantiate() объект затем сохраняется в переменной
_enemy, блокируя повторный запуск кода создания экземпляров.
78    Глава 3. Добавляем в игру врагов и снаряды

После попадания объект-враг разрушается, переменная _enemy становится пустой, что


приводит к вызову метода Instantiate(). Благодаря этому враг всегда присутствует
в сцене.

РАЗРУШЕНИЕ ИГРОВЫХ ОБЪЕКТОВ И УПРАВЛЕНИЕ ПАМЯТЬЮ ____________


Тот факт, что существующие ссылки при разрушении объекта начинают указывать на значение
null, является до некоторой степени неожиданным. В языках программирования с автоматическим
управлением памятью, к которым относится C#, мы, как правило, не можем напрямую удалять
объекты; можно обнулить все ведущие на них ссылки, после чего удаление объекта произойдет
автоматически. Это верно и в Unity, но фоновый способ обработки объектов GameObject выглядит
в Unity так, как если бы удаление совершалось напрямую.
Для отображения объектов в Unity ссылки на все эти объекты должны присутствовать в графе
сцены. Соответственно, даже после удаления всех ссылок на конкретный игровой объект в коде на
него все равно будет ссылаться граф сцены, что сделает автоматическое удаление невозможным.
Поэтому в Unity существует метод Destroy(), заставляющий игровой движок удалять объект из
графа сцены. Как часть этой фоновой функциональности в Unity также перегружается оператор ==
и начинает возвращать значение true при проверке на наличие значения null. Технически объ-
ект все еще находится в памяти, но при этом он может больше не существовать, поэтому Unity
показывает его равенство значению null. Это можно проверить, вызвав для уничтоженного
объекта метод GetInstanceID().
Впрочем, разработчики Unity обдумывают замену этого поведения более стандартным вариантом
управления памятью. Если подобное произойдет, придется поменять и код порождения врагов,
указав вместо проверки (_enemy==null) новый параметр, например (_enemy.isDestroyed).
Следите за новостями в социальной сети Facebook по адресу https://www.facebook.com/unity3d/
posts/10152271098591773.
(Если большая часть данного примечания осталась вам непонятной, просто не обращайте на
него внимания; это было лирическое отступление для пользователей, заинтересованных в не-
понятных деталях.)

3.5. Стрельба путем создания экземпляров


Хорошо, добавим врагам еще немного способностей. Пойдем по тому же пути, что
и при работе над игроком. Первым делом мы научили врагов двигаться, теперь дадим
им возможность стрелять! Как я упоминал, знакомя вас с приемом испускания луча,
этот прием является всего лишь одним из подходов к реализации стрельбы. Другой
вариант реализации — создание экземпляров из шаблона. Воспользуемся им, чтобы за-
ставить врагов отстреливаться. Результат, который нужно получить, показан на рис. 3.9.

Рис. 3.9. Враг бросает в игрока «огненный шар»


3.5. Стрельба путем создания экземпляров   79

3.5.1. Шаблон снаряда


Если раньше мы стреляли без применения реальных снарядов, то теперь они нам
понадобятся. Стрельба приемом испускания лучей, по сути, мгновенна, и попадание
регистрируется в момент щелчка кнопкой мыши, враги же будут бросать «огненные
шары», летающие по воздуху. Разумеется, хотя они двигаются довольно быстро,
у игрока будет возможность уклониться. Фиксировать попадания мы будем не методом
испускания лучей, а методом распознавания столкновений (это уже знакомый вам
метод, не дающий игроку проходить сквозь стены).
Код будет порождать огненные шары тем же способом, каким порождаются вра-
ги, — созданием экземпляров из шаблона. Как вы знаете из предыдущего раздела,
первым шагом в подобных случаях является создание объекта, который послужит
основой для шаблона. Так как нам требуется огненный шар, выберите в меню
GameObject команду 3D Object и в дополнительном меню команду Sphere. Присвойте
появившейся сфере имя Fireball. Теперь создайте новый сценарий с таким же именем
и присоедините его к объекту. Чуть позже мы напишем для него код, а пока оставьте
вариант, предлагаемый по умолчанию, так как первым делом мы завершим работу
над снарядом. Чтобы он напоминал огненный шар, ему нужно присвоить ярко-оран-
жевый цвет. Такие свойства поверхностей, как цвет, контролируются при помощи
материалов.

ОПРЕДЕЛЕНИЕ  Материалом (material) называется пакет информации, определяющий свойства


поверхности любого трехмерного объекта, к которому этот пакет присоединен. К свойствам поверх-
ности относятся цвет, блеск и даже небольшая шероховатость.

Выберите в меню Assets команду Create и в дополнительном меню команду Material.
Присвойте новому материалу имя Flame. Выберите его на вкладке Project, чтобы
увидеть его свойства на панели Inspector. Как показано на рис. 3.10, щелкните на
цветовой ячейке с именем Albedo (это технический термин, означающий основной
цвет поверхности). В отдельном окне откроется палитра цветов; переместите рас-
положенный справа ползунок и точку в основной области таким образом, чтобы
выбрать оранжевый цвет.

Щелкните на цветовой Выберите оттенок с помощью


ячейке, чтобы открыть ползунка справа и значение
палитру в основной области

Рис. 3.10. Выбор цвета материала


80    Глава 3. Добавляем в игру врагов и снаряды

Еще мы увеличим яркость материала, чтобы придать ему большее сходство с пламе-
нем. Отредактируйте параметр Emission (он тоже находится в списке атрибутов панели
Inspector). По умолчанию он равен 0, присвойте ему значение 0.3, чтобы сделать ма-
териал более ярким.
Теперь можно превратить наш огненный шар в шаблон, перетащив его со вкладки
Hierarchy на вкладку Project, как мы делали, создавая шаблон врага. Теперь у нас есть
основа для наших снарядов! Осталось написать код стрельбы.

3.5.2. Стрельба и столкновение с целью


Отредактируем шаблон врага, наделив его способностью кидать огненные шары. Чтобы
код распознавал игрока, потребуется новый сценарий (аналогично тому, как сценарий
ReactiveTarget требовался для распознавания мишеней), поэтому создадим сценарий
с именем PlayerCharacter и присоединим его к игроку. А теперь откроем сценарий
WanderingAI и введем в него код следующего листинга.

Листинг 3.11. Сценарий WanderingAI с возможностью кидать огненные шары


Эти два поля добавляются
... перед любыми методами, как
[SerializeField] private GameObject fireballPrefab; и в сценарии SceneController.
private GameObject _fireball; Игрок распознается тем же способом,
... что и мишень в сценарии RayShooter.
if (Physics.SphereCast(ray, 0.75f, out hit)) {
Та же самая логика с пустым
GameObject hitObject = hit.transform.gameObject;
игровым объектом, что
if (hitObject.GetComponent<PlayerCharacter>()) {
и в сценарии SceneController.
if (_fireball == null) {
_fireball = Instantiate(fireballPrefab) as GameObject;
_fireball.transform.position =
transform.TransformPoint(Vector3.forward * 1.5f);
_fireball.transform.rotation = transform.rotation;
}
} Поместим огненный шар
else if (hit.distance < obstacleRange) { перед врагом и нацелим
float angle = Random.Range(-110, 110); в направлении его движения.
transform.Rotate(0, angle, 0); Метод Instantiate() работает
} так же, как и в сценарии
} SceneController.
...

Думаю, вы заметили, что все примечания к этому листингу ссылаются на сходные


(или аналогичные) фрагменты предыдущих сценариев. Фактически в предыдущих
листингах вы уже видели весь код, необходимый для бросания огненных шаров; мы
просто смешали эти фрагменты друг с другом в соответствии с новым контекстом.
Как и в листинге SceneController, в верхнюю часть сценария нужно добавить два
поля GameObject: сериализованную переменную, с которой будет связываться шаблон,
и закрытую переменную для слежения за экземпляром, созданным кодом этого ша-
блона. После испускания луча идет проверка попадания в объект PlayerCharacter;
это работает аналогично тому, как код стрельбы проверял наличие у пораженного
объекта компонента ReactiveTarget. Код, создающий экземпляр огненного шара
3.5. Стрельба путем создания экземпляров   81

при отсутствии таких шаров в сцене, работает как код создания экземпляров врага.
Отличаются только размещение и ориентация объектов; на этот раз мы помещаем
экземпляр, полученный из шаблона, перед врагом и нацеливаем в направлении его
движения.
Как только новый код окажется на своем месте, на панели Inspector появится новое поле
Fireball Prefab, точно так же как в свое время для компонента SceneController появилось
поле Enemy Prefab. Щелкните на шаблоне врага на вкладке Project, и на панели Inspector
появятся компоненты этого объекта, как если бы вы выделили его в сцене.
Хотя упомянутое ранее неудобство интерфейса часто проявляется при редактировании
шаблонов, именно интерфейс дает нам возможность легко редактировать компонен-
ты объекта, чем мы сейчас и воспользуемся. Перетащите шаблон огненного шара со
вкладки Project на поле Fireball Prefab панели Inspector, как показано на рис. 3.11.

Перетаскивание шаблона со вкладки Project


на поле панели Inspector

Рис. 3.11. Соединение огненного шара со сценарием

Теперь, как только игрок окажется непосредственно перед врагом, в него полетит
огненный шар… Попробуйте выполнить воспроизведение — перед врагом появится
огненная сфера, но никуда не полетит, потому что мы пока не написали соответ-
ствующий сценарий. Давайте сделаем это сейчас. Код сценария Fireball приведен
в следующем листинге.

Листинг 3.12. Сценарий Fireball, реагирующий на столкновения


using UnityEngine;
using System.Collections;

public class Fireball : MonoBehaviour {


public float speed = 10.0f;
public int damage = 1;

void Update() {
transform.Translate(0, 0, speed * Time.deltaTime);
} Эта функция вызывается,
когда с триггером
сталкивается другой объект.
void OnTriggerEnter(Collider other) {
PlayerCharacter player = other.GetComponent<PlayerCharacter>();
if (player != null) { Проверяем, является
Debug.Log("Player hit"); ли этот другой объект
} объектом PlayerCharacter.
Destroy(this.gameObject);
}
void Update() {
transform.Translate(0, 0, speed * Time.deltaTime);
} Эта функция вызывается,
82    Глава 3. Добавляем в игру врагов и снаряды когда с триггером
сталкивается другой объект.
void OnTriggerEnter(Collider other) {
PlayerCharacter player = other.GetComponent<PlayerCharacter>();
if (player != null) { Проверяем, является
Debug.Log("Player hit"); ли этот другой объект
} объектом PlayerCharacter.
Destroy(this.gameObject);
}
}

Существенным новшеством в этом коде является метод OnTriggerEnter(). Он авто-


матически вызывается при столкновении объекта, например, со стеной или с игро-
ком. Пока что наш код работает не совсем корректно; после его запуска огненный
шар благодаря строке Translate() полетит вперед, но триггер не сработает, а вместо
этого будет запрошен новый шар путем разрушения уже существующего. Требуется
внести в компоненты огненного шара еще пару дополнений. Прежде всего, нужно
превратить коллайдер в триггер. Для этого установите флажок Is Trigger в разделе
Sphere Collider.

СОВЕТ  После превращения в триггер компонент Collider продолжит реагировать на соприкосно-


вение/перекрытие с другими объектами, но уже не будет препятствовать физическому пересечению
с этими объектами.

Огненному шару также требуется компонент Rigidbody, относящийся к системе моде-


лирования законов физики. Именно он позволит этой системе зарегистрировать триг-
геры столкновений для рассматриваемого объекта. Щелкните на кнопке Add Component
на панели Inspector и по очереди выберите команды Physics и Rigidbody. После добавления
указанного компонента сбросьте флажок Use Gravity, как показано на рис. 3.12, чтобы
на огненный шар перестала действовать сила тяжести.

Ñáðîñüòå
ýòîò ôëàæîê

Рис. 3.12. Отключение гравитации для компонента Rigidbody

Воспроизведите сцену, и вы увидите, как огненные шары разрушаются после столк­
новения с каким-либо объектом. Так как код генерации этих шаров запускается сразу
после исчезновения очередного экземпляра, обстрел игрока теперь становится не-
прерывным. Осталось добавить всего одну деталь: заставить игрока реагировать на
попадание.

3.5.3. Повреждение игрока


Ранее мы создали сценарий PlayerCharacter, но оставили его пустым. Теперь введите
в него из следующего листинга код реакции на попадание.
Заключение   83

Листинг 3.13. Игрок может получать повреждения


using UnityEngine;
using System.Collections;

public class PlayerCharacter : MonoBehaviour {


private int _health;

void Start() {
_health = 5; Инициализация переменной health.
}

public void Hurt(int damage) {


_health -= damage; Уменьшение здоровья игрока.
Debug.Log("Health: " + _health);
}
}

В листинге определяется поле для здоровья игрока, и по команде этот показатель
уменьшается. В следующих главах вы научитесь отображать на экране текстовую ин-
формацию, пока же сведения о состоянии игрока можно вывести в виде отладочного
сообщения.
Теперь нужно вернуться к сценарию Fireball, чтобы вызвать для игрока метод Hurt().
Вставьте вместо отладочной строчки в сценарии Fireball строку player. Hurt(damage),
которая будет сообщать игроку о попадании. Всё. Последний фрагмент кода встал на
свое место!
Ну что же, это была достаточно насыщенная глава с большим количеством новой
информации. В предыдущей и этой главах вы реализовали большую часть функцио-
нальности, необходимой в шутере от первого лица.

Заключение
 Луч — это спроецированная в сцену воображаемая линия.
 Как для стрельбы, так и для обнаружения объектов применяется метод бросания
лучей.
 Для моделирования хаотичных перемещений персонажа по сцене используется
базовый искусственный интеллект.
 Новые объекты генерируются путем создания экземпляров из существующего
шаблона.
 Сопрограммы позволяют растянуть функцию во времени.
4 Работа с графикой

33 Основные сведения о графических ресурсах.


33 Процесс создания геометрической модели сцены.
33 Использование в Unity двумерных изображений.
33 Импорт собственных трехмерных моделей.
33 Моделирование эффектов частиц.

Пока что мы рассматривали в основном принципы функционирования игры, не


обращая особого внимания на то, как она выглядит. И это не случайно, ведь книга
посвящена программированию игр в Unity. Тем не менее важно знать, каким обра-
зом создается и улучшается визуальная картинка. В следующей главе мы вернемся
к основной теме — написанию кода для различных частей игры, — а пока поговорим
о такой вещи, как графика, чтобы ваши проекты не превращались в набор одинаковых
с виду примитивов, плавно перемещающихся по сцене.
Все, что составляет визуальное содержимое игры, называется графическими ресурсами
(art assets). Но что именно скрывается за этим термином?

4.1. Основные сведения о графических ресурсах


Графическим ресурсом называется отдельная единица визуальной информации
(обычно — файл), используемая игрой. Это всеобъемлющее собирательное название
для всего визуального содержимого; к графическим ресурсам относятся файлы изо-
бражений, трехмерные модели и т. п. На самом деле это всего лишь частный случай
ресурса, который, как вы знаете, представляет собой любой используемый игрой файл
(например, сценарий). Все они в Unity находятся в основной папке Assets. В табл. 4.1
перечислены и описаны пять основных видов графических ресурсов, применяемых
при создании игр.
4.1. Основные сведения о графических ресурсах   85

Таблица 4.1. Типы игровых ресурсов


Тип Определение типа
Двумерное изо- Плоские изображения. В реальном мире им соответствуют картины
бражение и фотографии
Трехмерная модель Виртуальные трехмерные объекты (почти синоним для «сеточных объ-
ектов»). В реальном мире им соответствуют скульптуры
Материал Пакет информации, определяющий свойства поверхности любого объек-
та, к которому присоединен материал. К таким свойствам относят цвет,
блеск и даже небольшие шероховатости
Анимация Пакет информации, определяющий движение связанного с ним объекта.
Существуют как заблаговременно созданные детализированные последо-
вательности перемещений, так и код, вычисляющий положения объектов
«на лету»
Система частиц Механизм создания большого количества небольших движущихся объ-
ектов и управления ими. Позволяет создавать различные визуальные
эффекты, такие как огонь, дым и водяные брызги

Создание графики для новой игры в общем случае начинается с двумерных изо-
бражений или трехмерных моделей, так как эти ресурсы формируют базу для всего
остального. Как несложно догадаться, двумерные изображения служат основой для
двумерной графики, в то время как трехмерные модели — для трехмерной. Точнее
говоря, двумерные изображения представляют собой плоские картинки. Даже если
у вас нет представления о графике, используемой в играх, с ними вы, скорее всего,
сталкивались, например, на сайтах. При этом трехмерные модели могут оказаться со-
вершенно незнакомым новичкам понятием, поэтому я дам им определение.

ОПРЕДЕЛЕНИЕ  Модель (model) — это трехмерный виртуальный объект. В главе 1 вы столкнулись


с термином «сеточный объект»; трехмерная модель — это практически синоним. Оба термина часто
используются в одном и том же значении, но термин «сеточный объект» относится исключительно
к геометрии трехмерных объектов (соединенные друг с другом линии и фигуры), в то время как термин
«модель» имеет более общий смысл и часто подразумевает и другие атрибуты объекта.

Следующие два типа ресурсов в нашем списке — это материалы и анимация. В отличие
от двумерных изображений и трехмерных моделей, они не имеют смысла сами по себе,
поэтому новичкам бывает непросто понять, что это такое. Изображения и модели легко
представимы благодаря аналогам из реального мира. Первым соответствуют картины,
вторым — скульптуры. Материалы и анимация не имеют прямых ассоциаций с реаль-
ными объектами. Это абстрактные пакеты информации, накладываемые на трехмерные
модели. Например, базовый смысл материалов уже был рассмотрен в главе 3.

ОПРЕДЕЛЕНИЕ  Материалом (material) называется пакет информации, определяющий свойства


поверхности любого трехмерного объекта, к которому он присоединяется. К таким свойствам отно-
сятся, к примеру, цвет, блеск и даже небольшая шероховатость.

Если продолжить аналогию с искусством, материал можно представить как вещество


(глина, бронза, мрамор и т. п.), из которого создается скульптура. Подобным же образом
86    Глава 4. Работа с графикой

анимация представляет собой присоединяемый к видимому объекту абстрактный


слой информации.

ОПРЕДЕЛЕНИЕ  Анимацией (animation) называется пакет информации, определяющий движение


связанного с ним объекта. Так как эти движения можно задать независимо от объекта, их можно ис-
пользовать, сочетая и комбинируя с различными объектами.

В качестве конкретного примера рассмотрим перемещающийся по сцене персонаж.


Его положение в каждый момент определяется кодом игры (например, сценариями
движения, которые вы писали в главе 2). Но подробные движения ног, ступающих
по земле, размахивающих рук и поворачивающихся бедер представляют собой вос-
производимые в цикле анимационные последовательности, которые и являются
графическим ресурсом.
Чтобы понять, как связаны друг с другом анимация и трехмерные модели, представьте
себе кукольный театр: трехмерная модель будет куклой, аниматор  — кукольником,
заставляющим ее двигаться, а анимация — записью движений куклы. Определенные
таким способом движения записываются заранее и обычно происходят в небольшом
масштабе, не меняя общего положения объекта. Это отличается от крупномасштабных
перемещений, которые мы программировали в предыдущих главах.
Последний графический ресурс, упоминавшийся в табл. 4.1, — это система частиц.

ОПРЕДЕЛЕНИЕ  Системой частиц (particle system) называется механизм создания большого


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

Системы частиц служат для создания таких визуальных эффектов, как огонь, дым
или водяные брызги. Роль частиц (то есть отдельных объектов, контролируемых
системой) может играть любой сеточный объект, но для большинства эффектов до-
статочно квадрата, воспроизводящего изображение (например, искры пламени или
клуба дыма).
В основном создание графики для игры выполняется во внешних программах, никак не
связанных с Unity. В Unity можно генерировать только материалы и системы частиц.
Список внешних программ вы найдете в приложении Б; для создания трехмерных
моделей и анимации применяются самые разные графические редакторы. Получен-
ные во внешней программе трехмерные модели затем сохраняются как графические
ресурсы, то есть импортируются в Unity. Я пользуюсь программой Blender, о которой
рассказывается в приложении В. Ее можно скачать с сайта www.blender.org. Мой выбор
обусловлен тем, что это программа с открытым исходным кодом, бесплатно доступная
всем желающим.

ПРИМЕЧАНИЕ  Проект, который можно скачать для этой главы, включает в себя папку с именем
scratch. Хотя эта папка помещена в проект Unity, она не является его частью; я поместил в эту папку
дополнительные внешние файлы.

Работая над проектом этой главы, вы познакомитесь с примерами большинства типов


графических ресурсов (анимация пока относится к слишком сложным темам, поэтому
4.2. Создание геометрической модели сцены   87

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


рой фигурируют двумерные изображения, трехмерные модели, материалы и система
частиц. В некоторых случаях вы будете пользоваться уже готовыми графическими
ресурсами, импортируя их в Unity, но будут и ситуации (особенно с системой частиц),
когда графический ресурс придется создавать с нуля.
В этой главе мы только слегка затронем тему создания игровой графики. Все-таки эта
книга посвящена в основном программированию в Unity, поэтому подробное рассмо-
трение вопросов, связанных с графикой, уменьшит количество информации по глав-
ной теме. Создание игровой графики — огромная предметная область, для детального
описания которой потребуется не одна книга. В большинстве случаев программисты
работают в паре со специалистами по графике. Принимая во внимание сказанное, че-
ловек, занимающийся программированием игр, должен понимать, как Unity работает
с графическими ресурсами, и, возможно, даже уметь создавать их грубые заменители;
их еще называют программистской графикой (programmer art) и позднее (в готовой
игре) заменяют нужными графическими ресурсами.

ПРИМЕЧАНИЕ  Проекты из предыдущей главы для выполнения заданий вам не потребуются.


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

4.2. Создание геометрической модели сцены


Разговор о моделировании сцен мы начнем с рассмотрения процесса создания гео-
метрической модели сцены (whiteboxing). Обычно это первый шаг моделирования
уровня с помощью компьютера (следующий за разработкой этого уровня на бумаге).
Как следует из английского термина, объекты сцены создаются из набора примитивов,
то есть белых ящиков (white boxes). В списке различных ресурсов пустые декорации
соответствуют базовому виду трехмерной модели и являются основой для отобра-
жения двумерных картинок. Вспомните сцену, которую мы создали из примитивов
в главе 2. Это и есть геометрическая модель (просто на тот момент вы еще не знали
этого термина). В некоторых разделах вы найдете отсылки к вещам, которые мы делали
в начале главы 2, но на этот раз я буду касаться их совсем коротко, попутно сократив
обсуждение новой терминологии.

ПРИМЕЧАНИЕ  В английском языке также используется термин grayboxing (gray boxes — серые
ящики). Он также означает геометрическую модель сцены,но я предпочитаю слово whiteboxing, потому
что узнал его раньше. Реальный цвет примитивов может различаться, точно так же как светокопии,
называемые «синьками» (blueprints), далеко не всегда имели синий цвет.

4.2.1. Назначение геометрической модели


Составление сцены из примитивов практикуется по двум причинам. Во-первых, это
позволяет быстро получить «набросок», который со временем будет постепенно совер-
шенствоваться. Эта деятельность близко связана с проектированием игровых уровней.
88    Глава 4. Работа с графикой

ОПРЕДЕЛЕНИЕ  Проектирование уровней (level design) — это дисциплина, касающаяся плани-


рования и создания в игре сцен (или уровней). Проектировщик уровней — это тот, кто занимается
проектированием уровней.

По мере увеличения количества разработчиков в группе и сужения специализации


каждого отдельного члена группы создание первой версии каждого игрового уровня
в виде геометрической модели отдается на откуп проектировщикам уровней. Затем
эта заготовка передается специалистам по графике для визуальной доработки. Но
даже в маленькой группе, где за проектирование уровней и работу с графикой может
отвечать один и тот же человек, такая последовательность действий является оптималь-
ной. В конце концов, нужно с чего-то начинать, а геометрическая модель становится
хорошей основой для создания визуальных эффектов.
Второй причиной использования геометрических моделей является возможность бы-
стро привести сцену в подходящее для игры состояние. Она может быть не окончена
(более того, уровень, на котором построена только геометрическая модель, очень далек
от завершения), но это уже функциональная версия, поддерживающая игровой про-
цесс. Как минимум, игрок в состоянии перемещаться по сцене (вспомните демонстра-
ционный ролик из главы 2). Можно провести тестирование и убедиться, что игровой
уровень построен корректно (например, комнаты имеют подходящий для игры размер),
и только после этого тратить время и энергию на его проработку. Если окажется, что
чего-то не хватает (скажем, вы поняли, что требуется больше места), несложно внести
изменения и провести повторное тестирование на стадии геометрической модели.
Более того, возможность поиграть даже на стадии конструирования уровня хорошо
поднимает моральный дух. Не сбрасывайте это преимущество со счетов. Создание
богатой в визуальном отношении сцены может занять долгое время, и вы почувствуете
усталость от того, что никак не можете воспользоваться плодами своего труда. Гео-
метрическая модель сразу дает вам готовый (хотя и примитивный) игровой уровень
и возможность играть в постоянно совершенствующуюся игру.
Итак, теперь, когда вы понимаете, почему разработки всех уровней начинаются с гео-
метрических моделей, давайте приступим к созданию игры!

4.2.2. Рисуем план уровня


Созданию игрового уровня на компьютере предшествует его проектирование на бу-
маге. Мы не будем подробно обсуждать тему проектирования уровней; достаточно
сделанного в главе 2 примечания по поводу проектирования игр. Проектирование
уровней (представляющее собой разновидность проектирования игр) — это обширная
область знаний, достойная отдельной книги. Мы же нарисуем базовый план уровня,
чтобы обозначить цель, к которой нужно стремиться.
Рисунок 4.1 демонстрирует вид сверху простого помещения, состоящего из четырех
комнат и центрального коридора. Это все, что нам на данный момент нужно: набор
отдельных областей и внутренние стены, которые требуется установить на свои места.
План реальной игры будет содержать больше деталей, например, таких, как враги
и фрагменты обстановки. Попрактиковаться в создании геометрической модели сцены
можно как на примере этого плана, так и взяв за основу собственные идеи. Конкретное
4.2. Создание геометрической модели сцены   89

расположение комнат в этом упражнении не имеет значения. Важно только наличие


у вас нарисованного плана, что позволит перейти к следующему этапу.

Стена

Начальное
положение
игрока

Рис. 4.1. План игрового уровня: четыре комнаты и центральный коридор

4.2.3. Расставляем примитивы в соответствии с планом


Построение геометрической модели сцены в соответствии с имеющимся планом
включает в себя позиционирование и масштабирование множества параллелепипе-
дов, которые будут играть роль стен. Как описано в разделе 2.2.1, выберите в меню
GameObject команду 3D Object, а затем команду Cube, чтобы получить куб для дальнейших
преобразований.

ПРИМЕЧАНИЕ  При желании вместо кубов вы можете воспользоваться объектом QuadsBox,


скачанным вместе с проектом. Он представляет собой куб, созданный из шести частей, что дает до-
полнительную гибкость при назначении материала. Выбор в данном случае зависит только от вашего
желания; лично я не стал пользоваться объектом QuadsBox, так как вся геометрия со временем все
равно заменяется графикой.

Первым объектом сцены станет пол; на панели Inspector поменяйте имя примитива
и его положение по координате Y на –0.5, как показано на рис. 4.2. Это делается для
компенсации высоты объекта. Затем растяните куб по осям X и Z.

Имя объекта

Положение объекта
(он слегка опущен вниз Масштабирование
для компенсации по осям X и Z
толщины)

Рис. 4.2. Параметры куба на панели Inspector после перемещения и масштабирования


90    Глава 4. Работа с графикой

Повторите эти шаги, чтобы получить стены. Скорее всего, вы захотите привести в по-
рядок вкладку Hierarchy, сделав стены потомками общего базового объекта (напоминаю,
что такой объект нужно поместить в точку с координатами 0, 0, 0, а затем на вкладке
Hierarchy перетащить на него остальные объекты), но это действие не является обяза-
тельным. И не забудьте расположить в сцене несколько простых источников света,
чтобы видеть окружающее пространство. В главе 2 вы уже узнали, что для создания
источника света нужно выбрать его тип в дополнительном меню, которое появляется
после выбора команды Light в меню GameObject. Примерный вид вашего уровня после
этих операций показан на рис. 4.3.

Объект Player

Комната (намеченная
внутренними стенами) Источник света
(в уровне их будет несколько)

Рис. 4.3. Геометрическая модель игрового уровня, построенного по плану,


представленному на рис. 4.1

Заставьте объект, представляющий игрока или камеру, двигаться по сцене (для соз-
дания игрока воспользуйтесь контроллером персонажа и сценариями движения —
эта тема подробно объяснялась в главе 2). Теперь можно походить по сделанной из
примитивов сцене, чтобы протестировать получившийся уровень! Все очень просто,
но пока у вас есть только чистая геометрия. Декорируем ее с помощью двумерных
изображений.

ЭКСПОРТ ГЕОМЕТРИЧЕСКИХ МОДЕЛЕЙ В ДРУГИЕ ПРОГРАММЫ ____________


Большая часть работы по визуальному оформлению сцены выполняется во внешних приложениях
для работы с трехмерной графикой, например в программе Blender. Удобнее всего это делать,
загрузив во внешнюю программу свою геометрическую модель. Хотя по умолчанию возможность
экспорта скомпонованных примитивов в Unity отсутствует, существуют сценарии, позволяющие
добавить в редактор такую функциональность. Большинство из них дают возможность выделить
в сцене всю геометрию и щелкнуть на кнопке Export (я упоминал их в главе 1, в разделе, по-
священном настройкам редактора). Эти сценарии обычно экспортируют геометрию как OBJ-файл
(этот тип файлов обсуждается чуть позже). На сайте Unity3D щелкните на кнопке поиска и вве-
дите запрос obj exporter. Или можете посмотреть пример такого сценария на странице http://wiki.
unity3d.com/index.php?title=ObjExporter.

4.3. Наложение текстур


Пока что наш уровень представляет собой грубый набросок. Он уже доступен для игры,
но очевидно, что над внешним видом сцены требуется еще долго работать. Следующим
шагом по совершенствованию уровня будет наложение текстур.
4.3. Наложение текстур   91

ОПРЕДЕЛЕНИЕ  Текстурой (texture) называется двумерное изображение, применяемое для


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

В трехмерной графике текстуры имеют разное применение, но наиболее простым


является отображение их на поверхности трехмерных моделей. Позднее вы узнаете,
как это происходит в случае сложных моделей, сейчас же мы, по сути, покроем стены
обоями, как показано на рис. 4.4.

Перед наложением текстур После наложения текстур


(только тени от источников света) (одной на пол, другой на все стены)

Рис. 4.4. Внешний вид игрового уровня до и после наложения текстур

Как видно из рисунка, текстура превращает откровенно нереалистичную цифровую


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

4.3.1. Выбор формата файла


Для сохранения двумерных изображений существует множество форматов. Какой из
них лучше выбрать? Форматы, поддерживаемые Unity, перечислены в табл. 4.2.

ОПРЕДЕЛЕНИЕ  Альфа-канал (alpha channel) служит для хранения информации о прозрачности


изображения. Видимые цвета поступают по трем «каналам»: красному, зеленому и синему. Альфа —
это дополнительный невидимый канал, управляющий прозрачностью изображения.

Хотя Unity допускает импорт и использование в качестве текстур изображений всех


перечисленных в табл. 4.2 форматов, форматы значительно различаются по количеству
поддерживаемых функций. Для файлов, импортируемых в качестве текстур, особенно
важны два фактора: как именно сжимается изображение и есть ли у него альфа-канал.
С альфа-каналом все просто: так как он часто используется в трехмерной графике,
лучше, чтобы у изображения он был. Объяснить важность разных аспектов сжатия
чуть сложнее. Впрочем, объяснение можно свести к фразе «сжатие с потерями — это
плохо». Изображения, используемые без сжатия и допускающие сжатие без потерь,
сохраняют свое качество, в то время как при сжатии с потерями качество падает по
мере уменьшения размера файла.
92    Глава 4. Работа с графикой

Таблица 4.2. Форматы файлов двумерных изображений, поддерживаемые Unity


Формат Достоинства и недостатки

PNG Повсеместно используется в интернете. Сжатие без потерь; есть альфа-канал


JPG Повсеместно используется в интернете. Сжатие с потерями; нет альфа-канала
GIF Повсеместно используется в интернете. Сжатие с потерями; нет альфа-канала.
(Технически потери возникают не из-за сжатия, а в результате преобразования
к 8-битному изображению. Но конечный результат все равно один и тот же)
BMP Формат, по умолчанию используемый в Windows. Применяется без сжатия; нет
альфа-канала
TGA Повсеместно применяется в трехмерной графике; во всех прочих областях малоиз-
вестен. Используется без сжатия, но возможно и сжатие без потерь; есть альфа-
канал
TIFF Повсеместно применяется в цифровой фотографии и издательском деле. Использу-
ется без сжатия, но возможно и сжатие без потерь; нет альфа-канала
PICT Формат по умолчанию на старых компьютерах Mac. Сжатие с потерями; нет альфа-
канала
PSD Собственный формат Photoshop. Используется без сжатия; есть альфа-канал. Ос-
новным достоинством является возможность непосредственной работы с файлами
в Photoshop

С учетом этих соображений я рекомендовал бы использовать в качестве текстур в Unity


файлы формата PNG или TGA. Формат Targas (TGA) был любимым вариантом для
создания текстур, пока в интернете не получил распространение формат PNG. В наше
время PNG с технологической точки зрения является практически эквивалентом фор-
мата TGA, но получил большее распространение благодаря применению в качестве
текстур в интернете. К числу рекомендуемых форматов относится также PSD, потому
что удобно работать с одним и тем же файлом как в Photoshop, так и в Unity. Хотя
я предпочитаю хранить рабочие файлы отдельно от «готовых» экспортированных
в Unity вариантов (аналогичных взглядов я придерживаюсь касательно хранения
трехмерных моделей, но об этом речь пойдет позже).
В результате все изображения, представленные в примере проекта, имеют формат PNG,
и я рекомендую вам работать именно с ним. А теперь пришло время импортировать
в Unity несколько изображений и применить их к объектам сцены.

4.3.2. Импорт файла изображения


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

ОПРЕДЕЛЕНИЕ  Бесшовное изображение (tileable image) представляет собой рисунок, края ко-
торого совпадают друг с другом. Именно это позволяет повторять его на поверхности без видимых
швов в  местах соединения. Концепция назначения текстур в  трехмерном моделирования сходна
с использованием фоновых рисунков на веб-страницах.
4.3. Наложение текстур   93

Получить бесшовное изображение можно различными способами: например, обрабо-


тать фотографию или нарисовать собственный вариант картинки. Учебные пособия
и объяснения можно найти в различных книгах и на сайтах, но сейчас мы не будем
тратить на это время. Вместо этого мы воспользуемся изображениями с сайтов, пред-
лагающих наборы графики для трехмерного моделирования. Например, показанные на
рис. 4.5 текстуры я скачал с сайта www.textures.com. Именно их я собираюсь назначить
полу и стенам; вы можете выбрать собственные картинки, подходящие, по вашему
мнению, для этой цели.

Рис. 4.5. Бесшовные текстуры камня и кирпичей, скачанные с сайта textures.com

Скачайте выбранные вами изображения и подготовьте их к использованию в качестве


текстур. С технической точки зрения ничто не мешает задействовать их сразу, но в ис-
ходном виде они далеки от идеала. Разумеется, они являются бесшовными (именно
поэтому мы их и скачали), но рисунок имеет некорректный размер, а файл — не тот
формат, который нам нужен. Размер текстуры должен выражаться в степенях двойки.
Графические процессоры показывают максимальную эффективность при обработке
изображений, размер которых выражается числом 2N: 4, 8, 16, 32, 64, 128, 256, 512, 1024,
2048 (следующее в этом ряду — число 4096, но это слишком большое изображение,
чтобы использовать его в качестве текстуры). В графическом редакторе (это может
быть Photoshop, GIMP или любой другой вариант из перечисленных в приложении Б)
отмасштабируйте скачанные изображения до размера 256 × 256 и сохраните их в фор-
мате PNG.
Теперь перетащите эти файлы из папки на вашем компьютере на вкладку Project в Unity,
как показано на рис. 4.6. Это действие позволит скопировать их в Unity-проект, после
чего их можно будет использовать в трехмерной сцене. Если перетаскивать файлы
вам по каким-то причинам неудобно, щелкните правой кнопкой мыши на вкладке
Project и выберите в появившемся меню команду Import New Asset, чтобы открыть окно
выбора файлов.

СОВЕТ  По мере усложнения проектов имеет смысл распределить ресурсы по отдельным папкам;
на вкладке Project создайте папки для сценариев и текстур и перетащите в них соответствующие
ресурсы.
94    Глава 4. Работа с графикой

Папка на вашем компьютере

Вкладка Project в Unity

Рис. 4.6. Перетащите изображения на вкладку Project, чтобы импортировать их в Unity

ВНИМАНИЕ  В Unity есть несколько ключевых слов, совпадающих с именами папок. Они ини-
циируют обработку содержимого этих папок специальным образом. Это ключевые слова Resources,
Plugins, Editor и Gizmos. Зачем нужны эти папки, вы узнаете позже, а пока просто избегайте этих слов,
выбирая имена для своих папок.

Теперь изображения импортированы в Unity как текстуры и готовы к использованию.


Как же назначить их объектам сцены?

4.3.3. Назначение текстуры


С технической точки зрения наложить текстуру непосредственно на геометрию
невозможно. Текстуры должны входить в состав материалов, которые, собственно,
и назначаются объектам. Как объяснялось во введении, материалом называется пакет
информации, описывающий свойства поверхности; эта информация может включать
в себя и отображаемую текстуру. Подобный подход имеет смысл, так как позволяет
использовать одну и ту же текстуру для разных материалов. Но так как обычно все
текстуры фигурируют в составе разных материалов, для удобства в Unity можно просто
поместить текстуру на объект, в результате новый материал создается автоматически.
Если вы перетащите текстуру с вкладки Project на объект сцены, как показано на рис. 4.7,
Unity создаст новый материал и назначит его объекту. Попытайтесь таким образом
получить материал для пола.
Кроме этого удобного метода автоматического создания материалов существует еще
и «корректный» способ через подменю, которое появляется после выбора в меню Assets
команды Create; новый ресурс появляется на вкладке Project. Остается только выделить
полученный новый материал, чтобы его свойства отобразились на панели Inspector, и,
как показано на рис. 4.8, перетащить текстуру на ячейку с именем Albedo (это техниче-
ский термин для базового цвета). Перетащите полученный материал со вкладки Project
на объект сцены. Попробуйте проделать все описанное с текстурой для стены: создайте
новый материал, перетащите в него текстуру и назначьте его стене. Вы увидите, что на
поверхности пола и стен появились изображения камня и кирпичей, но они выглядят
4.4. Создание неба с помощью текстур   95

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


растянутым на весь пол? Мы же хотели, чтобы оно повторялось на поверхности не-
сколько раз. Такой эффект дает свойство Tiling: выделите материал на вкладке Project
и измените числа в полях Tiling на панели Inspector (существуют отдельные значения для
координат X и Y, отвечающие за количество повторений в каждом направлении). Про-
верьте, что вы задаете повторение основной, а не вторичной карты (данный материал
поддерживает вторичную карту текстуры для усовершенствованных эффектов). По
умолчанию число повторений равно 1 (то есть единственная текстура растягивается на
всю поверхность); присвойте этому параметру, к примеру, значение 8 и посмотрите, как
изменится вид пола. Подберите и для второго материала кратность, обеспечивающую
оптимальный вид. Итак, пол и стены нашей комнаты обзавелись текстурами! Но вы
можете назначить текстуру и небу; давайте посмотрим, как это делается.

Рис. 4.7. Одним из способов наложения текстур является их перетаскивание


со вкладки Project на объекты сцены

Рис. 4.8. Выделите материал для просмотра его свойств на панели Inspector
и перетащите структуру на ячейку одного из свойств

4.4. Создание неба с помощью текстур


Текстуры камня и кирпича придали стенам и полу намного более естественный вид.
Но небо пока выглядит пустым и ненатуральным, мы же хотим придать сцене реали-
стичность. Чаще всего эта задача решается при помощи специальных текстур с изо-
бражениями неба.
96    Глава 4. Работа с графикой

4.4.1. Что такое скайбокс?


По умолчанию камера показывает фоновый темно-синий цвет. Он заполняет все пустое
пространство сцены (например, пространство над стенами). Но в качестве фона можно
визуализировать и изображение неба. В этом нам поможет скайбокс.

ОПРЕДЕЛЕНИЕ  Скайбоксом (skybox) называется окружающий камеру куб, на грани которого


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

Корректная реализация скайбокса — дело непростое; принцип его работы иллюстри-


рует рис. 4.9. Существует ряд приемов, позволяющих отобразить грани куба как уда-
ленный фон. К счастью, все детали реализации в Unity уже учтены.

Скайбокс — необходимая функциональность.


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

Рис. 4.9. Схема скайбокса

Новые сцены создаются с уже готовым скайбоксом. Именно поэтому вместо равно-
мерного темно-синего фона цвет неба постепенно меняется от светлого к темно-синему.
Если открыть окно диалога с параметрами освещенности (выбрав в меню Window коман­
ду Lighting), первым вы увидите параметр Skybox со значением Default. Этот параметр
находится в свитке Environment Lighting; окно диалога разделено на свитки, связанные
с усовершенствованной системой освещения в Unity. Впрочем, пока нас интересует
только самый первый параметр.
Текстуры для скайбокса, как и текстуры кирпича и камня, можно найти на различных
сайтах. Воспользуйтесь поисковым запросом текстуры для скайбокса (skybox textures).
Например, я нашел несколько прекрасных вариантов на сайте www.93i.de, в том числе
набор TropicalSunnyDay. После добавления к скайбоксу текстуры неба сцена начнет
выглядеть так, как показано на рис. 4.10.
Как и прочие текстуры, изображения для скайбокса сначала назначаются материалу
и только потом используются в сцене. Давайте попробуем создать для скайбокса но-
вый материал.
4.4. Создание неба с помощью текстур   97

Рис. 4.10. Сцена с фоновым изображением неба

4.4.2. Создание нового материала для скайбокса


Сначала создайте новый материал (как обычно, щелкнув правой кнопкой мыши
и выбрав команду Create или выбрав эту же команду в меню Assets).Его параметры
отобразятся на панели Inspector. Первым делом нам нужно поменять шейдер матери-
ала. В верхней части списка настроек находится меню Shader, показанное на рис. 4.11.
В разделе 4.3 мы не обращали на него внимания, так как шейдер, предлагаемый по
умолчанию, подходит большинству стандартных текстур, но скайбокс требует дру-
гого варианта.

ОПРЕДЕЛЕНИЕ  Шейдером (shader) называется короткая программа с инструкциями, описываю-


щими способ рисования поверхности. В ней указываются, в частности, используемые текстуры. Ком-
пьютер задействует эти инструкции для вычисления пикселов в процессе визуализации изображения.
В наиболее распространенном шейдере цвет материала затемняется в соответствии с освещенностью.
Шейдеры применяются для всех видов визуальных эффектов.

Каждый материал имеет шейдер, который определяет его вид (можно представить
материал как экземпляр шейдера). Новому материалу по умолчанию назначается
шейдер Standard. Он отображает цвет материала (включая назначенную текстуру),
одновременно применяя к поверхности основные настройки теней и освещенности.
Для скайбоксов используется другой шейдер. Щелкните на этом меню, чтобы открыть
выпадающий список с перечнем доступных шейдеров. Выделите строку Skybox и вы-
берите в появившемся дополнительном меню вариант 6 Sided, как показано на рис. 4.11.
Теперь в настройках материала появилось шесть ячеек для текстур (вместо одной ма-
ленькой ячейки Albedo, которую мы видели у стандартного шейдера). Эти шесть текстур
соответствуют шести сторонам куба. Они должны совпадать друг с другом в местах
стыка, чтобы картинка получилась бесшовной. Например, рис. 4.12 демонстрирует
изображения для солнечного скайбокса.
Импортируйте в Unity изображения для скайбокса тем же способом, которым импорти-
ровалась текстура кирпича: перетащите файлы на вкладку Project или щелкните правой
кнопкой мыши на вкладке Project и выберите команду Import New Asset. Впрочем, в данном
случае есть одно небольшое отличие; щелчком выделите импортированную текстуру,
98    Глава 4. Работа с графикой

чтобы увидеть ее свойства на панели Inspector, и поменяйте значение параметра Wrap


Mode с  Repeat на Clamp (рис. 4.13); не забудьте после этого щелкнуть на кнопке Apply.
Обычно текстуры укладываются на поверхность как плитки, а чтобы результат такой
укладки выглядел бесшовным, противоположные края изображений накладываются
друг на друга. Но в случае неба подобная операция может привести к появлению не-
больших линий, поэтому значение Clamp (аналогичное знакомой вам по главе 2 функции
Clamp()) очертит границы текстуры и уберет результат их наложения.

Свойство
Shader
у материала

Шейдер
Skybox в меню

Рис. 4.11. Выпадающее меню доступных раскрасок

Изображения для скайбокса с сайта 93i.de: верхнее, нижнее, фронтальное, заднее, левое, правое

Рис. 4.12. Шесть сторон скайбокса

На границах …поэтому поменяйте


изображений неба значение параметра
могут появиться Wrap Mode с Repeat
небольшие линии… на Clamp

Рис. 4.13. Избавление от линий путем редактирования параметра Wrap Mode

Теперь можно перетащить изображения на ячейки для текстур. Имена изображений


должны совпадать с именами ячеек (например, left или front). Как только все текстуры
окажутся на своих местах, можно использовать материал для скайбокса. Снова от-
кройте окно с параметрами освещенности и перетащите новый материал на ячейку
Skybox или щелкните на маленьком кружке с точкой в центре, расположенном справа
от ячейки, чтобы открыть окно выбора материала.
4.5. Собственные трехмерные модели   99

СОВЕТ  По умолчанию Unity отображает скайбокс (или, по крайней мере, его основной цвет) на
вкладке Scene редактора. Если это мешает редактированию объектов, видимость скайбокса можно
отключить. В верхней части вкладки Scene располагаются кнопки, управляющие видимостью раз-
личных элементов; щелчок на крайней правой кнопке, которая называется Effects, открывает меню,
через которое можно отключить видимость скайбокса.

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

4.5. Собственные трехмерные модели


В предыдущем разделе мы накладывали текстуры на большие плоские стены и пол.
А что делать с более детализированными объектами? Предположим, мы хотим об-
ставить комнаты мебелью. Для решения этой задачи нам потребуется внешнее при-
ложение для работы с трехмерной графикой. Вспомните определение, которое было
дано в начале этой главы: трехмерные модели — это помещенные в игру сеточные
объекты (то есть трехмерные фигуры). В этом разделе мы импортируем в игру сетку,
имеющую форму скамейки.
Для моделирования трехмерных объектов широко применяются такие приложения,
как Maya от Autodesk и 3ds Max. Но это дорогие коммерческие инструменты, поэтому
мы воспользуемся приложением с открытым исходным кодом, которое называется
Blender. Скачанный с сайта проект включает в себя файл с расширением .blend, ко-
торым вы можете воспользоваться; рис. 4.14 демонстрирует модель скамейки в про-
грамме Blender. На случай, если вы захотите научиться моделировать такие объекты
собственными руками, в приложение В включено упражнение по созданию скамейки.

Модель состоит как из определяющей ее форму


трехмерной сетки, так и из наложенной на эту сетку текстуры

Рис. 4.14. Модель скамейки в программе Blender

Кроме моделей, созданных лично вами или сотрудничающим с  вами художником,


можно скачивать подходящие варианты со специальных сайтов. К примеру, существует
такой замечательный ресурс, как Asset Store: https://www.assetstore.unity3d.com.
100    Глава 4. Работа с графикой

4.5.1. Выбор формата файла


Созданную в приложении Blender модель требуется экспортировать. Как и в случае
с двумерными изображениями, существует множество разных форматов экспорта,
каждый из которых обладает своими достоинствами и недостатками. Поддерживаемые
в Unity форматы трехмерных файлов перечислены в табл. 4.3.

Таблица 4.3. Форматы файлов трехмерных моделей, поддерживаемые в Unity


Формат Достоинства и недостатки
FBX Сетки и анимация; рекомендуемый формат
Collada (DAE) Сетки и анимация; еще один хороший вариант, если формат FBX недоступен
OBJ Только сетки; это текстовый формат, который иногда используется для транс-
ляции в интернет
3DS Только сетки; достаточно старый и примитивный формат
DXF Только сетки; достаточно старый и примитивный формат
Maya Работает через FBX; требует установки этого приложения
3ds Max Работает через FBX; требует установки этого приложения
Blender Работает через FBX; требует установки этого приложения

Выбор варианта сводится к поддержке анимации. Так как единственными удовлетво-


ряющими этому условию вариантами являются Collada и FBX, выбирать приходится
между ними. Когда есть такая возможность (не все инструменты для работы с трех-
мерной графикой экспортируют данные в этом формате), лучше всего пользоваться
форматом FBX, в противном случае подойдет и формат Collada. К счастью, приложение
Blender допускает экспорт файлов в формате FBX.
Обратите внимание, что в нижней части табл. 4.3 перечислено несколько приложений
для работы с 3D-графикой. Инструмент Unity позволяет прямо переносить их фай-
лы в ваши проекты, что сначала кажется удобным, но эта функциональность имеет
несколько подводных камней. Во-первых, Unity не загружает непосредственно сами
файлы. Модель загружается в фоновом режиме, затем загружается экспортированный
файл. Но так как модель в любом случае экспортируется в формате FBX или Collada,
лучше делать это в явном виде. Во-вторых, для подобной операции у вас должно
быть установлено соответствующее приложение. Если вы планируете организовать
доступ к файлам с разных компьютеров (например, для группы разработчиков), это
обстоятельство становится большой проблемой. Я не рекомендую загружать файлы
из приложения Blender (или Maya, или еще откуда-то) напрямую в Unity.

4.5.2. Экспорт и импорт модели


Итак, пришло время экспортировать модель из Blender и импортировать ее в Unity. Пер-
вым делом откройте файл со скамейкой в приложении Blender и выберите в меню File
команду Export4FBX. Сохраненный файл импортируйте в Unity тем же способом, каким
осуществлялся импорт изображений. Перетащите FBX-файл на вкладку Project или
щелкните на этой вкладке правой кнопкой мыши и выберите команду Import New Asset.
Трехмерная модель, готовая к вставке в сцену, будет скопирована в Unity-проект.
4.5. Собственные трехмерные модели   101

ПРИМЕЧАНИЕ  В доступный для скачивания пример проекта включен файл с расширением.blend,


чтобы вы могли попрактиковаться в экспорте FBX-файлов из приложения Blender; даже если вы не
хотите ничего моделировать самостоятельно, зачастую требуется конвертировать скачанные файлы
в доступный для Unity формат. Если вы предпочитаете пропустить все шаги, связанные с приложе-
нием Blender, задействуйте имеющийся FBX-файл.

При импорте моделей желательно сразу же поменять несколько параметров. Unity мас-
штабирует импортируемые модели до очень маленьких размеров (на рис. 4.15 показано,
что вы увидите на панели Inspector, выделив такую модель), поэтому введите в поле
Scale Factor значение 100, чтобы частично скомпенсировать параметр File Scale, равный
0,01. Можно также установить флажок Generate Colliders (Генерировать коллайдеры),
но это необязательно; просто без коллайдера персонажи смогли бы проходить сквозь
скамейку. Затем перейдите на вкладку Animation в параметрах импорта и сбросьте фла-
жок Import Animation (Импорт анимации) — ведь эту модель мы анимировать не будем.

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

Рис. 4.15. Редактирование параметров импорта трехмерной модели

Это обеспечит нам корректный импорт сетки. Теперь что касается текстуры. При
импорте FBX-файла инструмент Unity заодно создал материал для скамейки. По
умолчанию он пустой (как любой новый материал), поэтому назначьте ему текстуру
(показанную на рис. 4.16) тем же способом, каким вы назначали текстуру кирпича
стене: перетащите изображение на вкладку Project, чтобы импортировать его в Unity,
а затем — на ячейку текстуры в настройках материала скамейки. Изначально изо-
бражение будет выглядеть странновато, его разные части окажутся на разных частях
скамейки, поэтому текстурные координаты были отредактированы для приведения
изображения в соответствие с сеткой.

Изображение соотносится с моделью


с помощью «текстурных координат»

Концепция текстурных координат


объясняется в приложении В

Рис. 4.16. Двумерное изображение, служащее текстурой для скамейки


102    Глава 4. Работа с графикой

ОПРЕДЕЛЕНИЕ  Текстурными координатами (texture coordinates) называется дополнительный


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

ПРИМЕЧАНИЕ  Даже если вы не собираетесь вникать в тонкости моделирования скамейки, имеет


смысл ознакомиться с  информацией о  текстурных координатах (см. приложение В). Понимание
этой концепции (как и связанных с ней понятий UV-координат и проецирования) пригодится при
программировании игр.

Новые материалы часто имеют слишком сильный блеск, поэтому имеет смысл умень-
шить параметр Smoothness до нуля (чем более гладкой является поверхность, тем
сильнее она блестит). После этого скамейку можно поместить в сцену. Перетащите
модель с вкладки Project в одну из комнат. Как только вы поставите объект на место,
вы увидите нечто, напоминающее рис. 4.17. Поздравляю, вы создали для игрового
уровня текстурированную модель!

ПРИМЕЧАНИЕ  В этой главе мы не будем этим заниматься, но, как правило, геометрическая
модель сцены позднее заменяется моделями, созданными во внешних программах. Новая модель
может выглядеть почти идентично старой, но предоставлять большую гибкость при назначении
текстуре координат U и V.

Рис. 4.17. Импортированная в уровень скамейка

СИСТЕМА АНИМАЦИИ ПЕРСОНАЖЕЙ MECANIM_ ________________________


Созданная нами модель статична. Но вы можете анимировать ее в приложении Blender и вос-
произвести эту анимацию в Unity. Процесс создания анимации длинный и сложный, а эта книга
посвящена совсем другой теме, поэтому здесь мы его обсуждать не будем. Как уже упоминалось,
существует множество ресурсов для детального знакомства с  трехмерной анимацией. Просто
имейте в  виду  — это крайне обширная тема. Именно поэтому анимацией при разработке игр
занимается отдельный специалист. В Unity встроена сложная система управления анимацией мо-
делей. Она называется Mecanim. Это новая усовершенствованная система, недавно добавленная
в Unity взамен старой. Впрочем, старая версия пока доступна, хотя и может быть удалена при
4.6. Системы частиц   103

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

4.6. Системы частиц


Кроме двумерных изображений и трехмерных моделей у нас остался еще один тип
визуального содержимого — системы частиц. Данное в начале этой главы определение
поясняет, что системами частиц называются механизмы создания большого количества
движущихся объектов и управления ими. Системы частиц применяются при создании
таких эффектов, как огонь, дым или водяные брызги. Например, огонь на рис.  4.18
получен именно при помощи системы частиц.
Если большинство других ресурсов создается во внешних приложениях и импорти-
руется в проект, системы частиц генерируются непосредственно в Unity благодаря
гибким и мощным инструментам для создания различных эффектов.

ПРИМЕЧАНИЕ  Почти как в случае с системой анимации Mecanim, в Unity соседствовали старая
и более новая системы частиц. Последняя называлась Shuriken. В настоящее время старая система
полностью вышла из употребления, так что отдельное имя новой системе уже не требуется.

Рис. 4.18. Эффект огня, полученный при помощи системы частиц

Для начала создайте систему частиц и выполните воспроизведение, чтобы посмо-


треть, как эффект выглядит по умолчанию. В меню GameObject выберите команду
Particle System, и вы увидите, как из нового объекта полетят вверх белые пушинки.
Для прекращения этого процесса достаточно снять с объекта выделение. При вы-
делении системы частиц в нижнем правом углу экрана появляется панель воспро-
изведения эффекта, на которой, в частности, можно посмотреть, сколько времени
работает система (рис. 4.19).
104    Глава 4. Работа с графикой

Остановка или перезагрузка воспроизведения


эффекта частиц в сцене

Щелкните на метке Playback Time


и перетаскивайте ее в стороны,
меняя направление движения частиц

Рис. 4.19. Панель воспроизведения системы частиц

В принципе, эффект уже хорошо выглядит, но давайте рассмотрим список параметров,


которыми вы можете пользоваться для его дополнительной настройки.

4.6.1. Редактирование параметров эффекта


Рисунок 4.20 демонстрирует полный список вариантов настройки системы частиц. Мы
не будем рассматривать каждый параметр этого списка, а остановимся только на том,
что требуется для создания эффекта пламени. Как только вы поймете принцип рабо-
ты, остальное окажется достаточно очевидным. Каждая из надписей скрывает целый
информационный свиток. Изначально раскрыт только первый из них; все остальные
свернуты. Чтобы развернуть свиток, щелкните на его названии.
СОВЕТ  Изрядное количество параметров редактируется с помощью кривой, отображаемой в ниж-
ней части панели Inspector. Она показывает изменение параметра во времени: левая сторона графика
соответствует первому появлению частицы, правая — моменту ее исчезновения. Нижняя часть со-
ответствует значению 0, а верхняя — максимально возможному значению. Меняйте форму кривой
перетаскиванием точек, вставляя новые точки двойным щелчком или щелчком правой кнопки мыши.

Отредактируйте параметры системы частиц в соответствии с рис. 4.20, чтобы придать


ей сходство с факелом.

4.6.2. Новая текстура для пламени


Теперь наша система частиц больше напоминает факел, но нужно придать частицам
вид пламени, а не шариков. Для этого нам потребуется импортировать в Unity еще одно
изображение. На рис. 4.21 показана нарисованная мной картинка: я поставил оран-
жевую точку и воспользовался инструментом Smudge для имитации языков пламени
(затем я проделал то же самое с желтым цветом). Вне зависимости от того, возьмете
вы изображение из примера проекта, нарисуете собственный вариант или скачаете
подходящую картинку из интернета, первым делом его нужно импортировать в Unity.
Как я уже объяснял, перетащите картинку на панель Project или выберите в меню Assets
команду Import New Asset.
Системе частиц, как и трехмерной модели, невозможно назначить текстуру напрямую;
поэтому добавьте эту текстуру к материалу и назначьте его системе частиц. Создайте
новый материал и перетащите текстуру со вкладки Project на ячейку Albedo на панели
4.6. Системы частиц   105

Looping: система частиц работает без остановки;


оставьте установленный по умолчанию флажок

Lifetime: время жизни каждой частицы; уменьшите до 3

Speed: скорость перемещения частицы; уменьшите до 1

Size: размер частиц; оставьте значение по умолчанию

Rotation: ориентация частицы; щелкните на стрелке и выберите


в меню вариант Between Constants, укажите значения 0 и 180

Color: цвет частиц. Нам требуется темно-оранжевый,


например, со значениями RGB 182, 101, 58
Локальное пространство эффекта прекрасно подходит
для статичных систем частиц, но для движущейся системы
лучше выбрать значение World
Emission: скорость генерации новых частиц; оставьте
значение по умолчанию

Shape: форма области, испускающей частицы. По умолчанию


это широкий конус, нам же для получения компактного факела
нужен куб (выберите вариант Box, все значения 0.2)
Size over Lifetime: в процессе движения частица увеличивается
и уменьшается. По умолчанию эта настройка отключена.
Включите ее и сформируйте кривую, которая быстро
увеличивается от 0, а затем медленно уменьшается обратно
до 0 (как и показано на рисунке)

Rotation over Lifetime: в процессе движения частица вращается.


По умолчанию эта настройка отключена. Включите ее, выберите
вариант Random Between и присвойте значения –80 и 80, чтобы
частицы вращались в разных направлениях

Renderer: задает вид каждой частицы. Вы можете выбрать даже


значение Mesh, но оставьте вариант Billboard и перетащите
на ячейку новый материал (его мы сделаем позже)

Точки к кривой добавляются двойным щелчком или щелчком


правой кнопкой мыши и выбором команды Add Key

Рис. 4.20. Панель Inspector отображает варианты настройки системы частиц


(в данном случае — огня)

Рис. 4.21. Изображение, используемое для частиц пламени


106    Глава 4. Работа с графикой

Inspector. Это свяжет текстуру с материалом, после чего его можно будет добавить
к системе частиц. Эта процедура показана на рис. 4.22. Выделите систему частиц,
раскройте свиток Renderer в нижней части списка настроек и перетащите материал на
ячейку Material.

Рис. 4.22. Назначение материала системе частиц

Как и в случае материала для скайбокса, вам нужно выбрать другой вариант шейдера.
Щелчком откройте меню Shader в верхней части настроек материала, чтобы увидеть
список доступных раскрасок. Вместо используемого по умолчанию стандартного ва-
рианта материал для частиц нуждается в одном из вариантов шейдера, перечисленных
в дополнительном меню Particles. Как показано на рис. 4.23, на этот раз мы выбираем
вариант Additive (Soft). Это заставит частицы опалесцировать и освещать сцену, как на-
стоящий огонь.

Рис. 4.23. Выбор шейдера материала для частиц пламени

ОПРЕДЕЛЕНИЕ  Вариант шейдера Additive добавляет цвет частицы к  цвету фона за ней, а  не
заменяет пикселы. Благодаря этому пикселы становятся ярче, а черный цвет делается невидимым.
Противоположный эффект оказывает вариант Multiply, делающий все вокруг темнее; эти шейдеры
производят тот же самый эффект, что и эффекты слоя Additive и  Multiply в приложении Photoshop.

После назначения системе частиц материала, имитирующего огонь, мы получили


нужный эффект (см. рис. 4.18). Факел выглядит достаточно убедительно, причем
он в состоянии работать не только в статичном положении. Чтобы убедиться в этом,
давайте присоединим его к движущемуся объекту.
4.6. Системы частиц   107

4.6.3. Присоединение эффектов частиц к трехмерным объектам


Создайте сферу (напоминаю, что в  меню GameObject нужно выбрать команду 3D
Object4Sphere). Создайте новый сценарий с именем BackAndForth, показанный в следую-
щем листинге, и присоедините его к сфере.

Листинг 4.1. Движение объекта взад и вперед по прямой


using UnityEngine;
using System.Collections;

public class BackAndForth : MonoBehaviour {


public float speed = 3.0f;
public float maxZ = 16.0f; Объект движется между этими точками.
public float minZ = -16.0f;

private int _direction = 1; В каком направлении объект движется в данный момент?

void Update() {
transform.Translate(0, 0, _direction * speed * Time.deltaTime);

bool bounced = false;


if (transform.position.z > maxZ || transform.position.z < minZ) {
_direction = -_direction; Меняем направление на противоположное.
bounced = true;
}
if (bounced) {
transform.Translate(0, 0, _direction * speed * Time.deltaTime);
} Делаем дополнительное движение
} в этом кадре, если объект поменял
} направление.

Запустите этот сценарий, и сфера начнет двигаться взад и вперед по центральному ко-
ридору уровня. Теперь можно сделать систему частиц дочерней по отношению к сфере,
и огонь начнет перемещаться вместе с ней. Точно так же, как вы поступали со стенами,
на вкладке Hierarchy перетащите объект Particle system на объект Sphere.

ВНИМАНИЕ  Обычно после того, как объект становится потомком другого объекта, его положение
требуется обнулить. К примеру, нам нужно поместить систему частиц в точку 0, 0, 0 (относительно ее
предка). В Unity сохраняется положение, которое объект имел до формирования иерархической связи.

Теперь система частиц двигается вместе со сферой, но пламя при этом не отклоняет-
ся, что выглядит неестественно. Это связано с тем, что по умолчанию частицы пере-
мещаются корректно только в локальном пространстве собственной системы. Для
завершения модели горящей сферы найдите в настройках системы частиц параметр
Simulation Space (он находится в верхнем свитке, показанном на рис. 4.20) и измените
его значение с Local на World.

ПРИМЕЧАНИЕ  Наш объект перемещается по прямой, но обычно в играх используются более


замысловатые траектории. В Unity поддерживаются сложные варианты навигации и пути; прочитать
об этом можно на странице https://docs.unity3d.com/ru/current/Manual/Navigation.html.
108    Глава 4. Работа с графикой

Уверен, что вам не терпится реализовать собственные идеи и добавить в игру новые
объекты. Займитесь этим сейчас — можете создать больше графических ресурсов или
проверить свои способности, добавив в сцену разработанный в главе 3 механизм стрель-
бы. В следующей главе мы перейдем к другому игровому жанру и начнем создание
новой игры. Впрочем, несмотря на подобные переходы, вся информация из первых
четырех глав вполне применима и может оказаться вам полезной.

Заключение
 Термин «графические ресурсы» означает совокупность всей задействованной
в проекте графики.
 Создание графической модели сцены является для проектировщика уровней пер-
вым шагом по разметке игрового пространства.
 Текстуры и  двумерные изображения отображаются на поверхности трехмерных
моделей.
 Трехмерные модели создаются вне Unity и импортируются в виде FBX-файлов.
 Системы частиц позволяют создавать многочисленные визуальные эффекты
(огонь, дым, воду и т. п.).
Часть II
ОСВАИВАЕМСЯ
Вы создали первый прототип игры и готовы расширить арсенал рабочих
инструментов на примерах других игровых жанров. К настоящему моменту
приемы работы в Unity должны быть полностью освоены: вы уверенно соз-
даете сценарии с указанными функциями, перетаскиваете объекты на ячейки
панели Inspector и т. п. Я больше не буду подробно останавливаться на деталях
интерфейса, так как вы уже не нуждаетесь в повторении основ.
Давайте создадим несколько дополнительных проектов, чтобы проиллюстри-
ровать новые приемы разработки игр в Unity.
5 Двумерная игра Memory

33 Отображение двумерной графики в Unity.


33 Создание интерактивных объектов.
33 Программная загрузка изображений.
33 Поддержка и отображение состояния с помощью текстового пользовательского
интерфейса.
33 Загрузка уровней и перезапуск игры.

До сих пор вы имели дело только с трехмерной графикой. Но в Unity можно исполь-
зовать и двумерную графику, и в этой главе вы увидите, как это делается, на примере
создания двумерной игры. Это классическая детская игра Memory: карты выкладыва-
ются рубашкой вверх, переворачиваются щелчком и считается количество совпадений.
В процессе вы познакомитесь с основами разработки двумерных игр в Unity.
Изначально Unity предназначался для создания трехмерных игр, но у него есть и дру-
гие варианты применения. Начиная с версии 4.3, выпущенной в конце 2013 года, в Unity
появилась возможность отображения двумерной графики, хотя и раньше с помощью
этого инструмента разрабатывали двумерные игры (особенно мобильные, в создании
которых помогла кросс-платформенная природа Unity). Изначально для эмуляции
двумерной графики в трехмерных сценах требовался сторонний фреймворк (напри-
мер, 2D Toolkit от Unikron Software). В конечном счете основной редактор и игровой
движок поменяли, встроив в него двумерную графику. Именно с этой функциональ-
ностью и знакомит данная глава.
Рабочий процесс при создании двумерной и трехмерной графики в Unity пример-
но одинаков и включает в себя импорт графических ресурсов, перетаскивание их
в сцену и  написание сценариев, которые затем будут присоединены к  объектам.
Основной вид ресурсов, необходимых для создания двумерной графики, называется
спрайтом.
5.1. Подготовка к работе с двумерной графикой   111

ОПРЕДЕЛЕНИЕ  Спрайтом (sprite) называется отображаемое непосредственно на экране двумер-


ное изображение, в то время как такие же изображения, отображаемые на поверхности трехмерных
моделей, называются текстурами.

Импорт спрайтов по большей части напоминает импорт текстур (см. главу 4). С техни-


ческой точки зрения спрайты представляют собой объекты в трехмерном пространстве,
обладающие плоской поверхностью и ориентированные относительно оси Z. Они по-
вернуты в одном направлении, поэтому камеру можно нацелить прямо на них, при этом
игроки смогут наблюдать только перемещения вдоль осей X и Y (то есть в плоскости).
Координатные оси обсуждались в главе 2. Надеюсь, вы помните, что для получения
третьего измерения добавляется ось Z, перпендикулярная уже знакомым вам осям X
и Y. Именно этими осями представлены два измерения, с которыми мы будем работать.

5.1. Подготовка к работе с двумерной графикой


Мы собираемся воспроизвести классическую игру Memory. Опишем ее для тех, кто не
знаком с правилами. Набор парных карт разложен рубашкой вверх. Их расположение
игроку неизвестно. Он переворачивает карты по две, пытаясь найти совпадающие.
Не совпавшие карты переворачиваются назад, а игрок делает следующую попытку.
Макет игры показан на рис. 5.1; сравните его с планом, который мы составляли в главе 2.

Счет: 1

Счет — количество Кнопка перезапуска игры


совпавших карт

Карты изначально разложены рубашкой вверх


и переворачиваются щелчком мыши

Рис. 5.1. Макет игры Memory

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

5.1.1. Подготовка проекта


Первым делом нужно отобразить всю графику. Как и при создании трехмерного
демонстрационного ролика, начнем с помещения в сцену игровых объектов, а затем
напишем программы, определяющие их функциональность.
112    Глава 5. Двумерная игра Memory

То есть нужно создать все объекты с рис. 5.1: рубашку карт, их лицевую сторону, в одном
углу игрового пространства — табло, на котором будет отображаться количество на-
бранных очков, а в другом — кнопку перезагрузки. Еще нужен фон. Весь необходимый
арсенал показан на рис. 5.2.

Рубашка карты Фон


(верхняя часть игрального стола)

Лицевая часть карт


(4 разных символа)

Кнопка начала
новой игры

Рис. 5.2. Графические ресурсы для игры Memory

СОВЕТ  Готовый проект со всеми графическими ресурсами можно скачать со страницы https://www.
manning.com/books/unity-in-action-second-edition посвященного этой книге сайта. Скопируйте данные
там изображения и используйте их в своем проекте.

Соберите все требуемые изображения и создайте в Unity новый проект. В нижней части
окна есть пара кнопок (рис. 5.3) для переключения между 2D- и 3D-режимами. В преды-
дущих главах вы работали только с трехмерной графикой, а так как именно этот режим
создания проектов используется по умолчанию, мы не обращали на данную настройку
внимания. Сейчас же нужно переключиться в режим работы с двумерной графикой.

Кнопка
выбора
режима 2D

Рис. 5.3. Эти кнопки позволяют создавать как двумерные, так и трехмерные проекты

РЕЖИМ 2D ДЛЯ РЕДАКТОРА И ВКЛАДКИ SCENE_________________________


Выбирая двумерный или трехмерный режим при создании нового проекта, мы влияем на пару
настроек Unity, которые позднее всегда можно поменять. Это режим работы самого редактора
и режим отображения вкладки Scene. Последний определяет способ показа сцены в Unity; кнопка
перехода от одного режима к другому находится в верхней части вкладки Scene.

Кнопка на вкладке Scene, меняющая способ отображения


5.1. Подготовка к работе с двумерной графикой   113

Для изменения режима работы редактора откройте меню Edit, наведите указатель мыши на строчку
Project Settings и выберите в дополнительном меню вариант Editor. Среди различных настроек
на панели Inspector появится раскрывающийся список Default Behavior Mode, в котором можно
выбрать вариант 3D или 2D.

Список Default Behavior Mode, открываемый командой Edit > Project Settings > Editor

В двумерном режиме все изображения импортируются как спрайты; в  главе  4 вы видели, что
в обычном режиме они превращаются в текстуры. Кроме того, в режиме 2D сцены лишены настроек
освещения. Оно просто не требуется. Если когда-нибудь вы захотите получить трехмерную сцену
без освещения, удалите автоматически создаваемый источник света и отключите скайбокс (щелк­
ните на маленьком кружочке рядом с полем выбора файла и выберите в списке вариант None).

Итак, мы создали новый проект, выбрав для него режим 2D. Пришло время добавить
в сцену изображения.

5.1.2. Отображение спрайтов


Выполните импорт всех изображений, перетащив их на вкладку Project; убедитесь,
что изображения импортированы как спрайты, а не как текстуры. Для этого нужно
выделить любую картинку и посмотреть настройки ее импорта на панели Inspector,
а именно поле Texture Type. Затем перетащите спрайт table_top (фоновое изображе-
ние) со вкладки Project в пустую сцену. Сохраните сцену. Как и в случае с сеточными
объектами, у выделенного спрайта на панели Inspector окажется компонент Transform;
присвойте его полям значения 0, 0, 5, чтобы поменять положение фонового изо-
бражения.

СОВЕТ  Еще одна настройка импорта, на которую следует обратить внимание: Pixels To Units.
Так как к трехмерному движку Unity двумерную графику добавили относительно недавно, одна
единица Unity далеко не всегда соответствует одному пикселу изображения. Можно выбрать
вариант  1:1, но я  рекомендую оставить значение по умолчанию  100:1 (физический движок при
отображении  1:1 работает не совсем корректно, а  вариант по умолчанию обеспечивает лучшую
совместимость с остальным кодом).
114    Глава 5. Двумерная игра Memory

СОЗДАНИЕ АТЛАСОВ_ _____________________________________________


В нашем проекте будут использоваться только одиночные изображения, но в одно изображение
можно объединить и  набор спрайтов. Когда таким способом комбинируются кадры двумерной
анимации, итоговый набор называется листом спрайтов. Но для подобной структуры существует
еще один, более общий термин — атлас (atlas).
Анимированные спрайты часто встречаются в двумерных играх и будут реализованы в следующей
главе. Набор кадров можно импортировать в виде отдельных изображений, но в играх все кадры
анимации обычно существуют в виде листа спрайтов. По сути, все кадры выглядят как сетка на
одном большом изображении.
Атласы применяются не только для объединения кадров анимации, но и для статичных изобра-
жений. Дело в том, что они оптимизируют производительность: 1) уменьшая количество пустого
пространства в изображениях путем их плотной упаковки, 2) уменьшая количество вызовов отри-
совки (draw calls) видеокарты (каждое новое загруженное изображение означает дополнительную
нагрузку на видеокарту).
Для создания атласов спрайтов применяются внешние инструменты, например TexturePacker
(см.  приложение  Б), и  это прекрасно работает. Но в  Unity существует упаковщик спрайтов,
автоматически соединяющий друг с другом наборы изображений. По умолчанию он отключен
и  включается в  настройках редактора, для доступа к  которым нужно выбрать в  меню Edit
коман­ду Project Settings, а затем Editor. После этого следует указать имя в поле Packing Tag. Оно
находится среди настроек импорта спрайта. Помеченные одним тегом спрайты Unity упакует
в атлас. Более подробно эта тема освещена на странице: https://docs.unity3d.com/ru/current/
Manual/SpritePacker.html.

Несложно понять, почему мы приравняли к 0 координаты X и Y. Ведь спрайт должен
заполнить весь экран, поэтому его нужно расположить в центре. А вот присвоение
значения 5 координате Z может показаться странным. Разве для двумерной графики
имеют значение какие-либо координаты, кроме X и Y? Дело в том, что эти координаты
определяют положение плоского объекта на экране, а координата Z вступает в игру,
когда объекты нужно положить друг на друга. Чем меньше ее значение, тем ближе
к камере находится спрайт (рис. 5.4). Соответственно, у спрайта, служащего фоном,
значение Z должно быть максимальным. Мы присвоили ему положительную координа-
ту Z, у а всех остальных объектов она будет иметь нулевое или отрицательное значение.

Положенные друг на друга


Трехмерная (перспективная) проекция
спрайты в двумерной
(ортографической) проекции
Спрайты

Ось Z

Рис. 5.4. Расположение спрайтов относительно оси Z


5.1. Подготовка к работе с двумерной графикой   115

Положение остальных спрайтов будет указываться с точностью до двух знаков после


запятой из-за упоминавшегося выше параметра Pixels To Units. Соотношение 100:1 озна-
чает, что 100 пикселов изображения соответствуют 1 измерительной единице в Unity;
или, если посмотреть с другой стороны, 1 пиксел равен 0,01 единицы. Но перед тем,
как мы начнем добавлять в сцену новые спрайты, нужно настроить камеру.

5.1.3. Переключение камеры в двумерный режим


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

ВНИМАНИЕ  Режим вкладки Scene не имеет никакого отношения к тому, что показывает камера
запущенной игры.

Дело в том, что настройки игровой камеры не зависят от режима вкладки Scene. Зачастую
это выгодно: например, вы можете вернуть вкладку Scene в трехмерный режим для работы
над какими-то эффектами. Но именно поэтому то, что вы видите в редакторе Unity, вовсе
не эквивалентно тому, что вы можете увидеть в игре, и новички об этом часто забывают.
Самая важная настройка камеры называется Projection. Скорее всего, наша камера уже
обладает нужной проекцией, ведь мы создали новый проект в двумерном режиме. Но
важно знать, как проверяются параметры камеры. Выделите камеру на вкладке Hierarchy,
чтобы ее настройки появились на панели Inspector, как показано на рис. 5.5. Для работы
с трехмерной графикой параметр Projection должен иметь значение Perspective, двумерная
же графика требует значения Orthographic.

Фоновый цвет

Перспективная/
Ортографическая
проекция

Размер камеры
(половина высоты экрана)

Рис. 5.5. Настройки камеры при работе с двумерной графикой

ОПРЕДЕЛЕНИЕ  Ортографическим называется изображение с  камеры, не имеющей видимой


перспективы. Этим ортографическая камера отличается от перспективной, в которой ближе распо-
ложенные объекты выглядят больше и уменьшаются с увеличением расстояния до камеры.

В данном случае важный параметр Projection в редактировании не нуждается, чего не ска-


жешь о прочих настройках камеры. Обратите внимание на параметр Size; он определяет
116    Глава 5. Двумерная игра Memory

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

Предположим, нужно попасть пиксел в пиксел при разрешении экрана 1024 × 768. Это
означает, что высота камеры должна составить 384 пиксела. Поделив это число на 100
(с учетом соотношения пикселов и единиц измерения), получим размер камеры, рав-
ный 3,84. То есть вычисления ведутся по формуле РАЗМЕР ЭКРАНА / 2 /100f (f указывает
на значение типа float, а не типа int). Так как наше фоновое изображение имеет размер
1024 × 768 (для проверки выделите этот ресурс), очевидно, что для камеры идеально
подойдет значение 3,84.
Еще на панели Inspector следует поменять фоновый цвет камеры и ее положение по
координате Z. Как уже упоминалось при рассмотрении спрайтов, чем больше значение
координаты Z, тем дальше расположен объект. Поэтому отодвинем камеру подальше
в область отрицательных значений Z, например 0, 0, -100. В качестве фонового цвета, на-
верное, лучше всего выбрать черный; по умолчанию фон имеет синий цвет, и, если экран
окажется шире фонового изображения (что вполне вероятно), синие полоски по сторонам
будут выглядеть странно. Щелкните на образце цвета Background и выберите черный цвет.
Сохраните сцену под именем Scene и нажмите кнопку Play; вы увидите игровое поле,
заполненное спрайтом, изображающим поверхность стола. Думаю, вы согласитесь, что
путь до этой точки был не совсем очевидным (напомню, что причиной этого является
трехмерный игровой движок Unity, в который только недавно добавили возможность
работы с двумерной графикой). Итак, пустая поверхность стола готова. Можно вы-
кладывать на нее карты.

5.2. Создание карт и превращение


их в интерактивные объекты
Итак, мы импортировали и подготовили к работе все нужные изображения, можно
приступать к созданию основных игровых объектов — карт. В игре Memory они вы-
кладываются рубашкой вверх и переворачиваются на короткий момент после щелчка
на паре карт. Подобной функциональностью обладают объекты, состоящие из поло-
женных друг на друга спрайтов. Для них потребуется написать код, заставляющий
карты переворачиваться по щелчку мыши.

5.2.1. Объект из спрайтов


Перетащите в сцену одну карту. Используйте изображение лицевой стороны, так как
поверх будет добавлена другая карта, скрывающая рисунок первой. С технической
точки зрения положение этого объекта пока не имеет значения, но в конечном счете
карта должна будет оказаться в определенной точке, поэтому присвойте полям Position
5.2. Создание карт и превращение их в интерактивные объекты   117

значения -3, 1, 0. Перетащите в сцену спрайт card_back и сделайте его потомком пре-
дыдущего спрайта. Напоминаю, что для этого на вкладке Hierarchy нужно перетащить
дочерний объект на родительский. Установите для нового спрайта положение 0, 0, -0.1.
Это положение относительно предка, то есть фактически мы поместили рубашку карты
в точку с теми же координатами X и Y, но чуть ближе по координате Z.
СОВЕТ  Вместо инструментов Move, Rotate и Scale, которыми мы пользовались в трехмерном режиме,
теперь доступен единый инструмент манипуляции Rect Tool. Он автоматически активируется после
перехода в двумерный режим. Кроме того, его можно включить нажатием крайней правой кнопки на
навигационной панели в верхнем левом углу Unity. Когда этот инструмент активен, перетаскивание
объектов приводит к выполнению всех трех операций (перемещения/поворота/масштабирования)
в двух измерениях.

Рубашка карты положена на свое место, как показано на рис. 5.6, и все готово для
создания интерактивного объекта, реагирующего на щелчок мыши.

Рубашка карты является потомком спрайта с лицевой стороной карты

Она располагается немного


впереди родительского спрайта

(Помните, что это локальное


положение относительно
родителя)

Рис. 5.6. Иерархическое связывание и местоположение спрайта с рубашкой карты

5.2.2. Код ввода с помощью мыши


Чтобы карта реагировала на щелчки мыши, требуется такой компонент, как коллайдер.
У новых спрайтов он отсутствует, поэтому щелкать на них бесполезно. Мы присоединим
коллайдер к корневому объекту, а не к рубашке карты, соответственно, к щелчкам будет
восприимчива лицевая сторона. Выделите корневой объект на вкладке Hierarchy (щелкнув
на карте в сцене, вы выделите рубашку, так как она располагается сверху) и щелкните на
кнопке Add Component в нижней части панели Inspector. Выделите строку Physics 2D (строка
Physics относится к физике трехмерных игр) и выберите вариант Box collider.
Теперь нужен сценарий, который заставит карту реагировать на действия игрока.
Создайте сценарий с именем MemoryCard.cs и присоедините его к корневой карте (а не
к рубашке). Вот как выглядит код, заставляющий карту сгенерировать отладочное
сообщение в ответ на щелчок мыши.

Листинг 5.1. Генерация отладочных сообщений по щелчку


using UnityEngine;
using System.Collections;

public class MemoryCard : MonoBehaviour {


public void OnMouseDown() { Эта функция вызывается после щелчка на объекте.
Debug.Log("testing 1 2 3"); Пока мы только генерируем текстовое
} сообщение, выводимое на консоль.
}
118    Глава 5. Двумерная игра Memory

СОВЕТ  Если у  вас пока нет такой привычки, приучайте себя распределять ресурсы по папкам;
создайте для сценариев отдельную папку и перетащите туда все сценарии. Это можно сделать непо-
средственно на вкладке Project. Избегайте имен, на которые реагирует Unity: Resources, Plugins, Editor
и Gizmos. О назначении этих папок мы поговорим позднее, а пока просто помните, что пользователь-
ские папки так называть нельзя.

Мы можем щелкать на карте! Подобно методу Update(), функция OnMouseDown() также


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

5.2.3. Открытие карты по щелчку


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

Листинг 5.2. Сценарий, скрывающий рубашку после щелчка на карте


using UnityEngine;
using System.Collections; Переменная, которая появляется
на панели Inspector.
public class MemoryCard : MonoBehaviour {
[SerializeField] private GameObject cardBack; Код деактивации запускается
public void OnMouseDown() { только в случае, когда объект
if (cardBack.activeSelf) { активен/видим.
cardBack.SetActive(false); Делаем объект неактивным/невидимым.
}
}
}

В этом сценарии есть два ключевых дополнения: ссылка на объект сцены и деактиви-
рующий данный объект метод SetActive(). Из предыдущих глав вы уже знаете, как
сделать ссылку: переменная помечается как сериализованная, а затем на эту перемен-
ную на панели Inspector перетаскивается объект со вкладки Hierarchy. После этого код
начнет влиять на объект сцены.
Метод SetActive, которому было передано значение false, деактивирует любой объект
GameObject, делая его невидимым. Так что если теперь перетащить объект card_back
на переменную этого сценария на панели Inspector, в процессе игры карта начнет ис-
чезать после щелчка на ней. Исчезнувшая рубашка открывает лицевую сторону карты;
как видите, мы решили еще одну важную задачу! Но на столе пока всего одна карта,
давайте исправим этот недостаток.

5.3. Отображение набора карт


Мы написали программу, которая отображает сначала рубашку карты, а после щелчка
на ней — лицевую сторону. Но карта пока только одна, а для игры требуется целый на-
бор с разными изображениями. Давайте разложим карты. Для этого мы воспользуемся
5.3. Отображение набора карт   119

парой уже знакомых вам по предыдущим главам концепций, а также познакомим


вас с новыми понятиями. В главе 3 вы научились, во-первых, применять невидимый
компонент SceneController, во-вторых, создавать экземпляры объекта. Именно этот
компонент позволит сопоставить различным картам разные изображения.

5.3.1. Программная загрузка изображений


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

Листинг 5.3. Тестовый код, демонстрирующий изменение картинки спрайта


...
[SerializeField] private Sprite image; Ссылка на загружаемый ресурс Sprite.
void Start() {
GetComponent<SpriteRenderer>().sprite = image; Сопоставляем спрайт
} компоненту SpriteRenderer.
...

После сохранения этого сценария на панели Inspector появилась новая переменная,


которая была указана как сериализованная. Перетащите на ячейку Image какой-либо
спрайт со вкладки Project (выберите одно из лицевых изображений, кроме того, кото-
рое уже есть в сцене). Запустите воспроизведение сцены, и вы увидите на карте новое
изображение.
Чтобы понять смысл кода, вспомним, что представляет собой компонент Sprite­
Renderer. На рис. 5.7 мы видим, что рубашка карты имеет всего два компонента:
стандартный компонент Transform, присутствующий у всех объектов сцены, и новый
компонент SpriteRenderer. Он делает из объекта спрайт и определяет, какой ресурс
будет отображен на этом спрайте. Обратите внимание, что первое свойство этого
компонента называется Sprite и связано с одним из спрайтов на вкладке Project; этим
свойством можно управлять программно, что, собственно, и происходит внутри на-
шего сценария.

Ресурс Sprite, отображаемый


на выделенном объекте Sprite

Цвет, которым подсвечивается этот объект Sprite


(по умолчанию белый, без оттенка)

Рис. 5.7. Компонент SpriteRenderer, присоединенный к объекту-спрайту


120    Глава 5. Двумерная игра Memory

Работая с компонентом CharacterController и нашими сценариями в предыдущих


главах, вы видели, что метод GetComponent() возвращает другие компоненты для одного
и того же объекта, поэтому воспользуемся им для ссылки на объект SpriteRenderer.
Свойству Sprite объекта SpriteRenderer можно присвоить любой ресурс, поэтому
в коде ему присваивается объявленная в верхней части переменная Sprite (в редакторе
мы заполнили ее одним из спрайтов).
Как видите, это не слишком сложно! Но это всего одно изображение, а у нас их четы-
ре. Поэтому удалите новый фрагмент кода из листинга 5.3 (он был нужен только для
демонстрации), чтобы подготовиться к следующему разделу.

5.3.2. Выбор изображения из компонента SceneController


В главе 3 мы создавали невидимый объект для управления генерацией объектов. Вос-
пользуемся этим приемом еще раз, но теперь невидимый объект будет контролировать
более абстрактные, не связанные с объектами сцены свойства. Для начала создайте
пустой объект GameObject (для этого в меню GameObject нужно выбрать команду Create
Empty). На вкладке Project создайте сценарий SceneController.cs и перетащите этот
ресурс на контроллер GameObject. Первым делом добавьте содержимое следующего
листинга (листинг 5.4) в сценарий MemoryCard вместо того, что было в листинге 5.3.

Листинг 5.4. Новые открытые методы в сценарии MemoryCard.cs


...
[SerializeField] private SceneController controller;

private int _id; Добавленная функция чтения (идиома


public int id { из таких языков, как C# и Java).
get {return _id;}
} Открытый метод, которым могут пользо-
ваться другие сценарии для передачи
public void SetCard(int id, Sprite image) { указанному объекту новых спрайтов.
_id = id;
GetComponent<SpriteRenderer>().sprite = image; Строка SpriteRenderer, совсем
} как в удаленном примере кода.
...

В отличие от предыдущего листинга, изображения спрайта задаются методом SetCard()


вместо метода Start(). Это открытый метод, принимающий спрайт в качестве параме-
тра, поэтому его можно вызывать из других сценариев и устанавливать изображение
для объекта. Обратите внимание, что метод SetCard() принимает в качестве параметра
ID и код его сохраняет. Пока этот параметр не нужен, но скоро мы напишем код срав-
нения карт, и сравнение будет производиться именно на основе значения ID.

ПРИМЕЧАНИЕ  Возможно, раньше вы пользовались языком программирования, в  котором


отсутствовали такие понятия, как метод чтения (getter) и устанавливающий метод (setter). Это
функции, которые запускаются при попытке получить доступ к связанному с ними свойству (на-
пример, получить значение card.id). Они применяются для разных целей, но в рассматриваемом
случае свойство  id предназначено только для чтения, поэтому вы можете только прочитать его
значение, но не задать его.
5.3. Отображение набора карт   121

Наконец, обратите внимание, что в коде есть переменная для контроллера; когда
SceneController начнет клонировать карты для заполнения сцены, картам потребуется
ссылка на этот контроллер для вызова его открытых методов. Как всегда, когда код
ссылается на объекты сцены, нужно перетащить объект-контроллер из редактора Unity
на ячейку переменной на панели Inspector. Достаточно сделать это для одной карты,
все появившиеся позже копии получат эту ссылку автоматически.
Теперь, когда в  сценарии MemoryCard появится новый код, введите в  сценарий
SceneController содержимое следующего листинга.

Листинг 5.5. Первый проход компонента SceneController в игре Memory


using UnityEngine;
using System.Collections;

public class SceneController : MonoBehaviour {


[SerializeField] private MemoryCard originalCard; Ссылка для карты в сцене.
[SerializeField] private Sprite[] images; Массив для ссылок
на ресурсы-спрайты.
void Start() {
int id = Random.Range(0, images.Length);
originalCard.SetCard(id, images[id]); Вызов открытого метода, добавленного
} в сценарий MemoryCard.
}

Этот короткий фрагмент демонстрирует принцип управления картами со стороны


контроллера SceneController. Большая часть представленного кода уже должна быть
вам знакома (например, перетаскивание объекта-карты в редакторе Unity на ячейку
переменной на панели Inspector), но с массивами изображений вы раньше не сталки-
вались. Как показано на рис. 5.8, на панели Inspector можно задать набор элементов.
Введите 4 в поле для размера массива, а затем перетащите спрайты с изображениями
карт на ячейки элементов массива. Эти спрайты станут доступными в массиве, как
и все остальные ссылки на объекты.

Здесь указывается
количество элементов
массива

Ресурсы-спрайты
перетаскиваются
на элементы массива

Рис. 5.8. Заполнение массива спрайтов

Метод Random.Range() применялся в главе 3, надеюсь, вы помните, как с ним рабо-


тать. Точные граничные значения в данном случае не важны, но имейте в виду, что
минимальное значение входит в диапазон и может быть возвращено, в то время как
возвращаемое значение всегда меньше максимального.
122    Глава 5. Двумерная игра Memory

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

5.3.3. Экземпляры карт


У компонента SceneController ссылка на объект-карту уже есть, поэтому остается
воспользоваться методом Instantiate() (см. следующий листинг) и получить набор
карт. Аналогичным способом мы генерировали объекты в главе 3.

Листинг 5.6. Восьмикратное копирование карты и выкладывание карт на стол


using UnityEngine;
using System.Collections;

public class SceneController : MonoBehaviour {


public const int gridRows = 2;
Значения, указывающие количество
public const int gridCols = 4;
ячеек сетки и их расстояние друг
public const float offsetX = 2f; от друга.
public const float offsetY = 2.5f;

[SerializeField] private MemoryCard originalCard; Положение первой


[SerializeField] private Sprite[] images; карты; положение
остальных карт
void Start() { отсчитывается от этой
Vector3 startPos = originalCard.transform.position; точки.

Вложенные циклы, задающие


for (int i = 0; i < gridCols; i++) {
как столбцы, так и строки сетки.
for (int j = 0; j < gridRows; j++) {
MemoryCard card; Ссылка на контейнер для
if (i == 0 && j == 0) { исходной карты или ее копий.
card = originalCard;
} else {
card = Instantiate(originalCard) as MemoryCard;
}

int id = Random.Range(0, images.Length);


card.SetCard(id, images[id]);

float posX = (offsetX * i) + startPos.x;


float posY = -(offsetY * j) + startPos.y;
card.transform.position = new Vector3(posX, posY, startPos.z);

} В двумерной графике требуется только


} смещение по X и Y; значение Z не меняется.
}
}

Этот сценарий намного длиннее предыдущих, но объяснять тут особо нечего, потому
что большинство дополнений представляют собой обычные объявления переменных
и математические вычисления. Самым странным фрагментом кода является, вероят-
но, выражение, начинающее условный оператор if (i == 0 && j == 0). Оно заставляет
5.3. Отображение набора карт   123

или выбрать исходный объект-карту для первой ячейки сетки, или клонировать этот
объект для остальных ячеек. Исходная карта в сцене уже есть, поэтому, копируя ее на
каждом шаге цикла, мы получим слишком много карт. Карты раскладываются путем
смещения в соответствии с количеством шагов цикла.

СОВЕТ  Двумерные объекты, как и трехмерные, можно двигать по экрану, меняя значение transform.
position, причем это значение может расти внутри метода Update(). Когда мы занимались перемеще-
ниями объекта-игрока, вы видели, что при прямом редактировании параметра transform.position
нельзя добавить распознавание столкновений. Для получения двумерных объектов с распознаванием
столкновений нужно добавить к ним компоненты группы Physics2D и отредактировать, например,
параметр rigidbody2D.velocity.

Запустите код, и в сцене появится сетка из восьми карт, показанная на рис. 5.9. Теперь
осталось разбить карты на пары.

Рис. 5.9. Сетка из восьми карт, которые открываются щелчком мыши

5.3.4. Тасуем карты


Сейчас карты не зависят друг от друга. Создадим из их идентификаторов (чисел от 0 до
3, сопоставленных каждой паре карт) массив и перемешаем его элементы. На основе
этого массива и будет осуществляться раскладка карт.

Листинг 5.7. Раскладка карт из перемешанного списка


Большая часть листинга — это контекст,
... показывающий, куда вставлять
void Start() { дополнения.
Vector3 startPos = originalCard.transform.position; Объявляем целочисленный
массив с парами идентификато-
ров для всех четырех спрайтов
int[] numbers = {0, 0, 1, 1, 2, 2, 3, 3};
с изображениями карт.
numbers = ShuffleArray(numbers);
Вызываем функцию, перемешивающую
for (int i = 0; i < gridCols; i++) { элементы массива.
for (int j = 0; j < gridRows; j++) {
MemoryCard card;
if (i == 0 && j == 0) {
card = originalCard;
} else {
void Start() { дополнения.
Vector3 startPos = originalCard.transform.position; Объявляем целочисленный
массив с парами идентификато-
ров для всех четырех спрайтов
int[] numbers = {0, 0, 1, 1, 2, 2, 3, 3};
124numbers
    Глава 5. Двумерная игра Memory
= ShuffleArray(numbers);
с изображениями карт.
Вызываем функцию, перемешивающую
for (int i = 0; i < gridCols; i++) { элементы массива.
for (int j = 0; j < gridRows; j++) {
MemoryCard card;
if (i == 0 && j == 0) {
card = originalCard;
} else {
card = Instantiate(originalCard) as MemoryCard;
}
Идентификаторы получаем
int index = j * gridCols + i; из перемешанного списка,
int id = numbers[index]; а не из случайных чисел.
card.SetCard(id, images[id]);

float posX = (offsetX * i) + startPos.x;


float posY = -(offsetY * j) + startPos.y;
card.transform.position = new Vector3(posX, posY, startPos.z);
}
}
}

private int[] ShuffleArray(int[] numbers) { Реализация алгоритма тасования Кнута.


int[] newArray = numbers.Clone() as int[];
for (int i = 0; i < newArray.Length; i++ ) {
int tmp = newArray[i];
int r = Random.Range(i, newArray.Length);
newArray[i] = newArray[r];
newArray[r] = tmp;
}
return newArray;
}
...

Нажмите кнопку Play, и сетка заполнится картами из набора, содержащего ровно по


две карты каждого вида. Массив карт пропущен через алгоритм тасования Кнута
(еще его называют алгоритмом Фишера — Йетса). Это простой, но эффективный
способ перемешать элементы массива. Алгоритм циклически просматривает элементы
массива и каждый элемент меняет местами с другим, случайным образом выбранным
элементом.
Щелчком мыши можно открыть все карты, но в игре Memory карты должны откры-
ваться парами, поэтому требуется дополнительный код.

5.4. Совпадения и подсчет очков


До полнофункциональной игры Memory не хватает проверки совпадений. Карты уже
положены на стол и открываются по щелчку, но они никак не связаны друг с другом.
Нам же нужно, чтобы каждая открытая пара проверялась на совпадение.
Эта абстрактная логическая схема — проверка совпадений и соответствующая реак-
ция — требует, чтобы карты посылали уведомление о щелчке сценарию SceneController.
Добавьте в этот сценарий следующий код.
5.4. Совпадения и подсчет очков   125

Листинг 5.8. Сценарий SceneController, следящий за открываемыми картами


...
private MemoryCard _firstRevealed;
private MemoryCard _secondRevealed;

public bool canReveal {


get {return _secondRevealed == null;} Функция чтения, возвращающая
} значение false, если вторая карта
... уже открыта.
public void CardRevealed(MemoryCard card) {
// изначально пусто
}
...

Метод CardRevealed() мы напишем чуть позже; пока он вставлен в код как вспомога-
тельный пустой метод, на который можно ссылаться в сценарии MemoryCard.cs без по-
явления ошибок компиляции. Обратите внимание, что мы снова применяем функцию
чтения, на этот раз чтобы определить, открыта ли вторая карта; возможность открыть
карту дается игроку только при отсутствии в сцене двух открытых карт.
Еще раз отредактируем сценарий MemoryCard.cs, добавив туда вызов метода (пока
пустого), который будет информировать компонент SceneController о щелчке на
карте. Внесите в код сценария MemoryCard.cs изменения в соответствии со следующим
листингом.

Листинг 5.9. Отредактированный сценарий MemoryCard.cs для открытия карт


Проверка свойства контроллера canReveal,
гарантирующая, что одновременно можно
... открыть всего две карты.
public void OnMouseDown() {
if (cardBack.activeSelf && controller.canReveal) {
cardBack.SetActive(false);
controller.CardRevealed(this); Уведомляем контроллер
} об открытии этой карты.
}

public void Unreveal() { Открытый метод, позволяющий компоненту SceneController


cardBack.SetActive(true); снова скрыть карту (вернув на место спрайт card_back).
}
...

Если поместить в метод CardRevealed() оператор отладки для проверки взаимодей-


ствия объектов, после щелчка на любой карте будет появляться тестовое сообщение.
Но для начала разберемся с одной парой открытых карт.

5.4.1. Сохранение и сравнение открытых карт


Объект-карта был передан в метод CardRevealed(), поэтому мы начнем фиксировать
открытые карты.
126    Глава 5. Двумерная игра Memory

Листинг 5.10. Учет открытых карт в сценарии SceneController


...
public void CardRevealed(MemoryCard card) {
if (_firstRevealed == null) { Сохраняем карты в одну из двух переменных,
_firstRevealed = card; в зависимости от того, какая из них свободна.
} else {
_secondRevealed = card;
Debug.Log("Match? " + (_firstRevealed.id == _secondRevealed.id));
}
Сравнение идентификаторов
}
двух открытых карт.
...

Этот код сохраняет открытые карты в одну из двух переменных. Если первая пере-
менная свободна, заполняется именно она; если ей уже присвоен другой объект-карта,
заполняется вторая переменная, а затем проверяется, совпадают ли идентификаторы
этих объектов. В зависимости от результата оператор отладки выводит на консоль
значение true или false.
Реакция на совпадения пока отсутствует, код только проверяет, совпадают карты или
нет. Поэтому давайте запрограммируем ответ.

5.4.2. Убираем несовпавшие карты


Воспользуемся сопрограммой, ведь если карты не совпали, выполнение программы
следует прервать, чтобы у игрока появилась возможность рассмотреть открытые карты.
Сопрограммы детально рассматривались в главе 3; коротко говоря, именно сопрограм-
ма позволит сделать паузу в процедуре проверки совпадений. Добавьте в сценарий
SceneController следующий код.

Листинг 5.11. Сценарий SceneController, подсчитывающий очки или скрывающий карты


при отсутствии совпадения
...
private int _score = 0; Добавляем в список в верхней части сценария SceneController.
...
public void CardRevealed(MemoryCard card) {
if (_firstRevealed == null) {
_firstRevealed = card;
} else {
_secondRevealed = card;
StartCoroutine(CheckMatch()); Единственная отредактированная строка
} в функции – вызывает сопрограмму после
} открытия двух карт.

private IEnumerator CheckMatch() {


if (_firstRevealed.id == _secondRevealed.id) {
_score++;
Увеличиваем счет на единицу
Debug.Log("Score: " + _score); при совпадении идентификаторов
} открытых карт.
else {
yield return new WaitForSeconds(.5f);

_firstRevealed.Unreveal(); Закрываем несовпадающие карты.


_secondRevealed.Unreveal();
}

_firstRevealed = null; Очищаем переменные вне зависимости


_score++;
Увеличиваем счет на единицу
Debug.Log("Score: " + _score); при совпадении идентификаторов
} открытых карт.
else {
yield return new WaitForSeconds(.5f); 5.4. Совпадения и подсчет очков   127

_firstRevealed.Unreveal(); Закрываем несовпадающие карты.


_secondRevealed.Unreveal();
}

_firstRevealed = null; Очищаем переменные вне зависимости


_secondRevealed = null; от того, было ли совпадение.
}
...

Сначала мы добавили переменную для слежения _score; затем после открытия второй
карты в метод CheckMatch() была загружена сопрограмма с двумя вариантами кода —
выбор варианта зависит от того, произошло ли совпадение. При совпадении выпол-
нение кода не прерывается; команда yield просто пропускается. В противном случае
происходит остановка на полсекунды, а потом для обеих карт вызывается скрывающий
их метод Unreveal(). В конце, вне зависимости от совпадения, переменные, в которых
хранятся карты, обнуляются, давая игроку возможность открыть следующие карты.
В процессе игры карты, которые не совпали, будут некоторое время демонстрироваться
игроку. Количество совпадений выводится в виде отладочных сообщений, нужно же
сделать так, чтобы оно выводилось на экран.

5.4.3. Текстовое отображение счета


Добавим в игру пользовательский интерфейс. Это позволит, во-первых, выводить на
экран информацию для игрока. Во-вторых, даст игроку способ ввода сведений. Кноп-
ки UI будут обсуждаться в главе 7.

ОПРЕДЕЛЕНИЕ  Аббревиатура UI (User Interface) означает пользовательский интерфейс. Другой


термин — GUI (от graphical user interface — графический интерфейс пользователя) — означает визуаль-
ную часть интерфейса, то есть текст и кнопки. Зачастую, когда речь заходит о UI, подразумевается GUI.

В Unity есть разные способы отображения текста. Можно, например, создать трех-
мерный текстовый объект. Это специальный сеточный компонент, поэтому требуется
пустой объект, к которому его можно будет присоединить. Выберите в меню GameObject
команду Create Empty. Затем щелкните на кнопке Add Component и выберите в разделе
Mesh вариант Text Mesh.

ПРИМЕЧАНИЕ  Может показаться странным, что в двумерную игру добавляется трехмерный текст.
Но не следует забывать, что с технической точки зрения мы работаем с трехмерной сценой, которая
демонстрируется через ортографическую камеру и поэтому выглядит плоской. Так что в игру можно
добавлять трехмерные объекты — просто они будут отображаться как плоские.

СОВЕТ  Я использую стандартный компонент Text Mesh, но для ваших собственных проектов имеет
смысл воспользоваться шрифтовыми ресурсами TextMesh Pro. Это улучшенная текстовая система,
недавно добавленная в Unity.

Поместите текстовый объект в точку с координатами -4.75, 3.65, -10, то есть сдвиньте
на 475 пикселов влево и на 365 пикселов вверх, чтобы он оказался в верхнем левом углу
стола, и приблизьте к камере, чтобы он отображался поверх других игровых объектов.
В нижней части панели Inspector находится параметр Font; щелкните на маленьком кружке,
128    Глава 5. Двумерная игра Memory

чтобы вызвать окно выбора файлов, и щелчком выделите единственный доступный


шрифт Arial. В поле Text введите слово Score:. Корректное позиционирование требует,
чтобы параметр Anchor имел значение Upper Left (он контролирует, каким образом рас-
тягиваются вводимые буквы), поэтому отредактируйте его, если нужно. По умолчанию
текст получится размытым, но это легко исправить, введя параметры с рис. 5.10.

Сделайте масштаб
объекта очень маленьким

Другие упомянутые Сделайте размер


параметры: шрифта очень большим
Text, Anchor и Font

Рис. 5.10. Параметры текстового объекта на панели Inspector, позволяющие получить четкий
и резкий текст

Можно импортировать в проект новый шрифт формата TrueType и использовать его, но


для наших целей подойдет вариант по умолчанию. Странно, что для получения четкой
и резкой надписи шрифтом по умолчанию требуется редактировать его размер. Сначала
мы присвоили параметру Font Size компонента TextMesh очень большое значение (у меня
это 80). Затем сделали масштаб этого компонента очень маленьким (например, 0.1,
0.1, 0.1). Увеличение размера шрифта добавило к отображаемому тексту множество
пикселов, а масштабирование сгруппировало эти пикселы на меньшем пространстве.
Для дальнейшей манипуляции текстовым объектом нужно внести в код изменения
из следующего листинга.

Листинг 5.12. Отображения счета на текстовом объекте


...
[SerializeField] private TextMesh scoreLabel;
...
private IEnumerator CheckMatch() {
if (_firstRevealed.id == _secondRevealed.id) { Отображаемый текст —
это задаваемое свойство
_score++;
текстовых объектов.
scoreLabel.text = "Score: " + _score;
}
...
5.5. Кнопка Restart   129

Как видите, текст — это свойство объекта, которому можно назначить новую строку.
Перетащите текст из сцены на только что добавленную к компоненту SceneController
переменную и нажмите кнопку Play. Теперь в процессе игры при каждом новом совпа-
дении будет расти счет. Ура, игра работает!

5.5. Кнопка Restart


Наша игра Memory стала полнофункциональной. В нее уже можно играть. Отсутствует
только один элемент, который рано или поздно потребуется. Дело в том, что сейчас
можно сыграть всего одну партию; после этого нужно выйти из игры и загрузить ее
снова. Поэтому давайте добавим элемент управления, позволяющий начинать новую
игру без перезагрузки.
Для этого нужно решить две задачи: создать кнопку и сделать так, чтобы щелчок на
ней вызывал перезагрузку. Итоговый вид игрового поля показан на рис. 5.11.

Рис. 5.11. Игра Memory вместе с кнопкой Start

С подобными задачами приходится сталкиваться не только в двумерных играх; поль-


зовательский интерфейс с кнопками и возможность перезагрузки требуются во всех
играх. Пример решения таких задач и завершит эту главу.

5.5.1. Метод SendMessage для компонента UIButton


Первым делом добавим в сцену спрайт с изображением кнопки, перетащив его со вклад-
ки Project. Поместите его в точку с координатами 4.5, 3.25, -10; чтобы кнопка оказалась
в верхнем правом углу (то есть сместилась на 450 пикселов вправо и на 325 пикселов
вверх) и подвинулась ближе к камере, чтобы отобразиться поверх остальных игровых
объектов. Этот объект должен реагировать на щелчки мыши, поэтому добавьте к нему
коллайдер (щелкните на кнопке Add Component и выберите в разделе Physics 2D строку
Box Collider).

ПРИМЕЧАНИЕ  Как упоминалось в предыдущем разделе, в Unity есть разные способы создания
элементов UI, к которым относится и появившаяся в последних версиях усовершенствованная система
пользовательских интерфейсов. Для начала создадим кнопку из стандартных экранных объектов.
А с усовершенствованной UI-функциональностью вы познакомитесь в главе 7; мы рассмотрим пользо-
вательские интерфейсы, идеально встроенные в систему, как для двумерных, так и для трехмерных игр.
130    Глава 5. Двумерная игра Memory

Создайте сценарий UIButton.cs, добавьте в него код из следующего листинга и на-


значьте его объекту, изображающему кнопку.

Листинг 5.13. Создание кнопки, допускающей повторное использование


using UnityEngine;
using System.Collections;
Ссылка на целевой объект
public class UIButton : MonoBehaviour { для информирования
о щелчках.
[SerializeField] private GameObject targetObject;
[SerializeField] private string targetMessage;
public Color highlightColor = Color.cyan;

public void OnMouseEnter() {


SpriteRenderer sprite = GetComponent<SpriteRenderer>();
if (sprite != null) {
sprite.color = highlightColor; Меняем цвет кнопки при
} наведении указателя мыши.
}
public void OnMouseExit() {
SpriteRenderer sprite = GetComponent<SpriteRenderer>();
if (sprite != null) {
sprite.color = Color.white;
}
}
В момент щелчка
размер кнопки слегка
public void OnMouseDown() { увеличивается.
transform.localScale = new Vector3(1.1f, 1.1f, 1.1f);
}
public void OnMouseUp() {
transform.localScale = Vector3.one; После щелчка на кнопке отправляем
if (targetObject != null) { сообщение целевому объекту.
targetObject.SendMessage(targetMessage);
}
}
}

Основная часть кода выполняется внутри функций вида OnMouseЧтоТо. Этот набор
функций, подобно методам Start() и  Update(), автоматически доступен всем ком-
понентам сценариев в Unity. С функцией MouseDown вы встречались в разделе 5.2.2.
У объектов с коллайдером все эти функции отвечают за взаимодействия с мышью.
Например, MouseOver и  MouseExit — это пара событий, возникающих при наведении
указателя на объект: первое событие — появление указателя над объектом, а второе —
его уход с объекта. Аналогично, функции MouseDown и MouseUp — это пара событий, воз-
никающих в момент щелчка. Первое событие генерируется при физическом нажатии
кнопки, а второе — когда кнопку отпускают.
Легко заметить, что при наведении указателя мыши на спрайт кнопки цвет спрайта
слегка меняется, а в момент щелчка он меняет еще и размер. В обоих случаях эти из-
менения (цвета или масштаба) возникают после начала взаимодействия с  мышью,
а затем свойство возвращается в исходное состояние (к белому цвету или к мас-
штабу 1). Масштабирование реализуется через стандартный компонент transform,
5.5. Кнопка Restart   131

присутствующий у всех объектов GameObjects. А вот для изменения цвета применен


компонент SpriteRenderer, присущий таким объектам, как спрайты; спрайту при-
сваивается цвет, определенный в редакторе Unity через общедоступную переменную.
В момент отпускания кнопки мыши происходит не только возвращение масштаба
к значению 1, но и вызов метода SendMessage() . Этот метод вызывает функцию
с указанным именем во всех компонентах рассматриваемого объекта GameObject.
Как целевой объект для рассылки, так и отправляемое сообщение определены через
сериализованные переменные. Такой подход позволяет использовать для всех видов
кнопок единый компонент UIButton. Достаточно на панели Inspector сопоставить цель
в виде различных кнопок разным объектам.
В таком сильно типизированном языке, как C#, для взаимодействия с целевым объек-
том нужно знать его тип (к примеру, для вызова открытого метода объекта целевой-объ-
ект.SendMessage()). Но сценарии для элементов пользовательского интерфейса могут
содержать целевые объекты разных типов, поэтому метод SendMessage() позволяет
отправлять таким объектам сообщения без знания типа.

ВНИМАНИЕ  Метод SendMessage() не так рационально использует ресурсы процессора, как


открытые методы, вызываемые для известных типов (имеются в  виду конструкции object.
SendMessage("Метод") вместо component.Method()), поэтому пользуйтесь им только в случаях, ког-
да это сильно упростит понимание кода и работу с ним. Как правило, это ситуация, когда сообщение
адресуется множеству объектов различных типов; в этом случае отсутствие гибкости наследования
или даже интерфейсов будет мешать процессу разработки игры и затруднять эксперименты.

Теперь нужно выполнить связывание общедоступных переменных кнопки на панели


Inspector. Выберите цвет выделения (хотя заданный по умолчанию бирюзовый вполне
подходит к синей кнопке). Кроме того, перетащите объект SceneController на ячейку
целевого объекта и введите в поле для сообщения слово Restart.
Если теперь запустить воспроизведение игры, в верхнем правом углу игрового поля
появится кнопка перезагрузки, меняющая цвет при наведении на нее указателя мыши
и слегка выступающая в момент щелчка на ней. Но после щелчка вы увидите сообще-
ние об ошибке; на консоли появится информация о том, что сообщение Restart уходит
«в никуда». Ведь у нас пока отсутствует метод Restart() в сценарии SceneController.
Давайте исправим этот недостаток.

5.5.2. Вызов метода LoadScene


Связанный с кнопкой метод SendMessage() пытается вызвать метод Restart() в сце-
нарии SceneController, поэтому добавьте туда код следующего листинга.

Листинг 5.14. Код перезагрузки уровня в сценарии SceneController


...
using UnityEngine.SceneManagement; Добавляем код управления сценой SceneManagement.
...
public void Restart() {
SceneManager.LoadScene("Scene"); Эта команда загружает ресурс scene.
}
...
132    Глава 5. Двумерная игра Memory

Единственное, что делает метод Restart(), — вызывает метод LoadScene(), вызываю-


щий, в свою очередь, загрузку такого ресурса, как сохраненная сцена (то есть файл,
появившийся после команды Save Scene). Передайте в метод имя сцены, которую вы
хотите загрузить; лично я сохранил ее под именем Scene, если же вы использовали
другое имя, укажите его.
Нажмите кнопку Play и посмотрите, что получилось. Откройте несколько карт и добей-
тесь нескольких совпадений; если после этого щелкнуть на кнопке Reset, игра начнется
сначала, со скрытых карт и нулевого счета. Именно то, что и требовалось!
Как следует из названия метода LoadScene(), он может загружать различные сцены.
Но что происходит в момент загрузки и почему игра при этом перезагружается? Все
содержимое текущего уровня (все объекты сцены и присоединенные к ним сценарии)
стирается из памяти, и загружается новая сцена. В нашем случае это сцена, сохраненная
перед началом игры, поэтому все удаляется из памяти и загружается с нуля.

СОВЕТ  Объекты, которые вы не хотите удалять из памяти при перезагрузке уровня, можно поме-
тить. В Unity есть метод DontDestroyOnLoad(), сохраняющий объекты в разных сценах; в следующих
главах он будет фигурировать как часть архитектуры кода.

Мы успешно создали еще одну игру! Разумеется, о «готовности» можно говорить


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

Заключение
 Двумерная графика в Unity отображается ортогональной камерой.
 Для отображения пиксел в пиксел размер камеры должен быть равен половине
высоты экрана.
 Чтобы спрайты реагировали на щелчок мыши, им следует назначить двумерный
коллайдер.
 Новые изображения спрайтов можно загружать программно.
 Текст для пользовательского интерфейса можно создавать в виде трехмерных тек-
стовых объектов.
 Загрузка игровых уровней позволяет перезагрузить сцену.
6 Базовый двумерный
платформер

33 Непрерывное движение спрайтов.


33 Анимация на базе листов спрайтов.
33 Физические явления в двумерном пространстве (столкновения, гравитация).
33 Управление камерой в сайд-скроллерах.

Продолжим знакомство с двумерной функциональностью Unity на примере еще одной


игры. Фундаментальные концепции мы рассмотрели в предыдущей главе, теперь вос-
пользуемся ими, чтобы построить более сложную игру, а именно: создадим основную
функциональность двумерного платформера. Это распространенный тип двумерных
игр, самым известным представителем которого является классическая игра Super
Mario Brothers: персонаж, показываемый сбоку, бежит и прыгает по платформам на
фоне перемещающейся сцены.
Результат, который мы хотим получить, показан на рис. 6.1.
Во время работы над этим проектом вы познакомитесь с такими концепциями, как
перемещение игрока влево и вправо, воспроизведение анимации спрайтов и добавление
возможности прыгать. Рассмотрим мы и вещи, характерные именно для платформеров,
например односторонние и движущиеся платформы. Переход от шаблона к полноцен-
ной игре по большей части сводится к многократному повторению всех этих вещей.
Первым делом нам нужен новый двумерный проект. Мы уже создавали такие про-
екты в предыдущей главе. Выберите команду New в окне, которое появляется после
запуска Unity, или команду New Project в меню File. В открывшемся окне выберите
вариант 2D. Создайте для нового проекта две папки с именами Sprites и  Scripts. В них
мы будем складывать различные ресурсы. Еще можно настроить камеру, как мы это
делали в предыдущей главе, но в данном случае достаточно уменьшить значение поля
Size до 4. В этом проекте точной настройки не требуется, но для окончательной версии
игры нужно скорректировать размер поля зрения камеры.
134    Глава 6. Базовый двумерный платформер

Рис. 6.1. Итоговый вид игры, которую мы создадим в этой главе

СОВЕТ  Чтобы значок камеры в центре экрана не мешал работе, его нужно скрыть. В верхней части
вкладки Scene откройте меню Gizmos и щелкните на значке Icon в строке Camera.

Сохраните пустую сцену, чтобы создать соответствующий набор ресурсов (в процессе


работы не забывайте периодически щелкать на кнопке Save). Теперь давайте приступим
к созданию графических ресурсов.

6.1. Создание графических ресурсов


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

6.1.1. Стены и пол


Нам нужно пустое белое изображение. Пример проекта для этой главы содержит файл
blank.png; скопируйте его оттуда в свой проект и поместите в папку Sprites. В параметрах
импорта на панели Inspector убедитесь, что это действительно спрайт, а не текстура.
Для двумерных проектов тип изображения выбирается автоматически, но проверить
в любом случае имеет смысл.
По сути, мы будем строить геометрическую модель сцены, как это делалось в главе 4,
просто на этот раз в режиме 2D. Поэтому основой послужат не сетки, а спрайты, но
мы точно так же разместим в сцене пол и стены, формируя пространство, по которому
будет перемещаться персонаж.
Чтобы получить пол, перетащите в сцену спрайт blank , как показано на рис. 6.2.
Расположите его в точке с координатами 0.15, -1.27, 0, а в поля масштаба введите
6.1. Создание графических ресурсов   135

значения 50, 2, 1. Присвойте спрайту имя Floor. Перетащите в сцену еще один пустой
спрайт, введите в поля масштаба значения 6, 6, 1 и поставьте на пол справа, указав
координаты 2, -0.63, 0. Присвойте ему имя Block.

Опустим его чуть ниже


Выделенный пол центра сцены

Рис. 6.2. Выбираем положение пола

Как видите, все очень просто. Пол и блок уже на месте, теперь нужно добавить в сцену
персонаж.

6.1.2. Импорт листов спрайтов


Теперь нам требуется спрайт, изображающий игрока, поэтому скопируйте из папки
с примером проекта файл stickman.png. На этот раз это не одно изображение, а объ-
единенный набор спрайтов. Он показан на рис. 6.3 и представляет собой кадры двух
вариантов анимации: бездействия и цикла ходьбы. Вдаваться в детали процесса
анимации мы не будем, скажем только, что термины бездействие (idle) и цикл (cycle)
повсеместно используются разработчиками игр. Бездействие относится к небольшим
движениям стоящей на месте фигурки, а цикл представляет собой непрерывно по-
вторяющуюся анимацию.

Дополнительное пространство,
Кадры 0–1: Кадры 2–5: превращающее размер в степень
стоящая фигура анимация ходьбы двойки для последующего сжатия

Рис. 6.3. Лист спрайтов Stickman — 6 кадров подряд

В предыдущей главе упоминалось, что изображение может представлять собой набор


соединенных друг с другом спрайтов. В этом случае оно называется листом спрайтов
(sprite sheet) и состоит из кадров анимации. В Unity листы спрайтов фигурируют на
136    Глава 6. Базовый двумерный платформер

вкладке Project как единый ресурс, но щелчок на расположенной сбоку стрелке позво-
ляет увидеть все входящие в их состав изображения, как показано на рис. 6.4.

Переключение типа нарезки

Введите размер кадров

Нажмите эту кнопку для нарезки


листа спрайтов на кадры

Щелкните на стрелке,
чтобы посмотреть
на результат нарезки
спрайта

Рис. 6.4. Нарезка листа спрайтов на кадры

Перетащите изображение stickman.png в папку Sprites, чтобы выполнить его импорт,


и внесите изменения в настройки импорта на панели Inspector. В меню Sprite Mode вы-
берите вариант Multiple, а затем щелкните на кнопке Sprite Editor, чтобы открыть окно
редактора. Откройте меню Slice в верхнем левом углу редактора и выберите в раскры-
вающемся списке Type вариант Grid By Cell Size (как показано на рис. 6.4). В поля Pixel Size
введите значения 32 и 64 соответственно (это размер одного кадра в листе спрайтов)
и щелкните на кнопке Slice. Лист распадется на отдельные кадры. Щелкните на кнопке
Apply, чтобы сохранить сделанные изменения, и закройте редактор кадров.
Лист спрайтов превратился в набор кадров. Щелкните на стрелке, чтобы их увидеть.
Перетащите один спрайт (например, самый первый) в сцену, поставьте на пол в центре
и присвойте ему имя Player. В нашей игре появился персонаж!

6.2. Смещение персонажа вправо и влево


Теперь, когда в сцене появилась вся необходимая графика, можно приступить к про-
граммированию перемещений персонажа. Первым делом следует добавить персонажу
пару компонентов. Как упоминалось в предыдущих главах, сымитировать объект,
подчиняющийся законам физики, можно только при наличии у этого объекта ком-
понента Rigidbody. Наш персонаж должен подчиняться законам физики (например,
испытывать действие силы тяжести). Кроме того, потребуется компонент Collider,
определяющий границы объекта при распознавании столкновений. Важно понимать
разницу между этими компонентами: Collider определяет форму, на которую будут
действовать законы физики, а Rigidbody указывает симулятору физики, на какие объ-
екты он должен действовать.

ПРИМЕЧАНИЕ  Эти компоненты существуют по отдельности (хотя они тесно связаны), потому
что многие объекты, для которых не требуется имитация физической среды, принимают участие
в столкновениях с объектами, которые должны подчиняться законам физики.
6.2. Смещение персонажа вправо и влево   137

Есть еще один тонкий момент, о котором не следует забывать. В Unity для двумерных
игр существует отдельная система имитации физической среды. Соответственно,
в этой главе будут использоваться компоненты из раздела Physics 2D, а не из раздела
Physics.
Выделите игрока и щелкните на кнопке Add Component на панели Inspector. Выберите
в появившемся меню команду Physics 2D > Rigidbody 2D, как показано на рис. 6.5. Еще раз
щелкните на кнопке Add Component и выберите уже команду Physics 2D > Box Collider 2D.
Теперь слегка отредактируем компонент Rigidbody. Выберите в меню Collision Detection
вариант Continuous. Затем установите флажок Freeze Rotation Z (обычно симулятор физи-
ческой среды пытается поворачивать объекты в процессе перемещения, но персонаж
нашей игры ведет себя немного по-другому), а в поле Gravity Scale введите значение 0
(позднее мы вернем исходное значение, но пока сила тяжести нам не требуется).
Осталось написать сценарий, который будет контролировать перемещения персонажа.

Щелкните на кнопке Add Component,


а затем выберите строки Physics 2D После добавления компонента
и Rigidbody в верхней части меню обратите внимание на его
параметры на панели Inspector

Присвойте
параметру Gravity Scale
значение 0

Выберите для параметра


Collision Detection
значение Continuous

Запретите вращение

Рис. 6.5. Добавление и настройка компонента Rigidbody 2D

6.2.1. Элементы управления клавиатурой


Первым делом заставим персонажа перемещаться из стороны в сторону. Вертикаль-
ные перемещения также играют в платформерах важную роль, но ими мы займемся
позднее. Создайте в папке Scripts сценарий с именем PlatformerPlayer и перетащите его
на объект Player.
Добавьте в него код следующего листинга.

Листинг 6.1. Сценарий PlatformerPlayer для перемещений при помощи клавиш


со стрелками
using UnityEngine;
using System.Collections;

public class PlatformerPlayer : MonoBehaviour {


public float speed = 250.0f;

private Rigidbody2D _body; Нужно, чтобы к объекту GameObject


был прикреплен этот второй
void Start() { компонент.
_body = GetComponent<Rigidbody2D>();
using UnityEngine;
using System.Collections;

138    
public Глава
class 6. Базовый двумерный
PlatformerPlayer платформер
: MonoBehaviour {
public float speed = 250.0f;

private Rigidbody2D _body; Нужно, чтобы к объекту GameObject


был прикреплен этот второй
void Start() { компонент.
_body = GetComponent<Rigidbody2D>();
}

void Update() {
float deltaX = Input.GetAxis("Horizontal") * speed * Time.deltaTime;
Vector2 movement = new Vector2(deltaX, _body.velocity.y);
_body.velocity = movement;
} Задаем только горизонтальное
движение; сохраняем заданное
}
вертикальное смещение.

Нажмите кнопку Play и посмотрите, как персонаж реагирует на кнопки со стрелками.


Основное отличие от кода движения из предыдущих глав состоит в том, что новый
код действует на компонент Rigidbody2D, а не на контроллер персонажа. Компонент
Character Controller используется в трехмерном режиме, а для двумерных игр приме-
няется компонент Rigidbody. Обратите внимание: движение применяется к переменной
velocity, то есть к скорости этого компонента, а не к его положению.

СОВЕТ  По умолчанию при нажатии кнопок со стрелками в Unity объект не сразу начинает дви-
гаться с указанной скоростью, а постепенно разгоняется до нее. Но это слишком вялое движение для
платформера. Для более быстрого управления увеличьте значения параметров Sensitivity и Gravity для
горизонтального ввода до 6. Доступ к этим настройкам открывается командой Project Settings > Input
из меню Edit. Они находятся в свитке Horizontal на панели Inspector.

Программирование движения по горизонтали практически завершено! Осталось на-


учиться распознавать столкновения.

6.2.2. Столкновения со стенами


В настоящий момент персонаж спокойно проходит сквозь блок, ведь ни у пола, ни
у блока нет коллайдера и сквозному движения ничто не мешает. Чтобы устранить эту
недоработку, добавьте к объектам Floor и  Block компонент Box Collider 2D. По оче-
реди выделите каждый объект, щелкните на кнопке Add Component на панели Inspector
и выберите в разделе Physics 2D строку Box Collider 2D.
Готово! Нажмите кнопку Play и убедитесь, что теперь персонаж останавливается перед
блоком. Как и в случае перемещения игрока в главе 2, при попытках непосредственно
редактировать положение перемещаемого объекта распознавание столкновений не-
возможно. Встроенный в Unity модуль распознавания столкновений работает, когда
мы начинаем двигать добавленные объекту компоненты имитации физической среды.
Другими словами, при изменениях переменной Transform.position распознавание
столкновений игнорируется, поэтому в сценарии движения мы работаем с переменной
Rigidbody2D.velocity.
К менее примитивным объектам добавить коллайдер чуть сложнее, но не намного. Даже
если фигура не похожа на прямоугольник, можно взять прямоугольный коллайдер
и окружить им форму, изображающую препятствие. Существуют и другие варианты
6.3. Анимация спрайтов   139

коллайдеров, в том числе и в виде определяемых пользователем наборов многоуголь-


ников. Принцип их применения к объектам сложной формы показан на рис. 6.6.

Щелкните на этой кнопке,


чтобы получить возможность
перетаскивать точки в сцене

Рис. 6.6. Переход в режим редактирования формы полигонального коллайдера


с помощью кнопки Edit Collider

С распознаванием столкновений мы разобрались, теперь давайте заставим персонажа


двигаться в процессе перемещения.

6.3. Анимация спрайтов


После импорта изображение stickman.png было превращено в набор кадров для после-
дующей анимации. Пришло время запустить эту анимацию, чтобы персонаж перестал
скользить, а начал бегать в разные стороны.

6.3.1. Система анимации Mecanim


В главе 4 я уже упоминал, что система анимации в Unity называется Mecanim. Она
позволяет визуально настроить для персонажа комплексную сеть анимационных
клипов и обойтись минимумом кода при управлении этими клипами. Чаще всего ее
применяют для трехмерных персонажей (эта тема будет детально рассмотрена в сле-
дующих главах), но работает она и с двумерными персонажами.
В основе системы анимации лежат два вида ресурсов: анимационные клипы и ани-
маторные контроллеры. Клипы представляют собой отдельные циклы анимации,
в то время как контроллер — это сеть, определяющая, когда будет воспроизводиться
каждый клип. Это диаграмма конечного автомата (state machine), а состояния на
ней соответствуют различным анимационным клипам. Контроллер перемещается
между состояниями, реагируя на окружающую среду, и  в каждом состоянии вос-
производится свой клип.
Оба ресурса — анимационный клип и аниматорные контроллер — Unity создает авто-
матически в момент перетаскивания двумерной анимации в сцену. Разверните кадры
нашего спрайта, как показано на рис. 6.7, выделите кадры 0–1, перетащите их в сцену
и укажите в открывшемся окне имя stickman_idle.
На вкладке с ресурсами появился клип stickman_idle и контроллер stickman_0; переиме-
нуйте контроллер в stickman. Вы только что создали анимацию для стоящего на месте
140    Глава 6. Базовый двумерный платформер

персонажа! Кроме того, в сцене появился объект stickman_0, но он нам не потребуется,


поэтому просто удалите его.

1. Выделяем несколько кадров 2. … чтобы автоматически создать анимацию.


и перетаскиваем их в сцену… (Удалите второй контроллер
и дополнительные объекты)
Анимационные
Контроллер клипы
анимации

3. После добавления
к персонажу компонента
Animator перетащите
сюда контроллер

Рис. 6.7. Переход от кадров в составе листа спрайтов к компоненту Animator у персонажа

Повторим этот процесс, чтобы получить анимацию ходьбы. Выделите кадры 2–5,


перетащите их в сцену и присвойте анимации имя stickman_walk. На этот раз удалите
как появившийся в сцене объект stickman_2, так и контроллер на вкладке ресурсов;
для управления обоими клипами достаточно одного контроллера, поэтому сохраните
первый и удалите stickman_2.
Теперь нужно добавить к персонажу контроллер. Выделите в сцене объект Player и до-
бавьте компонент Animator из раздела Miscellaneous. Как показано на рис. 6.7, опустите
контроллер stickman на поле для контроллера на панели Inspector. Убедитесь, что объект
Player все еще выделен, и выберите команду Animator в меню Window, чтобы открыть
одноименное окно, показанное на рис. 6.8.
Анимационные клипы отображаются в окне Animator в виде блоков, называемых со-
стояниями (states). Работающий контроллер совершает переходы между состояниями.
Нашему контроллеру уже сопоставлен клип с состоянием покоя, но нужно добавить
еще и клип с состоянием ходьбы; перетащите клип stickman_walk со вкладки Assets в окно
Animator.
По умолчанию клип со статичным состоянием воспроизводится слишком быстро.
Поэтому выделите блок для этого состояния и на панели справа введите в поле Speed
значение 0.2. Теперь осталось только написать запускающий анимацию код.
6.3. Анимация спрайтов   141

Каждый блок представляет собой «состояние» анимации


Снимите этот флажок,
Перейдите Щелкните на кнопке со знаком +, После запуска компонент Animator чтобы переход мог
на вкладку чтобы добавить параметр переключается между состояниями, совершаться в середине
Parameters типа Float с именем speed воспроизводя анимацию для текущего воспроизведения клипа
состояния

Щелкните на состоянии правой кнопкой мыши Щелкните на кнопке со знаком +, чтобы


и выберите команду Make Transition, добавить условие перехода. В нашем случае
чтобы соединить его с другим состоянием. переход из статичного состояния к ходьбе
Создайте переходы в обе стороны будет возникать при скорости больше 0,1

Рис. 6.8. Окно Animator, демонстрирующее анимационные состояния и переходы

6.3.2. Программный запуск анимационных клипов


Итак, мы настроили анимационные состояния в контроллере Animator, и теперь можем,
переключаясь между ними, воспроизводить различные клипы. В предыдущем разделе
упоминалось, что конечный автомат переходит из одного состояния в другое, реагируя
на определенные условия. В Unity они называются параметрами. Рассмотрим процесс
их добавления. Все необходимые элементы управления показаны на рис. 6.8. Нужно
перейти на вкладку Parameters и щелкнуть на кнопке +, чтобы открыть меню с типами
параметров. Добавьте параметр типа float и присвойте ему имя speed.
Теперь нужно организовать переход между состояниями анимации на базе этого па-
раметра. Щелкните правой кнопкой мыши на блоке статичного состояния и выберите
команду Make Transition; появится стрелка, исходящая из этого блока. Щелкните на
блоке walk, чтобы завершить соединение. Переходы совершаются только в одну сто-
рону, поэтому проделайте все процедуру еще раз, чтобы соединить блоки в обратном
направлении.
Выделите стрелку перехода из статичного состояния (это можно сделать щелчком
на самой стрелке), снимите флажок Has Exit Time и щелкните на расположенной ниже
кнопке +, чтобы добавить условие (это тоже показано на рис. 6.8). Создайте условие
142    Глава 6. Базовый двумерный платформер

speed Greater 0.2 (скорость больше, чем 0.2). Теперь выделите стрелку перехода из
состояния ходьбы в состояние покоя. Снова снимите флажок Has Exit Time и добавьте
условие speed Less 0.2 (скорость меньше, чем 0.2).
Остается только добавить сценарий движения, который будет управлять аниматором.

Листинг 6.2. Запуск анимации во время перемещения


... Строка из ранее написанного кода, помогаю-
private Animator _anim; щая понять, куда следует вставлять новый код.
...
void Start() {
_body = GetComponent<Rigidbody2D>(); Скорость больше нуля даже
_anim = GetComponent<Animator>(); при отрицательных значениях
} переменной velocity.

void Update() { Числа типа float не всегда полно-


... стью совпадают, поэтому сравним
_anim.SetFloat("speed", Mathf.Abs(deltaX)); их методом Approximately().
if (!Mathf.Approximately(deltaX, 0)) {
transform.localScale = new Vector3(Mathf.Sign(deltaX), 1, 1);
}
} В процессе движения масштабируем
положительную или отрицательную 1 для
...
поворота направо или налево.

Вы не поверите, но так выглядит практически любой код управления анимационны-


ми клипами! Практически всю работу берет на себя система Mecanim, и вам остается
добавить совсем немного. Запустите воспроизведение игры, подвигайтесь в разные
стороны, и вы увидите, что спрайт персонажа ожил. Игра начинает выглядеть все
интереснее. Пора переходить к следующему шагу!

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

6.4.1. Падение под действием силы тяжести


На первый взгляд это кажется нелогичным, но прежде чем научить персонажа прыгать,
следует добавить в сцену силу тяжести, которая будет мешать прыжкам. Возможно,
вы помните, что чуть раньше мы присвоили переменной Gravity Scale связанного
с персонажем компонента Rigidbody значение  0, после чего он утратил способность
падать под действием силы тяжести. Вернем этой переменной значение  1: выделите
объект Player и в разделе Rigidbody на панели Inspector введите 1 в поле Gravity Scale.
Теперь на персонажа действует сила тяжести, но пол не дает ему упасть (при условии,
что ранее объекту Floor был назначен компонент Box Collider). Но если выйти за
границу пола, персонаж упадет в пропасть. По умолчанию влияние силы тяжести не
очень велико и имеет смысл его увеличить. Для имитации физической среды суще-
ствуют глобальные настройки этого параметра, доступ к редактированию которых
6.4. Прыжки   143

осуществляется через меню Edit. Выберите в этом меню команду Project Settings > Physics
2D. Как показано на рис. 6.9, в верхней части списка элементов управления находится
параметр Gravity Y; присвойте ему значение -40.

В этом длинном списке


параметров нужно поменять
только силу тяжести.

Рис. 6.9. Интенсивность силы тяжести в настройках имитации физической среды

Возможно, вы уже заметили небольшую проблему: падающий персонаж прилипает


к краю пола. Чтобы обнаружить это поведение, выйдите за край платформы и сразу
нажмите кнопку, заставляющую персонаж пойти в противоположном направлении.
Происходящее выглядит неестественно. К счастью, это легко исправить средствами
Unity — достаточно добавить к объектам Block и Floor компонент Physics 2D > Platform
Effector 2D. Этот эффектор заставляет объекты сцены вести себя как платформы
в платформенных играх. Редактируемые параметры показаны на рис. 6.10. Установите
флажок Used By Effector в разделе с параметрами прямоугольного коллайдера и снимите
флажок Use One Way в разделе с параметрами эффектора (этот флажок пригодится для
других платформ, но сейчас он не нужен).

Используем этот
коллайдер для
платформенного
эффектора

Это не однонаправленная
платформа, поэтому
снимите этот флажок

Рис. 6.10. Настройки коллайдера и эффектора на панели Inspector


144    Глава 6. Базовый двумерный платформер

Теперь движение персонажа вниз выглядит удовлетворительно, осталось разобраться


с движением вверх.

6.4.2. Толчок вверх


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

Листинг 6.3. Прыжок при нажатии клавиши Пробел


... Строка из ранее написанного кода,
public float jumpForce = 12.0f; помогающая понять, куда следует
... вставлять новый код.
_body.velocity = movement;

if (Input.GetKeyDown(KeyCode.Space)) {
_body.AddForce(Vector2.up * jumpForce, ForceMode2D.Impulse);
}
... Сила добавляется только при
нажатии клавиши Пробел.

Обратите внимание на команду AddForce(). Восходящая сила добавляется к компо-


ненту Rigidbody в импульсном режиме; импульс — это внезапный толчок в отличие от
непрерывно действующей силы. В коде такой толчок возникает после нажатия клавиши
Пробел. При этом на персонаж продолжает действовать сила тяжести, благодаря чему
после прыжка он описывает красивую дугу.
Но, возможно, вы уже обратили внимание на один недостаток, устранением которого
мы сейчас и займемся.

6.4.3. Распознавание поверхности


У элемента управления прыжком есть один маленький недостаток: возможность
прыгать в полете (когда персонаж уже подпрыгнул или когда он падает). Нажатие
клавиши Пробел даже в этом случае даст импульс вверх, хотя этого делать нельзя.
Элемент управления прыжком должен срабатывать только для стоящего на платформе
персонажа. А значит, нужно научиться определять, стоит ли персонаж на поверхности.

Листинг 6.4. Распознавание поверхностей


...
private BoxCollider2D _box; Получаем этот компонент, чтобы использовать
... его как проверочную область для коллайдера
_box = GetComponent<BoxCollider2D>(); персонажа.
...
_body.velocity = movement;

Vector3 max = _box.bounds.max;


Vector3 min = _box.bounds.min;
Vector2 corner1 = new Vector2(max.x, min.y - .1f); Ниже проверяем значение мини-
Vector2 corner2 = new Vector2(min.x, min.y - .2f); мальной Y-координаты коллайдера.
Collider2D hit = Physics2D.OverlapArea(corner1, corner2);

bool grounded = false;


... его как проверочную область для коллайдера
_box = GetComponent<BoxCollider2D>(); персонажа.
...
_body.velocity = movement;
6.5. Дополнительные возможности для платформера   145
Vector3 max = _box.bounds.max;
Vector3 min = _box.bounds.min;
Vector2 corner1 = new Vector2(max.x, min.y - .1f); Ниже проверяем значение мини-
Vector2 corner2 = new Vector2(min.x, min.y - .2f); мальной Y-координаты коллайдера.
Collider2D hit = Physics2D.OverlapArea(corner1, corner2);

bool grounded = false;


if (hit != null) { Если под персонажем обнаружен коллайдер…
grounded = true;
} ...добавляем в условие для
прыжка переменную grounded.
if (grounded && Input.GetKeyDown(KeyCode.Space)) {
...

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


под персонажем коллайдер, и учитывает результат проверки в условии для совершения
прыжка. Первым делом определяются границы примитива столкновений нашего пер-
сонажа, а затем в области с такой же шириной непосредственно под ногами персонажа
ищутся перекрывающиеся коллайдеры. Результат проверки сохраняется в переменную
grounded и используется в условном операторе.

6.5. Дополнительные возможности для платформера


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

ПРОЕКТИРОВАНИЕ УРОВНЕЙ ПРИ ПОМОЩИ ИНСТРУМЕНТА TILEMAP_ ______


В рамках проекта пол и платформы созданы из пустых белых прямоугольников. Для окончатель-
ного варианта игры требуется более проработанная графика, но вряд ли компьютер сможет об-
работать изображение размером с целый уровень. Чаще всего эту проблему решают с помощью
инструмента tilemap. Он позволяет конструировать изображения по принципу координатной сетки.
Вот как это выглядит.

Едва заметные линии сетки показывают границы


ячеек игрового поля; на реальной карте сетка отсутствует

(Изображение любезно предоставлено сайтом mapeditor.org, с использованием ячеек с сайта lpc.opengameart.org)

Обратите внимание: карта целиком составлена из маленьких ячеек. Именно это позволяет на-
крывать изображением весь экран, не прибегая к  изображениям слишком больших размеров.
146    Глава 6. Базовый двумерный платформер

Официальный инструмент tilemap добавлен в  последние версии приложения Unity. Можно ис-
пользовать и внешние библиотеки, например Tiled2Unity, систему, которая импортирует карты,
созданные в популярном (и бесплатном) редакторе Tiled.
Дополнительную информацию (на английском языке) можно найти на сайтах: http://mng.bz/318f
и www.seanba.com/tiled2unity.

6.5.1. Наклонные и односторонние платформы


Сейчас в нашей игре есть только обычный пол и блок, через который можно пере-
прыгнуть. Но существуют и другие варианты платформ. Воспользуемся ими, чтобы
разнообразить сцену. Начнем с наклонного участка. Создайте копию объекта Floor,
присвойте ей имя Slope, введите в поля преобразования вращения значения 0, 0, -25
и сдвиньте ее влево, присвоив координатам преобразования перемещения значения
–3.47, –1.27, 0.
Запустите воспроизведение сцены. В процессе движения персонаж корректно сколь­
зит вверх и вниз. Но из-за силы тяжести он начинает скользить вниз и в статичном
состоянии. Для решения этой проблемы следует отключить имитацию силы тяжести
в случаях, когда персонаж (a) стоит на платформе, (b) находится в состоянии покоя.
К счастью, мы уже научили его распознавать поверхности и можем воспользоваться
этим в новом фрагменте кода. Нужно добавить всего одну строку.

Листинг 6.5. Отключение имитации силы тяжести при нахождении на поверхности


Остановка при нахождении
на поверхности и в статичном
... состоянии.
_body.gravityScale = grounded && deltaX == 0 ? 0 : 1;
if (grounded && Input.GetKeyDown(KeyCode.Space)) { Строка из ранее написанного
... кода, помогающая понять, куда
следует вставлять новый код.

После редактирования кода движения персонаж стал корректно вести себя на на-
клонных поверхностях. Теперь добавим так называемую одностороннюю платформу.
Персонаж может не только стоять на ней, но и прыгать сквозь нее. При попытке прыжка
сквозь обычную платформу персонаж ударится об нее головой.
Односторонние платформы часто встречаются в играх, поэтому в Unity есть средства
их реализации. Если помните, ранее при добавлении компонента Platform Effector
мы снимали флажок Use One Way. А вот теперь он нам потребуется! Еще раз создайте
копию объекта Floor, присвойте ей имя Platform, введите в поля для масштаба значения
10, 1, 1 и поместите полученный объект над полом, указав для него координаты -1.68,
0.11, 0. И обязательно установите флажок Use One Way в разделе Platform Effector 2D.
Персонаж сможет пролететь сквозь платформу при прыжке вверх, но движению вниз
она воспрепятствует. Нужно устранить только один недостаток. Его иллюстрирует
рис. 6.11. В Unity спрайт платформы может отобразиться поверх персонажа (чтобы
было проще это увидеть, присвойте переменной jumpForce значение  7. Можно отре-
дактировать координату Z персонажа, как это было сделано в предыдущей главе, но на
этот раз поступим по-другому. У компонента, отвечающего за визуализацию спрайтов,
6.5. Дополнительные возможности для платформера   147

существует порядок сортировки, определяющий, какой спрайт будет демонстриро-


ваться сверху. Выделите объект Player и присвойте параметру Order in Layer в разделе
Sprite Renderer значение 1.

Платформа может отображаться


поверх персонажа, а мы хотим,
чтобы было наоборот

Рис. 6.11. Спрайт Platform, перекрывающий спрайт Player

Теперь в сцене есть наклонный фрагмент пола и однонаправленная платформа. Но


существует еще один необычный вариант, который реализуется немного сложнее уже
знакомых вам платформ. Давайте посмотрим, как это делается.

6.5.2. Движущиеся платформы


Третий вариант необычного пола, распространенный в платформенных играх, — это
движущиеся платформы. Чтобы добавить в нашу игру такую платформу, потребуется,
во-первых, написать новый сценарий, контролирующий ее перемещения, во-вторых,
отредактировать сценарий движения персонажа. В новом сценарии платформа будет
перемещаться между двумя точками: начальной и конечной. Создайте сценарий C#
с именем MovingPlatform и добавьте в него следующий код.

Листинг 6.6. Сценарий MovingPlatform, заставляющий пол двигаться взад и вперед


using UnityEngine;
using System.Collections;

public class MovingPlatform : MonoBehaviour {


public Vector3 finishPos = Vector3.zero; Целевое положение.
public float speed = 0.5f;
Насколько далеко наше положение
private Vector3 _startPos; от старта до финиша.
private float _trackPercent = 0;
private int _direction = 1; Направление движения в данный момент.

void Start() {
_startPos = transform.position; Положение в сцене — это точка,
} от которой начинается движение.

void Update() {
_trackPercent += _direction * speed * Time.deltaTime;
float x = (finishPos.x - _startPos.x) * _trackPercent + _startPos.x;
float y = (finishPos.y - _startPos.y) * _trackPercent + _startPos.y;
transform.position = new Vector3(x, y, _startPos.z);

if ((_direction == 1 && _trackPercent > .9f) ||


(_direction == -1 && _trackPercent < .1f)) {
void Start() {
_startPos = transform.position; Положение в сцене — это точка,
} от которой начинается движение.
148    
void Глава {6. Базовый двумерный платформер
Update()
_trackPercent += _direction * speed * Time.deltaTime;
float x = (finishPos.x - _startPos.x) * _trackPercent + _startPos.x;
float y = (finishPos.y - _startPos.y) * _trackPercent + _startPos.y;
transform.position = new Vector3(x, y, _startPos.z);

if ((_direction == 1 && _trackPercent > .9f) ||


(_direction == -1 && _trackPercent < .1f)) { Меняем направление как
_direction *= -1; в начале, так и в конце.
}
}
}

DRAWING CUSTOM GIZMOS _________________________________________


Большая часть кода, который пишется в процессе чтения книги, программирует происходящее
в играх. Но сценарии Unity позволяют влиять и на встроенный редактор. Многие не знают, что
в  Unity можно добавлять дополнительные меню и  окна. Более того, с  их помощью создаются
вспомогательные объекты на вкладке Scene, которые называются габаритными контейнерами
(gizmos).
С габаритными контейнерами вы уже сталкивались. Помните зеленые параллелепипеды, обо-
значающие коллайдеры? Они встроены в Unity, но никто не мешает вам написать сценарий для
собственного габаритного контейнера. К примеру, создадим на вкладке Scene линию, соответ-
ствующую траектории перемещения платформы. Она показана на рисунке.

Габаритные контейнеры отображаются


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

Код, рисующий такую линию, очень прост. В верхнюю часть кода, влияющего на интерфейс Unity,
нужно добавить строку using UnityEditor; (потому что большинство функций редактора нахо-
дится именно в этом пространстве имен), но в данном случае этого не требуется. Просто добавьте
этот метод в сценарий MovingPlatform:
...
void OnDrawGizmos() {
Gizmos.color = Color.red;
Gizmos.DrawLine(transform.position, finishPos);
}
...
6.5. Дополнительные возможности для платформера   149

Про этот код важно понимать следующее. Во-первых, все действие совершается внутри метода
OnDrawGizmos(). Это системное имя, как и  имена методов Start и Update. Во-вторых, мы
добавляем в этот метод две строки кода: первая задает цвет линии, а вторая заставляет Unity
нарисовать линию от места, где находится платформа, до целевой точки.

Перетащите этот сценарий на платформу. Запустите воспроизведение сцены, и вы уви-


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

Листинг 6.7. Обработка движущейся платформы в сценарии PlatformerPlayer.cs


...
_body.AddForce(Vector2.up * jumpForce, ForceMode2D.Impulse);
}
Проверяем, может ли двигаться
MovingPlatform platform = null; платформа, находящаяся над
if (hit != null) { персонажем.
platform = hit.GetComponent<MovingPlatform>();
}
if (platform != null) { Выполняем связывание с платформой или
transform.parent = platform.transform; очищаем переменную transform.parent.
} else {
transform.parent = null;
} Строка из ранее написанного кода,
помогающая понять, куда следует
_anim.SetFloat("speed", Mathf.Abs(deltaX)); вставлять новый код.
...

Теперь запрыгнувший на платформу персонаж будет перемещаться вместе с ней. По


сути, все свелось к превращению персонажа в дочерний по отношению к платформе
объект. Надеюсь, вы помните, что после формирования иерархической связи дочерний
объект начинает перемещаться вслед за родительским. Метод GetComponent() в лис­
тинге 6.7 проверяет, является ли распознанная поверхность движущейся платформой.
В случае положительного результата проверки она становится предком по отношению
к персонажу; в противном случае персонаж отсоединяется от любого предка.
Но возникает одна проблема. Персонаж наследует от платформы и преобразование
масштабирования, что влияет на его размер. Ситуацию позволяет исправить обратное
преобразование (уменьшение масштаба персонажа, компенсирующее спровоцирован-
ное платформой увеличение размера).
Листинг 6.8. Коррекция масштабов персонажа
...
_anim.SetFloat("speed", Mathf.Abs(deltaX));

Vector3 pScale = Vector3.one;


При нахождении вне движущейся
if (platform != null) { платформы масштаб по умолчанию
pScale = platform.transform.localScale; равен 1.
}
if (deltaX != 0) {
transform.localScale = new Vector3(
Mathf.Sign(deltaX) / pScale.x, 1/pScale.y, 1); Замещаем существующее
} масштабирование новым
} кодом.
...
Vector3 pScale = Vector3.one;
При нахождении вне движущейся
if (platform != null) { платформы масштаб по умолчанию
pScale = platform.transform.localScale; равен 1.
150
}     Глава 6. Базовый двумерный платформер
if (deltaX != 0) {
transform.localScale = new Vector3(
Mathf.Sign(deltaX) / pScale.x, 1/pScale.y, 1); Замещаем существующее
} масштабирование новым
} кодом.
...

Математически компенсация масштабирования выглядит очень просто: игроку при-


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

6.5.3. Управление камерой


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

Листинг 6.9. Сценарий FollowCam, перемещающий камеру вместе с персонажем


using UnityEngine;
using System.Collections;

public class FollowCam : MonoBehaviour {


public Transform target;
Сохраняем координату Z,
void LateUpdate() { меняя значения X и Y.
transform.position = new Vector3(
target.position.x, target.position.y, transform.position.z);
}
}

Перетащите объект Player на поле Target в разделе Follow Cam (Script) панели Inspector.
Запустите воспроизведение сцены. Теперь камера перемещается таким образом,
чтобы персонаж все время оставался в центре экрана. Обратите внимание, что в коде
с камерой связывается положение целевого объекта, а мы превратили персонаж в це-
левой объект. Причем метод называется LateUpdate, а не просто Update; это еще одно
системное имя, принятое в Unity. Метод LateUpdate тоже исполняет каждый кадр, но
только после того, как со всеми кадрами поработает метод Update.
Камера, постоянно перемещающаяся вслед за персонажем, немного раздражает. В боль-
шинстве платформенных игр камера ведет себя достаточно деликатно, выделяя в про-
цессе движения персонажа различные фрагменты уровня. Вообще говоря, управление
камерой в платформерах на удивление сложная тема; воспользуйтесь поиском по
ключевым словам «platform game camera» или «камера в платформерах» и посмотрите
все результаты. Мы же ограничимся тем, что сделаем ее движение плавным и менее
раздражающим; вот отредактированная версия кода.
Заключение   151

Листинг 6.10. Сглаживание движения камеры


...
public float smoothTime = 0.2f;

private Vector3 _velocity = Vector3.zero;


... Сохраняем координату Z,
void LateUpdate() { меняя значения X и Y.
Vector3 targetPosition = new Vector3(
target.position.x, target.position.y, transform.position.z);

transform.position = Vector3.SmoothDamp(transform.position,
targetPosition, ref _velocity, smoothTime); Плавный переход от текущей
} к целевой позиции.
...

Основное дополнение сводится к вызову функции SmoothDamp; все остальные (напри-


мер, добавление переменных time и  velocity) только поддерживают эту функцию.
В Unity эта функция обеспечивает плавный переход от одного значения к другому.
В рассматриваемом случае переход совершается от текущего положения камеры
к целевому.
Теперь камера плавно следует за персонажем. Вы запрограммировали движение
персонажа, несколько видов платформ и элемент управления камерой. Работа над
проектом окончена!

Заключение
 Листы спрайтов часто используются как основа для двумерной анимации.
 Персонажи игр ведут себя не так, как реальные объекты, поэтому для них нужно
особым образом настраивать параметры физической среды.
 Для управления объектами с компонентом Rigidbody им назначаются различные
силы или напрямую задается их скорость.
 Для конструирования уровней в двумерных играх часто применяется инструмент
Tilemaps.
 Простой сценарий позволяет заставить камеру плавно следовать за персонажем.
7 Двумерный GUI
для трехмерной игры

33 Сравнение старой (до версии Unity 4.6) и новой систем GUI.


33 Создание холста для интерфейса.
33 Позиционирование элементов UI с помощью точек привязки.
33 Добавление к UI интерактивных элементов (кнопок, ползунков и т. п.).
33 Рассылка и прием уведомлений о UI-событиях.

В этой главе мы поработаем над двумерным интерфейсом для трехмерной игры. При
создании демонстрационного ролика от первого лица внимание концентрировалось
исключительно на виртуальной сцене. Но любая игра требует не только места, где
происходит действие, но и средств отображения абстрактных взаимодействий и инфор-
мации. Без этого невозможно представить себе ни двумерную, ни трехмерную игру, ни
шутер от первого лица, ни головоломку. Соответственно, все техники, которые будут
показаны в этой главе на примере трехмерной игры, применимы и к двумерным играм.
Эти средства отображения абстрактных взаимодействий называют пользовательским
интерфейсом (UI) или, точнее, графическим интерфейсом пользователя (GUI). Хотя
с технической точки зрения аббревиатура GUI относится к визуальной части интер-
фейса, например к тексту и кнопкам (рис. 7.1), а аббревиатура UI — к физическим
средствам управления, таким как клавиатура или джойстик, люди, говоря про «поль-
зовательский интерфейс», как правило, подразумевают и графическую часть.
Хотя UI требуется любому программному обеспечению, потому что иначе пользователь
просто не сможет контролировать работу приложения, GUI в играх зачастую функ-
ционирует несколько по-другому. Например, GUI веб-сайта, по сути, представляет
собой сам сайт (если говорить о визуальном представлении). В игре же текст и кнопки
зачастую накладываются на игровое пространство с помощью так называемого про-
екционного дисплея.
7.1. Перед тем как писать код…   153

Кнопка настроек:
часть проекционного
дисплея, отображаемая
поверх игровой сцены

Всплывающее окно, Стена в сцене —


отображенное поверх это основное игровое
игровой сцены пространство

Рис. 7.1. Графический интерфейс, который мы создадим для игры

ОПРЕДЕЛЕНИЕ  Проекционный дисплей (Heads-Up Display, HUD) накладывает графику поверх


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

В этой главе мы добавим в игру проекционный дисплей, используя новейшие ин-


струменты Unity. Как упоминалось в главе 5, создавать элементы пользовательского
интерфейса в Unity можно разными способами. Пришло время познакомиться с новой
UI-системой, которая впервые появилась в версии Unity 4.6. Будет упоминаться и ста-
рая система, на примере которой я продемонстрирую преимущества нововведений.
Инструменты создания UI в Unity будут рассматриваться на примере шутера от
первого лица, которым мы занимались в главе 3. Перед нами стоят следующие задачи:
1. Спланировать интерфейс.
2. Расположить на экране UI-элементы.
3. Запрограммировать взаимодействия с UI-элементами.
4. Запрограммировать GUI-реакции на события в игре.
5. Запрограммировать реакции сцены на действия с GUI.
Скопируйте проект из главы 3 и откройте его в Unity. Все необходимые ресурсы, как
обычно, находятся в доступном для скачивания примере проекта. Надеюсь, вы готовы
к построению пользовательского интерфейса для нашей игры.

ПРИМЕЧАНИЕ  По большому счету не имеет значения, какой именно проект послужит рабочей
основой для материала данной главы. Ведь мы просто добавим графический интерфейс поверх
существующего демонстрационного ролика. Все упражнения даются на примере шутера от первого
лица, созданного в главе 3. Можно скачать готовую версию этого проекта или воспользоваться любым
другим демонстрационным роликом игры.

7.1. Перед тем как писать код…


Чтобы приступить к работе над элементами HUD, важно понимать, как функциони-
рует UI-система. В Unity существуют разные подходы к построению проекционного
дисплея, и желательно выбрать наиболее подходящий для наших целей. После этого
154    Глава 7. Двумерный GUI для трехмерной игры

остается в общих чертах спланировать UI и подготовить необходимые графические


ресурсы.

7.1.1. IMGUI или усовершенствованный 2D-интерфейс?


Уже в  первой версии Unity появилась система GUI непосредственного режима
(Immediate Mode GUI, IMGUI).

ОПРЕДЕЛЕНИЕ  Словосочетание непосредственный режим (immediate mode) означает явное


выполнение команд рисования в  каждом кадре, в  отличие от режима удержания (retained mode),
в котором все визуальные элементы задаются однократно, после чего система сама решает, что именно
следует рисовать в каждом кадре.

Система IMGUI позволяет легко поместить на экран интерактивную кнопку. Код


этой операции показан в листинге 7.1. Остается только присоединить сценарий из
этого листинга к любому объекту сцены. Можно вспомнить еще один пример UI не-
посредственного режима. Это курсор прицеливания, который мы создавали в главе 3.
В основе такой системы GUI лежит исключительно код, работа в редакторе Unity
не требуется.

Листинг 7.1. Реализация кнопки в системе IMGUI


using UnityEngine; Функция вызывается в каждом кадре
using System.Collections; после того, как все остальное уже
визуализировано.
public class BasicUI : MonoBehaviour {
void OnGUI() {
if (GUI.Button(new Rect(10, 10, 40, 20), "Test")) {
Debug.Log("Test button");
} Параметры: положение по X,
} положение по Y, ширина, высота,
} текстовая подпись.

Основа листинга — метод OnGUI(). Все представители класса MonoBehaviour автоматиче-


ски отвечают как на методы Start() и Update(), так и на метод OnGUI(). Он запускается
в каждом кадре после визуализации трехмерной сцены, предоставляя место для команд
рисования GUI. В рассматриваемом случае код рисует кнопку; обратите внимание,
что команда рисования выполняется в каждом кадре (то есть в непосредственном
режиме). Эта команда фигурирует внутри условного оператора, срабатывающего при
щелчке на кнопке.
Так как GUI непосредственного режима позволяет с минимальными усилиями до-
бавить на экран несколько кнопок, мы будем пользоваться им и в следующих главах.
Но это практически единственный легко генерируемый элемент, поэтому в новейших
версиях Unity появилась новая система интерфейса, основанная на компонуемой
в редакторе двумерной графике. Ее настройка требует некоторых усилий, но, скорее
всего, в готовых играх вы предпочтете пользоваться именно этой системой, дающей
более проработанные элементы интерфейса.
Новая система UI функционирует в режиме удержания, поэтому вся графика за-
дается один раз и рисуется в каждом кадре без постоянных повторных определений
7.1. Перед тем как писать код…   155

компоновки. Размещение графических элементов UI при этом выполняется в редакто-


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

7.1.2. Выбор компоновки


В большинстве игр проекционный дисплей состоит из нескольких многократно повто-
ряющихся UI-элементов. Поэтому понять процесс создания UI несложно. Мы добавим
в углы экрана индикатор счета и кнопку настроек, разместив их поверх основного
изображения сцены, как показано на рис. 7.2. Кнопка будет вызывать всплывающее
окно с текстовым полем и ползунком.

Кнопка настроек.
2 Щелчок на ней открывает
Индикатор счета окно диалога
с изображением
и текстом
Имя
Кнопка Close. Закрывает
Скорость всплывающее окно
Окно, всплывающее
в центре экрана.
Открывается щелчком Элементы управления
на кнопке в виде вводом. Текстовое поле
шестерни для имени, ползунок
для скорости

Рис. 7.2. План будущего GUI

В рассматриваемом примере элементы управления вводом служат для указания имени


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

7.1.3. Импорт изображений для элементов UI


Нашему пользовательскому интерфейсу требуется графика для отображения таких
элементов, как, к примеру, кнопки. Воспользуемся двумерными изображениями, по-
этому все сведется к уже знакомой вам по главе 5 процедуре:
1. Импорт изображений (если нужно, вручную определите их как спрайты).
2. Перетаскивание спрайтов в сцену.
Первым делом перетащите все изображения на вкладку Project, чтобы импортировать
их в редактор, а затем на панели Inspector убедитесь, что у каждого из них параметр
Texture Type имеет значение Sprite (2D And UI).
156    Глава 7. Двумерный GUI для трехмерной игры

ВНИМАНИЕ  По умолчанию параметр Texture Type в трехмерных проектах принимает значение


Texture, а  в двумерных  — значение Sprite. Если требуются спрайты для трехмерного проекта, этот
параметр нужно редактировать вручную.

Необходимые изображения (рис. 7.3) есть в скачанном примере проекта. Проверьте,


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

close enemy gear popup


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

Рис. 7.3. Изображения для текущего проекта

7.2. Настройка GUI


В качестве графических ресурсов мы взяли уже знакомую по главе 5 разновидность
двумерных спрайтов, но на этот раз они будут использоваться немного по-другому.
В Unity есть специальные инструменты, которые превращают изображения в ото-
бражаемые поверх трехмерной сцены элементы HUD. Существуют и особые приемы
размещения UI-элементов, обусловленные тем, что на разных экранах фрагменты
интерфейса зачастую отображаются по-разному.

7.2.1. Холст для интерфейса


Один из наиболее фундаментальных и при этом неочевидных аспектов функциониро-
вания системы UI сводится к необходимости связывать все изображения с объектом
Canvas.

СОВЕТ  Холст (canvas) — это разновидность объектов, которую Unity визуализирует как игровой
пользовательский интерфейс.

Откройте меню GameObject с перечнем доступных объектов; в категории UI выберите ва-


риант Canvas. В сцене появится холст (для наглядности присвойте ему имя HUD Canvas).
Этот объект растянут на весь экран и имеет огромный в сравнении с трехмерной сценой
размер, так как масштабирует один пиксел экрана в одну единицу измерения сцены.

ВНИМАНИЕ  Вместе с холстом автоматически создается объект EventSystem. Он требуется для


UI-взаимодействий, в остальных случаях его можно просто игнорировать.
7.2. Настройка GUI   157

Переключитесь в режим 2D, как показано на рис. 7.4, и дважды щелкните на имени
холста на вкладке Hierarchy, чтобы увидеть этот объект целиком. Двумерный режим
включается автоматически для всех 2D-проектов, но в  случае 3D-проекта переход
между UI и основной сценой осуществляется при помощи переключателя. Для воз-
вращения к просмотру трехмерной сцены отключите 2D-режим и дважды щелкните
на строчке Building, чтобы развернуть этот объект на весь экран.

Двумерный режим:
переключайтесь в этот режим
для работы с двумерной графикой,
в том числе с UI-элементами

Объект-холст
на вкладке Scene.
Цветные стрелки
Он сильно
манипулятора указывают,
масштабирован,
что инструмент Rect
так как одна единица
НЕ активен. Он включается
измерения в сцене
кнопкой в верхнем левом
соответствует одному
углу Unity; вы увидите
пикселу в UI.
синие точки во всех углах
Границы холста двумерного объекта
масштабируются, чтобы
он поместился
на игровой экран

Рис. 7.4. Пустой холст на вкладке Scene

СОВЕТ  Хочу еще раз привести совет из главы 4: в верхней части вкладки Scene располагаются
кнопки, управляющие видимостью различных элементов, поэтому найдите кнопку Effects для от-
ключения скайбокса.

У холста есть доступные для редактирования параметры. Во-первых, параметр Render


Mode, которому следует оставить значение по умолчанию. Для него возможны следу-
ющие значения:
 Screen Space—Overlay — элементы UI визуализируются как наложенная поверх вида
с камеры двумерная графика (установлено по умолчанию).
 Screen Space—Camera — элементы UI также визуализируются поверх вида с камеры,
но могут вращаться, создавая эффекты перспективы.
 World Space  — холст помещается в  сцену, делая элементы UI частью трехмерной
сцены.
Последние два режима применяются для создания специальных эффектов, но имеют
несколько более сложную реализацию по сравнению с режимом по умолчанию.
Еще одна важная настройка — флажок Pixel Perfect. После его установки в процессе
визуализации начинает корректироваться положение изображений с целью придать
им четкий и контрастный вид (в отличие от размывания, возникающего при разме-
щении между пикселов). Установите этот флажок, чтобы окончательно подготовить
холст к размещению спрайтов.
158    Глава 7. Двумерный GUI для трехмерной игры

7.2.2. Кнопки, изображения и текстовые подписи


Холст задает область, которая будет отображаться как пользовательский интерфейс, но
туда следует добавить спрайты с изображениями отдельных элементов. В соответствии
с макетом с рис. 7.2 в верхнем левом углу находится изображение блока/противника
и рядом текст, отображающий набранные очки, а в верхнем правом углу — кнопка
в виде шестерни. В разделе  UI меню GameObject есть команды, позволяющие создать
изображение (image), текст (text) или кнопку (button). Создайте по одному элементу
каждого вида.

СОВЕТ  Как и при моделировании текстовых объектов в главе 5, для ваших собственных проектов
имеет смысл использовать шрифтовые ресурсы TextMesh Pro. Это внешняя система, призванная
улучшить вид текста в Unity.

Для корректного отображения элементы интерфейса должны быть потомками холста.


В Unity эта иерархическая связь возникает автоматически, но напомню, что подоб-
ные зависимости можно формировать и вручную, перетаскивая объекты на вкладке
Hierarchy (рис. 7.5).

Объект-холст

Объект-изображение
(потомок объекта-холста
на вкладке Hierarchy)

Рис. 7.5. Холст и связанное с ним изображение на вкладке Hierarchy

Между объектами на холсте также можно сформировать иерархические связи для


удобства их размещения. Например, если перетащить текст на изображение, надпись
будет перемещаться вместе с картинкой. Более того, у кнопки, созданной с параметрами
по умолчанию, есть дочерний по отношению к ней текстовый объект; в данном случае
мы не будем ничего на ней писать, поэтому просто удалите его.
Перетащите элементы интерфейса на предназначенные им места. В следующем разделе
мы зададим их точное положение, пока же это не имеет значения. Указателем мыши
перетащите объект-изображение в верхний левый угол холста, а кнопку — в верхний
правый.

СОВЕТ  В главе 5 упоминалось, что в двумерном режиме активируется инструмент Rect. Я описывал
его как средство, совмещающее все три преобразования: перемещение, поворот и масштабирование.
В трехмерном режиме эти операции выполняются отдельными инструментами, но при переходе к ра-
боте с двумерными объектами объединяются, так как у нас пропадает одно измерение. В 2D-режиме
этот инструмент выбирается автоматически, кроме того, можно активировать его нажатием кнопки
в верхнем левом углу Unity.

Пока оба элемента пусты. Если выделить объект UI, на панели Inspector, в верхней
части свитка Image вы увидите поле Source Image. Перетащите на это поле спрайты
(ни в коем случае не текстуры!) со вкладки Project, как показано на рис. 7.6. Назначьте
7.2. Настройка GUI   159

спрайт с изображением противника объекту Image, а спрайт с изображением шестерни


объекту Button. Чтобы спрайты приобрели корректный размер, щелкните на кнопке
Set Native Size.

1. Перетащите спрайт со вкладки


2. …и изображение появится
Project в поле Source Image…
на элементе UI

3. Щелкните на кнопке Set Native Size


для обеспечения корректного размера
спрайта

Рис. 7.6. Назначение двумерных спрайтов свойству Image элементов интерфейса

В результате картинки с изображениями противника и кнопки настроек приобретут


нужный нам вид. Многочисленные параметры текстового объекта также отображаются
на панели Inspector. Первым делом введите какое-нибудь число в большое поле Text;
позднее это значение будет переопределено, пока же введенная информация выглядит
в редакторе как отображение набранных очков. Увеличьте размер текста, присвоив
параметру Font Size значение 24 и выбрав в раскрывающемся списке Font Style вариант
Bold. При этом горизонтально эта текстовая подпись должна быть выровнена по левому
краю, а вертикально — по центру, как показано на рис. 7.7. Остальным параметрам пока
оставим значения по умолчанию.

Объект UI отображает
текст, введенный
в это поле
Импортируйте шрифт TrueType
и выберите его здесь

Эти кнопки отвечают


за выравнивание текста
по горизонтали и по вертикали

Рис. 7.7. Настройки текстового элемента UI

ПРИМЕЧАНИЕ  Кроме содержимого поля Text и выравнивания чаще всего редактируется такое
свойство, как шрифт. В Unity можно импортировать шрифт TrueType и выбрать его на панели Inspector.

Мы успешно назначили элементам интерфейса спрайты и указали параметры текста.


Теперь нажмите кнопку Play, чтобы посмотреть, как выглядит проекционный дисплей
поверх игровой сцены. Как показано на рис. 7.8, холст в редакторе Unity обозначает
границы экрана, а элементы UI появляются на этом экране в заданных точках.
160    Глава 7. Двумерный GUI для трехмерной игры

Холст, показанный в редакторе Во время игры проекционный дисплей


перекрывает основную игровую сцену

Рис. 7.8. Вид GUI в редакторе (слева) и в процессе игры (справа)

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


проекционный дисплей с элементами интерфейса! Осталось правильно расположить
эти элементы относительно холста.

7.2.3. Управление положением элементов интерфейса


У всех объектов UI существует точка привязки, отображаемая в редакторе в виде
крестика (рис. 7.9). Это гибкий способ позиционирования элементов интерфейса.

Значок точки привязки

Объект-изображение

Рис. 7.9. Точка привязки изображения

ОПРЕДЕЛЕНИЕ  Привязкой (anchor) объекта называется точка его присоединения к холсту или
экрану. Именно относительно этой точки указывается положение объекта.

Положение задается в виде «50 пикселов по оси X». Но возникает вопрос: от какой


точки отсчитывать 50 пикселов? На помощь приходит точка привязки. В то время как
объект остается статичным относительно этой точки, сама она перемещается относи-
тельно холста. Точку привязки можно задать, например, как «центр экрана», и она будет
оставаться в центре, когда экран меняет свой размер. Аналогично привязка к правой
стороне экрана позволит объекту оставаться справа даже при изменении размеров
(к примеру, при игре на другом мониторе).
Проще всего понять эти вещи на примере. Выделите объект Image. Настройки при-
вязки на панели Inspector находятся в верхней части раздела Rect transform (рис. 7.10).
По умолчанию привязка элементов интерфейса имеет значение Center, но нам в данном
случае требуется значение Top Left; рис. 7.10 демонстрирует процесс редактирования
данного параметра в окне Anchor Presets.
7.3. Программирование интерактивного UI   161

…чтобы открыть
меню Anchor presets.
Можно указать точную
координату точки привязки,
но лучше воспользоваться
предустановленными
значениями. Например,
щелкните на этой кнопке
для привязки к верхнему
Щелкните на кнопке Anchor правому углу
(она выглядит как мишень)…
(Предустановленные значения
Stretch влияют как на размер
изображения, так и на его
положение)
Рис. 7.10. Редактирование параметров привязки

Заодно поменяем привязку кнопки настроек. Щелчком на верхнем правом квадрате


в меню Anchor Preset установите для нее значение Top Right. Попробуйте менять размер
экрана, перетаскивая его боковые края влево и вправо. Благодаря привязкам при
изменении размеров холста объекты UI остаются на своих местах. Как показано на
рис. 7.11, элементы интерфейса перемещаются вместе с краями экрана.

Перетащите границу
вкладки Scene для изменения
размеров экрана

При этом холст масштабируется,


но изображения остаются
в точках привязки

Рис. 7.11. При изменении размеров экрана привязки остаются на месте

СОВЕТ  Точки привязки позволяют подстраиваться не только под изменение положения, но и под
изменение размера. В этой главе данная функциональность не требуется, но каждый угол изображе-
ния можно связать с одним из углов экрана. На рис. 7.11 изображения сохраняют свой размер, но
привязку можно отредактировать таким образом, что при изменении размеров экрана изображения
будут растягиваться вместе с ним.

Итак, все визуальные элементы на своих местах, пришло время обеспечить их инте-
рактивность.

7.3. Программирование интерактивного UI


Для взаимодействия с  пользовательским интерфейсом нужен указатель мыши.
Если помните, в этой игре его настройки редактируются в методе Start() сценария
RayShooter. Изначально мы блокировали и скрывали его. Такое поведение прекрасно
подходит для элементов управления в шутере от первого лица, но не позволяет работать
162    Глава 7. Двумерный GUI для трехмерной игры

с элементами интерфейса. Удалите эти строчки из сценария RayShooter.cs, чтобы по-


явилась возможность щелкать на элементах проекционного дисплея.
Кроме того, в сценарий RayShooter.cs нужно добавить строки, блокирующие в процессе
взаимодействия с GUI возможность стрелять. Вот новая версия кода.

Листинг 7.2. Добавление взаимодействий с GUI в код сценария RayShooter.cs


using UnityEngine.EventSystems; Подключаем библиотеку для UI-системы.
... Курсивом выделен код, который уже был
void Update() { в сценарии и теперь приведен для справки.
if (Input.GetMouseButtonDown(0) &&
!EventSystem.current.IsPointerOverGameObject()) {
Проверяем, что GUI
Vector3 point = new Vector3(
не используется.
camera.pixelWidth/2, camera.pixelHeight/2, 0);
...

Теперь в процессе игры можно щелкать на кнопках, хотя пока это не дает результа-
тов. Меняется только оттенок кнопки при наведении на нее указателя мыши и при
щелчке. Это изменение цвета задано по умолчанию, и для каждой кнопки его можно
отредактировать, но пока мы этого делать не будем. За скорость возвращения кнопки
к обычному цвету отвечает параметр Fade Duration в свитке Button. Попробуйте умень-
шить его до 0.01 и посмотрите, что получится.

СОВЕТ  Иногда элементы взаимодействия с UI мешают игре. Помните автоматически появившийся
вместе с холстом объект EventSystem? В числе прочего он контролирует элементы интерфейса, по
умолчанию используя для взаимодействия с  GUI кнопки со стрелками. Имеет смысл отключить
работу с этими кнопками для компонента EventSystem: выделите его на вкладке Hierarchy и на панели
Inspector снимите флажок Send Navigation Event.

Щелчок на кнопке пока ни к чему не приводит, так как она не связана с каким-либо
кодом. Давайте исправим этот недостаток.

7.3.1. Программирование невидимого объекта UIController


Как правило, программирование взаимодействия с элементами интерфейса сводится
к стандартной, общей для всех элементов последовательности:
1. В сцене создается UI-объект (в нашем случае это созданная в предыдущем раз-
деле кнопка).
2. Пишется сценарий, который будет вызываться при обращении к этому элементу
интерфейса.
3. Сценарий присоединяется к объекту в сцене.
4. Элементы интерфейса (например, кнопки) связываются с объектом, к которому
присоединен этот сценарий.
Кнопка у нас уже есть, осталось создать контроллер, который будет с ней связываться.
Создайте сценарий UIController и перетащите его на объект-контроллер в сцене.
7.3. Программирование интерактивного UI   163

Листинг 7.3. Сценарий UIController, предназначенный для программирования кнопок


using UnityEngine;
using UnityEngine.UI; Импортируем фреймворк для работы с кодом UI.
using System.Collections;

public class UIController : MonoBehaviour {


[SerializeField] private Text scoreLabel; Объект сцены Reference Text
для задания свойства text.
void Update() {
scoreLabel.text = Time.realtimeSinceStartup.ToString();
}

public void OnOpenSettings() { Метод, вызываемый кнопкой настроек.


Debug.Log("open settings");
}
}

СОВЕТ  Казалось бы, зачем нужны два объекта, SceneController и UIController, ведь наша сцена
настолько проста, что с управлением ее объектами и элементами интерфейса вполне мог бы справиться
один контроллер? Но по мере усложнения сцены вы убедитесь, что лучше иметь отдельные модули
управления, взаимодействующие друг с  другом косвенным образом. Этот принцип применим не
только к играм, но и к программному обеспечению в целом и в среде разработчиков ПО называется
разделением ответственности (separation of concerns).

Перетащим объекты на ячейки компонентов, чтобы связать их друг с другом. Пере-


тащите на поле Score Label объекта UIController текстовый объект, предназначенный
для отображения счета. Текст данной подписи начнет определять код сценария
UIController. В настоящее время отображается таймер, который мы добавили, чтобы
протестировать, как все работает; чуть позже мы заменим его на набранные игроком
очки.
Снабдим кнопку элементом OnClick, чтобы добавить ее к объекту-контроллеру. Вы-
делите кнопку и найдите в нижней части панели Inspector поле OnClick; изначально оно
пустое, но, как показано на рис. 7.12, можно щелкнуть на кнопке  + и добавить туда
элемент. Каждый элемент определяет одну функцию, вызываемую щелчком на кнопке;
в листинге присутствует как ячейка для объекта, так и меню для вызываемой функции.
Перетащите на ячейку объект-контроллер, выделите в меню строку UIController и вы-
берите в дополнительном меню вариант OnOpenSettings().

Поле события OnClick


в нижней части
панели настроек Перетащите объект сцены
на ячейку и выберите в меню
Щелкните на кнопке +
нужную функцию
для добавления
в поле элемента

Рис. 7.12. Поле OnClick в нижней части панели с настройками кнопки


164    Глава 7. Двумерный GUI для трехмерной игры

РЕАКЦИЯ НА ОСТАЛЬНЫЕ СОБЫТИЯ МЫШИ ___________________________


Наша кнопка пока реагирует только на событие OnClick, в то время как элементы интерфейса
могут отвечать на разные варианты взаимодействий. Для программирования взаимодействий,
отличных от заданных по умолчанию, применяется компонент EventTrigger.
Добавьте к кнопке новый компонент и найдите раздел Event в меню этого компонента. Выберите
вариант EventTrigger. Хотя событие OnClick кнопки отвечает только на полноценный щелчок
(кнопка мыши нажимается, а затем отпускается), попробуем запрограммировать реакцию толь-
ко на нажатие кнопки мыши. Последовательность действий будет такой же, как и для события
OnClick, просто на этот раз мы укажем реакцию на другое событие. Первым делом добавьте
в сценарий UIController еще один метод:
...
public void OnPointerDown() {
Debug.Log("pointer down");
}
...
Щелкните на кнопке Add New Event Type, чтобы добавить к компоненту EventTrigger новый тип.
Выберите вариант Pointer Down. Появится пустое поле для этого события, полностью аналогичное
полю для события OnClick. Щелкните на кнопке +, чтобы добавить листинг события, и перета-
щите на этот элемент объект-контроллер, после чего выберите в меню вариант OnPointerDown().
Все готово!

Запустите игру и щелкните на кнопке для вывода на консоль отладочных сообщений.


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

7.3.2. Всплывающее окно


Кнопка, входящая в состав нашего интерфейса, должна открывать окно диалога. Но
окно сначала нужно создать. Его роль сыграет новый объект Image с присоединенными
к нему элементами управления (кнопками и ползунками). Первым делом получим
новое изображение, поэтому выберите в меню GameObject команду UI, а затем команду
Image. Как и раньше, на панели Inspector вы найдете ячейку Source Image. Перетащите
на нее спрайт с именем popup.
По умолчанию спрайт растягивается на всю поверхность объекта-изображения; именно
так вели себя спрайты, предназначенные для отображения счета и кнопки настроек. Вы
щелкали на кнопке Set Native Size, чтобы подогнать объект под размер картинки. Это стан-
дартное поведение объектов-изображений, но у всплывающего окна другое назначение.
Как показано на рис. 7.13, у компонента image есть параметр Image Type. По умолчанию
он имеет значение Simple, которое раньше нас вполне устраивало. Но для всплывающего
окна присвойте параметру Image Type значение Sliced.

Измените тип изображения Кнопка Set Native Size применима


для всплывающего окна только для варианта Simple,
с Simple на Sliced поэтому вместо нее появился
флажок Fill Center

Рис. 7.13. Параметр Image Type компонента image


7.3. Программирование интерактивного UI   165

ОПРЕДЕЛЕНИЕ  Фрагментированное изображение (sliced image) разбито на девять частей, которые


масштабируются по-разному. Масштабирование краев отдельно от середины гарантирует сохранение
четких и резких границ при любом изменении размеров. В других инструментах разработки имена
таких изображений часто содержат цифру 9 (например, 9-slice, 9-patch, scale-9), что подчеркивает
факт разделения на девять частей.

Переход к фрагментированному изображению может завершиться сообщением об


ошибке, информирующим, что у изображения отсутствуют границы. Ведь спрайт popup
пока не разделен на части. Выделите его на вкладке Project и на панели Inspector нажмите
кнопку Sprite Editor, как показано на рис. 7.14. Откроется окно диалога Sprite Editor.

… чтобы открыть окно


и отредактировать
границы спрайта

Введите в поля L R B T
(Left Right Bottom Top)
значение 12, чтобы подвинуть
на 12 пикселов зеленые линии,
Щелкните на кнопке задающие положение границ
Sprite Editor…
Рис. 7.14. Кнопка Sprite Editor на панели Inspector и открываемое ею окно

В окне Sprite Editor вы увидите зеленые линии, указывающие способ разбивки изо-
бражения. Изначально границ у спрайта нет (то есть величина всех границ равна 0).
Увеличьте ширину границ со всех сторон, чтобы получить показанный на рис. 7.14
результат. Все четыре параметра (Left, Right, Bottom и Top) имеют значение 12 пикселов,
поэтому пересекающиеся зеленые линии поделят изображение на девять частей. За-
кройте окно редактора и примените сделанные изменения.
Теперь, когда спрайт разделен на девять частей, параметр Image Type без проблем примет
значение Sliced (а внизу появится флажок Fill Center). Перетащите находящийся в любом
из углов изображения манипулятор синего цвета, чтобы выполнить масштабирование
(если вы не видите манипуляторов, активируйте описанный в главе 5 инструмент Rect).
Боковые фрагменты при этом сохранят свои размеры, в то время как центральная
часть поменяет масштаб.
Это свойство боковых фрагментов сохраняет резкость границ изображения при любом
изменении размеров, что идеально подходит для элементов интерфейса — окна могут
иметь разные размеры, но выглядеть при этом должны одинаково. Укажите для ши-
рины окна значение 250, а для высоты — 200, придав ему такой же вид, как на рис. 7.15
(заодно убедитесь, что оно находится в точке с координатами 0, 0, 0).
СОВЕТ  Способ наложения элементов интерфейса друг на друга определяется порядком их следова-
ния на вкладке Hierarchy. Расположите всплывающее окно поверх остальных элементов (разумеется,
сохранив его связь с холстом). Теперь подвигайте окно по сцене и посмотрите, каким образом пере-
крываются изображения. Перетащите окно в самый низ иерархического списка дочерних элементов
холста, чтобы оно отображалось поверх всего остального.

Всплывающее окно готово, можно писать для него код. Создайте сценарий SettingsPopup
и перетащите его на наше окно.
166    Глава 7. Двумерный GUI для трехмерной игры

Рис. 7.15. Фрагментированное изображение отмасштабировано до размеров всплывающего окна

Листинг 7.4. Сценарий SettingsPopup для всплывающего окна


using UnityEngine;
using System.Collections;

public class SettingsPopup : MonoBehaviour {


public void Open() {
gameObject.SetActive(true); Активируем этот объект, чтобы открыть окно.
}
public void Close() {
gameObject.SetActive(false); Деактивируем объект, чтобы закрыть окно.
}
}

Теперь откройте сценарий UIController.cs и добавьте в него содержимое следующего


листинга.

Листинг 7.5. Возможность работы с окном для сценария UIController


...
[SerializeField] private SettingsPopup settingsPopup;
void Start() {
settingsPopup.Close(); Закрываем всплывающее окно в момент начала игры.
}
...
public void OnOpenSettings() {
settingsPopup.Open(); Заменяем отладочный текст методом всплывающего окна.
}
...

Этот код добавляет ячейку для всплывающего окна, поэтому свяжите это окно с объ-
ектом UIController. После этого оно начнет закрываться в начале игры и открываться
при щелчке на кнопке настроек.
Способа закрыть его вручную пока не существует. Нужно добавить соответствующую
кнопку. Последовательность действий будет практически такой же, как при создании
предыдущей кнопки: выберите в меню GameObject команду UI> Button, поместите новую
кнопку в верхний правый угол всплывающего окна, перетащите спрайт close на ячейку
Source Image данного элемента интерфейса и щелкните на кнопке Set Native Size, чтобы
7.3. Программирование интерактивного UI   167

изображение приобрело нужный размер. Но сейчас, в отличие от предыдущего случая,


нам требуется текстовая подпись, поэтому выделите дочерний компонент text и введите
в текстовое поле слово Close, сделав его цвет белым. На вкладке Hierarchy перетащите
эту кнопку на всплывающее окно, чтобы сформировать иерархическую связь. Как за-
ключительный штрих присвойте параметру Fade Duration значение 0.01 и сделайте чуть
темнее параметр Normal Color, присвоив ему значения 110, 110, 110, 255.
Чтобы кнопка закрывала окно, ей нужно сопоставить событие OnClick. Щелкните на
кнопке  + поля OnClick, перетащите всплывающее окно на ячейку объекта и выберите
в списке функций вариант Close(). Запустите воспроизведение игры и убедитесь, что
кнопка действительно закрывает всплывающее окно.
Итак, мы добавили к элементам проекционного дисплея всплывающее окно. Пока
оно пустое, значит, нужно добавить к нему элементы управления. Именно этим мы
и займемся в следующем разделе.

7.3.3. Задание значений с помощью ползунка и поля ввода


Процедура добавления элементов управления к всплывающему окну настроек состоит
из двух этапов, почти как знакомая вам процедура создания кнопок. Вы генерируете
присоединенные к холсту элементы интерфейса и связываете их со сценарием. Создадим
ползунок, текстовое поле и текстовую подпись к ползунку. Выберите в меню GameObject
команду UI > Text, чтобы получить текстовый объект, затем — команду UI > InputField,
чтобы получить текстовое поле, а потом — UI > Slider для получения ползунка (рис. 7.16).

Элементы управления
вводом для всплывающего
окна: Кнопка закрытия
располагается в верхнем
текстовый элемент
углу, в то время как
InputField
текстовая подпись
цифровой элемент находится над ползунком
Slider

Рис. 7.16. Элементы управления вводом, добавленные к всплывающему окну

Сделайте все три объекта потомками всплывающего окна, перетащив их на вкладке


Hierarchy, а затем расположите их в соответствии с рисунком, выровняв по центру всплы-
вающего окна. Присвойте текстовому элементу значение Speed, формируя подпись
к ползунку. Поле предназначено для ввода текста и по умолчанию, пока игрок ничего
не напечатал, там отображается слово Text; замените его словом Name. Параметрам
Content Type и  Line Type можно оставить значения по умолчанию. Кроме того, можно
ограничить вводимую информацию только буквами или только целыми числами,
изменив параметр Content Type, а также разрешить ввод нескольких строк с помощью
параметра Line Type.
ВНИМАНИЕ  Пользоваться ползунком невозможно, если его перекрывает текстовая подпись.
На вкладке Hierarchy текстовый объект должен располагаться над ползунком, чтобы обеспечить рас-
положение ползунка поверх подписи.
168    Глава 7. Двумерный GUI для трехмерной игры

ВНИМАНИЕ  В рассматриваемом случае имеет смысл оставить полю ввода размер по умолчанию.
Но если вы решите сделать его меньше, уменьшайте только значение Width. При значениях параметра
Height меньше 30 поле становится слишком маленьким для отображения текста.

Параметры ползунка находятся в  нижней части панели Inspector. Минимальное


значение задает параметр Min Value, по умолчанию равный  0; его мы менять не бу-
дем. Параметр же Max Value по умолчанию имеет значение 1, но в нашем примере мы
присвоим ему значение  2. Параметрам Value и  Whole Numbers оставим значения по
умолчанию; первый устанавливает начальное положение ползунка, а второй ограни-
чивает допустимые значения целыми числами (в данном случае такое ограничение
не требуется).
Параметры всех объектов заданы, осталось написать управляющий их поведением код;
добавьте в сценарий SettingsPopup.cs методы из следующего листинга.

Листинг 7.6. Методы для элементов управления вводом всплывающего окна


Этот метод срабатывает в момент начала
... ввода данных в текстовое поле.
public void OnSubmitName(string name) {
Debug.Log(name);
}
public void OnSpeedValue(float speed) {
Debug.Log("Speed: " + speed); Этот метод срабатывает при изменении
} положения ползунка.
...

Замечательно! Наши элементы управления обзавелись методами. Среди настроек


поля ввода появилось поле End Edit; перечисленные в этом поле события наступают
после завершения пользовательского ввода. Добавьте к этому полю элемент, щелкнув
на кнопке со значком +, перетащите всплывающее окно на ячейку для объекта и вы-
берите в списке функций вариант OnSubmitName().

ВНИМАНИЕ  Функция OnSubmitName() присутствует как в  верхнем списке Dynamic String, так
и в нижнем Static Parameters. Но выбор варианта из нижнего списка дает возможность отправлять
всего одну, заранее заданную строку. Нам же нужно, чтобы пересылалось любое введенное в поле
значение, то есть динамическая строка, поэтому выберите функцию OnSubmitName() из нижнего
списка Dynamic String.

Повторите эту последовательность действий для ползунка: найдите поле событий


в нижней части панели Inspector (в данном случае оно называется OnValueChanged),
щелкните на кнопке со значком  + для добавления элемента, перетащите на ячейку
объекта всплывающее окно и выберите в списке динамических функций вариант
OnSpeedValue().
Теперь оба элемента управления вводом связаны с кодом сценария для всплывающего
окна. Запустите воспроизведение игры и понаблюдайте, что происходит на консоли
в процессе перемещения ползунка или при нажатии клавиши Enter после ввода имени
в текстовое поле.
7.4. Обновление игры в ответ на события   169

СОХРАНЕНИЕ НАСТРОЕК МЕЖДУ ИГРАМИ_ ____________________________


В Unity есть несколько методов сохранения постоянных данных, простейшим из которых является
PlayerPrefs. Это абстрагированный вариант, работающий на всех платформах и  с разными
файловыми системами (то есть не нужно беспокоиться о деталях реализации), позволяющий со-
хранять небольшие фрагменты информации. При большом объеме данных метод PlayerPrefs
не помогает (в следующих главах для сохранения в процессе игры мы воспользуемся другими
методами), но для сохранения настроек он подходит просто идеально.
Метод PlayerPrefs предоставляет простые команды чтения и задания именованных значений
(его работа во многом напоминает хеш-таблицу или словарь). Например, для сохранения выбран-
ной скорости достаточно добавить в метод OnSpeedValue() сценария SettingsPopup строку
PlayerPrefs.SetFloat("speed", speed);. Метод сохранит десятичное значение скорости
в переменную speed.
Аналогичным образом происходит инициализация ползунка с использованием сохраненного зна-
чения. Добавьте в сценарий SettingsPopup следующий код:
using UnityEngine.UI;
...
[SerializeField] private Slider speedSlider;
void Start() {
speedSlider.value = PlayerPrefs.GetFloat("speed", 1);
}
...
Обратите внимание, что команде get предоставляется как значение переменной speed, так
и значение по умолчанию на случай, если сохраненная скорость отсутствует.

Наши элементы управления генерируют отладочные сообщения, но пока никак не


влияют на происходящее в игре. Программированию взаимодействий элементов про-
екционного дисплея с игрой посвящен последний раздел нашей главы.

7.4. Обновление игры в ответ на события


До текущего момента проекционный дисплей и игра существовали отдельно друг от
друга, в то время как они должны взаимодействовать. Мы уже программировали взаи-
модействия между объектами с помощью ссылок в сценариях, но в данном случае этот
вариант не подходит, так как приводит к формированию сильной связи между сценой
и элементами проекционного дисплея. Нам же нужны практически независимые друг
от друга фрагменты, дающие возможность свободно редактировать игру, не беспокоясь
о сохранности проекционного дисплея.
Создадим систему широковещательной рассылки сообщений, которая будет инфор-
мировать пользовательский интерфейс о том, что происходит в сцене. Принцип ее
работы иллюстрирует рис. 7.17. Сценарии могут подписываться на слушание события,
сообщение о котором рассылает конкретный код. Давайте посмотрим на практике, как
все это работает.

СОВЕТ  В языке C# существует встроенная система обработки событий. Почему мы ею не пользу-


емся? Дело в том, что эта система работает с нацеленными сообщениями, в то время как нам требуется
широковещательная рассылка. В нацеленной системе код должен заранее знать источник сообщения,
в то время как рассылка работает с сообщениями из произвольного источника.
170    Глава 7. Двумерный GUI для трехмерной игры

Служба сообщений является


центральным модулем, Другие объекты могут попросить
Объекты могут подписываться
пересылающим сообщения службу сообщений сделать рассылку
на слушание определенного
между издателями и подписчиками определенных событий.
события, назначая функцию
на роль обратного вызова Служба выполнит рассылку
Messenger всем подписчикам этого события
ListenObject
• Добавление подписчика
BroadcastObj
• Awake() • Рассылка сообщения
• Update()
• OnEventReceived

Рис. 7.17. Схема широковещательной рассылки сообщений

7.4.1. Интегрирование системы сообщений


Для оповещения пользовательского интерфейса о происходящем в сцене создадим
систему широковещательной рассылки сообщений. В Unity нет встроенной функ-
ции, подходящей для выполнения такой задачи, но можно скачать нужный сценарий.
Вики-сообщество Unify представляет собой репозиторий бесплатного кода от раз-
личных разработчиков. Их система для службы сообщений дает несвязанный способ
взаимодействия с остальной частью программы посредством событий. Рассылающий
сообщения код может ничего не знать о подписчиках, что позволяет легко менять или
добавлять взаимодействующие объекты.
Создайте сценарий с именем Messenger и скопируйте в него одноименный код со стра-
ницы Unify: http://wiki.unity3d.com/index.php/CSharpMessenger_Extended. Затем понадобится
еще один сценарий с именем GameEvent. Вот его код:

Листинг 7.7. Сценарий GameEvent, который будет использоваться вместе


со сценарием Messenger
public static class GameEvent {
public const string ENEMY_HIT = "ENEMY_HIT";
public const string SPEED_CHANGED = "SPEED_CHANGED";
}

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

7.4.2. Рассылка и слушание сообщений от сцены


До текущего момента вместо набранных игроком очков отображался таймер, ко-
торый применялся для тестирования функциональности текстового дисплея. Но
там должно отображаться количество пораженных игроком противников, поэтому
давайте отредактируем код сценария UIController. Первым делом удалите метод
7.4. Обновление игры в ответ на события   171

Update(), так как именно там находился тестовый код. В момент смерти противни-
ка генерируется событие. Следующий листинг заставляет сценарий UIController
слушать это событие.

Листинг 7.8. Добавление подписчиков на событие в сценарий UIController


...
private int _score; Объявляем, какой метод отвечает
на событие ENEMY_HIT.
void Awake() {
Messenger.AddListener(GameEvent.ENEMY_HIT, OnEnemyHit);
}
void OnDestroy() {
Messenger.RemoveListener(GameEvent.ENEMY_HIT, OnEnemyHit);
}
При разрушении объекта удаляем
void Start() { подписчика, чтобы избежать ошибок.
_score = 0;
scoreLabel.text = _score.ToString();
Присваиваем переменной score
settingsPopup.Close(); начальное значение 0.
}

private void OnEnemyHit() {


_score += 1; Увеличиваем переменную score на 1 в ответ на данное событие.
scoreLabel.text = _score.ToString();
}
...

Первым делом обратите внимание на методы Awake() и  OnDestroy(). Как и методы


Start() и Update(), все члены класса MonoBehaviour автоматически реагируют на ак-
тивацию или удаление объекта. Подписчик добавляется в методе Awake() и удаляется
в методе OnDestroy(). Будучи частью системы широковещательной рассылки сообще-
ний, при получении сообщения он вызывает метод OnEnemyHit(), который увеличивает
переменную score на 1 и выводит новое значение на текстовый дисплей.
Подписчик события задан в коде пользовательского интерфейса, поэтому каждое по-
ражение противника должно сопровождаться рассылкой сообщения. Код реакции на
смерть противника находится в сценарии RayShooter.cs, поэтому добавьте туда код
отправки сообщения из следующего листинга.

Листинг 7.9. Рассылка сообщения о событии в сценарии RayShooter


...
К реакции на попадания добавлена
if (target != null) {
рассылка сообщения.
target.ReactToHit();
Messenger.Broadcast(GameEvent.ENEMY_HIT);
} else {
...

Запустите игру и убедитесь, что теперь текстовый дисплей отображает количество


поверженных противников. При каждом попадании счетчик должен увеличиваться
на единицу. Мы успешно запрограммировали рассылку сообщений от трехмерной
172    Глава 7. Двумерный GUI для трехмерной игры

игры к двумерному интерфейсу, и теперь требуется создать рассылку в обратном на-


правлении.

7.4.3. Рассылка и слушание сообщений проекционного дисплея


В предыдущем разделе сообщение о  событии посылалось сценой и  принималось
элементом проекционного дисплея. Сходным образом элементы пользовательского
интерфейса могут посылать сообщения, которые будут слышать как игроки, так и про-
тивники. В результате на настройки игры начнут влиять параметры, которые игрок
указывает во всплывающем окне.
Откройте сценарий WanderingAI.cs и добавьте туда следующий код.

Листинг 7.10. Добавление подписчика события в сценарий WanderingAI


...
public const float baseSpeed = 3.0f; Базовая скорость, которая регулируется
... положением ползунка.
void Awake() {
Messenger<float>.AddListener(GameEvent.SPEED_CHANGED, OnSpeedChanged);
}
void OnDestroy() {
Messenger<float>.RemoveListener(GameEvent.SPEED_CHANGED, OnSpeedChanged);
}
...
private void OnSpeedChanged(float value) { Метод, объявленный в подписчике
speed = baseSpeed * value; для события SPEED_CHANGED.
}
...

Методы Awake() и  OnDestroy() в данном случае тоже выполняют добавление и уда-


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

СОВЕТ  В предыдущем разделе фигурировало обобщенное событие, в то время как система рас-
сылки позволяет передавать не только сообщения, но и значение. Для этого к подписчику достаточно
добавить определение типа; обратите внимание на дополнение <float> в команде задания подписчика.

Внесем аналогичные изменения в сценарий FPSInput.cs, чтобы получить возможность


влиять на скорость перемещения игрока. Содержимое следующего листинга отличается
от листинга 7.10 только значением переменной baseSpeed у игрока.

Листинг 7.11. Добавление подписчика события в сценарий FPSInput


...
public const float baseSpeed = 6.0f; Это значение отличается от указанного в листинге 7.10.
...
void Awake() {
Messenger<float>.AddListener(GameEvent.SPEED_CHANGED, OnSpeedChanged);
}
void OnDestroy() {
Messenger<float>.RemoveListener(GameEvent.SPEED_CHANGED, OnSpeedChanged);
}
Заключение   173

...
private void OnSpeedChanged(float value) {
speed = baseSpeed * value;
}
...

Напоследок сделаем рассылку значений скорости из сценария SettingsPopup как


ответ на изменение положения ползунка. Код этой рассылки приведен в следующем
листинге.

Листинг 7.12. Рассылка сообщения сценарием SettingsPopup


public void OnSpeedValue(float speed) {
Messenger<float>.Broadcast(GameEvent.SPEED_CHANGED, speed);
...
Значение, заданное положением
ползунка, рассылается как
событие <float>.

Теперь при изменении положения ползунка скорости противника и  игрока будут


меняться. Нажмите кнопку Play и убедитесь сами!

УПРАЖНЕНИЕ: ИЗМЕНЕНИЕ СКОРОСТИ ГЕНЕРИРУЕМЫХ ПРОТИВНИКОВ_____


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

Теперь вы умеете создавать графические интерфейсы с помощью инструментов Unity.


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

Заключение
 В Unity поддерживается как система GUI непосредственного режима, так и более
новая система, основой которой являются двумерные спрайты.
 Применение двумерных спрайтов для создания GUI требует наличия в сцене та-
кого объекта, как холст.
 Элементы пользовательского интерфейса можно привязывать к различным точ-
кам настраиваемого холста.
 Свойство Active включает и выключает элементы пользовательского интерфейса.
 Несвязанная система передачи сообщений является замечательным способом об-
мена информацией между интерфейсом и сценой.
8 Игра от третьего лица:
перемещения и анимация
игрока

33 Добавление теней в реальном времени.


33 Облет камеры вокруг цели.
33 Плавное изменение вращения с помощью алгоритма линейной интерполяции.
33 Распознавание поверхности при прыжках, а также с учетом краев и склонов.
33 Назначение анимации антропоморфному персонажу и управление ею.

В этой главе мы запрограммируем еще одну трехмерную игру, но на этот раз в другом
жанре. В главе 2 был создан демонстрационный ролик для игры от первого лица.
Давайте сделаем еще один демонстрационный ролик, но на этот раз сфокусируемся
на перемещениях персонажа. Основное отличие состоит в положении камеры относи-
тельно игрока: в игре от первого лица игрок смотрит на сцену глазами персонажа, в то
время как в игре от третьего лица камера находится в стороне от него. Скорее всего,
такое представление знакомо вам по серии приключенческих игр Legend of Zelda или
по более поздней серии игр Uncharted (виды сцены в игре от первого и от третьего
лица сравниваются на рис. 8.3).
Эта глава посвящена одному из самых интересных в визуальном отношении проектов
книги. Схема будущей сцены показана на рис. 8.1. Сравните ее с показанной на рис. 2.2
схемой игры от первого лица, с которой началось ваше знакомство с созданием игр
в Unity.
Как видите, конструкция комнаты не изменилась, сценарии также применяются сход-
ным способом. Коренным изменениям подверглись только вид игрока и положение
камеры. Еще раз упомяну аспекты, превращающие происходящее в игру от третьего
лица. Это расположение камеры в стороне от персонажа и ее нацеленность в сторону
персонажа. Вместо примитивной капсулы теперь потребуется модель человека, так
как игроки могут себя видеть.
Заключение   175

Включите отображение теней.


Так как теперь мы видим персонажа,
1. Расположите в сцене стены, тени играют важную роль
пол и источники света.
Просто импортируйте
Установите камеру. Она должна
их из предыдущих проектов
находиться в стороне от персонажа,
смотря на него сверху

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

Рис. 8.1. Макет демонстрационного ролика игры от третьего лица

В главе 4 упоминались такие типы графических ресурсов, как трехмерные модели


и анимация. Термин трехмерная модель — практически синоним сеточного объекта.
Это статическая форма, определенная вершинами и полигонами (то есть сеточная
геометрия). В случае антропоморфного персонажа этой геометрии придается форма
головы, рук, ног и т. д. (рис. 8.2).

Рис. 8.2. Каркасное представление модели, с которой мы будем работать в этой главе

Как обычно, сфокусируемся на последнем пункте нашего плана: программировании


объектов. Вот будущая последовательность действий:
1. Импорт модели персонажа в сцену.
2. Настройка элементов управления камеры и ее нацеливание на персонаж.
3. Написание сценария, позволяющего персонажу бегать по поверхности.
176    Глава 8. Игра от третьего лица: перемещения и анимация игрока

4. Добавление в сценарий движения возможности прыгать.


5. Воспроизведение анимации на модели на основе ее движений.
Скопируйте проект из главы 2, чтобы воспользоваться им как основой. Также можно
открыть новый проект Unity (убедитесь, что вы создаете проект 3D, а не 2D, как в гла-
ве 5) и скопировать в него файл сцены из главы 2. Кроме того, понадобится содержимое
папки scratch из материалов к данной главе. Именно в ней находится модель персонажа.
ПРИМЕЧАНИЕ  Новый проект будет разрабатываться в  интерьерах сцены из главы 2. Стены
и источники света останутся теми же, замене подлежат только персонаж и все сценарии. Если вам
нужны эти материалы, скачайте примеры файлов к данной главе.

Если вы начали с готового проекта из главы 2 (я имею в виду демонстрационный


ролик, а не более позднюю версию), первым делом следует разорвать связь камеры
с игроком. Это делается простым перетаскиванием на вкладке Hierarchy. Затем удалите
объект player. Если бы камера оставалась его дочерним объектом, она при этом тоже
исчезла бы. Нам же нужно избавиться только от капсулы, которая выполняла функ-
цию игрока, сохранив камеру. Если же вы случайно удалили камеру, создайте новую,
выбрав в меню GameObject команду Camera.
Заодно удалите все сценарии (для этого потребуется как избавить камеру от компо-
нента script, так и удалить все файлы сценариев на вкладке Project). В сцене должны
остаться только стены, пол и источники света.

8.1. Корректировка положения камеры


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

Демонстрационный ролик от первого лица Демонстрационный ролик от третьего лица

Рис. 8.3. Сравнительный вид сцены от первого и от третьего лица

Сцена полностью готова, осталось добавить в нее модель персонажа.


8.1. Корректировка положения камеры   177

8.1.1. Импорт персонажа


В папке scratch, доступной для скачивания вместе с остальными материалами к этой
главе, содержится как сама модель, так и текстура. В главе 4 мы говорили о том, что
файл FBX — это модель, а файл TGA — это текстура. Импортируйте в проект файл FBX,
перетащив его на вкладку Project или щелкнув на этой вкладке правой кнопкой мыши
и выбрав в открывшемся меню команду Import New Asset. Теперь обратите внимание
на панель Inspector, где нужно скорректировать параметры импорта модели. Редакти-
рованием анимации займемся чуть позже, а пока ограничимся парой параметров на
вкладке Model. Первым делом присвойте параметру Scale Factor значение 10 (частично
компенсируя слишком маленькое значение параметра File Scale), чтобы получить модель
корректного размера.
Ниже располагается параметр Normals (рис. 8.4). Он контролирует вид света и теней на
модели, используя для этого такое математическое понятие, как нормали.

Задайте параметр Scale Factor,


чтобы частично скомпенсировать
параметр File Scale. Он определяет
величину модели в Unity относительно
ее размера в той программе,
где она была создана

Укажите способ обработки


нормалей для модели

Рис. 8.4. Параметры импорта для модели персонажа

ОПРЕДЕЛЕНИЕ  Нормалями (normals) называются перпендикулярные полигонам векторы на-


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

По умолчанию параметр Normals имеет значение Import, в котором используются норма-


ли, задаваемые геометрией импортированной сетки. Но нормали нашей модели опре-
делены некорректно, что приводит к странной реакции на источники света. Поэтому
присвоим данному параметру значение Calculate, заставив Unity вычислять вектор для
лицевой стороны каждого полигона.
Завершив редактирование этих двух параметров, щелкните на кнопке Apply на панели
Inspector. Затем импортируйте в проект файл TGA и сделайте это изображение тексту-
рой. Для этого выделите назначенный персонажу материал и перетащите изображение
текстуры на ячейку Albedo на панели Inspector. Цвет модели практически не изменится
(эта текстура по большей части имеет белый цвет), но на текстуре нарисованы тени,
улучшающие внешний вид модели.
Перетащите модель персонажа со вкладки Project в сцену. Введите в поля Position значе-
ния 0, 1.1, 0, чтобы персонаж оказался в центре помещения. Первый шаг к созданию
игры от третьего лица сделан!
178    Глава 8. Игра от третьего лица: перемещения и анимация игрока

ПРИМЕЧАНИЕ  Руки персонажа вытянуты в стороны, хотя естественнее было бы опустить их


вниз. Это так называемая T-образная поза, по умолчанию придаваемая всем подлежащим анимации
персонажам.

8.1.2. Добавление теней


Рассмотрим такой важный аспект, как отбрасываемая персонажем тень. Наличие теней
считается само собой разумеющимся, но в виртуальном мире свои законы. К счастью,
Unity в состоянии позаботиться о таких вещах, и источник света, по умолчанию при-
сутствующий в любой новой сцене, приводит к формированию теней. Выделите на
вкладке Hierarchy строчку Directional Light и обратите внимание на параметр Shadow Type
на панели Inspector. Он, как показано на рис. 8.5, имеет значение Soft Shadows, при этом
среди доступных значений есть и вариант No Shadows (без теней).

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

Рис. 8.5. Вид без теней от направленного источника света и с тенями

В рамках данного проекта настройка теней завершена, но, разумеется, в целом создание
теней в играх представляет собой куда более сложный процесс. Расчет теней в сцене явля-
ется крайне затратной по времени операцией, поэтому для получения нужного эффекта
зачастую идут на хитрости и различными способами имитируют нужные детали. Тени
от персонажа относятся к так называемым теням, визуализируемым в реальном времени
(real-time shadow), так как они вычисляются в процессе игры и двигаются вместе с объ-
ектом. Можно создать идеальное реалистичное освещение, при котором все объекты
будут отбрасывать тени и формировать тени на своих поверхностях в реальном времени,
но обычно с целью ускорения расчетов накладывают ограничения как на вид теней, так
и на количество источников света, приводящих к их формированию. Обратите внимание,
что в нашей сцене тени формирует только направленный осветитель.
Также распространен способ создания теней на основе карт освещения.

ОПРЕДЕЛЕНИЕ  Картами освещения (lightmaps) называются текстуры, которые назначаются


геометрии игровых уровней и в которых «запечено» изображение теней.

ОПРЕДЕЛЕНИЕ  Рисование теней на текстуре модели называется запеканием теней (baking the
shadows).
8.1. Корректировка положения камеры   179

Эти изображения генерируются заранее (а не во время игры), поэтому они могут


быть крайне детализированными и реалистичными. Разумеется, они полностью ста-
тичны и используются с неподвижной геометрией, так как совершенно неприменимы
к динамическим объектам, например персонажам. Карты освещения генерируются
автоматически. Компьютер вычисляет, как имеющиеся в сцене источники света будут
освещать уровень, оставляя в углах небольшое затемнение. Система визуализации
карт освещения в Unity называется Enlighten. Используйте это название как ключевое
поисковое слово в справочниках по Unity.
К счастью, нам не приходится делать выбор между визуализацией теней в реальном
времени и картами освещения на уровне сцены в целом. У источников света есть
свойство Culling Mask, позволяющее визуализировать в реальном времени тени только
для определенных объектов, сымитировав детализированные тени от остальных объ-
ектов сцены с помощью карт освещения. Кроме того, хотя основной персонаж обыч-
но в обязательном порядке отбрасывает тень, далеко не всегда нужно, чтобы на нем
формировались тени от окружающих объектов. Поэтому для всех сеточных объектов
можно включать функции отбрасывания и восприятия теней, как показано на рис. 8.6.

Сетка проецирует
тени

Сетка затемняется проецируемыми


на нее тенями

Рис. 8.6. Параметры Cast Shadows и Receive Shadows на панели Inspector

ОПРЕДЕЛЕНИЕ  Термин отбор (culling) в общем случае означает исключение ненужных вещей.
В статьях о компьютерной графике он появляется в разных контекстах. В нашем случае свойство
Culling mask определяет набор объектов, которые не будут отбрасывать тени.

Итак, вы познакомились с основами процедуры добавления теней. Освещение сцен


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

8.1.3. Облет камеры вокруг персонажа


В демонстрационном ролике от первого лица мы связывали камеру с объектом-персо-
нажем на вкладке Hierarchy, обеспечивая их совместное вращение. В игре же от третьего
лица персонаж будет поворачиваться независимо от камеры. Поэтому связывать камеру
с персонажем на вкладке Hierarchy мы на этот раз не будем. Вместо этого напишем код,
который будет менять положение камеры вместе с положением персонажа, повора-
чивая ее независимо от последнего.
Первым делом выберем положение камеры относительно персонажа. Я ввел в поля
position значения 0, 3.5, -3.75, расположив камеру чуть выше персонажа и за его спиной
180    Глава 8. Игра от третьего лица: перемещения и анимация игрока

(в полях rotation при этом должны быть значения 0, 0, 0). После этого нужно создать
сценарий OrbitCamera и добавить в него код следующего листинга. Присоедините сце-
нарий к камере в виде нового компонента, а затем перетащите объект player на ячейку
Target. Запустите воспроизведение сцены, чтобы посмотреть, как работает код камеры.

Листинг 8.1. Сценарий вращения нацеленной на объект камеры вокруг объекта


using UnityEngine;
using System.Collections; Сериализованная ссылка
на объект, вокруг которого
производится облет.
public class OrbitCamera : MonoBehaviour {
[SerializeField] private Transform target;

public float rotSpeed = 1.5f;


private float _rotY;
private Vector3 _offset;

void Start() { Сохраняем начальное


_rotY = transform.eulerAngles.y; смещение между
_offset = target.position - transform.position; камерой и целью.
}
Медленный поворот
void LateUpdate() {
камеры при помощи
float horInput = Input.GetAxis("Horizontal"); клавиш со стрелками…
if (horInput != 0) {
_rotY += horInput * rotSpeed; … или быстрый
} else { поворот с помощью
_rotY += Input.GetAxis("Mouse X") * rotSpeed * 3; мыши.
}

Quaternion rotation = Quaternion.Euler(0, _rotY, 0);


transform.position = target.position - (rotation * _offset);
transform.LookAt(target); Где бы ни находилась Поддерживаем начальное
} камера, она всегда смещение, сдвигаемое
} смотрит на цель. в соответствии с поворо-
том камеры.

В листинге обратите внимание на переменную для целевого объекта. Код должен


знать, вокруг какого объекта будет вращаться камера, поэтому данная переменная
была сериализована, чтобы появиться в редакторе Unity и дать возможность связать
с ней объект player. Следующая пара переменных связана с углами поворота и ис-
пользуется тем же способом, что и в коде управления камерой в главе 2. Еще код со-
держит объявление переменной _offset; в методе Start() ей присваивается разница
в положении камеры и целевого объекта. Она позволяет в процессе выполнения кода
сценария сохранять относительное положение камеры. Другими словами, камера все
время остается на одном и том же расстоянии от целевого объекта, в какую бы сторону
она ни поворачивалась. Остальная часть кода помещена в метод LateUpdate().
СОВЕТ  Метод LateUpdate() также относится к классу MonoBehaviour и, подобно методу Update(),
запускается в каждом кадре. Как понятно из его имени, он вызывается для всех объектов после того,
как с ними поработал метод Update(). В результате обновление камеры происходит только после
перемещения целевого объекта.
8.1. Корректировка положения камеры   181

Во-первых, код увеличивает значение поворота в зависимости от элементов управле-


ния вводом. У нас есть элементы двух типов: клавиши с горизонтальными стрелками
и горизонтальные перемещения указателя мыши,  — поэтому для переключения
между ними используется условный оператор. Код проверяет, нажимаются ли
клавиши со стрелками; при положительном результате проверки применяется этот
тип ввода, в противном случае проверяется указатель мыши. Независимая проверка
двух вариантов ввода позволяет в каждом случае задавать собственную скорость
вращения.
Во-вторых, код задает положение камеры на основе положения целевого объекта и угла
поворота. Скорее всего, самый непонятный фрагмент кода — это строка transform.
position, содержащая математические вычисления, с которыми вы пока не сталки-
вались. Умножение вектора position на кватернион (обратите внимание, что угол
поворота преобразован в кватернион методом Quaternion.Euler) дает новое поло-
жение, смещенное в соответствии с углом поворота. Этот новый вектор положения
затем прибавляется к смещению от положения персонажа, что дает нам положение
камеры. Этапы и подробный механизм вычислений этой компактной строчки кода
иллюстрирует рис. 8.7.

Умножаем вектор смещения на кватернион


для получения положения смещения после поворота

transform.position = target.position - (rotation * _offset);

Затем определяем положение камеры, вычитая смещение


после поворота из положения целевого объекта

1. Определяем положение камеры, 2. Умножаем вектор смещения 3. Вычитаем из положения игрока,


чтобы выяснить ее смещение на кватернион для получения чтобы определить смещение
относительно персонажа смещения в результате поворота относительно игрока

Положение смещения
Итоговое
положение
камеры

Положение игрока

Рис. 8.7. Этапы вычисления положения камеры

ПРИМЕЧАНИЕ  У знатоков математики может возникнуть вопрос: «Почему не воспользовать-


ся преобразованиями от одной координатной системы к другой, о которых шла речь в главе 2?»
Разумеется, мы можем преобразовать положение точки смещения, воспользовавшись углами
Эйлера, но для этого сначала нужно перейти к  другой системе координат. Намного проще без
этого обойтись.
182    Глава 8. Игра от третьего лица: перемещения и анимация игрока

Завершает код метод LookAt(), направляющий камеру на целевой объект; он предна-


значен для нацеливания одного объекта (не обязательно камеры) на другой. Рассчи-
танное ранее значение поворота используется для размещения камеры под нужным
углом к целевому объекту, но при этом камера уже не поворачивается, меняется только
ее положение. То есть без заключительной строки с методом LookAt камера будет
летать вокруг персонажа, при этом смотря в разные стороны. Превратите эту строку
в комментарий и посмотрите, что получится.
Итак, мы написали сценарий, перемещающий камеру вокруг персонажа; пришла оче-
редь кода, перемещающего персонаж по сцене.

8.2. Элементы управления движением,


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

ЧТО ОЗНАЧАЕТ «СВЯЗАННЫЕ С КАМЕРОЙ»?_ __________________________


Принцип «связи с камерой» неочевиден, но понять его крайне важно. Во многом он напоминает
уже знакомую ситуацию с локальными и глобальными координатами, когда «лево» с точки зрения
объекта может не совпадать с «лево» в общем смысле. Аналогично обстоит дело с выражением
«персонаж бежит налево». Подразумеваться может как левая сторона с точки зрения персонажа,
так и левая сторона экрана.
В игре от первого лица камера помещена внутрь персонажа и перемещается вместе с ним, по-
этому понятие «лево» с точки зрения камеры и персонажа одно и то же. В игре от третьего лица
камера и персонаж существуют отдельно друг от друга, поэтому, к примеру, если камера смотрит
персонажу в лицо, лево с точки зрения камеры и с точки зрения персонажа находится с противо-
положных сторон. Именно поэтому настройки элементов управления нужно выбирать заранее.
Тут возможны разные варианты, но, как правило, в играх от третьего лица элементы управления
настраиваются относительно камеры. При нажатии клавиши с левой стрелкой персонаж бежит по
экрану налево. Многочисленные эксперименты с другими схемами элементов управления показа-
ли, что интуитивно понятным является именно вариант, когда «лево» означает «левую сторону
экрана» (что далеко не всегда совпадает с левой стороной персонажа).

Реализация связанных с камерой элементов управления состоит из двух этапов: сна-


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

8.2.1. Поворот персонажа лицом в направлении движения


Напишем код, разворачивающий персонаж в направлении клавиш со стрелками.
Создайте сценарий RelativeMovement (листинг 8.2), перетащите его на объект player
и свяжите камеру со свойством Target компонента Script (таким же образом, как вы
связывали персонаж со свойством Target сценария камеры). После этого при нажатии
управляющих клавиш персонаж будет поворачиваться в разные стороны, выбирая
8.2. Элементы управления движением, связанные с камерой   183

направление относительно камеры, а при его вращении с помощью мыши останется


статичным.

Листинг 8.2. Поворот персонажа относительно камеры


using UnityEngine; Сценарию нужна ссылка на
using System.Collections; объект, относительно которого
происходит перемещение.
public class RelativeMovement : MonoBehaviour {
[SerializeField] private Transform target;
Начинаем с вектора (0, 0, 0),
постепенно добавляя
void Update() { компоненты движения.
Vector3 movement = Vector3.zero;
Движение обрабатывается
float horInput = Input.GetAxis("Horizontal"); только при нажатии клавиш
float vertInput = Input.GetAxis("Vertical"); со стрелками.
if (horInput != 0 || vertInput != 0) {
movement.x = horInput;
Сохраняем начальную ориентацию,
movement.z = vertInput;
чтобы вернуться к ней после завер-
шения работы с целевым объектом.
Quaternion tmp = target.rotation;
target.eulerAngles = new Vector3(0, target.eulerAngles.y, 0);
movement = target.TransformDirection(movement); Преобразуем направление
target.rotation = tmp; движения из локальных
в глобальные координаты.
transform.rotation = Quaternion.LookRotation(movement);
}
} Метод LookRotation() вычисляет кватернион,
} смотрящий в этом направлении.

Как и в листинге 8.1, наш код начинается с сериализованной переменной для целевого
объекта. Если в предыдущем случае требовалась ссылка на объект, вокруг которого
должен совершаться облет, то теперь нужна ссылка на объект, относительно которо-
го будет выполняться перемещение. После чего переходим к методу Update(). Его
первая строчка объявляет, что Vector3 имеет значение  0, 0, 0. Важно взять нулевой
вектор и затем присвоить ему значения, а не просто создать вектор с вычисленными
значениями перемещения. Дело в том, что перемещение по вертикали и по горизон-
тали вычисляется на разных этапах, но при этом их значения должны быть частями
одного и того же вектора.
Затем проверяем элементы управления вводом, как это делалось в предыдущих сцена-
риях. Именно здесь вектору движения присваиваются значения координат X и Z, опре-
деляющие горизонтальные перемещения по сцене. Если ни одна клавиша не нажата,
метод Input.GetAxis() возвращает значение 0, а при нажатии клавиш со стрелками его
значение меняется от 1 до –1. Присваивая это значение вектору движения, мы задаем
перемещение в положительном или отрицательном направлении оси (в случае оси X
это перемещение влево и вправо, в случае оси Z — вперед и назад).
В следующих строках вектор движения корректируется относительно камеры, а имен-
но: метод TransformDirection() выполняет преобразование локальных координат
в глобальные. Он уже применялся для этой цели в главе 2, но на этот раз мы соверша-
ем переход для системы координат целевого объекта, а не персонажа. Код до и после
184    Глава 8. Игра от третьего лица: перемещения и анимация игрока

строки с методом TransformDirection() выравнивает систему координат нужным нам


образом: первым делом сохраняется ориентация целевого объекта, чтобы потом к ней
можно было вернуться, а затем преобразование поворота корректируется таким об-
разом, чтобы оно совершалось только относительно оси Y, а не всех трех осей. После
чего мы выполняем преобразование и восстанавливаем ориентацию целевого объекта.
Весь этот код вычисляет направление движения в виде вектора. Последняя строка на-
значает полученный результат к персонажу, преобразуя Vector3 в Quaternion методом
Quaternion.LookDirection() и присваивая это значение. Попробуйте запустить игру
и посмотреть, что получится!

ПЛАВНОЕ ВРАЩЕНИЕ С ПОМОЩЬЮ АЛГОРИТМА


ЛИНЕЙНОЙ ИНТЕРПОЛЯЦИИ________________________________________
Сейчас персонаж меняет ориентацию скачками, мгновенно поворачиваясь лицом в направлении,
заданном клавишей. Но плавное движение смотрится куда лучше. Здесь на помощь приходит
линейный алгоритм интерполяции. Добавьте в сценарий переменную:
public float rotSpeed = 15.0f;
Затем замените строчку transform.rotation... в конце листинга 7.2 следующим кодом:
...
Quaternion direction = Quaternion.LookRotation(movement);
transform.rotation = Quaternion.Lerp(transform.rotation, direction,
rotSpeed * Time.deltaTime);
}
}
}
В результате вместо привязки непосредственно к  значению, возвращаемому методом
LookRotation(), это значение начнет использоваться в качестве целевого положения, в ко-
торое объект будет постепенно поворачиваться. Метод Quaternion.Lerp() выполняет плавный
поворот из текущего положения в  целевое (третий параметр метода контролирует скорость
вращения).
Кстати, плавный переход от одного значения к другому называется интерполяцией; интерполиро-
вать можно значения любых типов. Термин Lerp расшифровывается как linear interpolation — линей-
ная интерполяция. В Unity есть методы Lerp как для векторов, так и для значений типа float (то
есть интерполировать можно положение объектов, цвета и другие параметры). Для кватернионов
имеется родственный метод Slerp (сферической линейной интерполяции). Для поворотов на
маленькие углы преобразования Slerp зачастую подходят лучше, чем Lerp.

Пока персонаж умеет только поворачиваться на месте; в следующем разделе мы на-


пишем код, который заставит его перемещаться по сцене.

ПРИМЕЧАНИЕ  Так как поворотом налево и направо управляют клавиши, отвечающие за облет
камеры, поворот персонажа в сторону будет сопровождаться медленным вращением. Такое дубли-
рование функций элементов управления является желательным поведением для данного проекта.

8.2.2. Движение вперед в выбранном направлении


В главе 2 вы узнали, что для перемещения персонажа по сцене к нему нужно добавить
соответствующий контроллер. Выделите объект player и выберите в меню Components
8.2. Элементы управления движением, связанные с камерой   185

команду Physics > Character Controller. На панели Inspector уменьшите параметр Radius до
0,4, остальным параметрам оставьте значения по умолчанию, так как они вполне под-
ходят для нашей модели персонажа.
Следующий листинг содержит код, который нужно добавить в  сценарий
RelativeMovement.

Листинг 8.3. Код, меняющий положение персонажа


using UnityEngine;
Окружающие строки показывают
using System.Collections; контекст размещения метода
RequireComponent().
[RequireComponent(typeof(CharacterController))]
public class RelativeMovement : MonoBehaviour {
...
public float moveSpeed = 6.0f; Шаблон, знакомый по
предыдущим главам.
private CharacterController _charController; Используется для
доступа к другим
void Start() { компонентам.
_charController = GetComponent<CharacterController>();
}
Переписываем существующие
void Update() {
строки X и Z, чтобы добавить
... скорость движения.
movement.x = horInput * moveSpeed;
movement.z = vertInput * moveSpeed;
movement = Vector3.ClampMagnitude(movement, moveSpeed);
... Ограничиваем движение по диагонали той
} же скоростью, что и движение вдоль оси.

movement *= Time.deltaTime;
Перемещения всегда нужно умножать
_charController.Move(movement); на deltaTime, чтобы они были
} независимыми от частоты кадров.
}

Запустив воспроизведение игры, вы увидите, как персонаж перемещается по сцене


(в T-образной позе). Практически весь код этого листинга вы уже видели, поэтому
ограничусь кратким обзором.
Во-первых, в верхней части кода появился метод RequireComponent(). В главе 2 я объ-
яснял, что этот метод заставляет Unity проверять наличие у объекта GameObject ком-
понента именно того типа, который был передан в команду. Эта строка добавляется
по желанию; но без этого компонента в сценарии появятся ошибки.
Затем мы объявляем переменную для перемещений, после чего сценарий получает
ссылку на контроллер персонажа. Как вы помните, метод GetComponent() возвращает
остальные присоединенные к этому объекту компоненты, и если объект не указыва-
ется явным образом, подразумевается вариант this.GetComponent() (то есть объект,
с которым работает сценарий).
Величины перемещений берутся из данных, которые предоставляют элементы управ-
ления вводом. В предыдущем листинге такой прием уже использовался, но сейчас мы
186    Глава 8. Игра от третьего лица: перемещения и анимация игрока

учитываем еще и скорость перемещения. Мы умножаем обе оси, вдоль которых про-
исходит движение, на его скорость, а затем метод Vector3.ClampMagnitude() ограни-
чивает модуль вектора этой скоростью. Если так не сделать, у движения по диагонали
будет больший вектор, чем у движения вдоль осей (нарисуйте катеты и гипотенузу
прямоугольного треугольника).
Напоследок мы умножаем значения перемещения на параметр deltaTime, чтобы сделать
данное преобразование независимым от частоты кадров (это обеспечивает перемеще-
ние персонажа с одной и той же скоростью на разных компьютерах с разной частотой
кадров). Полученные значения передаются методу CharacterController.Move(), ко-
торый и приводит персонаж в движение.
Этот код обеспечивает перемещения в горизонтальном направлении, нам же нужно,
чтобы персонаж мог перемещаться еще и по вертикали.

8.3. Прыжки
В предыдущем разделе мы написали код перемещения персонажа по поверхности. Но
в начале главы упоминалась возможность прыжков. Пришла пора ее добавить, тем бо-
лее что в большинстве игр от третьего лица есть соответствующий элемент управления.
И даже при его отсутствии перемещение по вертикали происходит практически всегда,
так как персонаж может сорваться вниз, например, пытаясь преодолеть пропасть. Код,
который мы добавим, будет обрабатывать как прыжки, так и падения. Точнее, он будет
включать в себя силу тяжести, все время тянущую персонаж вниз, а в момент прыжка
его будет толкать вверх.
Но сначала добавим в сцену пару платформ. Ведь пока персонажу некуда запры-
гивать и неоткуда падать! Создайте несколько кубов, поменяйте их размер по соб-
ственному вкусу и расположите в сцене. Лично я добавил два куба со следующими
параметрами: Position 5, 0.75, 5 Scale 4, 1.5, 4; и  Position 1, 1.5, 5.5, Scale 4, 3, 4. Их вид
показан на рис. 8.8.

Position 1, 1.5, 5.5


Scale 4, 3, 4

Position 5, .75, 5
Scale 4, 1.5, 4

Рис. 8.8. Пара платформ, добавленных в сцену

8.3.1. Вертикальная скорость и ускорение


В начале работы над сценарием RelativeMovement я упоминал, что величины переме-
щений вычисляются по отдельности и все время добавляются к вектору перемещения