My Component Base Entity System, Part 4

Published December 24, 2011
Advertisement
As the last part of describing my Component Based Entity System (CBES), I'll show how we tie all the components to entities, and the Entities into a game.

Again, you can get the code here: SmashPCComp-12-23-11 and the Component System library: ComponentSystem-12-23-11


I won't go over the specifics (you can find them at my old blog), but I have some xml files that I define the different bullets, items, levels, etc. I use these as my basis for defining the entities. In reality, I should use xml that are tied very closely to the components, and how they are defined, but, being lazy, I'm using my old defines, and just shaping the components from that.

In my main function, before the Game Loop, I am doing this:


int main(int argc, char *argv[])
{
U32 u32ScreenX, u32ScreenY;
BOOL bFullScreen;
U32 u32Lives = 3;
int Level = 1;
cpSpace *pSpace;

GameSound::Init((char *)"SmashPcSounds.xml");
Utilities::LoadConfig((char *)"SmashPcCfg.xml");

Utilities::ScreenResolutionGet(u32ScreenX, u32ScreenY, bFullScreen);

sf::RenderWindow App(sf::VideoMode(u32ScreenX, u32ScreenY, 32), "SmashPC", (bFullScreen ? sf::Style::Fullscreen : sf::Style::Default));

cpInitChipmunk();

pSpace = cpSpaceNew();
cpSpaceInit(pSpace);
pSpace->iterations = 10;

// Init the SmashPcData
SmashPcData GameData;

GameData.LoadLevel(Levelnames[Level-1]);

SmashPcEntitySystem EntitySystem(&App, &GameData, pSpace);

cpVect Location = GameData.GetSpawnLocation();

EntitySystem.AddPlayer(Location);

srand(time(NULL));

App.ShowMouseCursor(true);
App.SetCursorPosition(u32ScreenX/2, u32ScreenX/2);

App.EnableVerticalSync(true);
App.SetFramerateLimit(60);


Basically, just setting up my SFML window, and when I instantiate the SmashPCData variable GameData, it loads all the xml files and holds the properties. We load the 1st level, then pass that GameData variable (as well as the SFML RenderWindow and the chipmunk physics 2d space) to the SmashPcEntitySystem (which I'l get to later).

We then get a spawn location from the GameData, and call AddPlayer from the EntitySystem.

The main game loop looks like this:


// main game loop
while (App.IsOpened())
{
if (EntitySystem.IsPlayerDead())
{
// Check for Lives?
if (u32Lives > 0)
{
Sleep(1000);
u32Lives--;
Location = GameData.GetSpawnLocation();
EntitySystem.AddPlayer(Location);
}
else
{
// just quit I guess
GameSound::Play("GameOver");
Sleep(4000);
break;
}
}

if (GameData.IsLevelOver())
{
BOOL bGameOver = FALSE;

// Change Level
if (Level == NumLevels)
{
bGameOver = TRUE;
}
else
{
Level++;
}

// Call the intermission function
// also handles end-game
Intermission(&App, &GameData, bGameOver,
u32ScreenX, u32ScreenY);

// Reset GameData to make sure everything is cleared
GameData.Reset();
GameData.LoadLevel(Levelnames[Level-1]);
EntitySystem.Reset(true);

// Start player at new spawn locations
Location = GameData.GetSpawnLocation();

EntitySystem.AddPlayer(Location);
}

cpSpaceStep(pSpace, 1.0f/60.0f);

/* clear screen and draw map */
App.Clear(sf::Color(200, 200, 200, 255));

EntitySystem.FrameTick();

App.Display();

if (CheckGameEnd(&App))
{
App.Close();
break;
}
}

I won't go over everything, but point out what we do with the Entity System. We check if a player's dead, and if so, we reload a new player at a spawn spot, unless there's no lives left.

We check if the level is over, and if so, we reset the Entity System, load the new level, find a spawn for the player, and put the player back in.

Finally, we step the 2d physics space, Clear the screen, Notify the EntitySystem of a frame tick, which kicks off all the events. When done we draw, and loop back.

Here's SmashPcEntitySystem.h


/******************************************************************************
*
* class SmashPcEntitySystem - Handles the entities and Components for SmashPC
*
******************************************************************************/
class SmashPcEntitySystem
{
public:
/******************************************************************************
*
* SmashPcEntitySystem() - Loads all the data for the SmashPC Game: Items, Bullets,
* Enemies, etc.
*
******************************************************************************/
SmashPcEntitySystem(sf::RenderWindow *pApp,
SmashPcData *pGameData, cpSpace *pSpace);

/******************************************************************************
*
* ~SmashPcEntitySystem() - Deletes the data
*
******************************************************************************/
~SmashPcEntitySystem();

void AddPlayer(cpVect &Location);
void Reset(bool bSavePlayer);

void FrameTick();

static void OnEventData(TEvent &Event, void *pThis);
static void OnDeadEvent(TEvent &Event, void *pThis);
static void OnAddEvent(TEvent &Event, void *pThis);

void EventData(TEvent &Event);
void DeadEvent(TEvent &Event);
void AddEvent(TEvent &Event);

bool IsPlayerDead() { return mbPlayerDead; }

private:
void AddItem(std::string Item, cpVect Location);
void AddEnemy(cpVect &Location, U32 u32Level);
void LoadLevelData();

TEvent mTickEvent;

sf::RenderWindow *mpApp;
SmashPcData *mpGameData;
cpSpace *mpSpace;
TEntityManager *mpEntMgr;

bool mbPlayerDead;

// save off the player's health, armor and weaponry between level loads
TComponent *mpWeapComp;
TComponent *mpHealthComp;
TComponent *mpArmorComp;

};

The main functions are the Event callbacks: These are notified from the different entities. The main events the EntitySystem is interested in is DEATH_EVENT's, so we know when a player or an enemy dies, EVENT_DATA_EVENT's, so we can give components the SFMl RenderWindow (for drawing), and ADD_ENTITY_EVENT's, so we can add an Enemy to the system when an EnemySpawn item generates one.

The Constructor does some initial setup.
1st we want to make sure we're registered for the events we want, which happens in the Reset() function:


SmashPcEntitySystem::SmashPcEntitySystem(sf::RenderWindow *pApp,
SmashPcData *pGameData,
cpSpace *pSpace) :
mTickEvent(FRAME_UPDATE_EVENT, (TEntity *)NULL)
{
mpApp = pApp;
mpGameData = pGameData;
mpEntMgr = TEntityManager::GetInstance();
mbPlayerDead = false;
mpWeapComp = NULL;
mpHealthComp = NULL;
mpArmorComp = NULL;
mpSpace = pSpace;

Reset(false);

}
void SmashPcEntitySystem::Reset(bool bSavePlayer)
{

printf("SmashPcEntitySystem::Reset save %d\n", bSavePlayer);

if (bSavePlayer){
TEntity *pPlayer = mpEntMgr->GetEntity("Player");
mpWeapComp = pPlayer->GetComponent(WEAPONRY_COMPONENT);
pPlayer->RemoveComponent(mpWeapComp);

mpHealthComp = pPlayer->GetComponent(HEALTH_COMPONENT);
pPlayer->RemoveComponent(mpHealthComp);

mpArmorComp = pPlayer->GetComponent(ARMOR_COMPONENT);
pPlayer->RemoveComponent(mpArmorComp);
}
mpEntMgr->RemoveAllEntities();

// We want to get the message for dead events
mpEntMgr->RegisterForEvent(SmashPcEntitySystem::OnDeadEvent,
(void *)this,
DEATH_EVENT);

// We want to get the message for our location from entity manager
mpEntMgr->RegisterForEvent(SmashPcEntitySystem::OnEventData,
(void *)this,
EVENT_DATA_EVENT);

// We want to know when we shold add an entity
mpEntMgr->RegisterForEvent(SmashPcEntitySystem::OnAddEvent,
(void *)this,
ADD_ENTITY_EVENT);

LoadLevelData();

}

You can see we 1st check if we need to save the player specific into; this is the current health, armor, and weapons. This is used for changing levels, which we basically wipe clean all Entities, and start over.

We then register for the events we want to be notified (as mentioned earlier).

The 2nd thing we want to do is load the level data; this is mainly the walls, but there are also items in the level, specifically, EnemySpawn items (but others could be there too):


void SmashPcEntitySystem::LoadLevelData()
{
GameLevel *pLevel;

pLevel = mpGameData->GetLevel();

// Load walls then items
std::vector::iterator it;
for (it = pLevel->mWalls.begin();
it != pLevel->mWalls.end(); it++)
{
TEntity *pWallEnt = new TEntity("Wall", TEntity::UPDATE_EARLY);
Component::TPhysicalObject::TAttributes Attribs;

Attribs.Angle = 0.0f;
Attribs.MaxSpeed = 0.0f;
Attribs.InitialForce = 0.0f;
Attribs.CollisionType = WALL_COL_TYPE ;
Attribs.CollisionLayer = 0xFFFFFFFF;
Attribs.bStatic = true;

if (it->bIsRect)
{
Attribs.eShape = Component::TPhysicalObject::RECTANGLE_SHAPE;
Attribs.RectangleInfo.StartingPoint = it->StartingPoint;
Attribs.RectangleInfo.EndingPoint = it->EndingPoint;

}
else
{
Attribs.eShape = Component::TPhysicalObject::LINE_SHAPE;
Attribs.LineInfo.StartingPoint = it->StartingPoint;
Attribs.LineInfo.EndingPoint = it->EndingPoint;
Attribs.LineInfo.Width = 10.0f;
}

Component::TPhysicalObject *pWallComp = new Component::TPhysicalObject(
mpSpace, &Attribs);

pWallEnt->AddComponent(pWallComp);

sf::Color WallColor(128, 128, 128, 255);
Component::TGraphicsShape *pGfxShape = new Component::TGraphicsShape(
WallColor);

pWallEnt->AddComponent(pGfxShape);
TEntityManager::GetInstance()->AddEntity(pWallEnt, true);

// tell component to get callback for hittign a bullet
pWallComp->SetCollision("Bullet", true);
// Walls occupy all layers
}

std::vector::iterator it2;

for (it2 = pLevel->mItems.begin(); it2 != pLevel->mItems.end(); it2++) {
AddItem(it2->ItemName, it2->Location);
}
}

We begin by looping through the list of walls, and for each wall, we create an entity. The wall entity only has a PhysicalObject and a GraphicsShape component. We generate those 2 components, add them to the entity, and then add the Entity to the EntityManager.

Finally, we tell the Wall's PhysicalObject component it wants to get notified when an Entity of type "Bullet" collides with it, and to remove the other entity upon collision (in this case, the Bullet). No component in the Wall's Entity wants to know about the collision, so it will basically just delete the bullet entity.


We then loop through all the items in the level, and add them to the System.

After loading the level data, we'll get notified to Add a Player to the system:


void SmashPcEntitySystem::AddPlayer(cpVect &Location)
{
printf("SmashPcEntitySystem::AddPlayer\n");

GameSound::Play("PlayerSpawn");

if (mbPlayerDead) {
printf("Delete old player 1st\n");

TEntity *pDeadPlayer = mpEntMgr->GetEntity("Player");

// store off weaponry
mpWeapComp = pDeadPlayer->GetComponent(WEAPONRY_COMPONENT);
pDeadPlayer->RemoveComponent(mpWeapComp);
mpEntMgr->DeleteEntity(pDeadPlayer, true);
}

mbPlayerDead = false;

TEntity *pPlayerEntity = new TEntity("Player", TEntity::UPDATE_LAST);

if (mpHealthComp) {
printf("Add in old health/armor component\n");
pPlayerEntity->AddComponent(mpHealthComp);
pPlayerEntity->AddComponent(mpArmorComp);
mpHealthComp = NULL;
mpArmorComp = NULL;
}
else {
pPlayerEntity->AddComponent(new Component::TArmor(100));
pPlayerEntity->AddComponent(new Component::THealth(100));
}
pPlayerEntity->AddComponent(new Component::TTimer());
pPlayerEntity->AddComponent(new Component::TPlayer(PLAYER_SPEED));
pPlayerEntity->AddComponent(new Component::TCamera());
pPlayerEntity->AddComponent(new Component::TInput());
pPlayerEntity->AddComponent(new Component::TSound());

Component::TGraphicsObject *pGraphicsObj = new Component::TGraphicsObject(
"Gfx/Player.bmp", PI/2.0f);
pPlayerEntity->AddComponent(pGraphicsObj);

uint32_t Width, Height;
pGraphicsObj->GetDimensions(Width, Height);

Component::TPhysicalObject::TAttributes Attribs;
Attribs.eShape = Component::TPhysicalObject::CIRCLE_SHAPE;
Attribs.bStatic = false;
Attribs.CircleInfo.Radius = (Width + Height)/4;
Attribs.CircleInfo.Location = Location;
Attribs.Angle = PI/2.0f;
Attribs.MaxSpeed = PLAYER_SPEED;
Attribs.InitialForce = 0.0f;
Attribs.CollisionType = PLAYER_COL_TYPE;
Attribs.CollisionLayer = PLAYER_LAYER;

Component::TPhysicalObject *pPhysicalObject =
new Component::TPhysicalObject(mpSpace, &Attribs);

// we want to collide with bullets and items
pPhysicalObject->SetCollision("Bullet", true);
pPhysicalObject->SetCollision("Armor", true);
pPhysicalObject->SetCollision("Weapon", true);

pPlayerEntity->AddComponent(pPhysicalObject);

if (mpWeapComp) {
printf("Add in old weapon component\n");
pPlayerEntity->AddComponent(mpWeapComp);
mpWeapComp = NULL;
}
else {
SmashPcData::tBulletList BulletList;
mpGameData->GetBulletList(BulletList);
Component::TWeaponry::TWeaponInfo WeaponInfo;
Component::TWeaponry::TWeapons Weapons;

for (SmashPcData::tBulletList::iterator it = BulletList.begin();
it != BulletList.end(); it++) {
WeaponInfo.Name = it->Name;
if (it == BulletList.begin()) {
WeaponInfo.Available = true;
}
else {
WeaponInfo.Available = false;
}

WeaponInfo.RefireRate = it->u32Refire;
WeaponInfo.Speed = it->Speed;
WeaponInfo.Damage = it->u32Damage;
WeaponInfo.TimeToLive = it->u32TimeToLive;
WeaponInfo.CollisionType = BULLET_COL_TYPE;
WeaponInfo.CollisionLayer = BULLET_LAYER;
WeaponInfo.ImageName = it->ImageName;
WeaponInfo.bContSound = it->bContSound;
Weapons.push_back(WeaponInfo);
}

Component::TWeaponry *pWeaponry = new Component::TWeaponry(
Weapons);
pPlayerEntity->AddComponent(pWeaponry);
}

mpEntMgr->AddEntity(pPlayerEntity, true);

printf("SmashPcEntitySystem::AddPlayer Exiting\n");
}

A Player Entity is made up of these Components (information about them listed in Part 3):
Health, Armor, Timer, Player Logic, Camera, Input, Sound, Physical Object, Graphics Object, and Weaponry Component.

First, we check if we're adding a player after he's died, and if so, we store off his old Weaponry Component (which stores what weapons he has), and delete the old dead player entity.

Then, we check if we have a saved Health Component; if so, that means we've changed levels, and we want to load the player with his old Health and Armor; otherwise, we gets default 100 Health, 0 armor.

Next we add all the simple components that don't need much external data.

When we load the Physical Object component, we have to tell it what general shape it is (rectangle, line, or circle) the dimensions of the object, location, angle, maximum speed, and the collision information. There are other optional attributes you can load into a PhysicalObject, but we don't need them for a player.

We tell the Player's Physical Object component that he needs to be notified of colliding with Bullets, Armor, and Weapons. This will generate collision events when the physical object his entities of those types, and different component will be register for those collisions (Armor component wants to know when it's entity acquires armor, Health wants to know when it's entity hits a bullet, and Weaponry wants to know when he hits a weapon).

Finally, we check if we have a weaponry component saved; if so we use it, otherwise we load the default weaponry component, with all the weapons in the game, but only make the 1st one available (the PeaShooter).

Then, we add the entity to the entity manager.

I wanted to point out what we do when FameTick is called:


void SmashPcEntitySystem::FrameTick()
{
mpEntMgr->SetEvent(mTickEvent, true);
}

All we do is tell the EntityManager that a frame tick occurred. The Entity Manager will tell all the Entities of the Frame Tick, and the Components who are registered for the FRAME_UPDATE_EVENT will get notified.

So, when the user pressed a movement direction, the Input Component, when it gets a FRAME_UPDATE_EVENT, will see a movement key is pressed, and will SetEVent() with INPUT_EVENT. The Player Component gets the INPUT_EVENT, see the user presses left, and sets it's entity's Physical Object's Force, so it will start moving. The Graphics Object will draw where the entity is, based on the Physical Object's world location, and the location of the Camera in the world.

What happens now is the EnemySpawn items (loaded from the level data) timer will expire, notifying them to release an enemy. That will generate an ADD_ENTITY_EVENT with the filter "Enemy" (see function TEnemySpawn::Timer(TEvent &Event)). The EntitySystem will get a callback (since it registered for the ADD_ENTITY_EVENT) to add an Enemy. So, it will call AddEnemy(), which basically does the same thing AddPlayer did, except the Enemy uses these components:
Health, Timer, Sound, Enemy Logic, Graphics Object, Physical Object, and Weaponry
Enemy differs from Player by swapping Enemy Logic for Player Logic, and it doesn't use Input, nor Camera components.

An Enemy will set a timer to move towards the player. When it expires, the Enemy Component will ask where the Player's Location is. It will then tell it's Physical Object component to change it's angle to be towards the player, and give it a force towards the player. Also, the Enemy has a separate timer for firing bullets, and, when it expires, the enemy will ask where the player is and fire a bullet in that direction (via the Weaponry component).

When a Bullet hits an Enemy, the Enemy's Health Component will get notified of a COLLISION_EVENT (because the Enemy's Physical Object component was told it needs to generate a collision event when it hits a bullet). The Enemy's Health Component will subtract the damage of the bullet (retrieved by getting the Value from the Bullet Entity's Value Component) from it's health. If it's 0 or less, it generates a DEATH_EVENT. The SmashPC Entity System will get a callback telling it the enemy has died, and it will remove the entity form the EntityManager.


And, that's, basically, how it all works. You can look over the code for more detailed information, but that's the basis for my CBES. I'll try and use it for my next project and see how much it helps (or hinders). I'm fairly happy with it, but, I know there are some efficiency issues I could improve. I won't truly know how much it helps until I try and make a different game.

I also realize I'm missing many details. For example, I can't have a glowing image around bullets, I don't have the smoke trails, nor does my flame thrower fade out before the fire expires. I'll explore those aspects for the next game.
1 likes 1 comments

Comments

Programming020195BRook
Wow... You're going all out here! Nice work!
January 01, 2012 11:44 AM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Advertisement