- Введение в программирование C# на Unity
- Установка среды разработки
- Вывод сообщения в консоль Unity
- Переменные
- Счетчик времени с помощью Time.deltaTime
- Позиция объекта
- Консоль
- Obsolete API Warnings and Automatic Updates
- Stack trace logging
- Вывод внутриигровых сообщений с помощью Particle System
- Задача
- Алгоритм решения
- Пошаговая реализация
- Система сообщений или “мягкая связь” между компонентами для Unity3D
- Введение
- Система сообщений на основе UnityEvents/UnityAction
- Классическая система C# на Event/Delegate
- Reflection система сообщений с идентификаций на string
- Reflection система сообщений с идентификаций на типах данных
Введение в программирование C# на Unity
Несмотря на то, что встроенные компоненты Unity могут быть очень разносторонними, вскоре ты обнаружишь, что тебе нужно выйти за пределы их возможностей, чтобы реализовать свои собственные особенности геймплея. Unity позволяет тебе создавать свои компоненты, используя скрипты. Они позволяют активировать игровые события, изменять параметры компонентов, и отвечать на действия Игрока каким тебе угодно способом.
Unity поддерживает следующие языки программирования: C# и Javascript. Языки C# и Javascript считаются одними из самых популярных среди программистов. На C# пишутся программы для компьютера, веб приложения, а Javascript активно используется для создания интернет-страниц. В нашем случае:
Установка среды разработки
Чтобы писать скрипты, необходим редактор кода. В комплекте с Unity идет MonoDevelop, так что его не требуется устанавливать отдельно. Другой вариант — использовать Visual Studio — мы рекомендуем ее. Сменить среду разработки можно в настройках: Edit → Preferences, а затем выбери панель External Tools.
Вывод сообщения в консоль Unity
Когда ты напишешь свой первый код и добавишь его в Unity, Unity проверит код и, если в нем нет ошибок, произойдет компиляция.
Консоль (англ. — Console) – это панель в Unity, которая отображает ошибки в коде (отображаются красным значком), предупреждения (отображается желтым значком) и позволяет выводить справочную информацию (белый значок). По умолчанию эта панель отображается в нижней части программы в закладке Console. Если эта панель не отображается на экране, то в главном меню выбери Window → Console.
Консоль Unity
Чтобы создать скрипт выбери Assets → Create → C# Script. Имя скрипта должно быть обязательно на английском и начинаться с большой буквы. Открой созданный скрипт и напиши:
Скрипт не будет работать до тех пор, пока экземпляр скрипта не будет присоединен к игровому объекту. Ты можешь прикрепить скрипт перетащив его на игровой объект в окно Иерархия или через окно Инспектор выбранного игрового объекта.
Код, который должен вызываться один раз при старте программы, ты можешь написать внутри функции Start(), как в примере выше.
Код, который должен вызываться каждый кадр, ты можешь написать внутри функции Update().
Переменные
Если ты хочешь сохранить данные (количество жизней игрока, патронов или очков опыта), то для этого используются переменные. Переменные разделяются на типы, которых существует огромное множество.
Переменная — поименованная область памяти, адрес которой можно использовать для осуществления доступа к данным и изменять значение в ходе выполнения программы. Переменные хранят в себе информацию разного типа, эти данные называют значением переменной. Для каждого типа данных есть свой тип переменной. Например:
- Целочисленные: int (от –2 147 483 648 до 2 147 483 647), long (от –9 223 372 036 854 775 808 до 9 223 372 036 854 775 807).
- Символы и строки: char (элемент в кодировке UTF-16), string (последовательность элементов в кодировке UTF-16).
- С плавающей запятой (дробные): float (от 1,5 × 10–45 до 3,4 × 1038, с точностью до 7 цифр), double (от 5,0 × 10–324 до 1,7 × 10308, с точностью до 15 цифр).
- Логические: bool (значение true или false).
Инициализация переменной
Все переменные в C# должны быть объявлены (инициализированы) до их применения. Например:
Задать значение переменной можно, в частности, с помощью оператора присваивания — знак «=». Кроме того, при объявлении переменной можно сразу задать ее значение. Например:
Переменные численных типов можно складывать, вычитать, умножать и делить:
+ — операция сложения;
— — операция вычитания;
* — операция умножения;
/ — операция деления.
А вот так сумму чисел можно вывести в консоль:
Счетчик времени с помощью Time.deltaTime
Практически во всех играх требуется знать сколько времени прошло с момента запуска игры (пример: игры на время). Но в Unity мы можем узнать только сколько прошло времени между кадрами (напоминаю, что игра – это набор кадров, которые быстро меняются. В одну секунду количество таких кадров может достигать 60 и больше).
Чтобы узнать сколько времени прошло между кадрами — используется Time.deltaTime.
Чтобы сделать дробное число целым, ставим перед ним (int).
Позиция объекта
Координаты объекта хранятся в компоненте Transform, свойство — position. Каждая координата имеет тип float. Вот так можно выводить координаты объекта в консоль:
Источник
Консоль
The Console Window (menu: Window > Console) shows errors, warnings and other messages generated by Unity. To aid with debugging, you can also show your own messages in the Console using the Debug.Log, Debug.LogWarning and Debug.LogError functions.
The toolbar of the console window has a number of options that affect how messages are displayed.
The Clear button removes any messages generated from your code but retains compiler errors. You can arrange for the console to be cleared automatically whenever you run the game by enabling the Clear On Play option.
You can also change the way messages are shown and updated in the console. The Collapse option shows only the first instance of an error message that keeps recurring. This is very useful for runtime errors, such as null references, that are sometimes generated identically on each frame update. The Error Pause option will cause playback to be paused whenever Debug.LogError is called from a script (but note that Debug.Log will not pause in this way). This can be handy when you want to freeze playback at a specific point in execution and inspect the scene.
Finally, there are two options for viewing additional information about errors. The Open Player Log and Open Editor Log items on the console tab menu access Unity’s log files which record details that may not be shown in the console. See the page about Log Files for further information.
Obsolete API Warnings and Automatic Updates
Among other messages, Unity shows warnings about the usage of obsolete API calls in your code. For example, Unity once had “shortcuts” in MonoBehaviour and other classes to access common component types. So, for example, you could access a Rigidbody on the object using code like:
These shortcuts have been deprecated, so you should now use code like:
When obsolete API calls are detected, Unity will show a warning message about them. When you double-click this message, Unity will attempt to upgrade the deprecated usage to the recommended equivalent automatically.
Stack trace logging
You can specify how accurate stack trace should be captured when log message is printed to the console or log file.
This is especially helpful when the error message is not very clear, by looking at the stack trace, you can deduct from what engine area the error appears. There are three options for logging stack trace:
- None — stack trace won’t be printed
- ScriptOnly — only managed stack trace will be printed
- Full — both native and managed stack trace will be printed, note — resolving full stack trace is an expensive operation and should be only used for debugging purposes
Источник
Вывод внутриигровых сообщений с помощью Particle System
Задача
При разработке нашей игры The Unliving, мы поставили перед собой задачу по отображению различных сообщений, таких, как нанесенный урон, нехватка здоровья или энергии, величина награды, количество восстановленных очков здоровья и т.д., с помощью Particle System. Это было решено сделать для того, чтобы получить больше возможностей для кастомизации эффектов появления и дальнейшего поведения таких сообщений, что проблематично при использовании стандартных элементов UI-системы Unity.
Кроме того, данный подход подразумевает использование всего лишь одного инстанса Particle System для каждого типа сообщений, что дает огромный прирост в производительности по сравнению с выводом этих же сообщений с помощью Unity UI.
Сообщение о величине урона
Текстовое сообщение о нехватке здоровья
Алгоритм решения
С помощью шейдера отображаем заранее подготовленную текстуру, используя правильные UV-координаты. Информацию с UV-координатами передаем двумя потоками (vertex streams) в ParticleSystem с помощью ParticleSystem.SetCustomParticleData в виде списка Vector4.
Наша реализация предполагает использование текстуры содержащей 10 строк и 10 столбцов символов. В качестве шрифта может быть использован любой моноширинный шрифт. Это необходимо во избежание разных интервалов между символами сообщения.
Пошаговая реализация
Создание Vector4 для передачи в Vertex Stream
Для описания набора символов будем использовать структуру SymbolsTextureData.
Массив chars необходим заполнить вручную, по порядку добавив в него все символы текстуры шрифта начиная с левого-верхнего угла.
В результате мы получим класс TextRendererParticleSystem. При вызове публичного метода SpawnParticle, будет происходить спаун одной частицы Particle System в нужную позицию, с нужным значением, цветом и размером.
Particle System в Unity позволяет передать кастомные данные в виде 2х потоков Vector4.
Мы намеренно добавили лишний поток с UV2, чтобы избежать сдвига по координатам потоков. Если этого не сделать, то координатам X и Y Custom1-вектора в C# будут соответствовать Z и W TEXCOORD0 шейдера. И соответственно, Custom1.z = TEXCOORD1.x, Custom1.w = TEXCOORD1.y. Что доставит много неудобств в дальнейшем.
Как было описано ранее, для передачи длины сообщения и UV-координат символов мы будем использовать два Vector4. Так как Vector4 содержит 4 элемента типа float, то по умолчанию мы можем упаковать в него 4 * 4 = 16 байт данных. Т.к. наше сообщение будет содержать только длину сообщения (двузначное число) и координаты символов (двузначное число для каждого символа), то диапазон типа byte (0-255) для нас избыточен. В то время как использование десятичных разрядов подойдет отлично.
Точность float составляет 6-9 символов, значит мы смело можем использовать 6 разрядов каждой координаты Vector4 и не переживать за целостность и точность данных. На самом деле, мы пробовали паковать 7, 8 и 9 символов, но точности float не хватает.
Получается, что в каждый float, используя десятичные разряды, мы упакуем целых 6 цифр, в отличии от стандартного варианта с четырьмя байтами. Итого, один Vector4 будет содержать 24 однозначных числа.
Мы можем передать в потоке 2 вектора, поэтому будем использовать оба для передачи сообщения длиной до 23 символов:
Custom1.xyzw — первые 12 символов сообщения.
Custom2.xyzw — еще 11 символов сообщения + длина сообщения (последние 2 символа).
Например, сообщение «Hello» будет выглядеть следующим образом.
Координатам символов соответствуют номер столбца и строка положения символа в текстуре.
В коде упаковка строки в два Vector4 будет выглядеть следующим образом:
Вектора с CustomData готовы. Пришло время вручную заспаунить новую частицу с нужными параметрами.
Первое, что мы должны сделать, убедиться, что CustomData потоки активированы в настройках Renderer системы частиц:
Для создания частицы воспользуемся методом Emit() класса ParticleSystem.
Добавим оба блока в метод SpawnParticle() и C# часть готова: сообщение упаковано и передано GPU в виде двух Vector4 в Vertex Stream. Осталось самое интересное — принять эти данные и правильно отобразить.
Создаем материал и назначаем ему наш шейдер. На сцене создаем объект с компонентом ParticleSystem, назначаем созданный материал. Затем настраиваем поведение частиц и отключаем параметр Play On Awake. Из любого класса вызываем метод RendererParticleSystem.SpawnParticle() или используем дебажный метод.
С исходным кодом, ресурсами и примером использования можно ознакомиться здесь.
Система вывода сообщений в действии
Вот и все. Вывод сообщений с помощью Particle System готов! Надеемся, это решение принесет пользу разработчикам игр на Unity.
UPD: Производительность предлагаемого решения
В комментариях у нескольких человек возник вопрос насчет производительности данного способа. Специально сделал замеры профайлером юнити. Условия одинаковые — 1000 движущихся, меняющих цвет объектов.
Результат использования стандартного UI (единственный канвас, на котором находятся только 1000 UI Text объектов):
Общее время построения кадра не менее 50мс, из них 40мс уходит на обновление канваса. При этом объекты даже не спаунятся, а просто движутся.
Результат спауна 1000 партиклов с помощью нашего решения:
Вся магия происходит на ГПУ, даже в момент спауна 1000 частиц кадр рассчитывается менее чем за 5мс.
Источник
Система сообщений или “мягкая связь” между компонентами для Unity3D
Введение
В данной статье будут затронуты темы, связанные с реализацией возможности “мягкой связи” компонентов игровой логики на основе системы сообщений при разработке игр на Unity3D.
Ни для кого не секрет, что в подавляющем большинстве случаев средств, которые предоставляет движок в базовом виде, недостаточно для полноценной реализации систем обмена данными между компонентами игры. В самом примитивном варианте, с которого все начинают, мы получаем информацию через экземпляр объекта. Получить этот экземпляр можно разными способами от ссылки на объект сцены, до функций Find. Это не удобно, делает код не гибким и заставляет программиста предусматривать множество нестандартных поведений логики: от “объект исчез из сцены”, до “объект не активен”. Помимо прочего может страдать и скорость работы написанного кода.
Прежде, чем приступить к рассмотрению способов решения проблемы, остановимся подробнее на ее предпосылках и базовых терминах, которые будут упомянуты ниже. Начнем с того, что подразумевается под “мягкой связью”. В данной случае — это обмен данными между компонентами игровой логики таким образом, чтобы эти компоненты абсолютно ничего не знали друг о друге. Исходя из этого, любые ссылки на объекты сцены или же поиск объекта в сцене по имени или типу дают нам “жесткую связь”. Если эти связи начнут выстраиваться в цепочки, то в случае необходимости изменения поведения логики программисту придется все перенастраивать заново. Как не сложно догадаться, гибкостью здесь и не пахнет. Конечно, можно написать расширение редактора, которое будет автоматически заполнять ссылки, но это не решит другую проблему – покомпонентное тестирование игровой логики.
Для того, чтобы написанное выше было более понятно, рассмотрим простой пример. В логике нашей сферической игры есть несколько компонентов: оружие, враг и пуля. Произведя выстрел из оружия, мы должны получить следующую информацию: попала или нет пуля во врага, если попала, то сколько нанесла врагу урона, если нанесла урон, то умер враг или нет. Помимо этого, мы должны передать часть этой информацию другим компонентам, таким как графический интерфейс, который отобразит нам количество нанесенного урона, количество здоровья у врага и количество боеприпасов в оружии. Сюда же можно отнести отображение соответствующих эффектов выстрела, попадания, а также анимации и т.п… Не трудно представить количество взаимосвязей и передаваемых данных. Реализовать это на “жесткой связи” можно, однако, что будет, если нам нужно протестировать логику пули, если при этом у нас еще нет оружия и нет врагов или же протестировать логику работы интерфейса, но у нас нет при этом ни логики оружия, ни врагов, ни пули, или мы захотели заменить пулю на ракету. Именно решением подобной проблемы и продиктована необходимость создания систем “мягкой связи”, которая позволит нам с легкостью имитировать различные компоненты, даже если их еще не существует, а также менять их без изменения, связанного с ними кода.
Остановимся более подробно на базовом принципе реализации “мягкой связи”. Как было сказано выше, чтобы “мягко” связать два компонента, мы должны передать данные от одного к другому, так, чтобы они не знали ничего друг о друге. Для того, чтобы это обеспечить, нам нужно получить данные не по запросу (имея на руках экземпляр объекта), а использовать механизм уведомлений. Фактически это означает, что при наступлении каких-либо событий в объекте/компоненте, мы не спрашиваем этот объект о его состоянии, объект сам уведомляет о том, что в нем произошли изменения. Набор таких уведомлений формирует интерфейс (не путать с interface в C#), с помощью которого игровая логика получает данные о нашем объекте. Наглядно это можно представить следующим образом:
Таким образом любой компонент системы через интерфейс уведомлений может получить необходимые данные об объекте игровой логике, при этом наличие самого объекта для тестирования связанных с ним компонентов необязательно, нам достаточно реализовать интерфейс, а затем подменить его на рабочий экземпляр.
Рассмотрим более подробно способы реализации описанного выше механизма, а также их плюсы и минусы.
Система сообщений на основе UnityEvents/UnityAction
Данная система появилось сравнительно недавно (в 5 версии движка Unity3D). Пример того, как реализовать простую систему сообщений можно посмотреть по этой ссылке.
Плюсы использования данного способа:
- Встроенная в Unity возможность.
Минусы:
- Встроенная в Unity возможность (не всегда родные системы лучше).
- Не гибко из-за использования UnityAction (хотя это можно обойти), который дает ограничение на количество параметров (четыре максимум).
- Не гибко из-за сложностей с изменением параметров сообщения, так как придется во многих местах кода менять типы, обработчики и т.п..
- Непонятно зачем использовать при наличии в C# event/delegate.
Классическая система C# на Event/Delegate
Самый простой и достаточно эффективный способ реализации связи компонентов на основе уведомлений — это использование пары event/delegate, которая является частью языка C# (подробнее можно почитать в статьях на habr’е или msdn).
Есть много разных вариантов реализации системы сообщений на основе event/delegate, некоторые из них можно найти на просторах интернета. Я приведу пример, на мой взгляд, наиболее удобной системы, однако для начала хочу упомянуть об одной важной детали, связанной с использованием event’ов. Если у события (event) нет подписчиков, то при вызове этого события произойдет ошибка, поэтому перед использованием обязательна проверка на null. Это не совсем удобно. Конечно можно написать обертку для каждого event, где будет проводиться проверка на null, но это еще более не удобно. Перейдем к реализации.
Как видно из примера, подписка происходит в методе OnEnable, а отписка в OnDisable и в данном случае она обязательна, иначе гарантирована утечка памяти и исключения по нулевой ссылке (null reference exception), если объект будет удален из игры. Саму подписку можно осуществлять в любой необходимый момент времени, это не обязательно делать только в OnEnable.
Легко заметить, что при таком подходе, мы можем без каких-либо проблем тестировать работу класса GUILogic, даже в отсутствии реальной логики GameLogicObject. Достаточно написать имитатор, использующий интерфейс уведомлений и использовать вызовы вида GameLogicObject.StartEvent ().
Какие плюсы дает нам данная реализация:
- Стандартный механизм языка C#, как итог 100% кроссплатформенность без танцев с бубнами.
- Простота реализации системы (без дополнительных вложений в код).
Минусы:
- Необходимо следить за памятью (отписка от уведомлений). При больших объемах кода, легко забыть отписаться от одного события и потом долго ловить баги.
- Необходимо отписываться от событий, если объект сцены деактивируется на время, в противном случае для него будет вызван обработчик.
- Не гибко из-за сложностей с изменением параметров сообщения, так как придется во многих местах кода менять типы, обработчики, вызовы и т.п..
- Необходимо учитывать, что событие может не иметь подписчиков.
Reflection система сообщений с идентификаций на string
Прежде чем приступить к описанию системы и ее реализации, хотелось бы остановиться на предпосылках, которые толкнули на ее создание. До прихода к этим мыслям в своих приложениях я использовал описанную выше систему на основе event/delegate. Те проекты, которые мне пришлось разрабатывать на тот момент, были относительно простые, в них требовалась скорость реализации, минимум багов на тестах, исключение по максимуму человеческого фактора в фазе разработки игровой логики. Исходя из этого, родился ряд некоторых требований относительно обмена данными между компонентами:
- Автоматическая подписка на события.
- Отсутствие необходимости следить за памятью (самоочистка системы).
- Отсутствие необходимости следить за подписчиками, система должна работать даже если их нет.
Итогом недолгих размышлений стало рождение идеи использовать рефлексию через атрибуты методов класса/компонента.
GlobalMessanger.MessageHandler – атрибут, который указывает нам, что метод является обработчиком события. Для того, чтобы определить тип события, которое данный метод обрабатывает, существует два способа (хотя на самом деле может быть и больше):
- Указывать типа события в параметрах атрибута:
- Использовать тип события в имени метода с префиксом “On” (или любым другим). Я использую именно этот способ, поскольку в 100% случаях, чтобы не путаться именую методы именно так.
Для того, чтобы осуществить подписку в автоматическом режиме, опять же, существует два способа:
- Использовать скрипт, которые будет осуществлять поиск всех компонентов на объекте, а затем через рефлексию искать в них методы с атрибутом. Для того, чтобы не добавлять этот скрипт руками, достаточно будет во всех компонентах, где это потребуется, проставить
Я лично считаю этот способ менее удобным, чем второй, поскольку требует лишних телодвижений. - Создание обертки над классом MonoBehaviour:
Как видно, данная обертка содержит в себе два метода, которые позволяют подписываться и отписываться от события (тип события забирается из имени метода). Они необходимы в случае, если нам нужно осуществить подписку на событие по ходу работы логики. Автоматическая подписка осуществляется в методе Awake. Отписка от событий осуществляется автоматически, но об этом чуть позже.
Класс GlobalMessanger является обычным компонентом Unity, доступ к которому осуществляется на основе Unity-синглетона. При этом, для этого компонента создается отдельный объект сцены, который существует только внутри нее и будет удален, когда сцена будет выгружена. Поскольку у нас события идентифицируются на основе строк, то информацию о событиях и подписчиках я решил хранить в хеш-таблице.
Как видно, при вызове события происходит проверка на существование объекта и на активность объекта. В первом случае, удаленный объект убирается из подписчиков, во втором же игнорируется при вызове методов обработки события. Таким образом, следить за отпиской событий у удаленного объекта не требуется, все осуществляется автоматически. При этом, если объект был временно деактивирован, то не осуществляется отписка от событий и повторная подписка, а также при вызове наличие подписчиков на событие не обязательно.
Легко заметить, что описанная выше система не представляет из себя ничего сверхординарного и не несет в себе откровений, однако она проста и удобна и хорошо подходит для относительно небольших проектов.
Думаю, сразу заметно насколько сократился код по сравнению с event/delegate, что меня лично радует.
Какие плюсы дает нам данная реализация:
- Автоматическая подписка на события.
- Нет необходимости следить за отпиской от событий (даже в случае ручной подписки).
- Относительно простая реализация.
- Удобное чтение кода, по атрибутам легко видно, кто является обработчиком событий и каких именно.
- Меньший объем кода по сравнению с event/delegate
- Нет необходимости думать о том, что у события нет подписчиков.
Минусы:
- Поскольку отписка от событий происходит в отложенном режиме, на очень больших проектах, возможно это будет забирать лишнюю память, но это легко решаемо за счет ручной отписки событий, аналогично тому как это было показано в разделе про event/delegate.
- Привязка к строкам, так как если захочется сделать имя события более красивым, придется менять его во многих местах.
- Нет гибкости по параметрам и типам данных событий, изменения требует многих действий по коду.
- Возможны проблемы с кроссплатформенностью из-за использования рефлексии.
Reflection система сообщений с идентификаций на типах данных
В предыдущем разделе была описана система более удобная (на мой взгляд) по сравнению с event/delegate, однако она все также имеет ряд недостатков, которые сильно влияют на гибкость нашего кода, поэтому следующим шагом было ее развитие с учетом этих факторов.
Итак, нам нужно сохранить все плюсы предыдущей системы, но сделать ее гибче и более устойчивой к возможным изменениям в игровой логике. Поскольку основная проблема — это изменения имени события и передаваемых параметров, то возникла идея идентифицировать события именно по ним. Фактически это означает, что любое событие, которое возникает в компоненте характеризуется ничем иным, как данными, которые оно передает. Поскольку мы не можем просто привязаться к стандартным типам (int, float и т.п.), потому что в логике такие данные могут передавать многие компоненты, то логичным шагом было сделать обертку над ними, которая была бы удобной, легко читаемой и однозначно интерпретирующей событие.
Как видно, у событий появились атрибуты. Это дает нам возможность получить отладочную информацию, в случае, если событие требует подписчика, а в коде его по каким-то причинам нет.
Метод вызова событий Call (и его перегрузки), который ранее у нас был частью класса GlobalMessanger, теперь является статическим и находится в GEvents.BaseEvent и принимает теперь в качестве параметра экземпляр класса, описывающего тип события.
Подписка и отписка на события, осуществляется тем же самым способом, что и ранее, через атрибуты методов, однако теперь идентификация типа события происходит не по строковому значению (имя метода или параметр атрибута), а по типу параметра метода (в примере это классы StartEvent, ChangeHealthEvent и DeathEvent).
Таким образом, используя описанную выше реализацию, мы получили максимально возможную гибкость в коде, поскольку теперь мы можем как угодно менять передаваемые данные в событиях без значительных затрат, нам достаточно изменить тело обработчика под новые параметры. В случае же, если мы захотим изменить имя события (имя класса), компилятор скажет нам в каких местах кода используется старый вариант. При этом необходимость менять имя метода обработчика полностью отпадает.
Я постарался описать в этой статье все возможные на мой субъективный взгляд способы построения систем обмена данными между компонентами на основе уведомлений. Все эти способы были использованы мной в разных проектах и разной сложности: от простых мобильных проектов, до сложных PC. Какую систему использовать в вашем проекте – решать только вам.
PS: я намеренно не стал описывать в статье построение системы сообщения на основе SendMessage-функций, поскольку по сравнению с остальными она не выдерживает критики не только по удобству, но и по скорости работы.
Источник