We will now move on to creating a simple menu state with visuals and mouse handling. We will use two new screenshots for our buttons, which are available with the source code downloads:
The following screenshot shows the exit feature:
These are essentially sprite sheets with the three states of our button. Let's create a new class for these buttons, which we will call MenuButton
. Go ahead and create MenuButton.h
and MenuButton.cpp
. We will start with the header file:
class MenuButton : public SDLGameObject { public: MenuButton(const LoaderParams* pParams); virtual void draw(); virtual void update(); virtual void clean(); };
By now this should look very familiar and it should feel straightforward to create new types. We will also define our button states as an enumerated type so that our code becomes more readable; put this in the header file under private
:
enum button_state { MOUSE_OUT = 0, MOUSE_OVER = 1, CLICKED = 2 };
Open up the MenuButton.cpp
file and we can start to flesh out our MenuButton
class:
MenuButton::MenuButton(const LoaderParams* pParams) : SDLGameObject(pParams) { m_currentFrame = MOUSE_OUT; // start at frame 0 } void MenuButton::draw() { SDLGameObject::draw(); // use the base class drawing } void MenuButton::update() { Vector2D* pMousePos = TheInputHandler::Instance() ->getMousePosition(); if(pMousePos->getX() < (m_position.getX() + m_width) && pMousePos->getX() > m_position.getX() && pMousePos->getY() < (m_position.getY() + m_height) && pMousePos->getY() > m_position.getY()) { m_currentFrame = MOUSE_OVER; if(TheInputHandler::Instance()->getMouseButtonState(LEFT)) { m_currentFrame = CLICKED; } } else { m_currentFrame = MOUSE_OUT; } } void MenuButton::clean() { SDLGameObject::clean(); }
The only thing really new in this class is the update
function. Next, we will go through each step of this function:
Vector2D
object:Vector2D* pMousePos = TheInputHandler::Instance()->getMousePosition();
if(pMousePos->getX() < (m_position.getX() + m_width) && pMousePos->getX() > m_position.getX() && pMousePos->getY() < (m_position.getY() + m_height) && pMousePos->getY() > m_position.getY())
MOUSE_OVER (1)
:m_currentFrame = MOUSE_OVER;
CLICKED(2)
:if(TheInputHandler::Instance()->getMouseButtonState(LEFT)) { m_currentFrame = CLICKED; }
MOUSE_OUT (0)
:else { m_currentFrame = MOUSE_OUT; }
We can now test out our reusable button class. Open up our previously created MenuState.hand
, which we will implement for real. First, we are going to need a vector of GameObject*
to store our menu items:
std::vector<GameObject*> m_gameObjects;
Inside the MenuState.cpp
file, we can now start handling our menu items:
void MenuState::update() { for(int i = 0; i < m_gameObjects.size(); i++) { m_gameObjects[i]->update(); } } void MenuState::render() { for(int i = 0; i < m_gameObjects.size(); i++) { m_gameObjects[i]->draw(); } }
The onExit
and onEnter
functions can be defined as follows:
bool MenuState::onEnter() { if(!TheTextureManager::Instance()->load("assets/button.png", "playbutton", TheGame::Instance()->getRenderer())) { return false; } if(!TheTextureManager::Instance()->load("assets/exit.png", "exitbutton", TheGame::Instance()->getRenderer())) { return false; } GameObject* button1 = new MenuButton(new LoaderParams(100, 100, 400, 100, "playbutton")); GameObject* button2 = new MenuButton(new LoaderParams(100, 300, 400, 100, "exitbutton")); m_gameObjects.push_back(button1); m_gameObjects.push_back(button2); std::cout << "entering MenuState "; return true; } bool MenuState::onExit() { for(int i = 0; i < m_gameObjects.size(); i++) { m_gameObjects[i]->clean(); } m_gameObjects.clear(); TheTextureManager::Instance() ->clearFromTextureMap("playbutton"); TheTextureManager::Instance() ->clearFromTextureMap("exitbutton"); std::cout << "exiting MenuState "; return true; }
We use TextureManager
to load our new images and then assign these textures to two buttons. The TextureManager
class also has a new function called clearFromTextureMap
, which takes the ID of the texture we want to remove; it is defined as follows:
void TextureManager::clearFromTextureMap(std::string id) { m_textureMap.erase(id); }
This function enables us to clear only the textures from the current state, not the entire texture map. This is essential when we push states and then pop them, as we do not want the popped state to clear the original state's textures.
Everything else is essentially identical to how we handle objects in the Game
class. Run the project and we will have buttons that react to mouse events. The window will look like the following screenshot (go ahead and test it out):
Our buttons react to rollovers and clicks but do not actually do anything yet. What we really want to achieve is the ability to create MenuButton
and pass in the function we want it to call once it is clicked; we can achieve this through the use of function pointers. Function pointers do exactly as they say: they point to a function. We can use classic C style function pointers for the moment, as we are only going to use functions that do not take any parameters and always have a return type of void
(therefore, we do not need to make them generic at this point).
The syntax for a function pointer is like this:
returnType (*functionName)(parameters);
We declare our function pointer as a private member in MenuButton.h
as follows:
void (*m_callback)();
We also add a new member variable to handle clicking better:
bool m_bReleased;
Now we can alter the constructor to allow us to pass in our function:
MenuButton(const LoaderParams* pParams, void (*callback)());
In our MenuButton.cpp
file, we can now alter the constructor and initialize our pointer with the initialization list:
MenuButton::MenuButton(const LoaderParams* pParams, void (*callback)() ) : SDLGameObject(pParams), m_callback(callback)
The update
function can now call this function:
void MenuButton::update() { Vector2D* pMousePos = TheInputHandler::Instance() ->getMousePosition(); if(pMousePos->getX() < (m_position.getX() + m_width) && pMousePos->getX() > m_position.getX() && pMousePos->getY() < (m_position.getY() + m_height) && pMousePos->getY() > m_position.getY()) { if(TheInputHandler::Instance()->getMouseButtonState(LEFT) && m_bReleased) { m_currentFrame = CLICKED; m_callback(); // call our callback function m_bReleased = false; } else if(!TheInputHandler::Instance() ->getMouseButtonState(LEFT)) { m_bReleased = true; m_currentFrame = MOUSE_OVER; } } else { m_currentFrame = MOUSE_OUT; } }
Note that this update
function now uses the m_bReleased
value to ensure we release the mouse button before doing the callback again; this is how we want our clicking to behave.
In our MenuState.h
object, we can declare some functions that we will pass into the constructors of our MenuButton
objects:
private: // call back functions for menu items static void s_menuToPlay(); static void s_exitFromMenu();
We have declared these functions as static; this is because our callback functionality will only support static functions. It is a little more complicated to handle regular member functions as function pointers, so we will avoid this and stick to static functions. We can define these functions in the MenuState.cpp
file:
void MenuState::s_menuToPlay() { std::cout << "Play button clicked "; } void MenuState::s_exitFromMenu() { std::cout << "Exit button clicked "; }
We can pass these functions into the constructors of our buttons:
GameObject* button1 = new MenuButton(new LoaderParams(100, 100, 400, 100, "playbutton"), s_menuToPlay); GameObject* button2 = new MenuButton(new LoaderParams(100, 300, 400, 100, "exitbutton"), s_exitFromMenu);
Test our project and you will see our functions printing to the console. We are now passing in the function we want our button to call once it is clicked; this functionality is great for our buttons. Let's test the exit button with some real functionality:
void MenuState::s_exitFromMenu() { TheGame::Instance()->quit(); }
Now clicking on our exit button will exit the game. The next step is to allow the s_menuToPlay
function to move to PlayState
. We first need to add a getter to the Game.h
file to allow us to access the state machine:
GameStateMachine* getStateMachine(){ return m_pGameStateMachine; }
We can now use this to change states in MenuState
:
void MenuState::s_menuToPlay() { TheGame::Instance()->getStateMachine()->changeState(new PlayState()); }
Go ahead and test; PlayState
does not do anything yet, but our console output should show the movement between states.
We have created MenuState
; next, we need to create PlayState
so that we can visually see the change in our states. For PlayState
we will create a player object that uses our helicopter.png
image and follows the mouse around. We will start with the Player.cpp
file and add the code to make the Player
object follow the mouse position:
void Player::handleInput() { Vector2D* target = TheInputHandler::Instance() ->getMousePosition(); m_velocity = *target - m_position; m_velocity /= 50; }
First, we get the current mouse location; we can then get a vector that leads from the current position to the mouse position by subtracting the current position from the mouse position. We then divide the velocity by a scalar to slow us down a little and allow us to see our helicopter catch up to the mouse rather than stick to it. Our PlayState.h
file will now need its own vector of GameObject*
:
class GameObject; class PlayState : public GameState { public: virtual void update(); virtual void render(); virtual bool onEnter(); virtual bool onExit(); virtual std::string getStateID() const { return s_playID; } private: static const std::string s_playID; std::vector<GameObject*> m_gameObjects; };
Finally, we must update the PlayState.cpp
implementation file to use our Player
object:
const std::string PlayState::s_playID = "PLAY"; void PlayState::update() { for(int i = 0; i < m_gameObjects.size(); i++) { m_gameObjects[i]->update(); } } void PlayState::render() { for(int i = 0; i < m_gameObjects.size(); i++) { m_gameObjects[i]->draw(); } } bool PlayState::onEnter() { if(!TheTextureManager::Instance()->load("assets/helicopter.png", "helicopter", TheGame::Instance()->getRenderer())) { return false; } GameObject* player = new Player(new LoaderParams(100, 100, 128, 55, "helicopter"); m_gameObjects.push_back(player); std::cout << "entering PlayState "; return true; } bool PlayState::onExit() { for(int i = 0; i < m_gameObjects.size(); i++) { m_gameObjects[i]->clean(); } m_gameObjects.clear(); TheTextureManager::Instance() ->clearFromTextureMap("helicopter"); std::cout << "exiting PlayState "; return true; }
This file is very similar to the MenuState.cpp
file, but this time we are using a Player
object rather than the two MenuButton
objects. There is one adjustment to our SDLGameObject.cpp
file that will make PlayState
look even better; we are going to flip the image file depending on the velocity of the object:
void SDLGameObject::draw() { if(m_velocity.getX() > 0) { TextureManager::Instance()->drawFrame(m_textureID, (Uint32)m_position.getX(), (Uint32)m_position.getY(), m_width, m_height, m_currentRow, m_currentFrame, TheGame::Instance()->getRenderer(),SDL_FLIP_HORIZONTAL); } else { TextureManager::Instance()->drawFrame(m_textureID, (Uint32)m_position.getX(), (Uint32)m_position.getY(), m_width, m_height, m_currentRow, m_currentFrame, TheGame::Instance()->getRenderer()); } }
We check whether the object's velocity is more than 0
(moving to the right-hand side) and flip the image accordingly. Run our game and you will now have the ability to move between MenuState
and PlayState
each with their own functionality and objects. The following screenshot shows our project so far:
Another very important state for our games is the pause state. Once paused, the game could have all kinds of options. Our PauseState
class will be very similar to the MenuState
, but with different button visuals and callbacks. Here are our two new screenshots (again available in the source code download):
The following screenshot shows the resume functionality:
Let's start by creating our PauseState.h
file in the project:
class GameObject; class PauseState : public GameState { public: virtual void update(); virtual void render(); virtual bool onEnter(); virtual bool onExit(); virtual std::string getStateID() const { return s_pauseID; } private: static void s_pauseToMain(); static void s_resumePlay(); static const std::string s_pauseID; std::vector<GameObject*> m_gameObjects; };
Next, create our PauseState.cpp
file:
const std::string PauseState::s_pauseID = "PAUSE"; void PauseState::s_pauseToMain() { TheGame::Instance()->getStateMachine()->changeState(new MenuState()); } void PauseState::s_resumePlay() { TheGame::Instance()->getStateMachine()->popState(); } void PauseState::update() { for(int i = 0; i < m_gameObjects.size(); i++) { m_gameObjects[i]->update(); } } void PauseState::render() { for(int i = 0; i < m_gameObjects.size(); i++) { m_gameObjects[i]->draw(); } } bool PauseState::onEnter() { if(!TheTextureManager::Instance()->load("assets/resume.png", "resumebutton", TheGame::Instance()->getRenderer())) { return false; } if(!TheTextureManager::Instance()->load("assets/main.png", "mainbutton", TheGame::Instance()->getRenderer())) { return false; } GameObject* button1 = new MenuButton(new LoaderParams(200, 100, 200, 80, "mainbutton"), s_pauseToMain); GameObject* button2 = new MenuButton(new LoaderParams(200, 300, 200, 80, "resumebutton"), s_resumePlay); m_gameObjects.push_back(button1); m_gameObjects.push_back(button2); std::cout << "entering PauseState "; return true; } bool PauseState::onExit() { for(int i = 0; i < m_gameObjects.size(); i++) { m_gameObjects[i]->clean(); } m_gameObjects.clear(); TheTextureManager::Instance() ->clearFromTextureMap("resumebutton"); TheTextureManager::Instance() ->clearFromTextureMap("mainbutton"); // reset the mouse button states to false TheInputHandler::Instance()->reset(); std::cout << "exiting PauseState "; return true; }
In our PlayState.cpp
file, we can now use our new PauseState
class:
void PlayState::update() { if(TheInputHandler::Instance()->isKeyDown(SDL_SCANCODE_ESCAPE)) { TheGame::Instance()->getStateMachine()->pushState(new PauseState()); } for(int i = 0; i < m_gameObjects.size(); i++) { m_gameObjects[i]->update(); } }
This function listens for the Esc key being pressed, and once it has been pressed, it then pushes a new PauseState
class onto the state array in FSM. Remember that pushState
does not remove the old state; it merely stops using it and uses the new state. Once we are done with the pushed state, we remove it from the state array and the game continues to use the previous state. We remove the pause state using the resume button's callback:
void PauseState::s_resumePlay() { TheGame::Instance()->getStateMachine()->popState(); }
The main menu button takes us back to the main menu and completely removes any other states:
void PauseState::s_pauseToMain() { TheGame::Instance()->getStateMachine()->changeState(new MenuState()); }
We are going to create one final state, GameOverState
. To get to this state, we will use collision detection and a new Enemy
object in the PlayState
class. We will check whether the Player
object has hit the Enemy
object, and if so, we will change to our GameOverState
class. Our Enemy object will use a new image helicopter2.png
:
We will make our Enemy
object's helicopter move up and down the screen just to keep things interesting. In our Enemy.cpp
file, we will add this functionality:
Enemy::Enemy(const LoaderParams* pParams) : SDLGameObject(pParams) { m_velocity.setY(2); m_velocity.setX(0.001); } void Enemy::draw() { SDLGameObject::draw(); } void Enemy::update() { m_currentFrame = int(((SDL_GetTicks() / 100) % m_numFrames)); if(m_position.getY() < 0) { m_velocity.setY(2); } else if(m_position.getY() > 400) { m_velocity.setY(-2); } SDLGameObject::update(); }
We can now add an Enemy
object to our PlayState
class:
bool PlayState::onEnter() { if(!TheTextureManager::Instance()->load("assets/helicopter.png", "helicopter", TheGame::Instance()->getRenderer())) { return false; } if(!TheTextureManager::Instance() ->load("assets/helicopter2.png", "helicopter2", TheGame::Instance()->getRenderer())) { return false; } GameObject* player = new Player(new LoaderParams(500, 100, 128, 55, "helicopter")); GameObject* enemy = new Enemy(new LoaderParams(100, 100, 128, 55, "helicopter2")); m_gameObjects.push_back(player); m_gameObjects.push_back(enemy); std::cout << "entering PlayState "; return true; }
Running the game will allow us to see our two helicopters:
Before we cover collision detection, we are going to create our GameOverState
class. We will be using two new images for this state, one for new MenuButton
and one for a new type, which we will call AnimatedGraphic
:
The following screenshot shows the game over functionality:
AnimatedGraphic
is very similar to other types, so I will not go into too much detail here; however, what is important is the added value in the constructor that controls the speed of the animation, which sets the private member variable m_animSpeed
:
AnimatedGraphic::AnimatedGraphic(const LoaderParams* pParams, int animSpeed) : SDLGameObject(pParams), m_animSpeed(animSpeed) { }
The update
function will use this value to set the speed of the animation:
void AnimatedGraphic::update() { m_currentFrame = int(((SDL_GetTicks() / (1000 / m_animSpeed)) % m_numFrames)); }
Now that we have the AnimatedGraphic
class, we can implement our GameOverState
class. Create GameOverState.h
and GameOverState.cpp
in our project; the header file we will create should look very familiar, as given in the following code:
class GameObject; class GameOverState : public GameState { public: virtual void update(); virtual void render(); virtual bool onEnter(); virtual bool onExit(); virtual std::string getStateID() const {return s_gameOverID;} private: static void s_gameOverToMain(); static void s_restartPlay(); static const std::string s_gameOverID; std::vector<GameObject*> m_gameObjects; };
Our implementation file is also very similar to other files already covered, so again I will only cover the parts that are different. First, we define our static variables and functions:
const std::string GameOverState::s_gameOverID = "GAMEOVER"; void GameOverState::s_gameOverToMain() { TheGame::Instance()->getStateMachine()->changeState(new MenuState()); } void GameOverState::s_restartPlay() { TheGame::Instance()->getStateMachine()->changeState(new PlayState()); }
The onEnter
function will create three new objects along with their textures:
bool GameOverState::onEnter() { if(!TheTextureManager::Instance()->load("assets/gameover.png", "gameovertext", TheGame::Instance()->getRenderer())) { return false; } if(!TheTextureManager::Instance()->load("assets/main.png", "mainbutton", TheGame::Instance()->getRenderer())) { return false; } if(!TheTextureManager::Instance()->load("assets/restart.png", "restartbutton", TheGame::Instance()->getRenderer())) { return false; } GameObject* gameOverText = new AnimatedGraphic(new LoaderParams(200, 100, 190, 30, "gameovertext", 2), 2); GameObject* button1 = new MenuButton(new LoaderParams(200, 200, 200, 80, "mainbutton"), s_gameOverToMain); GameObject* button2 = new MenuButton(new LoaderParams(200, 300, 200, 80, "restartbutton"), s_restartPlay); m_gameObjects.push_back(gameOverText); m_gameObjects.push_back(button1); m_gameObjects.push_back(button2); std::cout << "entering PauseState "; return true; }
That is pretty much it for our GameOverState
class, but we must now create a condition that creates this state. Move to our PlayState.h
file and we will create a new function to allow us to check for collisions:
bool checkCollision(SDLGameObject* p1, SDLGameObject* p2);
We will define this function in PlayState.cpp
:
bool PlayState::checkCollision(SDLGameObject* p1, SDLGameObject* p2) { int leftA, leftB; int rightA, rightB; int topA, topB; int bottomA, bottomB; leftA = p1->getPosition().getX(); rightA = p1->getPosition().getX() + p1->getWidth(); topA = p1->getPosition().getY(); bottomA = p1->getPosition().getY() + p1->getHeight(); //Calculate the sides of rect B leftB = p2->getPosition().getX(); rightB = p2->getPosition().getX() + p2->getWidth(); topB = p2->getPosition().getY(); bottomB = p2->getPosition().getY() + p2->getHeight(); //If any of the sides from A are outside of B if( bottomA <= topB ){return false;} if( topA >= bottomB ){return false; } if( rightA <= leftB ){return false; } if( leftA >= rightB ){return false;} return true; }
This function checks for collisions between two SDLGameObject
types. For the function to work, we need to add three new functions to our SDLGameObject
class:
Vector2D& getPosition() { return m_position; } int getWidth() { return m_width; } int getHeight() { return m_height; }
The next chapter will deal with how this function works, but for now, it is enough to know that it does. Our PlayState
class will now utilize this collision detection in its
update
function:
void PlayState::update() { if(TheInputHandler::Instance()->isKeyDown(SDL_SCANCODE_ESCAPE)) { TheGame::Instance()->getStateMachine()->pushState(new PauseState()); } for(int i = 0; i < m_gameObjects.size(); i++) { m_gameObjects[i]->update(); } if(checkCollision(dynamic_cast<SDLGameObject*> (m_gameObjects[0]), dynamic_cast<SDLGameObject*> (m_gameObjects[1]))) { TheGame::Instance()->getStateMachine()->pushState(new GameOverState()); } }
We have to use a dynamic_cast
object to cast our GameObject*
class to an SDLGameObject*
class. If checkCollision
returns true
, then we add the GameOverState
class. The following screenshot shows the GameOver
state:
3.17.157.6