Структури даних для інтерполяції та нитки?


20

Останнім часом я маю справу з деякими проблемами тремтіння частоти кадрів зі своєю грою, і, здається, найкращим рішенням було б те, що запропонував Гленн Фідлер (Gaffer on Games) у класичному « Виправити свій графік часу! стаття.

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

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

Очевидно, мені потрібно буде зберігати (де? / Як?) Дві копії інформації про стан гри, що стосуються мого рендерінга, щоб вона могла інтерполювати між ними.

Додатково - це здається гарним місцем для додавання ниток. Я думаю, що потік оновлення може працювати над третьою копією ігрового стану, залишаючи інші дві копії лише для читання для потоку візуалізації. (Це гарна ідея?)

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

Особливу увагу, на мою думку, є проблема, як обробляти додавання та видалення об'єктів із ігрового стану.

Нарешті, здається, що якийсь стан або безпосередньо не потрібен для візуалізації, або буде занадто складно відслідковувати різні версії (наприклад: сторонній фізичний двигун, який зберігає один стан) - тому мені було б цікаво знати, як люди обробляли такі дані в такій системі.

Відповіді:


4

Не намагайтеся повторити весь стан гри. Інтерполяція це була б кошмаром. Просто виділіть деталі, які є змінними та потрібними для візуалізації (назвемо це "візуальним станом").

Для кожного класу об'єктів створіть супровідний клас, який зможе вмістити об'єкт Visual State. Цей об'єкт буде вироблятися за допомогою моделювання та споживатися при візуалізації. Інтерполяція буде легко вставлятися між ними. Якщо стан є незмінним і передається за значенням, у вас не виникне проблем із ниткою.

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

Приклад

Традиційний дизайн

class Actor
{
  Matrix4x3 position;
  float fuel;
  float armor;
  float stamina;
  float age;

  void Simulate(float deltaT)
  {
    age += deltaT;
    armor -= HitByAWeapon();
  }
}

Використання стану Visual

class IVisualState
{
  public:
  virtual void Interpolate(const IVisualState &newVS, float f) {}
};
class Actor
{
  struct VisualState: public IVisualState
  {
    Matrix4x3 position;
    float fuel;
    float armor;
    float stamina;
    float age;

    virtual auto_ptr<IVisualState> Interpolate(const IVisualState &newVS, float f)
    {
      const VisualState &newState = static_cast<const VisualState &>(newVS);
      IVisualState *ret = new VisualState;
      ret->age = lerp(this->age,newState.age);
      // ... interpolate other properties as well, using any suitable interpolation method
      // liner, spline, slerp, whatever works best for the given property
      return ret;
    };
  };

  auto_ptr<VisualState> state_;

  void Simulate(float deltaT)
  {
    state_->age += deltaT;
    state_->armor -= HitByAWeapon();
  }
}

1
Ваш приклад було б легше прочитати, якби ви не використовували "new" (зарезервоване слово в C ++) як ім'я параметра.
Steve S

3

Моє рішення набагато менш елегантне / складніше, ніж більшість. Я використовую Box2D як двигун фізики, тому зберігання більше однієї копії стану системи не піддається керуванню (клонуйте фізичну систему, тоді спробуйте тримати їх синхронізувати. Можливо, буде кращий спосіб, але я не міг придумати один).

Натомість я продовжую працювати лічильником покоління фізики . Кожне оновлення збільшує покоління фізики, коли фізична система подвійно оновлюється, а лічильник поколінь подвоюється також.

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

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

Коли я писав інтерполяційні матеріали, я працював із графікою на 60 Гц та фізикою на 30 ГГц. Виявляється, Box2D набагато стійкіший, коли він працює на 120 Гц. Через це мій інтерполяційний код отримує дуже мало користі. При подвоєнні цільової частоти кадрів фізика в середньому оновлюється двічі на кадр. З тремтінням, яке також може бути 1 або 3 рази, але майже ніколи 0 або 4+. Більш висока швидкість фізики фіксує проблему інтерполяції сама по собі. Під час роботи фізики та частоти кадрів при 60 Гц ви можете отримати 0-2 оновлення на кадр. Візуальна різниця між 0 і 2 величезна в порівнянні з 1 і 3.


3
Я також це знайшов. Фізичний цикл на 120 Гц з оновленням кадру на майже 60 Гц робить інтерполяцію майже марною. На жаль, це працює лише для набору ігор, які можуть дозволити фізичний цикл 120 Гц.

Я щойно спробував перейти на цикл оновлення 120 Гц. Це, мабуть, має подвійну перевагу - зробити мою фізику більш стабільною та зробити мою гру гладкою при не зовсім 60 ГГц частоті кадрів. Мінус у тому, що він порушує всю мою ретельно налаштовану фізику геймплея - тож це, безумовно, варіант, який потрібно вибрати на початку проекту.
Ендрю Рассел

Також: Я насправді не розумію вашого пояснення вашої системи інтерполяції. Насправді це трохи схоже на екстраполяцію?
Ендрю Рассел

Гарний дзвінок. Я фактично описав систему екстраполяції. З огляду на позицію, швидкість та час, коли минуло останнє оновлення фізики, я екстраполюю, де був би цей об'єкт, якби двигун фізики не зупинився.
deft_code

2

