Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
Mastering SFML Game Development

You're reading from   Mastering SFML Game Development Inject new life and light into your old SFML projects by advancing to the next level.

Arrow left icon
Product type Paperback
Published in Jan 2017
Publisher Packt
ISBN-13 9781786469885
Length 442 pages
Edition 1st Edition
Languages
Tools
Arrow right icon
Author (1):
Arrow left icon
Raimondas Pupius Raimondas Pupius
Author Profile Icon Raimondas Pupius
Raimondas Pupius
Arrow right icon
View More author details
Toc

Table of Contents (11) Chapters Close

Preface 1. Under the Hood - Setting up the Backend FREE CHAPTER 2. Its Game Time! - Designing the Project 3. Make It Rain! - Building a Particle System 4. Have Thy Gear Ready - Building Game Tools 5. Filling the Tool Belt - a few More Gadgets 6. Adding Some Finishing Touches - Using Shaders 7. One Step Forward, One Level Down - OpenGL Basics 8. Let There Be Light - An Introduction to Advanced Lighting 9. The Speed of Dark - Lighting and Shadows 10. A Chapter You Shouldnt Skip - Final Optimizations

Application states

Another important aspect of a more complex application is keeping track of and managing its states. Whether the player is in the thick of the game, or simply browsing through the main menu, we want it to be handled seamlessly, and more importantly, be self-contained. We can start this by first defining different types of states we'll be dealing with:

enum class StateType { Intro = 1, MainMenu, Game, Loading }; 

For seamless integration, we want each state to behave in a predictable manner. This means that a state has to adhere to an interface we provide:

class BaseState{ 
friend class StateManager; 
public: 
  BaseState(StateManager* l_stateManager)  
    :m_stateMgr(l_stateManager), m_transparent(false), 
    m_transcendent(false){} 
  virtual ~BaseState(){} 
 
  virtual void OnCreate() = 0; 
  virtual void OnDestroy() = 0; 
 
  virtual void Activate() = 0; 
  virtual void Deactivate() = 0; 
 
  virtual void Update(const sf::Time& l_time) = 0; 
  virtual void Draw() = 0; 
  ... 
  sf::View& GetView(){ return m_view; } 
  StateManager* GetStateManager(){ return m_stateMgr; } 
protected: 
  StateManager* m_stateMgr; 
  bool m_transparent; 
  bool m_transcendent; 
  sf::View m_view; 
}; 

Every state in the game will have its own view that it can alter. In addition to that, it is given the hooks to implement logic for various different scenarios, such as the state's creation, destruction, activation, deactivation, updating, and rendering. Lastly, it enables the possibility of being blended with other states during updating and rendering, by providing the m_transparent and m_transcendent flags.

Managing these states is pretty straightforward:

class StateManager{ 
public: 
  StateManager(SharedContext* l_shared); 
  ~StateManager(); 
  void Update(const sf::Time& l_time); 
  void Draw(); 
  void ProcessRequests(); 
  SharedContext* GetContext(); 
  bool HasState(const StateType& l_type) const; 
  StateType GetNextToLast() const; 
  void SwitchTo(const StateType& l_type); 
  void Remove(const StateType& l_type); 
  template<class T> 
  T* GetState(const StateType& l_type){ ... } 
  template<class T> 
  void RegisterState(const StateType& l_type) { ... } 
  void AddDependent(StateDependent* l_dependent); 
  void RemoveDependent(StateDependent* l_dependent); 
private: 
  ... 
  State_Loading* m_loading; 
  StateDependents m_dependents; 
}; 

The StateManager class is one of the few classes in the project that utilizes the shared context, since the states themselves may need access to any part of the code base. It also uses the factory pattern to dynamically create any state that is bound to a state type during runtime.

In order to keep things simple, we're going to be treating the loading state as a special case, and only allow one instance of it to be alive at all times. Loading might happen during the transition of any state, so it only makes sense.

One final thing that's worth noting about the state manager is it's keeping a list of state dependants. It's simply an STL container of classes that inherit from this interface:

class StateDependent { 
public: 
  StateDependent() : m_currentState((StateType)0){} 
  virtual ~StateDependent(){} 
  virtual void CreateState(const StateType& l_state){} 
  virtual void ChangeState(const StateType& l_state) = 0; 
  virtual void RemoveState(const StateType& l_state) = 0; 
protected: 
  void SetState(const StateType& l_state){m_currentState=l_state;} 
  StateType m_currentState; 
}; 

