26.10.2026 17 мин чтения Просмотров:0

Баллистика в Unity: угол выстрела, упреждение и траектория

Как посчитать скорость снаряда под цель при заданной скорости, времени или высоте дуги, взять упреждение по движущейся цели, заставить Rigidbody попадать точно и нарисовать предпросмотр траектории.

Баллистика в Unity: угол выстрела, упреждение и траектория

Гранатомёт, который попадает туда, куда указывает курсор. Турель, которая стреляет в бегущего игрока, а не в место, где он был секунду назад. Пунктир траектории, как в Angry Birds. Всё это решается одной формулой движения под гравитацией и парой квадратных уравнений. В статье разберём, как посчитать скорость выстрела под нужный угол, за заданное время и через заданную высоту, как брать упреждение по движущейся цели, почему Rigidbody прилетает чуть ниже расчётной точки и как это исправить, и как нарисовать предпросмотр траектории без мусора в памяти. Весь код готов к копированию в проект.

Одна формула, на которой всё держится

Снаряд без сопротивления воздуха движется по простому закону. Если в момент старта он находится в точке p0 и имеет скорость v0, то через время t его позиция равна:

p(t) = p0 + v0 * t + 0.5 * g * t * t

Здесь g — вектор гравитации. В Unity это Physics.gravity, по умолчанию (0, -9.81, 0). Скорость в любой момент — v(t) = v0 + g * t. Траектория в вертикальной плоскости — парабола, а горизонтальная составляющая скорости не меняется всё время полёта.

Почему не «выстрелить и посмотреть, куда упадёт»? Потому что игре почти всегда нужен обратный ответ: дана цель, найти скорость. Подбор скорости перебором в цикле с физической симуляцией медленный и неточный, а аналитическое решение считается за доли микросекунды и даёт точный результат. Симуляция нужна только там, где формула перестаёт работать: сопротивление воздуха, отскоки, ветер. До этого дойдём в конце.

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

  • фиксированная скорость снаряда — пушка, лук, гранатомёт: начальная скорость задана, нужно найти угол;
  • фиксированное время полёта — бросок способности в точку, «снаряд всегда летит 1 секунду»;
  • фиксированная высота дуги — прыжковые площадки, броски через стену, лут, вылетающий из сундука.

Угол выстрела при заданной скорости

Разложим задачу на две оси. Пусть x — горизонтальное расстояние до цели, y — разница высот (цель выше старта — положительная), v — модуль начальной скорости, g — модуль гравитации (положительное число, 9.81). Если исключить из уравнений время, получится квадратное уравнение относительно тангенса угла, и его решение:

tan(θ) = (v² ± sqrt(v⁴ − g * (g * x² + 2 * y * v²))) / (g * x)

Из формулы сразу видно три вещи, которые полезно держать в голове:

  • Если подкоренное выражение отрицательно, цель недостижима с такой скоростью. Это не ошибка, а нормальный игровой случай: турель должна понять, что не добивает, и не стрелять.
  • Если оно положительно, решений два: настильная траектория (знак минус) и навесная (знак плюс). Миномёту нужна навесная, танку — настильная.
  • Если оно равно нулю, решение одно: это предельная дальность. На ровной земле максимальная дальность равна v² / g и достигается под углом 45°.

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

using UnityEngine;

public static class Ballistics
{
    // gravity — модуль ускорения свободного падения, положительное число.
    // Возвращает 0, 1 или 2: сколько решений найдено.
    public static int SolveArc(Vector3 origin, Vector3 target, float speed, float gravity,
                               out Vector3 low, out Vector3 high)
    {
        low = high = Vector3.zero;

        Vector3 delta = target - origin;
        Vector3 flat = new Vector3(delta.x, 0f, delta.z);
        float x = flat.magnitude;
        float y = delta.y;

        // Цель строго над или под стрелком: угол не определён, обрабатываем отдельно.
        if (x < 1e-4f) return 0;

        float v2 = speed * speed;
        float disc = v2 * v2 - gravity * (gravity * x * x + 2f * y * v2);
        if (disc < 0f) return 0;

        float root = Mathf.Sqrt(disc);
        float gx = gravity * x;
        Vector3 dir = flat / x;

        low = Build(dir, speed, Mathf.Atan((v2 - root) / gx));
        high = Build(dir, speed, Mathf.Atan((v2 + root) / gx));
        return root < 1e-4f ? 1 : 2;
    }

    static Vector3 Build(Vector3 flatDir, float speed, float angle)
    {
        return flatDir * (speed * Mathf.Cos(angle)) + Vector3.up * (speed * Mathf.Sin(angle));
    }
}