Я чув, що такий підхід до часових кроків пропонується досить часто, але за 10 років в іграх я ніколи не працював над реальним проектом, який спирався на фіксований графік часу та інтерполяцію.

Здається, загалом більше зусиль, ніж система змінних часових кроків (якщо припустити розумний діапазон частот, в діапазоні від 25 Гц до 100 ГГц).

Я спробував один раз підходити фіксований часовий крок + інтерполяцію для дуже малого прототипу - не врізання різьби, а логічне оновлення фіксованого часового кроку та якомога швидше візуалізація, коли це не оновлювали. Мій підхід полягав у тому, щоб було кілька класів, таких як CInterpolatedVector та CInterpolatedMatrix - які зберігали попередні / поточні значення та використовували аксесуар з коду візуалізації, щоб отримати значення для поточного часу візуалізації (яке завжди буде між попереднім та попереднім поточний час)

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

Зауважте, що деякі значення не можуть бути тривіально інтерпольовані (наприклад, "кадр анімації спрайту", "активний спеціальний ефект"). Ви можете повністю пропустити інтерполяцію, або це може спричинити проблеми, залежно від потреб вашої гри.

IMHO, найкраще просто пройти змінний часовий крок - якщо ви не робите RTS або іншу гру, де у вас величезна кількість об'єктів, і вам потрібно синхронізувати 2 незалежні симуляції для мережевих ігор (надсилаючи лише замовлення / команди через мережа, а не об'єктні позиції). У цій ситуації фіксований графік - це єдиний варіант.


1
Здається, що принаймні Quake 3 використовував цей підхід, за замовчуванням "галочка" становила 20 кадрів в секунду (50 мс).
Сума

Цікаво. Я вважаю, що це має переваги для висококонкурентних багатокористувацьких комп'ютерних ігор, щоб гарантувати, що більш швидкі ПК / більші рамки не отримують надто великої переваги (більш чуйне управління або невеликі, але експлуатовані відмінності у фізиці / поведінці при зіткненні) ?
bluescrn

1
Ви за 10 років не зіткнулися з жодною грою, яка б займалась фізикою не в замку за допомогою симуляції та візуалізації? Тому що в той момент, коли ви це зробите, вам доведеться втрутитися або прийняти сприйняті поштовхи у ваших анімаціях.
Кай

2

Очевидно, мені потрібно буде зберігати (де? / Як?) Дві копії інформації про стан гри, що стосуються мого рендерінга, щоб вона могла інтерполювати між ними.

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

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

Додатково - це здається гарним місцем для додавання ниток. Я думаю, що оновлення потоку може працювати над третьою копією ігрового стану, залишаючи інші дві копії лише для читання для потоку візуалізації. (Це гарна ідея?)

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


1

Зауважте, я насправді не розглядаю інтерполяцію, тому ця відповідь не відповідає їй; Мене просто хвилює наявність однієї копії стану гри для потоку візуалізації, а іншої для потоку оновлення. Тому я не можу коментувати питання інтерполяції, хоча ви можете змінити наступне рішення для інтерполяції.

Мені було цікаво про це, коли я проектував і думав про багатопотоковий двигун. Тому я поставив запитання щодо Stack Overflow про те, як реалізувати якусь схему дизайну "журналів" чи "транзакцій" . Я отримав кілька хороших відповідей, і прийнята відповідь дійсно змусила мене задуматися.

Важко створити непорушний предмет, оскільки всі його діти також повинні бути непорушними, і вам потрібно бути дуже обережними, щоб все було справді непорушним. Але якщо ви дійсно обережні, ви можете створити суперклас, GameStateякий містить усі дані (і піддані тощо) у вашій грі; частина "Модель" організаційного стилю Model-View-Controller.

Тоді, як каже Джеффрі , екземпляри вашого об’єкта GameState швидкі, пам'ять ефективна і безпечна для потоків. Великий мінус полягає в тому, що для того, щоб змінити щось про модель, вам начебто потрібно відтворити модель, тому вам потрібно бути дуже обережним, щоб ваш код не перетворився на величезний безлад. Встановлення змінної в об'єкті GameState до нового значення більше стосується, ніж просто var = val;, з точки зору рядків коду.

Мене це страшенно заінтригує. Вам не потрібно копіювати всю структуру даних у кожен кадр; ви просто скопіюєте покажчик на незмінне структуру. Це саме по собі дуже вражає, чи не згодні ви?


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

Динамічний розподіл у подібному випадку дуже легко зробити ефективно. Можна використовувати круговий буфер, рости з одного боку, відновлювати з другого.
Сума

... це не було б динамічним розподілом, а лише динамічним використанням попередньо виділеної пам'яті;)
Кай

1

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

Тоді я зрозумів, що мені потрібно зберегти лише три стани того, що планується винести. Тепер мій поновлювальний потік заповнює один з трьох набагато менших буферів "RenderCommands", і Renderer читає з найновішого буфера, на який зараз не записується, що заважає потокам ніколи не чекати один одного.

У моїй установці кожен RenderCommand має 3d геометрію / матеріали, матрицю перетворення та перелік світлів, які впливають на нього (все ще роблять подачу вперед).

Моїй нитці візуалізації більше не потрібно робити жодних відсічок чи обчислень на відстані, і це значно прискорило роботу на великих сценах.

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