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

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

Ссылка на его блог: http://catlikecoding.com/

Ссылка где можно отблагодарить: https://www.patreon.com/catlikecoding

Вот что пишет автор на странице https://www.patreon.com/catlikecoding

Привет! Я Jasper Flick с сайта Catlike Coding. Я делаю высококачественные уроки, которые позволят Вам узнать все о программировании с использованием языка C# в Unity. Мои уроки поданы в текстовом формате потому что считаю, что текст — это лучшее средство для данной тематики (для раскрытия данной темы). Так же они содержат набольшие вставки (картинки, анимации) которые, как я считаю, добавляют ценности.

Я в Patreon с Мая 2014 года и в настоящее время выпускаю по 2 урока в месяц. Сейчас я работаю над двумя разделами, шестиугольная карта (Hex Map) и рендеринг (Rendering).

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

Не хотите заморачиваться с Patreon? Всегда можно сделать единовременное пожертвование

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

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

  • Создадим часы из простых объектов
  • Напишем скрипты на C#
  • Заставим стрелки часов вращаться для отображения времени
  • Добавим плавности

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

Данный урок сделан с использованием Unity версии 2017.1.0.

Пришло время создать часы

Пришло время создать часы

1. Создадим часы

Откройте Unity и создайте новый 3D проект. Вам не понадобятся какие-либо уникальные ассеты с готовыми компонентами, аналитика вам тоже не понадобится, потому ее можно просто отключить. Если вы еще не настроили редактор, то у Вас будет установлено расположение окон по умолчанию.

_Вариант расположения окон по умолчанию_

Вариант расположения окон по умолчанию

Я использую другой вариант 2 к 3 (2 by 3) который вы можете выбрать в правом верхнем углу редактора. Изменим не много окно Project нажав на кнопку в правом верхнем углу и в выпадающем меню выбрав One Column Layout. Так же советую отключить показ координатной сетки Show Grid во вкладке Scene. Этот пункт можно найти в выпадающем меню на вкладке Scene

_Измененное расположение окон_

Измененное расположение окон

Почему окно с игрой маленькое и черными границами по краям?

Такое может произойти есть вы используете монитор с большим разрешением и с нестандартным соотношением сторон. Для того чтобы избавиться от этого стоит на вкладке Game нажать на кнопку, которая находится рядом с Display 1. В выпавшем меню нужно убрать галку Low Resolution Aspect Ratios и установить Free Aspect.

Low resolution aspect ratios отключена

1.1 Создадим игровой объект (Game Object)

По умолчанию в сцене присутствуют 2 игровых объекта. Эти объекты находятся во вкладке Hierarchy и именно их иконки вы можете наблюдать во вкладке Scene. Первое это Main Camera, используется для отображения всех объектов на сцене. Окно Game отображается все что попадает в поле зрения камеры. Второй это Directional Light этот компонент добавляет базовый свет на сцену (он как солнце освещает всю сцену равномерно из одной точки).

Создадим собственный игровой объект выбрав в строке меню GameObject / CreateEmpty. Так же подобное вы можете сделать просто, нажав правой клавишей мыши на свободное место в окне Hierarchy. Давайте дадим только что добавленному объекту имя, для этого нажмите ПКМ на только что добавленном объекте и выберете Rename. Поскольку мы создаем часы, то назовем объект соответствующе - Clock.

Вкладка Hierarchy с объектом Clock

Познакомимся с окном Inspector. В этом окне отображаются компоненты, которые есть у нашего игрового объекта. При выборе нашего объекта Clock он будет содержать заголовок с именем объекта и параметрами конфигурации. По умолчанию объект включен, не является статическим, не имеет тега и принадлежит слою Default. Эти настройки отлично подойдут нам. Ниже приведен список всех компонентов игрового объекта. На любом созданном объекте всегда присутствует компонент Transform.

Вкладка Inspector с выбранными часами

