I am currently creating a game in SFML using C++ and I'm wondering: what is good practise for a location for textures? Should I store it with my project? Or my executable? Or even in something like the Documents folder? What would be the most efficient when the game would theoretically be released, as it will not simply include the project but rather a compiled and build version of it?
问题:
回答1:
Most common game releases have their textures inside a Media folder or similar. Inside that folder are also placed sounds, music, and other content, tipically into separate folders.
They can't be part of the executable (as far as I know). More important is how do you manage those textures inside your code, it should be an efficient way. I've added an explanation about how to do that, if you're interested.
TL DR
From my own experience making some small videogames, I found better to use a Resource Holder. This is a generic container for any heavy resource (textures, music, sounds or even fonts).
The main idea behind this is to have a map which relates a key (an ID) with a resource. As you may want to store diferent kinds of resources, it's better to make a generic class.
A basic implementation:
template <typename Resource, typename Identifier>
class ResourceHolder
{
public:
void load(Identifier id, const std::string& filename){
// Create and load resource
std::unique_ptr<Resource> resource(new Resource());
if (!resource->loadFromFile(filename))
throw std::runtime_error("ResourceHolder::load - Failed to load " + filename);
// If loading successful, insert resource to map
insertResource(id, std::move(resource));
}
Resource& get(Identifier id){
auto found = mResourceMap.find(id);
assert(found != mResourceMap.end());
return *found->second;
}
const Resource& get(Identifier id) const {
auto found = mResourceMap.find(id);
assert(found != mResourceMap.end());
return *found->second;
}
protected:
void insertResource(Identifier id, std::unique_ptr<Resource> resource){
// Insert and check success
auto inserted = mResourceMap.insert(std::make_pair(id, std::move(resource)));
assert(inserted.second);
}
protected:
std::map<Identifier, std::unique_ptr<Resource>> mResourceMap;
};
I normally prefer to keep separate .hpp
and .cpp
, but I merged them to avoid a (even) longer post.
To keep things clean and useful, it's a good practice to have Resource Identifier header file, where you can declare types for your resource holders, and your resource identifiers too.
// Forward declaration of SFML classes
namespace sf
{
class Texture;
// If you need, you can use other SFML classes into your holders the same way
//class Font;
//class SoundBuffer;
}
namespace Textures
{
enum ID
{
TitleScreen,
LoadingScreen,
GameOverScreen,
Title,
Controls,
GUI,
TileMap,
Player,
Enemy,
Key,
PlayerMods
};
}
// Forward declaration and a few type definitions
template <typename Resource, typename Identifier>
class ResourceHolder;
typedef ResourceHolder<sf::Texture, Textures::ID> TextureHolder;
//typedef ResourceHolder<sf::Font, Fonts::ID> FontHolder;
//typedef ResourceHolder<sf::SoundBuffer, Sounds::ID> SoundHolder;
As an example of use, if you have something like a Game
class (a class that will be loaded as long as your application is running), you can do like this:
class Game {
public:
Game() :
_window(sf::VideoMode(WINDOW_WIDTH, WINDOW_HEIGHT), "Game")
{
// EXAMPLES
//_fonts.load(Fonts::Main, FONTS_FOLDER + "font.ttf");
//_musics.load(Musics::Game, MUSIC_FOLDER + "main.ogg");
//_musics.get(Musics::Game).setLoop(true);
//_sounds.load(Sounds::Key, SOUNDS_FOLDER + "key.wav");
_textures.load(Textures::TitleScreen, TEXTURES_FOLDER + "titlescreen.png");
// More code ...
}
void run(){
// Your game loop: process inputs, update and render until you close
}
private:
void update(sf::Time dt){
// ...
}
void processInput(){
// ...
}
void render(){
_window.clear(sf::Color::Black);
// Here you can use your resources to draw
sf::Sprite sp(_textures.get(Textures::TitleScreen));
_window.draw(sp);
_window.display();
}
sf::RenderWindow _window;
TextureHolder _textures;
//FontHolder _fonts;
//SoundHolder _sounds;
};
The key with this approach is to have your holders inside an always loaded class, and pass your holders as pointers or references. Another good way to do that is to have a Context
class, which holds and group those pointers into only one class, and use that context as a parameter (even by copy, because it's a light class) of all your classes that will need a resource:
struct Context
{
Context(sf::RenderWindow& window, TextureHolder& textures, FontHolder& fonts, MusicHolder& musics, SoundHolder& sounds);
sf::RenderWindow* window;
TextureHolder* textures;
FontHolder* fonts;
MusicHolder* musics;
SoundHolder* sounds;
};
You can find more info about this here: SFML Game Development, source of this implementation.