web-dev-qa-db-fra.com

Objets du jeu qui se parlent

Quelle est la bonne façon de traiter les objets et de les faire se parler?

Jusqu'à présent, tous mes jeux de passe-temps/étudiant étaient de petite taille, ce problème a donc été généralement résolu de manière plutôt laide, ce qui a entraîné une intégration étroite et des dépendances circulaires. Ce qui convenait à la taille des projets que je réalisais.

Cependant, mes projets ont augmenté en taille et en complexité et je souhaite maintenant commencer à réutiliser le code et à simplifier ma tête.

Le principal problème que je rencontre est généralement le suivant: Player a besoin de connaître la Map et la même chose pour la Enemy.

J'ai réfléchi dans le sens d'un système de style de message. mais je ne vois pas vraiment comment cela réduirait les dépendances, car j'enverrais toujours les pointeurs partout.

PS: Je suppose que cela a déjà été discuté, mais je ne sais pas comment cela s'appelle, mais le besoin que j'ai.

39
user245019

EDIT: Je décris ci-dessous un système de messagerie d’événements de base que j’ai utilisé maintes et maintes fois. Et il m'est apparu que les deux projets d'école sont open source et sur le Web. Vous pouvez trouver la deuxième version de ce système de messagerie (et bien plus encore) sur http://sourceforge.net/projects/bpfat/ .. Enjoy, et lisez ci-dessous pour une description plus complète du système!

J'ai écrit un système de messagerie générique et je l'ai introduit dans une poignée de jeux publiés sur la PSP, ainsi que dans des logiciels d'application de niveau entreprise. L’intérêt du système de messagerie est de ne transmettre que les données nécessaires au traitement d’un message ou d’un événement, selon la terminologie que vous souhaitez utiliser, afin que les objets ne se connaissent pas. 

Un rapide aperçu de la liste des objets utilisés pour accomplir ceci est quelque chose comme:

struct TEventMessage
{
    int _iMessageID;
}

class IEventMessagingSystem
{
    Post(int iMessageId);
    Post(int iMessageId, float fData);
    Post(int iMessageId, int iData);
    // ...
    Post(TMessageEvent * pMessage);
    Post(int iMessageId, void * pData);
}

typedef float(*IEventMessagingSystem::Callback)(TEventMessage * pMessage);

class CEventMessagingSystem
{
    Init       ();
    DNit       ();
    Exec       (float fElapsedTime);

    Post       (TEventMessage * oMessage);

    Register   (int iMessageId, IEventMessagingSystem* pObject, FObjectCallback* fpMethod);
    Unregister (int iMessageId, IEventMessagingSystem* pObject, FObjectCallback * fpMethod);
}

#define MSG_Startup            (1)
#define MSG_Shutdown           (2)
#define MSG_PlaySound          (3)
#define MSG_HandlePlayerInput  (4)
#define MSG_NetworkMessage     (5)
#define MSG_PlayerDied         (6)
#define MSG_BeginCombat        (7)
#define MSG_EndCombat          (8)

Et maintenant une petite explication. Le premier objet, TEventMessage, est l’objet de base pour représenter les données envoyées par le système de messagerie. Par défaut, l'id du message envoyé sera toujours indiqué, donc si vous voulez vous assurer que vous avez reçu le message que vous attendiez, vous pouvez le faire (en général, je ne le fais que dans le débogage).

Ensuite, la classe Interface fournit un objet générique à utiliser par le système de messagerie pour la diffusion lors des rappels. De plus, cela fournit également une interface «facile à utiliser» pour différents types de données Post () dans le système de messagerie.

Après cela, nous avons notre typedef Callback, simplement, elle attend un objet du type de la classe d’interface et lui transmettra un pointeur TEventMessage ... Vous pouvez éventuellement définir le paramètre const, mais j’ai déjà utilisé le traitement de relance auparavant, par exemple pile de débogage et tel du système de messagerie.

Enfin, l’objet CEventMessagingSystem est au cœur. Cet objet contient un tableau de piles d'objets de rappel (ou de listes ou de files d'attente liées ou de la manière dont vous souhaitez stocker les données). Les objets de rappel, non représentés ci-dessus, doivent conserver (et sont uniquement définis par) un pointeur sur l'objet ainsi que la méthode pour appeler cet objet. Lorsque vous vous inscrivez (), vous ajoutez une entrée sur la pile d'objets sous la position du tableau de l'identifiant de message. Lorsque vous annulez l'inscription (), vous supprimez cette entrée.