Компонент Transform хранит в себе позицию, углы поворота, масштаб в 3D пространстве. Убедитесь, что координаты и угол поворота у наших часов равен 0, а масштаб 1.

Что насчет 2D объектов?

При работе в 2D вместо 3D игнорируется одно из трех измерений. Объекты, специально предназначенные для 2D-подобных элементов пользовательского интерфейса, обычно имеют Rect Transform, который является специализированным компонентом Transform для 2D объектов.

1.2 Создадим основу для часов.

Хоть у нас и есть объект часы, на сцене мы не видим ничего. Хорошо, давайте добавим на сцену 3D модель, которая будет отображаться на нашей сцене. В Unity «из-под коробки» есть несколько примитивных объектов, которые мы можем использовать для того чтобы создать наши часы. Давайте начнем. Добавим на нашу сцену цилиндр нажав на GameObject/3DObject/Cylinder. Убедитесь, что он имеет те же значения Transform что и объект Clock.

Игровой объект, представляющий собой цилиндр

Наш только что добавленный объект имеет несколько незнакомых нам компонентов. Первый это Mesh Filter, этот компонент просто содержит в себе ссылку на Mesh (сетку) цилиндра. Второй это Capsule Collider, который предназначен для взаимодействия объектов. Без этого компонента все объекты будут просто проходить насквозь друг через друга и никак не взаимодействовать между собой. Третий компонент это Mesh Renderer. Этот компонент обеспечивает визуализацию нашего объекта, путем наложения текстуры на сетку. Он так же контролирует какой материал вы используете для отрисовки, по умолчанию устанавливается Default-Material, но вы всегда можете его изменить в соответствующем компоненте, который расположен ниже Mesh Renderer.

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

Наш объект без коллайдера

Для того чтобы превратить наш цилиндр в нечто похожее на часы, необходимо сделать циферблат, путем сплющивания нашего цилиндра. Это делается уменьшением значения Scale по оси Y до 0,1, а X, Z увеличением на 10.

Растянутый цилиндр

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

Face является дочерним к объекту Clock.

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

1.3 Создание циферблата

Теперь сделаем градуировку на нашем циферблате, по которой можно будет ориентироваться во времени или по-другому контур часов. Добавим объект Куб на сцену кликнув GameObject / 3D Object / Cube. Изменим масштаб на (0.5, 0.2, 1) сделав его длинным узким блоком похожий на брусок. Установим ему позицию (0, 0.2, 4). Это положение соответствует 12 часам. Назовите его Hour Indicator.

Отметка 12 часов

Пока нашу отметку очень сложно увидеть, потому что она сливается с циферблатом. Для того чтобы изменить текущее положение дел нужно нажать Assets / Create / Material и создать новый материал или сделать аналогичные действия в окне Project. Это создаст нам материал, который дублирует материал по умолчанию. Изменим Albedo на что-то более темное, установив значение 73 для красного, зеленого и синего. В результате мы получим темно-серый материал. Давайте дадим ему соответствующее имя – Clock dark.

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

Что за альбедо такое?

Альбедо происходит от латинского слова белизна. Это просто цвет материала.

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

Перетащите этот материал на нашу 12 часовую отметку или просто переместите наш материал в ячейку Element 0 компонента Mesh Renderer.

Отметка 12 часов темно-серого цвета

Наша отметка корректно показывает 12 часов, но что если мы хотим показать 1 час на часах? Все 12 часов располагаются на круге 360°, разделив 360° на 12 отметок получим 30°. Получается, что каждую отметку нам нужно будет поворачивать на 30° по оси Y. Давайте сделаем это.

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

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

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

Временный родитель

Теперь установим у родителя значение поворота по оси Y равное 30°. Таким образом наша отметка будет вращаться вокруг пустого объекта родителя, а это именно то что нам нужно.

Правильное положение часовой отметки

Продублируйте нашего временного родителя используя сочетанием клавиш Ctrl+D или воспользовавшись контекстным меню во вкладке hierarchy. В каждом дубликате увеличьте значение поворота по оси Y на 30°. Сделайте подобным образом все 12 отметок.

