Определение функции
В действительности вы писали функции, начиная с первой программы Hello World,
Определение функции 429
В первый листинг этой главы были включены все строки, составляющие класс
CodeExample, но в последующих примерах будет приводиться не весь код. Если
вы захотите опробовать примеры, следующие далее, вы должны заключить код
в класс. Больше подробностей вы узнаете в главе 26 «Классы», а пока просто
добавляйте строки, выделенные жирным в следующем листинге — вокруг кода
примеров:
1 using UnityEngine;
2 using System.Collections;
3
4 public class CodeExample : MonoBehaviour {
5
// Вставляйте код из листингов на место этого комментария
16
17 }
Например, вот как мог бы выглядеть первый в данной главе листинг без этих
строк:
Чтобы ввести строки 6-15 из этого листинга в сценарий на С#, вам нужно добавить
вокруг них строки, выделенные жирным в предыдущем сценарии. В результате
в MonoDevelop должен получиться сценарий, как показано в первом листинге
в этой главе.
В оставшейся части главы нумерация строк в листингах будет начинаться с 6,
чтобы показать, что перед этим кодом и за ним должны находиться другие строки.
432 Глава 24. Функции и параметры
Как вы узнали в главе 19 «Hello World: ваша первая программа», метод Update()
каждого игрового объекта GameObject вызывается в каждом кадре, а метод
Start() — один раз, непосредственно перед первым вызовом UpdateQ. Во
многих сценариях Unity кроме Start() и UpdateQ используется также метод
Awake(). Он вызывается один раз, так же как метод Start(), но вызов его про
исходит непосредственно в момент создания игрового объекта. То есть вызов
метода Awake() каждого игрового объекта всегда происходит непосредственно
перед Start().
Параметры и аргументы функций 433
void Test() {
print( ’’Before instantiation" );
Instantiate<GameObject>( testPrefab );
print( "After instantiation" );
}
Возвращаемые значения
Кроме приема входных значений в параметрах функции могут также возвращать
единственное значение — результат функции, как показано в строке 13 следую
щего листинга:
6 void Awake() {
7 int num = Add( 2, 5 );
8 print( num ); // Выведет в консоль число 7
9 }
10
11 int Add( int numA, int numB ) {
12 int sum = numA + numB;
13 return( sum );
14 }
В этом примере функция Add() имеет два параметра, целочисленные numA и numB.
Она складывает два целых числа, переданных ей, и возвращает результат. Слово
int в начале объявления функции в строке И указывает, что Add() возвращает
целое число в результате. Так же как, объявляя переменную, вы должны указать
ее тип, объявляя функцию, вы должны указать тип возвращаемого ею результата.
14 if (go.name == theName) { // d
15 go.transform.position = Vector3.zero; П e
16 return; // f
17 }
18 }
19 }
a. List<GameObject> reallyLongList — очень длинный список игровых объектов,
который, как предполагается, предопределен в инспекторе Unity. Так как в дей
ствительности список reallyLongList не существует, этот код не будет работать,
если вы не определите его сами.
b. Вызов функции MoveToOrigin() со строковым литералом "Phil" в виде аргу
мента.
c. Инструкция foreach выполняет обход элементов списка reallyLongList.
d. Если найден первый игровой объект с именем "Phil" (то есть с именем, указан
ным в theName)...
e. ...он перемещается в начало координат — в точку [ 0, 0, 0 ].
f. Строка 16 возвращает управление в строку 9. Это предотвращает обход осталь
ных игровых объектов в списке.
В функции MoveToOrigin() нас действительно не интересуют другие игровые объ
екты, кроме объекта с именем Phil, поэтому лучше прервать работу функции раньше
и вернуть управление, не тратя времени на обход остальных элементов списка. Если
объект Phil окажется последним в списке, никакой экономии не получится; но если
Phil окажется первым в списке, экономия будет огромная.
Обратите внимание, что когда return используется в функции с типом возвра
щаемого значения void, ее можно использовать без круглых скобок (и даже когда
возвращаемое значение имеется, круглые скобки можно опустить).
бится изменить в нем, изменения придется внести в трех местах в функции AlignX().
Это одна из главных причин использования функций. В следующем листинге
строки 11-23 из предыдущего листинга заменены вызовами новой функции SetX().
Жирным выделены строки, изменившиеся в сравнении с предыдущим листингом.
6 void AlignX( GameObject go0, GameObject gol, GameObject go2 ) {
7 float avgX = go0.transform.position.x;
8 avgX += gol.transform.position.x;
9 avgX += go2.transform.position.x;
10 avgX = avgX/3.0f;
11
12 SetX ( go0, avgX );
13 SetX ( gol, avgX );
14 SetX ( go2, avgX );
15 }
16
17 void SetX( GameObject go, float eX ) {
18 Vector3 tempPos = go.transform.position;
19 tempPos.x = eX;
20 go.transform.position = tempPos;
21 }
Перегрузка функций
Перегрузка функций — причудливый термин, описывающий возможность функций
в C# действовать по-разному в зависимости от типов и количества передаваемых
им параметров. В следующем листинге жирным выделены фрагменты, демонстри
рующие перегрузку функций.
6 void Awake() {
7 print( Add( 1.0f, 2.5f ) );
8 // Л Выведет: ”3.5”
9 print( Add( new Vector3(l, 0, 0), new Vector3(0, 1, 0) ) );
10 11 л Выведет "(1.0, 1.0, 0.0)”
11 Color colorA = new Color( 0.5f, 1, 0, 1);
12 Color colorB = new Color( 0.25f, 0.33f, 0, 1);
13 print( Add( colorA, colorB ) );
438 Глава 24. Функции и параметры
Необязательные параметры
Иногда желательно, чтобы функция имела необязательные параметры, которые
могут передаваться или не передаваться. В следующем листинге параметр еХ функ
ции SetX() является необязательным. Если задать значение по умолчанию для
параметра в определении функции, компилятор будет интерпретировать его как
необязательный (как, например, в строке 13 в следующем листинге, где параметр еХ
получает значение по умолчанию 0,Of). Код, выделенный жирным, демонстрирует
использование необязательного параметра.
6 void Awake() {
7 SetX( this.gameObject, 25 ); // b
Ключевое слово params 439
9 SetX( this.gameObject ); // с
10 print( this.gameObject.transform.position.x ); // Выведет: •’0"
11 }
12
13 void SetX( GameObject go, float eX=0.0f ) { // а
14 Vector3 tempPos = go.transform.position;
15 tempPos.x = eX;
16 go.transform.position = tempPos;
17 }
1 Точнее говоря, переменная типа float может хранить большинство значений типа int.
Как рассказывалось в главе 19 «Переменные и компоненты», тип float не способен точно
представлять очень большие и очень маленькие числа, поэтому очень большие значения
int могут округляться до ближайшего числа, которое может быть представлено типом
целые
440 Глава 24. Функции и параметры
Теперь функция Add() может принимать любое количество целых чисел и воз
вращать их сумму Подобно необязательным параметрам, список params должен
следовать в объявлении функции после всех обязательных параметров (то есть все
обязательные параметры должны быть объявлены перед списком params).
Это ключевое слово позволяет также переписать предыдущую функцию AlignX(),
чтобы она принимала любое количество игровых объектов, как показано в следу
ющем листинге.
6 void AlignX( params GameObject[] goArray ) { // a
7 float sumX = 0;
8 foreach (GameObject go in goArray) { // b
9 sumX -»■= go.transform.position.x; 11 c
10 }
11 float avgX = sumX / goArray.Length; H d
12
13 foreach (GameObject go in goArray) { H e
14 SetX ( go, avgX );
15 }
16 }
17
18 void SetX( GameObject go, float eX ) {
19 Vector3 tempPos = go.transform.position;
20 tempPos.x = eX;
21 go.transform.position = tempPos;
22 }
a. Ключевое слово params создает массив и включает в него все игровые объекты,
переданные в вызов функции.
b. Цикл foreach выполняет обход всех игровых объектов в goArray. Переменная
GameObject go доступна только в теле цикла foreach, в строках 8-10, поэтому она
не конфликтует с переменной GameObject go в цикле foreach в строках 13-15.
c. Координата X текущего игрового объекта добавляется к сумме sumX.
d. Определяется среднее значение координаты X делением суммы на количество
игровых объектов. Обратите внимание, что если в вызов функции не передать
ни одного игрового объекта, эта строка вызовет ошибку деления на ноль.
e Второй цикл foreach выполняет обход игровых объектов в goArray и вызывает
Рекурсивные функции 441
Рекурсивные функции
Иногда функции должны вновь и вновь вызывать самих себя, — такие функции
называют рекурсивными. Простым примером может служить вычисление факто
риала числа.
В математике 5! (5 факториал) — это произведение этого числа и всех других на
туральных чисел ниже него. (Натуральными называют целые числа больше нуля.)
51=5*4*3*2*1= 120
Итоги
В этой главе вы познакомились с функциями и некоторыми разными способами
их использования. Функции составляют основу большинства современных языков
программирования, и чем больше вы будете программировать, тем очевиднее для
вас будет важность и нужность функций.
Следующая глава, «Отладка», покажет вам, как пользоваться инструментами от
ладки в Unity. Эти инструменты призваны помогать с поиском проблем в коде, но их
также с успехом можно использовать для изучения работы кода. После знакомства
с отладкой я советую вернуться сюда и более внимательно исследовать работу функ
ции Fac(). Разумеется, вы также можете заняться исследованием любых функций
из этой главы с помощью отладчика.
Отладка
Знакомство с отладкой
Прежде чем начать поиск ошибок, их нужно сначала допустить. В этой главе мы
начнем с проекта, созданного в главе 19 «Hello World: ваша первая программа».
Если у вас нет этого проекта под рукой, вы всегда можете загрузить его с веб-сайта
книги http://book.prototools.net/.
На веб-сайте найдите раздел Chapter 25: "Debugging" и щелкните на нем, чтобы за
грузить начальный проект для этой главы.
На протяжении этой главы я неоднократно буду предлагать вам преднамеренно
вносить ошибки. Это может показаться странным, но в этой главе моя цель — дать
вам некоторый опыт поиска и исправления ошибок, с которыми вы наверняка
столкнетесь, работая в Unity. Каждый пример знакомит с одной из потенциальных
проблем и показывает, как искать и исправлять их.
~»1 На протяжении всей главы я буду ссылаться на ошибки по номерам строк. Иногда эти
номера будут точно соответствовать строкам с ошибками, иногда номера будут смещены вниз
просто
444 Глава 25. Отладка
СОВЕТ ПРОФЕССИОНАЛА
ы 90% ОШИБОК — ПРОСТЫЕ ОПЕЧАТКИ. Я потратил так много времени, помогая
студентам исправлять ошибки, что с ходу замечаю опечатки в коде1. Вот наиболее распро
страненные из них:
Орфографические ошибки: даже если вы ошибетесь в единственной букве, компьютер не
поймет, что вы хотели сказать ему.
Ошибки в регистре символов: для компилятора C# буквы А и а — это два разных символа,
то есть с его точки зрения слова variable, Variable и variAble — это три совершенно
разных слова.
Отсутствие точки с запятой: так же, как в русском языке, почти все предложения должны
заканчиваться точкой, почти все инструкции в C# должны заканчиваться точкой с запятой
( ; ). Отсутствие точки с запятой часто заставляет компилятор сообщать об ошибке в сле
дующей строке. К вашему сведению: эта роль возложена на точку с запятой по той простой
причине, что точка уже используется для отделения целой и дробной частей в вещественных
числах, а также в точечном синтаксисе ссылок на переменные (например, varName. х).
A$sets/CubeSpawnerlxs(4,14): error CSO1O1: The namespace ' global;:* already contains a definition for ' Cub<
_j\______ ;________________________________________________________________________________________
\ssets/CubeSpawner 1X5(4,14): error CSO1O1: The namespace 'global::' already contains a definition for
'CubeSpawner*
по словам «Unity error» с кодом ошибки. (В данном случае строка поиска должна
иметь вид «Unity error CS0101».) Подобный поиск почти всегда приводит к со
общению на форуме или в другое место, где дается описание вашей проблемы.
Как показывает мой опыт, хорошие ответы обычно можно найти на http://forum.
unity3d.com и http://answers.unity3d.com, а некоторые из лучших ответов — на http://
stackoverflow.com.
The namespace ’global::’ already contains a definition for 'CubeSpawner11
Qconsole ; . -7-:"
Clear Collapse I Clear on play 1 Error pause I Open Player Log Open Editor Log
YOU probably need to assign the cubePrerabVar variable or the CubeSpawner2 script in the inspector. A
UnassignedReferenceException: The variable cubePrefabVar of *CubeSpawner2’ has not been assigned.
" You probably need to assign the cubePrefabVar variable of the CubeSpawner2 script in the inspector.
Это ошибка времени выполнения, то есть ошибка, возникающая, только когда Unity
фактически пытается запустить проект. Ошибки времени выполнения возникают,
когда вы набрали код правильно (с точки зрения компилятора), но что-то идет не
так, когда этот код выполняется.
Это сообщение об ошибке выглядит немного иначе, чем предыдущие, что мы видели.
Прежде всего, сообщение об ошибке не содержит информации о месте ее появления;
но если щелкнуть на одном из сообщений, в нижней половине консоли появится
дополнительная информация. Для ошибки времени выполнения последняя строка
сообщения подсказывает, где она была замечена. Иногда это место соответствует
строке с ошибкой, иногда следующей строке. В данном случае сообщение указыва
ет, что ошибку нужно искать где-то рядом со строкой 14 в файле CubeSpawner2.cs.
CubeSpawner2.Update () (at Assets/CubeSpawner2.cs:14)
С точки зрения компилятора в этой строке нет ошибок. Давайте продолжим ис
следование сообщения:
UnassignedReferenceException: The variable cubePrefabVar of 'CubeSpawner2' has
not been assigned. You probably need to assign the cubePrefabVar variable of the
CubeSpawner2 script in the inspector. UnityEngine.Object.Instantiate (UnityEngine.
Object original) CubeSpawner2.Update () (at Assets/CubeSpawner2.cs:l4)1
6
7 // Используйте этот метод для инициализации
8 void Start () {
9 // Instantiate( cubePrefabVar );
10 }
11
12 // Update вызывается в каждом кадре
13 void Update () {
14 SpellltOutQ; 11 b
15 Instantiate( cubePrefabVar );
16 }
17
18 public void SpellltOut () { // c
19 string sA = "Hello World!";
20 string sB = "";
21
22 for (int i=0; i<sA.Length; i++) { 11 d
23 sB += sA[i]; // e
24 }
25
26 print(sB);
27 }
28 }
Рис. 25.6. Щелкните на кнопке, чтобы подключить отладчик к процессу Unity Editor
HrtloWorid.cs
CubeSpawner? ► Updated
1 using UnityEngine;
2 using Systen.Collections;
3
4(3 public class Cubespawner2 : MonoSehaviour {
public GamObject cubePrefabVar;
print(sB);
CJ void SpehltOutO
£;votdSt»rtO
void UpdateO
Q GameObject cubePrefabVar
3. Когда выполнение вновь остановится в строке 14, щелкните на кнопке Step Over
(Шаг через). Желтая подсветка переместится на строку 15 без входа в функцию
SpellItOut(). Функция SpellItOut() запустится и вернет управление, но отлад
чик перешагнет через нее. Операцию Step Over (Шаг через) удобно использовать,
когда не требуется исследовать работу вызываемой функции.
4. Снова щелкните на кнопке Run (Пуск). Unity перейдет к выполнению следую
щего кадра, и выполнение снова остановится в строке 14.
5. На этот раз щелкните на кнопке Step Into (Шаг внутрь). Желтая подсветка пере
прыгнет из строки 14встроку 19 в функции SpellItOut(). Когда вы щелкаете на
кнопке Step Into (Шаг внутрь), отладчик входит в вызываемую функцию, тогда
как щелчок на Step Over (Шаг через) просто выполняет функцию, не входя в нее.
6. Теперь, находясь внутри функции SpellItOut(), щелкните несколько раз на
кнопке Step Over (Шаг через), чтобы пройти по строкам в функции SpellltOut ().
7. Продолжая щелкать на Step Over (Шаг через), можете понаблюдать, как изме
456 Глава 25. Отладка
Итоги
Это было краткое введение в отладку. Даже при том, что мы не использовали отлад
чик для поиска ошибок, вы смогли увидеть, как он помогает понять код. Запомните:
всякий раз, когда вы сомневаетесь в работе кода, вы всегда можете выполнить его
по шагам в отладчике.
Признаюсь честно: было не особенно приятно заставлять вас намеренно добавлять
ошибки в код, но я искренне надеюсь, что смог дать вам опыт встречи с ними и по
казать, как их анализировать и исправлять, что поможет вам в будущем, когда вы
столкнетесь с настоящими ошибками в своем коде. Помните, что всегда можно
попробовать поискать в интернете (по тексту ошибки или хотя бы по ее коду) ре
цепт исправления. Как я писал в начале этой главы, умение пользоваться отладчи
компетентным
Классы
К концу этой главы вы узнаете, как создавать классы. Класс — это коллекция пере
менных и функций в одном объекте на С#. Классы являются основными строительными
блоками в современных играх и, в более широком смысле, в объектно-ориентированном
программировании.
Основы классов
Классы объединяют функции и данные. Говоря иначе, классы состоят из функций
и переменных, которые, когда используются в классе, называются методами и по
лями соответственно. Классы часто используются для представления объектов
в мире вашего игрового проекта. Например, представьте персонажа в типичной
ролевой игре. Такой персонаж мог бы иметь несколько полей (или переменных):
string name; // Имя персонажа
float health; // Текущий уровень здоровья персонажа
float healthMax; // Максимальный уровень здоровья, который он мог бы иметь
List<ltem> inventory; // Список всех предметов, которыми может владеть персонаж
List<Item> equipped; // Список предметов, которыми он уже владеет
Все эти поля есть у каждого персонажа в ролевой игре, потому что все персонажи
имеют определенный уровень здоровья, владеют определенным набором предметов
и имеют имена. Кроме того, некоторые методы (функции) могли бы использоваться
персонажем или применяться для управления им. (Многоточия [... ] в следующем
листинге отмечают место, куда нужно добавить код, описывающий работу функций.)
void Move(Vector3 newLocation) {...} // Позволяет персонажу перемещаться
void Attack(Character target) {...} // Атакует указанного персонажа target
// текущим оружием или заклинанием
void TakeDamage(float damageAmt) (...) // Понижает уровень здоровья персонажа
void Equip(Item newltem) (...) // Добавляет предмет newltem в список
// equipped предметов, имеющихся в наличии
1 using System.Collections;
2 using System.Collections.Generic;
3 using UnityEngine;
4
5 public class Enemy : MonoBehaviour {
6
7 public float speed = 10f; // Скорость в м/с
8 public float fireRate = 0.3f; // Выстрелов в секунду (не используется)
9
10 // Update вызывается в каждом кадре
11 void Update() {
12 Move();
13 }
14
15 public virtual void Move() {
16 Vector3 tempPos = pos;
17 tempPos.у -= speed * Time.deltaTime;
18 pos = tempPos;
19 }
20
21 void OnCollisionEnter( Collision coll ) {
22 GameObject other = coll.gameobject;
23 switch (other.tag) {
24 case ''Hero":
25 // Пока не реализовано, но здесь наносятся повреждения герою
26 break;
462 Глава 26. Классы
Большая часть кода должна выглядеть для вас простой и знакомой, кроме свойства
и виртуальной функции, о которых я расскажу в этой главе.
1 using System.Collections;
2 using System.Collections.Generic;
3 using UnityEngine;
4
5 class CountltHigher : MonoBehaviour {
6 private int _num = 0; И а
7
8 void Update() {
9 print( nextNum );
10 }
11
12 public int nextNum { // ь
13 get {
14 _num++; // Увеличить значение _num на 1
15 return( num ); // Вернуть новое значение _num
16 }
17 }
18
19 public int currentNum { // с
20 get { return( _num ; } И d
21 set { _num = value; } // d
22 }
23 }
При каждом чтении свойства nextNum (в инструкции print ( nextNum );) происходит
увеличение скрытого поля _num, и затем возвращается новое значение (строки 14
и 15 в листинге). Это был очень простой пример, но вообще методы доступа get
и set могут выполнять любые операции, доступные обычным методам, и даже вы
зывать другие методы и функции.
Аналогично, currentNum — это общедоступное свойство, позволяющее читать и из
менять значение _num. Так как _num является скрытым полем, очень удобно иметь
общедоступное свойство currentNum для работы с ним.
Наследование классов
Классы обычно расширяют (дополняют) содержимое и поведение других классов
(то есть они основаны на других классах). В первом листинге в этой главе класс
Enemy расширяет MonoBehaviour, как и все классы, которые вы видели в этой книге
до сих пор. Выполните следующие инструкции, чтобы задействовать класс Enemy
в вашей игре, а потом мы продолжим обсуждение.
О Inspector
а/ Main Camera 5 OSCMk w
Projection
Size 20
Clipping Палее Near 10J
Far 1000
Viewport Rec t
X 0 Y 0
W 1 H 1
Depth
Rendering Path i U»» Plivtr Settings •J
Target Texture None (Render Texture) О
Occlusion Culling
HDR Cl •
Рис. 26.3. Настройки камеры в сцене _Scene_l и результат в панели Game (Игра)
Панель Game (Игра), изображенная справа на рис. 26.3, должна показывать сцену
такой, как она выглядит через камеру.
5. Щелкните на кнопке Play (Играть). Вы должны увидеть, как экземпляр Enemy
перемещается по экрану сверху вниз с постоянной скоростью.
6. Сохраните сцену! Всегда сохраняйте свои сцены.
Суперклассы и подклассы
Термины суперкласс и подкласс описывают отношение родства между двумя класса
468 Глава 26. Классы
3. Выберите в панели Hierarchy (Иерархия) пункт меню Create > 3D Object > Cube
(Создать > 3D объект > Куб).
a. Переименуйте его в EnemyZigGO.
b. Установите координаты EnemyZigGO в значения [ -4,4, 0 ].
c. Перетащите сценарий EnemyZig на игровой объект EnemyZigGO в панели
Hierarchy (Иерархия).
d. Перетащите EnemyZigGO из панели Hierarchy (Иерархия) в панель Project (Про
ект), чтобы создать шаблон EnemyZigGO.
4. Щелкните на кнопке Play (Играть). Заметили, что кубик EnemyZigGO падает
вниз с той же скоростью, что и сфера EnemyGO? Это объясняется тем, что класс
EnemyZig наследует поведение класса Enemy!
5. Теперь добавьте в EnemyZig новый метод Move() (новые строки выделены жир
ным):
1 using System.Collections;
2 using System.Collections.Generic;
3 using UnityEngine;
4
5 public class EnemyZig : Enemy {
6
7 public override void Move () {
8 Vector3 tempPos = pos;
9 tempPos.x = Mathf.Sin(Time.time * Mathf.PI*2) ♦ 4;
10 pos = tempPos; // используется свойство pos суперкласса
11 base.Move(); // вызов метода Move() суперкласса
12 }
Итоги 469
Итоги
Способность класса объединять данные с функциями дает разработчикам возмож
ность использовать объектно-ориентированный подход, который будет представлен
в следующей главе. Объектно-ориентированное программирование позволяет про
граммистам рассуждать о своих классах как об объектах, которые могут двигаться
и принимать решения независимо друг от друга. Этот подход очень хорошо сочета
ется с организацией Unity, основанной на игровых объектах GameObject, и помогает
легко и быстро писать игры.
Объектно-ориентированное
мышление
Объектно-ориентированная метафора
Объектно-ориентированный подход проще всего описать с применением метафо
ры. Представьте стаю птиц. Стая может включать сотни и даже тысячи отдельных
птиц, каждая из которых должна уклоняться от препятствий и других птиц, про
должая перемещаться вместе со стаей. Стаи птиц демонстрируют блестяще ско
ординированное поведение, которое на протяжении многих лег не поддавалось
моделированию.
a. Это список игровых объектов, где хранятся все враги. Ни к одному из врагов не
подключен никакой код.
b. Цикл foreach в строке 13 перебирает игровые объекты в списке врагов enemies.
c. Так как к объектам врагов не подключено никакого кода, пришлось использовать
эту инструкцию switch, реализующую движение для всех видов врагов.
В этом простом примере код получился довольно коротким и на самом деле не осо
бенно «монолитным», но он утратил изящество и способность к расширению кода
из главы 26 «Классы». Если бы кто-то взялся написать игру с 20 разными видами
врагов, используя подобный монолитный класс, единственная функция Update()
легко могла бы вырасти до нескольких сотен строк, потому что для каждого вида
472 Глава 27. Объектно-ориентированное мышление
Объектно-ориентированная
реализация стаи
В этом учебном примере мы напишем простую реализацию роя Рейнолдса, де
монстрирующую возможность создания сложного коллективного поведения с по
мощью объектно-ориентированного кода. Сначала создайте новый проект, следуя
инструкциям во врезке. Когда вы будете выполнять инструкции по реализации
этого проекта, я советую вооружиться карандашом и отмечать каждый шаг по его
выполнении.
Объектно-ориентированная реализация стаи 473
Main Camera
Directional Light
▼ Boid
▼ Fuselage
Cube
▼ Wing
Cube
Рис. 27.2. Вложенные игровые объекты в иерархии (то есть один является дочерним
по отношению к другому)
▼ Т ransform
Position X 0 Y 0 Z 0
Rotation X 7.5 Y 0 Z 0
Scale X 0.5 Y 0.5 Z 2
▼ Т ransf orm
Position X 0 Y 0 ”20
Rotation X 45 Y 0 Z 45
Объектно-ориентированная реализация стаи 475
нент > Эффекты > Визуализатор следа). В результате в Boid добавится ком
понент Trail Renderer. Выберите компонент Trail Renderer и в панели Inspector
(Инспектор):
a. Щелкните на пиктограмме с треугольником рядом с элементом Materials,
чтобы распахнуть его.
b. Щелкните на маленьком кружке справа от Element 0 None (Material).
c. В открывшемся списке выберите только что созданный материал TrailMaterial.
d. Установите параметр Time в Trail Renderer равным 1.
e. Установите ширину в поле Width компонента Trail Renderer равной 0.25. Если
теперь воспользоваться инструментом Move (Перемещение), чтобы подви
нуть Boid в окне Scene (Сцена), в процессе перемещения за объектом должен
оставаться светящийся след.
9. Пока объект Boid остается выделенным в иерархии, выберите в главном меню
Unity пункт Component > Physics > Sphere Collider (Компонент > Физика > Сфе
рический коллайдер). В результате в Boid добавится компонент Sphere Collider.
Выберите компонент Sphere Collider и в инспекторе:
a. Установите флажок Is Trigger.
b. Установите координаты [ 0, 0, 0 ] в поле Center.
c. Установите значение Radius равным 4 (оно будет скорректировано далее
в коде).
10. Пока объект Boid остается выделенным в иерархии, выберите в главном меню
Unity пункт Component > Physics > Rigidbody (Компонент > Физика > Твердое
тело). Снимите флажок Use Gravity для компонента Rigidbody в инспекторе.
И. Перетащите Boid из панели Hierarchy (Иерархия) в панель Project (Проект), чтобы
создать шаблон с именем Boid. Завершенная модель Boid должна выглядеть так,
как показано на рис. 27.1.
12. Удалите синий экземпляр Boid из панели Hierarchy (Иерархия). Теперь, когда
в панели Project (Проект) имеется шаблон Boid, объект в иерархии стал не
нужен.
13. Выберите главную камеру Main Camera в иерархии и установите параметры ее
компонента Transform так, как показано на рис. 27.4. Это отдалит главную камеру
от центра сцены и позволит видеть сразу несколько птиц.
14. Выберите в главном меню пункт GameObject > Create Empty (Игровой объект >
Создать пустой). Дайте новому игровому объекту имя Boid Anchor. Этот пустой
игровой объект BoidAnchor будет служить родителем для всех экземпляров Boid,
которые будут добавлены в сцену, и поможет сократить объем информации,
отображаемой в панели Hierarchy (Иерархия).
все.
Объектно-ориентированная реализация стаи 477
Сценарии на C#
В этом проекте будет использоваться пять разных сценариев на С#, каждый из
которых выполняет важную работу
О Boid — этот сценарий мы подключим к шаблону Boid. Он будет управлять движе
нием отдельных объектов Boid. Поскольку программа является объектно-ориен
тированной, каждый объект Boid будет «мыслить» самостоятельно и реагировать,
опираясь на свое видение окружающего мира.
О Neighborhood — этот сценарий мы также подключим к шаблону Boid. Он будет
следить за соседями по стае. Ключом к пониманию мира каждой птицей Boid
является знание о существовании других птиц, находящихся по соседству.
О Attractor — птицам в стае нужен некий «центр притяжения», вокруг которого они
будут концентрироваться, и этот простой сценарий будет подключен к игровому
объекту, играющему роль такого центра.
О Spawner — этот сценарий будет подключен к главной камере Main Camera. Spawner
содержит поля (переменные), используемые всеми экземплярами, которые он
создает из шаблона Boid.
О LookAtAttractor — также будет подключен к главной камере Main Camera. Этот
сценарий будет поворачивать камеру в сторону Attractor в каждом кадре.
Конечно, то же самое можно было бы сделать с меньшим количеством сценариев, но
тогда каждый из них получился бы больше, чем хотелось бы. Этот пример написан
в духе расширения объектно-ориентированного программирования, известного как
компонентно-ориентированное проектирование. Более подробную информацию
вы найдете во врезке.
КОМПОНЕНТНО-ОРИЕНТИРОВАННОЕ ПРОЕКТИРОВАНИЕ
’ Erich Gamma, Richard Helm, Ralph Johnson, and John Vissides, «Design Patterns: Elements
of Reusable Object-Oriented Software» (Reading, MA: Addison-Wesley, 1994). В этой книге
также был формализован шаблон проектирования «Одиночка» (Singleton) и некоторые
другие, используемые в моей книге {Гамма Эрих, Хелм Ричард, Джонсон Ральф, Влиссидес,
Джон. Приемы объектно-ориентированного проектирования. Паттерны проектирования.
СПб.: Питер, 2016. — Примеч. пер.).
478 Глава 27. Объектно-ориентированное мышление
Сценарий Attractor
Начнем со сценария Attractor. Attractor — это объект, вокруг которого будут концен
трироваться члены стаи Boid. Без него члены стаи могли бы стремиться держаться
рядом друг с другом, но сама стая разлетится за края экрана.
1. Выберите в главном меню Unity пункт GameObject > 3D Object > Sphere (Игровой
объект > 3D объект > Сфера), чтобы создать новую сферу, и переименуйте ее
в Attractor.
компонен
Remove
Объектно-ориентированная реализация стаи 479
1 using System.Collections;
2 using System.Collections.Generic;
3 using UnityEngine;
4
5 public class Attractor : MonoBehaviour {
6 static public Vector3 POS = Vector3.zero; // a
7
8 [Header("Set in Inspector’’)]
9 public ■float radius =10;
10 public ■float xPhase = 0.5f;
11 public float yPhase = 0.4f;
12 public float zPhase = 0.1f;
13
14 // FixedUpdate вызывается при каждом пересчете физики (50 раз в секунду)
15 void FixedUpdate () { // b
16 Vector3 tPos = Vector3.zero;
17 Vector3 scale = transform.localscale;
18 tPos.x = Mathf.Sin(xPhase * Time.time) * radius * scale.x; // c
19 tPos.y = Mathf.Sin(yPhase ♦ Time.time) * radius ♦ scale.y;
20 tPos.z = Mathf.Sin(zPhase ♦ Time.time) ♦ radius * scale.z;
21 transform.position = tPos;
22 POS = tPos;
23 }
24
480 Глава 27. Объектно-ориентированное мышление
Unity старается работать как можно быстрее, поэтому она отображает новый
кадр, как только это становится возможно. Это означает, что Time.deltaTime
между соседними вызовами Update () может изменяться в пределах от менее
чем 1/400 секунды на быстрых компьютерах до более 1 секунды на медленных
мобильных устройствах. Кроме того, частота вызовов Update () может существенно
меняться от кадра к кадру на одном и том же компьютере в зависимости от мно
жества факторов, поэтому величина Time.deltaTime между соседними вызовами
Update() всегда будет получаться разной.
Физические движки, такие как NVIDIA PhysX, используемый в Unity, полагаются
на постоянную периодичность, чего Update() не в состоянии обеспечить. Как
результат, Unity всегда выполняет физические расчеты с постоянной частотой не
зависимо от мощности компьютера. Частота вызовов FixedUpdate() определяется
статическим полем Time.fixedDeltaTime. По умолчанию Time.fixedDeltaTime
получает значение 0.02f (то есть 1/50), иными словами движок PhysX выполняет
расчеты и вызывает FixedUpdateQ 50 раз в секунду.
Как результат, метод FixedUpdate() лучше использовать для задач, которые
касаются движущихся объектов с твердым телом Rigidbody (именно поэтому мы
использовали его для управления движением объектов Attractor и Boid). Он также
хорошо подходит для случаев, когда требуется постоянная периодичность, не за
висящая от быстродействия компьютера.
Объектно-ориентированная реализация стаи 481
Сценарий LookAtAttractor
31 void Awake () {
32 // Сохранить этот экземпляр Spawner в S
33 S = this; // d
34 // Запустить создание объектов Boid
35 boids = new List<Boid>();
36 InstantiateBoid();
37 }
38
39 public void InstantiateBoid() {
40 GameObject go = Instantiate(boidPrefab);
41 Boid b = go.GetComponent<Boid>();
42 b.transform.SetParent(boidAnchor); 11 e
43 boids.Add( b );
44 if (boids.Count < numBoids) {
45 Invoke( "InstantiateBoid", spawnDelay ); // f
46 }
47 }
48
ция Invoke принимает два аргумента: имя метода для вызова (в виде строки
"InstantiateBoid") и задержку во времени в секундах (spawnDelay, или 0.1
секунды).
4. Сохраните сценарий Spawner, вернитесь в Unity и выберите главную камеру Main
Camera в панели Hierarchy (Иерархия).
5. Перетащите шаблон Boid из панели Project (Проект) в поле boidPrefab компонента
Spawner (Script) главной камеры Main Camera в инспекторе.
6. Перетащите игровой объект BoidAnchor из панели Hierarchy (Иерархия) в поле
boidAnchor компонента Spawner (Script) главной камеры Main Camera в инспекторе.
Щелкните на кнопке Play (Играть) в Unity. Вы увидите, как каждую десятую долю
секунды Spawner создает новые экземпляры Boid и подчиняет их объекту BoidAnchor,
но пока экземпляры Boid просто накапливаются в точке нахождения BoidAnchor
в центре сцены, ничего не делая. Пришло время вернуться к сценарию Boid.
30 г.material.color = randColor;
31 }
32 TrailRenderer tRend = GetComponent<TrailRenderer>();
33 tRend.material.SetColor("_TintColor"3 randColor);
34 }
35
36 void LookAhead() { // d
37 // Ориентировать птицу клювом в направлении полета
38 transform.LookAt(pos + rigid.velocity);
39 }
40
41 public Vector3 pos { H b
42 get { return transform.position; }
43 set { transform.position = value; }
44 }
45
46
a. Вызов GetComponent< > () требует довольно много времени, поэтому ради высокой
производительности мы кэшируем ссылку (то есть сохраним ее, чтобы потом
можно было быстро обратиться к ней) на компонент Rigidbody. Поле rigid по
зволит нам избавиться от необходимости вызывать GetComponent<>() в каждом
кадре.
b. Статическое свойство insidellnitSphere класса Random, доступное только для
чтения, генерирует случайный Vector3 с координатами внутри сферы с ради
усом, равным 1. Затем элементы вектора умножаются на общедоступное поле
spawnRadius объекта-одиночки Spawner, чтобы поместить новый экземпляр Boid
в случайно выбранную точку на расстоянии spawnRadius от начала координат
(позиция [ 0,0,0 ]). Получившийся вектор Vector3 присваивается свойству pos,
которое определяется в конце этого листинга.
c. Статическое свойство Random. onUnitSphere генерирует Vector3 с координатами
точки где-то на поверхности сферы с радиусом, равным 1. Проще говоря, оно
возвращает вектор Vector3 с длиной, равной 1, и указывающий в случайном
направлении. Мы умножаем его на поле velocity объекта-одиночки Spawner
и присваиваем результат полю velocity компонента Rigidbody экземпляра Boid.
d. LookAhead() ориентирует Boid (птицу) клювом в направлении вектора rigid,
velocity.
e. Строки 24-33, по большому счету, не особенно нужны, но они делают сцену
более красочной. Эти строки выбирают для каждой птицы случайный цвет (до
статочно ярко выглядящий на экране).
f. Вызов gameobject.GetComponentsInChildren<Renderer>() возвращает массив
всех компонентов Renderer, подключенных к данному игровому объекту Boid,
и всех его потомков. В данном случае он вернет компоненты Renderer кубов —
объектов Fuselage Wing.
486 Глава 27. Объектно-ориентированное мышление
Сценарий Neighborhood
а. Этот метод Start () создает экземпляр списка List neighbors, получает ссылку
коллайдер SphereCollider этого игрового объекта (напомню, что этим игровым
объектом является экземпляр шаблона Boid, к которому также подключен ком
понент SphereCollider) и устанавливает радиус коллайдера SphereCollider равным
половине значения поля neighborDist в объекте-одиночке Spawner. Половине,
490 Глава 27. Объектно-ориентированное мышление
Boid должны видеть друг друга, и если радиус каждого коллайдера будет равен
половине этого расстояния, они будут обнаруживать друг друга, едва сопри
коснувшись, находясь точно на расстоянии neighborDist.
b. В каждом вызове FixedUpdate() класс Neighborhood проверяет изменение ве
личины neighborDist и, если она изменилась, изменяет радиус в SphereCollider.
Изменение радиуса в SphereCollider вызывает массивные вычисления в движке
PhysX, поэтому мы присваиваем новое значение, только если это необходимо.
c. OnTriggerEnter () вызывается, когда что-то другое оказывается в зоне действия
триггера этого коллайдера SphereCollider (триггер — это коллайдер, позволяющий
другим объектам проходить через него). Экземпляры Boid должны быть един
ственными объектами, имеющими прикрепленные к ним коллайдеры, но для
большей уверенности мы вызываем метод GetComponent<Boid>() коллайдера
other и продолжаем, только если результат не равен null. Если обнаруженный
поблизости экземпляр Boid еще не включен в список соседей neighbors, добав
ляем его.
d. Аналогично, когда другой экземпляр Boid выходит из соприкосновения с триг
гером этого экземпляра Boid, вызывается OnTriggerExit(), и мы удаляем отда
лившийся экземпляр Boid из списка neighbors.
e. avgPos — свойство, доступное только для чтения, — просматривает все экземпля
ры Boid в списке neighbors и вычисляет координаты центральной точки в группе.
Обратите внимание, как при этом используется общедоступное свойство pos
каждого экземпляра Boid. Если список соседей пуст, возвращается Vector3. zero.
-F. Аналогично, свойство avgVel возвращает среднюю скорость всех соседних эк
земпляров Boid.
символы (пробелы, переводы строк, табуляции и т. д.) как одно и то же, поэтому
лишние пустые строки здесь или там не имеют никакого значения. Для ясности
я включил в книгу полный сценарий Boid.
1 using System.Collections;
2 using System.Collections.Generic;
3 using UnityEngine;
4
5 public class Boid : MonoBehaviour {
6
7 [Header("Set Dynamically")]
8 public Rigidbody rigid;
9
10 private Neighborhood neighborhood;
11
12 // Используйте этот метод для инициализации
13 void Awake () {
14 neighborhood = Get Compone nt < Neighborhood^);
15 rigid = GetComponent<Rigidbody>();
16
17 // Выбрать случайную начальную позицию
18 pos = Random.insideUnitSphere * Spawner.S.spawnRadius;
19
20 // Выбрать случайную начальную скорость
21 Vector3 vel = Random.onUnitSphere * Spawner.S.velocity;
22 rigid.velocity = vel;
23
24 LookAhead();
25
26 // Окрасить птицу в случайный цвет, но не слишком темный
27 Color randColor = Color.black;
28 while ( randColor.r + randColor.g 4- randColor.b < 1.0f ) {
29 randColor = new Color(Random.value, Random.value, Random.value);
30 }
31 Rendererf] rends = gameObject.GetComponentsInChildren<Renderer>();
32 foreach ( Renderer r in rends ) {
33 r.material.color = randColor;
34 }
35 TrailRenderer trend = GetComponent<TrailRenderer>();
36 trend.material.SetColor("_TintColor", randColor);
37 }
38
39 void LookAhead() {
40 // Ориентировать птицу клювом в направлении полета
41 transform.LookAt(pos + rigid.velocity);
42 }
43
44 public Vector3 pos {
45 get { return transform.position; }
46 set { transform.position = value; }
47 }
48
492 Глава 27. Объектно-ориентированное мышление
50 void FixedUpdate () {
51 Vector3 vel = rigid.velocity;
52 Spawner spn = Spawner.S;
53
54 // ПРЕДОТВРАЩЕНИЕ СТОЛКНОВЕНИЙ - избегать близких соседей
55 Vector3 velAvoid = Vector3.zero;
56 Vector3 tooClosePos = neighborhood.avgClosePos;
57 // Если получен вектор Vector3.zero, ничего предпринимать не надо
58 if (tooClosePos != Vector3.zero) {
59 velAvoid = pos - tooClosePos;
60 velAvoid.Normalize();
61 velAvoid *= spn.velocity;
62 }
63
64 // СОГЛАСОВАНИЕ СКОРОСТИ - попробовать согласовать скорость с соседями
65 Vector3 velAlign = neighborhood.avgVel;
66 // Согласование требуется, только если velAlign не равно Vector3.zero
67 if (velAlign != Vector3.zero) {
68 // Нас интересует только направление, поэтому нормализуем скорость
69 velAlign.Normalize();
70 // и затем преобразуем в выбранную скорость
71 velAlign ♦= spn.velocity;
72 }
73
74 // КОНЦЕНТРАЦИЯ СОСЕДЕЙ - движение в сторону центра группы соседей
75 Vector3 velCenter = neighborhood.avgPos;
76 if (velCenter != Vector3.zero) {
77 velCenter -= transform.position;
78 velCenter.Normalize();
79 velCenter ♦= spn.velocity;
80 }
81
82 // ПРИТЯЖЕНИЕ - организовать движение в сторону объекта Attractor
83 Vector3 delta = Attractor.POS - pos;
84 // Проверить, куда двигаться, в сторону Attractor или от него
85 bool attracted = (delta.magnitude > spn.attractPushDist);
86 Vector3 velAttract = delta.normalized * spn.velocity;
87
88 // Применить все скорости
89 float fdt = Time.fixedDeltaTime;
90 if (velAvoid != Vector3.zero) {
91 vel = Vector3.Lerp(vel, velAvoid, spn.collAvoid*fdt);
92 } else {
93 if (velAlign ’= Vector3.zero) {
94 vel = Vector3.Lerp(vel, velAlign, spn.velMatching*fdt);
95 }
96 if (velCenter •= Vector3.zero) {
97 vel = Vector3.Lerp(vel, velAlign, spn.flockCentering’fdt);
98 }
99 if (velAttract != Vector3.zero) {
100 if (attracted) {
101 vel = Vector3.Lerp(vel, velAttract, spn.attractPull*fdt);
Итоги 493
104 }
105 }
106 }
107
108 // Установить vel в соответствии с velocity в объекте-одиночке Spawner
109 vel = vel.normalized * spn.velocity;
110 // В заключение присвоить скорость компоненту Rigidbody
111 rigid.velocity = vel;
112 // Повернуть птицу клювом в сторону нового направления движения
113 LookAhead();
114 }
И5 }
velocity 30 30 30 30
neighborDist 30 30 8 30
collDist 4 10 2 10
velMatching 0,25 0,25 0,25 10
flockCentering 0,2 0.2 8 0,2
collAvoid 2 4 10 4
attractPull 2 1 1 3
attractPush 2 2 20 2
attractPushDist 5 20 20 1
Итоги
В этой главе вы познакомились с идеей объектно-ориентированного программи
рования, которая будет эксплуатироваться в оставшейся части книги. Благодаря
своей организации с использованием игровых объектов и компонентов, Unity пре
объектно-ориентированного Вы
494 Глава 27. Объектно-ориентированное мышление