Comment mettre en œuvre l'interaction entre les pièces du moteur?

10

Je veux poser une question sur la façon dont l'échange d'informations entre les parties du moteur de jeu doit être implémenté.

Le moteur est séparé en quatre parties: logique, données, interface utilisateur, graphiques. Au début, j'ai fait cet échange à travers les drapeaux. Par exemple, si le nouvel objet est ajouté dans les données, l'indicateur isNewdans la classe d'un objet sera défini comme true. Et après cela, la partie graphique du moteur vérifiera ce drapeau et ajoutera l'objet dans le monde du jeu.

Cependant, avec cette approche, je devais écrire beaucoup de code pour traiter chaque indicateur de chaque type d'objet.

J'ai pensé à utiliser un système d'événement, mais je n'ai pas assez d'expérience pour savoir si ce serait la bonne solution.

Le système d'événements est-il la seule approche appropriée, ou dois-je utiliser autre chose?

J'utilise Ogre comme moteur graphique, si cela importe.

Userr
la source
C'est une question très vague. L'interaction de vos systèmes va être très liée à la conception de vos systèmes et au type d'encapsulation que vous finissez par faire. Mais une chose ressort: "Et après cela, la partie graphique du moteur vérifiera ce drapeau et ajoutera l'objet dans le monde du jeu." Pourquoi la partie graphique du moteur ajoute-t-elle des choses au monde ? Il semble que le monde devrait dire au module graphique ce qu'il doit rendre.
Tetrad
Dans le moteur, la partie "graphiques" contrôle l'Ogre (par exemple, lui dit d'ajouter un objet dans la scène). Mais pour cela, il recherche également dans les "données" l'objet qui est nouveau (et après cela dit à Ogre de l'ajouter dans la scène) Mais je ne sais pas si cette approche est bonne ou mauvaise en raison du manque d'expérience.
Userr

Réponses:

20

Ma structure de moteur de jeu préférée est l'interface et le modèle de composant objet <-> utilisant la messagerie pour la communication entre presque toutes les parties.

Vous disposez de plusieurs interfaces pour les principales parties du moteur telles que votre gestionnaire de scène, chargeur de ressources, audio, moteur de rendu, physique, etc.

J'ai le gestionnaire de scène en charge de tous les objets dans la scène / monde 3D.

L'objet est une classe très atomique, ne contenant que quelques éléments communs à presque tout dans votre scène, dans mon moteur, la classe d'objet ne contient que la position, la rotation, une liste de composants et un ID unique. L'ID de chaque objet est généré par un int statique, de sorte que deux objets n'auront pas tous le même ID, cela vous permet d'envoyer des messages à un objet par son ID, plutôt que d'avoir à avoir un pointeur sur l'objet.

La liste des composants sur l'objet est ce qui donne à ces objets ses propriétés principales. Par exemple, pour quelque chose que vous pouvez voir dans le monde 3D, vous donneriez à votre objet un composant de rendu qui contient les informations sur le maillage de rendu. Si vous voulez qu'un objet ait de la physique, vous lui donnerez une composante physique. Si vous voulez que quelque chose agisse comme une caméra, donnez-lui un composant de caméra. La liste des composants peut s'allonger indéfiniment.

La communication entre les interfaces, les objets et les composants est essentielle. Dans mon moteur, j'ai une classe de message générique qui ne contient qu'un ID unique et un ID de type de message. L'ID unique est l'ID de l'objet auquel vous voulez que le message aille et l'ID de type de message est utilisé par l'objet qui reçoit le message afin qu'il sache de quel type de message il s'agit.

Les objets peuvent gérer le message s'ils en ont besoin, et ils peuvent transmettre le message à chacun de leurs composants, et les composants feront souvent des choses importantes avec le message. Par exemple, si vous souhaitez modifier la position de l'objet et lui envoyer un message SetPosition, l'objet peut mettre à jour sa variable de position lorsqu'il reçoit le message, mais le composant de rendu peut avoir besoin d'un message pour mettre à jour la position du maillage de rendu, et le composant physique peut avoir besoin du message pour mettre à jour la position du corps physique.

Voici une présentation très simple du gestionnaire de scène, de l'objet et du composant, et du flux de messages, que j'ai fouetté en environ une heure, écrite en C ++. Lorsqu'il est exécuté, il définit la position sur un objet et le message passe par le composant de rendu, puis récupère la position de l'objet. Prendre plaisir!

