Part 7: Game Objects, Buttons and State Switching
Creating Game Objects, unique type game objects and using them to switch states.
Part of the core functionality of our game are game objects. These encompass pretty much everything in your game from buttons to the player to a weapon. So we want to be able easily update, render and handle input for our objects in our states so we need a base class which the states can act upon in a loop.
First things first, let’s make the base class which our object classes will derive from. Create a new class and call it “GameObject. Once done, open up GameObject.h, clear it out and insert the following code:
First things first, let’s make the base class which our object classes will derive from. Create a new class and call it “GameObject. Once done, open up GameObject.h, clear it out and insert the following code:
#pragma once
#include "SDL.h" #include "Vector2D.h" #include <string> class GameObject { public: protected: }; |
So we have a simple header file set up with a public and protected section, (protected instead of private so that the derived classes can use them privately too) along with some includes at the top.
We include SDL.h for our SDL functionality, Vector2D (our custom vector point class) for the position of our objects and also string which we can be useful in general, but we will have an ID for the object’s texture (you will be able to override the drawing function in more complex classes like Player, but for simple classes with only 1 texture, it’s useful to store the ID in the class for drawing so we don’t have to write out a unique drawing function for each object).
So with that done, we can start adding some padding to our empty class header here; let’s start with our protected section (I’ll take you through adding each bit this time, then at the end of it I’ll show you what it should sort of look like (ordering/positioning of variables and functions doesn’t matter, as long as they’re in the right section, be it public or protected or private). Before we get started, go ahead and add a destructor to the protected section like so:
We include SDL.h for our SDL functionality, Vector2D (our custom vector point class) for the position of our objects and also string which we can be useful in general, but we will have an ID for the object’s texture (you will be able to override the drawing function in more complex classes like Player, but for simple classes with only 1 texture, it’s useful to store the ID in the class for drawing so we don’t have to write out a unique drawing function for each object).
So with that done, we can start adding some padding to our empty class header here; let’s start with our protected section (I’ll take you through adding each bit this time, then at the end of it I’ll show you what it should sort of look like (ordering/positioning of variables and functions doesn’t matter, as long as they’re in the right section, be it public or protected or private). Before we get started, go ahead and add a destructor to the protected section like so:
protected:
virtual ~GameObject() {} |
Now, we’re going to need a rectangle for drawing and positioning our object. We’re going to use the SDL class SDL_Rect and create ourselves a rectangle variable in the protected section of our GameObject header. An SDL_Rect has an x, y, w (for width) and y (for height) attribute to complete the rectangle, but we’ll worry about that in the .cpp file; for now just create the empty variable in the protected section below the deconstructor:
SDL_Rect rect;
|
Next, we want some movement variables for our object (not all, but a lot of our objects will move so we may as well add this functionality as it will prove to be optional as we create the movement function). Go ahead and add a couple Vector2D variables:
Vector2D pos, vel;
|
NOTE: If you haven’t seen this style of variable declaration before it may confuse you. Basically, if you are declaring multiple variables of the same type you can simply add a comma after each variable name (or definition if you define it here e.g. int num = 0, num1;) and then after the last one you add the semi colon.
For drawing our object, we will add the ability to scale the size which it is drawn at, for this we need a scale factor, so add a float variable called scale and make its default value equal to 1:
For drawing our object, we will add the ability to scale the size which it is drawn at, for this we need a scale factor, so add a float variable called scale and make its default value equal to 1:
float scale = 1;
|
Next up, some of our objects may be animated, so for animations (which I will go through with the examples later on) we need a integer variable for the currentRow of the sprite sheet (again, explain later) and for the currentFrame of the sprite sheet and finally for the number of frames in the animation. By default we will set these first two to equal “0” meaning be default the first frame in the first row will be selected. For the last variable, numFrames, we will set this to 1, because at the very least there will be 1 frame (a still image).
int curRow = 0, curFrame = 0, numFrames = 1;
|
Next we need a variable to track the rotation factor we want our texture to be drawn with:
double rotate = 0.0;
|
Next we’ll add a string variable to hold the id for the texture we want to give our object. Seen as the TextureManager needs an ID to retrieve the texture; we’ll leave this undefined here because it will be defined uniquely in each object’s class constructor:
std::string textureID;
|
Now that’s done we can move into the public section of the header file and set up our public functions; first though, this is roughly what your protected section should look like:
protected:
virtual ~GameObject() {} Vector2D pos, vel; SDL_Rect rect; float scale = 1; int curRow = 0, curFrame = 0, numFrames = 1; double rotate = 0.0; std::string textureID; |
So we can start off by declaring our constructor in the public section (unlike the protected deconstructor, we won’t be defining it as empty here as we’ll use it to initialise any attributes that need it in the constructor when we create each object:
public:
GameObject(); |
That done, we need to declare the fundamental functions for our objects; update, render and clean:
virtual void draw();
virtual void update(); virtual void clean(); |
The draw function will be called in the states render function that the object exists in, the update function in the states update function and the clean function will be called in the onExit function (more on this later).
Next up we can add some getter functions to get the protected variables outside this class (or its child classes) without editing them:
Next up we can add some getter functions to get the protected variables outside this class (or its child classes) without editing them:
float getW() { return rect.w; }
float getH() { return rect.h; } Vector2D& getPos() { return pos; } std::string getTextureID() { return textureID; } |
The first two getter functions are simply for getting the width and height of the object’s rectangle, useful for collision detection. The third one gets the position attribute which is the x and y position of the player in Vector2D form. Finally, the last getter function returns the texture ID attached to the class, useful for drawing or just finding out what texture is used.
We’re also going to add collision detection for our objects; in the example object class we create this will be used for telling if the mouse is hovering over it, but this can be used for any type of collision with a point. It is basically used to tell if the point (Vector2D) passed into the parameter is inside the objects rectangle:
We’re also going to add collision detection for our objects; in the example object class we create this will be used for telling if the mouse is hovering over it, but this can be used for any type of collision with a point. It is basically used to tell if the point (Vector2D) passed into the parameter is inside the objects rectangle:
virtual bool checkCollision(Vector2D* vec);
|
We will define this later on, for now we’re done in the header file for our GameObject class; let’s move over to GameObject.cpp, clear it out as usual and insert the following:
#include “GameObject.h"
#include "Game.h" |
Here are the includes we need for our base GameObject.cpp file. We include game so we can use all it’s functionality (i.e. the getRenderer() function) and also because Game.h in turn includes TextureManager and InputHandler etc (no need to include them again).
First thing’s first let’s deal with our constructor, write out the empty constructor:
First thing’s first let’s deal with our constructor, write out the empty constructor:
GameObject::GameObject()
{ } |
You will find that this causes an error and a squiggly red line appears under the first curly bracket. This is because we need to declare the vectors in our class in a special way we haven’t seen before. Look here and see:
GameObject::GameObject() : pos(0, 0), vel(0, 0)
{ } |
Adding this will remove the error, because we need to define the vectors we cannot leave them undefined at any point so we do it this way; we call the variable name and then in brackets we define it. Next we need to head back to the header file (GameObject.h) so we can add some parameters to the GameObject constructor like this:
GameObject(std::string TEX_ID, int X, int Y, int W, int H, float SCALE, int NUM_FRAMES);
|
Now that we’ve added these parameters to the constructor definition, they too need to be added to the constructor declaration, so back to GameObject.cpp it is to make these changes:
GameObject::GameObject(std::string TEX_ID, int X, int Y, int W, int H, float SCALE,
int NUM_FRAMES) : pos(X, Y), vel(0, 0) { } |
So we added the parameters and also used two of them to define the value of our Vector2D attribute “pos”. Next we need to define some more variables in the body of the constructor like so:
GameObject::GameObject(std::string TEX_ID, int X, int Y, int W, int H, float SCALE,
int NUM_FRAMES) : pos(X, Y), vel(0, 0) { rect = { X, Y, W, H }; scale = SCALE; textureID = TEX_ID; numFrames = NUM_FRAMES; } |
Here we’ve defined our left over variables that were previously undefined; our rectangle is now defined meaning our object has a position and size, we have a scale value defined for the object and we also have the texture ID and the number of frames defined so we can go ahead and draw it when we need to.
Now that this is done we can move onto the left over functions; update, render and clean. Let’s add them now starting with update:
Now that this is done we can move onto the left over functions; update, render and clean. Let’s add them now starting with update:
void GameObject::update()
{ pos += vel; rect.x = pos.getX(); rect.y = pos.getY(); } |
Here in the update function all we’re doing is adding our velocity (vel) value to our position (pos) value; if the x and y value of the velocity vector point were more than or less than 0 then it would affect our position and thus make movement happen. We then set the x and y of our rectangle to the x and y of the pos variable so that when we draw the object you will see the movement.
void GameObject::draw()
{ _TextureManager::Instance()->drawFrame(textureID, rect.x, rect.y, rect.w, rect.h, scale, curRow, curFrame, rotate, _Game::Instance()->getRenderer()); } |
Here in the draw function we very simply want to draw our texture at the position of the rectangle. We use the drawFrame function as opposed to the draw function from the texture manager in case our object is animated (has more than 1 frame).
Next up is our collision checking function:
Next up is our collision checking function:
bool GameObject::checkCollision(Vector2D* vec)
{ if (vec->getX() > pos.getX() && vec->getX() < pos.getX() + getW() && vec->getY() > pos.getY() && vec->getY() < pos.getY() + getH()) { return true; } return false; } |
Here we have 1 long if statement that says all the following separated by the logical operator “&&” (double ambersand) which means and in C++:
- Is the point more than the left edge of our object
- Is the point less than the right edge of our object
- Is the point more than the top edge of our object
- Is the point less than the bottom edge of our object
Because of the “&&” sign we use, there are no “or” (||) operators so all statements must be true for it to return true and let us in the if statement.
If all statements our true it means that the point is inside/has collided with our object, and so returns true. If not, the if statement doesn’t run the code within and we get a false return statement.
The final function in the class is clean() and we’ll add that in now:
- Is the point more than the left edge of our object
- Is the point less than the right edge of our object
- Is the point more than the top edge of our object
- Is the point less than the bottom edge of our object
Because of the “&&” sign we use, there are no “or” (||) operators so all statements must be true for it to return true and let us in the if statement.
If all statements our true it means that the point is inside/has collided with our object, and so returns true. If not, the if statement doesn’t run the code within and we get a false return statement.
The final function in the class is clean() and we’ll add that in now:
void GameObject::clean()
{ } |
We have nothing to clean by default in the base class; you could argue that the texture is worth clearing up but if another object uses the same texture and we remove it then problems will occur.
So that’s our GameObject base class all set up so let’s add our new functionality to the states, head over into GameState.h and first include our GameObject.h and map at the top with the other include statements (#include “GameObject.h”) (#include <map>). Next up we want to add a new array to our state, one which will hold and handle all our game objects. In the protected section add the following variable:
So that’s our GameObject base class all set up so let’s add our new functionality to the states, head over into GameState.h and first include our GameObject.h and map at the top with the other include statements (#include “GameObject.h”) (#include <map>). Next up we want to add a new array to our state, one which will hold and handle all our game objects. In the protected section add the following variable:
protected:
std::map<std::string, GameObject*> gameObjects; |
This is creating a new map array variable, a map array is different from a vector array in that it uses IDs to find the objects rather than a sequence of numbers (0, 1, 2, 3…) and so because of this, to run through them in a loop we need something called an iterator. We add this iterator at the bottom of our GameState.h file below the last curly brace and semi colon:
typedef std::map<std::string, GameObject*>::iterator it_type;
|
This simplifies our job in the .cpp file when handling iterators in the for loops for updating, rendering and cleaning our objects.
Now that we’ve got that down, we can add a getter function in the public section of GameState.h like so:
Now that we’ve got that down, we can add a getter function in the public section of GameState.h like so:
GameObject* getObjectByID(std::string id) { return gameObjects[id]; }
|
We take a string variable called id as the parameter because string is the type of ID we defined for our map array. We then call the map variable followed by the id variable in square brackets, that is one way of getting an element from the map array, we will see another when we run through the loops in GameState.cpp. So now we’re done here and seeing as this class is an abstract base class, we must head over into each of our states (we currently have one, MenuState) and add the functionality in there. So head into MenuState.cpp and first go in to the update function and add this:
void MenuState::update()
{ for (it_type iterator = gameObjects.begin(); iterator != gameObjects.end(); iterator++) { iterator->second->update(); } } |
Here is the first example of using the iterator to cycle through the map and call our game objects functionality. The iterator assumes the role of a position in the map array starting at the beginning with gameObjects.begin() and with each cycle steps up a position in the array until it reaches the end. Each cycle of the loop we call the iterator (position in the array) and the second element of that position (the game object rather than the string id which would be the first) and then we can call the functions from that object; in this case, update().
We do the same for render and onExit but we call their respective functions instead like so:
We do the same for render and onExit but we call their respective functions instead like so:
void MenuState::render()
{ SDL_SetRenderDrawColor(_Game::Instance()->getRenderer(), 0, 0, 0, 255); _TextureManager::Instance()->draw("test", 100, 100, 128, 128, 1, 0, _Game::Instance()->getRenderer()); _TextureManager::Instance()->drawFrame("test", 300, 300, 128, 128, 1, 0, 0, 0, _Game::Instance()->getRenderer()); for (it_type iterator = gameObjects.begin(); iterator != gameObjects.end(); iterator++) { iterator->second->draw(); } } bool MenuState::onExit() { for (it_type iterator = gameObjects.begin(); iterator != gameObjects.end(); iterator++) { iterator->second->clean(); } gameObjects.clear(); std::cout << "exiting MenuState..." << std::endl; return true; } |
Here you can see the two for loops in the two functions are identical to the one in update except render calls draw and onExit calls clean. In the onExit function we also call gameObjects.clear() which empties the array freeing up the memory space for our program. Once the state is exited they are no longer needed so this is good practice to keep your game optimised.
Once we’ve done that we have finished adding the gameObject functionality to our game. First thing we’re going to do is change the two images we had from the previous tutorials and turn them into objects and add some movement. First thing we do is go into the onEnter function for our menu state and change things up a bit. In here we can add the following lines below the texture loading functions:
Once we’ve done that we have finished adding the gameObject functionality to our game. First thing we’re going to do is change the two images we had from the previous tutorials and turn them into objects and add some movement. First thing we do is go into the onEnter function for our menu state and change things up a bit. In here we can add the following lines below the texture loading functions:
gameObjects.insert(std::pair<std::string, GameObject*> ("penguin0", new GameObject("test", 100, 100, 128,
128, 1.0, 1))); gameObjects.insert(std::pair<std::string, GameObject*> ("penguin1", new GameObject("test", 300, 300, 128, 128, 1.0, 1))); |
We use the map arrays “insert” function to add an element to the map, this function takes a pair variable, which takes two variable types in the “<>” symbols (the type of each part of the pair, which will be the same as our map types, string and GameObject). So in the parameters for the pair we first give the object an ID we can call it by if needs be, then the next parameter we create a new GameObject.
Now go into the render function of the state and remove the two individual draw functions we had there before as shown below, the render function should now look like this:
Now go into the render function of the state and remove the two individual draw functions we had there before as shown below, the render function should now look like this:
void MenuState::render()
{ SDL_SetRenderDrawColor(_Game::Instance()->getRenderer(), 0, 0, 0, 255); for (it_type iterator = gameObjects.begin(); iterator != gameObjects.end(); iterator++) { iterator->second->draw(); } } |
Now if we run the program again you will see that the two penguins are in the same place they were before, but they are now game objects (more exciting that it looks).
Now we have our basic GameObject base class made we can go ahead with a little advancement and create a new class called Button; so go ahead and do that, clear the header file and insert the following code:
Now we have our basic GameObject base class made we can go ahead with a little advancement and create a new class called Button; so go ahead and do that, clear the header file and insert the following code:
#pragma once
#include "GameObject.h" class Button : public GameObject { public: private: }; |
Here we have a bare bones class, derived from GameObject as you can tell from the “: public GameObject” which we first encountered with states if you remember.
First thing we need to sort out is our constructor, we’re going to add all the same parameters we had in GameObject except 1, NUM_FRAMES, because as a simple button it will only every have 3 frames; mouse out, mouse in, and mouse down. So let’s add that constructor declaration:
First thing we need to sort out is our constructor, we’re going to add all the same parameters we had in GameObject except 1, NUM_FRAMES, because as a simple button it will only every have 3 frames; mouse out, mouse in, and mouse down. So let’s add that constructor declaration:
#pragma once
#include "GameObject.h" class Button : public GameObject { public: Button(std::string TEX_ID, int X, int Y, int W, int H, int SCALE); private: }; |
Next up we have our three fundamental functions; draw, update and clean. Add these as follows to the public sector after the constructor:
virtual void draw();
virtual void update(); virtual void clean(); |
Once we’ve done this we need to add a couple things to the private section of our Button.h. The first is a Boolean variable to handle whether or not the button is released so we can handle what happens when it’s clicked (we handle click functionality on click release so that if the player changes their mind after pressing down the mouse button they can move the mouse out of the button area and release without it taking effect). Add this to our private section:
bool released;
|
The next thing we need to add is an enum type for the state of the button. It will have three states; MOUSE_OUT, MOUSE_OVER and MOUSE_DOWN. So let’s go ahead and create that enum inside of the private section:
enum button_state
{ MOUSE_OUT = 0, MOUSE_OVER = 1, MOUSE_DOWN = 2 }; |
Once we’ve done that we can move over into our Button.cpp file and clear it out before inserting the following lines:
#include "Button.h"
#include "Game.h" #include "InputHandler.h" |
We include the header file for the class by default but then we also include Game.h for functionality like the function getRenderer() and we include InputHandler.h so we can handle mouse input to tell when the mouse is hovering or clicked on our button to change frames (states).
The next thing to do is define the constructor so we go ahead and do that here:
The next thing to do is define the constructor so we go ahead and do that here:
Button::Button(std::string TEX_ID, int X, int Y, int W, int H, int SCALE) : GameObject(TEX_ID, X, Y, W, H, SCALE, 3)
{ curFrame = MOUSE_OUT; _TextureManager::Instance()->load("assets/play.png", "playbut", _Game::Instance()->getRenderer()) _TextureManager::Instance()->load("assets/exit.png", "exitbut", _Game::Instance()->getRenderer()); } |
Here we first include all the parameters from the header file as it is compulsory unless you’re in love with errors (god help you). Next we have to satisfy the constructor of the base class GameObject in the same way we did with pos and vel in the GameObject constructor. So we call the constructor GameObject and in the parameters we pass all the parameters of our Button constructor and then for the last parameter of GameObject we pass the value 3 as this is the num frames parameter and as a simple button it will always have just 3 frames.
Next, we fill out the constructor with some setting up; all we need to do is we set the curFrame variable to MOUSE_OUT (or 0, check the enum button_state).
Moving onto our first function definition; update():
Next, we fill out the constructor with some setting up; all we need to do is we set the curFrame variable to MOUSE_OUT (or 0, check the enum button_state).
Moving onto our first function definition; update():
void Button::update()
{ if (checkCollision(_InputHandler::Instance()->getMousePos())) { if (_InputHandler::Instance()->getMouseButtonState(LEFT)) { curFrame = MOUSE_DOWN; released = false; } else { released = true; curFrame = MOUSE_OVER; } } else { curFrame = MOUSE_OUT; } } |
Okay, so let’s go through our button’s update function. We have an if statement first checking to see if the mouse is inside our button using GameObject’s checkCollision() function using the mouse position as taken from the input handler as our point of collision. If it returns false (no collision) we go to the else statement and change our curFrame variable to MOUSE_OUT. However, if the mouse does collide we then check to see if the mouse is pressed down, if it is then we set the current frame to MOUSE_DOWN. Then we set our released variable to false because the button is pressed not released. If the mouse isn’t down we then move to the else statement (mouse isn’t pressed) at which point we then set released to true and set curFrame to MOUSE_OVER. We will handle what happens on release of the mouse in the states input functions later.
Next we can polish off the last two functions, update and clean, as they are almost the same and incredibly simple:
Next we can polish off the last two functions, update and clean, as they are almost the same and incredibly simple:
void Button::draw()
{ GameObject::draw(); } void Button::clean() { GameObject::clean(); } |
All these two functions all we do is call the parent class’ version of this function, you can add extra functionality after these function calls but that isn’t needed for this object type. We’ve done this now so we can add these two objects to the MenuState to test them. Go into MenuState.cpp, include “Button.h” at the top with the other includes, remove the two gameObject.insert lines and the two TextureManager load() lines that are already in the onEnter function (for the penguin images) and add the following lines:
_TextureManager::Instance()->load("assets/play.png", "playbut", _Game::Instance()->getRenderer());
_TextureManager::Instance()->load("assets/exit.png", "exitbut", _Game::Instance()->getRenderer()); gameObjects.insert(std::pair<std::string, GameObject*>("playbutton", new Button("playbut", 100, 100, 90, 30, 1))); gameObjects.insert(std::pair<std::string, GameObject*>("exitbutton", new Button("exitbut", 100, 140, 90, 30, 1))); |
What we’ve done here is load two textures, one for each button and then added two new buttons to the object map. These two buttons are going to be a play button and an exit button. Go -> here <- download the two images and save them in your assets folder where test.png should be right now.
Now that we’ve added those two objects and placed the images appropriately we can load up the game and you should have two buttons that currently do nothing but react to mouse over and click:
Now that we’ve added those two objects and placed the images appropriately we can load up the game and you should have two buttons that currently do nothing but react to mouse over and click:
(You can’t see it in the screenshot, but the mouse is over the play button and is clicked).
So now we’re going to give both buttons some functionality to show the possibilities. First things first we’re going want to create a new state called PlayState so that our play button has something to do. Go ahead and create a new class called PlayState, open the header file, clear it out and enter the following code, you should understand most of what is going on here, if you don’t refer back to the finite state machine tutorial:
So now we’re going to give both buttons some functionality to show the possibilities. First things first we’re going want to create a new state called PlayState so that our play button has something to do. Go ahead and create a new class called PlayState, open the header file, clear it out and enter the following code, you should understand most of what is going on here, if you don’t refer back to the finite state machine tutorial:
#pragma once
#include "GameState.h" #include <string> class PlayState : public GameState { public: virtual void update(); virtual void render(); virtual bool onEnter(); virtual bool onExit(); virtual void onKeyDown(SDL_Event* e); virtual void onKeyUp(SDL_Event* e); virtual void onMouseButtonUp(SDL_Event& e); virtual void onMouseButtonDown(SDL_Event& e); virtual void onMouseMove(SDL_Event& e); virtual std::string getStateID() const { return playID; } private: static const std::string playID; }; |
As you can see here, it is almost identical to MenuState.h except the class name is PlayState and the id string is called playID as opposed to menuID. Next up, PlayState.cpp:
#include "PlayState.h"
#include <iostream> #include "Game.h" #include "GameStateMachine.h" #include "TextureManager.h" #include "Button.h" const std::string PlayState::playID = "PLAY"; void PlayState::update() for (it_type iterator = gameObjects.begin(); iterator != gameObjects.end(); iterator++) { iterator->second->update(); } void PlayState::render() { SDL_SetRenderDrawColor(_Game::Instance()->getRenderer(), 0, 170, 241, 255); for (it_type iterator = gameObjects.begin(); iterator != gameObjects.end(); iterator++) { iterator->second->draw(); } } bool PlayState::onEnter() { std::cout << "entering MenuState..." << std::endl; return true; } bool PlayState::onExit() { for (it_type iterator = gameObjects.begin(); iterator != gameObjects.end(); iterator++) { iterator->second->clean(); } gameObjects.clear(); std::cout << "exiting MenuState..." << std::endl; return true; } void PlayState::onKeyDown(SDL_Event* e) { } void PlayState::onKeyUp(SDL_Event* e) { } void PlayState::onMouseButtonDown(SDL_Event& e) { } void PlayState::onMouseButtonUp(SDL_Event& e) { } void PlayState::onMouseMove(SDL_Event& e) { } |
Much like the header file, this is cpp file is almost identical to MenuState’s cpp file, except the name of the class is PlayState (remember the include at the very top too), the id is playID and there are no objects or textures loaded in the onEnter function; I also altered the colour in the SDL_SetRenderDrawColor function in the render function to make sure the state has indeed changed. So there we have a new state to switch to with our play button, now we can head back into MainMenu.cpp and add that functionality, so do so and go into the onMouseButtonUp function where we’ll handle it:
void MenuState::onMouseButtonUp(SDL_Event& e)
{ if (gameObjects["playbutton"]->checkCollision(_InputHandler::Instance()->getMousePos())) { _Game::Instance()->getStateMachine()->changeState(new PlayState()); } if (gameObjects["exitbutton"]->checkCollision(_InputHandler::Instance()->getMousePos())) { _Game::Instance()->quit(); } } |
This is the code we need inside our onMouseButtonUp to add functionality to our buttons. We use if statements to check collision with our individual objects, which we call using the IDs we set when creating them. We then proceed to right the code we want to execute if the mouse is released while inside the button. For the play button we get the state machine object from our game singleton and change the state (add the new state and remove the old one, check the state machine tutorial for more if you forgot stuff) to a new PlayState. For the exit button we simply call the games quit function which closes the game. Go ahead and test that now, the exit button should close the program and the play button should switch to the new state like so:
If you got that then this works and we’re done here, we showed that game objects can have functionality and are super easy to create and handle. You could technically go ahead and create your game with what you know now, but I will be writing maybe 1 or 2 more tutorials for optional lessons. Look out for them, there will be a button below when they’re ready.