< title>Разработка игры на Unity с нуля до создания билда< /title> Яндекс.Метрика
//навигации

Разработка игры на Unity с нуля до создания билда.

 



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

Приветствую вас, уважаемый читатель. Если вы держите в руках эту книгу, значит, у вас есть стремление создавать что-то своё, воплощать в жизнь идеи. И это не удивительно, ведь в человеческой природе заложено неудержимое желание творчества. Кто-то получает удовольствие от модернизации своего мотоцикла, кто-то разводит оранжерею на подоконнике, зимой делают снеговиков, ледяные горки для детей во дворе. Это прекрасно. А мы будем созидать в мире информационных технологий, попробуем сделать это нашим хобби, не рутинной работой, а увлекательным процессом реализации желания создания своей собственной игры. Да! Это возможно. В моей юности у нас в стране стали появляться первые приставки Atari, было открыто множество клубов, где они устанавливались, с присоединённым к ним телевизором, а молодёжь толпами стояла вокруг играющих. Эти игры запомнились на всю жизнь. И вот, через 30 лет я решил сделать одну из таких игр – «RiverRaider». И у меня получилось! Нет ничего невозможного. Есть замечательный игровой движок Юнити, которому и посвящена эта книга. Именно с помощью его мы будем создавать нашу игру. И это будет полностью готовая игра, которая в данный момент находится в магазине Steam и называется «Pirates. Naval battle». Вы всегда можете посмотреть на конечный результат вашей работы. Эта книга написана не как справочник по ознакомлению со всеми возможностями Юнити, руководство вы всегда сможете найти в Интернете. Книга написана по принципу: есть поставленная задача, далее объяснение как эту задачу реализовать с помощью Юнити, и, наконец, реализация самой задачи.

Что нужно знать, чтобы начать наш увлекательный процесс создания игры? Желательно, чтобы были знания основ программирования – что такое переменные, операторы, циклы, методы. Но даже если этих знаний у вас нет, вы всё равно сможете создать игру по данной книге путём простого копирования кода. Если же вы в дальнейшем хотите создавать свои собственные игры, узнать основы языка программирования C# придётся. Он достаточно прост, и к тому же для создания игры редко применяются сложные конструкции кода. Для индивидуальных разработчиков это в основном не требуется.  Итак, если вы готовы творить здесь и сейчас, то не откладывайте эту книгу на дальнюю полку. Мы отправляемся в путь. Начинаем!



Юнити.

Если уж мы решили работать с движком Юнити, то неплохо было бы установить его на наш компьютер. Все мы когда-то что-то скачивали с Интернета. В этом нет ничего сложного. Сегодня Unity предоставляется всем для бесплатного использования на сколь угодно долгий срок. Просто зайдите на официальный сайт Юнити, зарегистрируйтесь, скачайте программу Unity Hub к себе на компьютер, установите и запустите её (рис.1).

 Рис.1

Слева щёлкните на кнопке Installs, откроется соответствующая вкладка. Сверху справа нажмите Install Editor, и в появившемся окне выберите версию движка. Если не знаете, какую установить, то выберите одну из последних версий с надписью LTS. Эта версия будет поддерживаться долгое время. Автор будет работать на версии 2022.3.9f1. Установите и запустите Юнити.

Создавать и открывать проекты мы будем через Unity Hub. Слева кликните на кнопку Projects. Откроется соответствующая вкладка. Справа вверху нажмите на кнопку New Projects. Откроется окно (рис 2).

 Рис.2.

Выберите 3D проект, сверху укажите версию движка, справа нужно вписать название проекта, допустим Pirates_Book. Чуть ниже выберите расположение проекта на жёстком диске. Нажмите на кнопку Create projekt. Через некоторое время перед вами откроется пустой проект Юнити (рис.3).

Рис 3.

Рассмотрим, из каких окон состоит наш проект. 1 – это окно Сцены (Scene), в котором мы и будем создавать визуальную составляющую нашей игры. Именно в этом окне и происходят все действия. Сюда мы будем перетаскивать модели ландшафта, моря, кораблей. Здесь происходят взаимодействия между ними. 2 – это окно Иерархии (Hierarchy). В нём будут появляться названия тех объектов, которые перенесены на сцену, и наоборот, те объекты, которые будут создаваться в этом окне, или переноситься в него, автоматически появятся в окне сцены игры. 3 – окно Инспектор (Inspector). Здесь отображаются свойства, характеристики того объекта, который выделен в окне Иерархии или в окне Проект. 4 – окно Проект (Project). Устроено по принципу проводника в Windows. Корневой здесь является папка нашего проекта на Жёстком диске, а в ней уже располагаются все те папки, которые вы видите в левой части окна Проект. Главной для нас является папка Assets. Именно в ней мы будем создавать всю структуру папок нашего проекта, и помещать в них объекты игры. 5 – окно Игра (Game). В нём мы сможем наблюдать, как будет выглядеть наша игра с точки зрения игрока (в окне сцены мы можем наблюдать за игрой с любой точки, под любым ракурсом, как разработчик). 6 – окно Консоли (Console). Здесь появляются сообщения об ошибках в коде, информационные сообщения, и те сообщения, которые вы сами прописываете в скрипте.

Изначально проект Юнити создаётся для игр под платформу Windows. Давайте убедимся в этом. Меню File – Build Settings… (рис. 4).

 Рис. 4.

Синей полоской выделена нужная платформа. Если это не так, выберите платформу Windows и нажмите кнопку «Изменить платформу». Закроем это окно.



Небо и море.

