Jump to content

  • Log In with Google      Sign In   
  • Create Account

We need your feedback on a survey! Each completed response supports our community and gives you a chance to win a $25 Amazon gift card!






My Component Base Entity System, Part 2

Posted by BeerNutts, 23 December 2011 · 1,151 views

My last post was a pretty good bit to swallow. And, I realize I'm kind of getting away from the main purpose of this journal, which is supposed to show how to easily develop 2D games, but I wanted to show what I've been doing lately.

In this post, I'll show you some of the simple Components, and how they work. My next 2 posts will detail the more complicated Components, and putting it all together to make a game.

Again, using the same source code: ComponentSystem-12-23-11

All component's have an Initialize() and a Cleanup(). Initialize is called when an entity is added to an Entity Manager's system, and Cleanup when an entity is deleted.

First, the Value Component:

class TValue : public TComponent
{
public:

    TValue(int32_t Value);
    TValue(std::string StrValue);
    ~TValue();

    /******************************************************************
    *
    *  GetValue - Return the value of the... value
    *
    *******************************************************************/
    int32_t GetValue();

    /******************************************************************
    *
    *  SetValue - Set the value of the... value
    *
    *******************************************************************/
    void SetValue(int32_t u32NewValue);

    /******************************************************************
    *
    *  GetValue - Return the value of the... value
    *
    *******************************************************************/
    std::string GetStrValue();

    /******************************************************************
    *
    *  SetValue - Set the value of the... value
    *
    *******************************************************************/
    void SetStrValue(std::string NewStrValue);

    /******************************************************************
    *
    *  OnGetData - Handle data requests
    *
    *******************************************************************/
    static void OnGetData(TEvent &Event, void *pThis);

    /******************************************************************
    *
    *  OnGetData - Handle data requests
    *
    *******************************************************************/
    void GetData(TEvent &Event);

protected:
    virtual void Cleanup();
    virtual void Initialize();

    int32_t ms32Value;
    std::string mStrValue;

};

Notice we inherit from TComponent. So, we will have access to mpOwnerEntity, which is the Entity that holds this component. The mpOwnerEntity is set when a Component is added to an entity.

The Value component simply holds a value. This could be for a Bullet's Damage, an Armor's protection rating, a Health Box's value, etc. And, actually, I don't need the GetValue (nor GetStrValue(), since we retrieve the value using an Event. Those are left over.

You see the OnGetData() function is static; that's because it's a callback from the Event system. We;ll see how that works in a second.

Here's the important pieces from the .cpp code:

/******************************************************************
*
*  TValue - Constructor, assigns a value to the ... value
*
*******************************************************************/
TValue::TValue(int32_t Value) :
  TComponent(VALUE_COMPONENT)
{
    ms32Value = Value;

}

TValue::TValue(std::string StrValue) :
  TComponent(VALUE_COMPONENT)
{
    mStrValue = StrValue;
}

/******************************************************************
*
*  OnGetData - Handle data requests
*
*******************************************************************/

void TValue::OnGetData(TEvent &Event, void *pThis)
{
    if ((Event.GetFilter() == "int") ||
        (Event.GetFilter() == "string")) {
        TValue *pValue =
            reinterpret_cast<TValue *>(pThis);

        pValue->GetData(Event);
    }
}

/******************************************************************
*
*  OnGetData - Handle data requests
*
*******************************************************************/
void TValue::GetData(TEvent &Event)
{
    void *pData = Event.GetData();
    if (Event.GetFilter() == "int") {
        *((int32_t *)pData) = ms32Value;
    }
    else {
        *((std::string *)pData) = mStrValue;
    }
}


/******************************************************************
*
*  Initialize - Initialize all resources
*
*******************************************************************/
void TValue::Initialize()
{
    // register for Retreiving the data
    mpOwnerEntity->RegisterForEvent(TValue::OnGetData,
                                    (void *)this,
                                    EVENT_DATA_EVENT);
}
The constructor simply assigns the initial value, either a String or an integer.

Next, let's move to the bottom, Initialize(). Here we register for the Component to get notified of DATA_EVENT's. So, any EVENT_DATA_EVENT sent to the component's owner Entity will call TValue::OnGetData(), and it passes in the class as the pThis variable.

In the middle, OnGetData() check's it's an event we're interested in, then cast the pThis void* to the TValue class type (since that's what we register with), and then calls the class's GetData() function, which simply fills in the Event's Data pointer with the value.

I realize it's not the slickest piece of code, I hope it makes sense.

