- SFML Game Development
- Artur Moreira Henrik Vogelius Hansson Jan Haller
- 1115字
- 2021-08-13 17:11:11
An automated approach
Our goal is to encapsulate the just mentioned functionality into a class that relieves us from managing resources again and again. For resource management, the C++ idiom Resource Acquisition Is Initialization (RAII) comes in handy.
Note
RAII describes the principle that resources are acquired in a class' constructor and released in its destructor. Since both constructor and destructor are invoked automatically when the object is created or goes out of scope, there is no need to track resources manually. RAII is mostly used for automatic memory management (as in smart pointers), but it can be applied to any kind of resources. A great advantage of RAII over manual allocation and deallocation (such as new
/delete
pairs) is that deallocation is guaranteed to take place, even when there are multiple return statements or exceptions in a function. To achieve the same safety with manual memory management, every possible path would have to be protected with a delete
operator. As a result, the code becomes quickly unreadable and error-prone.
In our application, we want to take advantage of RAII to determine the construction (loading) and destruction (release) of SFML resource objects.
Let's begin with a class that holds sf::Texture
objects and loads them from files. We call it TextureHolder
. Once we have implemented the semantics for textures, we can generalize the implementation to work with other resource types.
Finding an appropriate container
First, we must find the right data structure to store the textures. We ought to choose an STL container that does not perform unnecessary copies. std::vector
is the wrong choice, since inserting new textures can trigger a reallocation of the dynamic array and the copying of all textures. Not only is this slow, but also all references and pointers to the textures are invalidated. As mentioned before, we like to access the textures by an enum, so the associative container std::map
looks like the perfect choice. The key type is our enumeration, the value type is the sf::Texture
.
Note
The C++11 standard introduces strongly typed enumerations, also known as enum class. Unlike traditional enums, they do not offer implicit conversion to integers, and their enumerators reside in the scope of the enum type itself. Since C++11 is still being implemented by compiler vendors, not all features are widely supported yet. In this book, we focus on C++11 features that have already been implemented for a few years. Unfortunately, strongly typed enums do not fall into this category, that's why we do not use them in the book. If they are supported by your compiler, we still recommend using them.
We call our enum as ID
, and let it contain three texture identifiers Landscape
, Airplane
, and Missile
. We nest it into a namespace Textures
. The namespace gives us a scope for the enumerators. Instead of writing just Airplane
, we have Textures::Airplane
which clearly describes the intention and avoids possible name collisions in the global scope:
namespace Textures { enum ID { Landscape, Airplane, Missile }; }
We do not store the sf::Texture
directly, but we wrap it into a std::unique_ptr
.
Note
Unique pointers are class templates that act like pointers. They automatically call the delete
operator in their destructor, thus they provide means of RAII for pointers. They support C++11 move semantics, which allow to transfer ownership between objects without copying. A std::unique_ptr<T>
instance is the sole owner of the T
object it points to, hence the name "unique".
Unique pointers give us a lot of flexibility; we can basically pass around heavyweight objects without creating copies. In particular, we can store classes that are non-copyable, such as, sf::Shader
. Our class then looks as shown in the following code:
class TextureHolder { private: std::map<Textures::ID, std::unique_ptr<sf::Texture>> mTextureMap; };
The compiler-generated default constructor is fine, our map is initially empty. Same for the destructor, std::map
and std::unique_ptr
take care of the proper deallocation, so we do not need to define our own destructor.
Loading from files
What we have to write now is a member function to load a resource. It has to take a parameter for the filename and one for the identifier in the map:
void load(Textures::ID id, const std::string& filename);
In the function definition, we first create a sf::Texture
object and store it in the unique pointer. Then, we load the texture from the given filename. After loading, we can insert the texture to the map mTextureMap
. Here, we use std::move()
to take ownership from the variable texture
and transfer it as an argument to std::make_pair()
, which constructs a key-value pair for the map:
void TextureHolder::load(Textures::ID id, const std::string& filename) { std::unique_ptr<sf::Texture> texture(new sf::Texture()); texture->loadFromFile(filename); mTextureMap.insert(std::make_pair(id, std::move(texture))); }
Accessing the textures
So far, we have seen how to load resources. Now we finally want to use them. We write a method get()
that returns a reference to a texture. The method has one parameter, namely the identifier for the resource. The method signature looks as follows:
sf::Texture& get(Textures::ID id);
Concerning the implementation, there is not much to do. We perform a lookup in the map to find the corresponding texture entry for the passed key. The method std::map::find()
returns an iterator to the found element, or end()
if nothing is found. Since the iterator points to a std::pair<const Textures::ID, std::unique_ptr<sf::Texture>>
, we have to access its second member to get the unique pointer, and dereference it to get the texture:
sf::Texture& TextureHolder::get(Textures::ID id) { auto found = mTextureMap.find(id); return *found->second; }
Note
Type inference is a language feature that has been introduced with C++11, which allows the compiler to find out the type of expressions. The decltype
keyword returns the type of an expression, while the auto
keyword deduces the correct type at initialization. Type inference is very useful for complex types such as iterators, where the syntactic details of the declaration are irrelevant. In the following code, all three lines are semantically equivalent:
int a = 7; decltype(7) a = 7; // decltype(7) is int auto a = 7; // auto is deduced as int
In order to be able to invoke get()
also, if we only have a pointer or reference to a const TextureHolder
at hand, we need to provide a const-qualified overload. This new member function returns a reference to a const sf::Texture
, therefore the caller cannot change the texture. The signature is slightly different:
const sf::Texture& get(Textures::ID id) const;
The implementation stays the same, so it is not listed again. Our class now looks as follows:
class TextureHolder { public: void load(Textures::ID id,const std::string& filename); sf::Texture& get(Textures::ID id); const sf::Texture& get(Textures::ID id) const; private: std::map<Textures::ID,std::unique_ptr<sf::Texture>> mTextureMap; };
Now the get()
method is easy to use and can directly be invoked when a texture is requested:
TextureHolder textures; textures.load(Textures::Airplane, "Media/Textures/Airplane.png"); sf::Sprite playerPlane; playerPlane.setTexture(textures.get(Textures::Airplane));
- Unity 2020 By Example
- Cocos2d Cross-Platform Game Development Cookbook(Second Edition)
- 大學計算機基礎(第三版)
- Mastering Objectoriented Python
- x86匯編語言:從實模式到保護模式(第2版)
- Animate CC二維動畫設計與制作(微課版)
- 薛定宇教授大講堂(卷Ⅳ):MATLAB最優化計算
- 移動界面(Web/App)Photoshop UI設計十全大補
- Java EE核心技術與應用
- 區塊鏈技術進階與實戰(第2版)
- Julia 1.0 Programming Complete Reference Guide
- 微信小程序開發實戰:設計·運營·變現(圖解案例版)
- 創意UI Photoshop玩轉移動UI設計
- Hands-On Robotics Programming with C++
- Keil Cx51 V7.0單片機高級語言編程與μVision2應用實踐