C'est fondamentalement ça. Maintenant, cela stipule que tout doit être renseigné sur IEventMessagingSystem et l'objet TEventMessage ... mais cet objet ne doit pas être modifié aussi souvent et ne transmet que les parties d'informations vitales pour la logique dictée par l'événement appelé. De cette façon, un joueur n'a pas besoin de connaître la carte ou l'ennemi directement pour lui envoyer des événements. Un objet géré peut également appeler une API vers un système plus important sans rien connaître à ce sujet.

Par exemple: quand un ennemi meurt, vous voulez qu'il produise un effet sonore. En supposant que votre gestionnaire de sons hérite de l'interface IEventMessagingSystem, vous devez configurer un rappel pour le système de messagerie qui accepterait un TEventMessagePlaySoundEffect ou quelque chose de ce genre. Le gestionnaire de sons enregistre ensuite ce rappel lorsque les effets sonores sont activés (ou annule l'enregistrement du rappel lorsque vous souhaitez désactiver tous les effets sonores pour faciliter l'utilisation des fonctions d'activation/désactivation). Ensuite, vous voudriez que l'objet ennemi hérite également du IEventMessagingSystem, assemblé un objet TEventMessagePlaySoundEffect (aurait besoin de MSG_PlaySound pour son ID de message, puis de l'identifiant de l'effet sonore à jouer, qu'il s'agisse d'un ID int ou du nom du son. effet) et appelez simplement Post (& oEventMessagePlaySoundEffect).

Maintenant, ceci est juste une conception très simple sans implémentation. Si vous avez une exécution immédiate, vous n'avez pas besoin de mettre en mémoire tampon les objets TEventMessage (ce que j'ai principalement utilisé dans les jeux de console). Si vous vous trouvez dans un environnement multithread, les objets et les systèmes s'exécutant dans des threads distincts constituent un moyen très bien défini, mais vous souhaitez conserver les objets TEventMessage afin que les données soient disponibles lors du traitement.

Une autre modification concerne les objets qui ont uniquement besoin de données Post (). Vous pouvez créer un ensemble statique de méthodes dans IEventMessagingSystem afin qu'elles ne soient pas obligées d'en hériter (elles sont utilisées pour la facilité d'accès et de rappel, pas -directly - nécessaire pour les appels Post ()).

Pour toutes les personnes qui mentionnent MVC, c'est un très bon modèle, mais vous pouvez le mettre en œuvre de différentes manières et à différents niveaux. Le projet actuel sur lequel je travaille professionnellement est une configuration MVC environ 3 fois. Il existe le MVC global de l'application entière, puis chaque modèle MV et C est conçu en tant que motif MVC autonome. Donc, ce que j’ai essayé de faire ici, c’est d’expliquer comment créer un C assez générique pour traiter n’importe quel type de M sans avoir à entrer dans une vue ...

Par exemple, un objet lorsqu'il "meurt" peut vouloir jouer un effet sonore .. Vous devez créer une structure pour le système audio telle que TEventMessageSoundEffect qui hérite du TEventMessage et ajoute un ID d'effet sonore (qu'il s'agisse d'un Int préchargé ou le nom du fichier sfx, quelle que soit leur trace dans votre système). Ensuite, tout objet doit simplement assembler un objet TEventMessageSoundEffect avec le bruit de mort approprié et appeler Post (& oEventMessageSoundEffect); object .. En supposant que le son n’est pas mis en sourdine (ce que vous voudriez désenregistrer des gestionnaires de son.

EDIT: Pour clarifier un peu le commentaire ci-dessous: Tout objet pour envoyer ou recevoir un message a simplement besoin de connaître l'interface IEventMessagingSystem, et c'est le seul objet dont EventMessagingSystem a besoin de connaître toutes les autres objets. C'est ce qui vous donne le détachement. Tout objet souhaitant recevoir un message doit simplement s’enregistrer (MSG, Object, Callback). Ensuite, lorsqu'un objet appelle Post (MSG, Data), il l'envoie à EventMessagingSystem via l'interface connue, le EMS notifiera ensuite chaque objet enregistré de l'événement. Vous pouvez faire un MSG_PlayerDied que d'autres systèmes gèrent, ou le joueur peut appeler MSG_PlaySound, MSG_Respawn, etc. pour permettre aux éléments en écoute de réagir à ces messages. Pensez à Post (MSG, Data) en tant qu'API abstraite pour les différents systèmes d'un moteur de jeu.

Oh! Une autre chose qui m'a été signalée. Le système que je décris ci-dessus correspond au modèle Observer de l'autre réponse donnée. Donc, si vous voulez une description plus générale pour donner un peu plus de sens à la mienne, c’est un court article qui lui donne une bonne description.

J'espère que cela aide et amusez-vous!

43
James

les solutions génériques de communication entre objets évitant les couplages étroits:

  1. Motif médiateur
  2. Modèle d'observateur
15
Stephane Rolland

Cela ne s’applique probablement pas seulement aux classes de jeux, mais aux classes au sens général. le modèle MVC (modèle-vue-contrôleur) avec votre pompe à message suggérée est tout ce dont vous avez besoin. 

"Ennemi" et "Joueur" s'intégreront probablement dans la partie Modèle de MVC. Peu importe, mais la règle générale est que tous les modèles et toutes les vues interagissent via le contrôleur. Donc, vous voudriez garder des références (mieux que des pointeurs) à (presque) toutes les autres instances de classe de cette classe 'controller', appelons-le ControlDispatcher. Ajoutez-lui une pompe de messages (varie en fonction de la plate-forme pour laquelle vous codez), instanciez-la d'abord (avant toute autre classe et intégrez les autres objets) ou enfin (et stockez les autres objets comme références dans ControlDispatcher). 

Bien sûr, la classe ControlDispatcher devra probablement être divisée davantage en contrôleurs plus spécialisés afin de conserver le code par fichier entre 700 et 800 lignes (ce qui est au moins la limite pour moi) et il se peut même que le nombre de threads pompant et traitement des messages en fonction de vos besoins.

À votre santé

4
kellogs

Voici un système d’événements écrit pour C++ 11 que vous pouvez utiliser. Il utilise des modèles et des pointeurs intelligents ainsi que des lambdas pour les délégués. C'est très flexible. Ci-dessous, vous trouverez également un exemple. Ecrivez-moi à [email protected] si vous avez des questions à ce sujet. 

Ces classes vous offrent un moyen d'envoyer des événements contenant des données arbitraires et un moyen simple de lier directement des fonctions acceptant les types d'arguments déjà convertis que le système génère et vérifie si la conversion est correcte avant d'appeler votre délégué. 

Fondamentalement, chaque événement est dérivé de la classe IEventData (vous pouvez l'appeler IEvent si vous le souhaitez). Chaque "cadre" que vous appelez ProcessEvents (), point auquel le système d'événements parcourt tous les délégués et appelle les délégués fournis par d'autres systèmes ayant souscrit à chaque type d'événement. Tout le monde peut choisir les événements auxquels il souhaite s’abonner, chaque type d’événement ayant un identifiant unique. Vous pouvez également utiliser lambdas pour vous abonner à des événements tels que: AddListener (MyEvent :: ID (), [&] (shared_ptr ev) { Faites votre travail} .. 

Quoi qu'il en soit, voici la classe avec toute l'implémentation: 

#pragma once

#include <list>
#include <memory>
#include <map>
#include <vector>
#include <functional>

class IEventData {
public:
    typedef size_t id_t; 
    virtual id_t GetID() = 0; 
}; 

typedef std::shared_ptr<IEventData> IEventDataPtr; 
typedef std::function<void(IEventDataPtr&)> EventDelegate; 

class IEventManager {
public:
    virtual bool AddListener(IEventData::id_t id, EventDelegate proc) = 0;
    virtual bool RemoveListener(IEventData::id_t id, EventDelegate proc) = 0; 
    virtual void QueueEvent(IEventDataPtr ev) = 0; 
    virtual void ProcessEvents() = 0; 
}; 


#define DECLARE_EVENT(type) \
    static IEventData::id_t ID(){ \
        return reinterpret_cast<IEventData::id_t>(&ID); \
    } \
    IEventData::id_t GetID() override { \
        return ID(); \
    }\

class EventManager : public IEventManager {
public:
    typedef std::list<EventDelegate> EventDelegateList; 

    ~EventManager(){
    } 
    //! Adds a listener to the event. The listener should invalidate itself when it needs to be removed. 
    virtual bool AddListener(IEventData::id_t id, EventDelegate proc) override; 

    //! Removes the specified delegate from the list
    virtual bool RemoveListener(IEventData::id_t id, EventDelegate proc) override; 

    //! Queues an event to be processed during the next update
    virtual void QueueEvent(IEventDataPtr ev) override; 

    //! Processes all events
    virtual void ProcessEvents() override; 
private:
    std::list<std::shared_ptr<IEventData>> mEventQueue; 
    std::map<IEventData::id_t, EventDelegateList> mEventListeners; 

}; 

//! Helper class that automatically handles removal of individual event listeners registered using OnEvent() member function upon destruction of an object derived from this class. 
class EventListener {
public:
    //! Template function that also converts the event into the right data type before calling the event listener. 
    template<class T>
    bool OnEvent(std::function<void(std::shared_ptr<T>)> proc){
        return OnEvent(T::ID(), [&, proc](IEventDataPtr data){
            auto ev = std::dynamic_pointer_cast<T>(data); 
            if(ev) proc(ev); 
        }); 
    }
protected:
    typedef std::pair<IEventData::id_t, EventDelegate> _EvPair; 
    EventListener(std::weak_ptr<IEventManager> mgr):_els_mEventManager(mgr){

    }
    virtual ~EventListener(){
        if(_els_mEventManager.expired()) return; 
        auto em = _els_mEventManager.lock(); 
        for(auto i : _els_mLocalEvents){
            em->RemoveListener(i.first, i.second); 
        }
    }

    bool OnEvent(IEventData::id_t id, EventDelegate proc){
        if(_els_mEventManager.expired()) return false; 
        auto em = _els_mEventManager.lock(); 
        if(em->AddListener(id, proc)){
            _els_mLocalEvents.Push_back(_EvPair(id, proc)); 
        }
    }
private:
    std::weak_ptr<IEventManager> _els_mEventManager; 
    std::vector<_EvPair>        _els_mLocalEvents; 
    //std::vector<_DynEvPair> mDynamicLocalEvents; 
}; 

Et le fichier Cpp: 

#include "Events.hpp"

using namespace std; 

bool EventManager::AddListener(IEventData::id_t id, EventDelegate proc){
    auto i = mEventListeners.find(id); 
    if(i == mEventListeners.end()){
        mEventListeners[id] = list<EventDelegate>(); 
    }
    auto &list = mEventListeners[id]; 
    for(auto i = list.begin(); i != list.end(); i++){
        EventDelegate &func = *i; 
        if(func.target<EventDelegate>() == proc.target<EventDelegate>()) 
            return false; 
    }
    list.Push_back(proc); 
}

bool EventManager::RemoveListener(IEventData::id_t id, EventDelegate proc){
    auto j = mEventListeners.find(id); 
    if(j == mEventListeners.end()) return false; 
    auto &list = j->second; 
    for(auto i = list.begin(); i != list.end(); ++i){
        EventDelegate &func = *i; 
        if(func.target<EventDelegate>() == proc.target<EventDelegate>()) {
            list.erase(i); 
            return true; 
        }
    }
    return false; 
}

void EventManager::QueueEvent(IEventDataPtr ev) {
    mEventQueue.Push_back(ev); 
}

void EventManager::ProcessEvents(){
    size_t count = mEventQueue.size(); 
    for(auto it = mEventQueue.begin(); it != mEventQueue.end(); ++it){
        printf("Processing event..\n"); 
        if(!count) break; 
        auto &i = *it; 
        auto listeners = mEventListeners.find(i->GetID()); 
        if(listeners != mEventListeners.end()){
            // Call listeners
            for(auto l : listeners->second){
                l(i); 
            }
        }
        // remove event
        it = mEventQueue.erase(it); 
        count--; 
    }
}

J'utilise une classe EventListener pour des raisons de commodité en tant que classe de base pour toutes les classes qui souhaitent écouter des événements. Si vous dérivez votre classe d'écoute de cette classe et la fournissez à votre gestionnaire d'événements, vous pouvez utiliser la fonction très pratique OnEvent (..) pour enregistrer vos événements. Et la classe de base va automatiquement désabonner votre classe dérivée de tous les événements quand elle est détruite. C'est très pratique, car oublier de supprimer un délégué du gestionnaire d'événements lorsque votre classe est détruite entraînera presque certainement le blocage de votre programme. 

Un moyen pratique d'obtenir un identifiant de type unique pour un événement en déclarant simplement une fonction statique dans la classe, puis en transformant son adresse en int. Comme chaque classe aura cette méthode sur différentes adresses, elle peut être utilisée pour une identification unique des événements de classe. Vous pouvez également convertir typename () en int pour obtenir un identifiant unique si vous le souhaitez. il y a différentes facons de faire cela. 

Voici donc un exemple d'utilisation: 

#include <functional>
#include <memory>
#include <stdio.h>
#include <list>
#include <map>

#include "Events.hpp"
#include "Events.cpp"

using namespace std; 

class DisplayTextEvent : public IEventData {
public:
    DECLARE_EVENT(DisplayTextEvent); 

    DisplayTextEvent(const string &text){
        mStr = text; 
    }
    ~DisplayTextEvent(){
        printf("Deleted event data\n"); 
    }
    const string &GetText(){
        return mStr; 
    }
private:
    string mStr; 
}; 

class Emitter { 
public:
    Emitter(shared_ptr<IEventManager> em){
        mEmgr = em; 
    }
    void EmitEvent(){
        mEmgr->QueueEvent(shared_ptr<IEventData>(
            new DisplayTextEvent("Hello World!"))); 
    }
private:
    shared_ptr<IEventManager> mEmgr; 
}; 

class Receiver : public EventListener{
public:
    Receiver(shared_ptr<IEventManager> em) : EventListener(em){
        mEmgr = em; 

        OnEvent<DisplayTextEvent>([&](shared_ptr<DisplayTextEvent> data){
            printf("It's working: %s\n", data->GetText().c_str()); 
        }); 
    }
    ~Receiver(){
        mEmgr->RemoveListener(DisplayTextEvent::ID(), std::bind(&Receiver::OnExampleEvent, this, placeholders::_1)); 
    }
    void OnExampleEvent(IEventDataPtr &data){
        auto ev = dynamic_pointer_cast<DisplayTextEvent>(data); 
        if(!ev) return; 
        printf("Received event: %s\n", ev->GetText().c_str()); 
    }
private:
    shared_ptr<IEventManager> mEmgr; 
}; 

int main(){
    auto emgr = shared_ptr<IEventManager>(new EventManager()); 


    Emitter emit(emgr); 
    {
        Receiver receive(emgr); 

        emit.EmitEvent(); 
        emgr->ProcessEvents(); 
    }
    emit.EmitEvent(); 
    emgr->ProcessEvents(); 
    emgr = 0; 

    return 0; 
}
3
Martin

Soyez prudent avec "un système de style de message", cela dépend probablement de la mise en œuvre, mais vous perdriez généralement la vérification de type statique, et vous risqueriez de générer des erreurs très difficiles à corriger. Notez que les méthodes de l'objet appelant sont déjà un système de type message.

Il manque probablement certains niveaux d'abstraction. Par exemple, pour la navigation, un joueur peut utiliser un navigateur au lieu de tout savoir sur la carte elle-même. Vous dites également que this has usually descended into setting lots of pointers, quels sont ces pointeurs? Probablement que vous leur donnez une mauvaise abstraction?… Faire en sorte que les objets connaissent les autres directement, sans passer par les interfaces et les intermédiaires, est un moyen simple d'obtenir une conception à couplage étroit.

0
Roman L

La messagerie est certainement une excellente façon de faire, mais les systèmes de messagerie peuvent avoir beaucoup de différences. Si vous voulez garder vos classes propres et nettes, écrivez-les pour ne pas connaître le système de messagerie et demandez-leur de prendre des dépendances sur quelque chose de simple, comme un 'ILocationService', qui peut ensuite être implémenté pour publier/demander des informations à des objets comme la classe Map. . Même si vous allez vous retrouver avec plus de classes, elles seront petites, simples et encourageront un design épuré.

La messagerie ne se limite pas au découplage, elle vous permet également de passer à une architecture plus asynchrone, concurrente et réactive. Patterns of Enterprise Integration de Gregor Hophe est un excellent livre qui parle de bons modèles de messagerie. La mise en œuvre par Erlang OTP ou Scala du modèle d'acteur m'a fourni de nombreuses indications.

0
Alex Robson