This week heralds the addition of a loading screen. Partly due to a thread here on gamedev.net i decided that perhaps now was the time to add one, as shown below.
This actually turned out to be quite a challenge, due to the way i had designed my game.
At the point where the assets are loaded, the game loop is not yet running, and nearly none of the subsystems are initialised. This makes it harder than it otherwise would be to display and represent a loading screen, and leaves me grappling with a slightly alien environment without all my event driven conveniences.
I decided to approach this as follows:
Firstly, I had to adjust my sprite cache, to allow pre-caching specific named items at startup without enumerating the entire directory (which contains hundreds of files) and support for streaming objects on request:
/** * The SpriteCache class is a container object which loads and stores the Sprite2D * objects, which can be referenced by a string key. SpriteCache is accessed via the * read only [] operator, e.g. sprites["name"]. */class SpriteCache{private: FireworkFactory* creator; std::unordered_map, std::hash> sprites; Sprite2D* Get(ResId id);public: SpriteCache(); SpriteCache(const SpriteCache ©); SpriteCache(FireworkFactory* creator, const std::wstring &directory); SpriteCache(FireworkFactory* creator, const std::wstring &directory, std::vector preload); void CacheDirectory(const std::wstring &directory); void Stream(const std::wstring &directory, const std::wstring &file); ~SpriteCache(); Sprite2D* operator[] (ResId id); void ReleaseAll();};
This can then be called when starting up the game to load a select small number of 2D sprites for the loading screen only. The loading screen is made up of several pieces, a bottom part of the bomb, the top part of the bomb (there are various versions of this showing different amounts of fuse remaining), the text saying "Loading..." and the spark, which is alpha blended into the scene. When we initialise the sprite cache, we load only the first "top portion" of the bomb with a complete fuse, and as further sprites are needed they are streamed on request:
sprites = SpriteCache( this, L"Sprites", { L"bomb-bottom.png", L"bomb-spark.png", L"bomb-top-1.png", L"bomb-top-overlay.png", L"loading.png" } );
I then created a class called LoadingScreen (more on this later), and i wrapped creation and destruction of the LoadingScreen object around my resource loading calls:
loader = new LoadingScreen(this, Util::ReadDirectory(L"Textures/*.dds", true).size() // Number of textures to load + Util::ReadDirectory(L"Sprites/*.png", true).size() // Number of sprites to load + Util::ReadDirectory(L"sfx/*.wav", true).size() // Number of sound effects to load ); /* Load assets here... */ delete loader; loader = nullptr;
The second parameter to LoadingScreen is simply a figure used to calculate how many steps should build the progress meter. For this, we do a total count of all sound effects, sprites, and textures in the game by enumerating the directories, and using only the size of the returned vector in-place.
The LoadingScreen class looks like this:
/* Represets the loading screen of the game. * Note that when this is instantiated, the game loop is not yet active. * To draw and update the loading screen, you must call the Draw() or * Increment() method which renders the current frame to the render target. * The loading screen requires only DirectX and Direct2D to be initialised. * Deleting the object triggers the explosion effect. This is will block until * complete. */class LoadingScreen{private: /* Creator object */ FireworkFactory* creator; /* Sprites (pre-loaded by SpriteCache) */ Sprite2D* bomb_bottom; Sprite2D* bomb_spark; Sprite2D* top_overlay; Sprite2D* loadingtext; /* Streamed sprites (loaded on request by SpriteCache) */ std::vector bomb_top; /* Sprite positions */ D2D1_RECT_F bomb_position; D2D1_RECT_F text_position; D2D1_POINT_2F spark_position; /* Number of completed steps */ int done; /* Total number of steps */ int steps; /* Last "top of bomb" sprite. When this changes, * we request a new sprite be streamed from disk. */ int last_top; /* Waypoint coordinates for positions of the spark along the fuse. * There are actually 235 of these, each contains an X and Y * coordinate making 470 values. */ static const double waypoints[470];public: /* Create a loading screen which expects 'steps' increments */ LoadingScreen(FireworkFactory* creator, int steps); /* dtor automatically plays the explosion animation */ ~LoadingScreen(); /* Draw current frame of loading screen at done/steps*100 percent */ void Draw(); /* Add completed steps to the animation */ void Increment(int step);};
As you can see, there is a Draw() method, and an Increment() method. The Draw() method is called directly by the constructor once everything is set up, and by the Increment() method. Different subsystems call the Increment() method as they load assets, increasing the number of steps completed, and redrawing the screen with the new progress:
sprites[crc32] = std::make_shared(new Sprite2D(creator->GetDX()->D2D->GetRenderTarget(), *f)); if (creator->GetLoader()) creator->GetLoader()->Increment(+1);
An important feature of the loading screen is that when its destructor is called, it will play the 'explosion' animation, which blocks for 100 frames while it updates. This then leaves the game in a state ready for the backbuffer to be cleared and the game proper to be launched.
You might also be asking yourselves "so if no subsystems are properly initialised when the loading screen is being displayed, where do the sounds come from?" - The answer to this is simple. Because we cannot guarantee that the sound system is initialised, or that any sound effect assets are loaded, the simple sound effect for the fuse is embedded into the executable as a win32 custom resource type. We use the WinMM PlaySound() API call to play the embedded wav file:
PlaySound(TEXT("IDW_FUSE"), GetModuleHandle(NULL), SND_RESOURCE | SND_ASYNC);
Of course, by the time the explosion is ready to be displayed, we can be sure the sound system and all the sound effects are ready for use, so we stop the PlaySound call, and use FMOD to play the explosion sound:
/* Stop the fuse sound */ PlaySound(NULL, NULL, SND_FILENAME); creator->GetSFX()->PlayEffect(SFX_LOADING_BOMB);
That about sums up the trickery and deception that is my loading screen. As always, comments are welcome below :)
Very cool! I added a loading screen to my game just last week. I definitely needed it, because at this point I'm loading assets that actually need a few seconds to load initially! My animation isn't as bomb as yours, though (a bar fills up). Oh, no pun intended! :)
It's good to have an animation at the beginning so the players will know that the game hasn't frozen or anything.