Циферблат со всеми часовыми отметками

Нам больше не понадобятся временные родители. Выберите все отметки часов использовав Ctrl+ЛКМ и сделайте их дочерними к нашим часам. Теперь объясню, как такое получилось. Если перед тем как сделать объект дочерним к часам вы посмотрите на компонент Transform у отметки, то вы заметите что у всех объектов одинаковое значения как координат, так и углов поворота. А после того как вы поменяете у него родителя, то эта значения изменятся. Это произошла трансформация из локальной системы координат в глобальную. Раньше наши отметки получали координаты и угол поворота от временного родителя, сейчас же их координаты преобразовались в локальные координаты для объекта Clock сохранив углы поворота.

Получившаяся иерархия

У меня значения координат равно 90,0000001. Это правильно?

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

1.4 Создадим стрелки часов

Мы можем использовать тот же подход чтобы создать стрелки часов. Создайте новый куб и назовите его Arm, сразу перетяните на него созданный нами материал и удалите коллайдер. Установите масштаб равный (0.3, 0.2, 2.5) таким образом сделав его уже и длиннее чем наша отметка. Установите координаты (0, 0.2, 0.75) при такой установке стрелка будет направлена в сторону 12 часов, а другая сторона стрелки будет не много смещена относительно центра часов. Со стороны это выглядит как не большой противовес.

Часовая стрелка

Куда делась иконка с солнцем?

Я переместил свет не много в сторону чтобы он больше не загромождал сцену. Directional Light освещается сцену равномерно независимо от его положения.

Чтобы заставить нашу стрелку правильно вращаться, создайте родительский пустой объект для него по аналогии с отметками. Убедитесь, что родитель имеет координаты и углы поворота равные 0, а масштаб равный 1. Проверить необходимо, потому что вращать стрелку мы будет чуть позже. Сделайте стрелку дочерним к пустому объекту, назовите родительский объект Hours Arm, после сделайте его дочерним к часам. Теперь стрелка является как бы «внуком» часов.

Колонка hierarchy после всех действий

Продублируйте Hours Arm дважды для создания минутной и секундной стрелки. Переименуйте их в Minutes Arm и Seconds Arm соответственно. Минутная стрелка должная быть длиннее и уже, чем часовая, для этого проставим значения масштаба равные (0.2, 0.15, 4) и координат равные (0, 0.375, 1). Таким образом наша стрелка будет расположена выше часовой стрелки.

Для секундной стрелки установим значения для масштаба равные (0.1, 0.1, 5) и координат (0, 0.5, 1.25). Выделим нашу секундную стрелку на общем фоне путем создания нового материала красного цвета со значением Albedo равным (197,0,0) и применим этот материал на нашей стрелке.

Часы со всеми стрелками

Наши часы полностью готовы. Если вы до этого не создавали и не сохраняли вашу сцену, то самое время это сделать, нажав File / Save Scene имя сцены выбирайте сами. Я назвал ее просто Scene.

Прим. От переводчика: сцены в Unity это как отдельные уровни одной большой игры. У вас может быть несколько сцен, на которых будут разные уровни. После завершения одного вы можете запускать другой, обеспечивая тем самым смену локаций. Заведите себе привычку при создании любого проекта первым делом создать сцену. Сохраняйте эту сцену каждые 5-10 минут нажатием клавиш Ctrl+S. Это не раз вас спасет если вы работаете не на самом мощном компьютере.

Сохраненная сцена

Если вы застряли, хотите сравнить или пропустить блок с созданием часов, то вы можете скачать сделанный мною вариант. Вы можете присоединить мой вариант к своему проекту через вкладку Assets / Import Package / Custom Package. Далее нажмите Import и запустите мою сцену.

2. Анимируем часы

