The first version of this tutorial has been made by Regress, I have just rewritten the original code to make it more understandable. I'm not a Yake or C++ specialist so don't hesitate to correct me if something is wrong. In fact I written this tutorial as an exercise to discover Yake, then I thought that it could be a good idea to share it.
Before making the RAF application we need some usefull hearders file.
#include "yake\base\yake.h" #include "yake\audio\yakeAudio.h" #include "yake\input\yakeInput.h" #include "yapp\base\yapp.h" #include "yapp\raf\yakeRaf.h" #if YAKE_PLATFORM == PLATFORM_WIN32 #define WIN32_LEAN_AND_MEAN #include "windows.h" #endif
Ok so the first thing we want to do in our RAF application is ⦠the application itself. With RAF it's pretty easy and we will just derivate the ExampleApplication class from the RAF frame work. As it will be a very simple application we will use only one state.
//Very basic application with only one state. class TheApp : public raf::ExampleApplication<TheConfiguration> { public: TheApp(void); protected: virtual raf::MainState* createMainState() { return new YapMainState(*this); } };
ExampleApplication use a template in order to wrap a configuration to the raf base application. This configuration is class which can be used to tell Yake which modules we are using and which plug ins they are based on:
//Configuration struct, storing the module used for our application. struct TheConfiguration : public raf::ApplicationConfiguration { virtual StringVector getLibraries() { return MakeStringVector() << "graphicsOgre" << "inputOgre"; } virtual StringVector getInputSystems() { return MakeStringVector() << "ogre"; } virtual StringVector getGraphicsSystems() { return MakeStringVector() << "ogre3d"; } };
In this tutorial we will be only using Ogre 3d for rendering and Ogreinput for keyboard and mouse input. In next tutorials we will just add the new module we want to use.
Last thing to do is to just write a main function which launch our application:
#if YAKE_PLATFORM == PLATFORM_WIN32 INT WINAPI WinMain( HINSTANCE hInst, HINSTANCE, LPSTR strCmdLine, INT ) #else int main(int argc, char *argv[]) #endif { // Use default executor for convenience. // It's always possible to manually execute TheApp::initialise() etc. return (raf::runApplication( TheApp() )) ? 0 : 1; }
That's all we need for our application !
In this tutorial we are using only one state so everything will be done here. Our main state will be based on the RTMainState in the raf framework. A lot of things, like worlds creation, camera and view port etc , are already handled in the base class so we don't need to worry about them. If you feel curious you can check the base class to see how it is done. All the rest of the interesting stuff will be done here.
This class looks as follow:
//Main (and only for the moment) state of our application. class YapMainState : public raf::RtMainState { public: // YapMainState(raf::Application& owner); <-- this constructor has linking problems YapMainState(raf::Application& owner) : raf::RtMainState(owner) {} // <- use this instead protected: virtual void onCreateScene(); //-> called at startup to build the scene. virtual void onFrame(const real timeElapsed); //->called every time a new frame is drawn. private: //graphical world yake::graphics::IWorld* mGWorld; //Well, a few lights might not be so bad... graphics::ISceneNode* mLightOneNode; graphics::ILight* mLightOne; //Light, from yonder sky, tis the sun! And an associated SceneNode, apparently. graphics::ILight* mSunLight; graphics::ISceneNode* mSunLightNode; //Shadows graphics::StringVector mShadowTechniques; size_t mCurrentShadowTechnique; //utils yake::math::RandomNumberGeneratorMT mRandgen; yake::math::Math mMath; struct Bounds { real leftBound; real rightBound; real lowerBound; real upperBound; }; //Let's define a simple class for easy access to a SceneNode and the associated //Entity. class SimpleObj { public: graphics::ISceneNode* pSN; graphics::IEntity* pE; real x,y,mSize,mHeight; real verticalDirection, horizontalDirection; Bounds bounds; void setPosition (real _x, real _y) { x = _x; y = _y; if (x > bounds.rightBound) x = bounds.rightBound; else if (x < bounds.leftBound) x = bounds.leftBound; if (y > bounds.upperBound) y = bounds.upperBound; else if (y < bounds.lowerBound) y = bounds.lowerBound; if (pSN) pSN->setPosition( Vector3(x,mHeight,y)); } void translate (real tx, real ty) { x += tx; y += ty; if (x > bounds.rightBound) x = bounds.rightBound; else if (x < bounds.leftBound) x = bounds.leftBound; if (y > bounds.upperBound) y = bounds.upperBound; else if (y < bounds.lowerBound) y = bounds.lowerBound; if (pSN) pSN->setPosition( Vector3(x,mHeight,y)); } }; SimpleObj mPaddle1; SimpleObj mPaddle2; SimpleObj mBall; SimpleObj mGroundPlane; Bounds mArena; real mBallSpeed; real mPaddleSpeed; private: //Setup function used to create the scene. void SetupGround(void); void SetupVariables(void); void SetupBall(void); void SetupPaddle1(void); void SetupPaddle2(void); void SetupLights(void); void SetupInput(void); void SetupShadowTechnique(void); //Input listeners void onKey(const yake::input::KeyboardEvent & e); void onMB(uint8 btn); void onMMove(Vector3 mVector); };
There is 2 important functions: onCreateScene and onFrame. The first one is called at startup so we will use it to build our scene and the second one is called every frame before the rendering. We also define a SimpleObj class that we can use to handle all our game objects properties and perform some basic operation (moving or translating).
Let's have a look at the scene creation:
YAKE_LOG_INFORMATION("OnCreateScene ..."); SetupVariables(); SetupInput(); YAKE_LOG_INFORMATION("Creating world"); mGWorld = getGraphicalWorld(); YAKE_ASSERT( mGWorld ); YAKE_LOG_INFORMATION("Creating viewport & camera"); //Camera and viewport creation is done by base class. // position camera getDefaultCamera()->setFixedYawAxis(Vector3::kUnitY); getDefaultCamera()->setPosition(Vector3( 0, 1500, 10)); getDefaultCamera()->lookAt(Vector3(0,0,0)); YAKE_LOG_INFORMATION("Creating ball"); SetupBall(); YAKE_LOG_INFORMATION("Creating ground"); SetupGround(); YAKE_LOG_INFORMATION("Creating Paddle1"); SetupPaddle1(); YAKE_LOG_INFORMATION("Creating Paddle2"); SetupPaddle2(); YAKE_LOG_INFORMATION("Initializing lights"); SetupLights(); YAKE_LOG_INFORMATION("Initializing shadow"); mShadowTechniques = mGWorld->getShadowTechniques (); SetupShadowTechnique(); mGWorld->setShadowsEnabled(true); YAKE_LOG_INFORMATION("Scene creation done.");
We create the graphical world, a view port a camera, some lights and shadows and finally our objects.
For the lights, we will use one as static to illuminate everything and one moving attached on the paddle1 node to create some nice shadows on our playground:
void YapMainState::SetupLights(void) { // fixed light (sun) mSunLight = mGWorld->createLight(); mSunLightNode = mGWorld->createSceneNode(); mSunLightNode->attachLight(mSunLight); mSunLight->setType(yake::graphics::ILight::LT_SPOT); mSunLightNode->setPosition(Vector3(1000,1250,500)); mSunLight->setSpotlightRange(30,50,1); Vector3 dir = -mSunLightNode->getPosition(); mSunLight->setDirection(dir.normalisedCopy()); mSunLight->setDiffuseColour(Color(0.35, 0.35, 0.38)); mSunLight->setSpecularColour(Color(0.9, 0.9, 1)); // movable light 1. // This light will be attached to the paddle 1 node to create some shadows. mLightOneNode = mPaddle1.pSN->createChildNode(); YAKE_ASSERT( mLightOneNode ); mLightOne = mGWorld->createLight(); YAKE_ASSERT( mLightOne ); mLightOneNode->attachLight( mLightOne ); mLightOne->setType( graphics::ILight::LT_POINT ); mLightOne->setDiffuseColour( Color(0.6,0.7,0.8) ); mLightOne->setSpecularColour( Color(1,1,1) ); mLightOne->setAttenuation( 8000, 1, 0.0005, 0 ); mLightOneNode->translate(Vector3( 0, 20, 0 )); }
As a demonstration, we will let the user swap between shadows technique so we retrieve from the graphical world all the shadow technique possible and the scroll through them in the SetupShadowTechnique method:
void YapMainState::SetupShadowTechnique(void) { if (mShadowTechniques.empty()) return; mCurrentShadowTechnique = ++mCurrentShadowTechnique % mShadowTechniques.size(); const String& name = mShadowTechniques[mCurrentShadowTechnique]; graphics::StringMap params; params["tex_size"] = "1024"; params["tex_count"] = "3"; params["far_distance"] = "3000"; params["directional_light_extrusion_distance"] = "3000"; mGWorld->selectShadowTechnique( mShadowTechniques[mCurrentShadowTechnique], params ); if (name == "stencil_additive") { mSunLight->setCastsShadows( true ); mLightOne->setType(yake::graphics::ILight::LT_POINT); mLightOne->setCastsShadows(true); mLightOne->setDiffuseColour( Color(0.9,0.7,0.7) ); mLightOne->setSpecularColour( Color(1,1,1) ); mLightOne->setAttenuation(8000,1,0.0005,0); } else if (name == "stencil_modulative") { mSunLight->setCastsShadows( false ); mLightOne->setType(yake::graphics::ILight::LT_POINT); mLightOne->setCastsShadows( true ); mLightOne->setDiffuseColour( Color(0.9,0.7,0.7) ); mLightOne->setSpecularColour( Color(1,1,1) ); mLightOne->setAttenuation(8000,1,0.0005,0); } else if (name == "texture_modulative") { mSunLight->setCastsShadows( true ); // Change fixed point light to spotlight mLightOne->setType(yake::graphics::ILight::LT_SPOT); mLightOne->setDirection(-Vector3::kUnitZ); mLightOne->setCastsShadows(true); mLightOne->setDiffuseColour( Color(0.9,0.7,0.7) ); mLightOne->setSpecularColour( Color(1,1,1) ); mLightOne->setAttenuation(8000,1,0.0005,0); mLightOne->setSpotlightRange(80,90,1); } std::cout << "SHADOW TECHNIQUE: " << name.c_str() << "\n"; }
Now let's create the ground, scale it and position it:
void YapMainState::SetupGround(void) { // 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(1200,1,1200) ); mGroundPlane.pSN->setPosition (Vector3(0,0,0)); //Set material. mGroundPlane.pE->setMaterial("Examples/GrassFloor"); }
Finally we can create the others object based on the same procedure. The ball will have a random direction at startup.
void YapMainState::SetupBall(void) { mBall.pSN = mGWorld->createSceneNode(); YAKE_ASSERT( mBall.pSN ); mBall.pE = mGWorld->createEntity("Sphere_d1.mesh"); YAKE_ASSERT( mBall.pE ); mBall.pSN->setScale(Vector3(mBall.mSize,mBall.mSize,mBall.mSize)); mBall.pE->setCastsShadow( true ); mBall.pE->setMaterial("Examples/BumpMapping/MultiLightSpecular"); mBall.pSN->attachEntity( mBall.pE ); mBall.setPosition(mBall.x,mBall.y); //Apply a random direction to the ball. real dir = mRandgen () * 2 * mMath.PI; mBall.horizontalDirection = mMath.Cos (dir); mBall.verticalDirection = mMath.Sin (dir); } void YapMainState::SetupPaddle1(void) { // setup Paddle1 mPaddle1.pSN = mGWorld->createSceneNode(); YAKE_ASSERT( mPaddle1.pSN ); mPaddle1.pE = mGWorld->createEntity("cube.mesh"); YAKE_ASSERT( mPaddle1.pE ); mPaddle1.pE->setMaterial("2 - Default"); mPaddle1.pE->setCastsShadow( true ); mPaddle1.pSN->attachEntity( mPaddle1.pE ); mPaddle1.pSN->setScale( Vector3( 30./100., 100./100., mPaddle1.mSize/100.)); //original cube size is 100. mPaddle1.setPosition( mPaddle1.x,mPaddle1.y); } void YapMainState::SetupPaddle2(void) { // setup Paddle2 mPaddle2.pSN = mGWorld->createSceneNode(); YAKE_ASSERT( mPaddle2.pSN ); mPaddle2.pE = mGWorld->createEntity("cube.mesh"); YAKE_ASSERT( mPaddle2.pE ); mPaddle2.pE->setMaterial("2 - Default"); mPaddle2.pE->setCastsShadow( true ); mPaddle2.pSN->attachEntity( mPaddle2.pE ); mPaddle2.pSN->setScale( Vector3( 30./100., 100./100., mPaddle1.mSize/100.)); //original cube size is 100. mPaddle2.setPosition( mPaddle1.x,mPaddle1.y); }
Now we need to initialize all the parameters of our game so we just add a method to do it.
void YapMainState::SetupVariables(void) { //One function to cleanly define all of the variables //CHALLENGE: Load all of the variables out of an XML config file. mBallSpeed = 600; mPaddleSpeed = 500.0; mPaddle1.x = -500; mPaddle1.y = 0; mPaddle1.mSize = 200; mPaddle1.mHeight = 50; mPaddle1.bounds.lowerBound = -600+(mPaddle1.mSize/2); mPaddle1.bounds.upperBound = 600-(mPaddle1.mSize/2); mPaddle1.bounds.leftBound = mPaddle1.x; mPaddle1.bounds.rightBound = mPaddle1.x; mPaddle2.x = 500; mPaddle2.y = 0; mPaddle2.mSize = 200; mPaddle2.mHeight = 50; mPaddle2.bounds.lowerBound = -600+(mPaddle2.mSize/2); mPaddle2.bounds.upperBound = 600-(mPaddle2.mSize/2); mPaddle2.bounds.leftBound = mPaddle2.x; mPaddle2.bounds.rightBound = mPaddle2.x; mBall.x = 0; mBall.y = 0; mBall.mSize = 40; mBall.mHeight = mBall.mSize / 2.; mBall.bounds.lowerBound = -600+(mBall.mSize/2); mBall.bounds.upperBound = 600-(mBall.mSize/2); mBall.bounds.leftBound = -600+(mBall.mSize/2); mBall.bounds.rightBound = 600-(mBall.mSize/2); mArena.leftBound = -600; mArena.rightBound = 600; mArena.upperBound = 600; mArena.lowerBound = -600; }
We will allow the user to change the camera orientation and by pressing the āSā key, to change the shadow technique. Pressing the ESC key will exit the application.
First we need to register our listeners to the keyboard and mouse event generator:
void YapMainState::SetupInput(void) { // setup event input generators and bind them to the correct function. getApp().getKeyboardEventGenerator()->subscribeToKeyDown( Bind1( &YapMainState::onKey, this ) ); getApp().getMouseEventGenerator()->subscribeToMouseButtonDown( Bind1( &YapMainState::onMB, this ) ); getApp().getMouseEventGenerator()->subscribeToMouseMoved( Bind1( &YapMainState::onMMove, this ) ); }
Moving the mouse will change the camera orientation. This is easly done with the following method:
void YapMainState::onMMove(Vector3 mVector) { std::cout << "MouseMove.x: " << static_cast<int>( mVector.x ) << std::endl; std::cout << "MouseMove.y: " << static_cast<int>( mVector.y ) << std::endl; getDefaultCamera()->yaw(-( mVector.x ) * 0.13); getDefaultCamera()->pitch(-( mVector.y ) * 0.13); }
Hitting a mouse button will just log the pressed button to the log window:
void YapMainState::onMB(uint8 btn) { std::cout << "MB: " << static_cast<int>(btn) << std::endl; }
And finally the keyboard listener:
void YapMainState::onKey(const yake::input::KeyboardEvent & e) { std::cout << "Key pressed: " << e.keyCode << "\n"; if (e.keyCode == input::KC_ESCAPE) this->requestQuit(); else if (e.keyCode == input::KC_S) this->SetupShadowTechnique(); }
The paddles movments will be handled synchronously with the rendering in the onFrame method.
Now we want to have everything move so we will get a closer look at the onFrame method:
void YapMainState::onFrame (const real timeElapsed) { real distance; if ( getApp().getKeyboard() ) { //Camera stuff. distance = -200. * timeElapsed; if ( getApp().getKeyboard()->isKeyDown(input::KC_LEFT)) getDefaultCamera()->moveRelative( distance*Vector3::kUnitX ); //Strafe Left (TS_LOCAL) if ( getApp().getKeyboard()->isKeyDown(input::KC_RIGHT)) getDefaultCamera()->moveRelative( -distance*Vector3::kUnitX ); //Strafe Right (TS_LOCAL) if ( getApp().getKeyboard()->isKeyDown(input::KC_UP)) getDefaultCamera()->moveRelative( distance*Vector3::kUnitZ );//Move Forward in TS_LOCAL if ( getApp().getKeyboard()->isKeyDown(input::KC_DOWN)) getDefaultCamera()->moveRelative( -distance*Vector3::kUnitZ );//Move Backwards in TS_LOCAL //Paddle movement. distance = mPaddleSpeed * timeElapsed; if ( getApp().getKeyboard()->isKeyDown(input::KC_A)) //paddle1 down mPaddle1.translate (0,distance); if ( getApp().getKeyboard()->isKeyDown(input::KC_Q)) //paddle 1up mPaddle1.translate (0,-distance); if ( getApp().getKeyboard()->isKeyDown(input::KC_L)) //paddle 2 down mPaddle2.translate (0,distance); if ( getApp().getKeyboard()->isKeyDown(input::KC_P)) //paddle 2 up mPaddle2.translate (0,-distance); } //move ball ! distance = mBallSpeed * timeElapsed; mBall.translate (mBall.horizontalDirection * distance,mBall.verticalDirection * distance); //rotate the ball. yake::math::Matrix3 RotMatrix; yake::math::Quaternion RotQuat; //Calculate rotation matrix depending of the ball speed and direction. RotMatrix.FromEulerAnglesXYZ (mBall.verticalDirection * distance * 2 / mBall.mSize,0,-mBall.horizontalDirection * distance * 2 / mBall.mSize); //Calculate quaternion from this matrix. RotQuat.FromRotationMatrix (RotMatrix); //apply rotation. mBall.pSN->rotate (RotQuat); //Check ball collision with arena. if (((mBall.x + mBall.mSize / 2) >= mArena.rightBound) || ((mBall.x - mBall.mSize / 2) <= mArena.leftBound)) mBall.horizontalDirection = -mBall.horizontalDirection + mRandgen()*0.01; //invert horizontal direction and add some random stuff for fun. if (((mBall.y + mBall.mSize / 2) >= mArena.upperBound) || ((mBall.y - mBall.mSize / 2) <= mArena.lowerBound)) mBall.verticalDirection = -mBall.verticalDirection + mRandgen()*0.01; //invert vertical direction and add some random stuff for fun. //Check ball collision with paddle1. (works ok but not so realistic.) real dx, dy; dx = mMath.Abs (mBall.x-mPaddle1.x); dy = mMath.Abs (mBall.y-mPaddle1.y); //First, check if there is a collision if ((dx <= (mBall.mSize / 2 +15)) && (dy <= (mBall.mSize / 2 + mPaddle1.mSize / 2))) { //calculate collision angle and compare with paddle diagonal. real angle = mMath.ATan2 ((dy),(dx)); real diag = mMath.ATan2 (mPaddle1.mSize/2,15.); std::cout << "ball collision with paddle 1, angle = " << angle << std::endl; if ( (angle >= -diag) && (angle <= diag)) { mBall.horizontalDirection = -mBall.horizontalDirection + (mRandgen()-0.5)*0.05; //invert horizontal direction and add some random stuff for fun. //Move the ball outside the paddle. if ((mBall.x-mPaddle1.x) > 0) mBall.setPosition (mPaddle1.x + mBall.mSize / 2. + 15.,mBall.y); else mBall.setPosition (mPaddle1.x - mBall.mSize / 2. - 15.,mBall.y); } else { mBall.verticalDirection = -mBall.verticalDirection + (mRandgen()-0.5)*0.05; //invert vertical direction and add some random stuff for fun. //Move the ball outside the paddle. if ((mBall.y-mPaddle1.y) > 0) mBall.setPosition (mBall.x,mPaddle1.y + mBall.mSize /2. + mPaddle1.mSize/2.); else mBall.setPosition (mBall.x,mPaddle1.y - mBall.mSize /2. - mPaddle1.mSize/2.); } } //Check ball collision with paddle2. (works ok but not so realistic.) dx = mMath.Abs (mBall.x-mPaddle2.x); dy = mMath.Abs (mBall.y-mPaddle2.y); //First, check if there is a collision if ((dx <= (mBall.mSize / 2 +15)) && (dy <= (mBall.mSize / 2 + mPaddle2.mSize / 2))) { //calculate collision angle and compare with paddle diagonal. real angle = mMath.ATan2 ((dy),(dx)); real diag = mMath.ATan2 (mPaddle2.mSize/2,15.); std::cout << "ball collision with paddle 2, angle = " << angle << std::endl; if ( (angle >= -diag) && (angle <= diag)) { mBall.horizontalDirection = -mBall.horizontalDirection + (mRandgen()-0.5)*0.05; //invert horizontal direction and add some random stuff for fun. //Move the ball outside the paddle. if ((mBall.x-mPaddle2.x) > 0) mBall.setPosition (mPaddle2.x + mBall.mSize / 2. + 15.,mBall.y); else mBall.setPosition (mPaddle2.x - mBall.mSize / 2. - 15.,mBall.y); } else { mBall.verticalDirection = -mBall.verticalDirection + (mRandgen()-0.5)*0.05; //invert vertical direction and add some random stuff for fun. //Move the ball outside the paddle. if ((mBall.y-mPaddle1.y) > 0) mBall.setPosition (mBall.x,mPaddle2.y + mBall.mSize /2. + mPaddle2.mSize/2.); else mBall.setPosition (mBall.x,mPaddle2.y - mBall.mSize /2. - mPaddle2.mSize/2.); } } }
The first things to do is to get the user input and move either the camera or the paddles. The movment is scaled as a function of the time elapsed so the movment speed doesn't depend on the frame rate.
Then the ball is updated for rotation and position.
And finally a check for collision with walls or paddles is done and the ball position and direction is updated accordingly.
This example is very basic and doesn't show really the interest of yake. In fact, it could have been done with Ogre alone nearly as easy. Its purpose however is to serve as support for the next tutorials where the interest of yake will be clearer.
Lythaniel
I would like to say thanks for this tutorial and all the great effort that went into creating it. This will definitely help not only me, but many others getting started with yake. Very much appreciated. ~Justin~
Very usefull tutorial. I have made some presentation changements.
~Flyers~