Ball Physics: сглаживание окончательных отскоков, когда мяч останавливается

12

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

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

Я могу понять, почему это происходит, но я не могу сгладить это.

Буду признателен за любые советы, которые могут быть предложены.

Мой код обновления:

public void Update()
    {
        // Apply gravity if we're not already on the ground
        if(Position.Y < GraphicsViewport.Height - Texture.Height)
        {
            Velocity += Physics.Gravity.Force;
        }            
        Velocity *= Physics.Air.Resistance;
        Position += Velocity;

        if (Position.X < 0 || Position.X > GraphicsViewport.Width - Texture.Width)
        {
            // We've hit a vertical (side) boundary
            // Apply friction
            Velocity *= Physics.Surfaces.Concrete;

            // Invert velocity
            Velocity.X = -Velocity.X;
            Position.X = Position.X + Velocity.X;
        }

        if (Position.Y < 0 || Position.Y > GraphicsViewport.Height - Texture.Height)
        {
            // We've hit a horizontal boundary
            // Apply friction
            Velocity *= Physics.Surfaces.Grass;

            // Invert Velocity
            Velocity.Y = -Velocity.Y;
            Position.Y = Position.Y + Velocity.Y;
        }
    }

Возможно, я должен также указать на это Gravity, Resistance Grassи Concreteвсе они типа Vector2.

Ste
источник
Просто чтобы подтвердить это: ваше «трение», когда мяч ударяется о поверхность, имеет значение <1, что в принципе является правильным для коэффициента восстановления ?
Хорхе Лейтао
@ JCLeitão - Правильно.
Ste
Пожалуйста, не ругайтесь, чтобы соблюдать голоса, когда вы присуждаете награду и правильный ответ. Пойти за все, что вам помогло.
aaaaaaaaaaaa
Это плохой способ справиться с щедростью, в основном вы говорите, что не можете судить себя, поэтому вы позволяете решающим голосам ... В любом случае, то, что вы испытываете, - это обычный джиттер при столкновении. Эту проблему можно решить, установив максимальную величину взаимопроникновения, минимальную скорость или любой другой вид «предела», который когда-либо достигнут, заставит вашу программу остановить движение и поставить объект на покой. Вы также можете добавить статус покоя для ваших объектов, чтобы избежать бесполезных проверок.
Darkwings
@Darkwings - я думаю, что сообщество в этом сценарии лучше меня знает, что является лучшим ответом. Вот почему противники будут влиять на мое решение. Очевидно, что если бы я попробовал решение с наибольшим количеством голосов, и оно мне не помогло, я бы не стал отвечать на этот ответ.
Ste

Ответы:

19

Вот шаги, необходимые для улучшения вашего цикла симуляции физики.

1. Временной шаг

Основная проблема, которую я вижу в вашем коде, заключается в том, что он не учитывает время шага по физике. Должно быть очевидно, что здесь что-то не так, Position += Velocity;потому что единицы не совпадают. Либо Velocityэто на самом деле не скорость, либо чего-то не хватает.

Даже если ваши значения скорости и гравитации масштабируются таким образом, чтобы каждый кадр происходил в единицу времени 1(что означает, например, что Velocityфактически означает расстояние, пройденное за одну секунду), время должно появляться где-то в вашем коде, либо неявно (путем фиксации переменных так, чтобы их имена отражают то, что они на самом деле хранят) или явно (путем введения временного шага). Я считаю, что проще всего объявить единицу времени:

float TimeStep = 1.0;

И используйте это значение везде, где это необходимо:

Velocity += Physics.Gravity.Force * TimeStep;
Position += Velocity * TimeStep;
...

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

Сейчас Position += Velocity * TimeStepвсе еще не совсем точно (см. Этот вопрос, чтобы понять, почему), но это, вероятно, будет делать пока.

Кроме того, это должно учитывать время:

Velocity *= Physics.Air.Resistance;

Это немного сложнее исправить; Один из возможных способов:

Velocity -= Vector2(Math.Pow(Physics.Air.Resistance.X, TimeStep),
                    Math.Pow(Physics.Air.Resistance.Y, TimeStep))
          * Velocity;

2. Двойные обновления

Теперь проверьте, что вы делаете, когда подпрыгиваете (показан только соответствующий код):

Position += Velocity * TimeStep;
if (Position.Y < 0)
{
    Velocity.Y = -Velocity.Y * Physics.Surfaces.Grass;
    Position.Y = Position.Y + Velocity.Y * TimeStep;
}

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

Position += Velocity * TimeStep;
if (Position.Y < 0)
{
    /* First, stop at Y = 0 and count how much time is left */
    float RemainingTime = -Position.Y / Velocity.Y;
    Position.Y = 0;

    /* Then, start from Y = 0 and only use how much time was left */
    Velocity.Y = -Velocity.Y * Physics.Surfaces.Grass;
    Position.Y = Velocity.Y * RemainingTime;
}

3. Гравитация

Проверьте эту часть кода сейчас:

if(Position.Y < GraphicsViewport.Height - Texture.Height)
{
    Velocity += Physics.Gravity.Force * TimeStep;
}            

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

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

4. Фиксированный код

А вот и полностью обновленный код:

public void Update()
{
    float TimeStep = 1.0;
    Update(TimeStep);
}

public void Update(float TimeStep)
{
    float RemainingTime;

    // Apply gravity if we're not already on the ground
    if(Position.Y < GraphicsViewport.Height - Texture.Height)
    {
        Velocity += Physics.Gravity.Force * TimeStep;
    }
    Velocity -= Vector2(Math.Pow(Physics.Air.Resistance.X, RemainingTime),
                        Math.Pow(Physics.Air.Resistance.Y, RemainingTime))
              * Velocity;
    Position += Velocity * TimeStep;

    if (Position.X < 0 || Position.X > GraphicsViewport.Width - Texture.Width)
    {
        // We've hit a vertical (side) boundary
        if (Position.X < 0)
        {
            RemainingTime = -Position.X / Velocity.X;
            Position.X = 0;
        }
        else
        {
            RemainingTime = (Position.X - (GraphicsViewport.Width - Texture.Width)) / Velocity.X;
            Position.X = GraphicsViewport.Width - Texture.Width;
        }

        // Apply friction
        Velocity -= Vector2(Math.Pow(Physics.Surfaces.Concrete.X, RemainingTime),
                            Math.Pow(Physics.Surfaces.Concrete.Y, RemainingTime))
                  * Velocity;

        // Invert velocity
        Velocity.X = -Velocity.X;
        Position.X = Position.X + Velocity.X * RemainingTime;
    }

    if (Position.Y < 0 || Position.Y > GraphicsViewport.Height - Texture.Height)
    {
        // We've hit a horizontal boundary
        if (Position.Y < 0)
        {
            RemainingTime = -Position.Y / Velocity.Y;
            Position.Y = 0;
        }
        else
        {
            RemainingTime = (Position.Y - (GraphicsViewport.Height - Texture.Height)) / Velocity.Y;
            Position.Y = GraphicsViewport.Height - Texture.Height;
        }

        // Remove excess gravity
        Velocity.Y -= RemainingTime * Physics.Gravity.Force;

        // Apply friction
        Velocity -= Vector2(Math.Pow(Physics.Surfaces.Grass.X, RemainingTime),
                            Math.Pow(Physics.Surfaces.Grass.Y, RemainingTime))
                  * Velocity;

        // Invert velocity
        Velocity.Y = -Velocity.Y;

        // Re-add excess gravity
        float OldVelocityY = Velocity.Y;
        Velocity.Y += RemainingTime * Physics.Gravity.Force;
        // If velocity changed sign again, clamp it to zero
        if (Velocity.Y * OldVelocityY <= 0)
            Velocity.Y = 0;

        Position.Y = Position.Y + Velocity.Y * RemainingTime;
    }
}

5. Дальнейшие дополнения

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

public void Update()
{
    float TimeStep = 1.0;
    Update(TimeStep / 4);
    Update(TimeStep / 4);
    Update(TimeStep / 4);
    Update(TimeStep / 4);
}
Сэм Хоцевар
источник
«Время должно появиться где-то в вашем коде.» Вы рекламируете, что умножение на 1 повсеместно - это не просто хорошая идея, это обязательно? Конечно, регулируемый временной шаг - хорошая функция, но она, безусловно, не обязательна.
аааааааааааа
@eBusiness: мой аргумент в большей степени касается согласованности и обнаружения ошибок, чем регулируемых временных шагов. Я не говорю, что умножение на 1 необходимо, я говорю, что velocity += gravityэто неправильно и velocity += gravity * timestepимеет смысл. Это может дать тот же результат в конце, но без комментария, говорящего «Я знаю, что я здесь делаю», это все равно означает ошибку кодирования, неаккуратный программист, отсутствие знаний о физике или просто прототип кода, который должен быть улучшенным.
Сэм Хоцевар
Вы говорите, что это неправильно , когда якобы хочу сказать, что это плохая практика. Это ваше субъективное мнение по этому вопросу, и хорошо, что вы выражаете его, но оно СУБЪЕКТИВНО, поскольку код в этом отношении делает именно то, для чего предназначен. Все, что я прошу, - это чтобы вы четко понимали разницу между субъективным и объективным в своем посте.
аааааааааааа
2
@eBusiness: если честно, то это неправильно по любому стандарту здравомыслящий. Код вовсе не «делает так, как задумано», потому что 1) добавление скорости и гравитации на самом деле ничего не значит; и 2) если он дает разумный результат, то это потому, что значение, хранимое в нем, gravityна самом деле ... не гравитация. Но я могу сделать это более ясным в посте.
Сэм Хоцевар
Напротив, называть это неправильно - это неправильно по любым нормам. Вы правы, что гравитация не хранится в переменной, называемой гравитацией, вместо этого есть число, и это все, что когда-либо будет, оно не имеет никакого отношения к физике, кроме того, которое, как мы представляем, оно имеет, умножая его на другое число не меняет этого. По-видимому, это меняет вашу способность и / или готовность установить ментальную связь между кодом и физикой. Кстати довольно интересное психологическое наблюдение.
аааааааааааа
6

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