Замечательно! Часы есть. Запустим сцену нажав на кнопку Play. Если у вас камера направлена прямо на часы, то вы заметите что часы не идут. Нажмем снова на Play, остановим игру и проясним ситуацию. На данный момент мы просто создали иерархию объектов, которые просто расположены на сцене. Ничего более. Если бы в Unity был уже готовый компонент, который мы могли бы прикрепить к часам, и он начал бы сразу бы работать, было бы круто, но такого компонента нет. Потому нам придется его создать самим. Компоненты создаются с помощью скриптов. Добавим новый скрипт нажав _ Assets / Create / C# Script_ и назвав его Clock.

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

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

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Clock : MonoBehaviour {
	// Use this for initialization
	void Start () {		
	}
	// Update is called once per frame
	void Update () {	
	}
}

Этот код написан на языке C#. Это язык программирования используется в Unity для создания скриптов. Для того чтобы более подробно разобраться с тем как работает код, удалим все что нам сгенерировал Unity с начнем сначала.

А что насчет JavaScript?

Unity так же поддерживает другой язык программирования, обычно называемый JavaScript, но его фактическое название – UnityScript. Unity 2017.1.0 до сих пор поддерживает его, но в версии Unity 2017.2.0 из меню для добавления компонентов была убрана возможность создать JavaScript. Как бы можно точно сказать, что поддержка этого языка полностью прекращена в Unity.

2.1 Введение в скриптинг

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

В C# мы определяем тип Clock начиная с определения класса. Нужно явно указать какую структуру мы будем использовать для определения наших часов. Строки кода в которых что-то изменилось будут закоментированы. Когда мы начинаем работу с полностью пустым файлом первое что мы должны объявить это class Clock. При желании можете поиграться со знаками табуляции нажав пробел или _Tab,

сlass Clock

Что такое класс с технической точки зрения?

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

Классы и структуры — это, по сути, шаблоны, по которым можно создавать объекты. Каждый объект содержит данные и методы, манипулирующие этими данными.

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

Поскольку мы не хотим ограничивать доступ к нашему классу, проставим модификатор доступа к Clock. Это делается путем подставленные префикса public перед определением класса.

public сlass Clock

Что такое модификатор доступа по умолчанию для классов?

Если мы не проставим самостоятельно модификатор доступа, то будет выбран модификатор по умолчанию ` internal class Clock`. Это ограничивает доступ к коду из одной сборки, что становится актуальным при использовании кода, упакованного в несколько DLL файлов.

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

public сlass Clock {}

Теперь наш скрипт скомпилируется. Нажмите Ctrl+S для того чтобы сохранить изменения в файле и вернитесь в окно редактора Unity. Unity «увидела» что файл был изменен и сразу перекомпилировала его. Давайте выберем наш скрипт. И в инспекторе мы можем увидеть надпись, которой до этого не было, которая говорит нам о том, что наш класс не содержит MonoBehaviour.

Отсутствует MonoBehaviour

Это означает что мы не можем использовать этот скрипт для создания компонента в Unity. На данном этапе наш класс Clock определяет универсальный тип объекта в C#. Для того чтобы наш компонент мог что-либо сделать (в данном случае нам нужно заставить стрелки часов вращаться) нужно реализовать методы, которые есть в классе MonoBehaviour.

Что за MonoBehaviour?

Идея заключается в том, что мы можем программировать наши собственные компоненты, чтобы добавить пользовательского поведения (от англ. Behaviour) для наших игровых объектов. Приставка Mono была добавлена в следствии того что ранее Unity использовал проекты типа mono что является кроссплатформенным расширением файлов в .NET framework. Потому и MonoBehaviour. Это старое название, которое пришло к нам из первых версий Unity. Узнать о Mono подробнее можно тут.

Чтобы превратить Clock в подтип MonoBehaviour мы должны явно это указать через знак двоеточия. Это позволяет нашим Clock унаследовать весь функционал ` MonoBehaviour `.

public сlass Clock : MonoBehaviour {}

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

public сlass Clock : UnityEngine.MonoBehaviour {}

Что такое пространство имен?

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

