Subscribe to GameFromScratch on YouTube Support GameFromScratch on Patreon


Game From Scratch C++ Edition Part 5

 

 

This section is going to be a bit of overkill for a simple pong game, but in a game of any greater complexity this is a task you are going to encounter eventually.  You see, until now we have just been declaring all the various bits and bobs ( really technical term there ) that we need in the Game class.  Our PlayerPaddle for example, but in coming times we are going to be adding another Paddle, a Ball, a Scoreboard.  A handful of items isn’t really a big deal, but when you start getting into the hundred, thousands or even millions, it obviously isn’t ideal.

 

 

Therefore we are going to develop an object that is going to hold all of our VisibleGameObjects, being in charge of updating, drawing and then finally removing them.  The only thing it isn’t going to be in charge of is creating them.  In the process, we are going to explore the incredibly handy std::map class, introduce the concept of a functor and even play with some naked pointers ( not nearly as fun as it sounds ).

 

 

Alright, lets get started.  First we need to declare our new object.  Create a header file named GameObjectManager.h.  Now we want to add the following code:

 

1 #pragma once 2 #include "VisibleGameObject.h" 3 4 class GameObjectManager 5 { 6 public: 7 GameObjectManager(); 8 ~GameObjectManager(); 9 10 void Add(std::string name, VisibleGameObject* gameObject); 11 void Remove(std::string name); 12 int GetObjectCount() const; 13 VisibleGameObject* Get(std::string name) const; 14 15 void DrawAll(sf::RenderWindow& renderWindow); 16 17 private: 18 std::map<std::string, VisibleGameObject*> _gameObjects; 19 20 struct GameObjectDeallocator 21 { 22 void operator()(const std::pair<std::string,VisibleGameObject*> & p) const 23 { 24 delete p.second; 25 } 26 }; 27 };

Click here to download GameObjectManager.h

 

 

On line 7 and 8 we have familiar constructor and destructor definitions.  You will find as a trend I generally define a constructor and destructor even if they are empty, as the compiler is going to make one behind the scenes anyways, so you might as well have one out in the open where you know exactly what it is doing!  In this case however, we are actually going to have some code in our destructor later on.

 

 

Next we add a series of member functions. Their names are pretty self explanatory although we will go into more detail when we cover the implementation in the CPP file.  I put a space between the first four methods and the fifth DrawAll() method simply because I found it logically separate in purpose than the other 4 functions.  Beyond the way I think and organize things, there is no other reason for this separation.

 

 

Next up, in the private: section is the absolute heart and soul of GameObjectManager, our std::map data type.  The map is an overwhelmingly powerful data type that often gets overlooked as new developers are used to and comfortable with the vector class instead, which is a shame as they often spend so much time and effort making a vector do what a map already does!  A map is a collection of key/value pairs, which put into more plain English means each item in a map consists of an identifier and some kind of data.  In our case the identifier is a std::string (name) and the data is a VisibleGameObject pointer.  What makes a map particularly powerful is you can look up and retrieve values based on the key, you can sort by the key and you can guarantee uniqueness, as each key needs to be unique.  As you will see shortly it is a very powerful datatype, if a bit confusing in syntax.

 

 

Lastly is our struct GameObjectDeallocator.  I ask that you forget it exists for now. Smile

 

 

Optional information

 

What, no virtual?

Unlike when we declared VisibleGameObject, in GameObjectManager none of the member functions have the virtual keyword specified.  This is for a very good reason, we do not want anyone to inherit from GameObjectManager.  In the shortest summary possible, if you don’t want an object to be inherited from, don’t make any of its methods virtual.

 

 

Alright, now lets take a look at the implementation.  Create a new source file, GameObjectManager.cpp.  In that add the following ( somewhat scary looking code ):

 

 

