- SFML Game Development
- Artur Moreira Henrik Vogelius Hansson Jan Haller
- 1123字
- 2021-08-13 17:11:12
Error handling
The basic steps are done, the main functionality is implemented. However, there may be errors which we have to recognize and handle meaningfully. The first error can occur during the loading of the texture. For example, the specified file might not exist, or the file might have an invalid image format, or be too big for the video memory of the graphics card. To handle such errors, the method sf::Texture::loadFromFile()
returns a Boolean value that is true in case of success, and false in case of failure.
There are several strategies to react to resource loading errors. In our case, we have to consider that the texture is later needed by sprites that are rendered on the screen—if such a sprite requests the texture, we must give something back. One possibility would be to provide a default texture (for example, plain white), so the sprites are just drawn as a white rectangle. However, we do not want the player of our game to fiddle around with rectangles; he should either have a proper airplane or nothing. But how can we implement "nothing"? We have to notify the caller of our load()
method that something did not work. A possibility to implement these notifications is shown in the next sections.
Boolean return values
We could follow SFML's philosophy and return a Boolean value denoting success or failure. This approach has some disadvantages. We cannot use the return type for something else. Additionally, the caller has to check the returned value every time he calls load()
. This is easily overlooked, and if it is not, it leads to messy usage code that is full of error checks. That is not what we want, as initially stated our objective consists of performing as much work as possible in the TextureHolder
, to relieve the user from writing boilerplate code.
Throwing exceptions
Another approach to react to a loading failure is to throw an exception. We choose the standard exception type std::runtime_error
. To its constructor, we pass an error message describing the problem as clearly as possible, including the filename:
if (!texture->loadFromFile(filename)) throw std::runtime_error("TextureHolder::load - Failed to load " + filename);
Exceptions have the big advantage that user code can be kept clean of error handling. Clients can now have the following code:
TextureHolder textures; textures.load(Textures::Landscape, "Media/Textures/Desert.png"); textures.load(Textures::Airplane, "Media/Textures/Airplane.png"); textures.load(Textures::Missile, "Media/Textures/Missile.png");
We do not need to check every single call. If an error occurs, an exception will be thrown until a try-catch
block catches it and reacts meaningfully. It is possible that the exception passes several functions before it is eventually handled.
Once the resource is loaded, we insert it into the map. Here, we have to be aware of possible error sources too. When the given ID is already stored, the map will refuse to insert our ID-resource pair, as it cannot contain duplicate keys. The member function std::map::insert()
returns a pair with an iterator to the inserted element and a Boolean value which is true if inserting was successful. We store this returned pair and check its second member (the Boolean value). Instead of writing std::pair<std::map<Textures::ID, std::unique_ptr<sf::Texture>>::iterator, bool>
, we can use C++11 type inference:
auto inserted = mTextureMap.insert(std::make_pair(id, std::move(resource)));
Now, inserted
is our pair containing an iterator and a Boolean value, inserted.second
is the Boolean value denoting the success of the insertion. If it is false, we know that the ID is already stored in the map. How do we react to this situation?
We could throw a std::runtime_error
exception again. However, in contrast to a loading failure, double insertion is not a runtime error. The attempt to insert the same ID twice in the map is a logic error, meaning, there is a mistake in the application logic—in other words, a bug. A well-formed program would not attempt to load the same resource twice. In comparison, runtime errors occur in correctly written programs too, for example, if the user renames or moves the resource files. For logic errors, the standard library provides the exception class std::logic_error
.
This raises already the next question: how do you handle such exceptions? It is not that once you have thrown an exception, you can forget about it and the world is in harmony. Somebody has to catch those exceptions, and it had better be you and not the operating system (unless you like crashing applications). In the case of a loading failure, we can tell the player that the files were not found, and prevent him from starting the game. But what in our double insertion case? Are we supposed to tell the player that the programmer accidentally called load()
twice? Certainly not. This bug must not occur in the final application. There is no way to recover from it—continuing the application is dangerous, because its logic is broken, and we risk upsetting even more if we ignore the error. What if the two load()
calls are passed the same ID, but different filenames? We do not know with which resource the ID is associated. If we later want to access a resource by its ID, we might get the wrong resource, and thus display a wrong image on the screen. In this manner, errors can propagate further and further, sometimes remaining for a long time before being noticed. In case of a logic error, we would like the program to interrupt immediately.
Assertions
Clearly, a mechanism apart from exceptions is appropriate, which shows us directly and inevitably when something goes wrong. This is where assertions come into play. The macro assert
evaluates its expression; if it is false in debug mode, a breakpoint is triggered, halting the program execution and directly pointing to the source of the error. In release mode, assertions are optimized away, so we do not waste any performance to check for errors that cannot occur. The assert
expression is completely removed in release mode, so make sure you only use it for error checks, and not to implement actual functionality with possible side effects.
We have to insert a single line, we expect that the Boolean member of the pair returned by std::map::insert()
is true:
assert(inserted.second);
That is already it. The whole method looks now as follows:
void TextureHolder::load(Textures::ID id, const std::string& filename) { std::unique_ptr<sf::Texture> texture(new sf::Texture()); if (!texture->loadFromFile(filename)) throw std::runtime_error("TextureHolder::load - Failed to load " + filename); auto inserted = mTextureMap.insert(std::make_pair(id, std::move(texture))); assert(inserted.second); }
In our method get()
, there are things that may go wrong too. The requested texture might not exist in the map. This is also a logic error; we are supposed to load the textures before we access them. Consequently, we verify whether the texture has been found, again using assert
:
sf::Texture& TextureHolder::get(Textures::ID id) { auto found = mTextureMap.find(id); assert(found != mTextureMap.end()); return *found->second; }
- C語言程序設(shè)計案例教程(第2版)
- Oracle Exadata性能優(yōu)化
- 軟件架構(gòu)設(shè)計:大型網(wǎng)站技術(shù)架構(gòu)與業(yè)務(wù)架構(gòu)融合之道
- Game Programming Using Qt Beginner's Guide
- Visual C++數(shù)字圖像模式識別技術(shù)詳解
- Mastering Scientific Computing with R
- 深入淺出RxJS
- Node.js全程實例
- HTML5與CSS3基礎(chǔ)教程(第8版)
- INSTANT Yii 1.1 Application Development Starter
- Scratch3.0趣味編程動手玩:比賽訓(xùn)練營
- Android移動開發(fā)案例教程:基于Android Studio開發(fā)環(huán)境
- Android群英傳
- 深入理解BootLoader
- Angular Design Patterns