Использование в турели выглядит так:

float g = -Physics.gravity.y;
int solutions = Ballistics.SolveArc(muzzle.position, target.position, projectileSpeed, g,
                                    out Vector3 low, out Vector3 high);
if (solutions == 0)
{
    // Цель вне досягаемости: не стреляем, можно подъехать ближе.
    return;
}
Vector3 velocity = useHighArc ? high : low;

Обратите внимание на вычисление через Mathf.Atan, а не через построение направления «на глаз». Угол получается в радианах, и Mathf.Cos/Mathf.Sin принимают именно радианы, так что никаких Mathf.Deg2Rad здесь не нужно. Если хочется показать игроку угол возвышения в интерфейсе, переводите уже на выходе: angle * Mathf.Rad2Deg.

Ещё одна деталь: формула предполагает, что гравитация направлена строго вниз по оси Y. Если в вашей игре гравитация повёрнута (планеты, стены), перейдите в локальную систему координат, где «вниз» — это -Vector3.up, решите задачу там и поверните результат обратно через Quaternion.FromToRotation.

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

Это самая простая и самая надёжная постановка. Из формулы движения выражаем начальную скорость:

v0 = (target − origin) / t − 0.5 * g * t

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

public static Vector3 VelocityForTime(Vector3 origin, Vector3 target, float time, Vector3 gravity)
{
    return (target - origin) / time - 0.5f * time * gravity;
}

Здесь gravity — уже вектор, то есть можно просто передать Physics.gravity, и формула сама работает при любом направлении гравитации.

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

float distance = Vector3.Distance(origin, target);
float time = Mathf.Lerp(minFlightTime, maxFlightTime, distance / maxRange);

Скорость при заданной высоте дуги

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

Решаем по частям. Чтобы подняться на высоту h, вертикальная скорость должна быть sqrt(2 * g * h). Время подъёма — vy / g. Время спуска с вершины до цели находим из того же закона свободного падения. Горизонтальную скорость берём такую, чтобы за суммарное время пройти горизонтальное расстояние.

// gravity — модуль, положительное число. apexHeight — запас над высшей точкой.
public static Vector3 VelocityForApex(Vector3 origin, Vector3 target, float apexHeight, float gravity)
{
    Vector3 delta = target - origin;

    // Высота вершины над точкой старта.
    float top = Mathf.Max(delta.y, 0f) + apexHeight;

    float vy = Mathf.Sqrt(2f * gravity * top);
    float timeUp = vy / gravity;
    float timeDown = Mathf.Sqrt(2f * (top - delta.y) / gravity);

    Vector3 flat = new Vector3(delta.x, 0f, delta.z);
    return flat / (timeUp + timeDown) + Vector3.up * vy;
}

Деление на ноль здесь невозможно, пока apexHeight больше нуля: и top, и top - delta.y строго положительны. Если же передать ноль, а цель окажется ниже старта, подъёма не будет вовсе, и формула всё равно отработает корректно — получится бросок «с обрыва».

Упреждение: стрельба по движущейся цели

Наводить прямо на цель имеет смысл, только если снаряд мгновенный (hitscan). У любого летящего снаряда за время полёта цель успевает сместиться. Начнём со случая без гравитации: пуля летит по прямой с постоянной скоростью, например лазерный болт или стрела с очень высокой скоростью.

Прямолинейный снаряд

Пусть D — вектор от стрелка до цели, V — скорость цели, s — скорость снаряда. Через время t цель окажется в точке D + V * t относительно стрелка, а снаряд пролетит расстояние s * t. Они встретятся, когда эти длины равны. Возводим в квадрат и раскрываем скалярные произведения:

(V·V − s²) * t² + 2 * (D·V) * t + D·D = 0

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

public static bool TryLead(Vector3 shooter, Vector3 target, Vector3 targetVelocity, float speed,
                           out Vector3 aimPoint, out float time)
{
    Vector3 d = target - shooter;
    float a = Vector3.Dot(targetVelocity, targetVelocity) - speed * speed;
    float b = 2f * Vector3.Dot(d, targetVelocity);
    float c = Vector3.Dot(d, d);
    time = -1f;

    if (Mathf.Abs(a) < 1e-6f)
    {
        // Скорости цели и снаряда равны: уравнение вырождается в линейное.
        if (Mathf.Abs(b) > 1e-6f) time = -c / b;
    }
    else
    {
        float disc = b * b - 4f * a * c;
        if (disc >= 0f)
        {
            float root = Mathf.Sqrt(disc);
            float t1 = (-b - root) / (2f * a);
            float t2 = (-b + root) / (2f * a);
            time = SmallestPositive(t1, t2);
        }
    }

    aimPoint = target + targetVelocity * Mathf.Max(time, 0f);
    return time > 0f;
}