1 #include "stdafx.h" 2 #include "GameObjectManager.h" 3 4 5 6 GameObjectManager::GameObjectManager() 7 { 8 } 9 10 GameObjectManager::~GameObjectManager() 11 { 12 std::for_each(_gameObjects.begin(),_gameObjects.end(),GameObjectDeallocator()); 13 } 14 15 void GameObjectManager::Add(std::string name, VisibleGameObject* gameObject) 16 { 17 _gameObjects.insert(std::pair<std::string,VisibleGameObject*>(name,gameObject)); 18 } 19 20 void GameObjectManager::Remove(std::string name) 21 { 22 std::map<std::string, VisibleGameObject*>::iterator results = _gameObjects.find(name); 23 if(results != _gameObjects.end() ) 24 { 25 delete results->second; 26 _gameObjects.erase(results); 27 } 28 } 29 30 VisibleGameObject* GameObjectManager::Get(std::string name) const 31 { 32 std::map<std::string, VisibleGameObject*>::const_iterator results = _gameObjects.find(name); 33 if(results == _gameObjects.end() ) 34 return NULL; 35 return results->second; 36 37 } 38 39 int GameObjectManager::GetObjectCount() const 40 { 41 return _gameObjects.size(); 42 } 43 44 45 void GameObjectManager::DrawAll(sf::RenderWindow& renderWindow) 46 { 47 std::map<std::string,VisibleGameObject*>::const_iterator itr = _gameObjects.begin(); 48 while(itr != _gameObjects.end()) 49 { 50 itr->second->Draw(renderWindow); 51 itr++; 52 } 53 }

Click here to download GameObjectManager.cpp

 

Alright, this is the first time we are going to deal directly with pointers, something I avoid like the plague.  So why then are we dealing with pointers here?  That is because we want the object that is pointed to to last beyond the scope of where it is created.  Currently ( in Pang4 ) our PlayerPaddle object is a static member of the Game class, so it lasts as long as the Game class exists and we don’t need to deal with such concepts as going out of scope.  Now that we are moving away from a static member variable to a dynamically generated variable, we need to start using pointers.  You may be thinking to yourself, why aren’t we using boost smart pointers?  The answer is, you should be!  In your implementation, if you feel comfortable, use a smart pointer!  In this case I am not because this is as much about education as it is about creating a great game and I felt some exposure to naked pointers was a good thing.

 

 

That said, this is one of those cases where the danger of naked pointers are mostly mitigated, as after initialization, GameObjectManager handles the entire life of a the pointer, including getting rid of it.  Of course this has a very important side effect, do not delete a pointer managed by GameObjectManager!  Alright, lets look at things in a bit more detail.

 

 

As I said earlier, the heart and soul of GameObjectManager is the std::map and I meant it.  The majority of the class is all about adding to, removing from and getting items within our std::map _gameObjects. So, obviously _gameObject is of some importance, lets look at it first.

 

 

A std::map is made up of a name/value pairs, internally it is composed of a collection of std::pair<> objects.  std::pair<> is an extremely straight forward object, its basically just two objects smushed together.  Really there isn’t anything more to it than that, other than the fact it makes our map declaration a bit long winded.  Lets look back at that for a second, _gameObjects was declared as:

 

std::map<std::string, VisibleGameObject*> _gameObjects;

 

 

This basically is saying we want a collection of VisibleGameObject pointers that we are going to access using a std::string as the identifier.  If it helps you to think of a file in a file folder in a physical real world filing cabinet, the VisibleGameObject (value) represents the contents of the folder, while the std::string (key) represents the label used to file it.  You can use the label to sort or access the folder, but normally it’s the contents you are interest in.  This is essentially what a map is.

 

 

Now with that explained, lets take a quick look at the functions, starting with Add():

 

void GameObjectManager::Add(std::string name, VisibleGameObject* gameObject) { _gameObjects.insert(std::pair<std::string,VisibleGameObject*>(name,gameObject)); }

 

Our map _gameObjects is literally composed of a collection of std::pair<> objects and in this case all we are doing is adding one more.

 

 

 

GameObjectManager::Get() is just a layer over our std::map, but introduces a new extremely important concept, the iterator.  An iterator is used to navigate a collection of results.  In the case of GameObjectManager::Get() results should only contain a single item.  Right away we check to see if the results iterator is equal to _gameObject.end() which is a special value like EOF ( end of file ) that indicates the iterator is at the end of the results and doesn’t point to anything valid.  Once we have verified results is actually pointing at something, we can go ahead and use it.  In this case, results is a std::pair<std::string,VisibleGameObject*> and std::pair exposes two simple values, first and second, which represent the two types that make up the pair.  In this case, we are interested in the VisibleGameObject* so we return results->second.

 

 

