Управляющие силы - "рыскание"

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

Идеи, лежащие в основе управляющих сил были предложены Craig W. Reynolds; они не используют сложные стратегии, связанные с поиском пути, или глобальными вычислениями, вместо этого используется локальная информация, например, векторы соседних сущностей. Это делает идею управляющих сил простой для понимания и реализации, но все еще способной производить очень сложные модели движения.

Для понимания данной статьи необходимо понимание некоторых математических действий над векторами, я опишу их функциями на javascript в контексте 2-х мерных векторов.

Сложение векторов

Для сложения векторов v1 и v2 необходимо выполнить покомпонентное их сложение. В результате сложения векторов получаем вектор:

function getVectorSum(v1, v2) {
    return {
        x: v1.x + v2.x, 
        y: v1.y + v2.y
    };
}

Вычитание векторов

Для вычитания векторов v1 и v2 необходимо выполнить покомпонентное их вычитание. В результате вычитания векторов получаем вектор:

function getVectorSub(v1, v2) {
    return {
        x: v1.x - v2.x, 
        y: v1.y - v2.y
    };
}

Нормализация вектора (приведение к единичному вектору)

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

function getNormalize(v) {
    let mg = getMagnitude(v);
    return {
        x: v.x / mg,
        y: v.y / mg
    };
}

Длина вектора

Получается по теореме Пифагора: квадрат гипотинузы равен сумме квадратов катетов В результате получаем скалярное число:

function getMagnitude(v){
    return Math.sqrt(
        v.x * v.x + 
        v.y * v.y
    );
}

Масштабирование вектора (умножение/деление на скаляр)

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

function getScaleUp(v, s) {
    return {
        x: v.x * s,
        y: v.y * s
    };
}

при делении вектора на скаляр все то же самое, но применяем соответственно операцию деления. В результате деления вектора на скаляр получаем вектор:

function getScaleDown(v, s) {
    return {
        x: v.x / s,
        y: v.y / s
    };
}

Скалярное произведение 2-х векторов

-Не путать с умножением на скаляр! Скалярное произведение 2-х векторов это сумма произведений компонент векторов поэтому иногда эту операцию называют внутренним произведением (inner product). В результате получаем скалярное число:

function getInnerProduct(v1, v2) {
    return v1.x * v2.x + v1.y * v2.y;
}

Скалярное произведение ортогональных (перпендикулярных) векторов всегда = 0! Этот факт можно удобно использовать: если скалярное произведение двух векторов = 0, то векторы ортогональны пример:

a = {1; 2} и b = {2; -1} ортогональны:
a · b = 1 · 2 + 2 · (-1) = 2 - 2 = 0!

Проекция вектора на вектор

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

// получить проекцию v1 на v2
function getProjection(v1, v2) {
    return getInnerProduct(
        v1, getNormalize(v2)
    );
}

Усечение вектора

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

function truncate(val) {
    if (this.magnitude() > val) {
        return this.normalize()
            .scaleUp(val);
    }
    return this;
}

Положение, скорость и движение.

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

Кстати, позиция персонажа тоже описывается вектором, в этом случае направление вектора игнорируется, важны лишь значения его компонент x и y

Персонажа располагается на в координате ху со скоростью AB

На рисунке выше показан персонаж, расположенный в координате P(x,y), двигающийся со скоростью V(a,b). Движение рассчитывается с использованием интеграции Эйлера (прочтение статьи по ссылке чревато травмой мозга - я там непонял ни строчки, но идея интуитивно проста: мы просто приращаваем на каждой итерации игрового цикла к текущей позиции персонажа величину его скорости):

Здесь и далее математические операторы +,-,*,/ и т.п будут использоваться, как если бы они были перегружены для работы с 2д векторами - это упростит примеры.

position = position + velocity

Направление вектора скорости будет контролировать то, куда движется персонаж, в то время, как длина вектора скорости (величина или магнитуда) будет контролировать, то как быстро он будет двигаться каждый кадр, т.е. это фактически величина шага персонажа.

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

Давайте посмотрим на картинку выше еще раз. Допустим, что мы переместили цель - курсор мыши (target) тогда персонаж (красный квадрат) будет двигаться к цели.

Это иллюстрирует поведение "рыскания" в чистом виде, без какого-либо подруливающего поведения. Зеленая линия представляет собой вектор скорости, который вычисляется следующим образом:

// получаем вектор-разницу позиций персонажа и цели и умножаем его на скорость
velocity = normalize(target - position) * max_velocity

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

Кликните мышкой на холсте, чтобы цель начала двигаться:

Расчет сил

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

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

Поведения поиска включает в себя две силы: желаемую скорость (desired velocity) и управляющую силу (steering):

Рулевые силы

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

Управляющая сила steering это результат вычитания желаемой скорости из текущей скорости. Эта сила также толкает персонажа в сторону цели.

Описанные силы вычисляются следующим образом:

// Это как в предыдущих примерах
desired_velocity = normalize(target - position) * max_velocity
steering = desired_velocity - velocity

Добавление сил

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

Добавление этих сил и вычисления конечных векторов скорости и положения:

steering = truncate(steering, max_force)
steering = steering / mass

velocity = truncate(velocity + steering, max_speed)
position = position + velocity

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

Кликните мышкой на холсте, чтобы цели начали двигаться:

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

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

Не сложно заметить, что объект с большей массой более неповоротлив чем с меньшей.

Оригинал статьи здесь: Understanding Steering Behaviors: Seek