static float SmallestPositive(float a, float b)
{
    if (a > b) (a, b) = (b, a);
    return a > 0f ? a : b;
}

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

Откуда брать targetVelocity? Для объекта с Rigidbody — из linearVelocity (до Unity 6 свойство называлось velocity). Для персонажа на CharacterController — из CharacterController.velocity. Для объекта, который двигается через Transform, скорость придётся считать самому: запоминать позицию в прошлом кадре и делить разницу на Time.deltaTime. Такую оценку стоит сгладить, иначе на рывках прицел будет дрожать.

Снаряд под гравитацией

С гравитацией замкнутое решение тоже существует, но это уравнение четвёртой степени, и его аккуратная реализация занимает экран кода с кучей частных случаев. На практике почти всегда хватает итераций:

  1. Считаем баллистическое решение для текущей позиции цели.
  2. Из него получаем время полёта.
  3. Предсказываем, где будет цель через это время.
  4. Пересчитываем решение для предсказанной точки и повторяем.

Для целей, которые медленнее снаряда, процесс сходится за 3–4 итерации до точности, которую глазом не отличить от точного ответа.

public static bool TryLeadArc(Vector3 origin, Vector3 target, Vector3 targetVelocity,
                              float speed, float gravity, bool highArc, out Vector3 velocity)
{
    velocity = Vector3.zero;
    Vector3 predicted = target;

    for (int i = 0; i < 4; i++)
    {
        if (SolveArc(origin, predicted, speed, gravity, out Vector3 low, out Vector3 high) == 0)
            return false;

        velocity = highArc ? high : low;
        float time = FlightTime(origin, predicted, velocity);
        predicted = target + targetVelocity * time;
    }
    return true;
}

static float FlightTime(Vector3 origin, Vector3 target, Vector3 velocity)
{
    float horizontalDistance = new Vector2(target.x - origin.x, target.z - origin.z).magnitude;
    float horizontalSpeed = new Vector2(velocity.x, velocity.z).magnitude;
    return horizontalDistance / Mathf.Max(horizontalSpeed, 1e-4f);
}

Время полёта здесь считается по горизонтали: горизонтальная скорость постоянна, поэтому это точно и не требует решать ещё одно уравнение.

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

Упреждение при фиксированном времени

Если вы используете постановку с заданным временем полёта, упреждение становится тривиальным и точным без всяких итераций: время уже известно, значит, точка встречи — просто target + targetVelocity * time. Подставляем её в VelocityForTime, и задача решена. Это хороший аргумент выбирать именно эту постановку для способностей, где скорость снаряда не важна геймплейно.

Запуск Rigidbody: почему снаряд падает чуть ниже

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

Физический движок интегрирует движение шагами длиной Time.fixedDeltaTime, и делает это полунеявным методом Эйлера: сначала к скорости прибавляется g * dt, затем позиция сдвигается на новую скорость. Если просуммировать такие шаги, позиция через время t отличается от точной формулы на 0.5 * g * dt * t. При шаге 0.02 с и полёте в 2 секунды это около 20 сантиметров — на большой дистанции уже заметный промах.

Хорошая новость: ошибка линейна по времени, а значит, её можно полностью скомпенсировать поправкой к начальной скорости. Достаточно вычесть из неё 0.5 * g * dt:

public class Launcher : MonoBehaviour
{
    [SerializeField] Rigidbody projectilePrefab;
    [SerializeField] Transform muzzle;
    [SerializeField] float speed = 20f;
    [SerializeField] bool highArc;

    public void Fire(Vector3 target)
    {
        float g = -Physics.gravity.y;
        int n = Ballistics.SolveArc(muzzle.position, target, speed, g, out Vector3 low, out Vector3 high);
        if (n == 0) return;

        Vector3 velocity = highArc ? high : low;
        Rigidbody rb = Instantiate(projectilePrefab, muzzle.position, Quaternion.LookRotation(velocity));

        // Компенсация дискретного шага физики.
        rb.linearVelocity = velocity - 0.5f * Time.fixedDeltaTime * Physics.gravity;
    }
}

Поскольку Physics.gravity направлен вниз, вычитание добавляет крошечную вертикальную составляющую вверх, ровно столько, сколько «съест» дискретный шаг. После этой поправки снаряд приземляется в цель с точностью до погрешности float.