GameObjectManager::Remove() use almost exactly the same logic as Get() except it deletes the pointer to the VisibleGameObject* referred to in results->second.  Remember, GameObjectManager is in charge of the deletion of pointers stored within.  If you did not call delete here, you would leak memory.  GameObjectManager::GetObjectCount() is a straight wrapper around the map’s .size() function which simply returns the number of items contained.

 

 

Next lets look at GameObjectManager::DrawAll().  This method simply loops through all of the items stored within our map _gameObjects and calls the VisibleGameObject’s Draw method.  This removes the need to Draw() each item individually in Game.cpp.  The only new concepts in this code that Get() or Remove() is the fact there are multiple results.  In this case we assign the iterator itr to begin() which logically points to the first item.  Then after calling the VisibleGameObject*’s Draw() method, we ++ the iterator, which moves on to the next available item in the map.  Every single iterator, regardless to type, is guaranteed to have overloaded the ++ operator for navigating forward through a collection!  Bi-directional ( such as map is ) iterators will also support –- for navigating in reverse.

 

 

For more details on iterators, you may want to consult here.

 

 

Finally, lets look at a piece of code I skipped over earlier, the destructor:

GameObjectManager::~GameObjectManager() { std::for_each(_gameObjects.begin(),_gameObjects.end(),GameObjectDeallocator()); }

 

This little bit may take a bit to wrap your head around.  std::for_each is a handy little method that takes two iterators, one representing where to start, the other representing where to stop and finally it takes a unary function (meaning it takes a single parameter) that is to be called on each iteration.  For more details and examples check here.

 

 

In our case, we are using something called a functor for this third parameter.  Remember back in GameObjectManager.h I told you to ignore a certain bit of code for now… well its time to pay a bit more attention to it:

 

struct GameObjectDeallocator { void operator()(const std::pair<std::string,VisibleGameObject*> & p) const { delete p.second; } };

 

A functor is simply an object that can be called like a function.  This is facilitated by overloading the function operator ( also known as () ), like we have here.  The for_each call automatically passes the third parameter ( GameObjectDeallocator() ) the type the first two iterators point to, in this case std::pair<std::string,VisibleGameObject*> , so we override the () operator to accept that type.  Therefore each pass through the for_each loop, it creates/calls GameObjectDeallocator() passing it the currently pointed to value.  GameObjectDeallocator simple deletes the VisibleGameObject pointer.  Therefore when GameObjectManager is destroyed or goes out of scope, it automatically cleans up the memory of all the items it contains.  For more details on functors click here.

 

 

I know it is initially quite confusing, but I promise in time that it will make more sense, especially once you start using it more.  Now that we have our GameObjectManager, lets put it to use!  This means making a few changes to Game.h and Game.cpp.  Open up  Game.h and change it to look like the following:

1 #pragma once 2 #include "SFML/Window.hpp" 3 #include "SFML/Graphics.hpp" 4 #include "PlayerPaddle.h" 5 #include "GameObjectManager.h" 6 7 class Game 8 { 9 public: 10 static void Start(); 11 12 private: 13 static bool IsExiting(); 14 static void GameLoop(); 15 16 static void ShowSplashScreen(); 17 static void ShowMenu(); 18 19 enum GameState { Uninitialized, ShowingSplash, Paused, 20 ShowingMenu, Playing, Exiting }; 21 22 static GameState _gameState; 23 static sf::RenderWindow _mainWindow; 24 25 static GameObjectManager _gameObjectManager; 26 }; 27 28

Click here to download Game.h

 

Essentially we just got rid of the PlayerPaddle object and replaced it with our GameObjectManager on line 25.  We also added an include to our new header file, GameObjectManager.h.  Now we need to open up Game.cpp and make a couple changes as well:

 

 

