Welcome, Guest. Please login or register.

Login with username, password and session length

 
Advanced search

1411478 Posts in 69369 Topics- by 58424 Members - Latest Member: FlyingFreeStudios

April 23, 2024, 07:10:33 PM

Need hosting? Check out Digital Ocean
(more details in this thread)
TIGSource ForumsCommunityTownhallForum IssuesArchived subforums (read only)TutorialsThe Observer pattern in C++
Pages: [1]
Print
Author Topic: The Observer pattern in C++  (Read 10426 times)
Linus
Level 0
***


View Profile
« on: April 02, 2010, 03:38:14 PM »

I thought I'd do a discussion on the Observer pattern, and all the magic it can do for you in C++.

Let us take it all in order, I'll show you the problem an observer is meant to solve, and a multitude of ways to do it, both good and bad.

In the beginning, there was constant polling. It was and still is the most basic way of checking how content changes. It is still used today on the lower end of the software pipeline where other methods aren't even applicable, deep down in the kernel or wherever in the operating system keyboard is handled, it is done via polling and passed to any interested program.
The loop looks something akin to this:
Code:
while(1){
    keys = getKeyboardKeys();
    if(keysChanged(keys)) handleKeys(keys);
}
Polling is a classic way of working in some systems, but software patterns introduce a lot of different techniques we can use to do this better.
Erasing polling is most commonly done with, you guessed it, the observer pattern.
A more common polling structure in our object oriented games might be something akin to this:
Code:
class Keys{
    //...
    bool getKey(char key);
}

//...
class Game {
Keys keys;
bool shooting;

//...

while(running){
    if(keys.getKey('z'))
       shooting = true;
    else
       shooting = false;
    //...
}
This looks... good. A common way of handling input, fairly easy to work with in practice and not too hard to use.
ERR.
We have a couple of problems here. The most obvious one is apparent if we decide to create a weapon without repeat fire. Suddenly, we want a structure something akin to this:
Code:
if(!keys.lastKey('z') && keys.getKey('z')) 
    shooting = true;
else
    shooting = false;
So... That's one more function to implement. Next we decide to create a weapon that has one bullet type on the first shot, then another bullet type on repeat:
Code:
if(keys.getKey('z')){
    shooting = 1;
    if(!keys.lastKey('z'))
        shooting = 2;
}
else
    shooting = 0;
All of this goes into one function that keeps on growing and growing whenever we decide to add more content and features. Not only is it a large function that has a multitude of purposes, abstracting it out to configurable keybinds is a mess and it gets worse if we decide to move it into separate classes with random keys doing a multitude of tasks all over the place with constants and polling everywhere.

So, how does an Observer solve this?
Well, it doesn't. However, it makes everything more manageable if our structure is good to begin with.
What the observer actually does is quite simple. If we have a multitude of classes and objects that are interested in what an object does, we say to the object "Tell us when you change", rather than ask "Did you change? Did you change? Did you change?" whenever we can, often repeatedly as seen above.

A first attempt by many is to put such a behaviour in a class:
Code:
class Observable;
struct Event{/*???*/};

class Observer{
  public:
    virtual void notify(const Event &e, Observable *o) = 0;
    virtual ~Observer(){}
};
class Observable{
    std::set<Observer*> observers;
  protected:
    void notifyAll(Event &e){
        for(std::set<Observer*>::iterator i=observers.begin(); i!=observers.end(); ++i)
            (*i)->notify(e, this);
    }
  public:
    void addObserver(Observer *o){observers.insert(o);}
    void removeObserver(Observer *o){observers.erase(o);}
    virtual ~Observable(){}
};

This structure allows us to store a pointer to our observed object in a class, so that when something happens, we can identify where it happened. It also gives us an Event object containing what changes in that object.
This solution removes all of our polling from whatever previously handled this, and gives us a nice, clean function which can handle all our events.
ERR.
This is a bad solution. It is easy to believe that this is what the observer pattern is, but you'll be mistaken.
Looking back at our previous code, what has changed? How is it cleaner?
Now... instead our main loop is empty... but... our new function looks exactly like the old function, and now we have to do raw type casting on our events depending on what we think the event is! Horrible! We even need knowledge of how Keys, which we observe, sends out events, to identify what arrives! Bad! Get it out of your head!

There similar ways of doing this which work a lot better. One of the more common solutions that work a lot better removes type casting and implements the observer pattern for every observable object, leading to several implementations of almost the same code for differing purposes. It is cleaner, but there's still repeated code involved.
But wait! This is C++ we're working with! C++, with the magic of templates!
Look back a bit at that old code again... How could we use templates to improve this? Of course!

Code:
class Observable;

template<typename T>
class Observer{
  public:
    virtual void notify(const T &e, Observable<T> *o) = 0;
    virtual ~Observer(){}
};

template<typename T>
class Observable{
    std::set<Observer<T>* > observers;
  protected:
    void notifyAll(T &e){
        for(std::set<Observer<T>* >::iterator i=observers.begin(); i!=observers.end(); ++i)
            (*i)->notify(e, this);
    }
  public:
    void addObserver(Observer<T> *o){observers.insert(o);}
    void removeObserver(Observer<T> *o){observers.erase(o);}
    virtual ~Observable(){}
};

At this point, our code actually looks pretty good. Comparing to the last implementation, we went through almost zero changes, and even removed a class, but now we suddenly have both compile-time type safety, and events of differing types have different functions assigned to them.
There are still some problems, but arriving at this solution, or a similar one without templates, leaves most people pleased. Oftentimes, even me.
But we can go further, utilizing templates, to create an even better solution.
So, what problems DO we still have?
Well, first of all, any object that is interested in what an Observable<T> provides, has to implement the interface Observer<T>. A lot of times, we can end up with classes looking like this:
Code:
class A : public Observer<B>, public Observer<C>, public Observable<D>, public Observable<E>, public Observer<F> { /*...*/ };
Looking at that signature and figuring out what we can do with the class works, but it's not very nice, and we have to go all the way up there every time we want to add a dependency for something that MIGHT be an event. What if we have a hundred types, or a thousand, and we might subscribe to any of them, but we're only interested in what caller it is? Yeah, we'd have to go through and add all those interfaces.
Now, there is a nice feature in C++ we can use to take this one step further and remove the inheritance relationship, replacing it with a composition solution.
It's called, you know it... templates!
Consider a class like this:
Code:
template<typename Type>
class Trigger : public Observer<Type> {
    Observable<Type> *from;
  public:
    Trigger(Observable<Type> *from) : from(from) {
        from->addObserver(this);
    }
    ~Trigger(){ from->removeObserver(this); }
    void notify(const Type &t, Observable<Type> *o){}
};
So, what can this do for us?
In this form, nothing! But, it does provide an interesting concept to build on. What this is, is an object that observes 'from' as long as it exists.
Notice that we're not actually doing something when we observe an event, let's change this:
Code:
template<typename Type, typename Target>
class Trigger : public Observer<Type> {
    Observable<Type> *from;
    Target *target;
  public:
    Trigger(Observable<Type> *from, Target *target) : from(from), target(target) {
        from->addObserver(this);
    }
    ~Trigger(){ from->removeObserver(this); }
    void notify(const Type &t, Observable<Type> *o){
        target->notify(t, o);
    }
};
Suddenly, this is a magic template. The difference here is that a template knows at compile-time what functions exist on target, as it can be instantiated with the actual type of target rather than the Observer<> class, meaning that we can rid ourselves of all Observer<>-inheritance in any classes we create, instead replacing them with compositions such as this:
Code:
class A{
    Trigger<Key, A> trigger;
  public:
    A(Observable<Key> *k) : trigger(k, this) {}
    void notify(const Key &k, Observable<Key> *o) {/*...*/}
};
Of course, creating a trigger on construction might not always be ideal, but we can take this one step further:
Code:
class TriggerBase{
  public:
    virtual ~TriggerBase{}
};

template<typename Type, typename Target>
class Trigger : public TriggerBase, public Observer<Type> {
    Observable<Type> *from;
    Target *target;
  public:
    Trigger(Observable<Type> *from, Target *target) : from(from), target(target) {
        from->addObserver(this);
    }
    ~Trigger(){ from->removeObserver(this); }
    void notify(const Type &t, Observable<Type> *o){
        target->notify(t, o);
    }
};

Now, with a base type for the trigger, we can suddenly instantiate lists of triggers:
Code:
class A{
    std::vector<TriggerBase*> triggers;
public:
    A(Observable<Key> *o){
        triggers.push_back(new Trigger<Key, A>(o, this);)
        //...
    }
    ~A(){
        while(triggers.size()){
            delete triggers.back();
            triggers.pop_back();
        }
    }
  
    //We can still easily access all events with a single function and no inheritance
    template<typename T>
    void notify(const T& t, Observable<T> *o){}
  
    //And overload it when we want a specific result for a single type
    void notify(const Key& k, Observable<Key> *o){}
};

Now, not everyone likes having to manually instantiate and keep track of data with new and delete, as it creates other problems. But on the other hand this will often, probably even always, be present when we arrive at object oriented content in C++. Smart pointers are your friends.
For handling concepts like a trigger and other similar content, we could either subclass vector to delete it's own content or simply use a single inheritance to a base class that deletes it for us:
Code:
class SimpleTriggers{
protected:
    std::vector<BaseTrigger*> triggers;
public:
    virtual ~SimpleTriggers(){
        while(triggers.size()){
            delete triggers.back();
            triggers.pop_back();
        }
    }
};

Here, almost everything is gone. We have a minimal amount of inheritance in our observers, there's no polling, we have multiple functions for handling a multitude of events... What's left?
Well, at this point, we're moving away from good old Observer and into what might be considered personal preferences.
Some people would like to keep all their event handling in the class, others might move it into helper classes:
Code:
class A : public SimpleTriggers{
    struct B{ notify(const Key &k, Observable<Key> *o){
        if(k == 'z') /*act*/;
    } } b;
    A(Observable<Key> *o) { triggers.push_back(new Trigger<Key B>(o, &b)); }
};

What I did was modify Trigger with further template data:
Code:

struct Any{
    template<typename T>
    bool operator()(const T &t){return true;}
};
template<typename T>
struct EqCond{
    T _t;
    EqCond(T _t) : _t(_t) {}
    bool operator()(const T &t){return (t == _t);}
};

template<typename Type, typename Target, typename Cond = Any >
class Trigger : public TriggerBase, public Observer<Type> {
    Observable<Type> *from;
    Target *target;
    Cond c;
  public:
    Trigger(Observable<Type> *from, Target *target, Cond c) : from(from), target(target), c(c) {
        from->addObserver(this);
    }
    ~Trigger(){ from->removeObserver(this); }
    void notify(const Type &t, Observable<Type> *o){
        if(c(t)) target->notify(t, o);
    }
};
Which allowed constructs like these:
Code:
class A : public SimpleTriggers {
    struct B { void notify(Key k, Observable<Key> *o){
       //B no longer cares about which key caused this function to be accessed.
       bool shootLazor = key.down;
    } } b;
  public:
    A(Observable<Key> *o) {
        triggers.push_back(new Trigger<Key, B, EqCond<Key> >(o, &b, EqCond<Key>(Key('z'))));
    }
};
Thus moving the conditions for an event to be executed into an external location.
However, this is unrelated to the actual observer pattern, and could just as easily be implemented with if, decorators, or some other type of condition handler, depending mostly on your own preferences.

So. This is as far as we're getting... Or is it?
There is still one small, insignificant issue that keeps plaguing us. How do we begin observing, where should the trigger be created? Should the class itself do it, requiring us to pass data the class doesn't actually need to know to the constructor, such as what to observe?
Should the parent class do it, creating another initialization requirement?
Should there be a magic mediator singleton that we send all our new classes to with some sort of identifier, and have that class figure out what to observe?

Personally, I use all of these solutions in some form or another. Normalizing to one is ideal, but sacrificed to development time. My personal favourite is the second, defining from where a class receives input outside a class to me seems cleaner than forcing the class to figure out where it should retrieve data, since either way the parent class must know what to pass in, giving it the additional responsibility of saying how it should works seems logical.
A class for picking in 3D shouldn't have to know what button needs to be pushed to initiate a pick, whilst a class setting it up may be interested in this.

Not to mention, there is always the Boost.Signals package; which provides a completely different observer implementation that may also be worth looking at. Boost.Signals is a very flexible solution that can do much of what we can, and in some instances more. The downside then, is that Boost.Signals is completely uncoupled from the rest of the code, meaning you have less access to modify it for your own needs. Having some knowledge of what the Observer pattern can do should help you find the solution you prefer.

Whichever way you pick, we are now as close as we will get to the core of the Observer pattern, at least for today. Hope you enjoyed, and perhaps also learned something, not only about the Observer pattern, but also about the mystery of templates. Smiley


Disclaimer: All of this is written with hand-eye compilation, any errors are a compiler bug on my end.

Linus
« Last Edit: April 02, 2010, 04:16:57 PM by Linus » Logged
mewse
Level 6
*



View Profile WWW
« Reply #1 on: April 02, 2010, 05:56:47 PM »

Many of the code samples in that post make use of a "this" pointer (sometimes explicitly, sometimes implied) from within a class constructor.  It's important to take care with that, as the "this" object is not guaranteed to have been fully set up until after the constructor exits.  Virtual functions, in particular, are not going to work correctly (or at all!) if called from within that object's constructor;  best practice is to try to avoid using the 'this' pointer within constructors.

Apart from that minor note, very neat article; had never considered using such highly-templated construction for observers.  Smiley
Logged
Linus
Level 0
***


View Profile
« Reply #2 on: April 03, 2010, 04:09:35 AM »

Well, this is true, however.
this IS fully defined for any class where the constructor has been entered. Initializer lists:
Code:
class A {
  int a, c;
  B b;
public:
  A() : a(0), c(100), b(this) {}
  virtual void f() { std::cout<<"A"<<std::endl; }
};

class B {
  B(A* a) { a->f(); }
};
These lists are there to support initialization, and are implicitly run for all variables in an object, even if they have not been defined. Moreso, when an initializer list is entered, the vtable has already been constructed for the current class, meaning the above code is safe, unless B would access the variable it is constructed in. Which is why you should normally not have "this" in initializer lists, before they have passed through this list, they are guaranteed NOT to be initialized.
Our problem then, if we do not use this in the initializer list, is when we subclass:
Code:
class C : public A {
  B b2;
  C() : b2(this) {}
  void f() { std::cout<<"C"<<std::endl; }
};
What happens here is, in order:
The vtable is built for the base class, A.
A runs through its initializer list.
A constructs b with 'this' as a parameter.
b runs a->f();
The class is constructed up to the point where it has the vtable for A, so the old version of f is run, output is "A".
constructor for A finishes.
vtable for C is built.
C initializer list is run through.
b2 is initialized with 'this' as a parameter.
b2 runs a->f();
The new vtable has now been generated, and we now have access to the new version of f, so output is "C".
constructor for C finishes.

So, the result may not be what you would expect it to be, but it is for all purposes well-defined. If f had been a pure virtual in the base class, the code would have ended execution with an error signal. This behaviour is what I'd expect it to be, since I would, in these cases, be sending a pointer to what is constructed up to the point of a base class.
With multiple base classes, the execution result would be dependent on the order the base classes are initialized, something which can be defined at the start of an initializer list, but executed before the vtable is built. This is a case I'm presuming you'd want to avoid at all costs though. Smiley

Most of this was tested by experiementation in g++/gcc, but I would expect the behaviour to be in the standard, as it is, as far as I can tell, perfectly logical.

What I would say is, best practice is to not use 'this' in a constructor that will be derived into subclasses, but using it in the most derived class is possible, if done when all other data has been constructed at the end of the constructor. This would make it comparable to a post-construction signal, which is useful in a lot of circumstances.

If you, like me, in most cases have a single very simple base class and build via composition, you will probably never run into this problem normally. With a complex inheritance structure it is harder to discover and could cause problems.
A good way of completely avoiding this construct in the observer pattern, is as I discussed to have the parent object add triggers or observables to the recently constructed class, instead of doing it in the constructor.

Great comment, thanks.


Linus
« Last Edit: April 03, 2010, 04:15:19 AM by Linus » Logged
muku
Level 10
*****


View Profile
« Reply #3 on: April 09, 2010, 05:05:07 AM »

This is a nice effort, but I think you may be making your life more complicated than it needs to be.

We have a couple of problems here. The most obvious one is apparent if we decide to create a weapon without repeat fire. Suddenly, we want a structure something akin to this:
Code:
if(!keys.lastKey('z') && keys.getKey('z')) 
    shooting = true;
else
    shooting = false;
So... That's one more function to implement.

I think you're solving this at the wrong place. Just add a method keyPressed(char) to your Keys class which returns true only if the key was pressed down in this frame, and you can write this simply as
Code:
shooting = keys.keyPressed('z');


Quote
Next we decide to create a weapon that has one bullet type on the first shot, then another bullet type on repeat:
Code:
if(keys.getKey('z')){
    shooting = 1;
    if(!keys.lastKey('z'))
        shooting = 2;
}
else
    shooting = 0;

Again, I think you have the wrong approach. This kind of logic is local to the weapon, and there is nothing wrong with just writing

Code:
if(keys.getKey('z'))
    shooting = (shooting == 0) ? 1 : 2;
else
    shooting = 0;

In short I don't see which problem you are trying to solve by heaping towers of complicated templates, vectors requiring dynamic memory management and polymorphic objects requiring slow virtual calls on it. Certainly the code I've just shown you is easier to read and probably has better performance as well.

Don't get me wrong, the Observer pattern certainly has its uses, and in high-level game logic code (which will often be written in a scripting language), I can see it being useful. But in this kind of low-level engine code, I'd argue that polling is a very natural and simple solution since your game loop will run from front to end every frame anyway! Observers, I think, are more suited to event-based/reactive programming, like if you're writing code for some windowing framework.

Now you may say these were just (maybe badly chosen) examples. I just think people often get a bit too pattern-happy once they learn about them since everyone seems to rave how they're the swellest thing ever, to the point of getting into "I have a new hammer, where's my nail" territory, without ever really thinking the actual problem at hand through, and I'd like to warn against that. No offense meant.
Logged
Linus
Level 0
***


View Profile
« Reply #4 on: April 09, 2010, 10:46:55 AM »

Yes, I see your point, and this is perhaps something I should have discussed.
I use this type of code in a heavily state based system, where multiple completely separate modules need to coexist without colliding. Using observers and triggers in such a situation makes sense, as they make it easy to activate and deactivate behaviour without having to modify logic in the code.

For example, if a trigger is part of a list of triggers, and I suddenly realise I need the ability to disable a category of triggers, it is easier to move a set of triggers than to search through code logic and isolate the structures that need to be disabled, which in a system such as the one you described can often grow in an "organic" manner, making it easy to overlook things and leading to problems down the line.
The same problem would appear if I decide to completely remove some logic, if segments are missed and left in place, issues appear.
A lot of the argument here is for code maintenance concerns, which the observer pattern can certainly help out with when used correctly.
As for virtuals and vectors, this depends on your circumstances.
If an event happens once in a thousand frames and it's being polled for every frame, an observer may be more efficient, since the virtual data members and the vectors are only accessed when necessary.
This grows more useful and saves even more performance when requests are blocking and an observer can allow it to run in a separate thread context.
As you said, however, the chosen example is not ideal. The larger argument of the discussion was how an Observer could be implemented, rather than when and why.


Linus
Logged
muku
Level 10
*****


View Profile
« Reply #5 on: April 09, 2010, 04:16:45 PM »

As for virtuals and vectors, this depends on your circumstances.
If an event happens once in a thousand frames and it's being polled for every frame, an observer may be more efficient, since the virtual data members and the vectors are only accessed when necessary.

In some more complicated setting, that's certainly possible. Just in the example you presented, there's no saving at all because you still have to check for the key having been pressed down in every frame -- no matter whether you do it in the consuming object or in some listening component which then fires off messages to observers.

Quote
As you said, however, the chosen example is not ideal. The larger argument of the discussion was how an Observer could be implemented, rather than when and why.

Ok, fine then Smiley Your introduction certainly sounded very motivation-like ("I'll show you the problem an observer is meant to solve..."), but raised all the wrong points I thought, which is why I thought I'd point this out. As far as the implementation goes, I didn't read it too carefully, but I guess it's solid if you really need this kind of heavy-duty machinery.
Logged
Glaiel-Gamer
Guest
« Reply #6 on: April 09, 2010, 09:00:49 PM »

every time I've worked with a system that would push events for keypresses, I just cache the key press in a boolean and do constant polling on it anyway, so this kinda seems like way more of a pain than the "problem" it "solves".
Logged
Linus
Level 0
***


View Profile
« Reply #7 on: April 10, 2010, 01:38:28 AM »

Well.
What you're getting with triggers and observers is,
A) A move from a pull protocol to a push protocol, which is a necessity whenever data is desired where checking for changes takes a lot of time.
B) A separation of behaviour from its source. An observable.add is often easier to replace with another compared to the replacement of a class member with another to allow for event simulation or similar effects.
C) An assurance of correctness. With a Variable b = c->getVariable(); system used in multiple places, you can never be certain that your result is constant for a full frame as c may change its state when accessed. Even if you at one point have a definition saying getVariable() does not change class state (Variable C::getVariable() const {...}) any change to this would not give you compile time errors and thus be problematic to debug, not to mention other changes that could occur in c from other sources. This can, however, be done with an observer, since the same data is sent to all listeners regardless of threads.

B and C are useful even if you later pull cached data locally, you're still reaping the benefits from the initial pushing.

Observer, when used correctly, can make your robust to changes, behaviour easier to change or simulate and thread separation easier to attain. Shrug


Linus
Logged
Mikademus
Level 10
*****


The Magical Owl


View Profile
« Reply #8 on: April 17, 2010, 11:59:13 AM »

I like what you've set out to do here! Smiley

Constructive criticism: I think your tutorial would be very well served by a small code example using the scaffolding you've laid down. The reason is that your core classes are named "Observer", "Observable", and "Trigger", which are all very concrete and self-descriptive names and the classes also well fleshed-out in code, but your only client class is called "A", which is very abstract, and all the business code is left out. Also, the overloaded A::notify methods are empty. So though the underpinning structure is well described, the usage of it is not and is at a different level of abstraction. As such, it is somewhat difficult to envision which audience you're addressing. I also think that these issues will be simply resolved by adding one or a few simple and concrete usage example(s).

Good work!
Logged

\\\"There\\\'s a tendency among the press to attribute the creation of a game to a single person,\\\" says Warren Spector, creator of Thief and Deus Ex. --IGN<br />My compilation of game engines for indies
Evan Balster
Level 10
*****


I live in this head.


View Profile WWW
« Reply #9 on: April 21, 2010, 03:55:27 PM »

In my own (implementation-independent) system I focus my efforts on API as simple as possible on the client-side.  I cache input values into simple booleans and use syntax like this:

Code:
if (player.keyboard.z.pressed && !player.keyboard.x) //do stuff
(Soon enough most code won't need the extraneous 'player' there)

A) A move from a pull protocol to a push protocol, which is a necessity whenever data is desired where checking for changes takes a lot of time.

A boolean doesn't have any significant overhead.

B) A separation of behaviour from its source. An observable.add is often easier to replace with another compared to the replacement of a class member with another to allow for event simulation or similar effects.

You might have me to a small extent there, but I can certainly swap out my implementation or change the 'keyboard' object being referenced in a reasonably simple way.  It does mean having a reference to a Keyboard object in my class, but the alternative means

C) An assurance of correctness. With a Variable b = c->getVariable(); system used in multiple places, you can never be certain that your result is constant for a full frame as c may change its state when accessed. Even if you at one point have a definition saying getVariable() does not change class state (Variable C::getVariable() const {...}) any change to this would not give you compile time errors and thus be problematic to debug, not to mention other changes that could occur in c from other sources. This can, however, be done with an observer, since the same data is sent to all listeners regardless of threads.

B and C are useful even if you later pull cached data locally, you're still reaping the benefits from the initial pushing.

Observer, when used correctly, can make your robust to changes, behaviour easier to change or simulate and thread separation easier to attain. Shrug

Caching results on one's own has low code overhead and fixes the correctness problem.


One more issue with push versus pull is that it's often convenient to be able to handle specific events in a given order.  This is perfectly possible with observer, but dramatically complexifies the necessary code on both the API and user side.  With passive systems it's a simple and intuitive matter of code ordering.  Additionally, if I want to look for combination events (a la Z + X) it's either more registration code or local caching of variables that I'll need.  If I need to use variables like that, the usefulness of observance has been defeated completely as I'm polling again.


Anyway, all those crits out there, I should clarify that I don't take any offense to what you're doing here, and that the point I'm making is that for the purposes of most games pull is dramatically simpler to use than push.  GUI systems, as separate from game logic, are much more suited to event-based input.
Logged

Creativity births expression.  Curiosity births exploration.
Our work is as soil to these seeds; our art is what grows from them...


Wreath, SoundSelf, Infinite Blank, Cave Story+, <plaid/audio>
Linus
Level 0
***


View Profile
« Reply #10 on: April 24, 2010, 02:57:36 AM »

How many times now have I said that keyboard input was a bad example and should be disregarded? Cry
Okay, for completeness...

In a gaming environment, there are several situations in which an observer pattern is useful. I'll list a few examples based on what I've seen in Ogre, for example:

Per-frame events.
Example:
You have a set of activities that should be updated on each frame, you do not always know what these activities are, and it may change per level, per game state or equivalent. Adding and removing animations, keyboard updates, entity updates and similar could be done in a per-frame observer. In some circumstances you would also want runtime textures updated here.

Per-camera and viewport events.
Example:
You have a set of viewports and cameras. Depending on where the render is being done, you want to be able to add and remove different render pipeline features, such as shaders and lighting, or modifications of a special matrix, for example with regards to portals so that they are perspective corrected. You would also want a way of updating runtime textures such as reflections when doing shader rendering as they would differ per view.

Long-running or thread-locking functions:
Example:
Retrieving an image from a camera may for example be a locking function where you want to throw out notifications when the image has been updated and different technologies may want access to the image, including, amongst others, conversion to texture, tracking technologies such as OpenCV, and more.
Another example is file loading in a separate thread pool and throwing events once it's done, although in such a circumstance a callback function may be enough. Shrug

Also, what I meant by caching leaving C useful still, is that a large set of objects may require access to the results, without the data being easily available in a common place. The push would assure that all objects have the same version of C after an input update.

Although I see what you mean by code ordering, stating that this is easier to via avoidance of the observer pattern without an example makes it sound like bad practice, as I can see other problems appearing if you require entity set X to run before set Y...
When order is required for good reasons, it is easy to define an Observer class that gives you a usage contract such as "Objects will acquire events in the order that they were added to the observer" and this is something that would be described in documentation and verified via automated tests or similar if necessary. Afterwards, adding observers in a desired order would have the required effect and would be done via code ordering in the same way.

Geh, I'm tired. I must have breakfast.

Linus
Logged
pcunite
TIGBaby
*


View Profile
« Reply #11 on: February 13, 2011, 05:28:21 PM »

Linus,
Excellent post ... I am adding the Observer Pattern to my newest project. I don't think it is to far to say that Gamma's design (the first one you show) is not a legitimate option. Good to learn with. However in the real world I see that your approach maybe better.
« Last Edit: February 13, 2011, 05:49:50 PM by pcunite » Logged
Pages: [1]
Print
Jump to:  

Theme orange-lt created by panic