Чем PVS-Studio может помочь инди-разработчикам
В процессе инди-разработки любой энтузиаст или даже целая группа сталкивается с одной серьёзной проблемой — с необходимостью поиска ошибок в коде игры. Ресурсы больших компаний на тестирование и долгую отладку недоступны абсолютному большинству инди-разработчиков или даже инди-студий. И как раз с этим может помочь широкий список инструментов QA кода. Среди которых немаловажным является статический анализ кода. О нем и пойдёт речь в этой статье. А конкретно, хотелось бы рассказать о том, как PVS-Studio может помочь инди-разработчикам со сложным процессом поиска ошибок.
Начнем с краткого описания того, что такое статический анализ. Это процесс выявления ошибок или недочетов и подозрительных мест в исходном коде программ. Самой очевидной аналогией для этого процесса будет код-ревью. Только вместо вашего коллеги код проверяет программа, которая не устанет и у которой не «замылится» глаз.
PVS-Studio, в свою очередь, это как раз статический анализатор, который ищет ошибки в исходном коде программ, написанных на C, C++, C# и Java. Кроме поиска ошибок PVS-Studio имеет диагностики, которые подсчитывают некоторые метрики кода, отлавливает использование bad practices (например, нарушение «The Law of The Big Two»: V690)
Как это выглядит на практике
Для наглядности приведу две вариации одной и той же ошибки, деления на ноль, на языке C++.
Найти деление на ноль может быть очень просто, если выглядеть оно будет вот так:
int x = 123;
int div = 3;
int y = x / (3 - div);
Хотя инструментарий самой Visual Studio молчит на таком простейшем примере:
PVS-Studio легко обнаруживает эту ошибку:
В результате исполнения такого кода мы получим неопределённое поведение. На этот счёт в нашем мануале есть даже отдельная заметка.
Но абсолютно такая же по своей сути ошибка может выглядеть и таким образом:
template <class T> class numeric_limits {
....
}
namespace boost {
....
}
namespace boost {
namespace hash_detail {
template <class T> void dsizet(size_t x) {
size_t length = x / (limits<int>::digits - 31); // <=
}
}
}
И такое деление на ноль также будет обнаружено анализатором: V609 Divide by zero. Denominator 'limits <int>::digits — 31' == 0. ConsoleApplication.cpp 12
Кто-то может подумать, что разработка игр это не rocket science. Будет там ошибка, ну и ничего страшного -если найдётся, поправим. Но когда несколько дней пытаешься отловить баг в коде, который был написан неделю/месяц/год назад, отношение к ошибкам становится уже не таким легкомысленным. И чем дольше ошибка сохраняется в продукте, тем дороже она стоит, а уж если она достигнет пользователя, то можно заработать не только бессонные ночи дебага, но и репутационные потери.
Статический анализ как раз-таки и позволяет находить ошибки на этом самом начальном этапе разработки, когда программист ещё пишет код. Подход, при котором анализатор запускается один раз в неделю/месяц/год, сводит это преимущество статического анализатора к нулю, ведь многие ошибки уже будут найдены и поправлены долгим и дорогим дебагом и тестированием.
Далее приведу несколько примеров ошибок, которые PVS-Studio обнаружил в исходном коде достаточно известных open-source игровых проектов. Ведь никто не застрахован от ошибок.
Пример № 1 (C++)
Эта ошибка пряталась в исходном коде «Bullet Physics SDK», который использовался при разработке «Red Dead Redemption», а также для разработки спецэффектов в «Шерлоке Холмсе» Гая Ричи.
Более того, эта ошибка приводила к реальному багу в работе движка, из-за неё силы могли применяться к объектам не с той стороны. Взглянем на ошибку:
struct eAeroModel
{
enum _
{
V_Point,
V_TwoSided,
....
END
};
};
void btSoftBody::addAeroForceToNode(....)
{
....
if (....)
{
if (btSoftBody::eAeroModel::V_TwoSided)
{
....
}
....
}
....
}
V768 The enumeration constant 'V_TwoSided' is used as a variable of a Boolean-type. btSoftBody.cpp 542
Итак, в этом участке кода забыли произвести сравнение со значением перечисления. Вместо этого проверялось само значение перечисления V_TwoSided.
Правильный код будет выглядеть следующим образом:
if (m_cfg.aeromodel == btSoftBody::eAeroModel::V_TwoSided)
{
....
}
Почитать подробнее об ошибках, найденных в «Bullet Physics SDK» с помощью PVS-Studio можно в этой статье.
Пример № 2 (С++)
Абсолютно аналогичную ошибку я описывала в своей недавней статье «Amnesia: The Dark Descent или как забыть поправить копипасту». Разработчики упомянутой серии игр как раз перед выходом новой части «Amnesia: Rebirth» выложили на GitHub исходный код двух первых частей.
Перейдём к самой ошибке:
void cLuxEnemyMover::UpdateMoveAnimation(float afTimeStep)
{
....
if(prevMoveState != mMoveState)
{
....
//Backward
if(mMoveState == eLuxEnemyMoveState_Backward)
{
....
}
....
//Walking
else if(mMoveState == eLuxEnemyMoveState_Walking)
{
bool bSync = prevMoveState == eLuxEnemyMoveState_Running
|| eLuxEnemyMoveState_Jogging
? true : false;
....
}
....
}
}
На этот код и его продолжение (else-if продолжаются и дальше) анализатор выдал несколько похожих предупреждений вида:
V768 The enumeration constant 'eLuxEnemyMoveState_Jogging' is used as a variable of a Boolean-type. LuxEnemyMover.cpp 672
Это предупреждение выдавалось на следующую строку:
bool bSync = prevMoveState == eLuxEnemyMoveState_Running
|| eLuxEnemyMoveState_Jogging
? true : false;
В этом случае потерялось сравнение значения переменной prevMoveState с элементом перечисления eLuxEnemyMoveState_Jogging. В оригинале это все было еще и записано в одну строку.
Скорее всего программист просто писал код по ходу своих мыслей и написал выражение так, как и думал: «prevMoveState должно быть равно eLuxEnemyMoveState_Running или eLuxEnemyMoveState_Jogging». Ну и к тому же, условный тернарный оператор тут лишний, выражение и так является логическим и вернёт или true или false.
Пример №3 (Java)
Следующая ошибка была найдена PVS-Studio в исходном коде клиент-серверного приложения XMage для легендарной «Magic: The Gathering».
public final class DragonsMaze extends ExpansionSet {
....
private List<CardInfo> savedSpecialRares = new ArrayList<>();
....
@Override
public List<CardInfo> getSpecialRare() {
if (savedSpecialRares == null) { // <=
CardCriteria criteria = new CardCriteria();
criteria.setCodes("GTC").name("Breeding Pool");
savedSpecialRares.addAll(....); // <=
criteria = new CardCriteria();
criteria.setCodes("GTC").name("Godless Shrine");
savedSpecialRares.addAll(....);
....
}
return new ArrayList<>(savedSpecialRares);
}
}
V6008 Null dereference of 'savedSpecialRares'. DragonsMaze.java (230)
Тут происходит разыменовывание нулевой ссылки savedSpecialRares. Скорее всего подразумевалось, что если этот контейнер пуст, то при вызове функции в него нужно добавить определённые редкие карты. Более того, savedSpecialRares изначально объявляется пустой коллекцией и нигде больше не переприсваивается. Поэтому значение null для неё попросту невозможно. В итоге этот метод всегда будет возвращать пустую коллекцию, никогда не выполняя условия savedSpecialRares == null.
Наиболее вероятный вариант исправления этой ошибки:
if (savedSpecialRares.isEmpty()) {
....
}
Эта ошибка рассматривалась в статье «Проверка кода XMage и почему недоступны специальные редкие карточки для коллекции Dragon’s Maze». И как раз из-за этой ошибки в этой игре возможно не удастся получить те самые редкие карточки.
Пример № 4 (C#)
И приведу еще один пример, скорее всего влияющий на логику работы игры. Эта ошибка была допущена в популярном музыкальном таппере «osu!».
protected override void CheckForResult(....)
{
....
ApplyResult(r =>
{
if (holdNote.hasBroken
&& (result == HitResult.Perfect || result == HitResult.Perfect))
result = HitResult.Good;
....
});
}
V3001 There are identical sub-expressions 'result == HitResult.Perfect' to the left and to the right of the '||' operator. DrawableHoldNote.cs 266
Судя по условию, если зажимание «ноты» прервалось и результат был Perfect, то надо снизить результат до Good. Но почему-то результат дважды проверяется на Perfect. Заглянем в возможные значения этого перечисления:
public enum HitResult
{
None,
Miss,
Meh,
Ok,
Good,
Great,
Perfect,
}
Видно, что между оценкой Good и Perfect, есть еще такая оценка, как Great. Возможно, вместо второй проверки на Perfect должна была быть именно проверка на Great. В таком случае логика присвоения промежуточных результатов работает неверно.
Эта ошибка освещалась в статье «В 'osu!' играй, про ошибки не забывай». Мне стала интересна дальнейшая судьба этого кода, и я полезла искать его в репозитории. Сначала не вышло найти, где этот код был исправлен, так как содержимое всего этого файла было полностью отрефакторено.
Потом я нашла вот этот коммит, который разбивал логику этого кода на еще пару файлов. В один из этих файлов и был вынесен код с ошибкой.
Покопавшись в истории уже нового файла, я в итоге нашла коммит который, наконец, исправлял этот код. Причем исправление было капитальным:
// If the head wasn't hit or the hold note was broken,
// cap the max score to Meh.
if ( result > HitResult.Meh
&& (!holdNote.Head.IsHit || holdNote.HasBroken))
result = HitResult.Meh;
Здесь при прерывании или пропуске «ноты», которую надо зажать, оценка не просто снижается, а вообще сводится к самой низкой. Возможно, разработчики решили кардинально изменить логику. Но не менее вероятно, что исходный код был ошибочным и, если бы разработчики «osu!» использовали PVS-Studio, то анализатор указал бы им на место с такой подозрительной логикой и они могли бы исправить его гораздо раньше.
Как использовать PVS-Studio бесплатно
Ну и перейдём к самому интересному. Как же использовать PVS-Studio бесплатно. Есть несколько вариантов. Возможно один из них подойдёт и Вам.
Возможность бесплатно использовать анализатор предоставляются следующим категориям:
- Открытые проекты;
- Закрытые проекты;
- Эксперты безопасности;
- Microsoft MVP.
Вторая категория из этого списка может использовать PVS-Studio бесплатно с помощью добавления в проект специальных комментариев и ей могут воспользоваться:
- Студенты и преподаватели;
- Индивидуальные разработчики;
- Открытые бесплатные проекты.
В этом случае разработчику просто будет нужно оставить специальный комментарий для PVS-Studio в своём коде. Для остальных категорий предоставляется бесплатный лицензионный ключ.
Думаю, читателя может смутить, что открытым проектам выдаётся бесплатный ключ, и в то же время они могут воспользоваться вариантом с комментариями в своём исходном коде. Такая «путаница» возникла из-за того, что новые категории и варианты бесплатного использования появились не сразу. В итоге вариант с комментариями для открытых проектов остался с более ранних времен. Мы решили не убирать такую возможность, и теперь разработчики открытых проектов могут сами выбрать подходящий для себя вариант.
Подробнее о деталях, как использовать PVS-Studio бесплатно, можно прочитать вот в этой статье из нашего блога.
Заключение
Как видите, статический анализ и конкретно PVS-Studio способны находить реальные ошибки в исходном коде программ. Ошибки, которые я привела в этой статье спокойно жили в кодовой базе своих проектов и скорее всего приводили к неправильному поведению этих программ. А уж сколько времени (а время — это, как известно, деньги) было потрачено разработчиками на локализацию и исправление ошибок, которые легко было бы отловить анализатором, страшно представить. Избежать этого можно было бы очень просто, причем даже в сам момент написания этого кода, с помощью как раз-таки регулярного использования статического анализа.
В нашем корпоративном блоге вы можете найти другие наши статьи про проверку игровых проектов, перейдя по тегу #GameDev.
- 16 ноября 2020, 11:50
- 04
Он не проверяет GML! :yak:
Зато проверяет C#. Интересно, насколько трудоёмко добавить интеграцию с Unity.
Через Visual Studio Unity проекты без проблем проверяются ╰(▔∀▔)╯
У вас есть GMCheck. Во всяком случае, будет, я верю.
У них
Да, пока у "вас", а не у "нас".
У кого?!
У тех, кто делает игры на GM.
Спасибо за пост! Цикл статей про PVS-Studio на Хабре в своё время вдохновил на хобби-проект статического анализатора для Game Maker.
Тут наверняка будут комментарии «почему всегда в примерах всякие скучные баги, которые вылазят раз в сто лет» — превентивно подчеркну тезис из заключения, что это «ошибка выжившего бага», потому что все более важные и интересные до этого были отловлены заведомо более трудоёмким, чем регулярный запуск утилиты, ручным тестированием и отладкой.
Ну, по сути, анализатор нужен будет только один раз - перед релизом запустить и посмотреть все места, на которые он ругается. Если вы не переписываете код каждый месяц.
Реально полезен будет только для больших проектов, там - да, критичный баг может стоить несколько сотен тысяч $. А для бесплатных небольших игр - ну есть баг и есть.
На сайте у вас чтобы узнать цены, надо сначала написать. Уже настораживает и намекает что цена кусается. Нормальные люди не стесняются писать цены, потому что они у них не конские.
Это говорит человек которому цена $100 на GameMaker - дорого. При этом да, она написана на официальном сайте, но этот же человек скажет что она конская. Самопротиворечие во все поля.
Не конская. А на данный момент экономически невыгодная, потому как я ни одной игры на нем не сделал, тем более платной. И не факт что собираюсь вообще.
А лицензии и подешевле есть.
Правильное использование анализатора, с наибольшей отдачей — это запускать его регулярно. В идеале — встроить в среду разработки. Кстати, это и к обычным ошибкам компиляции относится (по сути, компилятор подразумевает некоторый статический анализ); если редактор их сразу красненьким подчёркивает, жить становится лучше и веселей.
Для индивидуальных разработчиков лицензия бесплатная, вроде написано.
Анализатор багов не нужен. Я уже две игры выпустил на XBOX и прекрасно без анализатора багов. А где ваши игры с использованеим анализатора багов?
Сразу видно человека, который ни разу ничего более менее большого не разрабатывал. Даже самый мелкий баг может привести к переделке довольно большого куска кода. А после этого снова придётся запускать анализатор.
Вот именно, я ничего большого не разрабатывал. И мне не придется из-за мелкого бага переделывать большой кусок кода.
Разрабы-одиночки ничего большого и не делают. Анализатор больше нужен большим компаниям, где много кода и какая-нибудь такая опечатка может сделать забагованным какой-нибудь второстепенный квест. Тогда от него толку будет намного больше.
А так - ну будет один квест в инди-игре забагованным. Ну и что? Игру же не 1000 человек делала и не просят за нее 50$ на релизе.
Делают, но долго и маленькое.
Есть такое. Но я не хочу пилить игру в одно рыло 3 года подряд. Да еще и бросать игру 4-5 раз.
Зато я теперь понимаю тех кто уходит на Epic Games Store, портирует сразу на мобилки/консоли, Apple Store и т. д.
Эти люди уже давно продали машину / квартиру / жену и меньше всего им хочется через 3 года разработки сесть в лужу.
Так пойди на кикстартер!
Ага! Чтобы потом еще 2 года пилить игру на деньги с кикстартера?
По крайней мере это можно будет делать, не думая о заработках.
значит делай одну игру(можно даже чужую) и по пути продай ее три раза. В процессе потестируй всё, чего интересно за счет издателя.
А скольк ты хочешь бросать это чисто твое решение
Ооооооо =) Ну это ты зря так думаешь.
Под понятием "большой" я подразумевал любую игру, которая ушла дальше двухдневного прототипа. Даже в маленьких играх есть где ошибиться.
В маленьких играх в случае ошибки не так много придется переписывать.
Бывает, что всю игру приходится переписывать XD
Вот только не надо мне свои проблемы портирования GM => GM:S пихать.
Надо было сразу игру на GM:S делать когда его еще не выпустили.
Всё зависит от ошибки, которую допустил. У меня особо таких проблем нет как ты описываешь. Пока что все свои проекты я успешно портировал на GMS2 и доводил до релизов.
Не бывает такого. Все игры уже изначально готовы, мы их просто вынимаем из ноосферы и пытаемся обрамить какими-то там кодами на каких-то там языках. Как правило игры от этого очень много теряют и оказываются чем-то сомнительным, похожим на что-то совсем не из ноосферы.
Геймдев это геймдев, а программирование это программирование.
А если по теме, то я не только разрабатываю, но и издаю инди-игры десятка других инди-разработчиков. Для использования вашего проекта нужна интеграция с языком GML / средой GameMaker, но клиентура очень узкая, не думаю что это будет финансово целесообразно.
Геймдев = геймплей + графика + программирование + звук + интерфейс
Понятно почему такие как вы выбираете Game Maker. Вы не любите программировать.
А потом еще обижаетесь на "Game Maker - это конструктор".
Игры прекрасно можно делать без программирования, достаточно просто нажимать правильные кнопки.
А программирование - это и есть "нажимание правильных кнопок" на клавиатуре.
Задумайся, может и нет никакого программирования и ты только зря его пугаешься?
Ты, ведь, по сути, тоже программируешь игру, но на "языке" интерфейса GM.
В теории, Game Maker - это просто очередная парадигма программирования. Как ФП, ДП, ООП и т.д.
Айфон - это просто очередная парадигма телефонии. *Осень - пора покупать новый айфон!*
GMP.
Да нет, можно набирать символы мышью. Или вообще голосом.
Конечно нет. Но оно есть.
<Xitilon, 14:38> Невозможно сделать игру без программирования.
<Xitilon, 15.04> Игры прекрасно можно делать без программирования.
Сказано же - либо "прекрасно", либо "невозможно".
Выбирай какой вариант тебе лучше подходит.
Чистая правда. Лучшая игра это задуманная, но несделанная. Заканчивать разработку игр без программирования нельзя.
А с программированием - можно?
Выходит, все мои игры можно закончить?
Чисто теоретически. Но лучше этого не делать, потому что все законченные игры рискуют резко оказаться отстоем.
Тогда что? Лучше не заканчивать игры? Или все хорошие игры - незаконченные?
С той позиции, что хорошие игры всегда хочется чем-то дополнить, или продолжить, или чтоб была вторая такая - да, они незаконченные. Может и есть какая-то игра, которая абсолютно закончена и не вызывает мыслей о том чтоб её сделать лучше. Но я таких не знаю.
Можно привести такую аналогию: игра - это душа, а билд - это тело. Прогрессия жанра и игровых механик - это реинкарнация души в следующих телах, видоизменённых эволюцией. Точнее это две параллельно проходящие эволюции, одна - игр/душ, другая - тел/билдов. И по сути мы говорим не о незаконченности игры, а о её вечности, как вечности души. Закончены могут быть только билды, как тела.
Такие как мы. Не такие как вы, это точно =)
У таких как мы уже несколько коммерческих игр вышло, а у таких как вы ни одной игры не сделано толком =)
И пока такие как вы сравниваете таких как вы с такими как мы, такие как мы делаем и выпускаем игры XD
Ну да, ну да.
Вы все фигней страдаете, только профи-боксеры - тру.
А бесплатные игры - это не игры вообще. Нафига мы тут целую базу игр составили, пора выкинуть их все нафиг, оставить только те, что в стиме продаются.
Ловко ты тему переводишь =)
Вроде речь шла о том, что это я с Кситом страдаю фигнёй - делаем игры на GM. В то время как Д\Артаньяны (вроде тебя и Юри) несут свет в мир геймдева и программируют игры как надо XD
Ну хоть что-то у меня хорошо получается.
Могли бы и постесняться ставить на это ценники больше, чем бесплатно.
:yak:
Ксит имеет в виду, что для разработки игры программирование не такой важный аспект как геймплей, например. То же касается графики и звука. Ну то есть ты налажешь с программирование и сделаешь охуенный геймлей - будет хорошая игра. А если ты сделаешь охуенное программирование и налажаешь с геймплеем то всё равно гавно получится.
Syberia 3. Охуенный геймплей, но местами порятся состояния и запоротые перезаписываются в единственный слот сейва. Результат - запоротая игра.
баги не единственная и совсем не главная ее проблема. В принципе в ней хают все.
давай другой пример
Ну нет, там две проблемы - резко закончилась (но это только если скачал у кого-то сейвы и переиграл с них уже до конца) и серьёзные баги.
- в тупорылой клюкве
- концепт игры строили скорее уборщики, нежели люди.
- одну из самых омерзительных технически и визуально игр за весь 2017 год, я увидел совершенно неадекватное управление, полностью сломанные подсказки взаимодействия с предметом, я увидел абсолютно мёртвые лица и полное отсутствие анимаций персонажей. Я ощутил НАСТОЛЬКО гигантское разочарование
- Минусы: Вся техническая часть. Анимации - зашквар. Мёртвая камера. Нулевое взаимодействие с новым поколением игроков. Ноль красивых пейзажей, блеклость картинки. Сюжет к середине начал проседать всё больше и больше, в итоге всё скатилось в какую-то чушь про кровавую гэбню и злых советских докторов. Управление клаво-мыши. совершенно кончeнная механика "вращения". Прерывания речи, куча одинаковых голосов на второстепенных нпс. Полное оказуаливание. Уход от викторианского стимпанка.
- изменение в произношении имен. Очень раздражающая последовательность действий. Графика. Управление. Нельзя настроить графику, видео, управление.
- Если у вас нет геймпада или он не определяется этой чудо игрой, то играть будет почти невозможно. Курсор светло-белый. В игре много снега, белых стен и других пространств, где найти курсор будет очень непросто. .. жестких таймингов
- может понравится только русофобам. Ни замысловатых механизмов, ни атмосферы, ни изобретательства
- Утрачен весь шарм игры. Картинка посредственная. Управление жутко кривое. История не чувствуется цельной. Клюква.
- Просто помойка. Невозможно играть. Постоянно какие-то проблемы. То едва работает, то лагает, то баги, то вылетает. Еще добавлю про странную логику (сюжет)Просто худшая игра в серии.
- ужано, отвратительно. Управление, камера - всё плохо.
- Квесты - (часть) не отличается логикой и представляет собой мешанину мистической эзотерики. Большую проблемы для игрока составляет камера. локации сплошь мрачные "дышащие на ладан" здания, руины или помойки. Музыка. Пожалуй единственное, что не вызывает нареканий. Можно закрыть глаза на многие недостатки, если сюжет интересен, но это не про "Сибирия 3". кондовый сюжет
- играть с клавиатурой и мышкой крайне неудобно. камера расположена так, что ты ... бегаешь из угла в угол практически вслепую, пытаясь кликнуть куда-то, где будет активна твоя точка. Местами точку вообще не поставить. Кейт "тупит", когда речь идёт о лестницах или поворотах. Ты думаешь, что ты активируешь одно, а на деле совершенно другое. Поворачивать рычаги с мышкой тоже неудобно. нудновато сто раз проходить одни и те же места. постоянное повторение.
- buggy and horribly optimized game. Biggest problem is optimization. Horrible controls and bugs all the time. Game by itself is so sooo sooo slow, that it become very and very annoying... A lot of unnecessary, unlogical puzzles. Story is very wear, huge step back... Every time playing i wanted to quit
- шлак редкостный.
- всратое управление, сомнительная анимация героев, непонимание происходящего вследствие незнания предыдущих игр и какая-то странная русская озвучка.
Я играл сам. Причём после далеко не первого патча. Проблем с оптимизацием не заметил, а вот прохождение запарывал несколько раз.
качество игры, если мы рассматриваем качество с точки зрения общественного достижения или коммерческого продукта или продукта искусства и творчества или реализации практик не зависит от личного ощущения.
Если у игры столько негативных отзывов обо всем, очевидно, что ты не можешь утверждать, что ее проблема в программировании. Игра облажалась и геймплейно
Не тебе решать, что я могу утверждать.
Отзывы по игре смешанные. Половине написавших отзыв понравилась творческая часть, как и мне.
то это эквивалентно негативным отзывам ни о чём. Вкусовщина в чистом виде.
Но только с регулярным бэкапом сэйвов. ;D
лол
Ксит уже ничего не имеет в виду. Дохлый номер воевать со специалистами широкого профиля, которые думают что они автоматически охватывают и всё узкое.
Специалист широкого профиля - человек который умеет делать все одинаково фигово.
«А ещё я сделал GM XD, Tekx и саундтрек к Electric Highways».
Я имел в виду, что я видел намного больше исходников разных инди-игр, чем многие здесь присутствующие. За ошибки в которых я отвечал, хотя я их даже не делал.
Блин, капец. Как ты успеваешь не только издавать, но и разрабатывать сразу десяток игр других разрабов?
Не разрабатывать, а дорабатывать. Называется "портирование".
Неужели у блюющего единорога всё так плохо?