1 #include "stdafx.h" 2 #include "Game.h" 3 #include "MainMenu.h" 4 #include "SplashScreen.h" 5 6 void Game::Start(void) 7 { 8 if(_gameState != Uninitialized) 9 return; 10 11 _mainWindow.Create(sf::VideoMode(1024,768,32),"Pang!"); 12 13 PlayerPaddle *player1 = new PlayerPaddle(); 14 player1->Load("images/paddle.png"); 15 player1->SetPosition((1024/2)-45,700); 16 17 _gameObjectManager.Add("Paddle1",player1); 18 _gameState= Game::ShowingSplash; 19 20 while(!IsExiting()) 21 { 22 GameLoop(); 23 } 24 25 _mainWindow.Close(); 26 } 27 28 bool Game::IsExiting() 29 { 30 if(_gameState == Game::Exiting) 31 return true; 32 else 33 return false; 34 } 35 36 void Game::GameLoop() 37 { 38 sf::Event currentEvent; 39 _mainWindow.GetEvent(currentEvent); 40 41 42 switch(_gameState) 43 { 44 case Game::ShowingMenu: 45 { 46 ShowMenu(); 47 break; 48 } 49 case Game::ShowingSplash: 50 { 51 ShowSplashScreen(); 52 break; 53 } 54 case Game::Playing: 55 { 56 _mainWindow.Clear(sf::Color(0,0,0)); 57 58 _gameObjectManager.DrawAll(_mainWindow); 59 60 _mainWindow.Display(); 61 if(currentEvent.Type == sf::Event::Closed) _gameState = Game::Exiting; 62 63 if(currentEvent.Type == sf::Event::KeyPressed) 64 { 65 if(currentEvent.Key.Code == sf::Key::Escape) ShowMenu(); 66 } 67 68 break; 69 } 70 } 71 } 72 73 void Game::ShowSplashScreen() 74 { 75 SplashScreen splashScreen; 76 splashScreen.Show(_mainWindow); 77 _gameState = Game::ShowingMenu; 78 } 79 80 void Game::ShowMenu() 81 { 82 MainMenu mainMenu; 83 MainMenu::MenuResult result = mainMenu.Show(_mainWindow); 84 switch(result) 85 { 86 case MainMenu::Exit: 87 _gameState = Exiting; 88 break; 89 case MainMenu::Play: 90 _gameState = Playing; 91 break; 92 } 93 } 94 95 Game::GameState Game::_gameState = Uninitialized; 96 sf::RenderWindow Game::_mainWindow; 97 GameObjectManager Game::_gameObjectManager;

Click here to download Game.cpp

 

 

Source examples are starting to get kinda big so I am enabling scroll bars, please let me know if this gets annoying and you would prefer I just past the entire source without using scrolling.

 

 

Anyways, we have made a number of changes to Game.cpp.  First of all, PlayerPaddle is no longer a static member variable of Game, but instead is now created dynamically then pased off to _gameObjectManager to manage using the .Add() function.  We can retrieve our players paddle later on by using the string key “Paddle1”.  Since PlayerPaddle is now a pointer, we now use the “->” operator to access it’s members instead of the “.” operator we used before.  All of these changes are on lines 13 through 17.

 

 

Additionally we changed the drawing code.  Instead of calling PlayerPaddle’s Draw() method, we instead call _gameObjectManager’s DrawAll() method, which will in turn call the Draw() method of every object it contains.  This change is represented on line 58.  Finally at the bottom we need to remove PlayerPaddle and add an instantiation for _gameObjectManager as you see on line 97.

 

 

 

That’s it.  Now when you run your application, well frankly… it does exactly the same thing it did before!  Just now, it is slightly more organized.  As I said earlier, this system is actually a bit of overkill for a simple pong game, but as your game adds more and more items, a container such as GameObjectManager is a godsend.  You will see in the future though that having an object manager is extremely useful in some regards!

 

 

Next part we will actually add some new functionality to our game!  We are going to get things moving, literally.

 

 

To download the entire project solution click here.



Back to Part Four Forward to Part Six




blog comments powered by Disqus


Month List

Popular Comments