En outre, j'ai écrit une version C # et une version Scala du code ci-dessous pour tous ceux qui pourraient parler couramment ceux-ci plutôt que C ++.

#include <iostream>
#include <stdio.h>

#include <list>
#include <map>

using namespace std;

struct Vector3
{
public:
    Vector3() : x(0.0f), y(0.0f), z(0.0f)
    {}

    float x, y, z;
};

enum eMessageType
{
    SetPosition,
    GetPosition,    
};

class BaseMessage
{
protected: // Abstract class, constructor is protected
    BaseMessage(int destinationObjectID, eMessageType messageTypeID) 
        : m_destObjectID(destinationObjectID)
        , m_messageTypeID(messageTypeID)
    {}

public: // Normally this isn't public, just doing it to keep code small
    int m_destObjectID;
    eMessageType m_messageTypeID;
};

class PositionMessage : public BaseMessage
{
protected: // Abstract class, constructor is protected
    PositionMessage(int destinationObjectID, eMessageType messageTypeID, 
                    float X = 0.0f, float Y = 0.0f, float Z = 0.0f)
        : BaseMessage(destinationObjectID, messageTypeID)
        , x(X)
        , y(Y)
        , z(Z)
    {

    }

public:
    float x, y, z;
};

class MsgSetPosition : public PositionMessage
{
public:
    MsgSetPosition(int destinationObjectID, float X, float Y, float Z)
        : PositionMessage(destinationObjectID, SetPosition, X, Y, Z)
    {}
};

class MsgGetPosition : public PositionMessage
{
public:
    MsgGetPosition(int destinationObjectID)
        : PositionMessage(destinationObjectID, GetPosition)
    {}
};

class BaseComponent
{
public:
    virtual bool SendMessage(BaseMessage* msg) { return false; }
};

class RenderComponent : public BaseComponent
{
public:
    /*override*/ bool SendMessage(BaseMessage* msg)
    {
        // Object has a switch for any messages it cares about
        switch(msg->m_messageTypeID)
        {
        case SetPosition:
            {                   
                // Update render mesh position/translation

                cout << "RenderComponent handling SetPosition\n";
            }
            break;
        default:
            return BaseComponent::SendMessage(msg);
        }

        return true;
    }
};

class Object
{
public:
    Object(int uniqueID)
        : m_UniqueID(uniqueID)
    {
    }

    int GetObjectID() const { return m_UniqueID; }

    void AddComponent(BaseComponent* comp)
    {
        m_Components.push_back(comp);
    }

    bool SendMessage(BaseMessage* msg)
    {
        bool messageHandled = false;

        // Object has a switch for any messages it cares about
        switch(msg->m_messageTypeID)
        {
        case SetPosition:
            {               
                MsgSetPosition* msgSetPos = static_cast<MsgSetPosition*>(msg);
                m_Position.x = msgSetPos->x;
                m_Position.y = msgSetPos->y;
                m_Position.z = msgSetPos->z;

                messageHandled = true;
                cout << "Object handled SetPosition\n";
            }
            break;
        case GetPosition:
            {
                MsgGetPosition* msgSetPos = static_cast<MsgGetPosition*>(msg);
                msgSetPos->x = m_Position.x;
                msgSetPos->y = m_Position.y;
                msgSetPos->z = m_Position.z;

                messageHandled = true;
                cout << "Object handling GetPosition\n";
            }
            break;
        default:
            return PassMessageToComponents(msg);
        }

        // If the object didn't handle the message but the component
        // did, we return true to signify it was handled by something.
        messageHandled |= PassMessageToComponents(msg);

        return messageHandled;
    }

private: // Methods
    bool PassMessageToComponents(BaseMessage* msg)
    {
        bool messageHandled = false;

        auto compIt = m_Components.begin();
        for ( compIt; compIt != m_Components.end(); ++compIt )
        {
            messageHandled |= (*compIt)->SendMessage(msg);
        }

        return messageHandled;
    }

private: // Members
    int m_UniqueID;
    std::list<BaseComponent*> m_Components;
    Vector3 m_Position;
};

class SceneManager
{
public: 
    // Returns true if the object or any components handled the message
    bool SendMessage(BaseMessage* msg)
    {
        // We look for the object in the scene by its ID
        std::map<int, Object*>::iterator objIt = m_Objects.find(msg->m_destObjectID);       
        if ( objIt != m_Objects.end() )
        {           
            // Object was found, so send it the message
            return objIt->second->SendMessage(msg);
        }

        // Object with the specified ID wasn't found
        return false;
    }

