Book Image

Mastering SFML Game Development

By : Raimondas Pupius
Book Image

Mastering SFML Game Development

By: Raimondas Pupius

Overview of this book

SFML is a cross-platform software development library written in C++ with bindings available for many programming languages. It provides a simple interface to the various components of your PC, to ease the development of games and multimedia applications. This book will help you become an expert of SFML by using all of its features to its full potential. It begins by going over some of the foundational code necessary in order to make our RPG project run. By the end of chapter 3, we will have successfully picked up and deployed a fast and efficient particle system that makes the game look much more ‘alive’. Throughout the next couple of chapters, you will be successfully editing the game maps with ease, all thanks to the custom tools we’re going to be building. From this point on, it’s all about making the game look good. After being introduced to the use of shaders and raw OpenGL, you will be guided through implementing dynamic scene lighting, the use of normal and specular maps, and dynamic soft shadows. However, no project is complete without being optimized first. The very last chapter will wrap up our project by making it lightning fast and efficient.
Table of Contents (17 chapters)
Mastering SFML Game Development
Credits
About the Author
About the Reviewer
www.PacktPub.com
Customer Feedback
Preface

Common utility functions


Let's start by taking a look at a common function, which is going to be used to determine the full absolute path to the directory our executable is in. Unfortunately, there is no unified way of doing this across all platforms, so we're going to have to implement a version of this utility function for each one, starting with Windows:

#ifdef RUNNING_WINDOWS 
#define WIN32_LEAN_AND_MEAN 
#include <windows.h> 
#include <Shlwapi.h> 

First, we check if the RUNNING_WINDOWS macro is defined. This is the basic technique that can be used to actually let the rest of the code base know which OS it's running on. Next, another definition is made, specifically for the Windows header files we're including. It greatly reduces the number of other headers that get included in the process.

With all of the necessary headers for the Windows OS included, let us take a look at how the actual function can be implemented:

inline std::string GetWorkingDirectory() 
{ 
   HMODULE hModule = GetModuleHandle(nullptr); 
   if (!hModule) { return ""; } 
   char path[256]; 
   GetModuleFileName(hModule,path,sizeof(path)); 
   PathRemoveFileSpec(path); 
   strcat_s(path,""); 
   return std::string(path); 
} 

First, we obtain the handle to the process that was created by our executable file. After the temporary path buffer is constructed and filled with the path string, the name, and extension of our executable is removed. We top it off by adding a trailing slash to the end of the path and returning it as a std::string.

It will also come in handy to have a way of obtaining a list of files inside a specified directory:

