Acting on states

Now that we have our states defined, let's now make it so that we can actually do something in our code based on what state our object is in. For this first example, I'm going to update the ChasePlayerComponent class that already exists in the EngineTest project.

From the Solution Explorer tab on the right-hand side, open up the SpaceShooter/Components/ChasePlayerComp folder and access the ChasePlayerComponent.h file. From there, replace the class with the following changes in bold:

enum State{
Idle,
Follow,
Death
};

//!< Simple AI to Chase the Player
class ChasePlayerComponent : public M5Component
{
public:
ChasePlayerComponent(void);
virtual void Update(float dt);
virtual void FromFile(M5IniFile& iniFile);
virtual ChasePlayerComponent* Clone(void);
private:
float m_speed;
float m_followDistance;
float m_loseDistance;

void FollowPlayer();
float GetDistanceFromPlayer();
State m_currentState;

};

The FollowPlayer and GetDistanceFromPlayer functions are going to be helper functions for our functionality. We've added our state enum to store each of the possible states we can be in, and we added the m_currentState variable to hold the current state we are in. To determine when we should switch states, we have two other values, m_followDistance and m_loseDistance, which are the distance in pixels that our player needs to be from the enemy to follow them, and then how far the player needs to get away to escape, respectively.

Now that we have that finished, let's first go ahead and add in the helper functions at the bottom of the ChasePlayerComponent.cpp file so that we can have the proper functionality, once we update our other functions:

/*************************************************************************/ 
/*!
Makes it so the enemy will move in the direction of the player
*/
/*************************************************************************/
void ChasePlayerComponent::FollowPlayer()
{
std::vector<M5Object*> players;
M5ObjectManager::GetAllObjectsByType(AT_Player, players);
M5Vec2 dir;
M5Vec2::Sub(dir, players[0]->pos, m_pObj->pos);
m_pObj->rotation = std::atan2f(dir.y, dir.x);
dir.Normalize();
dir *= m_speed;
m_pObj->vel = dir;
}

/*************************************************************************/
/*!
Returns the distance of the object this is attached to the player
*/
/*************************************************************************/
float ChasePlayerComponent::GetDistanceFromPlayer()
{
std::vector<M5Object*> players;
M5ObjectManager::GetAllObjectsByType(AT_Player, players);

return M5Vec2::Distance(m_pObj->pos, players[0]->pos);
}

These functions use some basic linear algebra in order to move our object toward the player and to get the distance between two positions.

Diving into the mathematics behind it is out of the scope of this book, but if you're interested in learning more, I highly suggest you check out the following link. The code is written for Cocos2D so it will not be exactly the same as what Mach5 would use, but the concepts are explained very well: https://www.raywenderlich.com/35866/trigonometry-for-game-programming-part-1.

Now that we have that functionality in, we need to update a couple of things. First of all, we will use the constructor to set the initial value of our currentState variable:

/*************************************************************************/ 
/*!
Sets component type and starting values for player
*/
/*************************************************************************/
ChasePlayerComponent::ChasePlayerComponent(void):
M5Component(CT_ChasePlayerComponent),
m_speed(1)
{
m_currentState = Idle;
}

Next, we need to tell our object to read in the values of our object through its INI file:

void ChasePlayerComponent::FromFile(M5IniFile& iniFile) 
{
iniFile.SetToSection("ChasePlayerComponent");
iniFile.GetValue("speed", m_speed);
iniFile.GetValue("followDistance", m_followDistance);
iniFile.GetValue("loseDistance", m_loseDistance);

}

FromFile is only called once on the first object that gets created in initialization. In order to make it easy to tweak values without having to recompile the project, Mach 5 reads in information from a file to set variables. We haven't modified the .ini file yet, but we will once we finish all of these modifications:

M5Component* ChasePlayerComponent::Clone(void) 
{
ChasePlayerComponent* pNew = new ChasePlayerComponent;
pNew->m_speed = m_speed;
pNew->m_followDistance = m_followDistance;
pNew->m_loseDistance = m_loseDistance;
return pNew;
}

We then need to go to Windows Explorer and move to the project's EngineTest/EngineTest/ArcheTypes folder, and then access the Raider.ini file and add the new properties to the object:

posX   = 0 
posY = 0
velX = 0
velY = 0
scaleX = 10
scaleY = 10
rot = 0
rotVel = 0
components = GfxComponent ColliderComponent ChasePlayerComponent

[GfxComponent]
texture = enemyBlack3.tga
drawSpace = world

[ColliderComponent]
radius = 5
isResizeable = 0

[ChasePlayerComponent]
speed = 40
followDistance = 50
loseDistance = 75

If a text editor doesn't open for you, feel free to use Notepad. In this case, we are adding in two new properties which represent the values we created earlier.

Then, we need to update our stage so it's a little easier for us to do some testing. Back in Windows Explorer, open up the EngineTest/EngineTest/Stages folder and then open up the Level01.ini file and set it to the following:

ArcheTypes = Player Raider 

[Player]
count = 1
pos = 0 0

[Raider]
count = 1
pos = 100 10

With this, our level will just have our player in the center of the world and an enemy Raider positioned at (100, 10). With all of that accomplished, save the files and dive back into our ChasePlayerComponent.cpp file and replace the Update function with the following:

void ChasePlayerComponent::Update(float) 
{
// Depending on what state we are in, do different things
switch (m_currentState)
{
case Idle:
// No longer move if we were
m_pObj->vel = M5Vec2(0, 0);

// If the player gets too close, the enemy notices them
if (GetDistanceFromPlayer() < m_followDistance)
{
// And will begin to give chase
m_currentState = Follow;
}

return;
case Follow:
// Follow the player
FollowPlayer();

// If the player manages to get away from the enemy
if (GetDistanceFromPlayer() > m_loseDistance)
{
// Stop in your tracks
m_currentState = Idle;
}
break;
case Death:
// Set object for deletion
m_pObj->isDead = true;
break;
}

}

Save everything and go ahead and run the project. If all goes well, you should see a scene like this:

Notice that our enemy is not moving at the beginning due to it being in the Idle state. However, if we move closer to it, it would look something like this:

You'll see that it now follows us without stopping. If we manage to move far enough away from the enemy though, they'll stop:

This clearly shows the basic principles of the State pattern in use, though there are a number of things we can do to improve this, which we will talk about soon.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.219.249.210