Фізика м'яча: згладжування фінальних відмов, коли м'яч відпочиває


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.


Тільки для підтвердження цього: ваше "тертя", коли куля потрапляє на поверхню, - це значення <1, яке в основному є коефіцієнтом реституції ?
Хорхе Лейтао

@ JCLeitão - правильно.
Ste

Будь ласка, не присягайте дотримуватися голосів, коли ви присуджуєте нагороду та правильну відповідь. Ідіть за тим, що вам допомогло.
aaaaaaaaaaaaa

Це поганий спосіб поводження з виграшем, в основному ви говорите, що не можете судити про себе, і ви даєте вирішення результатів ... У будь-якому випадку те, що ви переживаєте, - це звичайне тремтіння зіткнення. Це можна вирішити, встановивши максимальну величину взаємопроникнення, мінімальну швидкість або будь-яку іншу форму "обмеження", яка колись буде досягнута, призведе до того, що ваша рутина зупинить рух і покладе об'єкт на відпочинок. Ви також можете додати своїм об'єктам статус спокою, щоб уникнути марних перевірок.
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має сенс. Це може дати такий самий результат у кінцевому підсумку, але без коментаря "Я знаю, що я тут роблю", це все ще означає помилку кодування, неохайний програміст, відсутність знань про фізику або просто код прототипу, який потрібно удосконалюватися.
sam hocevar

Ти кажеш, що це неправильно , коли те, що ти нібито хочеш сказати, - це погана практика. Це ваша суб'єктивна думка з цього питання, і добре, що ви висловите її, але це суб'єктивно, оскільки код у цьому плані робить саме так, як це було призначено. Все, що я прошу, - це ви робите різницю між суб'єктивним та об'єктивним у своїй посаді.
ааааааааааа

2
@eBusiness: якщо чесно, то це неправильно з будь-якого стандарту розсудлива. Код взагалі не "робить так, як це було призначено", тому що 1) додавання швидкості та сили тяжіння насправді нічого не означає; і 2) якщо це дає розумний результат, це тому, що значення, збережене в gravity, насправді…, а не гравітація. Але я можу зробити це зрозумілішим у пості.
sam hocevar

Навпаки, називати це неправильним помилковим будь-яким нормальним стандартом. Ви маєте рацію, що гравітація не зберігається у змінній, що називається гравітацією, натомість є число, і це все, що коли-небудь буде, вона не має жодного відношення до фізики, крім того, яке відношення ми уявляємо, воно помножує її на інше число цього не змінює. Що, здавалося б, змінює, це ваша здатність та / або бажання встановити ментальний зв'язок між кодом і фізикою. До речі, досить цікаве психологічне спостереження.
aaaaaaaaaaaa

6

Додайте чек, щоб зупинити відмов, використовуючи мінімальну вертикальну швидкість. І коли ви отримаєте мінімальний відмов, встановіть кулю в землю.

MIN_BOUNCE = <0.01 e.g>;

if( Velocity.Y < MIN_BOUNCE ){
    Velocity.Y = 0;
    Position.Y = <ground position Y>;
}

3
Мені подобається це рішення, але я не обмежував би відхилення до осі Y. Я би обчислював норму колайдера в точці зіткнення і перевіряв, чи величина швидкості зіткнення більша за поріг відскоку. Навіть якщо світ ОП дозволяє лише відмов Y, інші користувачі можуть знайти корисніше загальне рішення. (Якщо мені щось незрозуміло, подумайте про стрибок двох сфер у випадковій точці)
Брендон

@brandon, чудово, він повинен працювати краще з нормальним.
Чень

1
@Zhen, якщо ви використовуєте нормальну поверхню, ви маєте шанс, що м'яч у вас може прилипати до поверхні, яка має нормальне значення, не паралельне гравітації. Я б спробував врахувати гравітацію, якщо це можливо.
Нік Фостер

Жоден із цих рішень не повинен встановлювати жодної швидкості до 0. Ви лише обмежуєте віддзеркалення через норму вектора залежно від порогу відмов
брендон

1

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

Однак у вашій грі не використовується безперервний час. Це карта, яка використовує наближення до диференціального рівняння. І це наближення не є дійсним у цій обмежуючій ситуації (ви можете, але вам доведеться вжити менших та менших часових кроків, що, на мою думку, неможливо.

Фізично кажучи, що трапляється так, що коли куля знаходиться дуже близько до поверхні, вона прилипає до неї, якщо сумарна сила знаходиться нижче заданого порогу.

@Zhen відповідь буде добре, якщо ваша система є однорідною, а це не так. Він має деяку гравітацію на вісь y.

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

Ця сила - це внесок сили, що чинить стіна на кулю + сила тяжіння.

Тоді умова має бути чимось подібним

if (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;
}

Ці загальні зміни повинні дати вам гідне моделювання. Але зауважте, що це все ще дуже просте моделювання.


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), використовував метод, майже ідентичний цьому, щоб зупинити підстрибування монет, коли вони сповільнилися. У цьому випадку ми не використовували динамічну фізику, тому нам довелося це робити вручну, а не дозволяти Хавоку піклуватися про нас.
Нік Фостер

@ NicFoster: Я розгублений, оскільки, на мій погляд, об’єкт міг би рухатися дуже швидко по горизонталі і навряд чи зовсім вертикально, і в цьому випадку ваш метод не запускатиметься. Я думаю, що ОП хотів би, щоб вертикальна відстань була встановлена ​​на нуль, незважаючи на високу довжину швидкості.
Джордж Дакетт

@GeorgeDuckett: А, дякую, я неправильно зрозумів оригінальне запитання. ОП не хоче, щоб м'яч переставав рухатися, просто зупиніть вертикальний рух. Я оновив відповідь, щоб враховувати лише швидкість підстрибування.
Нік Фостер

0

Інша річ: ви множите на постійну тертя. Змініть це - зменшіть константу тертя, але додайте фіксованого поглинання енергії на відмов. Це набагато швидше змочить останні останні відскоки.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.