2010-01-05 2 views
6

Это на C++.Двойная буферизация для игровых объектов, что такое чистый чистый общий C++-способ?

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

Вопрос в том, что является хорошим перспективным и универсальным способом ООП, чтобы разоблачить это для моих классов, пытаясь максимально скрыть детали реализации? Хотелось бы узнать ваши мысли и соображения.

Я думал, что можно использовать перегрузку оператора, но как мне перегрузить назначение для члена шаблонного класса в моем классе буфера?

, например, я думаю, что это пример того, что я хочу:

doublebuffer<Vector3> data; 
data.x=5; //would write to the member x within the new buffer 
int a=data.x; //would read from the old buffer's x member 
data.x+=1; //I guess this shouldn't be allowed 

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

Это то, что я рассматривал:

template <class T> 
class doublebuffer{ 
    T T1; 
    T T2; 
    T * current=T1; 
    T * old=T2; 
public: 
    doublebuffer(); 
    ~doublebuffer(); 
    void swap(); 
    operator=()?... 
}; 

и объект игра будет выглядеть так:

struct MyObjectData{ 
    int x; 
    float afloat; 
} 

class MyObject: public Node { 
    doublebuffer<MyObjectData> data; 

    functions... 
} 

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

+0

небольшое изменение для ясности – gtrak

+4

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

+0

Да, более сложная версия моей первоначальной идеи заключалась в том, что я мог бы иметь оператор + оператор вызова + в соответствующем члене соответствующей структуры данных и передать аргумент. Я понимаю, что вы имеете в виду, это может запутать. Преимущество в моем сознании - двойные буферизированные данные могут стать заменой нормальных данных, если я использую это, если я применяю некоторые правила, такие как члены, могут быть только числами или агрегатами. – gtrak

ответ

5

Недавно я рассмотрел подобное желание обобщенным способом путем «моментальной съемки» структуры данных, которая использовала Copy-On-Write под капотом. Аспект, который мне нравится в этой стратегии, заключается в том, что вы можете сделать много снимков, если они вам нужны, или просто иметь один за раз, чтобы получить ваш «двойной буфер».

Без потоотделения слишком много деталей реализации, вот некоторый псевдокод:

snapshottable<Vector3> data; 
data.writable().x = 5; // write to the member x 

// take read-only snapshot 
const snapshottable<Vector3>::snapshot snap (data.createSnapshot()); 

// since no writes have happened yet, snap and data point to the same object 

int a = snap.x; //would read from the old buffer's x member, e.g. 5 

data.writable().x += 1; //this non-const access triggers a copy 

// data & snap are now pointing to different objects in memory 
// data.readable().x == 6, while snap.x == 5 

В вашем случае, вы бы снимок своего состояния и передать его визуализацию. Затем вы разрешите вашему обновлению работать с исходным объектом. Чтение с помощью const-доступа через readable() не приведет к копированию ... при доступе с помощью writable() будет вызвать копию.

Для этого я использовал некоторые трюки поверх QSharedDataPointer Qt. Они различают const и non-const доступ через (- >), так что чтение из объекта const не приведет к копированию на механизме записи.

+0

ooo, очень интересно, я обязательно расследую это, спасибо. – gtrak

+0

звучит так, будто он может сэкономить некоторую память – gtrak

+0

Вы можете делать копии при записи разными способами, но если вы заинтересованы в потокобезопасном решении с использованием QSharedDataPointer, мой проект под строительство с открытым исходным кодом. Некоторые из дьяволов в деталях находятся в snapshottable.h: http://gitorious.org/thinker-qt/thinker-qt/blobs/master/include/thinkerqt/snapshottable.h – HostileFork

5

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

Не совсем понятно, что ваша схема особенно помогает в нескольких потоках записи в любом случае - как вы знаете, какой из них «выигрывает», когда несколько потоков читают старое состояние и записываются в одно и то же новое состояние, перезаписывая любые более ранние записи?

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

+0

спасибо, да, несколько писем. Мне нужно будет решить позже. Я просто хочу, чтобы это было легко, когда у меня было что-то вроде сумасшедшей физики, но разве эта проблема не была бы независимой от этой организации данных? – gtrak

2

Я не уверен, что наличие эффективны два состояния будет означать, что вам не нужны какие-либо синхронизации при доступе к записываемому состоянию, если у вас есть несколько потоков писать, но ...

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

class MyRealState { 
    int data1; 
    ... etc 

    protected: 
     void copyFrom(MyRealState other) { data1 = other.data1; } 

    public: 
     virtual int getData1() { return data1; } 
     virtual void setData1(int d) { data1 = d; } 
} 

class DoubleBufferedState : public MyRealState { 
    MyRealState readOnly; 
    MyRealState writable; 

    public: 
     // some sensible constructor 

     // deref all basic getters to readOnly 
     int getData1() { return readOnly.getData1(); } 

     // if you really need to know value as changed by others 
     int getWritableData1() { return writable.getData1(); } 

     // writes always go to the correct one 
     void setData1(int d) { writable.setData1(d); } 

     void swap() { readOnly.copyFrom(writable); } 
     MyRealState getReadOnly() { return readOnly; } 
} 

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

Это дает вам версию состояния только для чтения, которая будет меняться только при вызове swap и чистом интерфейсе, при котором вызывающий может игнорировать проблему двойного буфера при работе с состоянием (все, что не нуждается в знании старого и новые состояния могут взаимодействовать с интерфейсом MyRealState), или вы можете отключить/потребовать интерфейс DoubleBufferedState, если вы заботитесь о состоянии до и после состояний (что, вероятно, imho).

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

