invoke unity с параметрами

Система сообщений или “мягкая связь” между компонентами для Unity3D

Введение

В данной статье будут затронуты темы, связанные с реализацией возможности “мягкой связи” компонентов игровой логики на основе системы сообщений при разработке игр на Unity3D.

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

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

Остановимся более подробно на базовом принципе реализации “мягкой связи”. Как было сказано выше, чтобы “мягко” связать два компонента, мы должны передать данные от одного к другому, так, чтобы они не знали ничего друг о друге. Для того, чтобы это обеспечить, нам нужно получить данные не по запросу (имея на руках экземпляр объекта), а использовать механизм уведомлений. Фактически это означает, что при наступлении каких-либо событий в объекте/компоненте, мы не спрашиваем этот объект о его состоянии, объект сам уведомляет о том, что в нем произошли изменения. Набор таких уведомлений формирует интерфейс (не путать с interface в C#), с помощью которого игровая логика получает данные о нашем объекте. Наглядно это можно представить следующим образом:

invoke unity с параметрами. Смотреть фото invoke unity с параметрами. Смотреть картинку invoke unity с параметрами. Картинка про invoke unity с параметрами. Фото invoke unity с параметрами

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

Рассмотрим более подробно способы реализации описанного выше механизма, а также их плюсы и минусы.

Система сообщений на основе UnityEvents/UnityAction

Данная система появилось сравнительно недавно (в 5 версии движка Unity3D). Пример того, как реализовать простую систему сообщений можно посмотреть по этой ссылке.

Плюсы использования данного способа:

Классическая система 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 ().

Какие плюсы дает нам данная реализация:

Reflection система сообщений с идентификаций на string

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

GlobalMessanger.MessageHandler – атрибут, который указывает нам, что метод является обработчиком события. Для того, чтобы определить тип события, которое данный метод обрабатывает, существует два способа (хотя на самом деле может быть и больше):

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

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

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

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

Думаю, сразу заметно насколько сократился код по сравнению с event/delegate, что меня лично радует.

Какие плюсы дает нам данная реализация:

Reflection система сообщений с идентификаций на типах данных

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

Итак, нам нужно сохранить все плюсы предыдущей системы, но сделать ее гибче и более устойчивой к возможным изменениям в игровой логике. Поскольку основная проблема — это изменения имени события и передаваемых параметров, то возникла идея идентифицировать события именно по ним. Фактически это означает, что любое событие, которое возникает в компоненте характеризуется ничем иным, как данными, которые оно передает. Поскольку мы не можем просто привязаться к стандартным типам (int, float и т.п.), потому что в логике такие данные могут передавать многие компоненты, то логичным шагом было сделать обертку над ними, которая была бы удобной, легко читаемой и однозначно интерпретирующей событие.

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

Метод вызова событий Call (и его перегрузки), который ранее у нас был частью класса GlobalMessanger, теперь является статическим и находится в GEvents.BaseEvent и принимает теперь в качестве параметра экземпляр класса, описывающего тип события.

Подписка и отписка на события, осуществляется тем же самым способом, что и ранее, через атрибуты методов, однако теперь идентификация типа события происходит не по строковому значению (имя метода или параметр атрибута), а по типу параметра метода (в примере это классы StartEvent, ChangeHealthEvent и DeathEvent).

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

Я постарался описать в этой статье все возможные на мой субъективный взгляд способы построения систем обмена данными между компонентами на основе уведомлений. Все эти способы были использованы мной в разных проектах и разной сложности: от простых мобильных проектов, до сложных PC. Какую систему использовать в вашем проекте – решать только вам.

PS: я намеренно не стал описывать в статье построение системы сообщения на основе SendMessage-функций, поскольку по сравнению с остальными она не выдерживает критики не только по удобству, но и по скорости работы.

Источник

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

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

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

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

Кроме того, Вы-то наверняка пользуетесь одним из наиболее известных механизмов, предоставляемых средствами C#. Это хороший выбор — но, быть может, Вам будет интересно узнать и о возможностях встроенной в Unity системы и её собственных «плюшках»?

Ликбез, или «Будильник против таймера»

(если Вы знакомы с тем, что такое игровые события — спокойно пропускайте этот раздел)

invoke unity с параметрами. Смотреть фото invoke unity с параметрами. Смотреть картинку invoke unity с параметрами. Картинка про invoke unity с параметрами. Фото invoke unity с параметрамиБольшинство современных игровых движков чем-то схожи с обычными механическими часами. Объекты движка — шестерёнки большого часового механизма игрового мира. И, как часовой механизм функционирует за счёт того, что шестерёнки хитро соединены между собой, так жизнь и движение игрового мира осуществляются за счёт взаимодействии объектов друг с другом.

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

Рассмотрим пример из мира реального. Представим себе, что мы голодны, и ставим по этому поводу на огонь вариться большую кастрюлю пельменей. Алгоритм сего действа прост — закинуть пачку пельменей в горячую подсолёную воду, включить огонь, отойти на десять минут (посидеть в интернете пол-часика), снять угольки пельмени с огня.

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

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

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

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

В свою очередь, первый способ — способ регулярного активного обращения для получения каких-то сведений — называется в литературе polling (от англ. «poll» — опрос). Вызывающий событие объект называется отправителем, воспринимающий его объект — получателем, процесс слежения за событием называется прослушиванием (listen), а функция, вызывающаяся у получателя по получению события зовётся обработчиком.

Часы? Пельмени? Ась?!

invoke unity с параметрами. Смотреть фото invoke unity с параметрами. Смотреть картинку invoke unity с параметрами. Картинка про invoke unity с параметрами. Фото invoke unity с параметрами

Хорошо-хорошо, другой пример. Смотрели второго «Шрека»?

Вспомните момент, когда трое героев едут в карете в Далёкое-Далёкое Королевство. Замечательная юморитическая сценка, где непоседливый Осёл достаёт героев, регулярно интересуясь:

-Мы уже приехали?
-Нет.
-… мы уже приехали?
-Нет, не приехали!
-… мы уже приехали?
-Неееееет!

То, чем занимается Осёл — это хороший пример polling-а в чистом виде. Как видите, это здорово утомляет 🙂
Остальные герои, в свою очередь — условно — подписаны на событие «приезд», и потому попусту не тратят системного времени, ресурсов, а также не требуют дополнительно усилий программиста на реализацию добавочной жёсткой связи между объектами.

Секрет, который у всех на виду

Итак, как же реализовать события на Unity? Давайте рассмотрим на простом примере. Создадим в новой сцене два объекта — мы хотим, чтобы один генерировал сообщение по щелчку мыши на нём (или мимо него, или не мыши вообще), а второй — реагировал на событие и, скажем, перекрашиваться. Обзовём объекты соответствующе и создадим нужные скрипты — Sender (генерирующий событие) и Receiver (принимающий событие):

invoke unity с параметрами. Смотреть фото invoke unity с параметрами. Смотреть картинку invoke unity с параметрами. Картинка про invoke unity с параметрами. Фото invoke unity с параметрами

Код скрипта Sender.js:

Особое внимание обратите на строку:

public var testEvent: Events.UnityEvent = new Events.UnityEvent ();

И код скрипта Receiver.js:

Теперь посмотрим на объект Sender, и увидим:

invoke unity с параметрами. Смотреть фото invoke unity с параметрами. Смотреть картинку invoke unity с параметрами. Картинка про invoke unity с параметрами. Фото invoke unity с параметрами

Ага! Наше событие, объявленное как публичная переменная — объект типа UnityEvent — не замедлило показаться в редакторе свойств. Правда, пока оно выглядит одиноким и потерянным, так как ни один из объектов на него не подписан. Исправим этот недочёт, указав Receiver как слушающий событие объект, а в качестве вызываемого по получению события обработчика — нашу функцию Recolor():

invoke unity с параметрами. Смотреть фото invoke unity с параметрами. Смотреть картинку invoke unity с параметрами. Картинка про invoke unity с параметрами. Фото invoke unity с параметрами

Запустим, нажмём пару раз любую кнопку… есть, результат достигнут — объект послушно перекрашивается в самые неожиданные цвета:

invoke unity с параметрами. Смотреть фото invoke unity с параметрами. Смотреть картинку invoke unity с параметрами. Картинка про invoke unity с параметрами. Фото invoke unity с параметрами

«Эй! Стоп-стоп! Но мы же этим уже пользовались, когда работали с Unity UI!» — скажете Вы… и окажетесь неожиданно правы.

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

Но давайте двигаться дальше!

«Добавить второго получателя? Нельзя, всё вы врёте!»

Отлично, работает. Работает! Но из зала уже доносятся голоса людей, запустивших среду, потестировавших, и начинающих уличать автора в… неточностях. И правда — после недолгого изучения, пытливый читатель выясняет, что редактор Unity разрешает связать событие только с одним экземпляром Receiver, и начинает ругаться. Второй экземпляр просто некуда оказывается вписать — поле принимающего сигнал объекта не предусматривает множественности записей.
(upd: как выяснилось позднее, на самом деле предусматривает, см. комментарий)

И это провал, казалось бы.

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

Синтаксис команды для этого прост:

Если же, забегая вперёд, есть желание по ходу исполнения кода перестать слушать событие, то поможет функция:

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

Но есть нюанс — экземпляр события хранится в Sender, а Receiver о нём ничего не знает. Что же делать? Не тащить же указатель на Sender внутрь Receiver-а?

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

Итак, решение: событие, как любую уважающую себя переменную, можно без особых проблем сделать… статическим!

Изменим код Sender следующим образом:

public static var testEvent: Events.UnityEvent = new Events.UnityEvent ();

Переменная закономерно пропала из редактора, поскольку теперь она не принадлежит ни одному экземпляру класса. Зато теперь она доступна глобально. Чтобы её использовать, изменим код Receiver:

Размножим объект Receiver и запустим игру:

invoke unity с параметрами. Смотреть фото invoke unity с параметрами. Смотреть картинку invoke unity с параметрами. Картинка про invoke unity с параметрами. Фото invoke unity с параметрами

Да, теперь ансамбль Receiver-ов дружно перекрашивается в разные цвета на любое нажатие клавиши. Но что же мы сделали внутри скрипта?

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

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

Бонусный уровень — удаление объекта в обработчике

Казалось бы, всё элементарно — в том же Recolor теперь вызываем Destroy(this.gameObject) — и дело сделано? Попробуем, и.

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

Если удаление обрабатывающего событие объекта в обработчике мешает дальнейшей обработке — то давайте и не будем объект удалять в обработчике. Удалим его в сопрограмме, выполняющейся после обработки события — для чего движок, кстати, имеет стандартный метод. Будем удалять объект при помощи команды Destroy(this.gameObject, 0.0001). Удаление произойдёт, но не сразу, а будет отложено на 0.0001 секунды. Невооружённым глазом этой паузы не заметить, а вот процесс обработки события не запнётся на объекте и спокойно продолжится дальше.

Передача параметров

Иногда бывает нужно передать событие, сопроводив его какой-то характеристикой произошедшего — радиусом, уроном, количеством, подтипом, матерным комментарием игрока, и так далее. Для этих целей придуманы разновидности UnityEvent с числом аргументов от одного до четырёх. Их практическое применение не сложнее рассмотренного выше.

Работает — вот несколько склеенных в один кадров:

invoke unity с параметрами. Смотреть фото invoke unity с параметрами. Смотреть картинку invoke unity с параметрами. Картинка про invoke unity с параметрами. Фото invoke unity с параметрами

Как видите, ничего особо хитрого делать не надо.

Практическое применение системы событий

Часто бывает так, что объекту нужно постоянно иметь информацию о том, случилось ли определённое действие, или выполнено ли определённое условие. Сидящий в засаде монстр проверяет — пересёк ли кто-нибудь из персонажей черту, за которой его разрешено видеть и атаковать? Венерианские комары проверяют — не включился ли фумигатор, во время работы которого им положено с визгами разлететься во все стороны? Бесплотный и неосязаемый триггер на карте проверяет — уничтожил ли уже игрок десяток порученных ему по квесту сусликов?

Конечно, предложенные примеры предлагают весьма примитивную реализацию. Комаров, например, можно загнать при рождении в массив специального /менедежера комаров/, а у игрока хранить указатель на менеджер комаров. При включении фумигатора будет вызываться метод менеджера, который пройдёт по массиву и вызовет у каждого комара метод «пугайся-и-улетай»… но лишние менеджеры — оно нам так уж и надо?

Чем UnityEvent лучше альтернативных вариантов? И, кстати, а что с производительностью?

invoke unity с параметрами. Смотреть фото invoke unity с параметрами. Смотреть картинку invoke unity с параметрами. Картинка про invoke unity с параметрами. Фото invoke unity с параметрамиЕсли Вы не первый год работаете в Unity, то наверняка уже как-то реализовывали в своих проектах события. Теперь Вы знаете про ещё один механизм событий в Unity. Чем же лучше UnityEvent аналогичных средств, например, из числа стандартного функционала C#?

Есть разработчики, которые пишут на JavaScript. Да-да, именно для них, в первую очередь, в статье код и приводится на этом легковесном языке. По понятным причинам, средства, предоставляемые C#, им недоступны. Для них UnityEvent — достойный и удобный механизм, доступный «из коробки» — бери и пользуйся.

Удобно также то, что UnityEvent — один и тот же для кода на JS и на C#. Событие может создаваться в коде C# и слушаться кодом на JS — что, опять же, невозможно напрямую со стандартными делегатами C#. Следовательно, этими событиями можно без зазрения совести связать фрагменты кода на разных языках, буде такое непотребство завелось у Вас в проекте (например, после закупки в Unity Store особо важного и сложного ассета).

Анализ быстродействия событий разных типов в Unity показал, что UnityEvent может быть от 2 до 40 раз медленнее (в части затрат времени на вызов обработчика), чем та же система делегатов C#. Что же, разница не столь уж велика, поскольку это всё равно ОЧЕНЬ БЫСТРО, если не злоупотреблять.

Заключение

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

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

События — мощный механизм в ваших руках. Никогда не забывайте о его существовании. Применяйте его с умом — и разработка Ваших проектов станет быстрее, приятнее и успешнее.

Ссылки

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

Вот наиболее интересные из них, а также просто полезные ссылки по теме:

Источник

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *