O padrão do observador é muito usado em videogames quando vários subsistemas de jogo precisam ser notificados de um evento. O padrão original envolve a definição de uma interface que cada observador implementará, mas uma maneira muito mais simples de fazer isso é usando sinais . Neste artigo, discutimos a evolução desse padrão nos jogos móveis do SocialPoint .
Um exemplo típico de um evento de jogo observável seria uma mudança na pontuação do jogo. Implementar isso em C ++ é fácil.
class IGameScoreObserver
{
public:
virtual IGameScoreObserver(){};
virtual void notifyGameScoreChanged(unsigned score) = 0;
};
class GameScore
{
private:
std::vector<IGameScoreObserver*> _observers;
unsigned _value;
public:
GameScore(unsigned value=0) : _value(value)
{
}
void setValue(unsigned value)
{
_value = value;
for(auto observer : _observers)
{
observer->notifyGameScoreChanged(value);
}
}
void addObserver(IGameScoreObserver& observer)
{
_observers.push_back(&observer);
}
void removeObserver(IGameScoreObserver& observer)
{
_observers.erase(std::remove(_observers.begin(), _observers.end(), &observer), _observers.end());
}
};
Essa implementação é simples e funcionaria, mas tem dois problemas principais:
- há algum código padrão que deve ser escrito para cada observador (poderia ser melhorado escrevendo uma classe de assunto abstrato usando modelos)
- pode levar a herança múltipla em classes que implementam o
IGameScoreObserver
que pode levar ao temido diamante da morte
Usar sinais resolve esses dois problemas, ocultando o contêiner do observador dentro de um objeto de função . Em nosso caso, começamos a usar boost :: signal, pois em nossos primeiros jogos para celular ainda não havia suporte para C ++ 11 e já estávamos usando algumas das bibliotecas boost .
Nossa primeira implementação usando parecia algo assim:boost::signal
#include <boost/signal.hpp>
class Signals
{
private:
Signals(){};
public:
static boost::signal<void(unsigned value)> gameScoreChanged;
};
class GameScore
{
public:
GameScore(unsigned value=0) : _value(value)
{
}
void setValue(unsigned value)
{
_value = value;
Signals::gameScoreChanged(value);
}
};
Como você pode ver, muito menos código clichê e nenhuma interface necessária para o observador de pontuação do jogo. A conexão e o observador são feitos usando .boost::function
#include <boost/function.hpp>
ScoreView view;
boost::signal<void(unsigned value)>::connection connGameScoreChanged = Signals::gameScoreChanged.connect(boost::function(&ScoreView::updateScore, view, _1));
O retorna um objeto de conexão que pode ser usado para remover o observador. Há também um que se desconectará automaticamente quando o objeto for destruído. Desconectar o sinal é importante para evitar o travamento que ocorrerá ao chamar o em um objeto que já está destruído.boost::signal
boost::scoped_connection
boost::function
Conseguimos fazer isso funcionar e parecia ok, mas introduzimos novos problemas devido à maneira como estávamos usando:
boost::signal
pode retornar um valor não vazio, esse valor é gerado a partir dos valores de retorno das funções conectadas usando um argumento de modelo de sinal adicional denominado combinador . Essa funcionalidade é confusa e não faz sentido no contexto do padrão do observador original.boost::signal
os objetos são todos públicos e agrupados em um cabeçalho gigante. Isso cria dependências artificiais e também aumenta o tempo de compilação.- a implementação do padrão de observador usando é mostrado para o exterior
boost::signal
Em nossos jogos mais novos, tentamos resolver esses problemas e também tentar mudar para o C ++ 11 removendo a dependência do boost. A primeira coisa que fizemos foi implementar nossa própria classe de sinal.
template<class... F>
class SignalConnection;
template<class... F>
class ScopedSignalConnection;
template<class... F>
class SignalConnectionItem
{
public:
typedef std::function<void(F...)> Callback;
private:
Callback _callback;
bool _connected;
public:
SignalConnectionItem(const Callback& cb, bool connected=true) :
_callback(cb), _connected(connected)
{
}
void operator()(F... args)
{
if(_connected && _callback)
{
_callback(args...);
}
}
bool connected() const
{
return _connected;
}
void disconnect()
{
_connected = false;
}
};
template<class... F>
class Signal
{
public:
typedef std::function<void(F...)> Callback;
typedef SignalConnection<F...> Connection;
typedef ScopedSignalConnection<F...> ScopedConnection;
private:
typedef SignalConnectionItem<F...> ConnectionItem;
typedef std::list<std::shared_ptr<ConnectionItem>> ConnectionList;
ConnectionList _list;
unsigned _recurseCount;
void clearDisconnected()
{
_list.erase(std::remove_if(_list.begin(), _list.end(), [](std::shared_ptr<ConnectionItem>& item){
return !item->connected();
}), _list.end());
}
public:
Signal() :
_recurseCount(0)
{
}
~Signal()
{
for(auto& item : _list)
{
item->disconnect();
}
}
void operator()(F... args)
{
std::list<std::shared_ptr<ConnectionItem>> list;
for(auto& item : _list)
{
if(item->connected())
{
list.push_back(item);
}
}
_recurseCount++;
for(auto& item : list)
{
(*item)(args...);
}
_recurseCount--;
if(_recurseCount == 0)
{
clearDisconnected();
}
};
Connection connect(const Callback& callback)
{
auto item = std::make_shared<ConnectionItem>(callback, true);
_list.push_back(item);
return Connection(*this, item);
}
bool disconnect(const Connection& connection)
{
bool found = false;
for(auto& item : _list)
{
if(connection.hasItem(*item) && item->connected())
{
found = true;
item->disconnect();
}
}
if(found)
{
clearDisconnected();
}
return found;
}
void disconnectAll()
{
for(auto& item : _list)
{
item->disconnect();
}
clearDisconnected();
}
friend class Connecion;
};
template<class... F>
class SignalConnection
{
private:
typedef SignalConnectionItem<F...> Item;
Signal<F...>* _signal;
std::shared_ptr<Item> _item;
public:
SignalConnection()
: _signal(nullptr)
{
}
SignalConnection(Signal<F...>& signal, const std::shared_ptr<Item>& item)
: _signal(&signal), _item(item)
{
}
void operator=(const SignalConnection& other)
{
_signal = other._signal;
_item = other._item;
}
virtual ~SignalConnection()
{
}
bool hasItem(const Item& item) const
{
return _item.get() == &item;
}
bool connected() const
{
return _item->connected;
}
bool disconnect()
{
if(_signal && _item && _item->connected())
{
return _signal->disconnect(*this);
}
return false;
}
};
template<class... F>
class ScopedSignalConnection : public SignalConnection<F...>
{
public:
ScopedSignalConnection()
{
}
ScopedSignalConnection(Signal<F...>* signal, void* callback)
: SignalConnection<F...>(signal, callback)
{
}
ScopedSignalConnection(const SignalConnection<F...>& other)
: SignalConnection<F...>(other)
{
}
~ScopedSignalConnection()
{
disconnect();
}
ScopedSignalConnection & operator=(const SignalConnection<F...>& connection)
{
disconnect();
SignalConnection<F...>::operator=(connection);
return *this;
}
};
Esta implementação do C ++ 11 funciona exatamente da mesma maneira e usa um para compartilhar um . Este ponteiro compartilhado é usado pela conexão e o sinal para marcar a conexão como desconectada, mas não é público. O template de sinal não permite que você retorne valores diferentes de void, o que era um problema , e também implementamos uma classe para um idioma RAII mais simples .boost::signal
std::shared_ptr
SignalConnectionItem
boost::signal
ScopedConnection
Ao usar o novo sinal, mudamos a implementação para ocultá-lo inteiramente dos observadores.
class GameScore
{
private:
typedef Signal<unsigned> ChangedSignal;
ChangedSignal _changedEvent;
public:
typedef ChangedSignal::Callback ChangedCallback;
typedef ChangedSignal::Connection ChangedConnection;
typedef ChangedSignal::ScopedConnection ChangedScopedConnection;
GameScore(unsigned value=0) : _value(value)
{
}
void setValue(unsigned value)
{
_value = value;
_changedEvent(value);
}
ChangedConnection addObserver(const ChangedCallback& callback)
{
return _changedEvent.connect(callback);
}
};
Desta forma, a implementação do padrão do observador usando o sinal não é mostrada para o exterior. Também adicionamos cada sinal como uma propriedade ao objeto que irá gerar o evento, removendo o cabeçalho gigante da lista de sinais.
Agora, ouvir este evento é muito mais limpo.
class ScoreView
{
private:
GameScore::ScopedConnection _scoreChangedConnection;
public:
GameView(GameScore& score) :
_scoreChangedConnection(score.addObserver(std::bind(&ScoreView::updateScore, this, std::placeholders::_1)))
{
}
~GameView()
{
// no need to disconnect when using ScopedConnection
}
void updateScore(unsigned score)
{
// update the score view
}
};
Todo o código-fonte desta página Copyright (c) 2016 Miguel Ibero
Licenciado ao abrigo da Licença MIT
https://opensource.org/licenses/MIT