Начнём заполнять нашу сцену объектами игры. Добавим на неё небо и море. Все необходимые изображения, 3D модели, звуковые файлы (одним словом, ассеты) для создания игры размещены по этим ссылкам: (https://disk.yandex.kz/d/yutuMKA0KctaxQ, https://drive.google.com/file/d/1AxLAo6-3x6w0pj92EKio1hernECVb-nb/view?usp=sharing). Воспользуйтесь любой из них и скачайте всё необходимое к себе на компьютер. Нас интересуют изображения с названиями X+, X-, Y+, Y-, Z+, Z-. Это наше небо со всех 6-ти сторон. Нам нужно импортировать их в проект. В окне Проект в левой части выделим папку Assets. В правой части отобразится её содержимое. Сейчас там пока только папка Scenes. Создадим ещё одну. На свободном месте кликнем правой кнопкой мыши, и в контекстном меню выбираем – CreateFolder. Называем папку Img (рис.5).

 Рис. 5.

Открываем её, на свободном пространстве кликаем правой кнопкой мыши – Import New Assets… и импортируем 6 изображений неба. Выделим любую из картинок, и в окне Инспектор отобразятся её параметры (рис. 6).

Рис.6.

Изображение нужно преобразовать в спрайт, именно с ними работает Юнити. Параметр Texture Type измените на Sprite. Параметр Wrap Mode должен быть установлен в Clamp.  Это означает, что изображение не будет повторяться на объекте. Если мы создаём спрайт для текстуры (это изображение многократно повторяется на наложенном на него объекте), то этот параметр нужно будет установить в Repeat. Жмём кнопочку Apply, и повторяем действия для всех шести изображений. Можно выделить сразу несколько изображений, и установить параметры для них одновременно. Спрайты для неба готовы. Теперь нужно создать материал для него. В папке Assets создаём ещё одну папку, и называем её Materials. В ней мы будем создавать наши материалы, которые впоследствии будут налаживаться на различные объекты. В созданной папке на пустом пространстве кликаем правой кнопкой мыши и выбираем в контекстном меню CreateMaterial. Назовём его Sky. Выделяем материал, и в окне Инспектор напротив параметра Shader открываем список и выбираем – SkyBox – 6 sided (рис.7).

 Рис. 7.

Теперь в соответствующие окошки нужно перенести 6 наших спрайтов. В окно с названием Front[+Z] переносим спрайт Z+ из окна Проект (откройте только папку со спрайтами, по самим спрайтам щелкать не нужно, а сразу переносите их в нужное место, иначе в окне Инспектор отобразятся характеристики уже выбранного спрайта, а не материала Sky). То же самое проделайте с остальными пятью спрайтами. Теперь нужно применить созданный материал к нашей сцене. Откройте окно Lighting. Если у вас его нет, то зайдите в меню WindowRenderingLighting (Рис.8).

В поле справа от параметра SkyBox Material нужно перенести материал нашего неба, или щёлкнуть на окружности справа от поля, и из появившегося списка выбрать наш материал. Вот как должна выглядеть наша сцена (рис.9).

 Рис.9.

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

Теперь нам понадобится море. Оно есть в стандартном ассете Юнити, который бесплатно можно скачать на https://assetstore.unity.com. Так же этот ассет в урезанном виде находится в скаченном вами архиве. Давайте установим именно его, но если вы знаете, как установить его из магазина, то сделайте это.

В окне Проект в папке Assets на пустом пространстве кликните правой кнопкой мыши и выберите – Import PackegeCustom Packege. Найдите у себя на компьютере скаченный вами пакет water и импортируйте его. Откроется окно, в котором будет показано всё содержимое пакета. Нажмите кнопку Import. В папке Assets появится новая папка стандартных ассетов Unity, и в ней можно увидеть префаб нашего моря (рис. 10).

Рис. 10.

Что такое префабы, я расскажу немного позже. Сейчас просто ухватитесь за префаб WaterProDay и перетащите его в окно сцены. На сцене и в окне Иерархии у вас появится новый объект, а в окне Инспектор все его характеристики. Характеристики любого объекта состоят из компонентов. Каждый из них за что-то отвечает. Компонент Transform отвечает за координаты расположения объекта, его поворот по всем осям и масштаб. Установите все параметры компонента Transform как на рисунке 11.

 Рис 11.

Так же у компонента Water параметр Water Mode поменяйте на Reflective. Так вода станет отражающей, а не прозрачной.

Для навигации в окне сцены зажмите клавишу Alt, наведите указатель мыши на окно сцены, зажмите левую кнопку мыши и перемещайте мышь в разных направлениях. Другой способ – зажмите клавиши Ctrl  и Alt, также наведите указатель мыши на окно сцены, зажмите левую кнопку мыши и перемещайте мышь в разных направлениях. Для того, чтобы быстро приблизиться к какому либо объекту, выделите его в окне сцены или в окне Иерархии и нажмите клавишу F. Так мы приблизим вид окна сцены к этому объекту. Он станет в фокусе. При повторных нажатиях на клавишу F, объект будет поочерёдно то удаляться, то приближаться. Для приближения и удаления вида используйте колёсико мыши.

Импортируйте в ваш проект 3D модели земли, кораблей, деревьев. Для этого создайте в папке Assets папку Prefabs, а в ней ещё три папки: Initial, Land, Shipl_Player. И по аналогии, как мы делали с изображениями для неба, в эти папки нужно импортировать модели из скаченного вами архива. Всё должно получиться как на рисунках 12, 13 и 14.

Рис. 12.

Рис. 13.

Рис.14.



Земля.

Перетащите на сцену 3D-модель Land_1. и в окне Инспектор для компонента Transform установите следующие параметры: позиция по X, Y и Z соответственно (225; -2; 225), поворот по трём осям (-90; 0; 0), масштаб по трём осям (0.3; 0.3; 0.3). С помощью навигации в окне сцены посмотрите, как выглядит наша земля. Вершины холмов должны располагаться над водой, а основание под водой.

Создадим материал для нашей земли. В папке Img создайте папку textures, и импортируйте в неё все текстуры (рис. 15).

Рис. 15.

Выделите все текстуры, и на панели Инспектор установите их параметр Texture Type в Sprite, а параметр Wrap Mode в Repeat.

В папке Materials создадим ещё одну папку Land, и в ней, по аналогии с небом, создадим новый материал и назовём его Land_1. Выделите материал, и на панели Инспектор параметр Tiling по оси X и Y установите 40, параметр Metallic в 1, Smoothness в 0. Возле параметра Albedo кликните на окружность, и в появившемся окне найдите текстуру Land_1. Материал готов. Текстуры на объекте будут повторяться 40 раз по оси X и Y.

Для присвоения материала модели земля, просто перетащите материал на наш объект в окно сцены или в окно Иерархии (рис. 16).

Рис. 16.

На панели Иерархии выделите источник света и в окне Инспектор установите для него цвет – белый, а параметр intensity равным 2. Опять на панели Иерархии или в окне сцены сделайте источник света активным, нажмите клавишу F, чтобы он попал в фокус. Над окном сцены есть переключатель между глобальной и локальной системой координат (Global/Local). Переключитесь на глобальную систему координат. Включите режим вращения объектов (W – перемещение, E – вращение, R – масштабирование). И по оси Y поверните источник света так, чтобы освещение вас устраивало.



Корабль.

Перенесите модель Ship_Player_Poli из окна Проект в окно сцены и в компоненте Transform укажите следующие параметры местоположения корабля: (X:300; Y:0; Z:50). Корабль должен повиснуть на небольшом расстоянии от воды. Сфокусируйте корабль по центру окна нажатием клавиши F и при помощи навигации Alt+мышь, повращайте вид вокруг корабля. Обратите внимание, что парус можно увидеть только с одной стороны, поэтому перекиньте в окно сцены ещё и модель паруса Sail. Координаты для этой модели укажите такие же, как и для корабля. Дополнительный парус совместится с уже имеющимся, и теперь его можно увидеть с двух сторон. Этот парус нужно сделать дочерним объектом по отношению к кораблю, чтобы при его перемещении и вращении, дополнительный парус также перемещался. Для этого на панели Иерархии объект паруса переместите на объект корабля (рис. 17).

Рис.17.

На разные части корабля мы будем назначать разные материалы. Давайте начнём их создавать. В папке Materials добавьте папку Player_Ship. В ней создайте материал и назовите ship is outside. Он будет налаживаться на корабль снаружи. В качестве текстуры укажите изображение wood_2, цвет (RGB 67, 47, 31). Metallic и Smoodness в 0. Tilling по обоим направлениям – 10. Перенесите материал на обшивку корабля и на выступ, который расположен спереди.

Создайте материал для палубы. Название – desk, изображение для текстуры – wood_2. Цвет – (RGB 137, 137, 137). Metallic и Smoodness в 0. Tilling по X – 6, по Y – 5. Перенесите материал на палубу.

Материал для выступа посередине палубы сделаем следующим образом. Выделите материал desk и скопируйте его нажатием клавиш Ctrl+D. Появившийся новый материал назовите deck ledge. Цвет – (113, 80, 80). Tilling – 3 и 17 соответственно. Перенесите материал на выступ палубы, который перед мачтой.

Ещё раз скопируйте материал desk. Новый материал назовите ladder. (RGB 106, 68, 38). Metallic – 0,62. Tilling – 6 и 4. Перенесите материал на лестницу.

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

Материал для перилл на палубе вокруг руля. Название – Perilla. Изображение для текстуры – wood_4. Цвет (RGB 108, 95, 73). Metallic и Smoodness в 0. Tilling 10 и 10.

Материал для мачты. Название - The mast. Изображение для текстуры – wood_3. Цвет (RGB 101, 101, 101). Metallic и Smoodness в 0. Tilling 3 и 3.

Материал для основания руля. Название - base of the steering wheel. Изображение для текстуры – wood_2. Цвет (RGB 113, 80, 80). Metallic и Smoodness в 0. Tilling 3 и 6.

Материал барабана для подъёма якоря. Название - anchor lift. Изображение для текстуры – wood_2. Цвет (RGB 67, 53, 42). Metallic и Smoodness в 0. Tilling 1.6 и 2.4.

Материал для паруса. Название – Sail. Изображение для текстуры – отсутствует. Цвет (RGB 137, 137, 137). Metallic и Smoodness – 0.87 и 0.35 соответственно. Tilling 1 и 1. Наложите материал на главный парус с обеих сторон.

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

У нас не хватает руля на корабле. Давайте перенесём его на сцену, и координаты зададим такие же, как и у корабля. Перенесите на него материал от перилл.

Проделаем те же операции с рулём, какие мы делали с парусом – сделаем его дочерним объектом относительно корабля. На панели Иерархии перенесите руль на корабль. Вот что должно получиться (рис. 18).

 Рис 18.

Давайте ещё немного украсим наш корабль. Перенесите на сцену модель бочки. Она немного великовата. На панели Инспектор у компонента Transform установите масштабирование по всем осям 0.16. Я создал для бочки материал на основе текстуры wood_1 с параметром Tilling равным 1 и 1. Для перемещения бочки переключитесь в соответствующий режим нажатием клавиши W. Переместите бочку под верхнюю палубу. Её нужно сделать дочерним объектом по отношению к кораблю, так же, как мы это делали с парусом и рулём. При выделенном объекте бочка, нажмите комбинацию клавиш Ctrl+D. Создастся копия, но она будет располагаться в том же самом месте, что и первая. Если вы начнёте её перемещать, то увидите, что она будет отделяться от оригинала. Создайте ещё несколько копий и также разместите их под верхней палубой (рис.19).

 Рис. 19.

Справа вверху в окне сцены есть значок (рис. 20).

 Рис. 20.

Он помогает ориентировать вид сцены по нужным осям. Пощёлкайте по разноцветным пирамидкам с названием осей, и вы увидите, как меняется вид в окне сцены. Если нажать на какую-либо ось, а потом сразу на центральный квадрат, то вид перспективы (это привычный для нас вид, мы им обозреваем окружающий нас мир) изменится на аксонометрический. Иногда этот вид бывает удобным для более точного перемещения объектов по сцене. Но если в этом виде использовать инструмент навигации Alt+зажатая левая кнопка мыши, то вид окна сцены станет достаточно неказистым. Поэтому вид аксонометрии лучше использовать, когда окно сцены ориентировано чётко по какой-либо оси. Нажатием на центральный квадрат, опять перейдите в вид перспективы.



Pivot объекта и компоненты.

Щелкните по названию корабля в окне Иерархии и нажмите клавишу F для фокусировки выделенного объекта. Если вращать вид вокруг корабля, то можно увидеть, что точка управления кораблём (она называется pivot), находится не в самом центре нашего объекта. Ещё лучше это увидеть, если сориентировать вид по оси Z, и нажатием на квадрат перейти в вид аксонометрии (рис.21).

Рис. 21.

Это бывает очень часто с 3D моделями. То же самое вы могли наблюдать с моделью бочки. Так же бывает, когда направление осей модели, импортированной в проект, не совпадает с направлением осей объектов, создаваемых в Юнити. В некоторых 3D редакторах ось Z направлена вверх, а в Юнити она направлена вперёд. Всё управление объектами через программный код на движке Юнити основывается именно на осях, и расположении pivot объекта относительно самого объекта. Как вы понимаете управлять кораблём, у которого pivot расположен не там, где надо, будет достаточно сложно. Чтобы убедиться в этом, попробуйте вращать корабль по оси Z. Видно, что это выглядит не очень хорошо. Нажатием комбинации клавиш Ctrl+Z верните корабль на место.

Можно сделать управление объектом не через его pivot, а по центру объекта. Сверху слева окна сцены есть переключатель (Pivot/Center). Переключитесь на управление по центру и увидите, что точка с осями для управления кораблём переместилась в центр объекта корабль. Но этот способ правления бывает полезен только для перемещения объектов вручную в окне сцены. Для управления объектами через скрипт, всё-таки нужен pivot. Опять переключитесь в режим Pivot.

Для решения этой и других задач в Юнити есть замечательный объект-пустышка. Его используют постоянно во многих проектах. Давайте с ним познакомимся.

В окне Иерархии (часто называют эти окна панелями) на пустом месте кликните правой кнопкой мыши. В контекстном меню – Create Empty. Созданный объект назовите ___Рlayer_Ship. Мы возьмём за правило, названия всех пустых объектов начинать с тройного подчёркивания, чтобы отличать их от других объектов. Где-то на сцене появился новый объект. Попробуйте перемещать его за оси. Вы увидите, что перемещаются только оси координат, и больше ничего. Это и не удивительно, ведь у этого объекта кроме компонента Transform ничего нет. Этим он и прекрасен. Взгляните на панель Инспектор.

А теперь выделите объект нашей воды в панели Иерархии и посмотрите, сколько у него компонентов. Слева от названия каждого из них есть стрелочка, направленная или вниз или вправо. Щелкайте стрелочки так, чтобы все были направлены вправо, т.е. сверните все компоненты. Есть стрелочки слева от структур у некоторых компонентов. Они не считаются, т.к. это не компоненты, а составляющие части какого-либо компонента. Всего у нашего моря можно насчитать 5 компонентов (рис. 22).

Рис. 22.

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

Объекты в Юнити обладают компонентами. Итак, что же такое компонент? Это просто составные части какого-либо объекта. Как мы видим, самый простой объект обладает только одним компонентом, который отвечает за местоположение объекта в пространстве, повороты и масштабирование. Да, да, масштабирование тоже возможно, но этого пока нельзя заметить, так что будьте осторожны с масштабирование пустышек. Есть компоненты, которые отвечают за внешний вид объекта – это различные компоненты Mesh (сетка). То есть можно из нашей пустышки сделать куб, если добавим на него компонент с мешем куба. Именно так, компоненты можно добавлять и удалять. Посмотрите, на рис. 22, там можно увидеть специальную кнопку для добавления новых компонентов. Есть компоненты, которые отвечают за столкновение объекта с другими объектами. И пока такого компонента нет на объекте, наш объект будет просто видим в игре, но никак не взаимодействовать с другими объектами. При случае он будет проходить как призрак сквозь всё, что попадётся у него на пути. И такое бывает нужным в игре. Есть компоненты, которые отвечают за физические свойства объекта. Если добавить такой компонент, то у него появится масса, на него будет действовать гравитация, при столкновении с другими объектами, он будет отталкиваться от них, вести себя как настоящий объект из реального мира. Наконец есть компонент Скрипт. Т.е. это такой текстовый файл, в котором записан программный код на языке C#. И если объект с компонентом скрипта помещён на сцену, то этот программный код начнёт выполняться при запуске игры.

Мы потихоньку подошли к тому, как всё работает в Юнити, т.е. как связаны наши объекты, помещённые на сцену, с языком программирования. Просто налаживаем компонент со скриптом на объект, который на сцене (или будет помещён на сцену по время игры – и такое бывает), и код из скрипта начнет работать во время игры.

Когда мы станем активно работать с добавлением компонентов на объекты, всё будет ещё понятнее. А пока вернёмся к созданному нами пустому объекту с одним единственным компонентом Transform. Мы начали с того, что у корабля оси расположены не так, как нам нужно. Вернее, pivot корабля располагается не в том месте, а оси направлены так же, как и у объекта пустышка. Но это частный случай, бывает, что при импортировании модели, и оси направлены неправильно.

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

Мы сделаем следующее: пустой объект разместим там, где мы хотим сделать центр управления кораблем, или можно сказать центр его вращения. Далее сделаем корабль дочерним объектом по отношению к пустому объекту. После этого все трансформации (перемещение, вращение, масштабирование) пустышки будут применены и к кораблю. Переместите пустой объект так, чтобы по оси X он находился строго по центру корабля, по оси Z он должен совпадать с мачтой корабля, а по оси Y примерно посередине борта (рис. 23). Все эти действия лучше производить, когда включён вид аксонометрии.

Рис. 23.

В окне Иерархии перенесём модель корабля на пустой объект (рис. 24).

Рис. 24.

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

Так как мы приблизительно перемещали пустышку относительно корабля, то давайте более точно переместим уже сам корабль относительно пустого объекта в нужное место. Для начала кликните на пустой объект и посмотрите на его компонент Transform, а потом кликните на модель корабля, и посмотрите на тот же компонент. Хотя эти объекты и располагаются близко друг от друга, но значения по X и Y сильно отличаются. Дело в том, что для объектов, которые не являются дочерними по отношению к какому-либо другому объекту, компонент Transform отображает мировые (или глобальные) координаты, относительно центра координат сцены игры. А для дочерних объектов компонент Transform отображает локальные координаты. Центром координат в данном случае является родительский объект. Чтобы убедиться в этом, установим для модели корабля координаты по всем осям равными 0. Вы увидите, что pivot модели корабля совместился с pivot пустышки ___Рlayer_Ship. Теперь выставим правильные координаты. X = -0.1907; Y = -0.203; Z = 0.215. Выделите пустой объект. Он должен находиться по центру модели корабля.

С этого момента пустой объект с названием ___Рlayer_Ship и есть, и будет нашим кораблём. Все действия будем производить именно с ним, и в программном коде будем обращаться только к нему. Попробуйте вращать наш корабль. Теперь его поведение выглядит более естественным. Нажмите Ctrl+Z и верните корабль обратно.

Давайте спустим наш корабль на воду. Введите координаты корабля по трём осям (300; -0.8; 50) (рис.25).

Рис. 25.

 



Пушка.

Нам нужно собрать пушку из разных запчастей. Для удобства будем это делать вблизи корабля. Переместите модель Cannon_Base на сцену и задайте ей координаты (302; -1; 50). У этой модели с осями совсем беда. В компоненте Transform назначьте угол по оси Y 90 градусов и посмотрите, как повернулась модель. Верните поворот обратно, и теперь уже по оси Z задайте угол 90 градусов. Как можно видеть, модель опять повернулась в том же направлении. Такое случается, не удивляйтесь. Поэтому мы и будем работать не с самими 3D моделями, а с пустышками, у которых с осями всё в порядке.

Вручную на сцене поверните нашу модель основы пушки по вертикальной оси, чтобы она смотрела вперёд, по направлению корабля, затем в компоненте Transform откорректируйте полученный угол, чтобы он был ровен 90 градусов (рис.26).

Рис. 26.

Переместите на сцену модель пушки Cannon_init, и параметры местоположения и поворота задайте такие же, как и у предыдущей модели. Пушка должна встать на своё место. Переместите на сцену объект Cannon_Cylindr. Таким же образом расположите и поверните эту модель с помощью компонента Transform. И ещё раз проделайте то же самое с моделью Cannon_Wheel.

Материал для базы пушки и для колёс сделайте на основе изображения wood_2. Параметр Tilling 6.6 и 12.5 соответственно. Цвет выберите по желанию. Для пушки и цилиндра материал черный, немного блестящий. С материалами можно экспериментировать, как угодно, на игру это никак не влияет (в смысле работоспособности). Вот что должно получиться (рис.27).

 

Рис. 27.

 Какими объектами из всех этих мы будем управлять во время игры? У нас будут крутиться колёса во время отдачи пушки назад и её возвращения. Также нам понадобятся координаты и угол поворота непосредственно ствола пушки, чтобы создавать ядра и стрелять в нужном направлении. А это значит, что и для колёс, и для пушки нам необходимо также создать пустые объекты, чтобы мы могли правильно управлять их поворотами и координатами.

Создадим пустой объект на панели Иерархии, как мы это делали для модели корабля, и назовем его ___Cannon. Расположим его так, чтобы он находился по центру ствола пушки (рис. 28).

 

Рис. 28.

На панели Иерархии модель ствола пушки Cannon_init перенесем на созданный нами пустой объект, т.е. модель станет дочерним объектом по отношению к пустому объекту. Теперь поверните пустышку пушки на 16.5 градусов по оси X, и немного переместите вверх и назад, чтобы пушка приняла естественное положение на деревянной основе. Модели могут частично накладываться друг на друга в пространстве, здесь это не имеет значения.

Создайте ещё один пустой объект и назовите его ___Cannon_Wheel. Разместите его по центру колёс, затем колёса сделайте дочерними объектами для пустышки (рис. 29).

Рис. 29.

Выделите в окне Иерархии пустышку колёс и нажмите комбинацию клавиш Ctrl+D. Создастся копия этого объекта. Переместите его немного вперёд по оси Z, чтобы колёса приняли естественное положение.

Сейчас нужно объединить все разрозненные части пушки в единое целое. Для этого создайте ещё один пустой объект, назовите его ___Cannon_Base, разместите по центру основы пушки, а затем поочерёдно делайте его дочерними объектами ___Cannon, ___Cannon_Wheel, ___Cannon_Wheel (1), Cannon_Cylindr, Cannon_Base (рис. 30).

Рис. 30.

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

Переместите на сцену 3D модель стрелки, сделайте масштаб по всем осям равным 0.014. Поверните стрелку так, чтобы, она смотрела вниз, и поместите её над пушкой. Затем сделайте её четвёртым дочерним объектом ___Cannon_Base. Эта стрелка будет изначально скрыта, но, если игрок выберет данную пушку, мы сделаем стрелку активной, чтобы игрок видел, какая пушка готова стрелять. Мы не станем делать пустышку для данного объекта, т.к. здесь pivot располагается в правильном месте. И хотя мы будем обращаться из скрипта к стрелке, единственное предназначение её – вращаться вокруг вертикальной оси. Во время создания скрипта разберёмся, вокруг какой оси её вращать (рис. 31).

Рис. 31.

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

Выделим нашу собранную пушку (объект ___Cannon_Base) и добавим на неё новый компонент на панели Инспектор. Нажмите на кнопку Add Component, в появившемся списке выберите Physics и далее Capsule Collider. У пушки появится новый компонент (рис 32), а вокруг неё можно увидеть созданные границы для взаимодействия объекта с окружающим миром (рис. 33).

Рис. 32.  Рис.33.

Но такие границы нас не устраивают, давайте их корректировать. Нажмите кнопку Edit Collider, она станет активной, а на самом коллайдере появятся небольшие жёлтые квадратики, перетаскивая которые, придайте нашему коллайдеру нужную форму (рис. 34) Редактирование удобнее проводить в аксонометрическом виде.

Параметр Direction должен быть установлен по оси Z, т.к. наш коллайдер имеет продолговатую форму, и он должен быть вытянут вдоль пушки, а не в вертикальном направлении. Если не получается, можете просто скопировать параметры с рисунка. Этот коллайдер не покрывает всю поверхность пушки, и если игрок кликнет на верхнюю часть ствола, то никакой реакции не произойдёт, поэтому нам нужно добавить ещё один компонент коллайдера. Опять нажмите на кнопку Add Component, в появившемся списке выберите Physics и далее Capsule Collider. Также нажмите на кнопку Edit Collider (заметьте, она одна на два компонента), и приведите второй коллайдер к нужному виду (рис. 35).

 Рис. 35.

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

Нашу пушку мы также сделаем триггером, т.к. ни от какого объекта она отталкиваться не будет. У обоих компонентов коллайдеров поставьте галочку is Trigger.



Тег и слой.

Теперь нам нужно узнать, что такое «Тег» (Tag). Тег чем-то схож с именем объекта, но в отличие от имени, которое присваивается каждому объекту, тег может быть назначен объекту, а может и нет. Также каждый объект имеет уникальное имя, а вот тег можно присвоить нескольким объектам. Для примера возьмём учеников школы. Помимо того, что каждый из них имеет уникальные имя и фамилию, они также входят в состав какого-либо класса. Название класса – это и есть тег ученика. Несколько десятков учеников имеют один и тот же тег, например «4а», другая группа учеников имеет другой тег – «7в». В Юнити это очень полезная вещь. Допустим, идет стрельба из оружия, и каждой созданной пуле присваивается уникальное имя. При контакте с каким-либо объектом, нужно узнать, что за объект вошёл во взаимодействие. Перебирать все имена – это нереально. Для этого и существует тег. Всем пулям при создании присваивается один и тот же тег, и мы определяем объект, вошедший в контакт не по имени, а по тегу не нём.

Сразу остановимся на очень похожем понятии «Слой» (Layer). Для начинающего разработчика достаточно знать, что это почти то же самое, что и тег. Он также может присваиваться группе объектов (по желанию), за тем исключением, что Слои используются при особом роде взаимодействии на объект. Как раз наша пушка – это и есть особый случай. Когда игрок кликнет на каком-либо объекте указателем мыши, создаётся невидимый луч, который начинается от глаз пользователя, проходит через точку экрана, на которой произошёл клик, и дальше идёт вглубь игры. Все объекты, которые пронизывает луч, мы можем выявить, и произвести с ними какие-либо действия. Именно при таком взаимодействии и нужны слои. Луч может определить пронизывающий объект, только если мы указали для объекта нужный слой. Если на объекте нет слоя, или же слой отличается от того, который задан при пуске луча, то луч такой объект не определит.

Для нашей пушки мы установим и тег, и слой. Сверху в окне Инспектор раскройте список Tag и кликните на Add Tag… Далее щёлкните на плюсик и добавьте новый тег с названием «cannon». Создан новый тег, но он ещё не присвоен нашему объекту. На панели Иерархии выберите нашу пушку и на панели Инспектор опять раскройте список Tag, в котором вы увидите название созданного тега. Выберите его.

Теперь добавьте новый слой. Откройте список Layer, и выберите Add Layer… В пустом поле введите название cannon и нажмите Enter. Слой создан, но не назначен. Опять выделите объект пушка, и в раскрывающемся списке Layer укажите наш созданный слой. Если вас спросят: назначать ли слой для дочерних объектов, ответьте, что нет, только для родительского объекта (рис. 36). Всегда будьте внимательны - в название тега и слоя есть разница между заглавными и прописными буквами.

 Рис 36.



Префаб.

Мы собрали нашу пушку и присвоили ей тег и слой. Сейчас сделаем из пушки префаб. Познакомимся для начала с этим новым понятием. Префаб, это образец, на основе которого создаются точно такие же объекты. Допустим, мы один раз создали пулю и сделали из неё префаб. При стрельбе из пулемёта мы каждые несколько долей секунды даём команду – создать объект на основе префаба – и указываем, какого именно. И каждый раз создаётся пуля – точная копия префаба, только с уникальным именем – пуля1, пуля2 … Это уже в процессе игры. Но префабы часто используются и на этапе создания игры. На нашем корабле будут 12 пушек. Один раз мы создадим префаб, и затем на основе его создадим остальные пушки. Возникает вопрос. А почему просто не скопировать пушку 12 раз? Не так всё просто. Все мы люди, и нам свойственно ошибаться. Если мы один раз ошиблись при создании пушки, и потом вспомнили, что сделали что-то не так, то исправлять придётся 12 копий пушек. А если это кирпичи, и их несколько сотен на сцене? С префабом всё гораздо проще. Сколько бы ни было копий префабов, изменив сам префаб, все изменения будут применены и ко всем его копиям. В будущем будьте осторожны. Если вы сделали копию префаба, разместили где-то на сцене, и вносите какие-либо изменения в эту копию, то изменения самого префаба на эту копию могут и не повлиять. Это частая ошибка разработчиков. Почему мы не сделали префабы бочек, ведь у нас их несколько штук на корабле? Да, это можно было сделать. Но в данном случае мы точно знаем, что эти бочки только для украшения, и это очень простые объекты, к тому же на этапе их сознания было рано говорить о префабах.

Как создать из объекта на сцене префаб? Нужно ухватиться за его название в окне Иерархии и перенести в какую-либо папку в окна Проект. Автоматически будет создан префаб, а в окне Иерархии он окрасится в синий цвет, это будет означать, что на сцене находится копия префаба.

Давайте перенесём пустышку пушки (это и есть наша пушка) в папку Prefabs (рис. 37).

Рис. 37.

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



Много пушек.

Какое-то время придётся потратить на рутинную работу. Да, такое случается постоянно при разработке игр, когда творчество плавно перетекает в однообразный процесс создания уровня – размещения на нём предметов, а их бывает достаточно много. Но и в этом тоже можно получать огромное удовольствие. Нам предстоит разместить на корабле 12 пушек. Особенность состоит в том, что палуба у корабля не ровная, и пушки придётся поворачивать по всем трём осям, причём нужно учитывать, что при стрельбе будет отдача назад на расстояние, примерное равное длине самой пушки. Нужно, чтобы при отдаче пушка перемещалась по палубе, а не проваливалась под неё, или повисала над ней. При проделывании этой работы нужно будет переключиться в локальную систему координат (сверху окна сцены есть переключатель Local/Global). При установке пушки на место, попробуйте перетаскивать её вперёд/назад по локальной оси Z, чтобы увидеть, хватает ли ей места для отдачи. Вот как пушки располагаются в оригинальной игре (рис 38, 39, 40).

Рис. 38

Рис. 39

Рис. 40

Все созданные нами пушки пока ещё не находятся на корабле. Если сейчас вы попробуете перетаскивать корабль по сцене, то пушки останутся на своих местах. Перед перемещением пушек на корабль, давайте создадим ещё один пустой объект, который будет родительским объектом для всех пушек. Назовём его ___All_cannon. Теперь перенесите эту пустышку на корабль в панели Иерархии, т.е. сделайте его дочерним объектом по отношению к ___Player_Ship. Кликните на объекте ___All_cannon, и в окне Инспектор позицию по всем осям сделайте равной 0. Т.е. сейчас координаты нашей пустышки сравнялись с координатами корабля. В панели Иерархии поочередно выделяйте объект корабля и пустышку для всех пушек, и на сцене вы увидите, что они находятся в одной точке пространства. Теперь нужно переместить все наши пушки на пустышку ___All_cannon, чтобы сделать их дочерними объектами по отношению к этому пустому объекту (рис. 41).

Рис. 41.

Нужно понимать, что каждая пушка в отдельности не является дочерним объектом корабля, она дочерний объект пустышки ___All_cannon, которая в свою очередь имеет родительский объект – наш корабль. Сейчас, при попытке переместить корабль по сцене, а также его поворотах, можно увидеть, что пушки также перемещаются вместе с кораблём.

В игре наш корабль может быть атакован ядрами пиратов, столкнуться с землёй, или с другими кораблями, т.е. он должен взаимодействовать с другими объектами. Поэтому на корабль также нужно поместить компонент коллайдер. Выделим на панели Иерархии корабль, и в окне Инспектор добавим новый компонент Box Collider. Сделайте его триггером, отредактируйте размеры так, как на рис. 42.

Рис 42.



Камера на корабле.

Создадим камеру, через которую игрок будет наблюдать за игрой. Что увидит через объектив камера, то увидит и игрок. Удалите объект Main Camera со сцены (выделите его и нажмите Del). В окне Иерархии на пустом пространстве кликните правой кнопкой мыши и выберите создание камеры. Сразу сделайте камеру первым дочерним объектом корабля, и параметры компонентов Transform и Camera в панели Инспектор установите такими, как на рис. 43.

Рис.43.

Как можно видеть на изображении, активировано окно Игра (Game), а не Сцены (Scene), т.е. сейчас мы смотрим на игру не как разработчик, а как игрок через созданную нами камеру. Вы также можете это сделать, нажав на вкладку окна Игра, которая находится справа от вкладки окна Сцены. С этого момента мы будем часто переключаться между этими двумя окнами.

Нам понадобится ещё один пустой объект для управления камерой из программного кода. Создайте его, назовите ___Camera_Ctrl, сделайте его вторым дочерним объектом корабля, а параметры компонента Transform установите такими же, как и у камеры, то есть их координаты должны совпадать.



Главный объект и главный скрипт.

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

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

Создайте пустой объект, назовите его ___empty_shared_variables, координаты по всем осям установите равными 0.

После создания скриптов, они открываются в редакторе кода. По умолчанию в Юнити есть встроенный редактор, но при желании можно установить другой. Код для данной игры создавался на Microsoft Visual Studio 2019. Меню EditPreferens (рис. 44).

Рис.44.

В папке Assets создадим новую папку, и назовём её Scripts. В этой папке на свободном пространстве кликнем правой кнопкой мыши и в контекстном меню выберем Create – C# Script. Создастся файл скрипта, и пока ещё мигает курсор возле названия скрипта, введите название Class_common_variables. Мы не зря сейчас акцентируем внимание на этой, казалось бы, маловажной вещи. Дело в том, что при создании скрипта, в нём создаётся программный код с именем Класса, которое в точности соответствует названию файла скрипта. Поэтому вводить имя файла нужно сразу после создания. Если вы щёлкнули в каком-либо месте, и курсор исчез, то имя файла уже сформировано, и Класс в скрипте тоже с таким именем. При переименовании файла, имя Класса останется прежним, и Юнити в дальнейшем будет выдавать ошибки. Поэтому сразу удаляйте такой файл скрипта и создавайте новый. Также нельзя менять имя Класса в скрипте. Просто запомните это. Также всегда следите за окном Консоль. Если там появились красные надписи (если надписи отключены, то красный значок справа вверху окна), то значит где-то совершена ошибка, и игра не запустится.

Ещё совет из опыта. При создании скрипта подождите несколько секунд перед его открытием – Юнити производит действия при встраивании его в игру (в правом нижнем углу будет крутиться колёсико, и при завершении появится галочка). Бывали случаи, когда сразу открываешь скрипт, печатаешь код, а потом оказывается, что этот скрипт нельзя наложить ни на какой объект, Юнити его просто не видит. Это случается редко, но всё же нужно быть готовым. В таких случаях временно сохраните где-нибудь код из скрипта, удалите этот файл и создайте новый, скопировав в него данные.



Скрипт для корабля игрока.

Создадим ещё один скрипт и назовём его Players_ship.

Дважды кликните на последнем созданном скрипте. Откроется редактор кода. Вот, что мы увидим:

 

using System.Collections;

using System.Collections.Generic;

using UnityEngine;

 

public class Players_ship : MonoBehaviour

{

    void Start()

    {

       

    }

 

    void Update()

    {

       

    }

}

Весь код, который помещается в метод Start() будет выполняться сразу после создания объекта, на котором находится скрипт, до начала отсчёта кадров игры. Поэтому используйте его для инициализации всех необходимых переменных.

Метод Update() будет выполняться во всех скриптах игры перед прорисовкой каждого кадра. Соответственно здесь должен быть помещён код, требующий выполнения в каждом кадре. 

Первым делом мы научим нашу камеру передвигаться по кораблю вперёд и назад. Для этого нам понадобятся следующие переменные:

 

private GameObject camera_Player;

 

Эта переменная будет ссылаться на камеру игрока на корабле. GameObject — это универсальный тип Юнити, который может быть у созданной нами пушки, у корабля, у нашей камеры, а также у текстового поля или кнопки из интерфейса игры. Мы часто будем использовать этот тип данных в скриптах.

 

private GameObject empl_ctrl_camera;

 

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

 

private bool moving_camera_forward = false;

 

Эта переменная будет устанавливаться в истину, когда камеру нужно перемещать вперёд, к носовой части корабля.

 

private bool moving_camera_back = false;

 

То же самое для обратного направления.

 

private float z_min_camera = -1.7f;

private float z_max_camera = 1.1f;

Эти переменные устанавливают ограничение в движении камеры вперёд и назад, до какого максимального и минимального значения по координате Z может передвигаться камера. Это в локальных координатах, относительно корабля.

 

private float speed_moving_camera = 0.8f;

 

Эта переменная устанавливает скорость перемещения камеры.

В методе Start() нужно определить ссылку на камеру. Этот скрипт будет находится на объекте корабль, а доступ ко всему, что находится на объекте, на котором и лежит скрипт, очень прост:

 

camera_Player = this.transform.GetChild(0).transform.gameObject;

 

this означает, что мы обращаемся к тому объекту, на котором находится скрипт, т.е. к кораблю. Далее мы обращаемся к его компоненту transform и запрашиваем дочерний объект с индексом 0 (т.е. по счёту первый объект – камера). У компонента transform много возможностей, которые не отображены в окне Инспектор редактора Юнити. Как видите, через него мы можем обратиться к дочерним объектам. Т.к. переменная camera_Player имеет тип GameObject, то мы должны дописать конструкцию .transform.gameObject для получения именно gameObject дочернего объекта. Если мы оставим только .transform, то мы получит тип переменной Transform. У неё другие возможности. Обращение this является необязательным, и в дальнейшем мы будем его пропускать.

Подобным образом в методе Start() определяем ссылку на пустышку управления камерой.

 

empl_ctrl_camera = transform.GetChild(1).transform.gameObject;

 

Этот объект по счёту является вторым дочерним объектом корабля, поэтому мы обращаемся к объекту с индексом 1.

Напишем следующий метод для перемещения камеры.

 

 private void Moving_camera()

    {

       

        if (moving_camera_forward & empl_ctrl_camera.transform.localPosition.z < z_max_camera)

        {

            empl_ctrl_camera.transform.Translate(0, 0, speed_moving_camera * Time.deltaTime, Space.Self);

        }

        if (moving_camera_back & empl_ctrl_camera.transform.localPosition.z > z_min_camera)

        {

            empl_ctrl_camera.transform.Translate(0, 0, -speed_moving_camera * Time.deltaTime, Space.Self);

        }

       

        camera_Player.transform.position = empl_ctrl_camera.transform.position;

    }

 

Сначала в методе проверяется условие: если включен режим перемещения камеры вперёд, и при этом локальные координаты по оси Z у объекта управления камерой меньше максимального значения, то пустышку управления камерой перемещаем на некоторое расстояние по оси Z. Конструкция speed_moving_camera * Time.deltaTime очень полезна и применяется повсеместно при создании игр. Допустим, нам нужно переместить объект на какое-то расстояние за 1 сек. Но перемещение идёт в каждом кадре, а на одном компьютере игра будет выдавать 50 кадров в секунду, на другом только 30. Как нам узнать, на сколько нужно перемещать объект в каждом кадре? Time.deltaTime возвращает время, прошедшее между данным и предыдущим кадром. И если умножить расстояние, которое должна пройти наша камера за 1 сек (это и есть скорость) на время, прошедшее между двумя соседними кадрами, то мы получим расстояние, которое должна пройти камера за этот небольшой интервал времени. И если в итоге сложить все расстояния, которые камера проходила между отдельными кадрами, то мы получим расстояние, которое должна пройти камера за 1 секунду. Метод empl_ctrl_camera.transform.Translate(x, y, z, Space.Self) перемещает пустышку камеры на указанное расстояние по всем трём осям. Последним параметром указывается, в каких координатах перемещать – в мировых или локальных. Второе условие в методе абсолютно идентично, с той разницей, что камеру нужно перемещать в обратном направлении. Последней строчкой метода

 

camera_Player.transform.position = empl_ctrl_camera.transform.position;

 

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

Теперь нам нужно узнать, куда игрок хочет перемещать камеру, т.е. на какие клавиши он нажимает. Создадим следующий метод:

 

private void Tracking_clicks_on_computer()

    {

 

        if (Input.GetKey(KeyCode.W) & !Input.GetKey(KeyCode.S))

        {

            moving_camera_forward = true;

        }

        else

        {

            moving_camera_forward = false;

        }

        if (!Input.GetKey(KeyCode.W) & Input.GetKey(KeyCode.S))

        {

            moving_camera_back = true;

        }

        else

        {

            moving_camera_back = false;

        }

}

Первым условием мы проверяем: если нажата клавиша W, и при этом не нажата клавиша S, то переменную для перемещения камеры вперёд устанавливаем в истину, если условие не выполняется, то в ложь. То же самое делаем для переменной, устанавливающей движение камеры назад. Этот метод также нужно поместить в метод Update()  перед методом Tracking_clicks_on_computer(). Почему перед ним? Потому что сначала, в каждом кадре мы должны узнать, какие клавиши нажимает игрок, а уже потом, в зависимости от этого, производить те, или иные действия. Теперь посмотрим, как должен выглядеть наш скрипт целиком.

 

using System.Collections;

using System.Collections.Generic;

using UnityEngine;

 

public class Players_ship : MonoBehaviour

{

    private GameObject camera_Player;

    private GameObject empl_ctrl_camera;

    private bool moving_camera_forward = false;

    private bool moving_camera_back = false;

    private float speed_moving_camera = 0.8f;

    private float z_min_camera = -1.7f;

    private float z_max_camera = 1.1f;

 

    void Start()

    {

        camera_Player = transform.GetChild(0).transform.gameObject;

        empl_ctrl_camera = transform.GetChild(1).transform.gameObject;

       

    }

 

    void Update()

    {

        Tracking_clicks_on_computer();

        Moving_camera();

    }

    private void Moving_camera()

    {

       

        if (moving_camera_forward & empl_ctrl_camera.transform.localPosition.z < z_max_camera)

        {

            empl_ctrl_camera.transform.Translate(0, 0, speed_moving_camera * Time.deltaTime, Space.Self);

        }

        if (moving_camera_back & empl_ctrl_camera.transform.localPosition.z > z_min_camera)

        {

            empl_ctrl_camera.transform.Translate(0, 0, -speed_moving_camera * Time.deltaTime, Space.Self);

        }

       

        camera_Player.transform.position = empl_ctrl_camera.transform.position;

    }

    private void Tracking_clicks_on_computer()

    {

 

        if (Input.GetKey(KeyCode.W) & !Input.GetKey(KeyCode.S))

        {

            moving_camera_forward = true;

        }

        else

        {

            moving_camera_forward = false;

        }

        if (!Input.GetKey(KeyCode.W) & Input.GetKey(KeyCode.S))

        {

            moving_camera_back = true;

        }

        else

        {

            moving_camera_back = false;

        }

    }

}

 



Первый запуск игры.

Скрипт на данном этапе готов к использованию. Сохраните его и перейдите в редактор Юнити. Подождите, пока игровой движок проделает все необходимые действия, по внедрению кода в игру. Чтобы скрипт работал в игре, как вы уже знаете, его нужно поместить на какой-либо объект. Как понимаете, этим объектом является корабль. Выделите наш корабль в окне Иерархии, и в окне Инспектор добавим новый компонент. Нажмите кнопку Add ComponentScripts, и затем укажите наш созданный файл скрипта.

Теперь мы можем запустить игру, и посмотреть на результат проделанной работы. Нажмите на кнопку Play (треугольник, направленный вправо, вверху окна Юнити) (рис.45).

Рис. 45.

Если будем нажимать на клавиши W и S, то увидим, что мы перемещаемся по кораблю к носовой части и обратно до заданных нами пределов. Остановите игру, нажав на ту же самую кнопку. Не забывайте этого никогда, т.к. при включенном режиме проигрывателя, все изменения, сделанные в проекте, не сохраняются. Как можно было заметить, камера при перемещении назад заходит под верхнюю палубу. Это не совсем то, что нам нужно. Давайте сделаем так, чтобы при подходе к верхней палубе, камера начиналась подниматься вверх, и наоборот, при движении камеры вперед, в определённый момент камера спускалась вниз, и продолжала движение по нижней палубе. Путь перемещения камеры показан на рисунке 46.

Рис. 46.

Откроем наш скрип и добавим следующие переменные:

private float z_start_lifting_camera = -0.6f;

    private float z_start_descent_camera = -1.0f;

    private float y_min_camera = 0.7f;

  private float y_max_camera = 1.1f;

 

Переменная z_start_lifting_camera отвечает за начало подъема камеры, т.е. при перемещении её назад, при достижении этой координаты по оси Z, камера начнёт перемещаться не только назад, но и вверх.

Переменная z_start_descent_camera отвечает за начало спуска камеры, т.е. при перемещении её вперёд, при достижении этой координаты по оси Z, камера начнёт перемещаться не только вперёд, но и вниз.

Две другие переменные указывают, до какого максимального и минимального значения по оси Y должна подниматься или опускаться камера. Изначально, при старте игры камера находится в своём минимальном положении по координате Y.

В методе по перемещению камеры, при движении пустышки камеры вперёд добавим следующее условие:

 

if (empl_ctrl_camera.transform.localPosition.z > z_start_descent_camera & empl_ctrl_camera.transform.localPosition.y > y_min_camera)

            {

empl_ctrl_camera.transform.Translate(0, -speed_moving_camera * Time.deltaTime, speed_moving_camera * Time.deltaTime, Space.Self);

            }

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

Абсолютно идентично нужно сделать с перемещением пустышки назад. Введём условие: если координата Z пустышки меньше, чем Z начала подъёма камеры, и при этом координата пустышки Y ещё не достигла максимального значения, то мы перемещаем пустышку по оси Y вверх с такой же скоростью, что и перемещение назад. Вот как должен выглядеть сейчас наш метод целиком:

 

private void Moving_camera()

    {

 

        if (moving_camera_forward & empl_ctrl_camera.transform.localPosition.z < z_max_camera)

        {

            if (empl_ctrl_camera.transform.localPosition.z > z_start_descent_camera & empl_ctrl_camera.transform.localPosition.y > y_min_camera)

            {

                empl_ctrl_camera.transform.Translate(0, -speed_moving_camera * Time.deltaTime, speed_moving_camera * Time.deltaTime, Space.Self);

            }

            else

            {

                empl_ctrl_camera.transform.Translate(0, 0, speed_moving_camera * Time.deltaTime, Space.Self);

            }

 

        }

        if (moving_camera_back & empl_ctrl_camera.transform.localPosition.z > z_min_camera)

        {

            if (empl_ctrl_camera.transform.localPosition.z < z_start_lifting_camera & empl_ctrl_camera.transform.localPosition.y < y_max_camera)

            {

                empl_ctrl_camera.transform.Translate(0, speed_moving_camera * Time.deltaTime, -speed_moving_camera * Time.deltaTime, Space.Self);

            }

            else

            {

                empl_ctrl_camera.transform.Translate(0, 0, -speed_moving_camera * Time.deltaTime, Space.Self);

            }

        }

 

        camera_Player.transform.position = empl_ctrl_camera.transform.position;

    }

 

Давайте сохраним наш скрипт и перейдём в проект Юнити. Запустим игру, и теперь можно увидеть, что при перемещении камеры, она также поднимается на верхнюю палубу (рис. 47).

 

Рис. 47.

Далее сделаем повороты камеры. Для этого нужно добавить несколько новых переменных.

 

private bool rotation_Camera_r = false;

private bool rotation_camera_l = false;

private float speed_rotation_camera = 60f;

 

Как можно догадаться из их названия, две первые переменные отвечают за поворот камеры вправо и влево соответственно. Последняя переменная устанавливает скорость поворота камеры. В метод Moving_camera() перед последней строчкой добавим следующий код:

 if (rotation_camera_l)

        {

            camera_Player.transform.Rotate(0, -speed_rotation_camera * Time.deltaTime, 0, Space.Self);

        }

        if (rotation_Camera_r)

        {

            camera_Player.transform.Rotate(0, speed_rotation_camera * Time.deltaTime, 0, Space.Self);

        }

 

Здесь всё просто: если игрок выбрал поворот камеры влево, то используя метод camera_Player.transform.Rotate(x, y, z) поворачиваем камеру влево (конечно же, по оси Y), и соответственно таким же образом в правую сторону. Сейчас можно ответить на вопрос, зачем мы использовали дополнительную пустышку для управления камерой. Дело в том, что перемещение камеры идёт по локальным осям. И в случае поворота ось Z камеры постоянно меняла бы своё направление, и камера перемещалась бы в том направлении, куда она смотрит. А пустой объект направлен всегда вперёд, и никогда не поворачивается, поэтому его перемещение легко контролировать, чтобы не было выхода за пределы корабля.

Добавьте в метод Tracking_clicks_on_computer() следующий код:

 

if (Input.GetKey(KeyCode.A) & !Input.GetKey(KeyCode.D))

        {

            rotation_camera_l = true;

        }

        else

        {

            rotation_camera_l = false;

        }

        if (!Input.GetKey(KeyCode.A) & Input.GetKey(KeyCode.D))

        {

            rotation_Camera_r = true;

        }

        else

        {

            rotation_Camera_r = false;

  }

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



Звуки моря.

Давайте немного украсим нашу игру, чтобы при её запуске чувствовалась атмосфера присутствия на корабле. В папке Assets создайте новую папку, и назовите её Audio. Импортируйте в неё аудиофайлы из скаченного вами архива также, как вы импортировали 3D модели и изображения. На панели Иерархии кликните правой кнопкой мыши и создайте источник звука. Назовите его Audio Source_Waves_and_Ship. Кликните по нему, чтобы в панели Инспектор отобразились компоненты созданного нами объекта. В поле AudioClip из окна Проект перенесите звуковой файл sound Waves_and_Ship. Поставьте галочки напротив полей Play on Awake и Loop (рис. 48). Теперь этот звуковой файл будет проигрываться сразу при запуске игры, причём происходить это будет зациклено.

Рис. 48.

Добавьте на сцену ещё один источник звука и назовите его Audio Source. На панели Инспектор для этого объекта в поле AudioClip из окна Проект перенесите звуковой файл sound_sea. Поставьте те же галочки, что и для предыдущего источника звука. Эти два звуковые файла будут налаживаться друг на друга, и эффект будет ярче. Запустите игру и посмотрите, что получилось.



Покачивания корабля.

Наш корабль должен покачиваться на волнах. Давайте это сделаем. Откройте скрипт корабля и введите новые переменные:

private const float max_speed_Y = 0.2f;

    private float speed_Y = max_speed_Y;

    private float min_speed_Y;

    private bool ship_up = true;

    private int sign_speed_Y = 1;

    private const float delta_change_speed = 0.1f;

    private const float max_angle_rotation = 7f;

private float angle_up_down;

 

Первая константа – это максимальная скорость перемещения корабля на волнах вверх или вниз во время покачивания. Вторая переменная – это текущая вертикальная скорость корабля, она будет постоянно меняться. Третья переменная – это минимальная вертикальная скорость корабля. Четвёртая переменная показывает, движется корабль по оси Y вверх или вниз. Если вверх, то она равна истине. Следующая переменная – это знак скорости вертикального перемещения корабля, он равен 1 при перемещении вверх, и -1 при обратном направлении. Переменная delta_change_speed определяет, насколько быстро будет меняться скорость корабля, можно сказать, что это ускорение. Следующая константа определяет максимальный угол поворота корабля на волнах. Это поворот по локальной оси X корабля. Последняя переменная – это текущий угол поворота по оси X при покачивании, не путайте с поворотами корабля в сторону.

Для начала в методе Start() определим значение для минимальной скорости по оси Y.

 

min_speed_Y = max_speed_Y / 100;

 

Далее создадим метод, который будет постоянно менять скорость перемещения корабля по оси Y.

 

private void Speed_change_Y()

    {

        speed_Y += sign_speed_Y * delta_change_speed * Time.deltaTime;

        if (speed_Y >= max_speed_Y & sign_speed_Y > 0)

        {

            sign_speed_Y *= -1;

        }

        if (speed_Y <= min_speed_Y & sign_speed_Y < 0)

        {

            sign_speed_Y *= -1;

            ship_up = !ship_up;

        }

 

}

 

На верху волны скорость будет минимальной, и корабль начнёт опускаться вниз, при этом скорость корабля будет увеличиваться. После достижения определённой глубины, выталкивающая сила увеличится, и скорость перестанет нарастать, и станет постепенно уменьшаться до тех пор, пока не дойдёт до минимального значения. Далее выталкивающая сила заставит корабль подниматься наверх, при этом скорость поднятия начнёт возрастать до тех пор, пока сила тяжести не станет больше выталкивающей силы. С этого момента скорость начнёт опять уменьшаться, пока не дойдет до своего минимального значения. В это время корабль окажется в своей верхней точке. Затем всё повторяется циклически. Именно этот процесс и описывает данный метод. Переменная sign_speed_Y меняет своё значение на противоположное на середине пути, когда скорость достигает максимального значения, а также в самом верху и в самом низу, когда скорость становится минимальной. Переменная ship_up меняет своё значение только вверху и внизу, чтобы нам знать, поднимается корабль или опускается.

Теперь давайте напишем метод, который непосредственно перемещает корабль вверх и вниз в зависимости от скорости по оси Y.

private void Up_Down_Ship()

    {

        if (ship_up)

        {

            transform.Translate(0, speed_Y * Time.deltaTime, 0, Space.World);

        }

        else

        {

            transform.Translate(0, -speed_Y * Time.deltaTime, 0, Space.World);

        }

 

}

Здесь всё очень просто. Если переменная установлена в истину, то корабль поднимаем по оси Y, если же в ложь, соответственно опускаем вниз.

Создадим ещё один метод для покачивания корабля, который будет объединять в себе оба этих метода.

private void Rocking_ship()

    {

        Speed_change_Y();

        Up_Down_Ship();

}

Здесь также нет ничего сложного. Сначала мы вычисляем скорость корабля, и узнаём его направление, а затем перемещаем корабль в нужном направлении с нужной скоростью. Поместите метод Rocking_ship() в Update() самым первым.

Для того, чтобы хорошо увидеть эти покачивания вверх и вниз, для начала в окне Иерархии выделите корабль и нажмите клавишу F, т.е. сфокусируйтесь на неё. Затем запустите игру, и во время проигрывания, опять переключитесь на сцену. Со стороны хорошо видно, как корабль приподнимается и опускается на воде. Когда мы сделаем наклоны корабля вверх и вниз, тогда всё будет видно хорошо и с камеры на корабле.

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

 

private void Angle_calculation_up_down()

    {

        float s = speed_Y;

        if (s < 0)

        {

            s = 0.00001f;

        }

        angle_up_down = (s / max_speed_Y) * max_angle_rotation;

}

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

Далее создадим метод, который непосредственно поворачивает наш корабль по оси X на вычисленный нами угол.

 

private void Rotation_ship_up_down()

    {

        if (ship_up)

        {

            transform.localEulerAngles = new Vector3(-angle_up_down, transform.localEulerAngles.y, transform.localEulerAngles.z);

        }

        else

        {

            transform.localEulerAngles = new Vector3(angle_up_down, transform.localEulerAngles.y, transform.localEulerAngles.z);

        }

 

}

Если корабль поднимается вверх, то угол будет отрицательный, в обратном направлении – положительный. Поместите последние два созданные метода в метод Rocking_ship().

 

Вот как на данный момент должен выглядеть наш скрипт на корабле.

 

using System.Collections;

using System.Collections.Generic;

using UnityEngine;

 

public class Players_ship : MonoBehaviour

{

    private GameObject camera_Player;

    private GameObject empl_ctrl_camera;

    private bool moving_camera_forward = false;

    private bool moving_camera_back = false;

    private float speed_moving_camera = 0.8f;

    private float z_min_camera = -1.7f;

    private float z_max_camera = 1.1f;

    private float z_start_lifting_camera = -0.6f;

    private float z_start_descent_camera = -1.0f;

    private float y_min_camera = 0.7f;

    private float y_max_camera = 1.1f;

    private bool rotation_Camera_r = false;

    private bool rotation_camera_l = false;

    private float speed_rotation_camera = 60f;

    private const float max_speed_Y = 0.2f;

    private float speed_Y = max_speed_Y;

    private float min_speed_Y;

    private bool ship_up = true;

    private int sign_speed_Y = 1;

    private const float delta_change_speed = 0.1f;

    private const float max_angle_rotation = 7f;

    private float angle_up_down;

 

    void Start()

    {

        min_speed_Y = max_speed_Y / 100;

        camera_Player = transform.GetChild(0).transform.gameObject;

        empl_ctrl_camera = transform.GetChild(1).transform.gameObject;

       

    }

 

    void Update()

    {

        Rocking_ship();

        Tracking_clicks_on_computer();

        Moving_camera();

    }

    private void Moving_camera()

    {

 

        if (moving_camera_forward & empl_ctrl_camera.transform.localPosition.z < z_max_camera)

        {

            if (empl_ctrl_camera.transform.localPosition.z > z_start_descent_camera & empl_ctrl_camera.transform.localPosition.y > y_min_camera)

            {

                empl_ctrl_camera.transform.Translate(0, -speed_moving_camera * Time.deltaTime, speed_moving_camera * Time.deltaTime, Space.Self);

            }

            else

            {

                empl_ctrl_camera.transform.Translate(0, 0, speed_moving_camera * Time.deltaTime, Space.Self);

            }

 

        }

        if (moving_camera_back & empl_ctrl_camera.transform.localPosition.z > z_min_camera)

        {

            if (empl_ctrl_camera.transform.localPosition.z < z_start_lifting_camera & empl_ctrl_camera.transform.localPosition.y < y_max_camera)

            {

                empl_ctrl_camera.transform.Translate(0, speed_moving_camera * Time.deltaTime, -speed_moving_camera * Time.deltaTime, Space.Self);

            }

            else

            {

                empl_ctrl_camera.transform.Translate(0, 0, -speed_moving_camera * Time.deltaTime, Space.Self);

            }

        }

        if (rotation_camera_l)

        {

            camera_Player.transform.Rotate(0, -speed_rotation_camera * Time.deltaTime, 0, Space.Self);

        }

        if (rotation_Camera_r)

        {

            camera_Player.transform.Rotate(0, speed_rotation_camera * Time.deltaTime, 0, Space.Self);

        }

 

        camera_Player.transform.position = empl_ctrl_camera.transform.position;

    }

    private void Rocking_ship()

    {

        Speed_change_Y();

        Angle_calculation_up_down();

        Rotation_ship_up_down();

        Up_Down_Ship();

    }

    private void Speed_change_Y()

    {

        speed_Y += sign_speed_Y * delta_change_speed * Time.deltaTime;

        if (speed_Y >= max_speed_Y & sign_speed_Y > 0)

        {

            sign_speed_Y *= -1;

        }

        if (speed_Y <= min_speed_Y & sign_speed_Y < 0)

        {

            sign_speed_Y *= -1;

            ship_up = !ship_up;

        }

 

    }

    private void Up_Down_Ship()

    {

        if (ship_up)

        {

            transform.Translate(0, speed_Y * Time.deltaTime, 0, Space.World);

        }

        else

        {

            transform.Translate(0, -speed_Y * Time.deltaTime, 0, Space.World);

        }

 

    }

    private void Angle_calculation_up_down()

    {

        float s = speed_Y;

        if (s < 0)

        {

            s = 0.00001f;

        }

        angle_up_down = (s / max_speed_Y) * max_angle_rotation;

    }

    private void Rotation_ship_up_down()

    {

        if (ship_up)

        {

            transform.localEulerAngles = new Vector3(-angle_up_down, transform.localEulerAngles.y, transform.localEulerAngles.z);

        }

        else

        {

            transform.localEulerAngles = new Vector3(angle_up_down, transform.localEulerAngles.y, transform.localEulerAngles.z);

        }

 

    }

    private void Tracking_clicks_on_computer()

    {

 

        if (Input.GetKey(KeyCode.W) & !Input.GetKey(KeyCode.S))

        {

            moving_camera_forward = true;

        }

        else

        {

            moving_camera_forward = false;

        }

        if (!Input.GetKey(KeyCode.W) & Input.GetKey(KeyCode.S))

        {

            moving_camera_back = true;

        }

        else

        {

            moving_camera_back = false;

        }

        if (Input.GetKey(KeyCode.A) & !Input.GetKey(KeyCode.D))

        {

            rotation_camera_l = true;

        }

        else

        {

            rotation_camera_l = false;

        }

        if (!Input.GetKey(KeyCode.A) & Input.GetKey(KeyCode.D))

        {

            rotation_Camera_r = true;

        }

        else

        {

            rotation_Camera_r = false;

        }

    }

}



Интерфейс игры.

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

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

Рис А. Рис. Б.

В окне Иерархии кликните правой кнопкой мыши UI – Canvas. В окне сцены появится новый объект, гораздо больший по размеру, чем остальные. Это может показаться странным, что Канвас находится непосредственно на сцене, но со временем к этому привыкаешь. Если он сильно мешает во время редактирования объектов сцены, его можно сделать неактивным, отключив в панели Инспектор галочку, слева от имени объекта.

Параметр UI Scale Mode установите в Scale With Screen Size. Размеры Канваса 1280 на 720 (рис. 49).

Рис. 49.

Совет: при редактировании Канвас, соотношение сторон в окне Игра установите таким же, как и у Канвас. В нашем случае это 16:9 (рис. 50).

 

Рис.50.

Таким образом вы избежите множества проблем. Откройте папку Img в окне Проект и импортируйте туда из скаченного архива изображения Speed и Arrow. Сделайте их спрайтами и параметр Wrap Mode установите в Clamp. Нажмите кнопку применить. Разместим на Канвас изображение спидометра, справа от которого во время игры будет появляться значение скорости корабля.

В окне Иерархии на объекте Канвас кликните правой кнопкой мыши и выберите UI – Image. Назовите созданное изображение Image_Speed. Переместите появившийся квадрат наверх экрана (при перемещении не трогайте ось Z), сделайте привязку по правому верхнему углу и размеры задайте 65 на 50. В пустое поле параметра Source Image из окна Проект перенесите изображение спидометра (рис. 51).

Рис. 51.

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

Теперь нам нужно создать текстовое поле, в котором будем отображать текущую скорость корабля. В окне Иерархии на Канвасе вызовите контекстное меню UI - Legacy Text. Назовите созданный объект Text_Speed. Размеры задайте 90 на 50, привяжите его к правому верхнему углу, и расположите справа от изображения спидометра. Остальные параметры задайте такими же, как на рис. 52.

Рис. 52.

Сейчас нам нужно создать изображения для угловой скорости. Одно будет появляться, когда корабль будет поворачивать направо, другое, когда налево. Выделите изображение для спидометра, скопируйте его нажатием Ctrl+D. Вновь созданное изображение назовите Image_Speed_Rotation_L. Переместите его ниже изображения спидометра. В пустое поле параметра Source Image из окна Проект перенесите изображение стрелки влево. Скопируйте объект Image_Speed_Rotation_L, назовите его Image_Speed_Rotation_R и оставьте в той же точке пространства, но поверните по оси Y на 180 градусов. Проще это сделать с помощью компонента Transform (рис. 53).

Рис.53.

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

Теперь можно заняться управлением нашего корабля. Для начала создадим пустой объект, назовём его ___Empl_2, переместим в нулевые координаты по всем осям, и сделаем из него префаб – то есть перенесите этот объект в окно Проект в папку Prefabs. Затем удалите со сцены эту пустышку. Созданный префаб нам пригодится.

Откройте скрипт для корабля и создайте в нём новые переменные:

public Text text_speed;

    public Text text_speed_rotation;

    public GameObject img_arrow_r;

    public GameObject img_arrow_l;

 

    private const float max_speed_rotation_Y = 15f;

    private float speed_rotation_Y = 0f;

    private float previous_speed_rotation_Y = 0f;

    private int sign_speed_rotation = 0;

    private const float delta_change_speed_rotation_Y = 2f;

    private float speed_ship_Z;

    private const float max_speed_ship_Z = 1.5f;

    private const float delta_change_speed_Z = 0.1f;

    private bool ship_moving = false;

private float speed_conversion_factor = 15f;

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

 

using UnityEngine.UI;

 

Теперь мы сможем обращаться к элементам интерфейса, то есть с Канвасу из нашего скрипта. Первая переменная – это ссылка на текстовое поле для скорости корабля. Для того чтобы Юнити знал, о каком текстовом поле идёт речь, мы в панели Инспектор в дальнейшем его укажем. Следующая переменная – это ссылка на текстовое поле для угловой скорости. Третья и четвёртая переменные – это ссылка на изображения стрелок вправо и влево на Канвасе. С помощью этих переменных мы будем делать видимыми и невидимыми соответствующие изображения. Константа max_speed_rotation_Y определяет максимальную скорость поворота корабля. Следующая переменная – это текущая скорость корабля. За ней идёт переменная, в которой хранится скорость корабля в предыдущем кадре. Переменная sign_speed_rotation определяет, в какую сторону будет поворачивать корабль. Если значение равно 1, то вправо, если -1, то влево. Следующая константа указывает, насколько быстро будет меняться скорость поворота, можно сказать, что это ускорение поворота. Переменная speed_ship_Z определяет скорость корабля вперёд. Следующая переменная – это максимальная скорость. Константа delta_change_speed_Z по аналогии с поворотом показывает, насколько быстро будет ускоряться корабль. Как вы понимаете, нельзя сделать постоянными скорости поворота и линейной скорости, иначе поведение корабля будет слишком неестественным. Он будет моментально набирать скорость и останавливаться, так же будут осуществляться повороты. Поэтому и введены константы delta, чтобы и набор скорости, и остановка проходили постепенно. Переменная ship_moving показывает, движется корабль или нет. Последняя переменная – это коэффициент перевода полученных нами значений скорости в те числа, которые мы будем показывать игроку. Дело в том, что в тех единицах, которые мы используем в игре, скорость получается очень маленькая, и чтобы как-то приукрасить результат, мы будем умножать её на эту переменную.



Отправляемся в плавание.

Сейчас нам нужно понять, как будет осуществляться перемещение корабля. Как вы понимаете, просто двигать его оси Z не получиться, так как она направлена то верх, то вниз, и корабль будет то утопать, то взлетать над морем. Для перемещения нам понадобится пустой объект, из которого мы до этого сделали префаб. За одно узнаем, как пользоваться префабами из скрипта.

Введём еще две переменные.

 

  public GameObject empl_motion_abstract;

    private GameObject empl_motion_ship;

 

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

В методе Start() мы создадим копию нашей пустышки.

 

empl_motion_ship = Instantiate(empl_motion_abstract) as GameObject;

 

Теперь создадим метод для перемещения нашего корабля.

 

private void Ship_is_moving()

    {

        if (speed_ship_Z < max_speed_ship_Z)

        {

            speed_ship_Z += delta_change_speed_Z * Time.deltaTime;

        }

        empl_motion_ship.transform.position = transform.position;

        empl_motion_ship.transform.rotation = transform.rotation;

        empl_motion_ship.transform.Translate(0, 0, speed_ship_Z * Time.deltaTime, Space.Self);

        empl_motion_ship.transform.position = new Vector3(empl_motion_ship.transform.position.x, transform.position.y, empl_motion_ship.transform.position.z);

        transform.position = empl_motion_ship.transform.position;

}

Давайте будем в нём разбираться. Для начала проверяем, не достигла ли наша скорость максимального значения, и если нет, то прибавляем к ней delta_change_speed_Z * Time.deltaTime. Далее нашу пустышку помещаем в тоже самое место, что и корабль. Следующей строчкой поворачиваем пустышку таким же образом, как и корабль. Потом перемещаем пустышку по локальной оси Z на величину скорости, помноженной на время между кадрами. Далее помещаем пустышку в точку, координаты X и Z которой совпадают с пустышкой, а Y такой же, как и у корабля. То есть, если пустой объект при перемещении ушёл немного вверх или вниз, возвращаем её на прежнюю высоту. Вспоминайте, что покачивания корабля на волнах у нас уже прописано в другом методе, и при его движении не нужно смещение оси Y.

Последней строчкой метода мы помещаем корабль в те же координаты, что и пустой объект.

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

 

private void Ship_is_not_moving()

    {

        if (speed_ship_Z == 0)

        {

            return;

        }

        speed_ship_Z -= delta_change_speed_Z * Time.deltaTime;

        empl_motion_ship.transform.position = transform.position;

        empl_motion_ship.transform.rotation = transform.rotation;

        empl_motion_ship.transform.Translate(0, 0, speed_ship_Z * Time.deltaTime, Space.Self);

        empl_motion_ship.transform.position = new Vector3(empl_motion_ship.transform.position.x, transform.position.y, empl_motion_ship.transform.position.z);

        transform.position = empl_motion_ship.transform.position;

        if (speed_ship_Z <= 0)

        {

            speed_ship_Z = 0;

        }

}

Здесь практически всё то же самое, по аналогии с предыдущим методом. Сначала проверяем, не достигла ли скорость движения нуля, и в таком случае просто покидаем метод. Если же нет, то сбрасываем скорость на ту же величину, на которую мы её увеличивали. Далее действия по перемещению корабля и пустышки повторяются, и в конце делаем проверку: если скорость ушла в минус, то делаем её равной 0.

Давайте напишем метод, который будет объединять в себе два предыдущих.

 

private void Moving_the_ship()

    {

        if (ship_moving)

        {

            Ship_is_moving();

        }

        else

        {

            Ship_is_not_moving();

        }

    }

Здесь всё просто. Если переменная ship_moving установлена в истину, то выполняется метод Ship_is_moving(), иначе метод Ship_is_not_moving().

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

 

if (Input.GetKeyDown(KeyCode.I))

        {

            ship_moving = true;

        }

        if (Input.GetKeyDown(KeyCode.K))

        {

            ship_moving = false;

    }

То есть, если была нажата клавиша I, то запускаем корабль в путешествие, если игрок нажал клавишу K, та запускаем процесс остановки корабля.

Поместим метод Moving_the_ship() в метод Update().

Теперь нам нужно сделать отображение скорости в соответствующем текстовом поле на Канвасе.

Создадим ещё один метод.

 

private void Speed_display()

    {

        text_speed.text = (speed_ship_Z * speed_conversion_factor).ToString();

}

 

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

Добавим метод Speed_display() в метод Update() последним.

Перейдём в наш проект Юнити, кликнем на корабль в окне Иерархии, и в окне Инспектор увидим следующую картину (рис. 54).

Рис. 54.

Под нашим скриптом отображаются все публичные переменные, которые мы сейчас должны связать с теми или иными объектами. В пустое поле напротив первой переменной нужно из окна Иерархии перенести текстовое поле для отображения скорости корабля. Следующие три поля в скрипте у нас пока не задействованы, но мы заранее перенесём на них нужные объекты. Это текстовое поле для отображения угловой скорости и два изображения стрелки влево и вправо. Последняя переменная должна ссылаться на префаб пустышки. Из окна Проект перенесите сюда префаб ___Empl_2. Сохраняемся и запускаем игру. При нажатии на клавишу I, корабль начинает плыть вперёд, а показания скорости увеличиваются до отметки 22.5 (рис. 55).

 

Во время плавания вы также можете перемещаться, как и ранее по кораблю, используя клавиши A, S, D, W. Если немного подождать, то корабль приплывёт к дальним горам. При нажатии на клавишу K, корабль начнёт сбрасывать скорость и остановится.



Повороты.

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

Создадим метод для поворота корабля вправо.

private void Rotation_ship_r()

    {

        if (speed_rotation_Y < max_speed_rotation_Y)

        {

            speed_rotation_Y += delta_change_speed_rotation_Y * Time.deltaTime;

            previous_speed_rotation_Y = speed_rotation_Y;

        }

        transform.Rotate(0, speed_rotation_Y * Time.deltaTime, 0, Space.World);

}

 

Всё очень несложно. Проверяем, не достигла ли угловая скорость максимального значения, и если нет, то увеличиваем её на соответствующее значение. Далее записываем это значение скорости в переменную, хранящую скорость предыдущего кадра. Эта переменная нам ещё понадобится. Последней строчкой просто поворачиваем корабль по оси Y на значение скорости, помноженное на Time.deltaTime.

Метод для поворота влево очень похож на предыдущий.

 

private void Rotation_ship_l()

    {

        if (speed_rotation_Y > -max_speed_rotation_Y)

        {

            speed_rotation_Y -= delta_change_speed_rotation_Y * Time.deltaTime;

            previous_speed_rotation_Y = speed_rotation_Y;

        }

        this.transform.Rotate(0, speed_rotation_Y * Time.deltaTime, 0, Space.World);

}

 

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

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

 

private void Ship_not_rotating()

    {

        if (speed_rotation_Y == 0)

        {

            return;

        }

        else

        {

            if (speed_rotation_Y > 0)

            {

                speed_rotation_Y -= delta_change_speed_rotation_Y * Time.deltaTime;

            }

            else

            {

                speed_rotation_Y += delta_change_speed_rotation_Y * Time.deltaTime;

            }

 

            transform.Rotate(0, speed_rotation_Y * Time.deltaTime, 0, Space.World);

            if ((previous_speed_rotation_Y > 0 & speed_rotation_Y < 0) | (previous_speed_rotation_Y < 0 & speed_rotation_Y > 0))

            {

                previous_speed_rotation_Y = 0;

                speed_rotation_Y = 0;

            }

            else

            {

                previous_speed_rotation_Y = speed_rotation_Y;

            }

 

        }

}

 

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

Напишем метод, который будет управлять поворотами.

 

private void Rotation_Ship()

    {

        switch (sign_speed_rotation)

        {

            case -1:

                Rotation_ship_l();

                break;

            case 1:

                Rotation_ship_r();

                break;

            case 0:

                Ship_not_rotating();

                break;

 

        }

    }

 

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

Добавьте в него несколько новых строк.

 

if (Input.GetKey(KeyCode.J) & !Input.GetKey(KeyCode.L))

        {

            sign_speed_rotation = -1;

        }

        if (!Input.GetKey(KeyCode.J) & Input.GetKey(KeyCode.L))

        {

            sign_speed_rotation = 1;

        }

        if (!Input.GetKey(KeyCode.J) & !Input.GetKey(KeyCode.L))

        {

            sign_speed_rotation = 0;

    }

Как говорится, нет ничего проще. Если удерживается (именно удерживается, GetKey и GetKeyDown в этом и имеют различия) клавиша J, то переменная устанавливается в -1, если удерживается клавиша L, то переменная устанавливается в 1. Если не нажата ни та, ни другая клавиша, то переменная устанавливается в нулевое значение.

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

if (speed_rotation_Y > 0)

        {

            text_speed_rotation.text = speed_rotation_Y.ToString();

            img_arrow_r.SetActive(true);

            img_arrow_l.SetActive(false);

        }

        else if (speed_rotation_Y < 0)

        {

            text_speed_rotation.text = (-1 * speed_rotation_Y).ToString();

            img_arrow_r.SetActive(false);

            img_arrow_l.SetActive(true);

        }

        else

        {

            text_speed_rotation.text = " ";

            img_arrow_r.SetActive(false);

            img_arrow_l.SetActive(false);

    }

Здесь мы отображаем в текстовом поле угловую скорость корабля. Т.к. при повороте налево у нас скорость имеет минусовое значение, то умножаем её на -1. Также, если скорость равна 0, то в текстовом поле печатаем пробел. Таким же образом, в зависимости от направления поворота отображаем стрелку направо или налево. Если скорость равно нулю, то оба изображения делаем неактивными.

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

using System.Collections;

using System.Collections.Generic;

using UnityEngine;

using UnityEngine.UI;

 

public class Players_ship : MonoBehaviour

{

    public Text text_speed;

    public Text text_speed_rotation;

    public GameObject img_arrow_r;

    public GameObject img_arrow_l;

 

    private const float max_speed_rotation_Y = 15f;

    private float speed_rotation_Y = 0f;

    private float previous_speed_rotation_Y = 0f;

    private int sign_speed_rotation = 0;

    private const float delta_change_speed_rotation_Y = 2f;

    private float speed_ship_Z;

    private const float max_speed_ship_Z = 1.5f;

    private const float delta_change_speed_Z = 0.1f;

    private bool ship_moving = false;

    private float speed_conversion_factor = 15f;

 

    public GameObject empl_motion_abstract;

    private GameObject empl_motion_ship;

    private GameObject camera_Player;

    private GameObject empl_ctrl_camera;

    private bool moving_camera_forward = false;

    private bool moving_camera_back = false;

    private float speed_moving_camera = 0.8f;

    private float z_min_camera = -1.7f;

    private float z_max_camera = 1.1f;

    private float z_start_lifting_camera = -0.6f;

    private float z_start_descent_camera = -1.0f;

    private float y_min_camera = 0.7f;

    private float y_max_camera = 1.1f;

    private bool rotation_Camera_r = false;

    private bool rotation_camera_l = false;

    private float speed_rotation_camera = 60f;

    private const float max_speed_Y = 0.2f;

    private float speed_Y = max_speed_Y;

    private float min_speed_Y;

    private bool ship_up = true;

    private int sign_speed_Y = 1; //1- вверх -1 - вниз

    private const float delta_change_speed = 0.1f;

    private const float max_angle_rotation = 7f;

    private float angle_up_down;

 

    void Start()

    {

        min_speed_Y = max_speed_Y / 100;

        camera_Player = transform.GetChild(0).transform.gameObject;

        empl_ctrl_camera = transform.GetChild(1).transform.gameObject;

        empl_motion_ship = Instantiate(empl_motion_abstract) as GameObject;

 

    }

 

    // Update is called once per frame

    void Update()

    {

        Rocking_ship();

        Tracking_clicks_on_computer();

        Rotation_Ship();

        Moving_the_ship();

        Moving_camera();

        Speed_display();

    }

    private void Speed_display()

    {

        text_speed.text = (speed_ship_Z * speed_conversion_factor).ToString();

        if (speed_rotation_Y > 0)

        {

            text_speed_rotation.text = speed_rotation_Y.ToString();

            img_arrow_r.SetActive(true);

            img_arrow_l.SetActive(false);

        }

        else if (speed_rotation_Y < 0)

        {

            text_speed_rotation.text = (-1 * speed_rotation_Y).ToString();

            img_arrow_r.SetActive(false);

            img_arrow_l.SetActive(true);

        }

        else

        {

            text_speed_rotation.text = " ";

            img_arrow_r.SetActive(false);

            img_arrow_l.SetActive(false);

        }

    }

    private void Ship_is_moving()

    {

        if (speed_ship_Z < max_speed_ship_Z)

        {

            speed_ship_Z += delta_change_speed_Z * Time.deltaTime;

        }

        empl_motion_ship.transform.position = transform.position;

        empl_motion_ship.transform.rotation = transform.rotation;

        empl_motion_ship.transform.Translate(0, 0, speed_ship_Z * Time.deltaTime, Space.Self);

        empl_motion_ship.transform.position = new Vector3(empl_motion_ship.transform.position.x, transform.position.y, empl_motion_ship.transform.position.z);

        transform.position = empl_motion_ship.transform.position;

    }

    private void Ship_is_not_moving()

    {

        if (speed_ship_Z == 0)

        {

            return;

        }

        speed_ship_Z -= delta_change_speed_Z * Time.deltaTime;

        empl_motion_ship.transform.position = transform.position;

        empl_motion_ship.transform.rotation = transform.rotation;

        empl_motion_ship.transform.Translate(0, 0, speed_ship_Z * Time.deltaTime, Space.Self);

        empl_motion_ship.transform.position = new Vector3(empl_motion_ship.transform.position.x, transform.position.y, empl_motion_ship.transform.position.z);

        transform.position = empl_motion_ship.transform.position;

        if (speed_ship_Z <= 0)

        {

            speed_ship_Z = 0;

        }

    }

 

    private void Moving_the_ship()

    {

        if (ship_moving)

        {

            Ship_is_moving();

        }

        else

        {

            Ship_is_not_moving();

        }

    }

 

    private void Rotation_ship_r()

    {

        if (speed_rotation_Y < max_speed_rotation_Y)

        {

            speed_rotation_Y += delta_change_speed_rotation_Y * Time.deltaTime;

            previous_speed_rotation_Y = speed_rotation_Y;

        }

        transform.Rotate(0, speed_rotation_Y * Time.deltaTime, 0, Space.World);

    }

    private void Rotation_ship_l()

    {

        if (speed_rotation_Y > -max_speed_rotation_Y)

        {

            speed_rotation_Y -= delta_change_speed_rotation_Y * Time.deltaTime;

            previous_speed_rotation_Y = speed_rotation_Y;

        }

        this.transform.Rotate(0, speed_rotation_Y * Time.deltaTime, 0, Space.World);

    }

    private void Ship_not_rotating()

    {

        if (speed_rotation_Y == 0)

        {

            return;

        }

        else

        {

            if (speed_rotation_Y > 0)

            {

                speed_rotation_Y -= delta_change_speed_rotation_Y * Time.deltaTime;

            }

            else

            {

                speed_rotation_Y += delta_change_speed_rotation_Y * Time.deltaTime;

            }

 

            transform.Rotate(0, speed_rotation_Y * Time.deltaTime, 0, Space.World);

            if ((previous_speed_rotation_Y > 0 & speed_rotation_Y < 0) | (previous_speed_rotation_Y < 0 & speed_rotation_Y > 0)) //скрость стала нолевой и сменила знак

            {

                previous_speed_rotation_Y = 0;

                speed_rotation_Y = 0;

            }

            else

            {

                previous_speed_rotation_Y = speed_rotation_Y;

            }

 

        }

    }

    private void Rotation_Ship()

    {

        switch (sign_speed_rotation)

        {

            case -1:

                Rotation_ship_l();

                break;

            case 1:

                Rotation_ship_r();

                break;

            case 0:

                Ship_not_rotating();

                break;

 

        }

    }

    private void Moving_camera()

    {

 

        if (moving_camera_forward & empl_ctrl_camera.transform.localPosition.z < z_max_camera)

        {

            if (empl_ctrl_camera.transform.localPosition.z > z_start_descent_camera & empl_ctrl_camera.transform.localPosition.y > y_min_camera)

            {

                empl_ctrl_camera.transform.Translate(0, -speed_moving_camera * Time.deltaTime, speed_moving_camera * Time.deltaTime, Space.Self);

            }

            else

            {

                empl_ctrl_camera.transform.Translate(0, 0, speed_moving_camera * Time.deltaTime, Space.Self);

            }

 

        }

        if (moving_camera_back & empl_ctrl_camera.transform.localPosition.z > z_min_camera)

        {

            if (empl_ctrl_camera.transform.localPosition.z < z_start_lifting_camera & empl_ctrl_camera.transform.localPosition.y < y_max_camera)

            {

                empl_ctrl_camera.transform.Translate(0, speed_moving_camera * Time.deltaTime, -speed_moving_camera * Time.deltaTime, Space.Self);

            }

            else

            {

                empl_ctrl_camera.transform.Translate(0, 0, -speed_moving_camera * Time.deltaTime, Space.Self);

            }

        }

        if (rotation_camera_l)

        {

            camera_Player.transform.Rotate(0, -speed_rotation_camera * Time.deltaTime, 0, Space.Self);

        }

        if (rotation_Camera_r)

        {

            camera_Player.transform.Rotate(0, speed_rotation_camera * Time.deltaTime, 0, Space.Self);

        }

 

        camera_Player.transform.position = empl_ctrl_camera.transform.position;

    }

    private void Rocking_ship()

    {

        Speed_change_Y();

        Angle_calculation_up_down();

        Rotation_ship_up_down();

        Up_Down_Ship();

    }

    private void Speed_change_Y()

    {

        speed_Y += sign_speed_Y * delta_change_speed * Time.deltaTime;

        if (speed_Y >= max_speed_Y & sign_speed_Y > 0)

        {

            sign_speed_Y *= -1;

        }

        if (speed_Y <= min_speed_Y & sign_speed_Y < 0)

        {

            sign_speed_Y *= -1;

            ship_up = !ship_up;

        }

 

    }

    private void Up_Down_Ship()

    {

        if (ship_up)

        {

            transform.Translate(0, speed_Y * Time.deltaTime, 0, Space.World);

        }

        else

        {

            transform.Translate(0, -speed_Y * Time.deltaTime, 0, Space.World);

        }

 

    }

    private void Angle_calculation_up_down()

    {

        float s = speed_Y;

        if (s < 0)

        {

            s = 0.00001f;

        }

        angle_up_down = (s / max_speed_Y) * max_angle_rotation;

    }

    private void Rotation_ship_up_down()

    {

        if (ship_up)

        {

            transform.localEulerAngles = new Vector3(-angle_up_down, transform.localEulerAngles.y, transform.localEulerAngles.z);

        }

        else

        {

            transform.localEulerAngles = new Vector3(angle_up_down, transform.localEulerAngles.y, transform.localEulerAngles.z);

        }

 

    }

    private void Tracking_clicks_on_computer()

    {

 

        if (Input.GetKey(KeyCode.W) & !Input.GetKey(KeyCode.S))

        {

            moving_camera_forward = true;

        }

        else

        {

            moving_camera_forward = false;

        }

        if (!Input.GetKey(KeyCode.W) & Input.GetKey(KeyCode.S))

        {

            moving_camera_back = true;

        }

        else

        {

            moving_camera_back = false;

        }

        if (Input.GetKey(KeyCode.A) & !Input.GetKey(KeyCode.D))

        {

            rotation_camera_l = true;

        }

        else

        {

            rotation_camera_l = false;

        }

        if (!Input.GetKey(KeyCode.A) & Input.GetKey(KeyCode.D))

        {

            rotation_Camera_r = true;

        }

        else

        {

            rotation_Camera_r = false;

        }

        //------------------

        if (Input.GetKey(KeyCode.J) & !Input.GetKey(KeyCode.L))

        {

            sign_speed_rotation = -1;

        }

        if (!Input.GetKey(KeyCode.J) & Input.GetKey(KeyCode.L))

        {

            sign_speed_rotation = 1;

        }

        if (!Input.GetKey(KeyCode.J) & !Input.GetKey(KeyCode.L))

        {

            sign_speed_rotation = 0;

        }

        //---------------------

        if (Input.GetKeyDown(KeyCode.I))

        {

            ship_moving = true;

        }

        if (Input.GetKeyDown(KeyCode.K))

        {

            ship_moving = false;

        }

    }

}

 

Запустите игру. Управление кораблём I, K, J, L. Теперь вы можете путешествовать в любом направлении. Обратите внимание, что значения скорости, а также изображения со стрелками направления поворота отображаются корректно (рис. 56).

Рис. 56.



Выбор пушки.

Сейчас нам нужно сделать так, чтобы при клике на какую-либо пушку, она становилась активной, и над ней появлялась стрелка. Для начала создадим скрипт, который впоследствии наложим на префаб пушки. В соответствующей папке создайте новый C# скрипт и назовите его Cannon. Откройте его и впишите новую переменную.

private bool cannon_active = false;

 

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

Далее создадим публичный метод для установки этой переменной из другого скрипта.

 

public void Set_cannon_active(bool a)

    {

        cannon_active = a;

}

С этим скриптом пока закончим. Перейдите в редактор Юнити, в окне Проект кликните на префабе пушки и в окре Инспектор добавим новый компонент - скрипт, который мы только что создали.

Теперь откройте ранее созданный нами скрипт Class_common_variables. Добавим переменную

 

private GameObject camera_players;

 

Она будет ссылаться на нашу камеру на корабле. Но камеру мы определили в скрипте, который находится на объекте корабль. Давайте сделаем так, чтобы из скрипта на корабле наша камера передавалась в данный скрипт. Напомним себе, что этот скрипт будет главным в игре, и мы его поместим на пустой объект ___empty_shared_variables, ранее созданный нами. Откроем скрипт Players_ship и создадим в нём публичную переменную, которая будет ссылаться на главный скрипт Class_common_variables.

 

public Class_common_variables link_on_Class;

 

Теперь в методе Start() запишем код, который будет передавать нашу камеру в другой скрипт.

 

link_on_Class.Set_Camera(camera_Player);

 

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

 

public void Set_Camera(GameObject c)

    {

        camera_players = c;

}

 

Здесь всё просто, этот метод принимает в себя какую-либо переменную типа GameObject и присваивает её переменной camera_players.

Сейчас в редакторе Юнити нам для скрипта на корабле нужно указать ссылку на класс Class_common_variables. На объект ___empty_shared_variables поместите скрипт Class_common_variables. Затем кликните на нашем корабле и в панели инспектор увидите одно незаполненное поле (рис. 57), в которое мы должны переместить объект, на котором находится скрипт Class_common_variables. Этим объектом является ___empty_shared_variables.

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

В наш главный скрипт добавим ещё несколько переменных.

 

private GameObject active_cannon;

    private bool cannon_ready_fire = false;

    public LayerMask Сannon;

Первая из них – это ссылка на активную пушку, ей будет присваиваться выбранный объект, когда игрок кликнет на него.

Вторая переменная показывает, готова пушка к выстрелу или нет. Когда игрок только выбрал пушку, из неё сразу можно стрелять. Во время отдачи, эта переменная ставится в ложь. Третья переменная – это ссылка на слой, который мы затем укажем в окне Инспектор. Про то, что такое Слой, было рассказано немного ранее, и на префабе пушки у нас уже установлен нужный слой. Именно его нужно будет указать в этой переменной.

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

 

private void Launch_ray_search_cannon()

    {

        RaycastHit hit;

        Ray ray = camera_players.GetComponent<Camera>().ScreenPointToRay(Input.mousePosition);

 

        if (Physics.Raycast(ray, out hit, 100f, Сannon))

        {

            if (hit.collider.gameObject.tag == "cannon")

            {

                if (active_cannon != null)

                {

                    active_cannon.transform.GetComponent<Cannon>().Set_cannon_active(false);

                    active_cannon.transform.GetChild(3).gameObject.SetActive(false);

                }

                active_cannon = hit.collider.gameObject;

                active_cannon.transform.GetComponent<Cannon>().Set_cannon_active(true);

                active_cannon.transform.GetChild(3).gameObject.SetActive(true);

                cannon_ready_fire = true;

            }

        }

    }

 

Теперь давайте разбираться. Вначале мы создаём переменную hit. Это структура данных, в которую будет записываться вся информация о контакте луча с объектом. Второй сточкой создаётся луч ray, который выходит из камеры игрока через координаты, которые берём из Input.mousePosition, то есть, куда кликнул игрок. Метод Physics.Raycast(ray, out hit, 100f, Сannon) запускает созданный нами луч на расстояние 100 единиц от камеры, этот луч может контактировать только с теми объектами, у которых есть слой Сannon. Если луч нашёл такой объект, то вся необходимая информация записывается в переменную hit. Сам метод имеет возвращаемое значение типа bool. Если луч нашёл нужный объект, то возвращается истина, в противном случае – ложь.

В условии if мы проверяем, был ли контакт луча с нужным объектом, то есть при клике по экрану, указал ли игрок на одну из пушек. Если условие выполняется, то из структуры данных достаём тот объект, с которым вошел в контакт луч, и сравниваем его тег с тегом пушки "cannon". Вполне возможно, это излишняя проверка, так как слой Сannon у нас находится только на пушках, и с другими объектами луч не мог войти в контакт. Но данная проверка может пригодиться, если в дальнейшем вы захотите, чтобы этот слой был и на других объектах.

Если условие выполнилось, то есть игрок действительно кликнул по пушке, то в условии if (active_cannon != null) проверяем, нет ли у нас уже активной пушки, и если есть, то следующими двумя строками делаем её неактивной, и скрываем стрелку над пушкой. Далее назначаем переменной active_cannon новую пушку, т.е. ту, на которую указал игрок. Следующей строчкой передаём самой пушке информацию о том, что она активная. Далее делаем стрелку (как вы помните, она у нас четвёртый по счёту дочерний объект) активной. В конце устанавливаем готовность пушки в истину.

Метод для запуска луча готов. Осталось только записать код, который будет активировать этот метод. Это, как вы понимаете, нужно будет делать в тот момент, когда игрок кликнет мышью по экрану. Добавим следующий метод.

 

private void Tracking_clicks_on_computer()

    {

 

        if (Input.GetMouseButtonDown(0))

        {

            Launch_ray_search_cannon();

        }

}

В этот метод в дальнейшем мы будем ещё добавлять код, а на данном этапе, он просто отслеживает клик левой кнопки мыши, и запускает метод Launch_ray_search_cannon(). Добавьте вновь созданныё метод в Update(). Сейчас скрипт должен выглядеть следующим образом.

 

using System.Collections;

using System.Collections.Generic;

using UnityEngine;

 

public class Class_common_variables : MonoBehaviour

{

    private GameObject camera_players;

    private GameObject active_cannon;

    private bool cannon_ready_fire = false;

    public LayerMask Сannon;

    void Start()

    {

       

    }

 

    // Update is called once per frame

    void Update()

    {

        Tracking_clicks_on_computer();

    }

    private void Tracking_clicks_on_computer()

    {

 

        if (Input.GetMouseButtonDown(0))

        {

            Launch_ray_search_cannon();

        }

    }

        private void Launch_ray_search_cannon()

    {

        RaycastHit hit;

        Ray ray = camera_players.GetComponent<Camera>().ScreenPointToRay(Input.mousePosition);

 

        if (Physics.Raycast(ray, out hit, 100f, Сannon))

        {

            if (hit.collider.gameObject.tag == "cannon")

            {

                if (active_cannon != null)

                {

                    active_cannon.transform.GetComponent<Cannon>().Set_cannon_active(false);

                    active_cannon.transform.GetChild(3).gameObject.SetActive(false);

                }

                active_cannon = hit.collider.gameObject;

                active_cannon.transform.GetComponent<Cannon>().Set_cannon_active(true);

                active_cannon.transform.GetChild(3).gameObject.SetActive(true);

                cannon_ready_fire = true;

            }

        }

    }

    public void Set_Camera(GameObject c)

    {

        camera_players = c;

    }

}

 

Перейдите в проект Юнити, в окне Иерархии выберите пустышку, на которой находится наш главный скрипт, и в окне Инспектор можно будет увидеть, что в нашем скрипте есть незаполненное публичное поле (рис. 58), в котором мы должны указать слой для запуска луча. Это слой cannon, который мы наложили на пушку.

Рис. 58.

 

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

Рис. 59.

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

 

using UnityEngine;

 

public class Arrow : MonoBehaviour

{

    private float rotation_speed = 250f;

    void Start()

    {

 

    }

 

    void Update()

    {

        transform.Rotate(-rotation_speed * Time.deltaTime, 0, 0, Space.Self);

    }

 

}

Это очень простой скрипт, который мы сейчас наложим на четвёртый дочерний объект префаба пушки, т.е. стрелку. У нас есть одна переменная, которая определяет скорость вращения стрелки, а в методе Update() мы просто поворачиваем в каждом кадре объект, на котором находится скрипт, по оси X. Почему же не по оси Y? Да потому, что стрелку мы делали не на основу пустого объекта, у которого были бы оси направлены в нужном направлении, а взяли непосредственно 3D импортированную модель, с неправильным направлением осей. Можете поэкспериментировать и узнать, что будет, если вращать стрелку по другим осям. Откройте папку Prefabs и раскройте префаб пушки, дважды щёлкнув на него (рис. 60).

Рис. 60.

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



Ядро пушки.

Сейчас нам нужно будет создать материал, который нам понадобится для взрывов и для огненного шлейфа от летящего ядра. В соответствующей папке создайте материал, и назовите его Explosion. В панели Инспектор для него выберите следующий параметры – ShaderMobileParticlesAdditive. И в качестве налаживаемого изображения, выберите стандартную картинку Юнити (рис. 61).

Рис. 61.

Так же давайте создадим материал для нашего будущего ядра. Назовите его Cannonball_D. Текстура – отсутствует, цвет (RGB 91, 91, 91). Параметры Metallic и Smoodness в 0.

Далее будем конструировать ядро. На сцене создайте сферу (в окне Иерархии кликните правой кнопкой мыши), сделайте масштаб по всем осям 0.08, и разместите около какой-нибудь пушки (рис. 62).

Рис. 62.

Присвойте сфере материал ядра. Создадим огненный шлейф для ядра. В панели Иерархии вызовите контекстовое меню, и далее EffectsParticle System. Появившийся объект масштабируйте по всем осям с коэффициентом 0.08 (рис. 63).

Рис. 63.

Параметр Start Lifetime установите в 0.3, Параметр Start Speed сделайте равным 0. Справа от параметра Start Size выберите стрелочку, и выберите случайное между двумя значениями 1 и 3. Цвет (RGB 255, 216, 169). Параметр Simulation Space установите в World. Раскройте структуру Emission и параметр Rate over Time установите равным 100. В качестве фигуры выберите сферу. Поставьте галочку возле структуры Size over Lifetime и раскройте её. Кликните на график Size, внизу появится график для редактирования. Приведите его к виду, как на картинке 64.

Рис. 64.

Раскройте список Renderer, и в качестве материала выберите созданный нами материал для взрывов Explosion. Огненный шлейф для ядра готов. Осталось совместить в пространстве его с ядром, и сделать дочерним объектом, по отношению к нему. При этом мы увидим, что система частиц увеличилась в размерах, т.к. масштаб у дочернего объекта стал равным 1 (рис. 65). Опять исправьте масштаб на 0.08 (рис. 66).

Рис. 65. Рис.66.

Давайте сделаем из нашего ядра префаб. Перенесите сферу в папку Prefabs, и назовите Cannonballs_Player. Копию удалите со сцены. Немного отредактируем наше ядро. Откройте его префаб, дважды кликнув по нем, и в компоненте коллайдера сделайте его триггером, т.к. ядро у нас не будет ни с чем сталкиваться, а только будут отслеживаться его взаимодействия с другими объектами. Далее увеличьте радиус коллайдера до 1.4, т.к. скорость полёта ядра достаточно высокая, и может получиться так, что в одном кадре ядро окажется пред объектом, а в следующем кадре уже пролетит через него, и столкновение не будет отслежено.

К ядру мы будем применять силу, для его запуска из пушки, а для этого нужно, чтобы ядро обладало физическими свойствами, такими как масса, на него воздействовала гравитация. За всё это отвечает компонент Rigidbody. Давайте добавим его к ядру. Установите параметры компонента, как на рисунке 67.

Рис. 67.



Выстрел из пушки.

Сейчас мы сделаем так, чтобы при нажатии на клавишу «M», происходил выстрел из активной пушки. Пока это будет просто запуск ядра без различных эффектов, таких как отдача назад, дыма от пороховых газов и звукового сопровождения.

Для начала, в скрипт, который находится на префабе пушки, добавим две переменные.

 

private GameObject cannon;

public GameObject cannonball_abstract;

 

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

В методе Start() нужно записать код для инициализации переменной, указывающей на ствол пушки.

 

cannon = transform.GetChild(0).gameObject;

 

Вспоминаем, что в дочерних объектах пушки, ствол находится на самом верху, то есть имеет индекс 0. Далее создаём сам метод для создания и запуска ядра.

 

private void Creating_and_launching_cannonball()

    {

       

        GameObject yadro_sozdannoe = Instantiate(cannonball_abstract) as GameObject;

        yadro_sozdannoe.transform.position = cannon.transform.position;

        yadro_sozdannoe.transform.rotation = cannon.transform.rotation;

        yadro_sozdannoe.transform.Translate(0, 0, 0.25f, Space.Self);

        yadro_sozdannoe.GetComponent<Rigidbody>().AddRelativeForce(yadro_sozdannoe.transform.forward * 0.5f, ForceMode.Impulse);

}

 

Первой строчкой создаётся копия ядра на основе указанного нами префаба. Второй строчкой помещаем ядро в ту же точку пространства, что и ствол пушки. Третьей строчкой поворачиваем ядро таким же образом, как и пушку. Четвёртой строчкой кода перемещаем ядро по локальной оси Z вперёд на небольшое расстояние, чтобы оно находилось на выходе из ствола, а не посередине. Последней строчкой мы запускаем ядро в полёт. Для этого нужно обратиться к компоненту Rigidbody, у которого есть метод для применения силы к данному объекту AddRelativeForce(). В качестве первого параметра этот метод принимает направление прилаживаемой силы, в данном случае это локальная ось Z. Если это направление мы помножим на какое-нибудь значение, то силы выстрела будет меняться. В дальнейшем мы сделаем, что это значение будет зависеть от игрока, а пока просто помножим на 0,5. Вторым параметром в метод передаётся вид прилаживаемой силы. Для выстрела из пушки больше всего подходит ForceMode.Impulse.

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

 

public void Cannonball_shot()

    {

        Creating_and_launching_cannonball();

}

 

Сейчас вернёмся в наш главный скрипт Class_common_variables. В метод Tracking_clicks_on_computer() добавим следующее условие.

 

if (Input.GetKeyDown(KeyCode.M) & active_cannon != null)

        {

            active_cannon.transform.GetComponent<Cannon>().Cannonball_shot();

    }

 

В этом условии проверяется, нажал ли игрок клавишу M, и при этом активная пушка уже должна быть выбрана, то есть над какой-то пушкой на корабле уже есть стрелка. Если условие выполняется, то обращаемся к скрипту на активной пушке, и вызываем метод для запуска ядра. В проекте Юнити откройте префаб пушки, и на панели Инспектор в компоненте скрипта заполните поле для префаба ядра (рис. 68).

Рис. 68.

Запустите игру. Теперь, выбирая пушки, вы можете стрелять из них (рис. 69).

Рис. 69.



Отдача пушки.

Давайте начнём делать отдачу пушки после выстрела. Откройте скрипт Cannon. Добавим несколько новых переменных.

 

private GameObject empty_shared_variables;

    private GameObject wheel_1;

    private GameObject wheel_2;

    private Vector3 starting_position;

    private float previous_distance;

    private float forward_speed = 0.05f;

    private const float initial_recoil_velocity = 3f;

    private float speed_of_recoil = initial_recoil_velocity;

    private float delta_speed_change_back = 15f;

    private bool recoil_back = false;

    private bool moving_forward = false;

    private float speed_rotation_wheels_during_recoil = 800f;

private float speed_rotation_wheels_forward = 100f;

 

Первая переменная будет ссылаться на пустышку, на которой находится главный скрипт игры Class_common_variables. Следующие две переменные будут ссылаться на передние и задние колёса пушки. В четвёртой переменной будут храниться стартовые локальные координаты пушки. Переменная previous_distance показывает расстояние от стартовой позиции пушки до её текущего расположения. Она будет меняться в каждом кадре при возвращении пушки после отдачи. Следующая переменная определяет скорость движения пушки вперёд после отдачи. Константа initial_recoil_velocity устанавливает начальную скорость отдачи пушки. Переменная speed_of_recoil определяет текущую скорость отдачи, она будет уменьшаться в каждом кадре на величину delta_speed_change_back. Следующая переменная устанавливается в истину, если идёт отдача пушки назад. Переменная moving_forward устанавливается в истину, если идёт возвращение пушки после отдачи на начальную позицию. Последние две переменные устанавливают скорость вращения колёс при отдаче и при движении пушки вперёд соответственно.

В метод Start() добавим следующие строки кода.

 

empty_shared_variables = GameObject.Find("___empty_shared_variables");

        wheel_1 = transform.GetChild(1).gameObject;

        wheel_2 = transform.GetChild(2).gameObject;

    starting_position = transform.localPosition;

 

В первой строке мы ищем на сцене объект с именем "___empty_shared_variables" и присваиваем его нашей переменной. Следующими двумя строками мы берём второй и третий (если по порядку, а не по индексу) дочерние объекты пушки, то ест колёса, и присваиваем их переменным. Последней строкой начальная локальная позиция пушки присваивается нужной переменной.

В самое начало метода Creating_and_launching_cannonball() добавьте условие.

 

if (recoil_back | moving_forward)

        {

            return;

    }

 

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

 

recoil_back = true;

empty_shared_variables.GetComponent<Class_common_variables>().Set_cannon_ready_fire(false);

 

После создания ядра и выстрела, мы устанавливаем, что пушка находится в режиме отдачи. Следующей строкой мы обращаемся к главному скрипту, который находится на пустышке, на которую в свою очередь ссылается переменная empty_shared_variables. В этом скрипте мы вызываем метод, который устанавливает готовность активной пушки в ложь. Эта строка вызовет ошибку, т.к. этот метод мы ещё не создали. Давайте перейдём в скрипт Class_common_variables и добавим туда метод.

 

public void Set_cannon_ready_fire(bool g)

    {

        cannon_ready_fire = g;

}

 

Возвращаемся в скрипт для пушки. Создаём метод для отдачи пушки.

 

private void Speed_change_and_recoil()

    {

        if (!recoil_back)

        {

            return;

        }

        wheel_1.transform.Rotate(-speed_rotation_wheels_during_recoil * Time.deltaTime, 0, 0, Space.Self);

        wheel_2.transform.Rotate(-speed_rotation_wheels_during_recoil * Time.deltaTime, 0, 0, Space.Self);

        speed_of_recoil -= delta_speed_change_back * Time.deltaTime;

        if (speed_of_recoil <= 0)

        {

            recoil_back = false;

            moving_forward = true;

            speed_of_recoil = initial_recoil_velocity;

            previous_distance = Vector3.Distance(starting_position, transform.localPosition);

        }

        else

        {

            transform.Translate(0, 0, -speed_of_recoil * Time.deltaTime);

        }

   }

 

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

Теперь создадим метод для перемещения пушки вперёд после отдачи.

 

private void Moving_forward()

    {

        if (moving_forward)

        {

            wheel_1.transform.Rotate(speed_rotation_wheels_forward * Time.deltaTime, 0, 0, Space.Self);

            wheel_2.transform.Rotate(speed_rotation_wheels_forward * Time.deltaTime, 0, 0, Space.Self);

            transform.Translate(0, 0, forward_speed * Time.deltaTime);

            float dist = Vector3.Distance(starting_position, transform.localPosition);

            if (dist > previous_distance)

            {

                moving_forward = false;

                transform.localPosition = starting_position;

                if (cannon_active)

                {

                    empty_shared_variables.GetComponent<Class_common_variables>().Set_cannon_ready_fire(true);

                }

 

            }

            else

            {

                previous_distance = dist;

            }

        }

    }

 

Сначала проверяем, если идёт перемещение вперёд, то выполняем следующий код. Первыми двумя строками вращаем колёса, затем перемещаем пушку вперёд с заданной скоростью. Далее вычисляем расстояние от стартовой позиции пушки до её текущего положения. Следующим условием мы пробуем понять, достигла ли пушка начальной позиции или нет. Просто сравнивать переменную dist с нулём нельзя, так как при своём движении вперёд пушка вряд ли встанет точно на начальную позицию. Скорее всего, в одном кадре она будет до неё, а в следующем немного её переедет. Именно для этого и вводилась переменная previous_distance, которая после завершения отдачи пушки устанавливается в своё максимальное значение, и с каждым кадром при приближении пушки к стартовой позиции постепенно уменьшается. При переезде же пушки стартовой позиции, эта переменная начнёт, наоборот, увеличиваться, и в этот момент мы поймём, что пушка встала почти на место, и закончим её перемещение вперёд. Всё это и описывает следующий за условием код. Мы проверяем: если текущая дистанция больше дистанции в предыдущем кадре, то режим движения вперёд ставим в ложь, затем пушку устанавливаем точно в начальные координаты. Потом проверяем: если данная пушка до сих пор активна (игрок после выстрела мог сразу выбрать другую пушку), то в главном скрипте устанавливаем режим готовности пушки в истину. Если же условие не выполнено, то есть, если текущая дистанция меньше предыдущей, то просто для следующего кадра устанавливаем переменную previous_distance равной текущей дистанции в этом кадре.

Методы для отдачи пушки, и её возвращения на стартовую позицию нужно поместить в метод Update(). Вот как на данный момент должен выглядеть скрипт Cannon.

 

using System.Collections;

using System.Collections.Generic;

using UnityEngine;

 

public class Cannon : MonoBehaviour

{

    private bool cannon_active = false;

    private GameObject cannon;

    public GameObject cannonball_abstract;

   

    private GameObject empty_shared_variables;

    private GameObject wheel_1;

    private GameObject wheel_2;

    private Vector3 starting_position;

    private float previous_distance;

    private float forward_speed = 0.05f;

    private const float initial_recoil_velocity = 3f;

    private float speed_of_recoil = initial_recoil_velocity;

    private float delta_speed_change_back = 15f;

    private bool recoil_back = false;

    private bool moving_forward = false;

    private float speed_rotation_wheels_during_recoil = 800f;

    private float speed_rotation_wheels_forward = 100f;

    void Start()

    {

        empty_shared_variables = GameObject.Find("___empty_shared_variables");

        cannon = transform.GetChild(0).gameObject;

        wheel_1 = transform.GetChild(1).gameObject;

        wheel_2 = transform.GetChild(2).gameObject;

        starting_position = transform.localPosition;

    }

 

    void Update()

    {

        Speed_change_and_recoil();

        Moving_forward();

    }

    private void Creating_and_launching_cannonball()

    {

        if (recoil_back | moving_forward)

        {

            return;

        }

        GameObject yadro_sozdannoe = Instantiate(cannonball_abstract) as GameObject;

        yadro_sozdannoe.transform.position = cannon.transform.position;

        yadro_sozdannoe.transform.rotation = cannon.transform.rotation;

        yadro_sozdannoe.transform.Translate(0, 0, 0.25f, Space.Self);

        yadro_sozdannoe.GetComponent<Rigidbody>().AddRelativeForce(yadro_sozdannoe.transform.forward * 0.5f, ForceMode.Impulse);

        recoil_back = true;

        empty_shared_variables.GetComponent<Class_common_variables>().Set_cannon_ready_fire(false);

    }

    private void Speed_change_and_recoil()

    {

        if (!recoil_back)

        {

            return;

        }

        wheel_1.transform.Rotate(-speed_rotation_wheels_during_recoil * Time.deltaTime, 0, 0, Space.Self);

        wheel_2.transform.Rotate(-speed_rotation_wheels_during_recoil * Time.deltaTime, 0, 0, Space.Self);

        speed_of_recoil -= delta_speed_change_back * Time.deltaTime;

        if (speed_of_recoil <= 0)

        {

            recoil_back = false;

            moving_forward = true;

            speed_of_recoil = initial_recoil_velocity;

            previous_distance = Vector3.Distance(starting_position, transform.localPosition);

        }

        else

        {

            transform.Translate(0, 0, -speed_of_recoil * Time.deltaTime);

        }

    }

    private void Moving_forward()

    {

        if (moving_forward)

        {

            wheel_1.transform.Rotate(speed_rotation_wheels_forward * Time.deltaTime, 0, 0, Space.Self);

            wheel_2.transform.Rotate(speed_rotation_wheels_forward * Time.deltaTime, 0, 0, Space.Self);

            transform.Translate(0, 0, forward_speed * Time.deltaTime);

            float dist = Vector3.Distance(starting_position, transform.localPosition);

            if (dist > previous_distance)

            {

                moving_forward = false;

                transform.localPosition = starting_position;

                if (cannon_active)

                {

                    empty_shared_variables.GetComponent<Class_common_variables>().Set_cannon_ready_fire(true);

                }

 

            }

            else

            {

                previous_distance = dist;

            }

        }

    }

    public void Cannonball_shot()

    {

        Creating_and_launching_cannonball();

    }

    public void Set_cannon_active(bool a)

    {

        cannon_active = a;

    }

}

Запустите игру. При выстреле из пушки можно видеть, как она откатывается назад, а затем медленно приближается к своей стартовой позиции. В это время выстрел из данной пушки невозможен, но игрок может выбрать другую пушку, и производить выстрелы из неё (рис. 70).

Рис 70.



Пороховые газы.

Сейчас займёмся дымом от пороховых газов, с ним выстрел будет смотреться более эффектно. Создайте систему частиц так же, как мы это делали для огненного шлейфа ядра, и все параметры установите, как на изображениях 71-74. На рис. 73 показан градиент для структуры Color over Lifetime. Он определяет, что с течением времени частицы становятся невидимыми и исчезают. Так же кривая Size over Lifetime устанавливает уменьшение частиц с течением времени.

Рис. 71 Рис.72

Рис. 73 Рис. 74.

Сделаем из системы частиц префаб, то есть перенесём её с соответствующую папку в окне Проект и назовём Smoke_Cannon. Удалите копию со сцены. Нам осталось в скрипте во время выстрела создать дым после создания ядра в том же самом месте. Эффект будет проигрываться менее секунды. Давайте сделаем это. Откройте скрипт Cannon и добавьте новую переменную.

 

public GameObject smoke_from_cannon_abstract;

 

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

 

GameObject smoke = Instantiate(smoke_from_cannon_abstract) as GameObject;

smoke.transform.position = yadro_sozdannoe.transform.position;

 

Первой строчкой мы создаём дым на основе префаба, а второй помещаем его в то же место, где находится ядро перед запуском, то есть на выходе из ствола пушки. В панели Инспектор для префаба пушки появится пустое поле в компоненте скрипта, в которое переместите префаб дыма. Запустите игру. Теперь выстрел выглядит достаточно красиво (рис. 75). Осталось добавить звуковое сопровождение.

Рис. 75.



Звук выстрела.

Откройте скрипт Class_common_variables. Добавим в него новые переменные.

 

public AudioSource audioSource;

     public AudioSource audioSource_Waves_and_Ship;

public AudioClip sound_cannon_shot;

 

Первые две переменные – это ссылки на источники звука, которые мы создали, и они находятся на сцене. Последняя переменная – это ссылка на звуковой файл выстрела из корабельной пушки. Добавим метод.

 

private void Play_Audio(AudioClip clip)

    {

        audioSource.PlayOneShot(clip);

    }

 

В качестве параметра он принимает звуковой файл, и воспроизводит его, используя источник звука, на который ссылается переменная audioSource. Добавим ещё один метод уже для воспроизведения конкретного файла.

 

public void Sound_cannon_shot()

    {

        Play_Audio(sound_cannon_shot);

}

 

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

 

empty_shared_variables.GetComponent<Class_common_variables>().Sound_cannon_shot();

 

В проекте Юнити кликните на пустышке ___empty_shared_variables. В компоненте скрипта заполните три новые пустые поля. Перенесите туда источники звука из окна Иерархии. В качестве звукового файла для выстрела используйте Players cannon shot (рис. 76). Запускаем игру и наслаждаемся замечательными выстрелами со звуковым сопровождением.

 



Взрыв на воде.

Пора сделать всплеск воды, при попадании в неё пушечного ядра. Создайте систему частиц на панели Иерархии и все параметры установите, как на изображениях 77-79. Снимите галочку с параметра Looping, иначе взрыв будет циклически повторяться. Цвет частиц поставьте RGB (172,172,255). Цвет для градиента RGB (136,151,255). Обратите внимание, что установлена гравитация.

Рис. 77.

Рис. 78. Рис. 79.

Создайте из системы частиц префаб, перенеся её в папку Prefabs и назовите Explosion_Water. Удалите со сцены копию. Теперь, всё, что нам осталось сделать, это отслеживать контакт ядра с водой, и в координатах расположения ядра создавать копию данного префаба. Эффект будет проигрываться менее двух секунд. На ядре у нас есть компонент коллайдер, который мы преобразовали в триггер, а вот на воде ничего подобного мы не сделали. Создадим пустой объект, на который добавим прямоугольный компонент коллайдер. Верхнюю грань этого прямоугольника мы совместим с поверхностью воды. Как только ядро войдёт в контакт с этим коллайдером, мы будем знать, что есть соприкосновение с водой. Назовите пустышку ___Surface_Water, добавьте на неё новый компонент, и все параметры установите, как на рисунке 80. Обратите внимание, на этот объект добавлен тег Water, который сначала нужно создать, как мы это делали для пушки.

Рис. 80.

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

Введите следующие переменные.

 

public GameObject explosion_water_abstrakt;

    private float time_existence = 15f;

private float time_counter = 0;

 

Первая переменная – это ссылка на наш префаб взрыва на воде. Следующие две переменные нужны для уничтожения нашего ядра в том случае, если ядро никуда не попало. Это очень важный момент. В любой игре, при создании таких объектов, как пули, снаряды и подобные им, не забывайте уничтожать их (обычно это происходит по счётчику времени), т.к. в противном случае все они будут существовать до окончания игры, и могут использовать огромную часть памяти компьютера. В нашем случае ядро должно так или иначе попасть в воду, если не поразит корабль пиратов, но тем не менее проверку мы будем делать, чтобы никогда не забывать об этом. Кстати, со всеми эффектами взрывов, дыма от пороховых газов, нам в дальнейшем предстоит сделать то же самое. В метод Update() добавьте следующий код.

 

time_counter += Time.deltaTime;

        if (time_counter >= time_existence)

        {

            Destroy(gameObject);

    }

Здесь всё просто. Мы отслеживаем время, по истечении которого удаляется объект, на котором находится данный скрипт, то есть пушечное ядро. Добавьте следующий метод.

 

private void OnTriggerEnter(Collider other)

    {

        if (other.gameObject.tag == "Water")

        {

            GameObject explosion = Instantiate(explosion_water_abstrakt) as GameObject;

            explosion.transform.position = this.transform.position;

            Destroy(gameObject);

        }

       

}

 

Это встроенный метод Юнити, который вызывается каждый раз, как только объект входит в контакт с другим объектом. Необходимые условия для вызова этого метода: на каждом из объектов должен присутствовать компонент коллайдер, и хотя бы один из них должен быть триггером. Так же хотя бы на один объект нужно наложить компонент Rigidbody. В качестве входного параметра этот метод принимает коллайдер того объекта, с которым наш объект вошёл во взаимодействие. Внутри метода мы проверяем, является ли тег объекта Water, и если да, то создаём копию взрыва на воде на основе нашего префаба. Далее располагаем его в месте нахождения пушечного ядра, и последней строчкой удаляем ядро. Скрип готов. Добавьте его на префаб нашего ядра, и в панели Инспектор, в пустое публичное поле переместите водный взрыв (рис. 81).

Рис. 81.

 

 

 

Запустите игру. Сейчас, при попадании ядра в воду, появляется всплеск (рис. 82).

Рис. 82.

При взрыве на воде было бы неплохо воспроизводить соответствующий звук. Мы это сделаем в скрипте, который впоследствии наложим на префаб всплеска воды. Создайте такой скрипт, и назовите его Explosion_water. Он достаточно простой. Вот всё его содержание.

 

using UnityEngine;

 

public class Explosion_water : MonoBehaviour

{

    public AudioClip clip;

    private float time_counter = 3f;

    void Start()

    {

        Play_Audio(clip);

    }

 

    void Update()

    {

        time_counter -= Time.deltaTime;

        if (time_counter <= 0)

        {

            Destroy(gameObject);

        }

    }

    private void Play_Audio(AudioClip clip)

    {

        GetComponent<AudioSource>().PlayOneShot(clip);

    }

}

Первая публичная переменнаяэто ссылка на звуковой файл всплеска воды. Вторая переменная – это время существования данного объекта – то есть взрыва на воде. В методе Update() прописан код для уничтожения взрыва через 3 секунды после его создания. Метод Play_Audio(AudioClip clip) просто проигрывает звуковой файл. Обратите внимание, что для проигрывания звука используется не переменная, которая ссылается на источник звука, как это делали для озвучки выстрела из пушки. Здесь мы обращаемся к компоненту источника звука AudioSource нашего префаба взрыва. Как вы понимаете, в данный момент, на нашем префабе такого компонента нет, и нам придётся в дальнейшем его создать. Метод для проигрывания звука помещаем в Start(), то есть он будет выполняться сразу после создания всплеска воды. Переходим в проект Юнити, открываем префаб водного взрыва, и на панели Инспектор добавим два компонента. Один из них – это только что созданный нами скрипт, а другой – источник звука. У компонента скрипт заполните публичное поле – туда нужно переместить соответствующий звуковой файл (рис. 83).

Рис. 83.

Запустите игру. Теперь при попадании ядра в воду вместе с появлением всплеска воды, воспроизводится и соответствующий звук. Чтобы далеко не отходить от темы, давайте также создадим скрипт для дыма от пороховых газов. Назовите его Smoke_cannon.

 

using UnityEngine;

 

public class Smoke_cannon : MonoBehaviour

{

    private float time_existence = 3f; //время существования

    private float time_counter = 0; //счетчик времени

 

    void Start()

    {

 

    }

 

    void Update()

    {

        time_counter += Time.deltaTime;

        if (time_counter >= time_existence)

        {

            Destroy(gameObject);

        }

    }

}

Скрипт нужен только для того, чтобы уничтожить эффект дыма после его визуализации. Добавьте этот скрипт на префаб  Smoke_Cannon. Теперь после выстрелов из пушки, эти эффекты у нас не будут накапливаться на сцене, а уничтожаться через 3 секунды после из создания.



Мощность выстрела пушки.

Сейчас наши пушки стреляют только на определённое расстояние. Мы сделаем так, чтобы игрок сам определял мощность выстрела. Для этого на экране поместим слайдер, уменьшая или увеличивая значение которого, соответственно будем менять силу выстрела.

В панели Иерархии на Canvas кликните правой кнопкой мыши и в появившемся меню выберите UI – Slider. Назовите его Slider_Force и параметры всех компонентов, а также его дочерних объектов установите, как на рисунках 84-88. У объекта Handle Slide Area удалите его дочерний объект.

 

 

Рис.84.

Рис.85.

Рис. 86.

Рис. 87.

Рис. 88.

Обратите внимание, напротив параметра Interactable установлена галочка. Это означает, что пользователь может воздействовать на этот элемент интерфейса. Если во время игры кликнуть мышью на слайдере, его значение будет меняться. Этого мы и добивались. Всё, что нам нужно сделать, это из программного кода узнавать значение слайдера перед выстрелом из пушки, и именно с такой силой запускать ядро.

В скрипте Class_common_variables введите новую переменную.

 

public Slider slider_force_cannon;

 

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

 

using UnityEngine.UI;

 

В методе Start() установим значение слайдера равным единице.

 

slider_force_cannon.value = 1f;

 

Добавим следующий метод.

 

public float Get_force_cannon()

    {

        return slider_force_cannon.value;

}

 

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

 

yadro_sozdannoe.GetComponent<Rigidbody>().AddRelativeForce(yadro_sozdannoe.transform.forward * empty_shared_variables.GetComponent<Class_common_variables>().Get_force_cannon(), ForceMode.Impulse);

 

Здесь в качестве прилаживаемой силы к ядру, мы используем значение нашего слайдера, полученного из скрипта Class_common_variables. В окне Иерархии выделите пустышку, на которой находится данный скрипт, и заполните пустое поле в окне Инспектор, перенеся в него элемент интерфейса из Канваса. Если сейчас запустить игру, то мы сможем изменять значение слайдера, и тем самым влиять на дальность выстрела из пушки (рис. 89).

Рис. 89.

Давайте сделаем так, чтобы мощность выстрела также можно было менять, используя клавиатуру. В скрипте Class_common_variables введите новую переменную.

 

private float rate_change_force = 0.5f;

 

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

 

private void Change_force()

    {

        if (Input.GetKey(KeyCode.Z))

        {

            slider_force_cannon.value -= rate_change_force * Time.deltaTime;

        }

        if (Input.GetKey(KeyCode.X))

        {

            slider_force_cannon.value += rate_change_force * Time.deltaTime;

        }

 

}

 

Этот метод устанавливает значение для слайдера в зависимости от нажатия игроком клавиш Z и X. В первом случае, при удержании клавиши, значение слайдера постепенно уменьшается, во втором – увеличивается. Поместите этот метод в Update(). Запустите игру. Теперь для изменения силы выстрела, можно использовать клавиатуру.



Пиратский корабль.

Мы научились стрелять из пушек, осталось только сделать противников. Конечно же, это будут пиратские корабли. Из окна Проект перенесите на сцену 3D модель корабля Ship_14_poly (рис. 90).

Рис. 90.

Компонент трансформ заполните так же, как на изображении. Можно видеть, что у этой модели так же есть проблемы с отображением паруса, он виден только с одной стороны. Перенесите на сцену 3D модель паруса sail_Pirates и установите для его компонента трансформ такие же значения, как и для корабля пиратов. Затем сделайте парус дочерним объектом для 3D модели корабля (рис. 91).

Рис. 91.

Вспоминаем, как мы создавали корабль для игрока, и делаем всё по аналогии. Нужно создать пустышку на панели Иерархии, и назвать её ___Ship_Pirat_1, которая и будет кораблём пиратов. Разместите её в координатах (294.976, 0, 49.997). Сделайте 3D модель корабля дочерним объектом для пустышки, а затем у пустого объекта установите координаты по Y равной -0.85, то есть спустим корабль на воду (рис. 92).

Рис. 92.

В папке Materials создайте новую папку и назовите её Pirate_Ship. Зайдите в папку с материалами для корабля игрока, выделите три материала – для внешней обшивки, для перилл и для паруса. Сделайте копию этих материалов, нажав комбинацию клавиш Ctrl+D. Созданные копии перенесите в папку для материалов кораблей пиратов. Материал для паруса сделайте синего цвета. Материл для обшивки корабля наложите на внешнюю часть корабля пиратов, материал перилл наложите на мачту, а материал паруса примените на лицевую и обратную стороны (рис. 93).

Рис. 93.

Для того, чтобы корабль пиратов мог контактировать с другими объектами – с землёй, ядрами пушки игрока, на него нужно добавить несколько компонентов коллайдера. Они должны приблизительно закрывать объем корабля. Старайтесь делать так, чтобы коллайдеры не перекрывали друг друга, иначе при попадании ядра в эту область, событие возникнет сразу в двух коллайдерах, и в игре будет засчитано сразу два поражения от снаряда (рис. 94). Все коллайдеры нужно сделать триггерами.

Рис. 94.

Нам необходимо добавить компонент Rigidbody для того, чтобы корабль пиратов мог взаимодействовать с землёй и невидимыми стенами, ограничивающими участок морского сражения. Параметры установите, как на рисунке 95.

Рис. 95.

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

Откройте префаб, дважды щёлкнув по нему в папке, и добавьте на корабль тег ship_p. Теперь посмотрим, как наше пушечное ядро сможет взаимодействовать с кораблём пиратов. Откройте скрипт, находящийся на ядре Cannonball_players.  В метод OnTriggerEnter(Collider other) добавьте следующее условие.

 

if (other.gameObject.tag == "ship_p")

        {

            Debug.Log("Попали в пиратский корабль");

    }

Здесь проверяется попадание в объект с тегом ship_p, и в консоль выводится соответствующее сообщение. Запустите игру, и попробуйте из пушки попасть по пиратскому кораблю. В случае удачи, в консоли вы увидите сообщение (рис. 96).

Рис. 96.

Всё отлично работает, осталось только заменить вывод надписи в консоли на конкретные действия – появление взрыва и нанесение урона пиратам.



Взрыв ядра.

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

 

Рис. 97.

Рис. 98.

Рис. 99.

Рис. 100.

Перенесите систему частиц в папку с префабами, и назовите Explosion. Копию со сцены удалите. Нам понадобится новый материал для второго эффекта частиц. Создайте его в папке Materials и назовите Fire. На панели Инспектор выберите ShaderMobilesParticlesAlpha Blended. В качестве изображения выберите fire-pictures (оно уже должно быть в виде спрайта). Материал готов.

Создайте второй эффект частиц. Он аналогичен первому, нужно только внести некоторые изменения (рис. 101).

Рис. 101.

 

Так же в структуре Renderer в качестве материала укажите Fire. Сделайте из этой системы частиц префаб, и назовите его Explosion_d. Удалите со сцены копию. Теперь создадим скрипт, который будет налаживаться на оба префаба взрыва. Его предназначение очень простое – воспроизводить звук при появлении взрыва, и уничтожать объект по истечении нескольких секунд. Скрипт назовите Explosion.

 

using UnityEngine;

 

public class Explosion : MonoBehaviour

{

    public AudioClip clip;

    private float time_counter = 3f;

    void Start()

    {

        Play_Audio(clip);

    }

 

    void Update()

    {

        time_counter -= Time.deltaTime;

        if (time_counter <= 0)

        {

            Destroy(gameObject);

        }

    }

    private void Play_Audio(AudioClip clip)

    {

        GetComponent<AudioSource>().PlayOneShot(clip);

    }

}

 

Добавьте этот скрипт на оба префаба с эффектами частиц и у каждого в скрипте заполните пустое поле для звукового сопровождения. Используйте файл sound of an explosion. Здесь есть небольшой нюанс. Так как оба эти префаба будут появляться одновременно, то и звук будет воспроизводится наложением один на другой. В принципе, это особо не заметно, но если вы хотите оставить только однократное воспроизведение, то можете создать второй скрипт, в котором не будет воспроизведения звука, а останется только уничтожение объекта через промежуток времени. Один скрипт наложите на один префаб взрыва, а второй скрипт, соответственно на другой.

Не забудьте добавить компонент источника звука на оба префаба взрыва (или на один, если вы выбрали вариант с созданием второго скрипта) в окне Инспектор. Этот компонент мы уже добавляли для эффекта всплеска на воде.

В скрип Cannonball_players добавьте следующие переменные, которые будет ссылаться на два префаба взрывов.

 

public GameObject explosion_abstrakt;

public GameObject explosion_dark_abstrakt;

 

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

 

if (other.gameObject.tag == "ship_p")

        {

            GameObject explosion = Instantiate(explosion_abstrakt) as GameObject;

            explosion.transform.position = this.transform.position;

            GameObject explosion_d = Instantiate(explosion_dark_abstrakt) as GameObject;

            explosion_d.transform.position = this.transform.position;

 

            Destroy(gameObject);

    }

 

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

Рис. 102.

Запустите игру и попробуйте попасть из пушки в корабль пиратов (рис. 103).

Рис. 103.



Стены.

Всё замечательно работает, и звук также воспроизводится. Сейчас нам нужно сделать невидимые стены, которые будут располагаться по периметру земельного участка. Они будут ограничивать перемещение кораблей. Создайте пустой объект, расположите его на левой границе земли, добавьте компонент прямоугольного коллайдера, и параметры установите, как на рисунке 104. Добавьте на стену тег wall.

Рис. 104.

Нужно добавить ещё три стены. Можете просто дублировать первую стену, перемещая её в нужное место и поворачивая. Вот что должно получиться (рис.105).

Рис. 105.

Осталось сделать видимую границу, чтобы игрок мог видеть, до какого места можно плыть на корабле. Создайте 3D объект куб на панели Иерархии, в окне Инспектор удалите у него компонент коллайдера (нужно правой кнопкой кликнуть на три точки справа от названия компонента). Параметры задайте как на рисунке 106.

Рис. 106.

Материал для границы можете сделать по своему желанию. В игре для него использовалось изображение border с повторением по оси X 50 раз. Граница должна располагаться на пересечении невидимой стены и воды. Скопируйте объект 3 раза, и перенесите в нужные координаты, чтобы весь наш участок был ограничен.

На нашем ландшафте пока нет компонента коллайдера, а он необходим для взаимодействия с кораблями. Добавьте Mesh Collider, который будет повторять форму 3D модели земли. Также создайте новый тег land и добавьте его на землю.

В окне Иерархии находится слишком много объектов. Давайте наведём там небольшой порядок. Создайте пустой объект, назовите его ___Objects_of_the_environment. Координаты и повороты на панели Инспектор установите в ноль. Перенесите на него все невидимые стены, границы, воду, пустышку поверхности воды, источники звука, источник света. Создайте ещё один пустой объект, назовите ___All_Ship_Pirate. Это будет родительский объект для всех кораблей пиратов, которые принимают участие в сражении на данном уровне. Таким же образом координаты и повороты установите в ноль. Перенесите на этот объект корабль пиратов (рис. 107).

Рис. 107.



Урон кораблей пиратов.

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

Чтобы не создавать слайдер заново, мы будем использовать уже имеющийся на Канвасе, только немного его модернизируем. На панели Иерархии выделите элемент интерфейса Slider_Force, и сделайте дубликат нажатием Ctrl+D. Все параметры установите, как на рисунке 108.

Рис. 108.

Не забудьте снять галочку интерактивности – игрок вручную никак не должен влиять на этот слайдер. Цвета подберите по своему вкусу. Перемещая ползунок Value, можно увидеть, как будет меняться полоска жизни в игре. Максимальное и минимальное значения для слайдера мы установим из скрипта, здесь можно оставить их без изменения.

Сделайте три копии с этого слайдера, и каждый разместите ниже на 25 единиц по оси Y (рис. 109).

Рис. 109.

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

В нашей игре будет 4 типа пиратских кораблей. Каждый из них будет иметь уникальный внешний вид, различное количество пушек, а также разное количество жизней, то есть количество попаданий для их потопления. Мы создадим отдельный скрипт, и присвоим его каждому из префабов кораблей. Но так как скрипт один и тот же на разных типов кораблей, то из скрипта придётся определять, какой именно корабль находится под управлением скрипта. На всех пиратских кораблях наложен один и тот же тег, поэтому, по тегу, мы не сможет идентифицировать корабль. Сделаем следующее: первому (с индексом 0) дочернему объекту каждого корабля присвоим уникальный тег от 1 до 4, и таким образом из скрипта нам будет легко определить, с каким типом пиратского корабля сейчас работаем.

Откройте префаб корабля пиратов, и первому дочернему объекту (это 3D модель) присвойте тег «1» (рис. 110).

Рис. 110.

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

 

using UnityEngine;

 

public class Common_to_pirate_ships : MonoBehaviour

{

    public int number_lives = 1;

    private void Awake()

    {

        switch (this.transform.GetChild(0).gameObject.tag)

        {

            case "1":

                {

                    number_lives = 3;

                    break;

                }

            case "2":

                {

                    number_lives = 5;

                    break;

                }

            case "3":

                {

                    number_lives = 7;

                    break;

                }

            case "4":

                {

                    number_lives = 9;

                    break;

                }

 

        }

    }

}

 

Сначала мы вводим публичную переменную, в которую будет записано количество жизней данного корабля. Инициализируем её с количеством, равным 1, затем поменяем это значение. Метод Awake() является встроенным в Юнити, и выполняется ещё до метода Start(). Сейчас как раз тот случай, когда это нужно. В этом методе мы смотрим на тег первого дочернего объекта, на котором лежит данный скрипт (то есть корабль), и в зависимости от него устанавливаем количество жизней для корабля.

Присвойте этот скрипт префабу корабля пиратов (рис. 111).

Рис. 111.

На панели Инспектор можно увидеть поле под скриптом для публичной переменной, но заполнять его не нужно. Оно будет регулироваться из кода.

На пиратских кораблях будет находится ещё один скрипт, который уже будет отвечать за поведение корабля в игре. Создайте новый скрипт и назовите его Pirate_ship. Вот его начальный вид.

 

using UnityEngine;

using UnityEngine.UI;

 

public class Pirate_ship : MonoBehaviour

{

    private GameObject slider_life;

    void Start()

    {

       

    }

 

    void Update()

    {

       

    }

    public void Set_slider_life(GameObject s)

    {

        slider_life = s;

        slider_life.transform.GetComponent<Slider>().minValue = 0;

        slider_life.transform.GetComponent<Slider>().maxValue = gameObject.GetComponent<Common_to_pirate_ships>().number_lives;

    }

}

Обратите внимание, что мы сразу добавили новую библиотеку, так как в скрипте нужно будет работать с элементами интерфейса игры. Далее определяем переменную, которой будет присвоен объект слайдер. Изменяя эту переменную, мы сможем менять линию жизни корабля на экране. В единственном методе, который будет вызываться из внешнего скрипта, мы присваиваем нашей переменной переданный в данный метод объект, конечно же, это будет слайдер. Следующими двумя строчками слайдеру присваивается максимальное и минимальное значение. Обратите внимание, что мы имеем дело с типом GameObject, поэтому нельзя обратиться к свойствам слайдера, а нужно сначала сослаться на компонент Slider этого объекта, и далее работать с его свойствами. Максимальное значение для слайдера – это количество жизней пиратского корабля. Для того, чтобы достать это число, мы обращаемся ко второму скрипту на объекте корабль, и непосредственно берём его из публичной переменной. Добавьте скрипт Pirate_ship на префаб пиратского корабля.

Итак, давайте определимся, что же мы имеем на данный момент. На пиратском корабле есть два скрипта. Один отвечает за начальную установку количества жизней этого корабля, а другой присваивает кораблю переданный в него слайдер, и устанавливает максимальное и минимальное значение для него. Всё, что нам осталось сделать – это из главного скрипта при старте игры передать каждому кораблю свой слайдер. Для этого нужно определить 4 публичные переменные для слайдеров, и ещё одну публичную переменную для пустого объекта, который хранит в себе все пиратские корабли. Такой объект у нас уже есть на сцене. Откройте скрипт Class_common_variables и внесите новые переменные.

 

public GameObject all_pirate_ships;

    public GameObject slider_1;

    public GameObject slider_2;

    public GameObject slider_3;

public GameObject slider_4;

Создадим метод, который присваивает слайдеры каждому кораблю пиратов.

 

private void Assigning_sliders_ships()

    {

        switch (all_pirate_ships.transform.childCount)

        {

            case 1:

                slider_1.SetActive(true);

                all_pirate_ships.transform.GetChild(0).transform.GetComponent<Pirate_ship>().Set_slider_life(slider_1);

                break;

            case 2:

                slider_1.SetActive(true);

                all_pirate_ships.transform.GetChild(0).transform.GetComponent<Pirate_ship>().Set_slider_life(slider_1);

                slider_2.SetActive(true);

                all_pirate_ships.transform.GetChild(1).transform.GetComponent<Pirate_ship>().Set_slider_life(slider_2);

                break;

            case 3:

                slider_1.SetActive(true);

                all_pirate_ships.transform.GetChild(0).transform.GetComponent<Pirate_ship>().Set_slider_life(slider_1);

                slider_2.SetActive(true);

                all_pirate_ships.transform.GetChild(1).transform.GetComponent<Pirate_ship>().Set_slider_life(slider_2);

                slider_3.SetActive(true);

                all_pirate_ships.transform.GetChild(2).transform.GetComponent<Pirate_ship>().Set_slider_life(slider_3);

                break;

            case 4:

                slider_1.SetActive(true);

                all_pirate_ships.transform.GetChild(0).transform.GetComponent<Pirate_ship>().Set_slider_life(slider_1);

                slider_2.SetActive(true);

                all_pirate_ships.transform.GetChild(1).transform.GetComponent<Pirate_ship>().Set_slider_life(slider_2);

                slider_3.SetActive(true);

                all_pirate_ships.transform.GetChild(2).transform.GetComponent<Pirate_ship>().Set_slider_life(slider_3);

                slider_4.SetActive(true);

                all_pirate_ships.transform.GetChild(3).transform.GetComponent<Pirate_ship>().Set_slider_life(slider_4);

                break;

        }

}

 

В вначале метода мы узнаём количество пиратских кораблей на данном уровне, оно равно количеству дочерних объектов у нашей пустышки, содержащей все корабли. Далее, в зависимости от этого значения, мы присваиваем нужное количество слайдеров всем кораблям и делаем их активными. Обращение к каждому пиратскому кораблю очень простое – по индексу дочернего объекта. Здесь нам не нужно знать с каким типом корабля мы работам, просто нужно передать слайдер, а уже сам корабль в скрипте разберётся, какое максимальное значение жизней ему присвоить. Как вы помните, мы уже это прописали в скрипте.

Откройте проект Юнити, кликните на объекте ___empty_shared_variables, и в панели Инспектор заполните пустые поля под скриптом (рис. 112).

Рис. 112.

В скрипт Pirate_ship нужно добавить метод для обновления слайдера в каждом кадре.

 

private void Update_slider()

    {

        slider_life.transform.GetComponent<Slider>().value = gameObject.GetComponent<Common_to_pirate_ships>().number_lives;

}

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

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

 

if (other.gameObject.tag == "ship_p")

        {

            other.gameObject.GetComponent<Common_to_pirate_ships>().number_lives -= 1;

            GameObject explosion = Instantiate(explosion_abstrakt) as GameObject;

            explosion.transform.position = this.transform.position;

            GameObject explosion_d = Instantiate(explosion_dark_abstrakt) as GameObject;

            explosion_d.transform.position = this.transform.position;

 

            Destroy(gameObject);

    }

 

Да, всё верно. Это не самый лучший способ менять переменную из другого скрипта. Лучше конечно же создать метод, который будет вызываться, и менять переменную. Но это мы уже делали неоднократно, так что давайте применим и такой способ.

Итак, в скрипте Cannonball_players при попадании ядра в пиратский корабль, мы обращаемся к скрипту Common_to_pirate_ships на корабле, и уменьшаем количество жизней number_lives. Далее в скрипте Players_ship в каждом кадре обращаемся опять-таки к скрипту Common_to_pirate_ships и узнаём текущее количество жизней, которое благополучно отображаем на полоске жизни.

Запустите игру. Сейчас при попадании ядра в корабль, полоска жизни уменьшается (рис. 113). Нам осталось реализовать действия, которые будут происходить при заканчивании жизней у пиратского корабля. Конечно же, это будет его потопление.

Рис. 113.



Топим Корабль пиратов.

Откройте скрипт Players_ship. Сейчас мы внесём в немного дополнительного код, а потом всё по порядку выясним, что к чему. Итак, на данный момент скрипт должен выглядеть следующим образом.

 

using UnityEngine;

using UnityEngine.UI;

 

public class Pirate_ship : MonoBehaviour

{

    private GameObject slider_life;

    private float sinking_speed_Y = 0.05f;

    private float sinking_speed_rotation_Z = 10f;

    private bool ship_sinking = false;

    private float sinking_depth = -1.5f;

    void Start()

    {

       

    }

 

    void Update()

    {

        if (gameObject.GetComponent<Common_to_pirate_ships>().number_lives <= 0)

        {

            Update_slider();

            Ship_sinking();

            ship_sinking = true;

            return;

        }

        Update_slider();

    }

    public void Set_slider_life(GameObject s)

    {

        slider_life = s;

        slider_life.transform.GetComponent<Slider>().minValue = 0;

        slider_life.transform.GetComponent<Slider>().maxValue = gameObject.GetComponent<Common_to_pirate_ships>().number_lives;

    }

    private void Update_slider()

    {

        slider_life.transform.GetComponent<Slider>().value = gameObject.GetComponent<Common_to_pirate_ships>().number_lives;

    }

    private void Ship_sinking()

    {

        this.transform.Translate(0, -sinking_speed_Y * Time.deltaTime, 0, Space.World);

        this.transform.Rotate(0, 0, -sinking_speed_rotation_Z * Time.deltaTime, Space.Self);

        if (transform.position.y <= sinking_depth)

        {

            Destroy(slider_life);

            Destroy(gameObject);

        }

    }

}

 

Переменная sinking_speed_Y отвечает за вертикальную скорость потопления корабля. sinking_speed_rotation_Z определяет скорость поворота корабля при потоплении. Переменная ship_sinking устанавливается в истину, как только корабль пиратов начинает тонуть, она нам понадобится в дальнейшем. Переменная sinking_depth определяет максимальную глубину потопления корабля пиратов, при достижении которой он удаляется.

В методе Update проверяем, не закончились ли у корабля жизни, и если да, то в каждом кадре выполняется метод Ship_sinking(). Он отвечает за потопление. Его задача перемещать корабль вниз по оси Y, и при этом поворачивать по оси Z с заданными скоростями. Как только корабль достигнет необходимой отметки, он удаляется со сцены, а также удаляется слайдер жизни корабля. Обратите внимание, в Update(), в конце условия мы поставили return. Это означает выход из Update(), то есть весь дальнейший код (а его там будет достаточно) при потоплении пиратского корабля выполняться не будет. Метод для обновления слайдера мы включили в это условие на тот случай, если после последнего попадания полоска жизни не успеет обновиться, и корабль сразу начнёт тонуть. Запустите игру. Вам доставит огромное удовольствие потопить пиратский корабль (рис. 114).

Рис. 114.

Так как в нашей игре максимальное количество кораблей противника равно 4, то сделайте ещё три копии пиратского корабля, разместите их, где вам угодно (не перемещайте по оси Y). Подплывая к каждому из них (или находясь на месте), попробуйте уничтожить корабли. Самое главное, все они должны быть дочерними объектами пустышки ___All_Ship_Pirate (рис. 115).

Рис. 115.

Удалите сделанные копии со сцены, оставьте один пиратский корабль.



Движение корабля пиратов.

Корабли не должны стоять на одном месте. Они также, как и корабль игрока будут перемещаться по воде в пределах того участка, который мы ограничили невидимыми стенами. Сейчас нам с вами нужно будет прописать алгоритм поведения пиратов. Изначально при старте игры все они будут двигаться вперёд до тех пор, пока не войдут во взаимодействие с каким-либо препятствием. Препятствиями являются невидимые стены, другие корабли пиратов, земля и корабль игрока. После этого корабль начинает идти задним ходом заданное нами количество времени (10 секунд). По истечении этого времени вычисляется случайный угол поворота, на который должен повернуться корабль, и начинается поворот корабля. По окончании поворота движение продолжается по направлению вперёд. Если взаимодействие с каким-либо объектом произошло при движении назад, то включается поворот на случайный угол, если же при повороте, то включается движение вперёд. Для реализации этого алгоритма введём в скрипт Pirate_ship новые переменные.

 

private float speed_movement_ship = 1.2f;

    private bool moving_forward = true;

    private bool moving_back = false;

    private bool rotation_random_angle = false;

    private float random_angle;

    private float time_movement_back = 10f;

    private float time_counter_backward_movement = 0;

    private float counter_random_angle_rotation = 0;

private float delta_random_angle_rotation = 15f;

 

Первая переменная – это скорость перемещения вперёд. Вторая устанавливается в истину при движении корабля пиратов вперёд, третья переменная устанавливается в истину при движении назад. Четвертая переменная устанавливается в истину при повороте корабля на случайный угол. random_angle – это вычисленный случайный угол для поворота. Следующая переменная – это время движения корабля назад. time_counter_backward_movement – счётчик времени движения назад. counter_random_angle_rotation – угол, на который уже повернулся корабль, он постоянно увеличивается, пока не станет равным random_angle. delta_random_angle_rotation – скорость изменения угла поворота с течением времени.

Для начала напишем метод для перемещения корабля вперёд.

 

private void Moving_forward()

    {

        this.transform.Translate(0, 0, speed_movement_ship * Time.deltaTime);

}

 

Всё предельно просто – перемещаем объект по оси Z с заданной скоростью. Далее метод для перемещения корабля назад.

 

private void Moving_back()

    {

        this.transform.Translate(0, 0, -speed_movement_ship * Time.deltaTime);

        time_counter_backward_movement += Time.deltaTime;

        if (time_counter_backward_movement >= time_movement_back)

        {

            time_counter_backward_movement = 0;

            moving_forward = false;

            rotation_random_angle = true;

            moving_back = false;

            Calculating_random_angle();

        }

    }

 

Первой строчкой перемещаем корабль по оси Z в обратном направлении с заданной скоростью. Затем увеличиваем счётчик времени, и проверяем, не достиг ли он полного времени перемещения назад, и если да, то сбрасываем счётчик в ноль, для следующего перемещения назад, далее устанавливаем режим перемещения вперёд и назад в ложь, а режим поворота в истину. Последней строчкой запускаем метод для вычисления случайного угла поворота. Он вызовет в скрипте ошибку, так как мы его ещё не создали. Давайте сделаем это.

 

private void Calculating_random_angle()

    {

        random_angle = Random.Range(50, 160);

        int i = Random.Range(0, 2);

        if (i == 0)

        {

            random_angle *= -1;

 

        }

    }

 

Вычисляем случайный угол между 50 и 160 градусами. Далее вычисляем случайную переменную i, в зависимости от которой делаем угол или положительным или отрицательным. Нельзя сразу было вычислить случайный угол между -160 и 160 градусов, т.к. в таком случае могут попадаться маленькие углы до 50 градусов, а нам поворот на такой угол не нужен.

Далее создадим метод для поворота корабля на вычисленный случайный угол.

 

private void Rotation_random_angle()

    {

        if (random_angle >= 0)

        {

            this.transform.Rotate(0, delta_random_angle_rotation * Time.deltaTime, 0);

            counter_random_angle_rotation += delta_random_angle_rotation * Time.deltaTime;

            if (counter_random_angle_rotation >= random_angle)

            {

                counter_random_angle_rotation = 0;

                rotation_random_angle = false;

                moving_forward = true;

                moving_back = false;

            }

        }

        if (random_angle < 0)

        {

            this.transform.Rotate(0, -delta_random_angle_rotation * Time.deltaTime, 0);

            counter_random_angle_rotation -= delta_random_angle_rotation * Time.deltaTime;

            if (counter_random_angle_rotation <= random_angle)

            {

                counter_random_angle_rotation = 0;

                rotation_random_angle = false;

                moving_forward = true;

                moving_back = false;

            }

        }

   }

 

Сначала смотрим, если угол положительный, то следующей строчкой поворачиваем корабль с заданной скоростью. Дальше изменяем текущий угол на эту же величину поворота, чтобы нам знать, на сколько уже повернулся корабль. Следующим условием проверяем, не достиг ли текущий угол поворота рассчитанного случайного угла поворота, и если да, то совершаются действия по остановке поворота. Устанавливаем текущий угол поворота в 0, для следующего поворота. Следующими тремя строчками меняем режим поведения корабля – поворот и движение назад ставим в ложь, а движение вперёд устанавливаем в истину.

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

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

 

private void OnTriggerEnter(Collider other)

    {

        if (other.transform.tag == "land" | other.transform.tag == "wall" | other.transform.tag == "ship_p" | other.transform.tag == "players_ship")

        {

 

            if (rotation_random_angle)

            {

 

                moving_back = false;

                rotation_random_angle = false;

                moving_forward = true;

                counter_random_angle_rotation = 0;

 

                return;

            }

            if (moving_back)

            {

 

                moving_back = false;

                rotation_random_angle = true;

                moving_forward = false;

                Calculating_random_angle();

 

                return;

            }

            if (moving_forward)

            {

                moving_back = true;

                rotation_random_angle = false;

                moving_forward = false;

                return;

            }

 

        }

}

 

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

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

Кстати, у нас до сих пор на корабле игрока нет тега. Добавьте тег players_ship (рис. 116).

Рис. 116.

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

 

if (moving_forward)

        {

            Moving_forward();

        }

        if (moving_back)

        {

            Moving_back();

        }

        if (rotation_random_angle)

        {

            Rotation_random_angle();

    }

 

Как говорится, нет ничего проще. Если включен режим перемещения вперёд, то выполняем соответствующий метод, если режим перемещения назад или режим поворота, то запускаем движение корабля назад или выполняем поворот.

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

 

using UnityEngine;

using UnityEngine.UI;

 

public class Pirate_ship : MonoBehaviour

{

    private float speed_movement_ship = 1.2f;

    private bool moving_forward = true;

    private bool moving_back = false;

    private bool rotation_random_angle = false;

    private float random_angle;

    private float time_movement_back = 10f;

    private float time_counter_backward_movement = 0;

    private float counter_random_angle_rotation = 0;

    private float delta_random_angle_rotation = 15f;

    private GameObject slider_life;

    private float sinking_speed_Y = 0.05f;

    private float sinking_speed_rotation_Z = 10f;

    private bool ship_sinking = false;

    private float sinking_depth = -1.5f;

    void Start()

    {

       

    }

 

    void Update()

    {

        if (gameObject.GetComponent<Common_to_pirate_ships>().number_lives <= 0)

        {

            Update_slider();

            Ship_sinking();

            ship_sinking = true;

            return;

        }

        if (moving_forward)

        {

            Moving_forward();

        }

        if (moving_back)

        {

            Moving_back();

        }

        if (rotation_random_angle)

        {

            Rotation_random_angle();

        }

        Update_slider();

    }

    public void Set_slider_life(GameObject s)

    {

        slider_life = s;

        slider_life.transform.GetComponent<Slider>().minValue = 0;

        slider_life.transform.GetComponent<Slider>().maxValue = gameObject.GetComponent<Common_to_pirate_ships>().number_lives;

    }

    private void Update_slider()

    {

        slider_life.transform.GetComponent<Slider>().value = gameObject.GetComponent<Common_to_pirate_ships>().number_lives;

    }

    private void Ship_sinking()

    {

        this.transform.Translate(0, -sinking_speed_Y * Time.deltaTime, 0, Space.World);

        this.transform.Rotate(0, 0, -sinking_speed_rotation_Z * Time.deltaTime, Space.Self);

        if (transform.position.y <= sinking_depth)

        {

            Destroy(slider_life);

            Destroy(gameObject);

        }

    }

    private void OnTriggerEnter(Collider other)

    {

        if (other.transform.tag == "land" | other.transform.tag == "wall" | other.transform.tag == "ship_p" | other.transform.tag == "players_ship")

        {

 

            if (rotation_random_angle)

            {

 

                moving_back = false;

                rotation_random_angle = false;

                moving_forward = true;

                counter_random_angle_rotation = 0;

 

                return;

            }

            if (moving_back)

            {

 

                moving_back = false;

                rotation_random_angle = true;

                moving_forward = false;

                Calculating_random_angle();

 

                return;

            }

            if (moving_forward)

            {

                moving_back = true;

                rotation_random_angle = false;

                moving_forward = false;

                return;

            }

 

        }

    }

    private void Moving_forward()

    {

        this.transform.Translate(0, 0, speed_movement_ship * Time.deltaTime);

    }

    private void Moving_back()

    {

        this.transform.Translate(0, 0, -speed_movement_ship * Time.deltaTime);

        time_counter_backward_movement += Time.deltaTime;

        if (time_counter_backward_movement >= time_movement_back)

        {

            time_counter_backward_movement = 0;

            moving_forward = false;

            rotation_random_angle = true;

            moving_back = false;

            Calculating_random_angle();

        }

    }

    private void Rotation_random_angle()

    {

        if (random_angle >= 0)

        {

            this.transform.Rotate(0, delta_random_angle_rotation * Time.deltaTime, 0);

            counter_random_angle_rotation += delta_random_angle_rotation * Time.deltaTime;

            if (counter_random_angle_rotation >= random_angle)

            {

                counter_random_angle_rotation = 0;

                rotation_random_angle = false;

                moving_forward = true;

                moving_back = false;

            }

        }

        if (random_angle < 0)

        {

            this.transform.Rotate(0, -delta_random_angle_rotation * Time.deltaTime, 0);

            counter_random_angle_rotation -= delta_random_angle_rotation * Time.deltaTime;

            if (counter_random_angle_rotation <= random_angle)

            {

                counter_random_angle_rotation = 0;

                rotation_random_angle = false;

                moving_forward = true;

                moving_back = false;

            }

        }

    }

    private void Calculating_random_angle()

    {

        random_angle = Random.Range(50, 160);

        int i = Random.Range(0, 2);

        if (i == 0)

        {

            random_angle *= -1;

 

        }

    }

 }

 

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

Рис. 117.

Запустите игру, и сразу переключитесь в окно сцены. Корабль подплывёт к земле, затем даст задний ход и через 10 секунд начнёт поворот на случайный угол. После поворота корабль поплывёт вперёд (рис. 118).

Рис. 118.

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



Пиратские пушки.

Настало время сделать пушку для кораблей супостатов. Перенесите на сцену 3D модель Cannon_init. И расположите её вдоль мировой оси Z (рис. 119).

Рис. 119.

Создайте пустой объект, назовите его ___Cannon_Pirate и расположите в центре 3D модели пушки. На модель наложите материал, который на пушке у игрока, и сделайте её дочерним объектом пустышки (рис. 120).

Рис. 120.

Создайте ещё один пустой объект, назовите ___direction_Ray, сделайте его первым дочерними объектом пушки. Выставьте все локальные координаты в 0, а затем переместите вниз по оси Y на 0.39 (рис. 121).

Рис. 121.

Это вспомогательный объект, мы его будем использовать в скрипте. Так же на пушку нужно добавить новый компонент – Audio Source, он нам понадобится для воспроизведения звука выстрела. На данном этапе пушка готова. Сделайте из неё префаб, перенеся в соответствующую папку и удалите копию со сцены.

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

Задачу по поиску корабля игрока мы возложим именно на дочернюю пустышку. В каждом карде она будет отслеживать пространство впереди пушки от 15 градусов слева до 15 градусов справа. Вот как это будет происходить. Сначала поворачиваем пустышку на 15 градусов по оси Y влево (на самом деле это угол 345 градусов) и запускаем из неё луч вдаль по оси Z для поиска корабля игрока. Далее поворачиваем пустышку вправо на 1 градус и опять запускаем луч. Так в одном кадре проделываем это 30 раз, пока пустышка не повернётся на 30 градусов. Если обнаружен корабль, то пушке, то есть родительскому объекту, передаётся информация, что мишень находится в видимости, а также передаётся по ссылке сам корабль игрока, чтобы пушка смогла вычислить его координаты. Если же при повороте на все 30 градусов корабль не был обнаружен, по пушке передаётся сообщение, что корабль не найден. Весь этот цикл повторяется каждый кадр. На практике это будет выглядеть так: корабль входит в поле видимости какой-то пушки, и пока он там будет находится, всё это время от пустышки будет посылаться сообщение, что цель найдена, и на пушке будет установлена соответствующая переменная в истину. Как только корабль выходит из видимости пушки, эта переменная ставится в ложь. Так же для разрешения стрельбы будет установлен счётчик времени, только по истечении которого пушка может вести огонь. То есть должны быть выполнены эти два условия – и время для стрельбы подошло, и корабль игрока обнаружен.

Для начала создадим скрипт, который будет на пустом объекте. Назовите его Direction_cannon_ray. Вот его полный код.

 

using UnityEngine;

 

public class Direction_cannon_ray : MonoBehaviour

{

    private float initial_angle = 345f;

    private int delta = 1;

    private int total_degrees = 30;

    public LayerMask players_ship;

    void Start()

    {

        transform.localRotation = Quaternion.Euler(0f, initial_angle, 0f);

    }

 

    void Update()

    {

        Rotating_empty_object_many_times();

    }

    private void Rotating_empty_object_many_times()

    {

        for (int i = 0; i <= total_degrees; i++)

        {

            RaycastHit hit;

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

            if (Physics.Raycast(ray, out hit, 300f, players_ship))

            {

                transform.parent.transform.GetComponent<Cannon_Pirate>().Set_players_ship_is_in_sight(true, hit.collider.gameObject);

                transform.localRotation = Quaternion.Euler(0f, initial_angle, 0f);

                return;

            }

            transform.Rotate(0, delta, 0, Space.Self);

        }

        transform.localRotation = Quaternion.Euler(0f, initial_angle, 0f);

        transform.parent.transform.GetComponent<Cannon_Pirate>().Set_players_ship_is_in_sight(false, null);

    }

}

У вас вызовет ошибку обращение к скрипту Cannon_Pirate. Этот скрипт создадим немного позже, он будет находится на самой пушке.

Переменная initial_angle задаёт начальный локальный угол поворота пустышки в каждом кадре. Следующая переменная определяет, на сколько градусов будем изменять угол поворота. За ней переменная указывает общее количество градусов, на которое должна повернуться пустышка. Публичная переменная public LayerMask players_ship ссылается на слой, по которому будет луч искать наш корабль. А вот это очень важно, так как корабль игрока до сих пор не имеет такого слоя. Давайте сделаем это промо сейчас (рис. 122).

Рис. 122.

При назначении слоя кораблю, Юнити спросит, присваивать ли его и на дочерние объекты? Ответьте, что нет, только на родительский.

Возвращаемся к нашему скрипту. В методе Start() мы поворачиваем пустышку на начальный угол. Затем в Update() в каждом кадре выполняется метод по поиску корабля игрока Rotating_empty_object_many_times(). Давайте в нём разбираться. Внутри метода запускается цикл, который повторяется 30 раз. Сначала создаётся переменная hit, которая хранит данные о взаимодействии луча с объектом поиска. Далее создаётся луч ray. При создании указывается начало луча – это координаты самой пустышки, и направление – это локальная ось Z. Метод Physics.Raycast запускает луч на 300 единиц вперёд и с поиском объекта по маске, которая указана в публичной переменной players_ship. Сам метод должен вернуть значение истины, если он нашёл нужный объект, или ложь, в противном случае. Поэтому метод помещён в условие, и если оно выполняется, то есть луч нашёл корабль, то выполняются следующие действия. На родительском объекте, он же пушка, запускается метод, в который передаётся значение истины и сам корабль игрока. Затем поворачиваем пустышку на начальный угол и выходим из метода, так как дальнейший поворот пустышки в этом кадре не имеет смысла – цель уже найдена. Если же условие не выполняется, то есть корабль не найден, то осуществляется поворот на 1 градус и цикл продолжается. По завершении цикла пустышка поворачивается на начальный угол для следующего кадра, и в родительский объект передаётся информация, что корабль не найден, и вместо корабля передаётся значение null. Пока этот скрипт нельзя поместить на нужный объект, т.к. в нём имеется ошибка – нет скрипта, на который он ссылается. Давайте его создадим.

Назовите новый скрипт Cannon_Pirate. На данном этапе вот всё его содержимое.

 

using UnityEngine;

 

public class Cannon_Pirate : MonoBehaviour

{

    private bool players_ship_is_in_sight = false;

    public GameObject target_abstract;

    private GameObject target;

    void Start()

    {

        target = Instantiate(target_abstract) as GameObject;

    }

 

    void Update()

    {

       

    }

    public void Set_players_ship_is_in_sight(bool v, GameObject korabl)

    {

        players_ship_is_in_sight = v;

        if (korabl != null)

        {

            target.transform.position = korabl.transform.position;

        }

        else target.transform.position = new Vector3(0, 0, 0);

 

    }

}

 

Первая переменная показывает, находится ли корабль игрока в видимости пушки. Следующая публичная переменная ссылается на префаб пустого объекта, на основе которого мы будем делать копию этой пустышки, она нам понадобится для стрельбы по кораблю. Она не имеет ничего общего с пустышкой, которую мы используем для поиска корабля. Третья переменная – это объект, которая будет создан на основе префаба пустого объекта в методе Start(). Set_players_ship_is_in_sight(bool v, GameObject korabl) это как раз тот метод, который мы вызывали из дочернего пустого объекта для передачи информации по результату поиска корабля игрока. В нём устанавливается переменная players_ship_is_in_sight в ложь или истину, в зависимости от результатов поиска, а также если передана ссылка на корабль, а не null, то пустышка, на которую ссылается переменная target помещается в те же координаты, что и корабль игрока.

Откройте скрипт Direction_cannon_ray. Ошибки должны исчезнуть. Откройте префаб пушки пиратов, и на дочерний объект ___direction_Ray добавьте данный скрипт. На панели Инспектор заполните публичное поле маской слоя (рис. 123).

Рис. 123.

На саму пушку добавьте скрипт Cannon_Pirate. На панели инспектор в публичное поле скрипта перенесите префаб пустого объекта (рис. 124).

Рис. 124.

 



Стрельба из пиратских пушек.

Сейчас создадим метод по созданию и запуску ядра из пушки, конечно же, он должен находится в скрипте Cannon_Pirate. Создать само ядро и запустить с определённой силой, это не сложная задача. Мы уже проделывали её при запуске ядра из пушки игрока. Там мы точно знали силу, которую нужно приложить к ядру, она определялась с помощью слайдера, а также ориентация ядра в пространстве была известна – она совпадала с направлением ствола пушки. С ядром пиратов не так всё просто. Во-первых, все пушки пиратов должны располагаться строго горизонтально в мировых координатах, так как при поиске корабля игрока пускается луч от дочерней пустышки, и если этот луч уйдет вверх или вниз, то корабль не будет найден. Поэтому вертикальный угол для ядра будем задавать отличным от угла поворота пушки. То же касается горизонтального угла поворота ядра. Пираты не должны стрелять строго по направлению к кораблю игрока. Мы сделаем так, чтобы ядра летели иногда немного левее корабля, иногда немного правее, а иногда и по направлению к кораблю. Поэтому угол будет вычисляться случайным образом. То же касается силы выстрела. Нужно чтобы ядра иногда не долетали до корабля, иногда перелетали его, а иногда попадали точно в цель. Никто из игроков не хочет, чтобы боты уничтожали его с первого выстрела. Давайте сначала создадим переменные, необходимые для этого метода.

 

public GameObject cannonball_abstract;

    public GameObject smoke_from_cannon_abstract;

    public AudioClip sound_cannon_shot;

    private float vertical_angle_cannonball = 15f;

    private float horizontal_angle_cannonball = 10f;

float dist;

 

Первая публичная переменная – это ссылка на префаб ядра пиратов. Нам его ещё предстоит создать. Вторая переменная – это ссылка на префаб дыма от пороховых газов. Мы не будем использовать дым от пушки игрока, так как по размеру он слишком мал, чтобы хорошо видеть его из далека. Третья переменная ссылается на звук выстрела из пушки пиратов. Следующая переменная показывает, на сколько градусов будет повёрнуто ядро по локальной оси X (это и есть вертикальный угол). Переменная horizontal_angle_cannonball определяет горизонтальный угол ядра (это поворот по оси Y). Последняя переменная – это дистанция до корабля игрока.

Теперь запишем сам метод.

 

    private void Creating_cannonball_and_firing()

    {

        dist = Vector3.Distance(target.transform.position, transform.position);

        float force;

        if (dist < 10f)

        {

            horizontal_angle_cannonball = 10f;

            force = Random.Range(12f, 15f);

        }

        else if (dist >= 10 & dist < 20)

        {

            horizontal_angle_cannonball = 10f;

            force = Random.Range(10f, 23f);

        }

        else if (dist >= 20 & dist < 30)

        {

            horizontal_angle_cannonball = 10f;

            force = Random.Range(15f, 27f);

        }

        else if (dist >= 30 & dist < 45)

        {

            horizontal_angle_cannonball = 10f;

            force = Random.Range(20f, 32f);

        }

 

        else if (dist >= 45 & dist < 60)

        {

            horizontal_angle_cannonball = 10f;

            force = Random.Range(29f, 37f);

        }

        else if (dist >= 60 & dist < 80)

        {

            horizontal_angle_cannonball = 10f;

            force = Random.Range(33f, 41);

        }

        else if (dist >= 80 & dist < 110)

        {

            horizontal_angle_cannonball = 8f;

            force = Random.Range(40f, 48f);

        }

        else if (dist >= 110 & dist < 150)

        {

            horizontal_angle_cannonball = 7f;

            force = Random.Range(47f, 56f);

        }

        else if (dist >= 150 & dist < 180)

        {

            horizontal_angle_cannonball = 7f;

            force = Random.Range(55f, 62f);

        }

        else if (dist >= 180 & dist < 220)

        {

            horizontal_angle_cannonball = 7f;

            force = Random.Range(60f, 68f);

        }

        else if (dist >= 220 & dist < 260)

        {

            horizontal_angle_cannonball = 6f;

            force = Random.Range(65f, 74f);

        }

        else

        {

            force = 0;

            return;

        }

 

 

        GameObject cannonball = Instantiate(cannonball_abstract) as GameObject;

        GameObject smoke = Instantiate(smoke_from_cannon_abstract) as GameObject;

        cannonball.transform.position = this.transform.position;

        cannonball.transform.rotation = this.transform.rotation;

        cannonball.transform.Translate(0, 0, 0.25f, Space.Self);

        Vector3 napravlenie_Yadra = target.transform.position - cannonball.transform.position;

        cannonball.transform.rotation = Quaternion.LookRotation(napravlenie_Yadra, Vector3.up);

        float ugol_horizont = Random.Range(-horizontal_angle_cannonball, horizontal_angle_cannonball);

        cannonball.transform.Rotate(0, ugol_horizont, 0, Space.Self);

        cannonball.transform.Rotate(-vertical_angle_cannonball, 0, 0, Space.Self);

        smoke.transform.position = cannonball.transform.position;

        cannonball.GetComponent<Rigidbody>().AddRelativeForce(cannonball.transform.forward * force, ForceMode.Impulse);

        transform.GetComponent<AudioSource>().PlayOneShot(sound_cannon_shot);

}

 

Для начала вычисляем расстояние до корабля игрока, как вы помните, пустышку target мы поместили в его координаты. Определяем переменную force, которую в дальнейшем будем вычислять. Дальше идёт множество условий, которые в зависимости от дистанции до корабля случайным образом задают горизонтальный угол для поворота ядра и силу для его запуска. Все эти значения подобраны опытным путём так, чтобы ядро летело примерно в сторону корабля. Вы всегда можете поэкспериментировать, и установить свои значения. Далее осталось только создать ядро и дым на основе префабов. Ядро размещаем в координаты пушки и поворачиваем таким же образом. Далее смещаем ядро на небольшое расстояние, чтобы оно находилось на выходе из ствола. Переменная napravlenie_Yadra указывает направление от ядра до корабля игрока. Поворачиваем ядро по этому направлению. Далее вычисляем горизонтальный угол ядра в указанных пределах. Следующими двумя строчками поворачиваем ядро сначала по локальной оси Y, затем по локальной оси X. Затем созданный ранее дым помещаем в координаты ядра. Наконец выстреливаем ядро с вычисленной силой и проигрываем звуковой файл выстрела с помощью компонента Audio Source, который мы уже разместили на префабе пушки пиратов.

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

 

private float time_interval_for_the_shot = 8f;

    private float time_counter_for_the_shot = 0;

    private float time1 = 5f;

private float time2 = 12f;

 

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

Теперь создадим сам метод.

 

private void Shot_from_cannon_after_period_time()

    {

        time_counter_for_the_shot += Time.deltaTime;

        if (time_counter_for_the_shot >= time_interval_for_the_shot & players_ship_is_in_sight)

        {

 

            Creating_cannonball_and_firing();

            time_interval_for_the_shot = Random.Range(time1, time2);

            time_counter_for_the_shot = 0;

        }

 

}

 

Сначала увеличиваем счётчик времени, а затем проверяем, не достиг ли он нужного значения. Также вводится второе условие – находится ли корабль игрока в видимости пушки. Если оба условия выполняются, то вызываем метод создания и запуска ядра. Затем высчитываем новый интервал времени (будет плохо выглядеть, если все пушки будут стрелять синхронно через равный промежуток времени). Последней строчкой сбрасываем счётчик времени для следующего выстрела. Поместите этот метод в Update().



Ядро для пушки пиратов.

Почти всё готово для стрельбы пиратов. Осталось только создать ядро для их пушки и дым от пороховых газов. Переходим в проект Юнити.

В окне Проект выделите префаб ядра игрока (не открывайте его). Сделайте копию нажатием Ctrl+D. Назовите новый префаб Cannonballs_pirate. Установите параметры как на рисунке 125.

Рис. 125.

Нужно будет поменять масштаб, радиус коллайдера, массу и удалить компонент скрипта. Также откройте дочерний объект системы частиц и масштаб измените с 0.08 на 0.12. Для этого ядра создадим новый скрипт и назовём его Cannonball_Pirates. Он очень похож на скрипт ядра игрока.

 

using UnityEngine;

 

public class Cannonball_Pirates : MonoBehaviour

{

    public GameObject explosion_abstrakt;

    public GameObject explosion_dark_abstrakt;

    public GameObject explosion_water_abstrakt;

    private float time_existence = 15f;

    private float time_counter = 0;

 

    void Start()

    {

    }

    private void Update()

    {

        time_counter += Time.deltaTime;

        if (time_counter >= time_existence)

        {

            Destroy(gameObject);

        }

    }

 

    private void OnTriggerEnter(Collider other)

    {

        if (other.gameObject.tag == "Water")

        {

 

            GameObject explosion = Instantiate(explosion_water_abstrakt) as GameObject;

            explosion.transform.position = this.transform.position;

 

 

            Destroy(gameObject);

        }

        if (other.gameObject.tag == "players_ship")

        {

 

            GameObject explosion = Instantiate(explosion_abstrakt) as GameObject;

            explosion.transform.position = this.transform.position;

            GameObject explosion_d = Instantiate(explosion_dark_abstrakt) as GameObject;

            explosion_d.transform.position = this.transform.position;

            Destroy(gameObject);

        }

    }

}

 

Отличие в том, что здесь отслеживается попадание в корабль игрока, и пока кроме взрыва ничего не происходит. Нанесённый ущерб будем прописывать позже. Добавьте этот скрипт на префаб ядра пиратов, и в окне Инспектор заполните пустые поля теми же взрывами, какие мы использовали для ядра игрока (рис. 126).

Рис. 126.

Дым будем создавать таким же образом. Выделите префаб дыма для пушки игрока и сделайте копию. Назовите новый префаб Smoke_Cannon_Pirate. Поменяйте параметры как на рисунке 127.

Рис. 127.

Не забудьте изменить масштаб, и цвет установите белым. Скрипт не удаляйте, он нам подходит.

Мы уже долго занимаемся пушками пиратов, и так до сих пор не увидели, как они стреляют по нашему кораблю. Осталось всё соединить в единое целое. Выделите префаб пушки пиратов и в окне Инспектор заполните все пустые поля. Туда нужно перенести префаб ядра пиратов, дым пушки пиратов, и звук выстрела (он отличается от звука выстрела пушки игрока) (рис. 128).

Рис. 128.

 

Казалось бы, уже всё, но нет, на корабле пиратов нет ни одной пушки. Откройте префаб корабля пиратов и из окна Проект перетащите на него пушку пиратов. Переместите на нужное место, сделайте ещё три копии и разместите примерно, как на изображении 129. Помните, что пушки должны располагаться строго горизонтально.

Рис. 129.

Когда выйдете из префаба, на корабле, который на сцене, пушки должны появиться автоматически. Запустите игру. Дождитесь, когда пираты повернуться к вам любым бортом, и тогда будет открыт огонь в направлении корабля игрока (рис 130-132).

Рис. 130.

Рис. 131.

Рис. 132.

При попадании в воду образуются всплески, а при попадании в корабль, создаётся двойной взрыв.



Урон, наносимый кораблю игрока.

Сейчас нужно разобраться, какой урон будет наноситься кораблю игрока. Максимальное количество пушек находится на корабле сейчас – 12 штук, сколько мы изначально разместили. При старте игры будут показаны только две пушки. Игрок должен будет собирать соответствующие предметы с поверхности воды для добавления на корабль пушек и ядер. При каждом попадании будет убираться одно орудие. При попадании в корабль, при отсутствии на нём пушки, будет активироваться потопление корабля. На самом деле пушки удаляться с корабля не будут, мы просто будем их делать неактивными, и наоборот, при собирании нужного предмета – активировать. Осталось только реализовать этот алгоритм в скрипте.

Для начала добавим в наш главный скрипт несколько новых переменных.

 

    public GameObject all_cannon;

    private int number_cannon = 2;

    private int max_number_cannon = 12;

    private int number_cannonballs = 10;

 

Первая публичная переменная ссылается на пустой объект, который находится на корабле игрока и содержит в себе все пушки. Вторая переменная указывает текущее количество пушек. Следующая переменная – максимальное количество пушек. Последняя переменная показывает, сколько ядер осталось у игрока. Нам понадобится метод Awake(). Как вы помните, это встроенный метод, и запускается он ещё до метода Start().

 

private void Awake()

    {

        if (PlayerPrefs.HasKey("number_cannon"))

        {

            number_cannon = PlayerPrefs.GetInt("number_cannon");

            if (number_cannon <= 0)

            {

                number_cannon = 2;

            }

        }

        else

        {

            number_cannon = 2;

        }

 }

 

Здесь мы обращаемся к сохранениям игры, и узнаём, нет ли у нас в сохранениях такого параметра как "number_cannon", и если есть, то считываем из него количество пушек, которое оставалось после прошлой сессии игры. Если количество пушек было 0, то даём игроку две пушки. Если же сохранённого параметра нет, то есть игра запущена в первый раз, то игрок получает две пушки.

Следующий метод отображает на экране нужное количество пушек.

 

private void Display_required_number_cannon()

    {

 

        for (int i = 0; i < all_cannon.transform.childCount; i++)

        {

            if (i <= number_cannon - 1)

            {

                all_cannon.transform.GetChild(i).gameObject.SetActive(true);

            }

            else

            {

                all_cannon.transform.GetChild(i).gameObject.SetActive(false);

            }

        }

}

 

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

Создадим метод для уменьшения и увеличения количества пушек.

 

public void Set_number_cannon(int p)

    {

        if (number_cannon >= max_number_cannon & p > 0)

        {

            return;

        }

        number_cannon += p;

        Display_required_number_cannon();

}

 

Этот метод вызывается из внешних скриптов, и в него передаётся значение 1, если добавляется пушка, или -1, если пушку нужно убрать. В случае максимального количества пушек, больше уже не добавляется. В конце вызывается метод для отображения пушек на экране. Метод Display_required_number_cannon() также добавьте в Start(), чтобы в начале игры игрок увидел правильное количество пушек. На сцене выделите пустой объект, содержащий главный скрипт и в незаполненное поле перенесите с корабля дочерний объект ___All_Cannon.

Запустите игру. На корабле должно быть только две пушки (рис. 133).

Рис. 133.

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

 

private GameObject empty_shared_variables;

 

Она будет ссылаться на пустышку, на которой находится главный скрипт. Затем в Start() прописываем код для поиска этой пустышки.

 

void Start()

    {

        empty_shared_variables = GameObject.Find("___empty_shared_variables");

}

 

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

 

if (other.gameObject.tag == "players_ship")

        {

            GameObject explosion = Instantiate(explosion_abstrakt) as GameObject;

            explosion.transform.position = this.transform.position;

            GameObject explosion_d = Instantiate(explosion_dark_abstrakt) as GameObject;

            explosion_d.transform.position = this.transform.position;

            empty_shared_variables.GetComponent<Class_common_variables>().Set_number_cannon(-1);

            Destroy(gameObject);

    }

 

Запустите игру. Дождитесь, пока в корабль попадёт ядро пиратов и вы увидите, что количество пушек уменьшилось (рис. 134). Быстрее всего в вас попадёт ядро, если вы близко подплывёте к кораблю, и будете находится в поле видимости его пушек.

Рис. 134.



Уничтожение корабля игрока.

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

 

    private float min_Y_Cameri = -0.7f;

    private bool sink_ship = false;

    private float sinking_speed = 0.16f;

 

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

 

public void Set_sink_ship()

    {

        sink_ship = true;

    }

    private void Sink_ship()

    {

        if (camera_Player.transform.position.y > min_Y_Cameri)

        {

            transform.Translate(0, -sinking_speed * Time.deltaTime, 0, Space.World);

        }

 

    }

 

С первым всё ясно. Он вызывается из другого скрипта и просто устанавливает нашу переменную в истину. Второй метод топит наш корабль с определённой скоростью, пока камера игрока не достигла определённой отметки. Заметьте, если камера находится на верхней палубе, корабль больше уйдёт под воду. Осталось записать в Update() условие для потопления. В самом начале метода пишем.

 

if (sink_ship)

        {

            Sink_ship();

            Tracking_clicks_on_computer();

            if (camera_Player.transform.position.y > min_Y_Cameri)

            {

                Moving_camera();

            }

 

            return;

        }

 

Если установлена переменная в истину, то выполняется метод потопления корабля. Также вызывается метод отслеживания нажатия клавиш пользователем. Далее проверяем, если камера не достигла минимальной отметки, то можно перемещать камеру. В конце условия вызывается return, значит весь остальной код из Update() выполняться не будет во время потопления корабля.

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

 

    public GameObject players_ship;

    public GameObject panel_loss_life;

    public AudioClip sound_shipwreck;

    private bool completing_level = false;

    private bool actions_loss_life_begun = false;

 

Первая публичная переменная ссылается на корабль игрока. На панели Инспектор нужно будет заполнить это поле. Вторая переменная указывает на панель проигрыша, которую мы ещё должны сделать на Канвасе. Следующая переменная ссылается на звуковой файл кораблекрушения. Четвертая переменная ставится в истину при успешном завершении уровня, и последняя устанавливается в истину при совершении действия по проигрышу.

Добавим несколько методов.

 

public void Sound_shipwreck()

    {

        Play_Audio(sound_shipwreck);

    }

 

Этот метод просто проигрывает звуковой файл при потоплении.

 

private void Actions_loss_life()

    {

        if (actions_loss_life_begun | completing_level)

        {

            return;

        }

        actions_loss_life_begun = true;

 

 

        StartCoroutine(Actions_loss_life_Coroutine());

 

    }

 

Этот метод запускается сразу после начала потопления, и устанавливает переменную, которая указывает, что действия для завершения уровня начались. Также вызывается каротина (метод замедленного действия). Вот как она выглядит.

 

IEnumerator Actions_loss_life_Coroutine()

    {

        yield return new WaitForSeconds(6f);

        {

            Time.timeScale = 0;

            panel_loss_life.SetActive(true);

        }

 

    }

 

Она просто ждёт 6 секунд, затем останавливает время в игре и делает активной панель проигрыша.

И наконец метод, который связывает всё это воедино и вызывается сразу после попадания ядра в корабль (при отсутствии на нём пушек).

 

public void Sink_ship()

    {

        players_ship.transform.GetComponent<Players_ship>().Set_sink_ship();

        Sound_shipwreck();

        Actions_loss_life();

    }

 

Теперь нужно вызвать сам этот метод из метода Set_number_cannon. Вот так он сейчас должен выглядеть.

 

    public void Set_number_cannon(int p)

    {

        if (number_cannon >= max_number_cannon & p > 0)

        {

            return;

        }

        number_cannon += p;

        if (number_cannon < 0)

        {

            Sink_ship();

 

            return;

        }

        Display_required_number_cannon();

    }

 

Мы добавили условие – если пушек после уменьшения стало отрицательное количество, то активируем действия по потоплению корабля.

Итак, подведём итог. Метод, по уменьшению количества пушек Set_number_cannon вызывает метод активации всех действий проигрыша на уровне Sink_ship(). Он в свою очередь в скрипте корабля устанавливает переменную, которая вызывает метод потопления корабля. Также Sink_ship() запускает метод Actions_loss_life(), который устанавливает переменную завершения уровня при проигрыше и запускает метод замедленного действия Actions_loss_life_Coroutine() для остановки игры и вывода панели проигрыша.



Панель проигрыша.

Для того, чтобы посмотреть результат проделанной работы, нам не хватает панели проигрыша. Давайте её создадим. На Канвасе кликните правой кнопкой мыши и выберите UIPanel. Назовите её Panel_loss_life. Цвет выберите, какой считаете нужным. Сделайте непрозрачной (рис. 135).

Рис. 136.

На названии панели в окне Иерархии кликните правой кнопкой мыши, создайте изображение. Оставьте его по центру и присвойте ему картинку из файла like. Размеры установите 300 на 300 и поверните по оси Z на 180 градусов. Таким же образом создайте кнопку (button). Перенесите её в правый нижний угол и привяжите к нему. Размеры поставьте 120 на 120 и присвойте картинку зелёной галочки. Удалите с кнопки дочерний объект (надпись) (рис. 136).

 

 

 

Рис. 136.

Кликните на нашей созданной панели (она должна быть последним дочерним объектом на Канвасе) и в окне Инспектор сделайте её неактивной (рис. 137)

Рис. 137.

Всё готово. Осталось в главном скрипте заполнить пустые поля (рис. 138).

Рис. 138.

Перенесите туда корабль игрока со сцены, созданную нами панель из Канваса и звук кораблекрушения. Запускаем игру и подставляемся под ядра пиратов. После нескольких попаданий наш корабль благополучно тонет, а затем появляется панель потери жизни (рис 139, 140).

 

Рис. 139.Рис. 140.



Начальная сцена игры.

Нам нужно как-то закрыть это появляющееся окно, информирующее нас о том, что мы проиграли. Но дело в том, что при закрытии нужно будет загрузить сцену заново. А загружать мы будем сцены по их порядковому номеру. Сцене с индексом 1 будет соответствовать первый уровень игры и так далее. Но будет ещё сцена с индексом 0. Это начальная заставка игры, где игроку нужно нажать на кнопку Play. Давайте создадим эту сцену, чтобы разместить её в нужном месте для правильной загрузки сцен.

Зайдите в папку Scenes и на пустом пространстве вызовите контекстное меню. Выберите Создать – Сцену. Назовите её Opening_scene. Дважды кликните по ней, чтобы открыть. На панели Иерархии вызовите контекстное меню и далее UICanvas. Параметры задайте как на рисунке 141.

Рис. 141.

На Канвасе создайте изображение, оставьте его по центру и размеры задайте 1920 на 1280. В качестве картинки используйте файл Image Sequence_011_0000 (рис. 142).

Рис. 142.

Оно выходит за пределы Канваса. Это сделано для экранов с нестандартным разрешением. Если экран будет широким, то часть картинки, которая сейчас за пределами Канваса, будет отображена на экране, иначе бы появились серые полосы. Вместо этого можно было сделать растяжение картинки по размеру Канваса, но очень бы не хотелось менять её пропорции и искажать.

Создайте ещё одно изображение на Канвасе, разместите в правом верхнем углу и привяжите к нему. Размеры 540 на 300. Поместите туда логотип игры (рис. 143).

Рис. 143.

Создайте кнопку, привяжите к правому нижнему углу, там же разместите. Размеры задайте 200 на 100. Наложите на него картинку PlayButton.

Создайте текстовое поле и параметру установите, как на рисунке 144. На панели Иерархии перенесите его выше кнопки, чтобы этот объект случайно не перекрыл кнопку, тогда её невозможно будет нажать. Тот объект, который на Канвасе находится ниже по дереву дочерних объектов, тот находится в более верхнем слое на экране, и перекрывает другие объекты, если они накладываются друг на друга.

Рис. 144.

Сделайте текстовое поле неактивным (снимите галочку возле его имени).

В этой сцене у нас будет только один скрипт, который будет запускать следующую сцену, то есть первый уровень. Создайте его и назовите Opening_scene. Вот его содержимое.

 

using UnityEngine;

using UnityEngine.SceneManagement;

using UnityEngine.UI;

 

public class Opening_scene : MonoBehaviour

{

    public Text text_Load;

 

    public void Start_game()

    {

        text_Load.gameObject.SetActive(true);

        SceneManager.LoadScene(1);

    }

 

}

В начале скрипта добавлена библиотека для работы со сценами и вторая библиотека для работы с элементами интерфейса. Публичная переменная ссылается на текстовое поле. Метод Start_game() будет активироваться при нажатии кнопки на Канвасе. Сначала делается активным текстовое поле, которое показывает, что идёт загрузка, а потом выполняется метод по загрузке сцены с индексом 1.

Этот скрипт нужно поместить на какой-нибудь объект сцены. Давайте выберем камеру. На панели Инспектор в пустое поле перенесите с Канваса объект Text_Load. Сейчас на кнопку Play нужно назначить действие, которое будет выполняться при клике на ней. Этим действием будет запуск из скрипта метода Start_game(). В окне Иерархии кликните на кнопке и на панели Инспектор у компонента Button под структурой On Click() нажмите +. В нижнее поле нужно перенести объект, на котором находится скрипт – это камера, а в правом поле выберите нужный скрипт и в нём метод (рис. 145).

Рис. 145.

Сейчас нужно разместить наши сцены в нужном порядке, чтобы Юнити знал индексы всех сцен игры. Для удобства первую сцену переименуем. В папке Scenes кликните по названию сцены и через пару секунд ещё раз. Появится курсор. Переименуйте её в Level_1 (рис. 146).

 

Рис. 146.

 

Выберите меню File – Build Settings. В верхнюю часть окна перенесите по порядку две наших сцены (рис. 147).

Рис. 147.

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



Запекание света.

Находясь в начальной сцене, запустите игру. Нажмите на кнопку Play. Должны появиться точки посередине экрана и через пару секунд загрузится первый уровень. Если вам показалось, что освящение сцены стало темнее, то такое случается очень часто. При загрузке игры непосредственно из сцены, её освещение нормальное, а при загрузке этой же сцены во время игры из других сцен, освещение меняется. Это можно исправить запеканием света. Находясь на сцене Level_1 (кликните по ней 2 раза в окне Проект), откройте вкладку Lighting и снизу нажмите кнопку Generate Lighting. Через какое-то то время после просчётов в папке Scenes должна появиться новая папка с настройками запекания света на этом уровне.

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

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

 

using UnityEngine.SceneManagement;

 

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

 

private int level = 1;

 

В методе Awake() самой первой строчкой впишите код для установки значения этой переменной.

 

level = SceneManager.GetActiveScene().buildIndex;

 

Теперь мы знаем, какой уровень у нас загружен.

 

Осталось создать метод, который будет загружать сцену с нужным индексом при нажатии на кнопку закрытия панели проигрыша.

 

public void On_Click_Close_loss_life_panel()

    {

        SceneManager.LoadScene(level);

}

 

Присвойте кнопке на панели проигрыша этот метод. Действия проделайте точно такие же, как вы делали для кнопки Play. В качестве объекта, на котором лежат скрипт укажите ___empty_shared_variables, затем скрипт и название метода (рис. 148).

Рис. 148.

 

В методе Start() первой строчкой впишите код.

 

Time.timeScale = 1;

 

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



Финальная сцена.

Чтобы далеко не отходить от темы, давайте сразу создадим последнюю сцену игры, которая будет появляться при завершении игроком всех уровней. Её можно создать по тому же принципу, что и начальную сцену, добавив картинки по своему желанию и одну кнопку, которая будет загружать сцену с индексом 0. А можно сделать ещё проще. Сделайте дубликат начальной сцены, назовите её Final, картинку кнопки Play заменить на галочку и удалите скрипт с камеры. Сейчас мы создадим для этой сцены свой скрипт, он также будет называться Final.

 

using UnityEngine;

using UnityEngine.SceneManagement;

 

public class Final : MonoBehaviour

{

    public void Start_game()

    {

        PlayerPrefs.DeleteAll();

        SceneManager.LoadScene(0);

    }

}

Один публичный метод, который мы свяжем с кнопкой. Первой строчкой стираем все сохранения (создавать сохранения мы будем чуть позже), второй строкой загружаем начальную сцену игры. Скрипт наложите также на камеру, и кнопку свяжите с методом Start_game()(рис. 149).

Рис. 149.

Выберите меню File – Build Settings. В верхнюю часть окна перенесите созданную нами сцену, чтобы она была последней в списке.



Победа игрока.

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

Для начала создадим панель победы. Открываем сцену Level_1 и на Канвасе выделяем панель проигрыша. Нажатием Ctrl+D делаем дубликат этой модели, активируем её (галочка на панели Инспектор) и даём название Panel_Level_Up. Цвет панели меняем на какой-нибудь радостный, что-то вроде зелёного. Изображение с лайком поворачиваем по оси Z на 180 градусов. Для кнопки пока оставим прежний метод (рис. 150). Рис. 150.

Открываем главный скрипт и создаём новый метод.

 

private void Checking_remaining_ships()

    {

        if (all_pirate_ships.transform.childCount <= 0)

        {

            Successful_completion_level();

        }

    }

 

Сразу поместите этот метод в Update(). Он просто проверяет пустой объект, к которому прикреплены все пиратские корабли, и если число дочерних объектов будет 0, то вызывается метод завершения уровня. У нас пока ещё его нет, так ошибка неизбежна. Создадим такой метод.

 

private void Successful_completion_level()

    {

        if (actions_loss_life_begun | completing_level)

        {

            return;

        }

        completing_level = true;

        Time.timeScale = 0;

        level += 1;

        panel_successful_completion_level.SetActive(true);

    }

 

Здесь устанавливается переменная, указывающая, что уровень завершается, в истину, затем останавливается время в игре, номер уровня повышаем на 1 и показываем игроку панель победы. Мы ещё пока не создали переменную, которая будет ссылаться на эту панель так что эта строчка вызовет ошибку. К имеющимся переменным добавьте вот такую.

 

public GameObject panel_successful_completion_level;

 

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

 

public void on_Click_Close_panel_successful_completion_level()

    {

        SceneManager.LoadScene(level);

    }

 

Просто загружается сцена с номером level. Как вы помните, мы этот номер повысили на 1. На данный момент скрипт должен выглядеть следующим образом.

 

using System.Collections;

using UnityEngine;

using UnityEngine.UI;

using UnityEngine.SceneManagement;

 

public class Class_common_variables : MonoBehaviour

{

    public AudioSource audioSource;

    public AudioSource audioSource_Waves_and_Ship;

    public AudioClip sound_cannon_shot;

    private GameObject camera_players;

    private GameObject active_cannon;

    private bool cannon_ready_fire = false;

    public LayerMask Сannon;

    public Slider slider_force_cannon;

    private float rate_change_force = 0.5f;

    public GameObject all_pirate_ships;

    public GameObject slider_1;

    public GameObject slider_2;

    public GameObject slider_3;

    public GameObject slider_4;

    public GameObject all_cannon;

    private int number_cannon = 2;

    private int max_number_cannon = 12;

    private int number_cannonballs = 10;

   

    public GameObject players_ship;

    public GameObject panel_loss_life;

    public GameObject panel_successful_completion_level;

    public AudioClip sound_shipwreck;

    private bool completing_level = false;

    private bool actions_loss_life_begun = false;

    private int level = 1;

 

    private void Awake()

    {

        level = SceneManager.GetActiveScene().buildIndex;

        if (PlayerPrefs.HasKey("number_cannon"))

        {

            number_cannon = PlayerPrefs.GetInt("number_cannon");

            if (number_cannon <= 0)

            {

                number_cannon = 2;

            }

        }

        else

        {

            number_cannon = 2;

        }

    }

    void Start()

    {

        Time.timeScale = 1;

        Display_required_number_cannon();

        slider_force_cannon.value = 1f;

        Assigning_sliders_ships();

    }

 

    void Update()

    {

        Tracking_clicks_on_computer();

        Checking_remaining_ships();

        Change_force();

    }

    private void Assigning_sliders_ships()

    {

        switch (all_pirate_ships.transform.childCount)

        {

            case 1:

                slider_1.SetActive(true);

                all_pirate_ships.transform.GetChild(0).transform.GetComponent<Pirate_ship>().Set_slider_life(slider_1);

                break;

            case 2:

                slider_1.SetActive(true);

                all_pirate_ships.transform.GetChild(0).transform.GetComponent<Pirate_ship>().Set_slider_life(slider_1);

                slider_2.SetActive(true);

                all_pirate_ships.transform.GetChild(1).transform.GetComponent<Pirate_ship>().Set_slider_life(slider_2);

                break;

            case 3:

                slider_1.SetActive(true);

                all_pirate_ships.transform.GetChild(0).transform.GetComponent<Pirate_ship>().Set_slider_life(slider_1);

                slider_2.SetActive(true);

                all_pirate_ships.transform.GetChild(1).transform.GetComponent<Pirate_ship>().Set_slider_life(slider_2);

                slider_3.SetActive(true);

                all_pirate_ships.transform.GetChild(2).transform.GetComponent<Pirate_ship>().Set_slider_life(slider_3);

                break;

            case 4:

                slider_1.SetActive(true);

                all_pirate_ships.transform.GetChild(0).transform.GetComponent<Pirate_ship>().Set_slider_life(slider_1);

                slider_2.SetActive(true);

                all_pirate_ships.transform.GetChild(1).transform.GetComponent<Pirate_ship>().Set_slider_life(slider_2);

                slider_3.SetActive(true);

                all_pirate_ships.transform.GetChild(2).transform.GetComponent<Pirate_ship>().Set_slider_life(slider_3);

                slider_4.SetActive(true);

                all_pirate_ships.transform.GetChild(3).transform.GetComponent<Pirate_ship>().Set_slider_life(slider_4);

                break;

        }

    }

    private void Change_force()

    {

        if (Input.GetKey(KeyCode.Z))

        {

            slider_force_cannon.value -= rate_change_force * Time.deltaTime;

        }

        if (Input.GetKey(KeyCode.X))

        {

            slider_force_cannon.value += rate_change_force * Time.deltaTime;

        }

 

    }

    private void Tracking_clicks_on_computer()

    {

 

        if (Input.GetMouseButtonDown(0))

        {

            Launch_ray_search_cannon();

        }

        if (Input.GetKeyDown(KeyCode.M) & active_cannon != null)

        {

            active_cannon.transform.GetComponent<Cannon>().Cannonball_shot();

        }

    }

        private void Launch_ray_search_cannon()

    {

        RaycastHit hit;

        Ray ray = camera_players.GetComponent<Camera>().ScreenPointToRay(Input.mousePosition);

 

        if (Physics.Raycast(ray, out hit, 100f, Сannon))

        {

            if (hit.collider.gameObject.tag == "cannon")

            {

                if (active_cannon != null)

                {

                    active_cannon.transform.GetComponent<Cannon>().Set_cannon_active(false);

                    active_cannon.transform.GetChild(3).gameObject.SetActive(false);

                }

                active_cannon = hit.collider.gameObject;

                active_cannon.transform.GetComponent<Cannon>().Set_cannon_active(true);

                active_cannon.transform.GetChild(3).gameObject.SetActive(true);

                cannon_ready_fire = true;

            }

        }

    }

    private void Display_required_number_cannon()

    {

 

        for (int i = 0; i < all_cannon.transform.childCount; i++)

        {

            if (i <= number_cannon - 1)

            {

                all_cannon.transform.GetChild(i).gameObject.SetActive(true);

            }

            else

            {

                all_cannon.transform.GetChild(i).gameObject.SetActive(false);

            }

        }

    }

    public void Set_number_cannon(int p)

    {

        if (number_cannon >= max_number_cannon & p > 0)

        {

            return;

        }

        number_cannon += p;

        if (number_cannon < 0)

        {

            Sink_ship();

 

            return;

        }

        Display_required_number_cannon();

    }

    private void Play_Audio(AudioClip clip)

    {

        audioSource.PlayOneShot(clip);

    }

    public void Sound_cannon_shot()

    {

        Play_Audio(sound_cannon_shot);

    }

   

    public void Set_cannon_ready_fire(bool g)

    {

        cannon_ready_fire = g;

 

    }

    public void Set_Camera(GameObject c)

    {

        camera_players = c;

    }

    public float Get_force_cannon()

    {

        return slider_force_cannon.value;

    }

    public void Sound_shipwreck()

    {

        Play_Audio(sound_shipwreck);

    }

    private void Actions_loss_life()

    {

        if (actions_loss_life_begun | completing_level)

        {

            return;

        }

        actions_loss_life_begun = true;

 

 

        StartCoroutine(Actions_loss_life_Coroutine());

 

    }

 

    IEnumerator Actions_loss_life_Coroutine()

    {

        yield return new WaitForSeconds(6f);

        {

            Time.timeScale = 0;

            panel_loss_life.SetActive(true);

        }

 

    }

    public void Sink_ship()

    {

        players_ship.transform.GetComponent<Players_ship>().Set_sink_ship();

        Sound_shipwreck();

        Actions_loss_life();

    }

    public void On_Click_Close_loss_life_panel()

    {

        SceneManager.LoadScene(level);

    }

    private void Checking_remaining_ships()

    {

        if (all_pirate_ships.transform.childCount <= 0)

        {

            Successful_completion_level();

        }

    }

    private void Successful_completion_level()

    {

        if (actions_loss_life_begun | completing_level)

        {

            return;

        }

        completing_level = true;

        Time.timeScale = 0;

        level += 1;

        panel_successful_completion_level.SetActive(true);

    }

    public void on_Click_Close_panel_successful_completion_level()

    {

        SceneManager.LoadScene(level);

    }

}

 

Перейдите в проект Юнити. Кнопке на панели победы присвойте правильный метод, и в поле на главном скрипте перенесите панель с Канваса. Не забудьте панель победы сделать неактивной. Запустите игру и попробуйте уничтожить пиратский корабль. Когда он полностью уйдёт под воду, вы увидите сначала панель победы, а при нажатии кнопки загрузится следующая сцена, на данный момент – финальная (ри. 151 и 152). При нажатии кнопки на финальной сцене, загрузится начальная сцена (они у нас очень похожи).

Рис. 151.Рис. 152.

 



Ядра на воде.

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

На сцену добавьте объект сферу, используя панель Иерархии, удалите с неё компонент коллайдера, добавьте чёрный материал, размеры установите, как на рисунке 153.

Рис. 153.

Сделайте ещё три копии такой сферы и расположите их пирамидой (рис. 154).

Рис. 154.

Создайте пустой объект, назовите его ___Cannonballs_4. Расположите примерно по центру пирамиды и перенесите все 4 сферы на эту пустышку (ри. 155).

Рис. 155.

Добавьте на объект ___Cannonballs_4 компонент коллайдера и компонент Rigidbody. Все параметры установите, как на изображении 156. Размер коллайдера подгоните под ваш размер пирамиды. Добавьте тег cannonballs_4. Перенесите объект с панели Иерархии в папку с префабами. Первый объект для появления на поверхности воды готов. Как вы уже поняли, если корабль коснётся его, то игроку добавятся ядра. Осталось написать скрипт для этих водных ядер.

 Рис. 156.

Создайте скрипт и назовите его Cannonballs_4. Вот всё его содержание.

 

using UnityEngine;

 

public class Cannonballs_4 : MonoBehaviour

{

    private float min_height = -0.35f;

    private bool fall_over = false;

    private float rotation_speed = 120f;

    private float speed_fall = 2f;

    private float time_counter = 180f;

    void Start()

    {

 

    }

 

    void Update()

    {

        transform.Rotate(0, -rotation_speed * Time.deltaTime, 0, Space.Self);

        Fall();

        Timer();

    }

    private void Fall()

    {

        if (fall_over)

        {

            return;

        }

        transform.Translate(0, -speed_fall * Time.deltaTime, 0, Space.World);

        if (transform.position.y <= min_height)

        {

            fall_over = true;

        }

    }

    private void Timer()

    {

        time_counter -= Time.deltaTime;

        if (time_counter <= 0)

        {

            Destroy(gameObject);

        }

    }

    private void OnTriggerEnter(Collider other)

    {

        if (other.gameObject.tag == "land")

        {

            Destroy(gameObject);

        }

    }

}

 

Пирамида будет появляться на некотором расстоянии от водной поверхности (координаты появления, как и само появление этого предмета прописывается в другом скрипте) и падать до тех пор, пока не достигнет определённой высоты. При соприкосновении с землёй объект уничтожается. Через 3 минуты своего существования предмет удаляется. С самого начала своего падения пирамида из ядер будет вращаться вокруг своей оси Y. Вот всё, что описывает данный скрипт. А теперь более подробно. Первая переменная – это минимальная высота, до которой падает объект. Вторая переменная показывает, включен или нет режим падения. Следующая переменная – скорость вращения, потом скорость падения. И последняя переменная – счётчик времени существования объекта.

Переменная min_height у вас может быть установлена в другое значение, в зависимости от размеров пирамиды из ядер. Для того, чтобы его узнать, переместите префаб ___Cannonballs_4 на сцену непосредственно над водной гладью, посмотрите в окне Инспектор значение по оси Y. Именно это и будет минимальная высота падения. Не забудьте удалить префаб со сцены.

В Update() прописываем непрестанный поворот по оси Y и два метода – падение пирамиды и отсчёт таймера жизни. В методе Fall() проверяем, если уже установлена переменная, заканчивающая падение, то сразу выходим из метода, иначе смещаем объект вниз с заданной скоростью, и проверяем его высоту. При достижении нужной отметки, переменную fall_over ставим в истину, то есть падение закончено.

В методе Timer() Постоянно уменьшаем время, пока оно не достигнет нуля. Затем объект удаляется. Если в течении трёх минут игрок не собрал предмет, значит ему не повезло.

Во встроенный в Юнити метод OnTriggerEnter(Collider other) прописываем условие: если объект, с которым столкнулась наша пирамида из ядер имеет тег "land" (то есть это земля), то сразу удаляем объект. Добавьте этот скрипт на префаб четырёх ядер.



Пушки на воде.

Теперь таким же образом нужно создать пушку, которая будет появляться на поверхности воды, чтобы её мог собирать игрок. Перенесите на сцену 3D модель Cannon_init, отмасштабируйте по всем осям с коэффициентом 4, наложите материал пушки. Создайте пустой объект, назовите его ___Cannon_Water, расположите по центру пушки. Сделайте 3D модель дочерним по отношении к пустышке (рис. 157).

 Рис. 157.

На объект ___Cannon_Water добавьте два компонента – Box Collider и Rigidbody с параметрами, как на рисунке 158.

Рис. 158.

Скрипт, который будет добавлен на этот объект почти копирует предыдущий, за исключением названия и минимальной высоты падения пушки. Для того, чтобы её измерить, разместите созданный объект над водной гладью, и в компоненте Transform запомните значение по оси Y. Создайте скрипт, назовите его Cannon_Water. Скопируйте всё содержание класса из скрипта Cannonballs_4, удалите из вновь созданного скрипта полностью содержание класса, оставьте только открывающиеся и закрывающиеся фигурные скобки. Внутрь скобок вставьте скопированный ранее код. Поменяйте переменную min_height на то значение, которое вы измерили.

Класс Cannon_Water добавьте на объект ___Cannon_Water. Так же на объект нужно добавить тег cannon_water. Сделайте префаб из пушки на воде, перенеся в соответствующую в папку. Удалите копию со сцены.



Появление падающих пушек и ядер.

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

 

using UnityEngine;

 

public class Appearance_objects : MonoBehaviour

{

    public GameObject cannon_abstr;

    public GameObject cannonballs_4_abstr;

    private float size_X = 450f;

    private float size_Y = 450f;

    private float height_appearance = 5f;

    private const float time_for_cannon_const = 1f;

    private float time_for_cannon = time_for_cannon_const;

    private const float time_for_cannonballs_const = 0.9f;

    private float time_for_cannonballs = time_for_cannonballs_const;

 

    void Start()

    {

 

    }

 

    void Update()

    {

        Cannon();

        Cannonballs_4();

    }

    private void Cannon()

    {

        time_for_cannon -= Time.deltaTime;

        if (time_for_cannon <= 0)

        {

            time_for_cannon = time_for_cannon_const;

            GameObject cannon = Instantiate(cannon_abstr) as GameObject;

            float x = Random.Range(4f, size_X - 4f);

            float z = Random.Range(4f, size_Y - 4f);

            float y = Random.Range(height_appearance - 1, height_appearance + 1);

            cannon.transform.position = new Vector3(x, y, z);

        }

    }

    private void Cannonballs_4()

    {

        time_for_cannonballs -= Time.deltaTime;

        if (time_for_cannonballs <= 0)

        {

            time_for_cannonballs = time_for_cannonballs_const;

            GameObject cannonballs = Instantiate(cannonballs_4_abstr) as GameObject;

            float x = Random.Range(4f, size_X - 4f);

            float z = Random.Range(4f, size_Y - 4f);

            float y = Random.Range(height_appearance - 1, height_appearance + 1);

            cannonballs.transform.position = new Vector3(x, y, z);

        }

    }

 

}

Первые две публичные переменные, это ссылки на префабы водных пушек и водных четырёх ядер соответственно. Следующие две переменных показывают размер участка водного сражения, ограниченного невидимыми стенами. Пятая переменная – это высота появления объектов. Затем идёт интервал времени между появлениями пушек и переменная, которая отсчитывает время до появления следующей пушки. Изначально её значение равно интервалу времени. Последние две переменные – то же самое, только для пирамиды из четырёх ядер.

В скрипте всего два метода. Один контролирует появление пушек, другой – пирамиды из ядер. Они абсолютно идентичны. Сначала уменьшаем время и проверяем, не достигло ли оно нуля, и если это так, то время опять приравниваем к начальному интервалу, для следующего отсчёта. Создаём сам объект на основе префаба – пушку или ядра. Затем случайным образом высчитываем три локальные переменные x, y, z, которые будут координатами появления пушки или ядер. X и Y высчитывается с учётом того, что объект будет появляться не ближе 4 единиц от границы, а Z варьируется плюс-минус единица от начального значения. Последней строкой присваиваем созданному объекту вычисленные координаты. Оба метода помещены в Update().

Этот скрипт, так же, как и главный скрипт в игре, нужно поместить на объект ___empty_shared_variables. Пустые поля в окне Инспектор заполните правильными префабами (рис. 159).

Рис. 159.

Запустите игру, и через какое-то время вы увидите, как водная гладь заполняется падающими на неё предметами (ри. 160).

Рис. 160.



Собираем предметы на воде.

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

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

 

public AudioClip sound_took_cannon;

public AudioClip sound_took_cannon_balls;

 

Это ссылки на звуковые файлы, которые будут воспроизводиться при собирании на воде пушек и ядер. Далее нужно записать два публичные метода, которые будут воспроизводить эти звуки.

 

public void Sound_took_cannon()

    {

        Play_Audio(sound_took_cannon);

    }

    public void Sound_took_cannon_balls()

    {

        Play_Audio(sound_took_cannon_balls);

    }

 

Открываем скрипт Players_ship, которые находится на корабле игрока, и добавляем встроенный метод, которые отслеживает взаимодействие нашего корабля с другими объектами.

 

private void OnTriggerEnter(Collider other)

    {

        if (other.gameObject.tag == "cannon_water")

        {

            link_on_Class.Set_number_cannon(1);

            link_on_Class.Sound_took_cannon();

            Destroy(other.gameObject);

        }

       

    }

В условии проверяем, если объект контакта имеет тег, как у водной пушки, то вызываем из главного скрипта метод по добавлению пушки, далее воспроизводим соответствующий звук и уничтожаем пушку на воде. В панели Инспектор для главного скрипта заполните поля со звуковыми файлами (рис.161) и запустите игру.

Рис. 161.

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

Рис. 162.

Переходим к ядрам. Для начала на Канвасе создадим изображение и текстовое поле, которое будет отображать текущее количество ядер у игрока. На рисунке 163 показаны параметры для картинки.

Рис. 163.

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

Создайте текстовое поле, и расположите его как на рисунке 164. На панели Иерархии поместите его рядом с изображением ядер.

Рис. 164.

Откройте главный скрипт и добавьте переменную.

 

public Text text_number_cannonballs;

 

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

 

private void Display_information()

    {

        text_number_cannonballs.text = number_cannonballs.ToString();

    }

    public void Add_cannonballs()

    {

        number_cannonballs += 10;

    }

    public void Subtract_cannonballs()

    {

        number_cannonballs -= 1;

    }

    public int Get_number_cannonballs()

    {

        return number_cannonballs;

    }

 

Первый метод просто выводит информацию о количестве ядер в текстовое поле. Поместите этот метод в Update(). Остальные три метода вызываются из внешних скриптов. Один из них добавляет количество ядер игроку, другой отнимает одно ядро при выстреле из пушки, а последний метод передаёт количество оставшихся ядер. Это нужно для того, чтобы при команде игрока выстрела из пушки, мы проверяли, а не равно ли количество ядер нулю. И если это так, то выстрела игрок не дождётся.

Открываем скрипт Players_ship и в метод OnTriggerEnter дописываем следующее условие.

 

if (other.gameObject.tag == "cannonballs_4")

        {

            link_on_Class.Add_cannonballs();

            link_on_Class.Sound_took_cannon_balls();

            Destroy(other.gameObject);

        }

 

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

Сейчас сделаем проверку на количество ядер при выстреле из пушки. Откройте скрипт Cannon и в методе Creating_and_launching_cannonball() добавьте код в начальное условие. Теперь оно должно выглядеть таким образом.

 

if (recoil_back | moving_forward | empty_shared_variables.GetComponent<Class_common_variables>().Get_number_cannonballs() <= 0)

        {

            return;

        }

 

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

 

empty_shared_variables.GetComponent<Class_common_variables>().Subtract_cannonballs();

 

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

На панели Инспектор под главным скриптом в пустое место перенесите текстовое поле для количества ядер (рис. 165).

Рис. 165.

Запустите игру. Теперь можно собирать не только пушки, но и ядра. При взятии должен воспроизводится звук, водные ядра уничтожаться, а в соответствующем поле добавляться 10 ядер. При выстреле количество ядер уменьшается на единицу, и при нулевом значении, выстрел не совершается (рис. 166).

Рис. 166.



Сохранения.

Мы уже поверхностно касались темы сохранения, и в скрипте Class_common_variables метод Awake() содержит некоторый код для чтения сохранённых данных, но самого сохранения в скриптах пока не происходит. Пора этим заняться.

В метод Awake() к имеющейся проверке на количество сохранённых пушек добавим проверку на количество сохранённых ядер.

 

if (PlayerPrefs.HasKey("number_cannonballs"))

        {

            number_cannonballs = PlayerPrefs.GetInt("number_cannonballs");

        }

        else

        {

            number_cannonballs = 10;

        }

 

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

Создадим два новых метода.

 

private void Saving_parameters()

    {

        PlayerPrefs.SetInt("level", level);

        PlayerPrefs.SetInt("number_cannon", number_cannon);

        PlayerPrefs.SetInt("number_cannonballs", number_cannonballs);

    }

    public void onClick_Exit()

    {

        Saving_parameters();

        Application.Quit();

    }

 

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

 

On_Click_Close_loss_life_panel()

on_Click_Close_panel_successful_completion_level()

Set_number_cannon(int p)

Add_cannonballs()

Subtract_cannonballs()

 

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

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

Откроем скрипт Opening_scene и изменим метод Start_game(). Сейчас он должен выглядеть вот так.

 

public void Start_game()

    {

        text_Load.gameObject.SetActive(true);

        if (PlayerPrefs.HasKey("level"))

        {

            SceneManager.LoadScene(PlayerPrefs.GetInt("level"));

        }

        else

        {

            SceneManager.LoadScene(1);

        }

    }

 

Проверяем, есть ли сохранённый ключ "level", и если да, то загружаем сцену, индекс которой находится в этом ключе. Если же нет сохранений, то есть игрок запустил игру в первый раз, то загружаем первую сцену. У нас пока нет уровня больше первого, но это не значит, что его никогда не будет.

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

Давайте сделаем кнопку закрытия игры. К сожалению, в редакторе Юнити она работать не будет, но пригодится, когда мы создадим билд проекта. Параметры кнопки установите, как на рисунке 167.

Рис. 167.

Назначьте на неё метод onClick_Exit() из главного скрипта.



С высоты птичьего полёта.

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

На панели Иерархии создайте камеру, назовите Camera_Top, поверните вниз по оси X на 30 градусов (рис. 168). Слева от компонента Audio Listener отключите галочку. Этот компонент должен быть только на одной камере сцены. Параметр Depth установите равным единице. Если на сцене несколько камер, то изображение идёт с той, у которой это значение больше. Для камеры на корабле Depth равен 0.

Рис. 168.

 

Создайте пустой объект, назовите ___Empl и разместите в тех же координатах, что и камеру. Объект Camera_Top сделайте дочерним по отношению к пустышке. Из пустого объекта сделайте префаб. Удалите копию со сцены. Камера верхнего вида готова. Из скрипта управлять будем только пустышкой.

В главный скрипт добавим новые переменные.

 

    public GameObject camera_top_abstract;

    private GameObject camera_top;

    private float y_camera_top = 17.5f;

    private float camera_top_distance_ship_z = 15f;

    private float min_X_Camera = 10f;

    private float max_X_Camera = 450f;

    private float min_Z_Camera = 10f;

    private float max_Z_Camera = 450f;

    private bool moving_forward_camera = false;

    private bool moving_back_camera = false;

    private bool moving_r_camera = false;

    private bool moving_l_camera = false;

    private bool rotation_cameri_r = false;

    private bool rotation_cameri_l = false;

    private float speed_movement_camera = 50f;

    private float speed_rotation_camera = 100f;

 

Первая переменная ссылается на префаб пустышки с дочерним объектом камера. Вторая переменная будет ссылаться на созданную копию, на основе этой пустышки. Следующая переменная устанавливает высоту для камеры. camera_top_distance_ship_z определяет расстояние от корабля до камеры по оси Z. Следующие четыре переменные ограничивают движение камеры вдоль и поперёк участка боя. moving_forward_camera устанавливается в истину при движении камеры вперёд. Следующие пять переменных делают то же самое, но при движении камеры назад, вправо, влево, и поворотах вправо и влево. Последние две переменные указывают скорость перемещения и вращения камеры.

Добавим три метода.

 

private void Switching_camera()

    {

        if (Input.GetKeyDown(KeyCode.C))

        {

            if (camera_top == null)

            {

                Creating_camera_top();

            }

            else

            {

                Destroy(camera_top);

            }

        }

    }

 

    private void Creating_camera_top()

    {

        camera_top = Instantiate(camera_top_abstract) as GameObject;

        camera_top.transform.position = new Vector3(players_ship.transform.position.x, y_camera_top, players_ship.transform.position.z - camera_top_distance_ship_z);

    }

 

    public bool Get_camera_top()

    {

        return camera_top != null;

    }

 

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

Метод по созданию верхней камеры состоит из двух строк кода. В первой мы создаём копию пустышки с камерой и присваиваем её переменой camera_top, второй строчкой устанавливаем координаты для вновь созданной камеры. Координата X равна такой же координате корабля, по Y поднимаем вверх на величину y_camera_top, а по Z отдаляем от корабля немного назад на величину camera_top_distance_ship_z.

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

Метод Switching_camera() поместим в конец метода Update().

В метод Tracking_clicks_on_computer(), который отслеживает нажатие клавиш игроком добавим код для управления камерой сверху.

 

   if (!Get_camera_top())

        {

            return;

        }

        if (Input.GetKey(KeyCode.W) & !Input.GetKey(KeyCode.S))

        {

            moving_forward_camera = true;

        }

        else

        {

            moving_forward_camera = false;

        }

        if (!Input.GetKey(KeyCode.W) & Input.GetKey(KeyCode.S))

        {

            moving_back_camera = true;

        }

        else

        {

            moving_back_camera = false;

        }

 

 

        if (Input.GetKey(KeyCode.A) & !Input.GetKey(KeyCode.D))

        {

            moving_l_camera = true;

        }

        else

        {

            moving_l_camera = false;

        }

 

        if (!Input.GetKey(KeyCode.A) & Input.GetKey(KeyCode.D))

        {

            moving_r_camera = true;

        }

        else

        {

            moving_r_camera = false;

        }

 

 

        if (Input.GetKey(KeyCode.Q) & !Input.GetKey(KeyCode.E))

        {

            rotation_cameri_l = true;

        }

        else

        {

            rotation_cameri_l = false;

        }

        if (!Input.GetKey(KeyCode.Q) & Input.GetKey(KeyCode.E))

        {

            rotation_cameri_r = true;

        }

        else

        {

            rotation_cameri_r = false;

        }

 

Для управления верхней камерой мы используем те же клавиши, что для камеры на корабле, поэтому сначала проверяем, существует ли камера сверху. Если нет, то на этом месте выходим из метода. Далее идут установки нужных переменных при нажатии игроком клавиш. A, D – перемещение влево и вправо. W, S – вперёд и назад. Q, E – повороты камеры.

Создадим метод для перемещения и поворотов камеры в зависимости от установленных переменных.

private void Moving_camera()

    {

        if (!Get_camera_top())

        {

            return;

        }

        if (moving_forward_camera)

        {

            camera_top.transform.Translate(0, 0, speed_movement_camera * Time.deltaTime, Space.Self);

        }

        if (moving_back_camera)

        {

            camera_top.transform.Translate(0, 0, -speed_movement_camera * Time.deltaTime, Space.Self);

        }

        if (moving_l_camera)

        {

            camera_top.transform.Translate(-speed_movement_camera * Time.deltaTime, 0, 0, Space.Self);

        }

        if (moving_r_camera)

        {

            camera_top.transform.Translate(speed_movement_camera * Time.deltaTime, 0, 0, Space.Self);

        }

        if (rotation_cameri_l)

        {

            camera_top.transform.Rotate(0, -speed_rotation_camera * Time.deltaTime, 0, Space.Self);

        }

        if (rotation_cameri_r)

        {

            camera_top.transform.Rotate(0, speed_rotation_camera * Time.deltaTime, 0, Space.Self);

        }

 

 

 

        if (camera_top.transform.position.x < min_X_Camera)

        {

            camera_top.transform.position = new Vector3(min_X_Camera, camera_top.transform.position.y, camera_top.transform.position.z);

        }

        if (camera_top.transform.position.x > max_X_Camera)

        {

            camera_top.transform.position = new Vector3(max_X_Camera, camera_top.transform.position.y, camera_top.transform.position.z);

        }

        if (camera_top.transform.position.z < min_Z_Camera)

        {

            camera_top.transform.position = new Vector3(camera_top.transform.position.x, camera_top.transform.position.y, min_Z_Camera);

        }

        if (camera_top.transform.position.z > max_Z_Camera)

        {

            camera_top.transform.position = new Vector3(camera_top.transform.position.x, camera_top.transform.position.y, max_Z_Camera);

        }

    }

Сначала проверяем наличие верхней камеры, и, если её нет, выходи из метода. Далее просматриваем все 6 переменных управления камерой и в зависимости от их значения перемещаем и поворачиваем камеру. Последние 4 условия нужны для проверки выхода камеры за пределы участка. Если значения по X или Z превысили заданные значения, возвращаем камеру в нужные границы. Поместите этот метод в Update() последним.

Перейдите в скрипт корабля игрока, и в метод Moving_camera() в самое начало добавьте такое условие.

 

if (link_on_Class.Get_camera_top())

        {

            return;

        }

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

На панели Инспектор под главным скриптом в пустое поле перенесите префаб пустышки с камерой ___Empl. Запустите игру. Теперь мы можем вести бой с высоты птичьего полёта (рис. 169).

Рис. 169.



Столкновение корабля с препятствиями.

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

В метод OnTriggerEnter на корабле игрока добавьте условие.

 

if (other.gameObject.tag == "land" | other.gameObject.tag == "ship_p" | other.gameObject.tag == "wall")

        {

            link_on_Class.Sink_ship();

        }

 

Из условия узнаём, если тег вошедшего в контакт с кораблём объекта, является тегом земли, корабля пиратов или стены, то из главного скрипта вызываем метод, который активирует все действия для потопления и завершает уровень. Также на корабль нужно добавить компонент Rigidbody (рис. 170). До этого момента мы могли обходится без него, так как те объекты, с которыми взаимодействовал корабль – водные пушки и ядра, сами имели такой компонент. Но на невидимых стенах и земле его нет.

Рис. 170.

Запустите игру и подплывите к границе участка или земле, корабль начнёт тонуть (рис. 171).

Рис. 171.



Ещё один корабль пиратов.

Давайте сделаем второй тип корабля пиратов и разместим на него побольше пушек. Он будет сражаться с игроком на втором уровне.

Перенесите на сцену 3D модель Ship_01_poly. Отмасштабируйте по всем осям с коэффициентом 110, поверните по оси Y на 180 градусов (рис. 172).

Рис. 172.

Создадим пустой объект, назовём ___Ship_Pirat_2 и расположим в том месте, где хотим сделать Pivot корабля (рис. 173).

Рис. 173.

Сделайте 3D модель дочерним объектом для пустышки.

Добавьте материалы по вашему вкусу, несколько компонентов Box Collider. Чтобы они перекрывали объем корабля, компонент Rigidbody с параметрами, как у первого пиратского корабля. Также присвойте тег ship_p, а на первый дочерний объект тег 2. Вспоминайте, что мы это делаем для идентификации типа пиратского корабля из скрипта. На первом корабле тег был 1. Добавьте два скрипта, такие же, как и на первый корабль пиратов (рис. 174).

Рис. 174.

С каждой стороны добавьте по три пушки (рис. 175). На первом корабле находилось всего 4 пушки с двух сторон. Также в скрипте Common_to_pirate_ships для второго типа кораблей у нас прописано 5 жизней, так что уничтожить его будет сложнее. Сделаем из объекта ___Ship_Pirat_2 префаб, и удалим копию со сцены.

Рис. 175.



Второй уровень.

У нас всё готово для создания второго уровня. В окне Проект сделайте дубликат первого уровня нажатием Ctrl+D и назовите Level_2. Откройте его. (рис. 176).

Рис. 176.

Удалите из пустышки ___All_Ship_Pirate единственный пиратский корабль. Добавьте на сцену 3D модель земли Land_2. Установите те же параметры для компонента трансформ, что и для первой земли. Продублируйте материал Land_1 и назовите Land_2. Измените в нём текстуру на Land_2. Наложите материал на новый ландшафт. Так же нужно добавить не землю компонент Mesh Collider и тег land (рис. 177).

Рис. 177.

Удалите со сцены первую землю. Добавьте корабль пиратов второго типа и сделайте его дочерним объектом пустого объекта ___All_Ship_Pirate (рис. 178).

Рис. 178.

Второй уровень готов, осталось добавить его в список всех сцен нашего проекта. Меню FileBuild Settings (рис. 178).

Рис. 179.

 

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



Готовая игра.

Всё, что нам осталось сделать, это создать билд готовой игры, проще говоря файл с разрешением exe, чтобы можно было его запускать на любом компьютере. Это очень просто. Откройте окно Меню FileBuild Settings. В нём кликните кнопку Player Settings (рис 180). Выберите любое изображение небольшого размера для иконки игры и закройте это окно.

Рис. 180.

В окне Build Settings кликните на кнопку Build и выберите папку для будущей игры. Всё готово. Осталось немного подождать, и Юнити сделает свою работу (рис. 181).

Рис. 181.

 

 

 

 

 

 

 

 

 

 

 

 

//контент
//контейнер