Твининг-анимация в Unity с помощью LeanTween

Узнайте, как использовать LeanTween для анимации пользовательского интерфейса и различных игровых объектов в Unity 3D, создав клон игры-арканоида Breakout.

Версия: C# 7.3, Unity 2020.3, Unity

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

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

В этом уроке вы узнаете о твининге и о том, как:

  • Использовать анимацию в игровых объектах, таких как ассеты и пользовательский интерфейс.
  • Интегрировать пакет LeanTween в свой проект.
  • Применять эффекты вращения, смещения и масштабирования к игровым объектам.
  • Добавлять элементы пользовательского интерфейса в свои проекты.

К тому времени, когда вы закончите, вы будете думать об анимации не только с точки зрения Unity Animator, но и с использованием других типов пакетов. В качестве бонуса окончательный образец проекта будет выглядеть великолепно!

Итоговый гэймплей проекта арканоид в Unity, с отскакивающим мячиком, миганием цветов и вращением кирпичей
Вы замечаете tween?

Приступая к работе

Нажмите кнопку «Скачать материалы урока» вверху страницы, чтобы скачать начальный проект. Для этого проекта требуется Unity 2020.3.20f1 или более актуальная версия.

В Assets/RW вы найдете ассеты, используемые в проекте. Посмотрите на структуру папок:

  • Input: Файлы, используемые новой системой ввода Unity.
  • Physics Materials: Физические материалы, используемые для мяча в проекте.
  • Plugins: В этой папке установлен LeanTween. Позже в этом уроке вы узнаете, как установить LeanTween в свои проекты.
  • Prefabs: Префабы проекта.
  • Scenes: Здесь вы найдете образец сцены.
  • Scripts: Скрипты C# для проекта.
  • Sprites: Игровая графика, любезно предоставленная kenney.nl.
  • Text Mesh Pro: Файлы, используемые Text Mesh Pro, где вы создаете пользовательский интерфейс. Чтобы узнать больше, обязательно ознакомьтесь с уроком по TextMesh Pro.

Сколько много папок!

Откройте сцену TweenBreaker в Assets/RW/Scenes, затем нажмите на Play, чтобы попробовать клон Breakout. Используйте стрелки вправо и влево или кнопки A и D на клавиатуре, чтобы перемещать ракетку-брусок. Заставьте мяч подпрыгивать и разбейте столько кирпичиков, сколько пожелаете.

Теперь пришло время поближе познакомиться с твининг-анимацией.

Почему бы не использовать Unity Animator для всего?

Это отличный вопрос!

В Unity уже есть модуль, способный реализовать большинство видов анимации, так зачем вам еще один пакет? Разве это не лишнее?

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

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

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

Проще говоря, tweening или inbetweening (промежуточное положение) — это другое название интерполяции. В этой операции свойство может принимать любое значение между двумя пределами.

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

Движение игрового объекта между двумя точками в Unity

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

Получая значения между этими точками, анимационный движок может определить, что через две секунды игровой объект должен быть в (0.5, 0, 0), через одну секунду — (0.25, 0, 0), через три секунды — (0.75, 0, 0) и так далее. Эта простая интерполяция создает анимацию, используя только простые алгебраические операции.

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

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

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

Дополнительные сведения о функциях плавности и их эффектах смотрите по этой ссылке.

Компонент Animator против методов Tweening

Когда вы выбираете между двумя методами, то должны учитывать сильные и слабые стороны анимации.

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

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

Сколько затрат вычислительных ресурсов в компоненте Animator? Давайте сравним их производительность.

Сравнение производительности

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

Спрайт, двигающийся по сторонам в Unity

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

Изображение профайлера в Unity показывает, что аниматор потребляет вычислительную мощность около 5ms
Аниматор — это толстая синяя линия.

Эта толстая синяя линия показывает, сколько вычислительной мощности потребляет компонент Unity Animator. Выбор линии в заданной точке показывает, что происходит в основном цикле Unity:

Главный поток в окне Profiler редактора Unity

Обратите внимание, что главный злодей здесь — метод Animator.Update(). Этот метод занимает много времени обработки основного потока. Если бы только был способ избавиться от этого…

Вот где LeanTween имеет значение. LeanTween — это пакет, обеспечивающий простую и облегченную реализацию анимации движения. Без необходимости вызывать Animator весь процесс резко меняется. Посмотрите, что об этом говорит профайлер Unity:

График профайлера в Unity показывает насколько процесс потребления для твининг анимации ниже, чем для аниматора