Поскольку неудобно всегда использовать префикс UnityEngine, мы можем сказать компилятору чтобы он выполнял поиск в этом пространстве имен. Это делается путем добавления строки using UnityEngine; в заголовок скрипта. Знак точки с запятой : здесь очень важен потому что он явно говорит компилятору где именно закончилась команда.

using UnityEngine;
public сlass Clock : MonoBehaviour {}

Ну теперь наконец-то мы можем прикрепить наш компонент к объекту. Это можно легко сделать просто, перетащив наш скрипт на нужный нам объект, либо нажать на объекте кнопку Add Component, которая находится во вкладке Inpector.

Объект Clock с прикрепленным к нему скриптом

Теперь у нас создан экземпляр объекта C#, использующий наш класс Clock в качестве шаблона. В результате чего мы видим неизменяемое поле Script со значением Clock.

2.2 Получим стрелки часов

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

Hours transform будет подходящим именем для нашего поля. Для того чтобы Unity мог правильно распарсить имена наших переменных, то стоит их называть в соответствии со стилем написания составных слов, при котором несколько слов пишутся слитно без пробелов, при этом каждое слово внутри фразы пишется с заглавной буквы - CamelCase. В нашем случае будет правильно назвать наше поле hoursTransform.

public сlass Clock : MonoBehaviour 
{
	hoursTransform;
}

Куда делась строка с using?

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

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

Transform hoursTransform;

Наш класс теперь определяет поле, которое должно содержать ссылку на объект, тип которого должен быть Transform. Нам нужно убедиться, что мы передаем ссылку на компонент Transform объекта Hours Arm.

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

public Transform hoursTransform; 

Разве публичные поля это не плохо?

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

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

Поле Hours Transform типа Transform

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

Ссылка на компонент Transform объекта Hours Arms передана

2.3 Получим все стрелки часов

Сделаем то же самое для минутной и секундной стрелки.

public Transform hoursTransform;
public Transform minutesTransform;
public Transform secondsTransform;

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

public Transform hoursTransform, minutesTransform, secondsTransform;
// public Transform minutesTransform;
// public Transform secondsTransform;

Что означает //?

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

Теперь передайте ссылки на наши объекты так же как мы сделали это с часовой стрелкой.

Ссылки на все объекты переданы

2.4 Какой сейчас час?

Теперь, когда мы явно указали ссылки на все объекты с которыми мы хотим что-либо сделать, мы можем перейти к следующему шагу – узнать который сейчас час. Чтобы сделать это мы должны прописать код, который будет делать то что нам нужно. Это делается путем добавления метода в наш класс Clock. Метод этот должен определенное имя Awake и название такое он имеет потому что мы предполагаем, что наш код должен выполнится, как только запустится наша сцена.

public class Clock : MonoBehaviour {
	public Transform hoursTransform, 
	minutesTransform, 
	secondsTransform;
	Awake {}
}

Методы несколько похожи на математические функции, для примера . Эта функция берет некоторое число, умножает его на 2 и прибавляет 3. Она берет одно число на вход и возвращает тоже одно число. В случае метода это больше похоже на , где представляет собой входные параметры, а это выполнение кода. Такое объяснение может показаться слишком абстрактным, потому что не понятно какой результат выполнения нашей функции. Но на самом деле наша функция может и не возвращать никакого результата, она может просто выполнять код, который прописан внутри метода и все. Для того чтобы создать такой метод, то перед именем метода должен стоять префикс void (в дословном переводе «пустой»).

void Awake {}

Отлично. Но есть еще одна загвоздка. Компилятор ожидает что наш метод может принимать на вход какие-либо параметры. Но в нашем случае метод не принимает на вход никаких параметров и мы обязательно должны это указать поставив круглые скобки после имени нашего метода ().

void Awake() {}