Чтобы расчёт и физика совпадали, проверьте несколько настроек снаряда:

  • Linear Damping (в старых версиях Drag) должен быть равен нулю. Любое сопротивление движок применяет на каждом шаге, умножая скорость на коэффициент меньше единицы, и параболическая формула перестаёт быть верной.
  • Use Gravity включён, а гравитация в расчёте берётся из того же Physics.gravity, а не из захардкоженного 9.81. Иначе любой, кто поменяет гравитацию в Project Settings, сломает всю баллистику.
  • Масса не важна. Мы задаём скорость напрямую, а не силу. Если хочется именно AddForce, используйте ForceMode.VelocityChange: он меняет скорость без учёта массы, и результат тот же.
  • Collision Detection для быстрых снарядов — ContinuousDynamic или ContinuousSpeculative, иначе снаряд пролетит сквозь тонкую стену между двумя шагами.

Если вы двигаете снаряд сами, без Rigidbody, просто вычисляйте позицию по формуле p0 + v0 * t + 0.5 * g * t * t от времени жизни снаряда. Тогда никакой ошибки интегрирования нет в принципе, а столкновения проверяйте через Physics.Linecast между прошлой и новой позицией — так же, как в предпросмотре ниже.

Предпросмотр траектории через LineRenderer

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

using UnityEngine;

[RequireComponent(typeof(LineRenderer))]
public class TrajectoryPreview : MonoBehaviour
{
    [SerializeField] int maxPoints = 60;
    [SerializeField] float timeStep = 0.05f;
    [SerializeField] LayerMask hitMask = ~0;

    LineRenderer line;
    Vector3[] points;

    void Awake()
    {
        line = GetComponent<LineRenderer>();
        points = new Vector3[maxPoints];
    }

    // Возвращает точку, где траектория упрётся в препятствие (или последнюю точку).
    public Vector3 Draw(Vector3 origin, Vector3 velocity)
    {
        Vector3 g = Physics.gravity;
        points[0] = origin;
        int count = 1;

        for (int i = 1; i < maxPoints; i++)
        {
            float t = i * timeStep;
            Vector3 p = origin + velocity * t + 0.5f * t * t * g;

            if (Physics.Linecast(points[count - 1], p, out RaycastHit hit, hitMask,
                                 QueryTriggerInteraction.Ignore))
            {
                points[count++] = hit.point;
                break;
            }
            points[count++] = p;
        }

        line.positionCount = count;
        line.SetPositions(points);
        return points[count - 1];
    }

    public void Hide() => line.positionCount = 0;
}

Несколько практических замечаний:

  • Массив точек создаётся один раз в Awake. SetPositions копирует из него только первые positionCount элементов, поэтому массив может быть длиннее, чем нужно в конкретном кадре.
  • Слой снаряда и самого стрелка исключите из hitMask, иначе первый же отрезок упрётся в коллайдер персонажа и линия не нарисуется вовсе.
  • Шаг по времени задаёт плавность. 0.05 с даёт гладкую дугу для большинства скоростей. Для очень быстрых снарядов лучше шагать не по времени, а по расстоянию: timeStep = desiredSegmentLength / velocity.magnitude.
  • Метод возвращает точку попадания. По ней удобно ставить маркер приземления — декаль или круг зоны поражения гранаты.
  • Для пунктира не нужно резать линию на куски: достаточно материала с повторяющейся текстурой и режима Texture Mode = Tile у LineRenderer.

Связка с наведением выглядит естественно: каждый кадр, пока зажата кнопка прицеливания, считаем скорость под точку курсора через SolveArc или VelocityForTime и отдаём её в Draw. При отпускании кнопки вызываем Launcher.Fire с той же целью — линия и реальный полёт совпадут, потому что считаются одной и той же формулой (с учётом поправки на шаг физики, о которой шла речь выше).

Когда формулы не хватает: симуляция в отдельной физической сцене

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

using UnityEngine;
using UnityEngine.SceneManagement;

public class TrajectorySimulator : MonoBehaviour
{
    [SerializeField] Rigidbody ghostPrefab;
    [SerializeField] Transform obstaclesRoot;
    [SerializeField] int steps = 120;

    Scene simScene;
    PhysicsScene simPhysics;
    Rigidbody ghost;

    void Start()
    {
        simScene = SceneManager.CreateScene("TrajectorySim",
            new CreateSceneParameters(LocalPhysicsMode.Physics3D));
        simPhysics = simScene.GetPhysicsScene();

        // Копируем статичные препятствия, чтобы призрак от них отскакивал.
        foreach (Transform obstacle in obstaclesRoot)
        {
            GameObject copy = Instantiate(obstacle.gameObject, obstacle.position, obstacle.rotation);
            foreach (Renderer r in copy.GetComponentsInChildren<Renderer>()) r.enabled = false;
            SceneManager.MoveGameObjectToScene(copy, simScene);
        }

        ghost = Instantiate(ghostPrefab);
        ghost.GetComponent<Renderer>().enabled = false;
        SceneManager.MoveGameObjectToScene(ghost.gameObject, simScene);
    }

