Note: This tutorial builds on the previous YAP tutorial, and it assumes you have already worked through it. Now that we have a nice GUI, we need to make a true game with a score and a « gameover » message ! For that we will add to the pong, some states. This states will help us to track if the game is currently running and when to switch it off.
So far, the field is only a plane with a texture. But what would be a pong game without goals ? To symbolize the goals, I made a simple mesh of a wall. This wall is not a just a square as it was before so it is a little bit more difficult to surround it with fake physical world to handle the collision. So from the mesh I used to Octopus to export it physically using XODE. I just scaled it before exporting it (it is not possible to scale a IActor) and increased the wall height in case the ball bounce.
void YapMainState::SetupGround(void)
{
//Calculate variables.
real ArenaXsize = (mArena.rightBound -mArena.leftBound);
real ArenaZsize = (mArena.lowerBound -mArena.upperBound);
real FrameXscale = ArenaXsize / 100.;
real FrameZscale = ArenaZsize / 100.;
real GoalXsize = 15. * FrameXscale;
real GoalZsize = 60. * FrameZscale;
real WallXsize = 5 * FrameXscale;
real WallZsize = 5 * FrameZscale;
// create entity
mGroundPlane.pE = mGWorld->createEntity("plane_1x1.mesh");
YAKE_ASSERT( mGroundPlane.pE );
mGroundPlane.pE->setCastsShadow( false );
// create scene node and attach entity to node
mGroundPlane.pSN = mGWorld->createSceneNode("root");
YAKE_ASSERT( mGroundPlane.pSN );
mGroundPlane.pSN->attachEntity( mGroundPlane.pE );
//Set size and position.
mGroundPlane.pSN->setScale( Vector3(ArenaXsize + (GoalXsize + WallXsize) * 2. ,1,ArenaXsize + WallZsize * 2. ));
mGroundPlane.pSN->setPosition (Vector3(0,0,0));
//Set material.
mGroundPlane.pE->setMaterial("Examples/GrassFloor");
graphics::IEntity * pFrame = mGWorld->createEntity("frame.mesh");
YAKE_ASSERT( pFrame );
graphics::ISceneNode * pNodeFrame = mGWorld->createSceneNode("FrameNode");
YAKE_ASSERT( pNodeFrame );
pNodeFrame->attachEntity(pFrame);
//Set size and position.
pNodeFrame->setScale( Vector3(FrameXscale,1./10.,FrameZscale) );
pNodeFrame->setPosition (Vector3(0,0,0));
pFrame->setMaterial("Examples/Rockwall");
//Physics !
mGroundPlane.pA = mPWorld->createActor(ACTOR_STATIC);
YAKE_ASSERT( mGroundPlane.pA );
mGroundPlane.pA->createShape (physics::IShape::PlaneDesc( Vector3(0,1,0), 0 ,"Ground material" ));
mGroundPlane.pA->setPosition (Vector3(0,0,0));
//Create physic actor for frame from file.
yake::data::dom::xml::XmlSerializer ser;
ser.parse( "../../media/graphics.meshes/frame.xode", false );
data::parser::xode::XODEParser * pParser = new data::parser::xode::XODEParserV1();
pParser->subscribeToGeomSignal( Bind1( &YapMainState::processGeom, this ) );
pParser->load (ser.getDocumentNode());
pParser->reset();
ser.reset();
//Create goal .
//2 walls are created in the goals to detect collision.
yake::physics::IActor * actor = mPWorld->createActor(ACTOR_STATIC);
YAKE_ASSERT ( actor );
actor->createShape (physics::IShape::BoxDesc (Vector3(WallXsize,120,GoalZsize), "Wall material"));
actor->setPosition (Vector3 (mArena.leftBound -GoalXsize - WallXsize /2 + 0.1,0,0));
actor->subscribeToCollisionEntered( Bind1(&YapMainState::onCollisionGoal1,this));
actor = mPWorld->createActor(ACTOR_STATIC);
YAKE_ASSERT ( actor );
actor->createShape (physics::IShape::BoxDesc (Vector3(WallXsize,120,GoalZsize), "Wall material"));
actor->setPosition (Vector3 (mArena.rightBound +GoalXsize + WallXsize /2 - 0.1,0,0));
actor->subscribeToCollisionEntered( Bind1(&YapMainState::onCollisionGoal2,this));
}
void YapMainState::processGeom (const data::parser::xode::XODEParser::GeomDesc& desc)
{
yake::physics::IActorPtr actor = mPWorld->createActor(ACTOR_STATIC);
YAKE_ASSERT ( actor );
actor->createShape (*desc.shape_);
actor->getShapes().front()->setMaterial (mPWorld->getMaterial("Wall material"));
actor->setPosition (Vector3(0,0,0));
actor->subscribeToCollisionEntered( Bind1(&YapMainState::onCollisionWall,this));
}
As you can see this code use a XODE parser. We register a callback and every time a new geometry is found in the file, processGeom is called. This function will create the IActor from geometry. Here we only have one geometry in our file so we don't have to check for the name. In case of a complete scene with several mesh it could be needed.
The last trick we will use here is to detect that a goal has been scored. To do so, we just placed two « invisible » walls in each goals. Just in front of the “real” wall. By setting a callback for a collision on each wall, it is possible to detect that a goal is scored.
Ok now, we have everything to get started with the state machine. The first question we have to ask is what will be our states. The states machine is based on template so you declare a new one with whatever you like to represent states. Using a oostatemachine, it is also possible to use objects as states (it's what the raf framework is using by the way). For Yap, we would like to be able to execute some code for every states so let's store some pointers to functions so we can execute a piece of code associated with each step.
The state machine will looks as shown here:
http://lythaniel.free.fr/yake/tutorial6/YapStateMachine.JPG
Too complex for this kind of application but it will do the job.
The statemachine template in yake require that for each kind of states used a get_null_state () function is defined. This way the state machine can initialise itself with a null state. So let's do that and at the same time, declare a type for the states:
class YapMainState;
//declare typedef for statemachine and get_null_state function.
typedef boost::function <void (void)> fState;
typedef fState* fStatePtr;
namespace fsm {
template<>
inline const fStatePtr& get_null_state()
{
static fState* ms_null = 0;
return ms_null;
}
}
We will use boost::function so we can later bind our function pointer to function of the YapMainState class.
Declare it as a member of the YapMainState class as well as the states.
class YapMainState :
public raf::RtMainState
{
/..../
//State machine.
fsm::machine<fState*,String> mStateMachine;
void SetupStateMachine(void);
fState * pfStateQuit;
fState * pfStateStop;
fState * pfStateReset;
fState * pfStateRun;
fState * pfStateScoreP1;
fState * pfStateScoreP2;
fState * pfStatePause;
void StateQuit (void);
void StateStop (void);
void StateReset (void);
void StateRun (void);
void StateScoreP1 (void);
void StateScoreP2 (void);
void StatePause (void);
int mScoreP1;
int mScoreP2;
};
Finally we can initialize the state machine in the SetupStateMachine function (called at the end of onCreateScene).
void YapMainState::SetupStateMachine (void)
{
//States creation.
pfStateQuit = new fState;
pfStateStop = new fState;
pfStateReset = new fState;
pfStateRun = new fState;
pfStateScoreP1 = new fState;
pfStateScoreP2 = new fState;
pfStatePause = new fState;
//States init. Each state point to a function.
*pfStateQuit = boost::bind(&YapMainState::StateQuit,this);
*pfStateStop = boost::bind(&YapMainState::StateStop,this);
*pfStateReset = boost::bind(&YapMainState::StateReset,this);
*pfStateRun = boost::bind(&YapMainState::StateRun,this);
*pfStateScoreP1 = boost::bind(&YapMainState::StateScoreP1,this);
*pfStateScoreP2 = boost::bind(&YapMainState::StateScoreP2,this);
*pfStatePause = boost::bind(&YapMainState::StatePause,this);
//Adds the states to the statemachine.
mStateMachine.addState (pfStateQuit);
mStateMachine.addState (pfStateStop);
mStateMachine.addState (pfStateReset);
mStateMachine.addState (pfStateRun);
mStateMachine.addState (pfStateScoreP1);
mStateMachine.addState (pfStateScoreP2);
mStateMachine.addState (pfStatePause);
//Create transitions.
mStateMachine.addTransition (pfStateStop,"StartGame",pfStateReset);
mStateMachine.addTransition (pfStateReset,"GameReseted",pfStateRun);
mStateMachine.addTransition (pfStateRun,"PauseGame",pfStatePause);
mStateMachine.addTransition (pfStatePause,"ResumeGame",pfStateRun);
mStateMachine.addTransition (pfStatePause,"StartGame",pfStateReset);
mStateMachine.addTransition (pfStateStop,"QuitGame",pfStateQuit);
mStateMachine.addTransition (pfStatePause,"QuitGame",pfStateQuit);
mStateMachine.addTransition (pfStateRun,"GoalP1",pfStateScoreP1);
mStateMachine.addTransition (pfStateRun,"GoalP2",pfStateScoreP2);
mStateMachine.addTransition (pfStateScoreP1,"ScoreUpdated",pfStateRun);
mStateMachine.addTransition (pfStateScoreP2,"ScoreUpdated",pfStateRun);
mStateMachine.addTransition (pfStateScoreP1,"GameOver",pfStateStop);
mStateMachine.addTransition (pfStateScoreP2,"GameOver",pfStateStop);
//Set initial step
mStateMachine.setState (pfStateStop);
}
Several action can generate a transition.
Pushing a GUI button:
bool YapMainState::onStartButton(const CEGUI::EventArgs& e)
{
mStateMachine.processEvent ("StartGame");
DisplayMenu(false);
DisplayHelp(false);
return true;
}
bool YapMainState::onQuitButton(const CEGUI::EventArgs& e)
{
mStateMachine.processEvent("QuitGame");
return true;
}
Hiding/diplaying the GUI:
void YapMainState::DisplayMenu(bool display)
{
//Get the window.
CEGUI::WindowManager& wmgr = CEGUI::WindowManager::getSingleton();
CEGUI::Window* WelcomeWnd = wmgr.getWindow("Welcome");
YAKE_ASSERT (WelcomeWnd);
if (display)
{
//Show the window.
WelcomeWnd->show();
//Enable mouse for GUI.
CEGUI::MouseCursor::getSingleton().show();
getApp().enableMouseInputForCEGUI(true);
CEGUI::WindowManager& wmgr = CEGUI::WindowManager::getSingleton();
CEGUI::Window* WelcomeWnd = wmgr.getWindow("Welcome");
CEGUI::Window* MessageWnd = WelcomeWnd->getChild("Message");
MessageWnd->setText ("Game Paused, press ESC to resume or\n\"Play!\" to start a new game.");
mStateMachine.processEvent("PauseGame");
}
else
{
//Do not allow to switch off the menu if the game is not already running.
if (mStateMachine.current() == pfStateStop) return;
//Hide the window.
WelcomeWnd->hide();
//if the other window is not displayed, disable the mouse for GUI.
if (!mHelpDisplayed)
{
CEGUI::MouseCursor::getSingleton().hide();
getApp().enableMouseInputForCEGUI(false);
mStateMachine.processEvent("ResumeGame");
}
}
mMenuDisplayed = display;
}
void YapMainState::DisplayHelp(bool display)
{
//Get the window.
CEGUI::WindowManager& wmgr = CEGUI::WindowManager::getSingleton();
CEGUI::Window* HelpWnd = wmgr.getWindow("Help");
YAKE_ASSERT (HelpWnd);
if (display)
{
//Show the window.
HelpWnd->show();
//Enable mouse for GUI.
CEGUI::MouseCursor::getSingleton().show();
getApp().enableMouseInputForCEGUI(true);
CEGUI::WindowManager& wmgr = CEGUI::WindowManager::getSingleton();
CEGUI::Window* WelcomeWnd = wmgr.getWindow("Welcome");
CEGUI::Window* MessageWnd = WelcomeWnd->getChild("Message");
MessageWnd->setText ("Game Paused, press ESC to resume or\n\"Play!\" to start a new game.");
mStateMachine.processEvent("PauseGame");
}
else
{
//Hide the window.
HelpWnd->hide();
//if the other window is not displayed, disable the mouse for GUI.
if (!mMenuDisplayed)
{
CEGUI::MouseCursor::getSingleton().hide();
getApp().enableMouseInputForCEGUI(false);
mStateMachine.processEvent("ResumeGame");
}
}
mHelpDisplayed = display;
}
Scoring a goal:
void YapMainState::onCollisionGoal1(yake::physics::ActorCollisionInfo Info)
{
if (Info.pOther == mBall.pA)
{
YAKE_LOG_INFORMATION("GOAAAAALLL !");
//Collision with a goal.
mASource->stop();
mASource->setSoundData (mPingSound1);
mASource->setPosition(Vector3(mBall.x,mBall.mHeight,mBall.y));
mASource->play();
mStateMachine.processEvent("GoalP2");
}
}
void YapMainState::onCollisionGoal2(yake::physics::ActorCollisionInfo Info)
{
if (Info.pOther == mBall.pA)
{
YAKE_LOG_INFORMATION("GOAAAAALLL !");
//Collision with a goal.
mASource->stop();
mASource->setSoundData (mPingSound1);
mASource->setPosition(Vector3(mBall.x,mBall.mHeight,mBall.y));
mASource->play();
mStateMachine.processEvent("GoalP1");
}
}
You will notice that some functions do nothing so you may think they are useless. In fact they are still useful by tracking in which state we are (to update the physic or not for example):
void YapMainState::StateQuit()
{
//Request to quit.
requestQuit();
std::cout << "Quit\n";
}
void YapMainState::StateStop()
{
//Do nothing.
std::cout << "Stop\n";
}
void YapMainState::StateReset()
{
//Reset ball
ResetBall();
//Reset score
mScoreP1 = 0;
mScoreP2 = 0;
//Reset score display.
CEGUI::WindowManager& wmgr = CEGUI::WindowManager::getSingleton();
CEGUI::Window* Score1Wnd = wmgr.getWindow("Score Player 1")->getChild("Score1");
CEGUI::Window* Score2Wnd = wmgr.getWindow("Score Player 2")->getChild("Score2");
Score1Wnd->setText(" 0");
Score2Wnd->setText(" 0");
//transition to next state.
mStateMachine.processEvent("GameReseted");
}
void YapMainState::StateRun()
{
//do nothing.
std::cout << "Run\n";
}
void YapMainState::StateScoreP1()
{
std::cout << "Score P1\n";
//increment score.
mScoreP1++;
//reset ball
ResetBall();
//Display new score.
CEGUI::WindowManager& wmgr = CEGUI::WindowManager::getSingleton();
CEGUI::Window* Score1Wnd = wmgr.getWindow("Score Player 1")->getChild("Score1");
std::string str;
str << " " << mScoreP1;
Score1Wnd->setText(str);
//Check if game is over
if (mScoreP1 >= 10)
{
//transition to stop state.
DisplayMenu(true);
mStateMachine.processEvent("GameOver");
CEGUI::WindowManager& wmgr = CEGUI::WindowManager::getSingleton();
CEGUI::Window* WelcomeWnd = wmgr.getWindow("Welcome");
CEGUI::Window* MessageWnd = WelcomeWnd->getChild("Message");
MessageWnd->setText ("Player 1 win! \nPress \"Play !\" to start a new game.");
}
else
//return to run state.
mStateMachine.processEvent("ScoreUpdated");
}
void YapMainState::StateScoreP2()
{
std::cout << "Score P2\n";
//increment score.
mScoreP2++;
//reset ball
ResetBall();
//Display new score.
CEGUI::WindowManager& wmgr = CEGUI::WindowManager::getSingleton();
CEGUI::Window* Score2Wnd = wmgr.getWindow("Score Player 2")->getChild("Score2");
std::string str;
str << " " << mScoreP2;
Score2Wnd->setText(str);
//Check if game is over
if (mScoreP2 >= 10)
{
//transition to stop state.
DisplayMenu(true);
mStateMachine.processEvent("GameOver");
CEGUI::WindowManager& wmgr = CEGUI::WindowManager::getSingleton();
CEGUI::Window* WelcomeWnd = wmgr.getWindow("Welcome");
CEGUI::Window* MessageWnd = WelcomeWnd->getChild("Message");
MessageWnd->setText ("Player 2 win! \nPress \"Play !\" to start a new game.");
}
else
//return to run state.
mStateMachine.processEvent("ScoreUpdated");
}
void YapMainState::StatePause()
{
// do nothing.
std::cout << "Pause\n";
}
Now that the states function are done, you may want to execute the current one. We had to the onStep function the execution of the current state:
void YapMainState::onStep()
{
real lastTime = native::getTime();
while (!quitRequested())
{
fState * CurState = mStateMachine.current();
(*CurState)();
const real now = native::getTime();
real elapsed = now - lastTime;
if (elapsed < real(0.0001))
elapsed = real(0.0001);
if (getApp().getInputSystem())
{
getApp().getInputSystem()->update();
if (getApp().getKeyboardEventGenerator())
getApp().getKeyboardEventGenerator()->update();
if (getApp().getMouseEventGenerator())
getApp().getMouseEventGenerator()->update();
}
if (mStateMachine.current() == pfStateRun)
{
onFrame(elapsed);
if (mPWorld)
{
mPWorld->step( elapsed );
}
}
if (mGWorld)
{
mGWorld->render( elapsed );
}
lastTime = now;
}
}
You can also see that we modified the function to test if the current state is « Run » before stepping the physics world.
That's clear here that I'm doing a very bad use of the state machine. When using a state machine to handle game step, the code needs to be design with that in mind from the beginning and not added after like here. However, as I didn't want to break everything which was already done, I kept it like this. Anyway I think it's still good enough in this tutorial to show how to use the yake state machine.
Lythaniel