Because classes that deal with things such as sounds, GUI elements, or entity management need to support different states, they must also define what happens inside them as a state is created, changed, or removed, in order to properly allocate/de-allocate resources, stop updating data that is not in the same state, and so on.

Loading state

So, how exactly are we going to implement this loading state? Well, for flexibility and easy progress tracking by means of rendering fancy loading bars, threads are going to prove invaluable. Data that needs to be loaded into memory can be loaded in a separate thread, while the loading state itself continues to get updated and rendered in order to show us that things are indeed happening. Just knowing that the application did not hang on us should create a warm and fuzzy feeling.

First, let us implement the very basics of this system by providing an interface any threaded worker can use:

class Worker { 
public: 
  Worker() : m_thread(&Worker::Work, this), m_done(false), 
    m_started(false) {} 
  void Begin() { 
    if(m_done || m_started) { return; } 
    m_started = true; 
    m_thread.launch(); 
  } 
  bool IsDone() const { return m_done; } 
  bool HasStarted() const { return m_started; } 
protected: 
  void Done() { m_done = true; } 
  virtual void Work() = 0; 
  sf::Thread m_thread; 
  bool m_done; 
  bool m_started; 
}; 

It has its own thread, which is bound to the pure virtual method called Work. The thread is launched whenever the Begin() method is invoked. In order to protect the data from being accessed from multiple threads at once, a sf::Mutex class is used by creating a lock during sensitive calls. Everything else within this very basic class is simply there to provide information to the outside world about the worker’s state.

File loader

With threads out of the way, we can focus on actually loading some files now. This method is going to focus on working with text files. However, using binary formats should work in pretty much the exact same way, minus all the text processing.

Let's take a look at the base class for any file loading class we can think of:

using LoaderPaths = std::vector<std::pair<std::string, size_t>>; 
 
class FileLoader : public Worker { 
public: 
  FileLoader(); 
  void AddFile(const std::string& l_file);
  virtual void SaveToFile(const std::string& l_file);
  
  size_t GetTotalLines() const; 
  size_t GetCurrentLine() const; 
protected: 
  virtual bool ProcessLine(std::stringstream& l_stream) = 0; 
  virtual void ResetForNextFile(); 
  void Work(); 
  void CountFileLines(); 
 
  LoaderPaths m_files; 
  size_t m_totalLines; 
  size_t m_currentLine; 
}; 

It's a distinct possibility that two or more files may need to be loaded at some point. The FileLoader class keeps track of all of the paths that get added to it, along with a number that represents the number of lines within that file. This is useful for determining the amount of progress that has been made while loading. In addition to the line count for each individual file, a total line count is also kept track of.

This class provides a single purely virtual method, called ProcessLine. It will be the way derivatives can define exactly how the file is loaded and processed.

First, let us get the basic stuff out of the way:

FileLoader::FileLoader() : m_totalLines(0), m_currentLine(0) {}
void FileLoader::AddFile(const std::string& l_file) {
  m_files.emplace_back(l_file, 0);
}
size_t FileLoader::GetTotalLines()const {
  sf::Lock lock(m_mutex);
  return m_totalLines;
}
size_t FileLoader::GetCurrentLine()const {
  sf::Lock lock(m_mutex);
  return m_currentLine;
}
void FileLoader::SaveToFile(const std::string& l_file) {}
void FileLoader::ResetForNextFile(){}

The ResetForNextFile() virtual method is optional to implement, but can be used in order to clear the state of some internal data that needs to exist while a file is being loaded. Since file loaders that implement this class will only have the ability to process one line at a time inside a single method, any temporary data that would normally be stored as a local variable within that method would instead need to go somewhere else. This is why we must make sure that there is actually a way to know when we're done with one file and start loading another, as well as to perform some sort of action, if necessary.

Note

Note the mutex locks in the two getter methods above. They’re there to make sure those variables aren’t written to and read from at the same time.

Now, let's get into the code that is going to be executed in a different thread:

void FileLoader::Work() { 
  CountFileLines(); 
  if (!m_totalLines) { Done(); return; } 
  for (auto& path : m_files) { 
    ResetForNextFile(); 
    std::ifstream file(path.first); 
    std::string line; 
    std::string name; 
    auto linesLeft = path.second; 
    while (std::getline(file, line)) { 
      { 
        sf::Lock lock(m_mutex); 
        ++m_currentLine; 
        --linesLeft; 
      } 
      if (line[0] == '|') { continue; } 
      std::stringstream keystream(line); 
      if (!ProcessLine(keystream)) { 
        std::cout << 
          "File loader terminated due to an internal error." 
          << std::endl; 
        { 
          sf::Lock lock(m_mutex); 
          m_currentLine += linesLeft; 
        } 
        break; 
      } 
    } 
    file.close(); 
  } 
  Done(); 
} 