OK, let's take a look at the Armor Component. Armor is owned by any entity that can pick up Armor, and provides protection above the Health. Currently, only the player can wear it. So, the Player entity is given the Armor Component, which starts at 0 value. When the player's entity collides with an Armor item in game, the Armor Component gets notified, and it increases it's value to the value of the armor it collided with. The Health Component, when it collides with a bullet, will check if it's entity has Armor, and, if so, will call Armor's CalculateDamage function.

Here's the header:

class TArmor : public TComponent
{
public:

    TArmor(uint32_t MaxArmor,
       	uint32_t InitialArmor = 0);
    ~TArmor();

    /******************************************************************
    *
    *  OnArmorHit - Static callback, when our entitiy hits an armor
    *
    *******************************************************************/
    static void OnArmorHit(TEvent &Event, void *pThis);

    /******************************************************************
    *
    *  OnArmorHit - Increases armor based on Armor Value
    *
    *******************************************************************/
    void ArmorHit(TEvent &Event);

    /******************************************************************
    *
    *  CalculateDamage - Calculate how much damage the entity takes and
    *    how much damage the armor takes.
    *
    *******************************************************************/
    void CalculateDamage(uint32_t &u32Damage);

protected:
    virtual void Cleanup();
    virtual void Initialize();

    uint32_t mu32MaxArmor;
    uint32_t mu32Armor;

};
And TArmor.cpp:

/******************************************************************
*
*  TArmor - Constructor, assigns maximum armor
*
*******************************************************************/
TArmor::TArmor(uint32_t MaxArmor, uint32_t InitialArmor) :
  TComponent(ARMOR_COMPONENT)
{
    mu32MaxArmor = MaxArmor;
    mu32Armor = InitialArmor;
}

/******************************************************************
*
*  ~TArmor - DeConstructor
*
*******************************************************************/
TArmor::~TArmor() {}

/******************************************************************
*
*  OnArmorHit - Static callback, when our entitiy hits an armor
*
*******************************************************************/
void TArmor::OnArmorHit(TEvent &Event, void *pThis)
{
    if (Event.GetFilter() == "Armor") {
        TArmor *pArmor =
            reinterpret_cast<TArmor *>(pThis);

        pArmor->ArmorHit(Event);
    }
}

/******************************************************************
*
*  OnArmorHit - Increases armor based on Armor Value
*
*******************************************************************/
void TArmor::ArmorHit(TEvent &Event)
{
    uint32_t u32Value;
    printf("OnArmorHit got event %d\n", Event.GetType());

    // Set an event to get the value of the armor
    TEvent ValueEvent(EVENT_DATA_EVENT, mpOwnerEntity, "int");
    ValueEvent.SetData(&u32Value);
    Event.GetEntity()->SetEvent(ValueEvent);

    if (mu32Armor < mu32MaxArmor) {
        mu32Armor += u32Value;
        if (mu32Armor < mu32MaxArmor) {
            mu32Armor = mu32MaxArmor;
        }

        // Notify system this armor entity should be removed
        TEntityManager::GetInstance()->DeleteEntity(Event.GetEntity());
    }

}

/******************************************************************
*
*  CalculateDamage - Calculate how much damage the entity takes and
*    how much damage the armor takes.
*
*******************************************************************/
void TArmor::CalculateDamage(uint32_t &u32Damage)
{
    if (mu32Armor >= u32Damage) {
        mu32Armor -= u32Damage;
        u32Damage = 0;
    }
    else {
        u32Damage -= mu32Armor;
        mu32Armor = 0;
    }
}

/******************************************************************
*
*  Cleanup - Cleanup any used resources
*
*******************************************************************/
void TArmor::Cleanup()
{

}

/******************************************************************
*
*  Initialize - Initialize all resources
*
*******************************************************************/
void TArmor::Initialize()
{
    // register for Bullet Collisions
    mpOwnerEntity->RegisterForEvent(TArmor::OnArmorHit,
                                    (void *)this,
                                    COLLISION_EVENT);
}

You can hopefully follow how we register for a Collision event, and filter only for colliding with an Armor item.

Health Component works by registering for Collision Events, and filters for Bullets or HealthBox.
If it's a Bullet that hits him, then, he calls OnBulletHit(), which figures out how much damage thebullet will do, and subtracts health from the damage (if wearing armor, it will check with that component 1st). If Health goes to 0 or less, then it creates a Death Event, and broadcasts it to the System.
If it hits a HealthBox, then it will increase the Health by the Health box's value.

Here's Header and Source code for THealth:

class THealth : public TComponent
{
public:

    THealth(uint32_t MaxHealth, bool bPlayer = false);
    ~THealth();

    /******************************************************************
    *
    *  OnBulletHit - Static callback, when our entitiy hits a bullet
    *
    *******************************************************************/
    static void OnCollision(TEvent &Event, void *pThis);

    /******************************************************************
    *
    *  OnBulletHit - Decreases health based on bullet damage
    *
    *******************************************************************/
    virtual void OnBulletHit(TEvent &Event);

    /******************************************************************
    *
    *  OnHealthHit - Increases health based on healthbox value
    *
    *******************************************************************/
    void OnHealthHit(TEvent &Event);

protected:
    virtual void Cleanup();
    virtual void Initialize();

    uint32_t mu32MaxHealth;
    uint32_t mu32Health;
    bool mbPlayer;

};

/******************************************************************
*
*  THealth - Constructor, assigns maximum health
*
*******************************************************************/
THealth::THealth(uint32_t MaxHealth, bool bPlayer) :
  TComponent(HEALTH_COMPONENT)
{
    mu32MaxHealth = MaxHealth;
    mu32Health = mu32MaxHealth;
    mbPlayer = bPlayer;

}

/******************************************************************
*
*  ~THealth - DeConstructor
*
*******************************************************************/
THealth::~THealth() {}

/******************************************************************
*
*  OnBulletHit - Static callback, when our entitiy hits a bullet
*
*******************************************************************/
void THealth::OnCollision(TEvent &Event, void *pThis)
{
    if (Event.GetFilter() ==  "Bullet") {
        THealth *pHealth =
            reinterpret_cast<THealth *>(pThis);

        pHealth->OnBulletHit(Event);
    }
    else if (Event.GetFilter() ==  "HealthBox") {
        THealth *pHealth =
            reinterpret_cast<THealth *>(pThis);

        pHealth->OnHealthHit(Event);
    }
}

/******************************************************************
*
*  OnBulletHit - Decreases health based on bullet damage
*
*******************************************************************/
void THealth::OnBulletHit(TEvent &Event)
{
    uint32_t u32Damage;

    // Set an event to get the value of the armor
    TEvent ValueEvent(EVENT_DATA_EVENT, mpOwnerEntity, "int");
    ValueEvent.SetData(&u32Damage);
    Event.GetEntity()->SetEvent(ValueEvent);

    // Check if our Entity has armor
    if (mpOwnerEntity->GetComponent(ARMOR_COMPONENT)) {
        // Let Armor component calculate damage
        TArmor *pArmor = dynamic_cast<TArmor *>(
            mpOwnerEntity->GetComponent(ARMOR_COMPONENT));

        pArmor->CalculateDamage(u32Damage);
    }

    if (u32Damage >= mu32Health) {
        // Notify entity of Death event for next frame
        TEvent DeathEvent(DEATH_EVENT, mpOwnerEntity, mpOwnerEntity->GetType());

        TEntityManager::GetInstance()->SetEvent(DeathEvent, false);
    }
    else {
        mu32Health -= u32Damage;
    }
}

/******************************************************************
*
*  OnHealthHit - Increases health based on healthbox value
*
*******************************************************************/
void THealth::OnHealthHit(TEvent &Event)
{
    if (mbPlayer) {
        printf("PlayerHealth got event %d\n", Event.GetType());

        // Should be from a HealthItem Entiity
        // Get Value component from it

        if (mu32Health < mu32MaxHealth) {
            uint32_t u32Value;

            // Set an event to get the value of the armor
            TEvent ValueEvent(EVENT_DATA_EVENT, mpOwnerEntity, "int");
            ValueEvent.SetData(&u32Value);
            Event.GetEntity()->SetEvent(ValueEvent);
            mu32Health += u32Value;

            if (mu32Health > mu32MaxHealth) {
                mu32Health = mu32MaxHealth;
            }

            // Notify system this armor entity should be removed
            TEntityManager::GetInstance()->DeleteEntity(Event.GetEntity());
        }
    }
}

/******************************************************************
*
*  Cleanup - Cleanup any used resources
*
*******************************************************************/
void THealth::Cleanup()
{

}

/******************************************************************
*
*  Initialize - Initialize all resources
*
*******************************************************************/
void THealth::Initialize()
{
    // register for Collisions
    mpOwnerEntity->RegisterForEvent(THealth::OnCollision,
                                    (void *)this,
                                    COLLISION_EVENT);

}