    Object* CreateObject()
    {
        Object* newObj = new Object(nextObjectID++);
        m_Objects[newObj->GetObjectID()] = newObj;

        return newObj;
    }

private:
    std::map<int, Object*> m_Objects;
    static int nextObjectID;
};

// Initialize our static unique objectID generator
int SceneManager::nextObjectID = 0;

int main()
{
    // Create a scene manager
    SceneManager sceneMgr;

    // Have scene manager create an object for us, which
    // automatically puts the object into the scene as well
    Object* myObj = sceneMgr.CreateObject();

    // Create a render component
    RenderComponent* renderComp = new RenderComponent();

    // Attach render component to the object we made
    myObj->AddComponent(renderComp);

    // Set 'myObj' position to (1, 2, 3)
    MsgSetPosition msgSetPos(myObj->GetObjectID(), 1.0f, 2.0f, 3.0f);
    sceneMgr.SendMessage(&msgSetPos);
    cout << "Position set to (1, 2, 3) on object with ID: " << myObj->GetObjectID() << '\n';

    cout << "Retreiving position from object with ID: " << myObj->GetObjectID() << '\n';

    // Get 'myObj' position to verify it was set properly
    MsgGetPosition msgGetPos(myObj->GetObjectID());
    sceneMgr.SendMessage(&msgGetPos);
    cout << "X: " << msgGetPos.x << '\n';
    cout << "Y: " << msgGetPos.y << '\n';
    cout << "Z: " << msgGetPos.z << '\n';
}
Nic Foster
la source
1
Ce code est vraiment sympa. Ça me rappelle l'Unité.
Tili
Je sais que c'est une vieille réponse, mais j'ai quelques questions. Un «vrai» jeu n'aurait-il pas des centaines de types de messages, faisant un cauchemar de codage? De plus, que faites-vous si vous avez besoin (par exemple) de l'orientation du personnage principal pour le dessiner correctement. N'auriez-vous pas besoin de créer un nouveau GetSpriteMessage et de l'envoyer à chaque fois que vous effectuez le rendu? Cela ne devient-il pas trop cher? Je me demandais juste! Merci.
you786
Dans mon dernier projet, nous avons utilisé XML pour écrire les messages et un script python a créé tout le code pour nous pendant la construction. Vous pouvez vous séparer en plusieurs XML pour différentes catégories de messages. Vous pouvez créer des macros pour l'envoi de messages, les rendant presque aussi laconiques qu'un appel de fonction, si vous aviez besoin de la façon dont un personnage était confronté sans messagerie, vous auriez toujours besoin d'obtenir le pointeur sur le composant, puis de connaître la fonction à appeler (si vous n'utilisiez pas la messagerie). RenderComponent peut s'enregistrer auprès du moteur de rendu pour que vous n'ayez pas à l'interroger à chaque image.
Nic Foster
2

Je pense que c'est la meilleure façon d'utiliser Scene Manager et Interfaces. Avoir une messagerie implémentée mais je l'utiliserais comme approche secondaire. La messagerie est bonne pour la communication entre les threads. Utilisez l'abstraction (interfaces) partout où vous le pouvez.

Je ne connais pas grand chose à Ogre, donc je parle en général.

Au cœur, vous avez la boucle de jeu principale. Il reçoit les signaux d'entrée, calcule l'IA (du mouvement simple à l'IA complexe et à la logique de jeu), charge les ressources [, etc.] et rend l'état actuel. Il s'agit d'un exemple de base, vous pouvez donc séparer le moteur en ces parties (InputManager, AIManager, ResourceManager, RenderManager). Et vous devriez avoir SceneManager qui contient tous les objets présents dans le jeu.

Chacune de ces parties et leurs sous-parties ont des interfaces. Essayez donc d'organiser ces parties pour faire leur et seulement leur travail. Ils doivent utiliser des sous-parties qui interagissent en interne aux fins de leur partie parent. De cette façon, vous ne vous embarquerez pas sans risque de dérouler sans réécriture totale.

ps si vous utilisez C ++ pensez à utiliser le modèle RAII

edin-m
la source
2
RAII n'est pas un modèle, c'est un mode de vie.
Shotgun Ninja