The State pattern in action - the M5StateMachine class

The Mach5 Engine itself also has its own implementation of a State Machine, using inheritance to allow users to not have to rewrite the base functionality over and over again and using function pointers instead of having one function for each state. A function pointer is what it sounds like--a pointer to the address in memory where the function is--and we can call it from that information.

To learn more about function pointers and how they are used, check out http://www.cprogramming.com/tutorial/function-pointers.html.

You can take a look at the base version of one here, starting with the Header file:

#ifndef M5STATEMACNINE_H 
#define M5STATEMACNINE_H

#include "M5Component.h"
#include "M5Vec2.h"

//! Base State for M5StateMachines
class M5State
{
public:
//! Empty virtual destructor
virtual ~M5State(void) {}
//! Called when we first enter a state
virtual void Enter(float dt) = 0;
//! called once per frame
virtual void Update(float dt) = 0;
//! called before we exit a state
virtual void Exit(float dt) = 0;
};

//! Base class for Finite statemanchine component for AstroShot
class M5StateMachine : public M5Component
{
public:
M5StateMachine(M5ComponentTypes type);
virtual ~M5StateMachine(void);
virtual void Update(float dt);
void SetNextState(M5State* pNext);
private:
M5State* m_pCurr; //!< a pointer to our current state to be updated
};

#endif //M5STATEMACNINE_H

In the preceding code, note that we finally broke apart the StateMachine and the State object into their own classes, with the state function having its own Enter, Update, and Exit functions. The State Machine keeps track of the current state that we are in and updates appropriately using the Update and SetNextState functions, and a SetStateState function is used to dictate what state we should start from. The implementation for the class looks a little something like this:

#include "M5StateMachine.h"

M5StateMachine::M5StateMachine(M5ComponentTypes type):
M5Component(type),
m_pCurr(nullptr)
{
}

M5StateMachine::~M5StateMachine(void)
{
}

void M5StateMachine::Update(float dt)
{
m_pCurr->Update(dt);
}

void M5StateMachine::SetNextState(M5State* pNext)
{
if(m_pCurr)
m_pCurr->Exit();

m_pCurr = pNext;
m_pCurr->Enter();
}

This system provides a template that we can expand upon, in order to create more interesting behavior that does something a bit more complex. Take, for example, the RandomGoComponent class, whose header looks like this:

#ifndef RANDOM_LOCATION_COMPONENT_H 
#define RANDOM_LOCATION_COMPONENT_H

#include "CoreM5Component.h"
#include "CoreM5StateMachine.h"
#include "CoreM5Vec2.h"

//Forward declation
class RandomGoComponent;

class RLCFindState : public M5State
{
public:
RLCFindState(RandomGoComponent* parent);
void Enter(float dt);
void Update(float dt);
void Exit(float dt);
private:
RandomGoComponent* m_parent;
};
class RLCRotateState : public M5State
{
public:
RLCRotateState(RandomGoComponent* parent);
void Enter(float dt);
void Update(float dt);
void Exit(float dt);
private:
float m_targetRot;
M5Vec2 m_dir;
RandomGoComponent* m_parent;
};
class RLCGoState : public M5State
{
public:
RLCGoState(RandomGoComponent* parent);
void Enter(float dt);
void Update(float dt);
void Exit(float dt);
private:
RandomGoComponent* m_parent;
};


class RandomGoComponent : public M5StateMachine
{
public:
RandomGoComponent(void);
virtual void FromFile(M5IniFile&);
virtual M5Component* Clone(void);
private:
friend RLCFindState;
friend RLCGoState;
friend RLCRotateState;

float m_speed;
float m_rotateSpeed;
M5Vec2 m_target;
RLCFindState m_findState;
RLCRotateState m_rotateState;
RLCGoState m_goState;
};

#endif // !RANDOM_LOCATION_COMPONENT_H

This class contains three states, Find, Rotate, and Go, which have been added as objects in the RandomGoComponent. Each of the states has their own Enter, Update, and Exit functionality, in addition to the constructor and a reference to their parent. The implementation for the classes looks something like this:

#include "RandomGoStates.h"
#include "RandomGoComponent.h"

#include "CoreM5Random.h"
#include "CoreM5Object.h"
#include "CoreM5Intersect.h"
#include "CoreM5Gfx.h"
#include "CoreM5Math.h"
#include <cmath>