inline std::vector<std::string> GetFileList( 
   const std::string& l_directory, 
   const std::string& l_search = "*.*") 
{ 
   std::vector<std::string> files; 
   if(l_search.empty()) { return files; } 
   std::string path = l_directory + l_search; 
   WIN32_FIND_DATA data; 
   HANDLE found = FindFirstFile(path.c_str(), &data); 
   if (found == INVALID_HANDLE_VALUE) { return files; } 
   do{ 
       if (!(data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) 
       { 
          files.emplace_back(data.cFileName); 
       } 
     }while (FindNextFile(found, &data)); 
   FindClose(found); 
   return files; 
} 

Just like the directory function, this is specific to the Windows OS. It returns a vector of strings that represent file names and extensions. Once one is constructed, a path string is cobbled together. The l_search argument is provided with a default value, in case one is not specified. All files are listed by default.

After creating a structure that will hold our search data, we pass it to another Windows specific function that will find the very first file inside a directory. The rest of the work is done inside a do-while loop, which checks if the located item isn't in fact a directory. The appropriate items are then pushed into a vector, which gets returned later on.

The Linux version

As mentioned previously, both of the preceding functions are only functional on Windows. In order to add support for systems running Linux-based OSes, we're going to need to implement them differently. Let's start by including proper header files:

#elif defined RUNNING_LINUX 
#include <unistd.h> 
#include <dirent.h> 

As luck would have it, Linux does offer a single-call solution to finding exactly where our executable is located:

inline std::string GetWorkingDirectory() 
{ 
   char cwd[1024]; 
   if(!getcwd(cwd, sizeof(cwd))){ return ""; } 
   return std::string(cwd) + std::string("/"); 
} 

Note that we're still adding a trailing slash to the end.

Obtaining a file list of a specific directory is slightly more complicated this time around:

inline std::vector<std::string> GetFileList( 
   const std::string& l_directory, 
   const std::string& l_search = "*.*") 
{ 
   std::vector<std::string> files; 
    
   DIR *dpdf; 
   dpdf = opendir(l_directory.c_str()); 
   if (!dpdf) { return files; } 
   if(l_search.empty()) { return files; } 
   std::string search = l_search; 
   if (search[0] == '*') { search.erase(search.begin()); } 
   if (search[search.length() - 1] == '*') { search.pop_back(); } 
  struct dirent *epdf; 
  while (epdf = readdir(dpdf)) { 
    std::string name = epdf->d_name; 
    if (epdf->d_type == DT_DIR) { continue; } 
    if (l_search != "*.*") { 
      if (name.length() < search.length()) { continue; } 
      if (search[0] == '.') { 
        if (name.compare(name.length() - search.length(), 
          search.length(), search) != 0) 
        { continue; } 
      } else if (name.find(search) == std::string::npos) { 
        continue; 
      } 
    } 
    files.emplace_back(name); 
  } 
  closedir(dpdf); 
  return files; 
} 

We start off in the same fashion as before, by creating a vector of strings. A pointer to the directory stream is then obtained through the opendir() function. Provided it isn't NULL, we begin modifying the search string. Unlike the fancier Windows alternative, we can't just pass a search string into a function and let the OS do all of the matching. In this case, it falls more under the category of matching a specific search string inside a filename that gets returned, so star symbols that mean anything need to be trimmed out.

Next, we utilize the readdir() function inside a while loop that's going to return a pointer to directory entry structures one by one. We also want to exclude any directories from the file list, so the entry's type is checked for not being equal to DT_DIR.

Finally, the string matching begins. Presuming we're not just looking for any file with any extension (represented by "*.*"), the entry's name will be compared to the search string by length first. If the length of the string we're searching is longer than the filename itself, it's safe to assume we don't have a match. Otherwise, the search string is analyzed again to determine whether the filename is important for a positive match. Its first character being a period would denote that it isn't, so the file name's ending segment of the same length as the search string is compared to the search string itself. If, however, the name is important, we simply search the filename for the search string.

Once the procedure is complete, the directory is closed and the vector of strings representing files is returned.

Other miscellaneous helper functions

Sometimes, as text files are being read, it's nice to grab a string that includes spaces while still maintaining a whitespace delimiter. In cases like that, we can use quotes along with this special function that helps us read the entire quoted segment from a whitespace delimited file:

inline void ReadQuotedString(std::stringstream& l_stream, 
  std::string& l_string) 
{ 
  l_stream >> l_string; 
  if (l_string.at(0) == '"'){ 
    while (l_string.at(l_string.length() - 1) != '"' || 
      !l_stream.eof()) 
    { 
      std::string str; 
      l_stream >> str; 
      l_string.append(" " + str); 
    } 
  } 
  l_string.erase(std::remove( 
    l_string.begin(), l_string.end(), '"'), l_string.end()); 
} 

The first segment of the stream is fed into the argument string. If it does indeed start with a double quote, a while loop is initiated to append to said string until it ends with another double quote, or until the stream reaches the end. Lastly, all double quotes from the string are erased, giving us the final result.

Interpolation is another useful tool in a programmer's belt. Imagine having two different values of something at two different points in time, and then wanting to predict what the value would be somewhere in between those two time frames. This simple calculation makes that possible:

template<class T> 
inline T Interpolate(float tBegin, float tEnd, 
   const T& begin_val, const T& end_val, float tX) 
{ 
   return static_cast<T>(( 
      ((end_val - begin_val) / (tEnd - tBegin)) * 
      (tX - tBegin)) + begin_val); 
} 

Next, let's take a look at a few functions that can help us center instances of sf::Text better:

inline float GetSFMLTextMaxHeight(const sf::Text& l_text) { 
  auto charSize = l_text.getCharacterSize(); 
  auto font = l_text.getFont(); 
  auto string = l_text.getString().toAnsiString(); 
  bool bold = (l_text.getStyle() & sf::Text::Bold); 
  float max = 0.f; 
  for (size_t i = 0; i < string.length(); ++i) { 
    sf::Uint32 character = string[i]; 
    auto glyph = font->getGlyph(character, charSize, bold); 
    auto height = glyph.bounds.height; 
    if (height <= max) { continue; } 
    max = height; 
  } 
  return max; 
} 
 
inline void CenterSFMLText(sf::Text& l_text) { 
  sf::FloatRect rect = l_text.getLocalBounds(); 
  auto maxHeight = Utils::GetSFMLTextMaxHeight(l_text); 
  l_text.setOrigin( 
    rect.left + (rect.width * 0.5f), 
    rect.top + ((maxHeight >= rect.height ? 
      maxHeight * 0.5f : rect.height * 0.5f))); 
} 

Working with SFML text can be tricky sometimes, especially when centering it is of paramount importance. Some characters, depending on the font and other different attributes, can actually exceed the height of the bounding box that surrounds the sf::Text instance. To combat that, the first function iterates through every single character of a specific text instance and fetches the font glyph used to represent it. Its height is then checked and kept track of, so that the maximum height of the entire text can be determined and returned.

The second function can be used for setting the absolute center of a sf::Text instance as its origin, in order to achieve perfect results. After its local bounding box is obtained and the maximum height is calculated, this information is used to move the original point of our text to its center.

Generating random numbers

Most games out there rely on some level of randomness. While it may be tempting to simply use the classical approach of rand(), it can only take you so far. Generating random negative or floating point numbers isn't straightforward, to say the least, plus it has a very lousy range. Luckily, newer versions of C++ provide the answer in the form of uniform distributions and random number engines:

#include <random> 
#include <SFML/System/Mutex.hpp> 
#include <SFML/System/Lock.hpp> 
 
class RandomGenerator { 
public: 
  RandomGenerator() : m_engine(m_device()){} 
  ... 
  float operator()(float l_min, float l_max) { 
    return Generate(l_min, l_max); 
  } 
  int operator()(int l_min, int l_max) { 
    return Generate(l_min, l_max); 
  } 
private: 
  std::random_device m_device; 
  std::mt19937 m_engine; 
  std::uniform_int_distribution<int> m_intDistribution; 
  std::uniform_real_distribution<float> m_floatDistribution; 
  sf::Mutex m_mutex; 
}; 

First, note the include statements. The random library provides us with everything we need as far as number generation goes. On top of that, we're also going to be using SFML's mutexes and locks, in order to prevent a huge mess in case our code is being accessed by several separate threads.

The std::random_device class is a random number generator that is used to seed the engine, which will be used for further generations. The engine itself is based on the Marsenne Twister algorithm, and produces high-quality random unsigned integers that can later be filtered through a uniform distribution object in order to obtain a number that falls within a specific range. Ideally, since it is quite expensive to keep constructing and destroying these objects, we're going to want to keep a single copy of this class around. For this very reason, we have integer and float distributions together in the same class.

For convenience, the parenthesis operators are overloaded to take in ranges of numbers of both integer and floating point types. They invoke the Generate method, which is also overloaded to handle both data types:

int Generate(int l_min, int l_max) { 
  sf::Lock lock(m_mutex); 
  if (l_min > l_max) { std::swap(l_min, l_max); } 
  if (l_min != m_intDistribution.min() || 
    l_max != m_intDistribution.max()) 
  { 
    m_intDistribution = 
      std::uniform_int_distribution<int>(l_min, l_max); 
  } 
  return m_intDistribution(m_engine); 
} 
 
float Generate(float l_min, float l_max) { 
  sf::Lock lock(m_mutex); 
  if (l_min > l_max) { std::swap(l_min, l_max); } 
  if (l_min != m_floatDistribution.min() || 
    l_max != m_floatDistribution.max()) 
  { 
    m_floatDistribution = 
      std::uniform_real_distribution<float>(l_min, l_max); 
  } 
  return m_floatDistribution(m_engine); 
} 

Before generation can begin, we must establish a lock in order to be thread-safe. Because the order of l_min and l_max values matters, we must check if the provided values aren't in reverse, and swap them if they are. Also, the uniform distribution object has to be reconstructed if a different range needs to be used, so a check for that is in place as well. Finally, after all of that trouble, we're ready to return the random number by utilizing the parenthesis operator of a distribution, to which the engine instance is fed in.