And, finally, possibly the most useful of the generic Components, TTimer. The Timer Component allows other components to be notified of certain timers. We use it for when the Enemy should fire a bullet and when it should turn, when an Enemy Spawn should spawn the next enemy, when a weapon can fire again, and how long a bullet should live.

It works by calling SetTimer(), giving it a timeout value and an Event to send when the timer expires. Most often, the event is TIMER_EXPIRED_EVENT, but you can pass other events (like REMOVE_ENTITY_EVENT for when an item or bullet expires).

The Timer Component registers for FRAME_UPDATE_EVENT, which is every game tick. This allows it to accurately send it's timers out at the right time. Below is the header and source code.


class TTimer : public TComponent
{
public:

    TTimer();
    ~TTimer();

    /******************************************************************
    *
    *  SetTimer - Set to an event after u32Time mS
    *
    *******************************************************************/
    void SetTimer(uint32_t u32Time, TEvent &Event);

    /******************************************************************
    *
    *  OnFrameTick - Event every frame tick
    *
    *******************************************************************/
    static void OnFrameTick(TEvent &Event, void *pThis);

    /******************************************************************
    *
    *  OnFrameTick - Event every frame tick
    *
    *******************************************************************/
    void FrameTick(TEvent &Event);

protected:
    virtual void Cleanup();
    virtual void Initialize();

    struct TTimerDetails
    {
        uint32_t u32Time;
        TEvent *pEvent;
    };

    std::list<TTimerDetails> mTimerDetails;

};

/******************************************************************
*
*  TTimer - Constructor, simple
*
*******************************************************************/
TTimer::TTimer() :
  TComponent(TIMER_COMPONENT)
{

}

/******************************************************************
*
*  ~TTimer - DeConstructor
*
*******************************************************************/
TTimer::~TTimer()
{
    mTimerDetails.clear();
}


/******************************************************************
*
*  SetTimer - Set to an event after u32Time mS
*
*******************************************************************/
void TTimer::SetTimer(uint32_t u32Time, TEvent &Event)
{
    TTimerDetails TimerDetail;

    // Store when we want the timer to go off
    TimerDetail.u32Time = timeGetTime() + u32Time;
    TimerDetail.pEvent = new TEvent(Event);

    // Place in order in list, for faster retreival
    std::list<TTimerDetails>::iterator it;

    for (it = mTimerDetails.begin();
     	it != mTimerDetails.end(); it++) {
        if (it->u32Time > TimerDetail.u32Time) {
            // insert here
            mTimerDetails.insert(it, TimerDetail);
            break;
        }
    }

    // check it inserted it
    if (it == mTimerDetails.end()) {
        mTimerDetails.insert(it, TimerDetail);
    }
}

/******************************************************************
*
*  OnFrameTick - Event every frame tick
*
*******************************************************************/
void TTimer::OnFrameTick(TEvent &Event, void *pThis)
{
    TTimer *pTimer =
        reinterpret_cast<TTimer *>(pThis);

    pTimer->FrameTick(Event);
}

/******************************************************************
*
*  OnFrameTick - Event every frame tick
*
*******************************************************************/
void TTimer::FrameTick(TEvent &Event)
{
    uint32_t u32CurrentTime = timeGetTime();

    // Check 1st time on list, it is earliest
    if (!mTimerDetails.empty() && (mTimerDetails.front().u32Time <= u32CurrentTime)) {
        // Send this event and remove from list
        //printf("Timer Elapsed! En%s, Ev%d EF:%s\n",
        //   	mpOwnerEntity->GetType().c_str(), mTimerDetails.front().pEvent->GetType(),
        //   	mTimerDetails.front().pEvent->GetFilter().c_str());
        mpOwnerEntity->SetEvent(*(mTimerDetails.front().pEvent));
        delete mTimerDetails.front().pEvent;
        mTimerDetails.pop_front();
    }
}

/******************************************************************
*
*  Cleanup - Cleanup any used resources
*
*******************************************************************/
void TTimer::Cleanup()
{

}

/******************************************************************
*
*  Initialize - Initialize all resources
*
*******************************************************************/
void TTimer::Initialize()
{
    // register for Frame Tick updates
    mpOwnerEntity->RegisterForEvent(TTimer::OnFrameTick,
                                    (void *)this,
                                    FRAME_UPDATE_EVENT);
}

OK, that's the basics of how Components work. Next time we'll look at some of the SmashPC specific Components.

Till then!




December 2014 »

S M T W T F S
 123456
78910111213
14151617181920
21222324252627
28 29 3031   
PARTNERS