Теперь у нас есть валидный метод, который ничего не делает. Как бы Unity увидит наши поля, которые прописаны выше, увидит Awake метод и сделает то что написано в {} скобках. Если в нашем скрипте присутствует метод Awake и этот скрипт наследуется от ` MonoBehaviour`, то фрагмент кода написанный внутри этого метода выполняется сразу, как только компонент содержащий наш скрипт будет создан или загружен на сцену.

Почему у нашего метода Awake нет модификатора доступа?

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

Чтобы проверить как именно это работает давайте заставим нашу программу выводить сообщение в консоль. Как раз класс UnityEngine.Debug имеет публичный метод Log, который поможет решить нашу задачу. Давайте просто выведем простой текст в консоль. Текст, который мы хотим вывести обернем в “”. И не забудьте про ;, которая вспомните сами для чего нам нужна :)

void Awake()
{
	Debug.Log(Test);
}

Нажмите на кнопку Play в редакторе. И вы увидите, как текст, написанный нами, отобразится во вкладке Console. Либо же вы можете нажать Window / Console. В консоль может выводится вся информация, связанная с работой самой программы и кода, который вы написали. Если вы допустите ошибку при написании, то Unity выбросит вам исключение, которое отобразится в консоли.

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

void Awake()
{
	Debug.Log(Time.time);
}

Что такое свойство?

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

После запуска мы увидим в консоли цифру 0. Это произошло потому что Time.time возвращает время, которое прошло со времени запуска сцены. А поскольку, как мы говорили ранее, вывод в консоль происходит в момент инициализации компонента, а наш компонент инициализируется во время запуска сцены, а это значит, что времени с момента запуска сцены прошло ровно 0.

Для того чтобы получить доступ к системному времени компьютера, на котором мы работаем, мы можем использовать структуру DateTime. Данная структура не относится к Unity и находится в пространстве имен System. Эта структура является частью .Net framework.

Что такое структура?

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

Для разрешения подобных затруднений в C# предусмотрена структура, которая подобна классу, но относится к типу значения, а не к ссылочному типу данных. Т.е. структуры отличаются от классов тем, как они сохраняются в памяти и как к ним осуществляется доступ (классы — это ссылочные типы, размещаемые в куче, структуры — типы значений, размещаемые в стеке), а также некоторыми свойствами (например, структуры не поддерживают наследование). Из соображений производительности вы будете использовать структуры для небольших типов данных. Однако в отношении синтаксиса структуры очень похожи на классы.

DateTime имеет публичное свойство Now. Оно создает значение DateTime, которое содержит текущую системную дату и время. Let’s code it.

using System;
using UnityEngine;

public class Clock : MonoBehaviour {
	public Transform hoursTransform, minutesTransform, secondsTransform;
	void Awake () 
{
		Debug.Log(DateTime.Now);
	}
}

Теперь каждый раз при запуске нашей сцены у нас в консоли прописывается время, когда мы входим в Play mode.

2.5 Поворачиваем наши стрелки

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

void Awake () 
{
Debug.Log(DateTime.Now.Hour);
}

Мы можем использовать это свойство для предания вращения нашим стрелкам. Вращение объектов в Unity описывается при помощи кватернионов (quaternions). Вызовем публичный метод Quaternion.Euler. В качестве аргументов данный метод принимает углы поворота по осям X, Y и Z и возвращает он соответствующий этим аргументам кватернион.

void Awake () 
{
//Debug.Log(DateTime.Now.Hour);
Quaternion.Euler(0, DateTime.Now.Hour, 0);
}

Что такое кватернион?

Кватернионы базируются на комплексных числах и используются для описания поворота объекта в 3D пространстве. Их труднее понять, чем простые 3D векторы, но они имею ряд полезных свойств. Например, они не будут повержены эффекту складывания рамок

UnityEngine.Quaternion используется как простое значение. Это структура, не класс.

Все три аргумента это действительные числа, которые представлены в C# как числа с плавающей точкой. Чтобы явно объявить, что мы передаем методу числа с плавающей точкой, нужно к каждому нулю добавить суффикс f.

Quaternion.Euler(0f, DateTime.Now.Hour, 0f);

На часах наши отметки расположены по кругу через каждый 30°. Чтобы наше вращение стрелок было правильным мы должны умножить текущее значение часов на 30.

Quaternion.Euler(0f, DateTime.Now.Hour * 30f, 0f);

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

float degreesPerHour = 30f;
public Transform hoursTransform, minutesTransform, secondsTransform;

void Awake ()
{
	Quaternion.Euler(0f, DateTime.Now.Hour * degreesPerHour, 0f);
}

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

const float degreesPerHour = 30f;

Что особенного в константах?

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

Отлично, у нас есть строка, которая описывает то, как должен вращаться наш объект. Но если мы запустим сцену, то увидим, что ничего не произошло. Дело все в том, что мы не указали что именно мы хотим вращать. А вращать мы хотим часовую стрелку и вращать мы ее будем относительно ее центра координат. У нашего поля hoursTransform есть свойство, которое описывает поворот нашего объекта относительно центра координат localRotation и необходимо этому свойству присвоить значение, полученное нами выше. Как бы поворачиваем нашу часовую стрелку относительно начала координат на кватернион полученный из текущего значения часов, умноженные на угловой интервал расположения часовых отметок.

void Awake ()
{
hoursTransform.localRotation =
		Quaternion.Euler(0f, DateTime.Now.Hour * degreesPerHour, 0f);
}

Часовая стрелка показывает 4 часа

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

const float
	degreesPerHour = 30f,
	degreesPerMinute = 6f,
	degreesPerSecond = 6f;

	public Transform hoursTransform, minutesTransform, secondsTransform;

	void Awake () {
		hoursTransform.localRotation =
			Quaternion.Euler(0f, DateTime.Now.Hour * degreesPerHour, 0f);
		minutesTransform.localRotation =
			Quaternion.Euler(0f, DateTime.Now.Minute * degreesPerMinute, 0f);
		secondsTransform.localRotation =
			Quaternion.Euler(0f, DateTime.Now.Second * degreesPerSecond, 0f);
	}

Часы показывают 16:29:06

Теперь взглянем на наш код внимательно и увидим, что у нас DateTime.Now используется трижды для часов, минут и секунд. Чисто теоретически из-за того, что наше свойство вызывается трижды в разных строчках кода, то может возникнуть коллизия, связанная с задержкой выполнения какой-либо из строчки кода. Например, каким-то неведанным образом после того как выполнится ` DateTime.Now.Minute ` произойдет задержка в пару секунд, и следующая строка выполнится позже, а если это еще произойдет на 59 секунде, то наша минутная стрелка целую секунду будет показывать неправильное время. Для того чтобы такого не случилось нужно чтобы при каждом вызове нашего метода ` Quaternion.Euler значение времени было одинаковым независимо от того, когда выполнится строчка кода, которая отвечает за поворот стрелки. Для этого объявим внутри метода Awake` переменную в которую поместим текущее время.

void Awake ()
 {
	DateTime time = DateTime.Now;
	hoursTransform.localRotation =
		Quaternion.Euler(0f, time.Hour * degreesPerHour, 0f);
	minutesTransform.localRotation =
		Quaternion.Euler(0f, time.Minute * degreesPerMinute, 0f);
	secondsTransform.localRotation =
		Quaternion.Euler(0f, time.Second * degreesPerSecond, 0f);
}

2.6 «Заводим» наши часы

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

Для того чтобы завести наши часы, нужно чтобы наш код выполнялся не в момент инициализации компонента, а каждый кадр. Но подождите, что нам придется опять писать много сложно кода чтобы заставить наши часы отображать время? – Нет, в Unity есть готовая реализация как метода Awake так и метода, который вызывается каждый кадр и выполняет весь код что написан внутри него. Метод этот называется Update. И для того чтобы все заработало достаточно будет заменить Awake на Update.

