Archives pour la catégorie Gameplay

Système d’Events pour gérer les événements du jeu (2ème approche)

Intro :

gears2a

Dans la conception d’un jeu il est très important d’énumérer tous les mécanismes intrinsèques au comportement du jeu, c’est-à-dire les événements du gameplay, du son, de l’interface, du réseau, de la gestion des collisions et de l’IA et aussi de pouvoir les invoquer facilement, proprement et de manière élégante.

Ceci constitue la deuxième approche au problème.

Prérequis :

– La compréhension de l’utilisation de la classe DataParameters, voir cet article.

– La compréhension de l’utilisation des delegates en C++11, voir cet article.

Explications :

 

Résumé :

 

Références :

– Game Coding Complete 4 (code LGPL)

Système d’Events pour gérer les événements du jeu (1ère approche)

b7ef9923828960d1a0935b08fb5ac0d2

Intro :

Dans la conception d’un jeu il est très important d’énumérer tous les mécanismes intrinsèques au comportement du jeu, c’est-à-dire des événements du gameplay, du son, de l’interface, du réseau, de la gestion des collisions et de l’IA.

Ceci est une première approche au problème.

J’ai écrit un article sur une deuxième approche plus juste et mieux adaptée.

Ici on parle des événements, nommés aussi Events.

Prérequis :

La compréhension de l’utilisation de la classe DataParameters.

Explications / exemples :

On se sert de deux macros.

Voici des macros permettant de simplifier la définition de vos Events  :


#define DECLARE_GAME_EVENT(eventName) class eventName : public Event \
      { \
          public: \
          eventName() :  \
          Event(#eventName, 0) \
          {} \
          virtual bool performTask(); \
          static const int GameEventID = Event::eventName; \
      };

#define DECLARE_GAME_EVENT_WITH_PARAMETERS(eventName, argsNumber, ...) class eventName : public Event \
      { \
          public: \
          eventName() :  \
          Event(#eventName, argsNumber, __VA_ARGS__)\
          {} \
          virtual bool performTask(); \
          static const int GameEventID = Event::eventName; \
      };

#define IMPLEMENT_GAME_EVENT(eventName) bool eventName::performTask()

Je vous fournis deux macros : une pour un Event sans paramètres et une autre pour les Events
avec paramètres.

Voici le fichier .h de la classe Event :


class Event
{
public:
    static const enum GameEvents
    {
        /* Exemples d'events à titre indicatif */
        OnTakeScreenShot,
        OnMobAggro,
        OnMobUnAggro,
        OnMobIdle,
        OnMobAttack,
        OnConsoleScrollUp,
        OnConsoleScrollDown,
        OnHeal,
        OnEnemyDamage,
        OnFocusTarget,
        OnSpellCast,
        OnMount,
        OnUnMount,
        OnGUIButtonClick,
        OnGUIButtonHover,
        OnAddItem,
        OnRemoveItem,
        OnKill,
        OnTextPaste,
        OnToggleCameraMode
    };

    Event(const char* sName, unsigned int iArgsNumber, ...);

    virtual ~Event();
    virtual void trigger();

    const std::string& getName() const;
    time_t getTimeLastTriggered() const;
    bool hasLastTriggerSucceed() const;

    // Associe un autre event à invoquer en cascade
    void pushChildEvent(Event* pEvent);

    // Paramètre associé à l'Event invoqué
    void setParameter(DataParameters& params);
    // Paramètre associé à l'Event invoqué
    DataParameters* getParameters();

    bool getLogWriting();
    void setLogWriting(bool bWrite);

    void getParametersName(std::vector<std::string>& paramsNames);

protected:
    virtual bool performTask() = 0;
    void setName(const std::string& sName);

private:
    std::string m_sName;
    time_t m_timeLastTriggered;
    bool m_bLastTriggerSucceed;

    std::vector<Event*> m_childrenEvents;

    // Une liste des noms de paramètres de l'event
    std::vector<std::string> m_parametersNames;
    DataParameters m_params;

    bool m_bLogWriting;
};

Voici le fichier .cpp


Event::Event(const char* sName, unsigned int iArgsNumber, ...) :
m_sName(sName),
m_timeLastTriggered(0),
m_bLastTriggerSucceed(false),
m_bLogWriting(true)
{
    va_list args;
    va_start(args, iArgsNumber);
    for (unsigned i = 0; i < iArgsNumber; i++)
    {
        std::string sArgName = va_arg(args, char*);

        m_parametersNames.push_back(sArgName);
    }
    va_end(args);
}

Event::~Event()
{
}

void Event::trigger()
{       
    time(&m_timeLastTriggered);
    m_bLastTriggerSucceed = performTask();

    if (getLogWriting())
    {
        //SYSTEM_LOG->log(Logger::MessageType::MSG_TYPE_EVENT, std::string("Event effectué : " + getName()).c_str());
    }

    // Invoque aussi les events enfants et associés (en cascade) :
    for (unsigned int i = 0; i < m_childrenEvents.size(); i++)
    {
        Event* pEvent = m_childrenEvents[i];
        
        if (pEvent)
            pEvent->trigger();
    }
}

const std::string& Event::getName() const
{
    return m_sName;
}

time_t Event::getTimeLastTriggered() const
{
    return m_timeLastTriggered;
}

bool Event::hasLastTriggerSucceed() const
{
    return m_bLastTriggerSucceed;
}

void Event::setName(const std::string& sName)
{
    m_sName = sName;
}

void Event::pushChildEvent(Event* pEvent)
{
    if (pEvent)
    {
        m_childrenEvents.push_back(pEvent);
    }
}

void Event::setParameter(DataParameters& params)
{
    m_params = params;
}

DataParameters* Event::getParameters()
{
    return &m_params;
}

bool Event::getLogWriting()
{
    return m_bLogWriting;
}

void Event::setLogWriting(bool bWrite)
{
    m_bLogWriting = bWrite;
}

void Event::getParametersName(std::vector<std::string>& paramsNames)
{
    paramsNames = m_parametersNames;
}

On définit les Events comme suit :


DECLARE_GAME_EVENT( OnTakeScreenShot );
DECLARE_GAME_EVENT( OnMobAggro );
DECLARE_GAME_EVENT( OnMobUnAggro );
DECLARE_GAME_EVENT( OnMobIdle );
DECLARE_GAME_EVENT( OnMobAttack );
DECLARE_GAME_EVENT( OnConsoleScrollUp );
DECLARE_GAME_EVENT( OnConsoleScrollDown );
DECLARE_GAME_EVENT( OnHeal );
DECLARE_GAME_EVENT( OnEnemyDamage );
DECLARE_GAME_EVENT( OnFocusTarget );
DECLARE_GAME_EVENT( OnSpellCast );
DECLARE_GAME_EVENT( OnMount );
DECLARE_GAME_EVENT( OnUnMount );
DECLARE_GAME_EVENT( OnGUIButtonClick );
DECLARE_GAME_EVENT( OnGUIButtonHover );
DECLARE_GAME_EVENT( OnAddItem );
DECLARE_GAME_EVENT( OnRemoveItem );
DECLARE_GAME_EVENT( OnKill );
DECLARE_GAME_EVENT( OnTextPaste );
DECLARE_GAME_EVENT( OnToggleCameraMode );

Et on implémente les Events comme suit :


IMPLEMENT_GAME_EVENT( OnTextPaste )
{
    if (OpenClipboard(NULL))
    {
        HANDLE hData = GetClipboardData(CF_TEXT);

        if (hData)
        {
            char* pszText = static_cast<char*>(GlobalLock(hData));

            // Release the lock
            GlobalUnlock( hData );
            // Release the clipboard
            CloseClipboard();

            if (strlen(pszText) > 100)
            {
                return true;
            }

            switch (GAME_STATE_MANAGER->getState())
            {
                case GameState::ON_CONSOLE:
                    CONSOLE->appendCommandLineText(pszText);
                    break;

                case GameState::ON_LOADING_MENU:
                    break;
            }

        }
    }

    return true;
}

IMPLEMENT_GAME_EVENT( OnCameraMovementUpwardRight  )
{
    switch (GAME_STATE_MANAGER->getState())
    {
        case GameState::ON_WORLD:
            getGlobalSystems()->
                 getCameraManager()->setCameraMovement(CameraManager::CAMERA_MOVE_UPWARD |
                                                       CameraManager::CAMERA_STRAFE_RIGHT);
            break;
    }

    return true;
}

Voici le fichier EventManager.h :


class Event;
class DataParameters;

class EventManager
{
public:
    EventManager();
    virtual ~EventManager();
    
    void registerEvent(Event* const pEvent);

    // Invoque un event avec spécification de paramètres
    void triggerEvent(const std::string& sEventName);
    // Invoque un event
    void triggerEvent(const std::string& sEventName, DataParameters& p);

    // On invoque la fonction callback depuis un fichier script lua
    void triggerLuaCallback(const std::string& sEventName, DataParameters& p);
    void triggerLuaCallback(const std::string& sEventName);

    // On peut parenter un event

    void attachEventTo(Event* pEventToAttach, Event* pChildEvent);

    Event* getEvent(const std::string& sEventName);

    void setParametersToEvent(std::string sEventName, DataParameters pParam);

    void getEventsNames(std::vector<std::string>& paramNames);
    
    // On peut invoquer des events spécicifiques à travers une pile d'appel
    void pushEvent(const std::string& sName);
    void triggerPushedEvents();
    void clearPushedEvent();

private:
    // La liste des events
    std::map<std::string, Event*> m_events;
    std::map<std::string, Event*> m_pushedEvents;

};

Voici le fichier EventManager.cpp

EventManager::EventManager()
{
}

EventManager::~EventManager()
{
}

void EventManager::registerEvent(Event* const pEvent)
{
    SKIP_IF_FAILED_AssertMsg(pEvent != NULL, "pEvent NULL")
    {
        std::string sName = pEvent->getName();
        
        SKIP_IF_FAILED_AssertMsg(m_events.count(sName) == 0, "Event déjà enregistré")
        {
            m_events[sName] = pEvent;
        }
    }
}

// On déclenche sans paramètre
void EventManager::triggerEvent(const std::string& sEventName)
{
    if (m_events.count(sEventName) > 0)
    {
        Event* pEvent = m_events[sEventName];
        pEvent->trigger();

        triggerLuaCallback(sEventName);
    }
}

// On déclenche avec un paramètre
void EventManager::triggerEvent(const std::string& sEventName, DataParameters& p)
{
    if (m_events.count(sEventName) > 0)
    {
        Event* pEvent = m_events[sEventName];
        pEvent->setParameter(p);

        pEvent->trigger();

        triggerLuaCallback(sEventName, p);
    }
}

void EventManager::triggerLuaCallback(const std::string& sEventName)
{
    // todo
}

void EventManager::triggerLuaCallback(const std::string& sEventName, DataParameters& p)
{
    // todo
}

void EventManager::attachEventTo(Event* pEventToAttach, Event* pChildEvent)
{
    if (pEventToAttach && pChildEvent)
    {
        pEventToAttach->pushChildEvent(pChildEvent);
    }
}

Event* EventManager::getEvent(const std::string& sEventName)
{
    if (m_events.count(sEventName) > 0)
    {
        Event* pEvent = m_events[sEventName];
        return pEvent;
    }
    else
    {
        return NULL;
    }
}

void EventManager::setParametersToEvent(std::string sEventName, DataParameters pParam)
{
    Event* pEvent = getEvent(sEventName);

    SKIP_IF_FAILED_AssertMsg(pEvent == NULL, "pEvent est NULL")
    {
        pEvent->setParameter(pParam);
    }
}

void EventManager::getEventsNames(std::vector<std::string>& paramNames)
{
    std::map<std::string, Event*>::iterator it;

    for (it = m_events.begin(); it != m_events.end(); it++)
    {
        paramNames.push_back((*it).first);
    }
}

void EventManager::pushEvent(const std::string& sName)
{
    Event* pEvent = getEvent(sName);

    AssertNULLPointer(pEvent);

    std::string sEventName = pEvent->getName();
        
    m_pushedEvents[sEventName] = pEvent;
}

void EventManager::clearPushedEvent()
{
    m_pushedEvents.clear();
}

void EventManager::triggerPushedEvents()
{
    std::map<std::string, Event*>::iterator it;

    for (it = m_pushedEvents.begin(); it != m_pushedEvents.end(); it++)
    {
        Event* pEvent = it->second;

        pEvent->trigger();
    }
}

On enregistres les Events comme ci :

 

EVENT_MANAGER->registerEvent( new OnTextPaste() );
EVENT_MANAGER->registerEvent( new OnCameraMovementUpwardRight() );

Résumé :

L’implémentation d’un système d’Event est cruciale dans toute conception de jeu vidéo. Elle permet en effet de modulariser les mécanismes de jeu et rend le code plus élégant et plus facile à comprendre.

Classe BoundingVolumeTrigger pour vos triggers

1

Intro :

Dans un jeu vidéo on doit utiliser beaucoup de bounding boxes pour déclencher certains événements.

Prérequis :

Si vous ne savez pas ce qu’est une Bounding Box : se référer à l’article traitant les Bounding Boxes.
Si vous ne savez pas ce qu’est un Event : se référer à l’article traitant les Events.

Utilisation :

Par exemple lorsque que le joueur entre dans une nouvelle zone, on a besoin de tester deux bounding boxes : celle du joueur et celle du trigger (déclencheur), pour ensuite faire appel à l’événement (Event) associé.

Voici une classe à copier pour implémenter le mécanisme de triggers dans vos jeux.

Dans le fichier .h :


#ifndef BOUNDING_BOX_TRIGGER_H
#define BOUNDING_BOX_TRIGGER_H

class BoundingVolumeTrigger
{
public:
    BoundingVolumeTrigger(const Ogre::AxisAlignedBox& BV);
    virtual ~BoundingVolumeTrigger();

    // AABB / Sphere principale à tester
    void registerMainCheckedBV(const Ogre::AxisAlignedBox& BV);

    // Les AABB / Sphere sur lequels sont testées les intersections de AABB / Sphere
    void registerBoundingBox(const Ogre::AxisAlignedBox& BV);    

    // Les events à invoquer lors d'une collision
    void registerEvent(Event* pEvent);

    bool checkCollision();

    // Met à jour les vérification de déclenchement (triggering)
    void update();

private:
    Ogre::AxisAlignedBox m_testedAABB;

    std::vector<const Ogre::AxisAlignedBox> m_boundingBoxesToTest;

    std::vector<Event*> m_pCollisionEvents;

    bool m_bTriggerDone;
};

#endif

Dans le fichier .cpp :


BoundingVolumeTrigger::BoundingVolumeTrigger(const Ogre::AxisAlignedBox& BV)
{
    m_bTriggerDone = false;
    m_testedAABB = BV;
}

BoundingVolumeTrigger::~BoundingVolumeTrigger()
{
}

void BoundingVolumeTrigger::registerMainCheckedBV(const Ogre::AxisAlignedBox& BV)
{
    m_testedAABB = BV;
}

void BoundingVolumeTrigger::registerBoundingBox(const Ogre::AxisAlignedBox& BV)
{
    m_boundingBoxesToTest.push_back(BV);
}

void BoundingVolumeTrigger::registerBoundingSphere(const Ogre::Sphere& BV)
{
    m_boundingSpheresToTest.push_back(BV);
}

void BoundingVolumeTrigger::registerEvent(Event* pEvent)
{
    m_pCollisionEvents.push_back(pEvent);
}

void BoundingVolumeTrigger::update()
{
    bool bColision = checkCollision();

    // Invoque tous les events associés à la collision
    if (bColision && !m_bTriggerDone)
    {
        for (unsigned int i = 0; i < m_pCollisionEvents.size(); i++)
        {
            Event* pEvent = m_pCollisionEvents[i];
            
            pEvent->trigger();
        }

        m_bTriggerDone = true;
    }
}

bool BoundingVolumeTrigger::checkCollision()
{
    bool collided = false;

    // AABB à tester
    for (unsigned int i = 0; i < m_boundingBoxesToTest.size(); i++)
    {
        if (m_testedAABB.contains(m_boundingBoxesToTest[i]))
        {
            collided = true;

        }
    }

    return collided;
}

La variable m_bTriggerDone restreint l’invocation du déclencheur à un seul appel par rencontre
de collision.

Résumé :