- Game Development Patterns and Best Practices
- John P. Doran Matt Casanova
- 891字
- 2021-07-02 23:43:46
Templatizing Singletons
Now, assuming we get our Singleton working just the way that we want it to, you may wish to create more Singletons in the future. You could create them all from scratch, but a better thing to do is instead create a consistent approach, creating templates and inheritance to create a single implementation that you can use for any class. At the same time, we can also learn about an alternative way of creating a Singleton class, which will look something like the following:
template <typename T>
class Singleton
{
public:
Singleton()
{
// Set our instance variable when we are created
if (instance == nullptr)
{
instance = static_cast<T*>(this);
}
else
{
// If instance already exists, we have a problem
printf("\nError: Trying to create more than one Singleton");
}
}
// Once destroyed, remove access to instance
virtual ~Singleton()
{
instance = nullptr;
}
// Get a reference to our instance
static T & GetInstance()
{
return *instance;
}
// Creates an instance of our instance
static void CreateInstance()
{
new T();
}
// Deletes the instance, needs to be called or resource leak
static void RemoveInstance()
{
delete instance;
}
private:
// Note, needs to be a declaration
static T * instance;
};
template <typename T> T * Singleton<T>::instance = nullptr;
You'll notice that most of the differences have to do with the class itself. The very first line in our code above uses the template keyword which tells the compiler that we are creating a template, and typename T tells the compiler that, when we create a new object using this, the type T will be replaced with whatever the class we want it to be based on is.
I also want to point out the use of a static cast to convert our Singleton pointer to a T. static_cast is used in code generally when you want to reverse an implicit conversion. It's important to note that static_cast performs no runtime checks for if it's correct or not. This should be used if you know that you refer to an object of a specific type, and thus a check would be unnecessary. In our case, it is safe because we will be casting from a Singleton object to the type that we've derived from it (T).
Of course, it may be useful to see an example of this being used, so let's create an example of a class that we could use as a Singleton, perhaps something to manage the high scores for our game:
class HighScoreManager : public Singleton<HighScoreManager>
{
public:
void CheckHighScore(int score);
private:
int highScore;
};
Notice here that, when we declare our HighScoreManager class, we say that it's derived from the Singleton class and, in turn, we pass the HighScoreManager class to the Singleton template. This pattern is known as the curiously recurring template pattern.
For more information on the curiously recurring template pattern, check out https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern.
After defining the class, let's go ahead and add in an example implementation for the function we've created for this class:
void HighScoreManager::CheckHighScore(int score)
{
std::string toDisplay;
if (highScore < score)
{
highScore = score;
toDisplay = "\nNew High Score: " + std::to_string(score);
printf(toDisplay.c_str());
}
else
{
toDisplay = "\nCurrent High Score: " + std::to_string(highScore);
printf(toDisplay.c_str());
}
}
By using the templatized version of our class, we don't need to create the same materials as in the preceding class. We can just focus on the stuff that is particular to what this class needs to do. In this case, it's checking our current high score, and setting it to whatever we pass in if we happen to beat it.
Of course, it's great to see our code in action, and in this case I used the SplashStage class, which is located in the Mach5 EngineTest project, under SpaceShooter/Stages/SplashStage.cpp. To do so, I added the following bolded lines to the Init function:
void SplashStage::Init(void)
{
//This code will only show in the console if it is active and you
//are in debug mode.
M5DEBUG_PRINT("This is a demo of the different things you can do\n");
M5DEBUG_PRINT("in the Mach 5 Engine. Play with the demo but you must\n");
M5DEBUG_PRINT("also inspect the code and comments.\n\n");
M5DEBUG_PRINT("If you find errors, report to lazersquad@gmail.com");
HighScoreManager::CreateInstance();
HighScoreManager::GetInstance().CheckHighScore(10);
HighScoreManager::GetInstance().CheckHighScore(100);
HighScoreManager::GetInstance().CheckHighScore(50);
//Create ini reader and starting vars
M5IniFile iniFile;
// etc. etc.
In this case, our instance has been created by us creating a new HighScoreManager. If that is not done, then our project could potentially crash when calling GetInstance, so it's very important to call it. Then call our CheckHighScore functions a number of times to verify that the functionality works correctly. Then, in the Shutdown function, add the following bolded line to make sure the Singleton is removed correctly:
void SplashStage::Shutdown(void)
{
HighScoreManager::RemoveInstance();
M5ObjectManager::DestroyAllObjects();
}
With all of that gone, go ahead, save the file, and run the game. The output will be as follows:

As you can see, our code works correctly!
Note that this has the same disadvantages we discussed with our initial version of the script, with the fact that we have to manually create the object and remove it; but it takes away a lot of the busy work when creating a number of Singletons in your project. If you're going to be creating a number of them in your project, this could be a good method to look into.
- 從零開始:數字圖像處理的編程基礎與應用
- 自制編譯器
- Scala Design Patterns
- Ray分布式機器學習:利用Ray進行大模型的數據處理、訓練、推理和部署
- 假如C語言是我發明的:講給孩子聽的大師編程課
- PLC應用技術(三菱FX2N系列)
- 運維前線:一線運維專家的運維方法、技巧與實踐
- 貫通Tomcat開發
- PHP 8從入門到精通(視頻教學版)
- Python預測之美:數據分析與算法實戰(雙色)
- Mobile Forensics:Advanced Investigative Strategies
- 百萬在線:大型游戲服務端開發
- Clojure Web Development Essentials
- Mastering Machine Learning with scikit-learn
- Django 2.0 入門與實踐