2

I have a game engine built with SFML and I was attempting to create a loading screen which is updated while assets are loaded. In this case it is one big XML file.

I wanted to use a thread as this process took about ten seconds to complete and therefore the application would be stuck until it is completed.

I decided to use the thread and detach so that the AssetManager's LoadSpriteSheets method could be called. (Essentially loads an XML sheet and initialises them as sprites)

With the game state declaring a struct it is passed through the states class to give any state access to the struct. Which in returns give access to the AssetManager.

struct GameData
{
    StateMachine machine;
    sf::RenderWindow window;
    AssetManager assets;
    InputManager input;
};

In the initalise section of the SplashStage the thread is called

std::thread t1(&AssetManager::LoadSpriteSheets, this->_data->assets);
t1.detach();

As you can see, my idea was to pass the AssetManager (this->_data->assets) to the thread and call its LoadSpriteSheets Method. It does do this but it creates a new instance of the AssetManager resulting in my later polling of the GetStatus Method to result in 0. (GetStatus essentially gets the progress of the LoadSpriteSheets Method) I was going to use that to display an informative loading bar while waiting for the application to load.

Is there a way can I call a thread on the AssetManager without initialising a new Object? Or is a rewrite required as the struct is a shared_ptr.

Feel free to correct me also if you see a better solution to what I am trying to achieve.

Relevant Classes:

#include "SplashState.h"
#include <iostream>
#include <sstream>
#include <thread>

#include "AssetManager.h"

#include "Definitions.h"
SplashState::SplashState(GameDataRef data) : _data(data)
{

}

void SplashState::Init()
{
    this->_data->assets.LoadTexture("Splash State Background", SPLASH_STATE_BACKGROUND_FILEPATH);
    _background.setTexture(this->_data->assets.GetTexture("Splash State Background"));



    std::thread t1(&AssetManager::LoadSpriteSheets, this->_data->assets);
    t1.detach();


    std::cout << "Completed In Splash" << std::endl;
    /*_test.setTexture(this->_data->assets.GetTexture("spaceShips_001.png"));
    _test.setPosition((SCREEN_WIDTH / 2) - (_test.getGlobalBounds().width / 2), _test.getGlobalBounds().height / 2);*/

    //std::thread t2(&SplashState::LoadXML, this);

    /*_test.setTexture(this->_data->assets.GetTexture("spaceAstronauts_001.png"));
    _test.setPosition((SCREEN_WIDTH / 2) - (_test.getGlobalBounds().width / 2), _test.getGlobalBounds().height / 2);*/
}

Game.cpp

#pragma once
#include <memory>
#include <string>
#include <SFML\Graphics.hpp>
#include "StateMachine.h"
#include "AssetManager.h"
#include "InputManager.h"

struct GameData
{
    StateMachine machine;
    sf::RenderWindow window;
    AssetManager assets;
    InputManager input;
};

typedef std::shared_ptr<GameData> GameDataRef;

class Game
{
public:
    Game(int width, int height, std::string title);

private: 
    const float dt = 1.0f / 60.0f;
    sf::Clock _clock;

    GameDataRef _data = std::make_shared<GameData>();

    void Run();
};

#include "Game.h"
#include "SplashState.h"

Game::Game(int width, int height, std::string title)
{
    _data->window.create(sf::VideoMode(width, height), title, sf::Style::Close | sf::Style::Titlebar);
    _data->machine.AddState(StateRef(new SplashState(this->_data)));
    this->Run();
}
connormcwood
  • 323
  • 2
  • 12

2 Answers2

1

As AssetManager::LoadSpriteSheets has not been provided, I am not entirely sure, but it appears that you are invoking the AssetManager::LoadSpriteSheets member function on a copy of the AssetManager instance, whereas you want to invoke the AssetManager::LoadSpriteSheets member function on the this->_data->assets instance.

See the comments to this post and this comment. When constructing a thread that should run a member function on a particular object, you need to pass a pointer, reference wrapper, or shared_ptr to the object. One way to accomplish this is to use std::ref():

#include <functional>
#include <iostream>
#include <thread>
#include <utility>

struct AssetManager {
    AssetManager() { }
    virtual ~AssetManager() { }

    void LoadSpriteSheets() {
        std::cout << "(in thread): assets = " << static_cast<const void*>(this) << "\n";
    }
};

struct GameData
{
    AssetManager assets;
};

typedef std::shared_ptr<GameData> GameDataRef;

int main() {
    GameDataRef data = std::make_shared<GameData>();

    std::cout << "data->assets = " << static_cast<const void*>(&data->assets) << "\n";

    std::thread t1(&AssetManager::LoadSpriteSheets, std::ref(data->assets));
    t1.join();

    std::cout << "thread finished.\n";

    std::cout << "data->assets = " << static_cast<const void*>(&data->assets) << "\n";

    return 0;
}

This will output something like:

data->assets = 0x7fb942402808
(in thread): assets = 0x7fb942402808
thread finished.
data->assets = 0x7fb942402808

Without std::ref(), the output is something like:

data->assets = 0x7fb5bcc02808
(in thread): assets = 0x7fb5bcc02868
thread finished.
data->assets = 0x7fb5bcc02808

(notice that the AssetManager instance is different within the thread)

Be careful to make sure that the AssetManager object exists throughout the invocation of the thread.

Daniel Trebbien
  • 38,421
  • 18
  • 121
  • 193
  • 1
    You can't pass a reference, but as you correctly pointed out you can pass a `std::reference_wrapper`. This is since `std::thread`s constructor takes it's arguments by value. – super Feb 28 '18 at 22:45
  • 1
    @Daniel Thanks Daniel, std::ref() is what I was missing here. Thank you very much for taking the time to answer this question. – connormcwood Feb 28 '18 at 22:57
  • @super Yes, you are right. I have corrected my answer. – Daniel Trebbien Feb 28 '18 at 23:42
0

Personally I'd try to avoid multithreading wherever possible, especially when it's related to OpenGL, since in the end most things have to be synced again in some way, which is causing overhead not directly visible to you (and your typical loading screen shouldn't be too elaborate anyway).

The approach I typically use is a specific loading state that will continue drawing on screen and try to load (or download!) as many assets as possible between screen updates.

This is from memory and basically just to show the concept:

sf::Clock timer;
std::queue<std::string> assets;
std::size_t numAssets = 0;

// to queue some file for loading/processing
assets.push("/path/to/my/file.xml");
++numAssets;

// inside the main loop/state update
timer.restart();
do {
    loadMyAsset(assets.front());
    assets.pop();
} while (!assets.empty() && mTimer.getEllapsedTime() < sf::milliseconds(10))

// drawing
const std::size_t progress = 100 - 100 * assets.size() / numAssets;
Mario
  • 35,726
  • 5
  • 62
  • 78