Извините за любые ошибки синтаксиса C++, теперь я немного Java-человек.

+0

Спасибо, я думаю, что что-то вроде этого - то, что я ищу, возможно, чтобы сохранить некоторое повторение, я могу использовать шаблонное наследование от всех моих различных объектов состояния. – gtrak

+0

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

2

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

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

+0

Не нужно явно синхронизировать их, обязательно. Например, я могу заставить свой физический движок читать все старые буферы ... выполнять свои операции и записывать в новое. Первое, что меняет объекты в моем цикле игры, должно сделать достаточно вычислений, чтобы синхронизировать их. – gtrak

1

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

Что касается мелких предметов, возможно, подходит модель Flyweight.

+0

Хмм, это интересная идея. Прямо сейчас у меня есть моя основная структура данных как граф сцены, и узлы владеют данными. Если я правильно понимаю, ваше предложение потребует отделить данные от другой структуры данных и связать узлы с ней, но это может также дать некоторые преимущества. Я этого не думал. – gtrak

1

Вы должны сделать две вещи:

  1. собственное состояние отдельного объекта и его связь с другими объектами
  2. использование КПС для собственного состояния объекта

Почему?

Для рендеринга вам нужны только свойства объекта «back-version», которые влияют на рендеринг (например, положение, ориентация и т. Д.), Но вам не нужны объектные отношения.Это позволит вам освободиться от оборванных указателей и позволить обновить состояние игры. COW (copy-on-write) должен быть 1-уровневым, потому что вам нужен только один «другой» буфер.

Короткий: Я считаю, что выбор перегрузки оператора полностью ортогонален этой проблеме. Это всего лишь синтетический сахар. Если вы пишете + = или setNewState, это совершенно не имеет значения, так как оба используют одно и то же время процессора.

+0

Я немного опаздываю на вечеринку, но уверены ли вы, что объектные отношения не влияют на рендеринг? Как насчет изменчивого графика сцены? Соотношения в графе могут меняться между кадрами (например, добавление/удаление объекта) и, таким образом, влиять на рендеринг, предполагая, что поток рендеринга пересекает граф сцены, чтобы определить необходимые операции рендеринга. Единственная альтернатива, в которой поток обновлений перечисляет визуализируемые объекты сцены в резервном буферизованном списке, уменьшает параллельность, перемещая работу рендеринга в поток обновлений. – Dylan

+0

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

1

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

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

В то время как вы можете иметь два перегруженных -> дифференцированы const -ness, я хотел бы предостеречь против этого, поскольку это является проблематичным для действий чтения. Перегрузка выбирается путем ссылки на объект через константу или неконстантную ссылку, а не на то, действительно ли действие является фактически прочитанным или записывается. Этот факт делает склонность к ошибкам.

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

Этот класс хранит несколько экземпляров параметра шаблона T и сохраняет смещение, чтобы различные аксессоры могли извлекать передний/активный буфер или другие буферы путем относительного смещения. Использование параметра шаблона n == 1 означает, что существует только один экземпляр T, и мульти-буферизация эффективно отключена.

template< class T, std::size_t n > 
struct MultiBuffer 
{ 
    MultiBuffer() : _active_offset(0) {} 

    void ChangeBuffers() { ++_active_offset; } 
    T* GetInstance(std::size_t k) { return &_objects[ (_active_offset + k) % n ]; } 

private: 
    T _objects[n]; 
    std::size_t _active_offset; 
}; 

Этот класс абстрагирует выбор буфера. Он ссылается на MultiBuffer посредством ссылки, поэтому вы должны гарантировать, что его срок службы короче, чем MultiBuffer, который он использует. Он имеет собственное смещение, которое добавляется к смещению MultiBuffer, так что разные BufferAccess могут ссылаться на разные элементы массива (например, параметр шаблона n = 0 для доступа к фронт-буфера и 1 для доступа к буферу назад).

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

template< class T, std::size_t n > 
class BufferAccess 
{ 
public: 
    BufferAccess(MultiBuffer< T, n >& buf, std::size_t offset) 
     : _buffer(buf), _offset(offset) 
    { 
    } 

    T* operator->() const 
    { 
     return _buffer.GetInstance(_offset); 
    } 

private: 
    MultiBuffer< T, n >& _buffer; 
    const std::size_t _offset; 
}; 

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

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

class TestClass 
{ 
public: 
    TestClass() : _n(0) {} 

    int get() const { return _n; } 
    void set(int n) { _n = n; } 

private: 
    int _n; 
}; 

#include <iostream> 
#include <ostream> 

int main() 
{ 
    const std::size_t buffers = 2; 

    MultiBuffer<TestClass, buffers> mbuf; 

    BufferAccess<TestClass, buffers> frontBuffer(mbuf, 0); 
    BufferAccess<TestClass, buffers> backBuffer(mbuf, 1); 

    std::cout << "set front to 5\n"; 
    frontBuffer->set(5); 

    std::cout << "back = " << backBuffer->get() << '\n'; 

    std::cout << "swap buffers\n"; 
    ++mbuf.offset; 

    std::cout << "set front to 10\n"; 
    frontBuffer->set(10); 

    std::cout << "back = " << backBuffer->get() << '\n'; 
    std::cout << "front = " << frontBuffer->get() << '\n'; 

    return 0; 
} 
Смежные вопросы