MIN_BOUNCE = <0.01 e.g>;

if( Velocity.Y < MIN_BOUNCE ){
    Velocity.Y = 0;
    Position.Y = <ground position Y>;
}
Zhen
источник
3
Мне нравится это решение, но я бы не стал ограничивать отскок по оси Y. Я бы вычислил нормаль коллайдера в точке столкновения и проверил, больше ли величина скорости столкновения, чем порог отскока. Даже если мир OP допускает только отскоки Y, другие пользователи могут найти более общее решение полезным. (Если мне неясно, подумайте о том, чтобы соединить две сферы друг с другом в случайной точке)
Брэндон
@brandon, отлично, с нормальным должно работать лучше.
Жен
1
@Zhen, если вы используете нормаль поверхности, у вас есть шанс, что у вас может получиться, что шар прилипнет к поверхности, у которой есть нормаль, не параллельная гравитации. Я постараюсь учесть гравитацию в расчете, если это возможно.
Ник Фостер
Ни одно из этих решений не должно устанавливать какие-либо скорости на 0. Вы ограничиваете отражение по нормали вектора в зависимости от порога отскока
brandon
1

Итак, я думаю, что проблема в том, что это происходит, когда ваш мяч приближается к пределу. Математически мяч никогда не останавливается на поверхности, он приближается к поверхности.

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

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

@Zhen ответ будет хорошо, если ваша система однородна, а это не так. Он имеет некоторую гравитацию на оси Y.

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

Эта сила является вкладом силы, приложенной стенкой к мячу, и силой тяжести.

Условие должно быть что-то вроде

если (newVelocity + Physics.Gravity.Force <порог)

обратите внимание, что newVelocity.y является положительной величиной, если отскок находится на стенке дна, а гравитация - отрицательной величиной.

Также обратите внимание, что newVelocity и Physics.Gravity.Force не имеют такие же размеры, как вы написали в

Velocity += Physics.Gravity.Force;

Это означает, что, как и вы, я предполагаю, что delta_time = 1 и ballMass = 1.

Надеюсь это поможет

Хорхе Лейтао
источник
1

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

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

Простой способ решения этой проблемы - проверить, что мяч движется в правильном направлении, прежде чем менять его.

Таким образом вы должны сделать:

if (Position.X < 0 || Position.X > GraphicsViewport.Width - Texture.Width)

В:

if ((Position.X < 0 && Velocity.X < 0) || (Position.X > GraphicsViewport.Width - Texture.Width && Velocity.X > 0))

И похоже на направление Y.

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

Velocity += Physics.Gravity.Force;
if(Position.Y > GraphicsViewport.Height - Texture.Height && Velocity.Y > 0)
{
    Velocity.Y = 0;
}

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

AAAAAAAAAAAA
источник
0

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

public Vector3 Velocity
{
    public get { return velocity; }
    public set
    {
        velocity = value;

        // We get the direction that gravity pulls in
        Vector3 GravityDirection = gravity;
        GravityDirection.Normalize();

        Vector3 VelocityDirection = velocity;
        VelocityDirection.Normalize();

        if ((velocity * GravityDirection).SquaredLength() < 0.25f)
        {
            velocity.Y = 0.0f;
        }            
    }
}
private Vector3 velocity;

В приведенном выше методе мы ограничиваем отскок всякий раз, когда он находится на одной оси с гравитацией.

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

Ник Фостер
источник
Я не буду понижать голос, потому что это действительно, но вопрос задает пороги отказов, а не пороги скорости. По моему опыту, они почти всегда разные, потому что эффект дрожания во время подпрыгивания, как правило, отделен от эффекта продолжения расчета скорости, когда она визуально в покое.
Брэндон
Они один в одном. Физические движки, такие как Havok или PhysX, и JigLibX основывают восстановление на линейной скорости (и угловой скорости). Этот метод должен работать для любого движения мяча, включая отскок. Фактически, последний проект, в котором я участвовал (LEGO Universe), использовал метод, почти идентичный этому, чтобы остановить подпрыгивание монет, когда они замедлились. В этом случае мы не использовали динамическую физику, поэтому нам пришлось делать это вручную, а не позволять Havok позаботиться об этом за нас.
Ник Фостер
@NicFoster: Я в замешательстве, так как, на мой взгляд, объект может перемещаться очень быстро по горизонтали и почти не по вертикали, в этом случае ваш метод не сработает. Я думаю, что OP хотел бы, чтобы вертикальное расстояние было установлено равным нулю, несмотря на большую длину скорости.
Джордж Дакетт
@ GeorgeDuckett: Ах, спасибо, я неправильно понял оригинальный вопрос. ОП не хочет, чтобы шар прекратил движение, просто остановите вертикальное движение. Я обновил ответ, чтобы учесть только прыгающую скорость.
Ник Фостер
0

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

Лорен Печтель
источник