FindState::FindState(RandomGoComponent* parent): m_parent(parent)
{
}
void FindState::Enter()
{
M5Vec2 botLeft;
M5Vec2 topRight;
M5Gfx::GetWorldBotLeft(botLeft);
M5Gfx::GetWorldTopRight(topRight);

M5Vec2 target;
target.x = M5Random::GetFloat(botLeft.x, topRight.x);
target.y = M5Random::GetFloat(botLeft.y, topRight.y);

m_parent->SetTarget(target);

}
void FindState::Update(float)
{
m_parent->SetNextState(m_parent->GetState(RGS_ROTATE_STATE));
}
void FindState::Exit()
{
}

This class will just tell our main State Machine where its intended location is. This only needs to be done once, so it is done in the Enter state. The Update state just states that after this is done, we want to move to the Rotate state, and Exit does nothing. Technically, we could not create it, and that would be fine as well since the base class doesn't do anything as well, but it is here if you wish to expand upon it:

RotateState::RotateState(RandomGoComponent* parent): m_parent(parent)
{
}
void RotateState::Enter()
{
M5Vec2 target = m_parent->GetTarget();

M5Vec2::Sub(m_dir, target, m_parent->GetM5Object()->pos);

m_targetRot = std::atan2f(m_dir.y, m_dir.x);
m_targetRot = M5Math::Wrap(m_targetRot, 0.f, M5Math::TWO_PI);

m_parent->GetM5Object()->rotationVel = m_parent->GetRotationSpeed();
}
void RotateState::Update(float)
{
m_parent->GetM5Object()->rotation = M5Math::Wrap(m_parent->GetM5Object()->rotation, 0.f, M5Math::TWO_PI);

if (M5Math::IsInRange(m_parent->GetM5Object()->rotation, m_targetRot - .1f, m_targetRot + .1f))
m_parent->SetNextState(m_parent->GetState(RGS_GO_STATE));
}
void RotateState::Exit()
{
m_parent->GetM5Object()->rotationVel = 0;

m_dir.Normalize();
M5Vec2::Scale(m_dir, m_dir, m_parent->GetSpeed());

m_parent->GetM5Object()->vel = m_dir;
}

The Rotate state will just rotate the character till it is facing the location that it wants to go to. If it is within the range of the rotation, it will then switch to the Go state. Before leaving though, it will set the velocity of our parent to the appropriate direction in the Exit function:

GoState::GoState(RandomGoComponent* parent): m_parent(parent)
{
}
void GoState::Enter()
{
}
void GoState::Update(float)
{
M5Vec2 target = m_parent->GetTarget();
if (M5Intersect::PointCircle(target, m_parent->GetM5Object()->pos, m_parent->GetM5Object()->scale.x))
m_parent->SetNextState(m_parent->GetState(RGS_FIND_STATE));
}
void GoState::Exit()
{
m_parent->GetM5Object()->vel.Set(0, 0);
}

The Go state merely checks whether the enemy intersects with the target that we are set to go to. If it does, we then set our state to move back to the Find state and start everything over again, and also stop the player from moving in the Exit function:

RandomGoComponent::RandomGoComponent():
M5StateMachine(CT_RandomGoComponent),
m_speed(1),
m_rotateSpeed(1),
m_findState(this),
m_rotateState(this),
m_goState(this)
{
SetNextState(&m_findState);
}
void RandomGoComponent::FromFile(M5IniFile& iniFile)
{
iniFile.SetToSection("RandomGoComponent");
iniFile.GetValue("speed", m_speed);
iniFile.GetValue("rotationSpeed", m_speed);
}
RandomGoComponent* RandomGoComponent::Clone(void) const
{
RandomGoComponent* pNew = new RandomGoComponent;
pNew->m_speed = m_speed;
pNew->m_rotateSpeed = m_rotateSpeed;
return pNew;
}

M5State* RandomGoComponent::GetState(RandomGoStates state)
{
switch (state)
{
case RGS_FIND_STATE:
return &m_findState;
break;
case RGS_ROTATE_STATE:
return &m_rotateState;
break;
case RGS_GO_STATE:
return &m_goState;
break;
}

//In case somethings goes wrong
return &m_findState;
}

As you can see, this works in a very similar way to what we have done before--setting our first state, getting the initial values from the INI file, and then setting things properly when cloned. Finally, we also have a GetState function which will return the current state that the player has using a switch like we talked about previously.

To see this in action, go ahead and go to the Raider.ini file and modify the code to fit the following:

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

[GfxComponent]
texture = enemyBlack3.tga
drawSpace = world

[ColliderComponent]
radius = 5
isResizeable = 0

[RandomGoComponent]
speed = 40
rotationSpeed = 40

If all went well, save the file and then run the project!

Now we will see the enemy continually move into new areas, rotating before going there!

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

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