A private method for counting all the lines in whatever files are about to be loaded is called first. If, for any reason, the total line count is zero, there is no purpose in proceeding, so the Worker::Done() method is invoked just before a return. This little bit of code is really easy to forget, but is extremely important in order for this to work. All it does is set the m_done flag of the Worker base class to true, which lets outside code know that the process is finished. Since there is currently no way to check if an SFML thread is actually finished, this is pretty much the only option.

We begin looping through different files that need to get loaded and invoke the reset method before work begins. Note the lack of checking as we're attempting to open a file. This will be explained when we cover the next method.

As each line of the file is being read, it's important to make sure that all the line count information is updated. A temporary lock for the current thread is established, in order to prevent two threads from accessing the line count as its modified. In addition to that, lines that start with a pipe symbol are excluded, since this is our standard comment pragma.

Finally, a stringstream object is constructed for the current line, and passed into the ProcessLine() method. For extra points, it returns a boolean value that can signal an error and stop the current file from being processed any further. If that happens, the remaining lines within that specific file are added to the total count, and the loop is broken.

The final piece of the puzzle is this chunk of code, responsible for verifying file validity and determining the amount of work ahead of us:

void FileLoader::CountFileLines() {
  m_totalLines = 0;
  m_currentLine = 0;
  for (auto path = m_files.begin(); path != m_files.end();) {
    if (path->first.empty()) { m_files.erase(path); continue; }
    std::ifstream file(path->first);
    if (!file.is_open()) {
      std::cerr << “Failed to load file: “ << path->first
        << std::endl;
      m_files.erase(path);
      continue;
    }
    file.unsetf(std::ios_base::skipws);
    {
      sf::Lock lock(m_mutex);
      path->second = static_cast<size_t>(std::count(
        std::istreambuf_iterator<char>(file),
        std::istreambuf_iterator<char>(), ‘\n’));
      m_totalLines += path->second;
    }
    ++path;
    file.close();
  }
}

After initial zero values for line counts are set up, all added paths are iterated over and checked. We first trim out any paths that are empty. Each path is then attempted to be opened, and erased if that operation fails. Finally, in order to achieve accurate results, the file input stream is ordered to ignore empty lines. After a lock is established, std::count is used to count the amount of lines in a file. That number is then added to the amount of total lines we have, the path iterator is advanced, and the file is properly closed.

Since this method eliminates files that were either non-existent or unable to be opened, there is no reason to check for that again anywhere else.

Implementing the loading state

Everything is now in place in order for us to successfully implement the loading state:

using LoaderContainer = std::vector<FileLoader*>; 
 
class State_Loading : public BaseState { 
public: 
  ... 
  void AddLoader(FileLoader* l_loader); 
  bool HasWork() const; 
  void SetManualContinue(bool l_continue); 
  void Proceed(EventDetails* l_details); 
private: 
  void UpdateText(const std::string& l_text, float l_percentage); 
  float CalculatePercentage(); 
  LoaderContainer m_loaders; 
  sf::Text m_text; 
  sf::RectangleShape m_rect; 
  unsigned short m_percentage; 
  size_t m_originalWork; 
  bool m_manualContinue; 
}; 

The state itself will keep a vector of pointers to different file loader classes, which will have lists of their own files respectively. It also provides a way for these objects to be added. Also, note the Proceed() method. This is another call-back that will be used in the event manager we're about to cover soon.

For the visual portion, we will be using the bare essentials of graphics: a bit of text for the progress percentage, and a rectangle shape that represents a loading bar.

Let's take a look at all of the setup this class will do once it's constructed:

void State_Loading::OnCreate() { 
  auto context = m_stateMgr->GetContext(); 
  context->m_fontManager->RequireResource("Main"); 
  m_text.setFont(*context->m_fontManager->GetResource("Main")); 
  m_text.setCharacterSize(14); 
  m_text.setStyle(sf::Text::Bold); 
 
  sf::Vector2u windowSize = m_stateMgr->GetContext()-> 
    m_wind->GetRenderWindow()->getSize(); 
 
  m_rect.setFillColor(sf::Color(0, 150, 0, 255)); 
  m_rect.setSize(sf::Vector2f(0.f, 32.f)); 
  m_rect.setOrigin(0.f, 16.f); 
  m_rect.setPosition(0.f, windowSize.y / 2.f); 
 
  EventManager* evMgr = m_stateMgr->GetContext()->m_eventManager; 
  evMgr->AddCallback(StateType::Loading, "Key_Space", 
    &State_Loading::Proceed, this); 
} 