Главный поток тоже другой. Посмотрите:

Главный поток в окне Profiler редактора Unity отображает приток производительности LeanTween

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

Добавление LeanTween в ваш проект

Чтобы добавить LeanTween в свои проекты, перейдите на его страницу в магазине ассетов и добавьте в свои ассеты.

Когда вы закончите, он появится в окне проводника пакетов в Unity. Выберите его и нажмите Install. При появлении запроса нажмите Import, чтобы добавить пакет в проект.

Теперь об использовании недавно добавленного пакета в вашей игре.

Анимация ракетки с помощью библиотеки Tween.

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

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

Перемещение объектов с помощью LeanTween

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

Однако более специализированные функции перемещают игровой объект по одной оси: moveX, moveY и moveZ. В этом уроке вы будете использовать функцию, предназначенную для перемещения бруска по оси Y.

Отскок бруска

Вам нужно добавить некоторое смещение весла и заставить его реагировать на столкновение с мячиком. Перейдите в Paddle.cs и замените весь метод OnCollisionEnter2D() на:

private void OnCollisionEnter2D(Collision2D collision)
{
    //1
    if (collision.gameObject.tag.Equals("Cog"))
    {
        //2
        LeanTween.cancel(gameObject);
        //3
        LeanTween.moveY(gameObject, transform.position.y - 0.5f, 0.5f).setEaseShake();
    }
}

Этот код выполняет три основные функции:

  1. Эта строка проверяет, есть ли столкновение между бруском и мячом («шестеренка»). В этом примере брусок не может столкнуться ни с чем другим, но рекомендуется четко определить, какое столкновение вы хотите обрабатывать.
  2. Эта функция указывает LeanTween остановить любые другие эффекты, которые могут воздействовать на этот игровой объект. Такой шаг поможет вам избежать ошибок, гарантируя, что никакие другие эффекты анимации не будут работать одновременно с элементом.
  3. И наконец, это строка, которая действительно создает движение. Если бы это было предложение на обычном языке, то функция сказала бы «переместить ось Y игрового объекта на пол-единицы вниз в течение полсекунды».

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

Брусок подпрыгивает, когда мячик ударяет по нему в игре на Unity
Отскок, брусок, отскок!

Однако, несмотря на то, что брусок перемещается вдоль оси Y, в итоге он возвращается в исходное положение. Это происходит из-за setEaseShake(), добавленного в конце LeanTween.moveY(). Эта кривая замедления определяет, что движение должно заканчиваться в той же точке, где оно началось, создавая эффект отскока, показанный на бруске.

Если хотите, удалите код setEaseShake() и посмотрите, как брусок заходит за нижнюю часть экрана. Но не забудьте добавить его обратно, когда закончите.

Брусок отскакивает за экран в окне сцены редактора Unity
Куда это ты направляешься, брусок?

Добавление персонажа к мячу

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

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

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

Масштабирование мячика

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

Чтобы проиллюстрировать эту концепцию, замените метод OnCollisionEnter2D() в BallScript.cs на:

private void OnCollisionEnter2D(Collision2D collision)
{
    if (collision.gameObject.tag.Equals("Player"))
    {
        hitPosition = (ballRigidbody.position.x - collision.rigidbody.position.x) 
                / collision.collider.bounds.size.x;

            direction = new Vector2(hitPosition, 1).normalized;
            ballRigidbody.velocity = direction * BallSpeed;
    }
    // 1
    LeanTween.cancel(gameObject);
    transform.localScale = new Vector3(0.4f, 0.4f, 0.4f);
    // 2
    LeanTween.scale(gameObject, new Vector3(1.0f, 1.0f), 1.0f).setEase(LeanTweenType.punch);
}

Вот разбор кода:

  1. Эти две строки сбрасывают поведение игрового объекта. В дополнение к LeanTween.cancel() необходимо сбросить вес мячика, чтобы избежать распространения ошибок. Если мячик столкнется с другим элементом до окончания анимации, начальный масштаб будет неверным, а в конце изменится «обычный» размер мяча.
  2. Опять же, это код, который фактически выполняет операцию. Однако на этот раз вы масштабируете игровой объект, а не перемещаете его. Это масштабирует мяч от его нормального размера (0.4) до 1.0 и обратно благодаря функции ослабления punch.

Нажмите на кнопку Play. Посмотрите, как красиво мяч себя сейчас ведет:

Мячик отскакивает от бруска в окне сцены редактора Unity

Персонализированные функции плавности

