Portfolio von
Immanuel Scholz |
|
Inhaltsverzeichnis
Template Particle EngineParticleEngine Parameter Configuration The struct 'Particle' Engine Sourcecode Sequence Conditional Function Call Actions Boost and __if_exists Overdesign Resumé Template Particle EngineParticle Engines are new to me. One pattern I recognized quite a lot while reading tutorials is like this: An abstract base class provides (pure) virtual functions for updating the particle and copying into the frame buffer. But one article about a template based engine from Kent Lai impressed me more. It promised better performance by not using virutal function calls without loosing flexibility. Back in 2006, I already worked a lot with templates and meta programming and I wanted to refresh my knowledge and tune my skills here. Beside, I wanted to gain a feeling for easy performance critical environments. I put myself the restriction:The particle engine should be generic and extensible, but must not sacrifice performance compared to a hand-written engine. ParticleEngineMy goal was to get a specific particle engine by enumerating different "actions" (which are policy class templates). For example, the fire effect on the title screen:typedef Particle::Engine<FireParticle, 8192, Particle::LinearMovement, Particle::PhoenixLifeDecrease, Particle::AlphaCircle, Particle::Glowing, Particle::SetColor > Fire; Fire fire;FireParticle is the particle struct. There are a maximum of 8192 particles in the engine and the remaining parameter marking the actions to be done with those particles during update and rendering. All particle are moving with a linear movement (LinearMovement) and will respawn immediately on the emitter (PhoenixLifeDecrease). A circle with decreasing alpha-value is used as particle image (AlphaCircle). Particles glow by blending on the destination alpha (Glowing) and all particles have a constant colour (SetColor). Parameter ConfigurationSome actions need further parameters. Any parameter not specified over the typedef is done by assigning values to member variables. Which variables are present depends on the actions in the typedef. For example "Particle::SetColor" provides a member variable "particleColor" in the resulting type. Initializing is typically done in the constructor of the class who uses the particle effect (here, in the pimpl-class of Title):TitleImpl() { fire.particleColor = D3DCOLOR_COLORVALUE(0.1f,0.06f,0.03f,1.0f); } The struct 'Particle'The first parameter to the engine is the particle struct. FireParticle looks like this:struct FireParticle { struct Vertex { float x,y,z; Vertex() : x(randf()*gGame->width()-70) , y(500) , z(0) { } static const int FVF = D3DFVF_XYZ; }; float maxLife; float life; Vector velocity; FireParticle() : maxLife(randf()*5.0f) , life(maxLife) , velocity(randf()*70, randf()*randf()*60-70) { } };Requirements to the struct are as follows:
Engine SourcecodeUnfortunately, my compiler doesn't support varadic templates. I have to use a large list of template arguments with defaults:// No-Op type. You may safely ignore this. template<class P> struct EmptyTemplate {}; // Main Engine Template. template<class P, size_t MAX_SIZE, template<class> class U1, template<class> class U2 = EmptyTemplate, template<class> class U3 = EmptyTemplate, template<class> class U4 = EmptyTemplate, template<class> class U5 = EmptyTemplate, template<class> class U6 = EmptyTemplate, template<class> class U7 = EmptyTemplate, template<class> class U8 = EmptyTemplate, template<class> class U9 = EmptyTemplate, template<class> class U10 = EmptyTemplate, template<class> class U11 = EmptyTemplate, template<class> class U12 = EmptyTemplate, template<class> class U13 = EmptyTemplate, template<class> class U14 = EmptyTemplate, template<class> class U15 = EmptyTemplate> class Engine : public Sequence<P, U1, U2, U3, U4, U5, U6, U7, U8, U9, U10, U11, U12, U13, U14, U15> { public: size_t size; typedef P Particle; typedef typename Particle::Vertex Vertex; typedef Sequence<P, U1, U2, U3, U4, U5, U6, U7, U8, U9, U10, U11, U12, U13, U14, U15> BaseSequence; typedef Engine<Particle, MAX_SIZE, U1, U2, U3, U4, U5, U6, U7, U8, U9, U10, U11, U12, U13, U14, U15> EngineType; Particle particle[MAX_SIZE]; Vertex vertex[MAX_SIZE]; IDirect3DVertexBuffer9* vb; Engine() : size(MAX_SIZE) { deviceReset(); lostConnection = gGame->deviceLost.connect( std::bind(&EngineType::deviceLost, this)); resetConnection = gGame->deviceReset.connect( std::bind(&EngineType::deviceReset, this)); } ~Engine() { deviceLost(); lostConnection.disconnect(); resetConnection.disconnect(); } void update(float elapsed) { Sequence::emitParticles(elapsed, particle, vertex, size, MAX_SIZE); for (size_t i = 0; i < size;) { if (Sequence::updateParticle(elapsed, particle[i], vertex[i])) { particle[i] = particle[--size]; vertex[i] = vertex[size]; } else ++i; } } void paint() { if (!size) return; Sequence::prePaint(); gDevice->SetRenderState(D3DRS_POINTSPRITEENABLE, true); gDevice->SetFVF(Vertex::FVF); gDevice->SetStreamSource(0, vb, 0, sizeof(Vertex)); Vertex* vertexData; VERIFY(vb->Lock(0, sizeof(Vertex)*size, (void**)&vertexData, D3DLOCK_DISCARD)); memcpy(vertexData, vertex, sizeof(Vertex)*size); VERIFY(vb->Unlock()); VERIFY(gDevice->DrawPrimitive(D3DPT_POINTLIST, 0, size)); Sequence::postPaint(); } void deviceLost() { SAFE_RELEASE(vb); } void deviceReset() { VERIFY(gDevice->CreateVertexBuffer( sizeof(vertex), D3DUSAGE_DYNAMIC | D3DUSAGE_POINTS | D3DUSAGE_WRITEONLY, Vertex::FVF, D3DPOOL_DEFAULT, &vb, 0)); } private: boost::signals::connection resetConnection, lostConnection; }; SequenceThe class Sequence controls calling a sequence of actions, which it gets as parameter.// Combines update actions to a sequence using this template // and call the appropiate member functions if present. template<class P, template<class> class U1, template<class> class U2, template<class> class U3, template<class> class U4, template<class> class U5, template<class> class U6, template<class> class U7, template<class> class U8, template<class> class U9, template<class> class U10, template<class> class U11, template<class> class U12, template<class> class U13, template<class> class U14, template<class> class U15> class Sequence : public U1<P>, public Sequence<P, U2, U3, U4, U5, U6, U7, U8, U9, U10, U11, U12, U13, U14, U15, EmptyTemplate> { typedef Sequence<P, U2, U3, U4, U5, U6, U7, U8, U9, U10, U11, U12, U13, U14, U15, EmptyTemplate> Tail; public: void emitParticles(float elapsed, P* p, typename P::Vertex* v, size_t& size, size_t MAX_SIZE) { Call<Traits<U1<P>,P>::hasEmitParticles, U1<P>, P>:: emitParticles(*this, elapsed, p, v, size, MAX_SIZE); Call<Traits<Tail,P>::hasEmitParticles, Tail, P>:: emitParticles(*this, elapsed, p, v, size, MAX_SIZE); } // returns whether the particle should be killed. bool updateParticle(float elapsed, P& p, typename P::Vertex& v) { return Call<Traits<U1<P>,P>::hasBoolUpdateParticle, U1<P>, P>:: boolUpdateParticle(*this, elapsed, p, v) || (Call<Traits<U1<P>,P>::hasUpdateParticle, U1<P>, P>:: updateParticle(*this, elapsed, p, v), false) || Call<Traits<Tail,P>::hasBoolUpdateParticle, Tail, P>:: boolUpdateParticle(*this, elapsed, p, v) || (Call<Traits<Tail,P>::hasUpdateParticle, Tail, P>:: updateParticle(*this, elapsed, p, v), false); } void prePaint() { Call<Traits<U1<P>,P>::hasPrePaint, U1<P>, P>::prePaint(*this); Call<Traits<Tail,P>::hasPrePaint, Tail, P>::prePaint(*this); } void postPaint() { Call<Traits<U1<P>,P>::hasPostPaint, U1<P>, P>::postPaint(*this); Call<Traits<Tail,P>::hasPostPaint, Tail, P>::postPaint(*this); } }; template<class P> struct Sequence<P, EmptyTemplate, EmptyTemplate, EmptyTemplate, EmptyTemplate, EmptyTemplate, EmptyTemplate, EmptyTemplate, EmptyTemplate, EmptyTemplate, EmptyTemplate, EmptyTemplate, EmptyTemplate, EmptyTemplate, EmptyTemplate, EmptyTemplate> {};Sequence inherits from the first action and from itself but with one less parameter. The end of the list is "marked" by specialization. Other than that, Sequence just consists of 4 functions, which forward calls to 5 other functions. Conditional Function CallThe forwarding is done by a "conditional function call" (that's what I call it). The template "Call" tests by using SFINAE (Substitution Failure Is Not An Error), whether the target function has been declared and calls it only, if it exists. My SFINAE - implementation looks like this:// The following stuff is used for SFINAE - detection of the member functions. template<class T, void (T::*)()> struct EmptyFunction {}; template<class T, class P, void (T::*)(float, P&, typename P::Vertex&)> struct EmptyUpdateFunction {}; template<class T, class P, bool (T::*)(float, P&, typename P::Vertex&)> struct EmptyBoolUpdateFunction {}; template<class T, class P, void (T::*)(float, P*, typename P::Vertex*, size_t&, size_t)> struct EmptyEmitFunction {}; template<class T, class P> float checkForFunction(...); template<class T, class P> char checkForFunction( EmptyFunction<T, &T::prePaint>*); template<class T, class P> char checkForFunction( EmptyFunction<T, &T::postPaint>*, int); template<class T, class P> char checkForFunction( EmptyUpdateFunction<T, P, &T::updateParticle>*, int, int); template<class T, class P> char checkForFunction( EmptyBoolUpdateFunction<T, P, &T::updateParticle>*, int, int, int); template<class T, class P> char checkForFunction( EmptyEmitFunction<T, P, &T::emitParticles>*, int, int, int, int); template<class T, class P> struct Traits { const static bool hasPrePaint = sizeof(checkForFunction<T,P>(0)) == 1; const static bool hasPostPaint = sizeof(checkForFunction<T,P>(0,0)) == 1; const static bool hasUpdateParticle = sizeof(checkForFunction<T,P>(0,0,0)) == 1; const static bool hasBoolUpdateParticle = sizeof(checkForFunction<T,P>(0,0,0,0)) == 1; const static bool hasEmitParticles = sizeof(checkForFunction<T,P>(0,0,0,0,0)) == 1; }; template<bool, class T, class P> struct Call { static void prePaint(T& t) {t.prePaint();} static void postPaint(T& t) {t.postPaint();} static void updateParticle( T& t, float elapsed, P& p, typename P::Vertex& v) {t.updateParticle(elapsed, p, v);} static bool boolUpdateParticle( T& t, float elapsed, P& p, typename P::Vertex& v) {return t.updateParticle(elapsed, p, v);} static void emitParticles( T& t, float elapsed, P* p, typename P::Vertex* v, size_t& size, size_t MAX_SIZE) {t.emitParticles(elapsed, p, v, size, MAX_SIZE);} }; template<class T, class P> struct Call<false,T, P> { static void prePaint(T&) {} static void postPaint(T&) {} static void updateParticle(T&, float, P&, typename P::Vertex&) {} static bool boolUpdateParticle( T&, float, P&, typename P::Vertex&) {return false;} static void emitParticles( T&, float, P*, typename P::Vertex*, size_t&, size_t) {} }; ActionsFinally the implementation of the different actions. Like seen in sequence, these are templates. They have one template parameter which will be the particle struct. One main aspect is, that these classes should be as easy to create and maintain as possible. That is because these are the classes a user of the framework has to know and understand. This is also the point where the user extends the framework with new functionality. Users of these actions can (but does not have to) implement five different functions. If he provides an implementation, this will be called while the engine runs. This description would be typical for a base class with only pure virtual functions ("interface" in other languages). With one exception: The function "updateParticle" can be implemented either as returning a bool or not returning anything (void). This kind overloading is not allowed for normal C++ classes.// pseudo code. Not part of Nightwatch. (not even legal C++) template<class P> // Partikel-struct struct PossibleBaseClassOfActions { // called before rendering (and before locking the vertex buffer) void prePaint(); // at the end of rendering (after unlocking the buffer) void postPaint(); // called every tick to update the particle state void updateParticle(float elapsed, P& p, typename P::Vertex& v); // like above, but if it returns true, the particle will be destroyed bool updateParticle(float elapsed, P& p, typename P::Vertex& v); // called to create particles void emitParticles(float elapsed, P* p, typename P::Vertex* v, size_t& size, size_t MAX_SIZE); };This code is only for demonstration of the current possibilities. Following examples of some real actions: /* Linear movement with a particle-specific velocity. The particle struct must have a member called "velocity". Requires: Vector velocity */ template<class P> struct LinearMovement { void updateParticle(float elapsed, P& p, typename P::Vertex& v) { v.x += p.velocity.x()*elapsed; v.y += p.velocity.y()*elapsed; } }; /* Acceleration towards a fixed constant vector. Requires: Vector velocity */ template<class P> struct Gravity { Vector gravity; Gravity() : gravity(0,80) {} void updateParticle(float elapsed, P& p, typename P::Vertex& v) { p.velocity = p.velocity + elapsed * gravity; } }; /* Decrease the lifetime and kills the particle, if it reaches zero life. Requires: float life; */ template<class P> struct LifeDecrease { bool updateParticle(float elapsed, P& p, typename P::Vertex& v) { p.life -= elapsed; return p.life < 0; } };Well, I am very satisfied with the ease of this code. Finally an example for an action used in the "CursorSparkle" (the animated cursor). The action moves a particle only, if it is not near the current mouse position. template<class P> struct IfNotUnderMouse : Particle::LinearMovement<P> { void updateParticle(float elapsed, P& p, typename P::Vertex& v) { if ((Gui::state.mouse - Vector(v.x-25, v.y-25)).length2() > 10) Particle::LinearMovement<P>::updateParticle(elapsed, p, v); } };Two remarks: Boost and __if_existsFirst, I didn't know of the library "boost::enable_if" and the Microsoft extension "__if_exists". Both would ease the structure of the engine a lot. Also, "boost::concepts" would improve error messages a lot. I think, I will adopt my implementation accordingly. The presented solutions and code examples are based on pure standard-C++ and don't require any additional libraries.OverdesignSecond, I use some very complex design techniques. Error messages will be very hard to decipher and the code looks just terrible for people who haven't done C++ meta programming before. I would use this kind of code only, if there is at least one more member of my project team who understand the whole framework. ;-) Some places where the framework could be simplified without sacrificing performance:
ResuméBy now I know: My framework is no alternative for real particle engines. Especially for particle systems, a data driven design is vastly superiour than any OOD. Its magnitudes of order more important to correctly sort and cull data and avoid cache misses as good as possible. Also, things like reducing traffic between CPU and GPU is much more important than my considerations here. Beside: A template based system is basically unusable by artists and designer. Or at least it makes any visual editor a lot harder to write. And it unnecessarily lengthen compile times, which are usually already quite a problem for bigger games. But I definetely achieved my goal: I feel quite comfortable with templates and meta programming again. ;-) |
|