Double buffering for game objects, what a nice clean common C ++ way?

This is in C ++.

So, I'm starting from scratch to write a game engine for fun and learning from scratch. One of the ideas I want to implement is to make the state of the game object (structure) a double buffer. For example, I may have subsystems that update new data of a game object, and the rendering stream from old data, ensuring that a consistent state is stored in the game object (data from the last time). After the rendering of the old and updating the new are completed, I can change the buffers and do it again.

Question: what is a good promising and general way of OOP to expose it to my classes, trying to hide implementation details as much as possible? I would like to know your thoughts and thoughts.

I thought you could use operator overloading, but how do I overload the assignment for a template member of a class in my buffer class?

for example, I think this is an example of what I want:

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 x member data.x+=1; //I guess this shouldn't be allowed 

If possible, I can choose to enable or disable double-buffered structures without changing a lot of code.

This is what I was considering:

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

and the game object will look like this:

 struct MyObjectData{ int x; float afloat; } class MyObject: public Node { doublebuffer<MyObjectData> data; functions... } 

Now I have functions that return pointers to the old and new buffer, and I think that any classes that use them should be aware of this. Is there a better way?

+6
c ++ operators oop class operator-overloading
source share
7 answers

I recently looked at a similar desire in a generalized way by “snapshot” a data structure that used Copy-On-Write under the hood. The aspect that I like about this strategy is that you can take a lot of pictures if you need them, or just have one at a time to get your “double buffer”.

Without a double-blind, there are too many implementation details, here is some pseudo-code:

 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 x member, eg 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 

In your case, you would take off your fortune and pass it on for rendering. Then you let your update work with the source object. Reading it with const access via readable() will not lead to copying ... when accessed with writable() will start a copy.

For this, I used some tricks on top of Qt QSharedDataPointer . They distinguish between const and non-const access via (->), so reading from a const object will not lead to copying on the write mechanism.

+5
source share

I would not be smart with operator overloading if I were you. Use it for completely unsurprising material that is as close as possible to what the native operator will do, and nothing more.

It’s not entirely clear that your circuit helps a lot in several write streams anyway - how do you know which one “wins” when several streams read the old state and write to the same new state, overwriting any earlier records?

But if this is a useful method in your application, then I would have the GetOldState and GetNewState methods that would fully understand what is happening.

+5
source share

I am not sure if having two states will mean that you do not need synchronization when accessing the recordable state, if you have several streams, but ...

I think the following is a simple and obvious (to maintain and understand) template that you could use with a little overhead.

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

Basically, I did something similar to your suggestion, but used overload. If you want to be careful / paranoid, I will have an empty class with getter / setter virtual methods as the base class, and not as above, so the compiler will save the code correctly.

This gives you a version of readOnly state that will only ever change when swap is called and a clean interface, in which the caller can ignore the double buffer problem when working with state (anything that does not require knowledge of old and new states can work with MyRealState interface) , or you can disable / require the DoubleBufferedState interface if you care about the state before and after the state (which is probably imho).

The clean code is likely to be understood (by each of you, including you), and will be easier to test, so I avoid operator overloading myself.

Sorry for any C ++ syntax errors, now I'm a bit of a Java person.

+2
source share

The more your game state, the more expensive the synchronization of two copies will be. It would be just as simple to create a copy of the game state for each rendering stream; you will have to copy all the data from the front to the back buffer, so you can just do it on the fly.

You can always try to minimize the number of copies between buffers, but then you have the overhead of tracking field changes so you know what to copy. It will be a less than stellar solution in the core of the video game engine, where performance is pretty important.

+2
source share

You might even want to create a new visualization state in each tick. Thus, your game logic is a producer, and your render is a consumer of rendering states. The old state is read-only and can be used as a link both for rendering and for the new state. After rendering, you delete it.

For small objects, Flyweight may be appropriate.

+1
source share

You need to do two things:

  • separate state of the object and its relation to other objects
  • use COW for native state of object

Why?

For rendering, you only need the properties of the "back-version" object, which affect the rendering (for example, position, orientation, etc.), but you do not need object relations. This will allow you to free yourself from dangling pointers and let you update the state of the game. COW (copy-on-write) must be 1-level because you only need one "different" buffer.

In short : I think the choice of operator overload is completely orthogonal to this problem. It is just syntactic sugar. If you write + = or setNewState, it does not matter at all, since both use the same processor time.

+1
source share

Generally, you should only use operator overloading when it is natural. If you scratch a suitable operator for some functionality, then this is a good sign that you should not force the operator to overload your problem.

Having said that, you are trying to create a proxy object that sends read and write events to one of a pair of objects. A proxy object often overloads the -> operator to give pointer-like semantics. (You cannot overload.)

Although you can have two overloads -> , differentiated by const -ness, I would caution against this, since this is problematic for read operations. Overloading is selected by referencing an object through a constant or non-constant reference, and not whether the action is actually read or written. This fact makes error prone.

What you can do is split access from the repository and create a multi-buffer class template and a buffer access template that accesses the corresponding member using operator-> for syntactic simplicity.

This class stores several instances of the template parameter T and stores the offset so that different accessors can retrieve the front / active buffer or other buffers by relative offset. Using the template parameter n == 1 means that there is only one instance of T , and multi-buffering is effectively disabled.

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

This class abstracts the choice of buffer. It refers to the MultiBuffer through a link, so you must ensure that its service life is shorter than the MultiBuffer that it uses. It has its own offset, which is added to the MultiBuffer offset, so different BufferAccess can refer to different members of the array (for example, template parameter n = 0 for access to the front buffer and 1 for access to the back buffer).

Note that the BufferAccess offset is a member, not a template parameter, so the methods that work with BufferAccess objects BufferAccess not bound to only one specific offset, or must be templates themselves. I made the count object a template parameter, because from your description it is most likely a configuration parameter, and this gives the compiler the maximum opportunity for optimization.

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

When compiling all this together with the test class, note that when overloading -> we can easily call the members of the test class from an instance of BufferAccess without BufferAccess , which needs some knowledge of what the members are.

In addition, no change is made between single and double buffering. Triple buffering is also trivial if you can find it.

 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; } 
+1
source share

All Articles