void Update ()
{
	DateTime time = DateTime.Now;
	hoursTransform.localRotation =
		Quaternion.Euler(0f, time.Hour * degreesPerHour, 0f);
	minutesTransform.localRotation =
		Quaternion.Euler(0f, time.Minute * degreesPerMinute, 0f);
	secondsTransform.localRotation =
		Quaternion.Euler(0f, time.Second * degreesPerSecond, 0f);
}

Часы наконец-то показывают актуальное время

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

Компонент с включенным переключателем

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

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

public Transform hoursTransform, minutesTransform, secondsTransform;

public bool continuous;

Булева переменная может иметь всего 2 значения true или false (истина или ложь), которые соответствуют нашем случае положению включено и выключено. По умолчанию переменная типа bool имеет значение false, поэтому вернитесь в окно Unity и поставьте галочку.

Плавное вращение стрелок включено

Теперь нам нужно написать две разных реализации вращения стрелок. Но сначала не много подготовимся к этому. Продублируйте метод Update со всем имеющимся в нем кодом. Теперь у нас есть 2 метода Update. Переименуем их. Один назовем UpdateContinuous, второй UpdateDiscrete.

void UpdateContinuous () 
{
		DateTime time = DateTime.Now;
		hoursTransform.localRotation =
			Quaternion.Euler(0f, time.Hour * degreesPerHour, 0f);
		minutesTransform.localRotation =
			Quaternion.Euler(0f, time.Minute * degreesPerMinute, 0f);
		secondsTransform.localRotation =
			Quaternion.Euler(0f, time.Second * degreesPerSecond, 0f);
}

void UpdateDiscrete () 
{
		DateTime time = DateTime.Now;
		hoursTransform.localRotation =
			Quaternion.Euler(0f, time.Hour * degreesPerHour, 0f);
		minutesTransform.localRotation =
			Quaternion.Euler(0f, time.Minute * degreesPerMinute, 0f);
		secondsTransform.localRotation =
			Quaternion.Euler(0f, time.Second * degreesPerSecond, 0f);
}

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

void Update () 
{
	if (continuous) 
{
		UpdateContinuous();
	}
	else 
{
		UpdateDiscrete();
	}
}

Где может быть объявлен метод Update?

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

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

Класс DateTime не содержит в себе удобных дробных данных. Но к нашему счастью у него есть свойство TimeOfDay, которое содержит в себе значение типа TimeSpan, которое содержит в себе данные в нужном нам формате. А именно TotalHours, TotalMinutes и TotalSeconds.

void UpdateContinuous () 
{
	TimeSpan time = DateTime.Now.TimeOfDay;
	hoursTransform.localRotation =
		Quaternion.Euler(0f, time.TotalHours * degreesPerHour, 0f);
	minutesTransform.localRotation =
		Quaternion.Euler(0f, time.TotalMinutes * degreesPerMinute, 0f);
	secondsTransform.localRotation =
		Quaternion.Euler(0f, time.TotalSeconds * degreesPerSecond, 0f);
}

Кажется, что на этом все, но если мы сохраним наши изменения и вернемся в окно редактора, то увидим в консоли ошибку компиляции. Произошло это потому что наши значения аргументов имеют тип double. Это тот же самый тип float только хранить он в себе может в 2 раза больше значащих цифр после запятой. Ну и что? – Скажете вы. А дело все в том, что Unity работает только с типом float и нам придется под это подстроиться.

А достаточно ли точности в этом типе?

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

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

hoursTransform.localRotation =
		Quaternion.Euler(0f, (float)time.TotalHours * degreesPerHour, 0f);
minutesTransform.localRotation =
		Quaternion.Euler(0f, (float)time.TotalMinutes * degreesPerMinute, 0f);
secondsTransform.localRotation =
		Quaternion.Euler(0f, (float)time.TotalSeconds * degreesPerSecond, 0f);

Часы с плавным вращением стрелок

Скачать вариант сделаный автором можно по ссылке: 1.unitypackage

Скачать вариант сделанный переводчиком можно по ссылке: 2.unitypackage

На этом все. Спасибо за внимание.