    public int Simulate(Vector3 origin, Vector3 velocity, Vector3[] result)
    {
        ghost.position = origin;
        ghost.linearVelocity = velocity;
        ghost.angularVelocity = Vector3.zero;

        int count = Mathf.Min(steps, result.Length);
        for (int i = 0; i < count; i++)
        {
            simPhysics.Simulate(Time.fixedDeltaTime);
            result[i] = ghost.position;
        }
        return count;
    }
}

Призрак — это копия настоящего снаряда с теми же коллайдером, массой, сопротивлением и физическим материалом. Поэтому траектория совпадает с реальной вплоть до отскоков. Цена — производительность: 120 шагов симуляции каждый кадр заметно дороже 60 вызовов формулы. Если препятствий много, копируйте только те, что рядом со стрелком, и обновляйте предпросмотр не каждый кадр, а раз в два-три кадра или только при заметном изменении прицела.

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

То же самое в 2D

Все формулы работают и в 2D, достаточно заменить Vector3 на Vector2 и выкинуть ось Z. Горизонтальное расстояние становится просто delta.x, причём со знаком: цель слева от стрелка даёт отрицательный x. Проще всего решать задачу для модуля, а направление по горизонтали восстанавливать через Mathf.Sign(delta.x).

Главная ловушка 2D — гравитация. У Rigidbody2D есть собственный множитель gravityScale, и реальное ускорение тела равно Physics2D.gravity * rb.gravityScale. Многие платформеры ставят gravityScale 2–3, чтобы прыжки были «тяжелее», и забывают учесть это в расчёте броска. Результат — снаряды стабильно не долетают вдвое-втрое. Берите гравитацию из конкретного тела:

Vector2 gravity = Physics2D.gravity * rb.gravityScale;
float g = -gravity.y;

Поправка на шаг физики в 2D такая же: velocity - 0.5f * Time.fixedDeltaTime * gravity.

Частые ошибки

  • Захардкоженное 9.81. Гравитацию всегда берите из Physics.gravity или Physics2D.gravity с учётом gravityScale.
  • Смешение градусов и радиан. Mathf.Atan возвращает радианы, Quaternion.Euler принимает градусы. Если строите поворот ствола из угла, не забудьте Mathf.Rad2Deg.
  • Стрельба из центра персонажа, а расчёт из дула. Точка старта в формуле и точка спавна снаряда должны совпадать. Разница в полметра на дистанции в 30 метров даёт заметный промах.
  • Упреждение по скорости из Transform без сглаживания. Скорость, посчитанная по разнице позиций за один кадр, скачет на каждом рывке, и турель трясёт стволом. Усредняйте её за несколько кадров.
  • Наведение ствола и вылет снаряда в разных кадрах. Если ствол поворачивается плавно, а снаряд вылетает по расчётной скорости, визуально выстрел может «вылететь не из ствола». Либо стреляйте только когда ствол довернулся до нужного угла, либо поворачивайте ствол мгновенно в момент выстрела.
  • Не обработанный случай «цель вне досягаемости». SolveArc возвращает ноль решений, а код всё равно стреляет с нулевой скоростью, и снаряд падает под ноги. ИИ должен знать, что не добивает, и менять позицию.
  • Сопротивление воздуха в префабе снаряда. Кто-то поставил Linear Damping 0.1 «для реализма», и вся аналитика поехала. Либо ноль, либо симуляция.

Итог

Баллистика в Unity сводится к одной формуле движения и выбору того, что считать заданным. Фиксированная скорость даёт угол через квадратное уравнение, два решения (настильное и навесное) и естественную проверку дальности. Фиксированное время даёт решение всегда и делает упреждение тривиальным. Фиксированная высота даёт контроль над формой дуги для прыжков и бросков через препятствия.

Упреждение по прямой — квадратное уравнение по времени встречи, по дуге — три-четыре итерации «предсказать точку, пересчитать решение». Чтобы Rigidbody попадал точно, уберите сопротивление и добавьте к начальной скорости поправку -0.5 * g * fixedDeltaTime. Предпросмотр траектории — те же точки формулы с проверкой Linecast между ними и один массив на всё время жизни компонента. А когда понадобятся отскоки и сопротивление воздуха, переходите к симуляции в отдельной физической сцене: дороже, но честно.

Комментарии

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

Пока нет комментариев. Будьте первым!

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