В этом примере вы использовали предопределенную кривую замедления для операции масштабирования. Но setEase не ограничивается только кривыми смягчения LeanTweenType.

Эта функция также дает вам возможность рисовать собственные кривые с помощью Unity. Добавьте следующую переменную animationCurve в класс BallScript:

public AnimationCurve animationCurve;

Затем замените:

LeanTween.scale(gameObject, new Vector3(1.0f, 1.0f), 1.0f).setEase(LeanTweenType.punch);

этим:

LeanTween.scale(gameObject, new Vector3(1.0f, 1.0f), 1.0f).setEase(animationCurve);

Сохраните изменения скрипта и перейдите в окно Hierarchy. Разверните игровой объект Player Objects, выберите игровой объект Cog и посмотрите в окно Inspector.

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

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

Интерфейс Unity для выбора или редактирования изогнутой направляющей плавности анимации

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

Например, если вы настроили кривую на быстрый подъем, вот так:

Кривая линия с быстрым подъемом для анимации в редакторе Unity

Сначала мячик быстро растет, а затем останавливается, вот так:

Изображение мячика, который быстро увеличивается сразу после столкновения в сцене редактора Unity

Однако, если кривая перевернута:

Кривая начинает плавно и быстро расти в настройках окна анимации редактора Unity

Мячик начнет медленно расти и набирать обороты:

Изображение, показывающее, что мячик в сцене редактора Unity медленно растет в начале

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

Изогнутая линия в редакторе Unity показывает три пика подъема и возвращается к началу

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

Мячик в окне сцены редактора Unity с отскакивающей и возвращающейся к оригинальному размеру шкалой

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

Эффекты изменения цвета

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

В BallScript.cs добавьте еще одну строку в конец метода OnCollisionEnter2D(), чтобы учесть цветовые эффекты:

gameObject.LeanColor(Color.yellow, 0.5f).setEasePunch();

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

Мячик в окне сцены редактора Unity меняет цвет при столкновении с другими элементами
Теперь мяч является персонажем игры.

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

Разрушение блоков

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

Откройте Crates.cs. Вы увидите код для OnCollisionEnter2D().

В OnCollisionEnter2D() вы найдете только ссылку на функцию, которая увеличивает счет, когда игрок разбивает ящики. Но вы сразу же добавите больше.

Блоки в окне сцены редактора Unity разрушаются и просто исчезают
Блоки просто исчезают…

Вращение объектов с помощью LeanTween

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

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

Замените весь метод OnCollisionEnter2D() на:

private void OnCollisionEnter2D(Collision2D collision)
{
    //1
    gameObject.GetComponent<Collider2D>().enabled = false;
    //2
    LeanTween.alpha(gameObject, 0.2f, 0.6f);
    //3
    LeanTween.rotateAround(gameObject, collision.GetContact(0).normal, 250.0f, 0.6f).setDestroyOnComplete(true);
    // Increases the score 
    GameManager.Instance.IncreaseScore(1);
}

Вот что делает код:

  1. Изначально вы отключаете коллайдер игрового объекта, чтобы избежать дополнительных столкновений между моментом удара по блоку и его исчезновением со сцены.
  2. Чтобы создать иллюзию исчезновения блока, вы используете альфа-канал для уменьшения непрозрачности элемента до 0,2 за 0,6 секунды.
  3. rotateAround() поворачивает игровой объект вокруг точки, где произошло столкновение, на 250 градусов за 0,6 секунды. Это создает более отзывчивое ощущение, поскольку кирпичик вращается вокруг точки контакта между собой и мячом. Затем код указывает LeanTween удалить игровой объект после завершения анимации, а также элементы из сцены сразу после завершения отмеченных операций.

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

Блоки в окне сцены редактора Unity вращаются перед разрушением
Теперь гораздо лучше!

Анимация элементов пользовательского интерфейса

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

Но если вы присмотритесь, то заметите, что не все имеет динамическое поведение.

Текст счета по-прежнему статичен. Он считает очки правильно, но в этом нет ничего особенного.

В Unity элементы пользовательского интерфейса также являются игровыми объектами, поэтому добавить некоторые эффекты должно быть просто, верно? Правильно!

Счет игры увеличивается просто изменением текста в окне сцены редактора Unity
Как это сейчас.

Анимация увеличения счета

GameManager имеет ссылку на текстовый объект и отвечает за обновление счета.

В GameManager.cs найдите и замените весь код метода IncreaseScore(int value) на этот:

public void IncreaseScore(int value)
{
    gameScore += value;
    scoreDisplay.text = gameScore.ToString();

    // 1
    LeanTween.cancel(scoreDisplay.gameObject);
    scoreDisplay.transform.rotation = Quaternion.Euler(0.0f, 0.0f, 0.0f);
    scoreDisplay.transform.localScale = Vector3.one;

    // 2
    LeanTween.rotateZ(scoreDisplay.gameObject, 15.0f, 0.5f).setEasePunch();
    LeanTween.scaleX(scoreDisplay.gameObject, 1.5f, 0.5f).setEasePunch();
}

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

  1. Этот блок сбрасывает внешний вид игрового объекта отображения счета. Эти строки останавливают любую операцию твининга, воздействующую на scoreDisplay, и сбрасывают его вращение и масштаб, чтобы избежать распространения ошибок во время игры.
  2. Функции в этом блоке добавляют эффекты вращения и масштабирования к игровому объекту scoreDisplay. Здесь вы объявляете вращение по оси Z и масштабирование по оси X с той же функцией плавности.

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

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

Анимация увеличения счета для игры в окне сцены редактора Unity
Теперь гораздо лучше!

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

Анимация цвета фона

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

Прежде чем перейти к коду, разверните игровой объект Level Geometry в окне Hierarchy. Затем выберите игровой объект Background и просмотрите его свойства в окне инспектора.

Обратите внимание, что цвет компонента Sprite Renderer отличается от белого. Это помогает создать иллюзию трехмерного пространства, при этом задний план находится на расстоянии от переднего плана.

Чтобы действовать, вам понадобится ссылка на фоновый игровой объект. Итак, вверху GameManager.cs, прямо внизу строки:

public GameObject Paddle;

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

public GameObject Background;
public float backgroundShakeRate = 2.0f;

Теперь снова замените код метода IncreaseScore(int value) на следующее:

public void IncreaseScore(int value)
{
    gameScore += value;
    scoreDisplay.text = gameScore.ToString();

    LeanTween.cancel(scoreDisplay.gameObject);
    scoreDisplay.transform.rotation = Quaternion.Euler(0.0f, 0.0f, 0.0f);
    scoreDisplay.transform.localScale = Vector3.one;

    LeanTween.rotateZ(scoreDisplay.gameObject, 15.0f, 0.5f).setEasePunch();
    LeanTween.scaleX(scoreDisplay.gameObject, 1.5f, 0.5f).setEasePunch();

    // 1
    LeanTween.move(Background.gameObject, Random.insideUnitCircle * backgroundShakeRate, 0.5f).setEasePunch();

    // 2
    Background.LeanColor(Color.red, 0.3f).setEasePunch().setOnComplete(() =>
        {
            Background.GetComponent<SpriteRenderer>().color = new Color(0.38f, 0.38f, 0.38f);
        });
}

И move, и LeanColor уже использовались для других элементов. Теперь вы будете использовать их немного по-другому:

  1. Этот код использует LeanTween.move(). Но в этом случае движение выполняется в случайном направлении с помощью Random.insideUnitCircle для возврата случайного Vector2 внутри единичного круга (круг с радиусом 1).
  2. Этот код показывает, как определить лямбда-выражение, которое будет выполняться сразу после завершения анимации. В этом случае код переопределяет атрибут цвета фонового спрайта на значение по умолчанию, чтобы избежать изменения цвета, точно так же, как размер мяча сбрасывается в каждом раунде анимации.

Не забудьте добавить ссылку, которую вы создали в скрипте, в редакторе! Перетащите игровой объект Background из окна иерархии в соответствующий слот в GameManager.

Добавление ссылки для фона в окне Inspector редактора Unity

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

Исходный проект игры в Unity без анимационных эффектов
Стартовый проект. Такое ощущение, что это было 20 минут назад…
Итоговый гэймплей проекта игры арканоид в Unity с прыгающими мячиками, мигающими цветами и вращающимися кирпичиками
Итоговый проект, он просто выглядит намного лучше.

Куда двигаться дальше

Отсюда вы можете увести что угодно!

Вы можете скачать готовые файлы проекта, нажав кнопку «Скачать материалы урока» вверху страницы, и продолжить изучение.

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

Вы также можете просмотреть официальную документацию LeanTween для получения дополнительной информации об API.

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

Автор перевода: Jean Winters

Источник: Tweening Animations in Unity with LeanTween

Смотрите также:

Введение в скрипты UnityВведение в скрипты Unity


Введение в Unity: приступая к работеВведение в Unity: приступая к работе


Комментировать

Почта не публикуется.Обязательные поля отмечены *