First, a font manager is obtained through the shared context. The font with a name "Main" is required and used to set up the text instance. After all of the visual bits are set up, the event manager is used to register a call-back for the loading state. This will be covered soon, but it's quite easy to deduce what's happening by simply looking at the arguments. Whenever the spacebar is pressed, the Proceed method of the State_Loading class is going to be invoked. The actual instance of the class is passed in as the last argument.

Remember that, by design, the resources we require must also be released. A perfect place to do that for the loading state is exactly as it is destroyed:

void State_Loading::OnDestroy() { 
  auto context = m_stateMgr->GetContext(); 
  EventManager* evMgr = context->m_eventManager; 
  evMgr->RemoveCallback(StateType::Loading, "Key_Space"); 
  context->m_fontManager->ReleaseResource("Main"); 
} 

In addition to the font being released, the call-back for the spacebar is also removed.

Next, let us actually write some code that's going to bring the pieces together into a cohesive, functional whole:

void State_Loading::Update(const sf::Time& l_time) 
  if (m_loaders.empty()) {
    if (!m_manualContinue) { Proceed(nullptr); }
    return;
  }
  auto windowSize = m_stateMgr->GetContext()->
    m_wind->GetRenderWindow()->getSize();
  if (m_loaders.back()->IsDone()) {
    m_loaders.back()->OnRemove();
    m_loaders.pop_back();
    if (m_loaders.empty()) {
      m_rect.setSize(sf::Vector2f(
        static_cast<float>(windowSize.x), 16.f));
      UpdateText(".Press space to continue.", 100.f);
      return;
    }
  }
  if (!m_loaders.back()->HasStarted()) {
    m_loaders.back()->Begin();
  }

  auto percentage = CalculatePercentage();
  UpdateText("", percentage);
  m_rect.setSize(sf::Vector2f(
    (windowSize.x / 100) * percentage, 16.f));
}

The first check is used to determine if all of the file loaders have been removed from the vector due to finishing. The m_manualContinue flag is used to let the loading state know if it should wait for the spacebar to be pressed, or if it should just dispel itself automatically. If, however, we still have some loaders in the vector, the top one is checked for having concluded its work. Given that's the case, the loader is popped and the vector is checked again for being empty, which would require us to update the loading text to represent completion.

To keep this process fully automated, we need to make sure that after the top file loader is removed, the next one is started, which is where the following check comes in. Finally, the progress percentage is calculated, and the loading text is updated to represent that value, just before the loading bar's size is adjusted to visually aid us.

Drawing is going to be extremely straightforward for this state:

void State_Loading::Draw() { 
  sf::RenderWindow* wind = m_stateMgr->GetContext()-> 
    m_wind->GetRenderWindow(); 
  wind->draw(m_rect); 
  wind->draw(m_text); 
} 

The render window is first obtained through the shared context, and then used to draw the text and rectangle shape that represent the loading bar together.

The Proceed call-back method is equally straightforward:

void State_Loading::Proceed(EventDetails* l_details){ 
  if (!m_loaders.empty()) { return; } 
  m_stateMgr->SwitchTo(m_stateMgr->GetNextToLast()); 
} 

It has to make a check first, to make sure that we don't switch states before all the work is through. If that's not the case, the state manager is used to switch to a state that was created before the loading commenced.

All of the other loading state logic pretty much consists of single lines of code for each method:

void State_Loading::AddLoader(FileLoader* l_loader) {
 m_loaders.emplace_back(l_loader);
  l_loader->OnAdd();
}
bool State_Loading::HasWork() const { return !m_loaders.empty(); }
void State_Loading::SetManualContinue(bool l_continue) {
  m_manualContinue = l_continue;
}
void State_Loading::Activate(){m_originalWork = m_loaders.size();}

Although this looks fairly simple, the Activate() method holds a fairly important role. Since the loading state is treated as a special case here, one thing has to be kept in mind: it is never going to be removed before the application is closed. This means that every time we want to use it again, some things have to be reset. In this case, it's the m_originalWork data member, that's simply the count of all the loader classes. This number is used to calculate the progress percentage accurately, and the best place to reset it is inside the method, which gets called every time the state is activated again.

You have been reading a chapter from
Mastering SFML Game Development
Published in: Jan 2017
Publisher: Packt
ISBN-13: 9781786469885
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime
Banner background image