Game Engine part 5

Whoa, it’s been a while since I’ve done one of these… didn’t mean for it to take this long, but here it is at last – part 5 of the Overdrive Assault engine tutorial. This one is about making a component architecture really – taking the stuff from the previous couple of tutorials, adding one little missing thing and wrapping it into, well, an application core really.

So this time the objective is to create a mini-framework that allows separate components to work together with a minimum of hassle. The first assumption I’m going to make is that most components need to be updated in some fashion – either poll-based or event-based. We have code for both these cases, but let’s start with the event-based stuff:

class System {
public:
    System() {}
    virtual ~System() {}
protected:
    EventChannel mChannel;
};

Ok, so now the System has easy access to the event system. It still needs a bit of extra stuff, for example initialization and shutdown functionality and some events that can be broadcasted upon executing said routines:

class System {
public:
    struct SystemInitializing {
        SystemInitializing(System* s): mSystem(s)    {}
        System* mSystem;
    };
    
    struct SystemShuttingDown {
        SystemShuttingDown(System* s): mSystem(s) {}
        System* mSystem;
    };
    
    System() {
    }
    
    virtual ~System() {
    }
    
    virtual bool init() {
        mChannel.broadcast(SystemInitializing(this));
        return true;
    }
    
    virtual void shutdown() {
        mChannel.broadcast(SystemShuttingDown(this));
    }
    
protected:
    EventChannel mChannel;
};

Nice, now the event system is pretty usable. Let’s add the poll-like update system now. Really, this comes down to making a task that updates the system:

class System {
    ...
    struct SystemUpdater: public Task {
        SystemUpdater(System* s, unsigned int taskFlags = SINGLETHREADED_REPEATING):
            mSystem(s), 
            Task(taskFlags) 
        {}

        virtual void run() {
            mSystem->update();
        }
        System* mSystem;
    };
    ...
    void enableUpdater(unsigned int taskFlags = SINGLETHREADED_REPEATING) {
	mSystemUpdater.reset(new SystemUpdater(this, taskFlags));
    }
    ...
    virtual void update() {
    }
    ...
protected:
    boost::shared_ptr mSystemUpdater;
};

Now this is somewhat incomplete, but it hints at the next piece that is needed to make this work – some kind of overarching class that actually has a task scheduler and can manage the subsystems: the Engine class 😀
The plan is to have a single engine instance keep a list of all active subsystems, with a task scheduler providing poll-based updating capabilities to all systems. As always, this is just one way to go about this, but I feel comfortable doing it in this manner.The event system is pretty much decoupled from everything else, so I don’t have to worry about that (too much). Also, it would be nice to have some way to access a specific system via the engine instance. Let’s get started with this:

class Engine {
public:
	typedef boost::shared_ptr SystemPtr;
	typedef std::list SystemList;
	
	Engine::Engine() {
	}
	
	Engine::~Engine() {
	}
	
	void Engine::run() {
		initializeSystems();
		mTaskManager.start();
		shutdownSystems();
	}
	
	void Engine::stop() {
		mChannel.broadcast(TaskManager::StopEvent());
	}
	
	void add(SystemPtr s) {
		if (s->mSystemUpdater.get() != NULL)
			mTaskManager.add(s->mSystemUpdater);
		mSystems.push_back(s);
	}
	
private:
	void initializeSystems() {
		for (SystemList::iterator it = mSystems.begin(); it != mSystems.end(); ++it)
			(*it)->init();
	}
	
	void shutdownSystems() {
		for (SystemList::iterator it = mSystems.rbegin(); it != mSystems.rend(); ++it)
			(*it)->shutdown();
	}
	
	SystemList mSystems;
	TaskManager mTaskManager;
	EventChannel mChannel;
};

This is actually getting somewhere… although I don’t really like how little feedback is given. Coincidentally, the subsystems themselves are a little… anonymous. We do have code that allows for logging properly, so let’s add that to the subsystems, combined with unique string names. That should make errors simpler to locate, which is a Good Thing™.

class System {
    ...
    System(const std::string& name): mName(name) {
    }
    ...
    const std::string& getName() const {
        return mName;
    }
    ...    
    virtual bool System::init() {
        mChannel.broadcast(SystemInitializing(this));
        
        mLog.setPrefix("[" + getName() + "] ");
        mLog.add(new std::ofstream(getName() + ".log"));
        
        return true;
    }
    ...
    Logger mLog;
    std::string mName;
};

Fantastic, now let’s add a bit of feedback to the Engine class, and make use of the new unique naming:

class Engine {
    typedef std::map SystemMap;
    ...
    void add(SystemPtr s) {
        if (mSystemMap.find(s->getName()) == mSystemMap.end()) {
            if (s->mSystemUpdater->get() != NULL)
                mTaskManager.add(s->mSystemUpdater);
            mSystemMap.insert(std::make_pair(s->getName(), s));
        }
        else
            gLog << "Engine::add - System already added: " << s->getName();
    }
    ...
    SystemPtr get(const std::string& name) const {
        SystemMap::const_iterator it = mSystemMap.find(name);
            
        if (it == mSystemMap.end()) {
            gLog << "Engine::get - Cannot find system: " << name;
            return SystemPtr;
        }
        
        return it->second;
    }
    ...
    void initializeSystems() {
        for (SystemMap::iterator it = mSystemMap.begin(); it != mSystemMap.end(); ++it) {
            gLog.info() << "Initializing " << it->second->getName();
            it->second->init();
        }
    }
    ...
    void shutdownSystems() {
        for (SystemMap::iterator it = mSystemMap.rbegin(); it != mSystemMap.rend(); ++it) {
            gLog.info() << "Shutting down " << it->second->getName();
            it->second->shutdown();
        }
    }
    ...
    SystemMap mSystemMap;
};

Phew… now the systems automatically provide *some* feedback on what’s happening during initialization and shutdown, there’s a guard in place that ensures unique naming and it is possible to get a system via it’s name. The last piece I’m going to add in today’s tutorial to this little architecture is ‘settings’ for each component. I find that usually only a couple of subsystems really require settings to be loaded dynamically, but even then it’s nice to have a dedicated system in place to do just that. Fortunately, boost provides us with a library that automates a lot of this (boost::program_options). Unfortunately, I find the syntax a bit of a mess, so I’m going to go ahead and wrap it into something that’s easy to use:

class Config: public System {
public:
    Config(): System("Config") {
    }
    
    virtual ~Config() {
    }
    
    bool load(const std::string& filename) {
        std::ifstream fs(filename.c_str());
            
        if (!fs) {
            mLog << "Failed to open config file: " << filename;
            return false;
        }
        else {
            boost::program_options::store(boost::program_options::parse_config_file(fs, mOptions, true), mVariables);
            boost::program_options::notify(mVariables);
            return true;
        }
    }
    
    void parseCommandLine(int argc, char* argv[]) {
        boost::program_options::store(boost::program_options::parse_command_line(argc, argv, mOptions), mVariables);
        boost::program_options::notify(mVariables);
    }
    
    bool isSet(const std::string& settingName) const {
        return (mVariables.count(settingName) > 0);
    }
    
    boost::program_options::options_description& settings() {
        return mOptions;
    }
        
    template 
    T get(const std::string& settingName) const {
        if (isSet(settingName))
            return mVariables[name.c_str()].as();
        else {
            mLog << "Failed to find variable: " << settingName;
            return T();
        }
    }
    
private:
    boost::program_options::options_description mOptions;
    boost::program_options::variables_map mVariables;
};

Now a bit more integration with the System architecture and we can integrate the brand new Config subsystem into the Engine as it's first default System. Right now, I'm making it so that each System has a list of settings, which are merged in the Config subsystem. There are some downsides to this, but at least it's not a whole lot of code, and actually works pretty conveniently.

class System {
    ...
    template 
    void addSetting(const std::string& nameInConfigFile, T* var) {
        std::stringstream sstr;
        sstr << getName() << "." << nameInConfigFile;
        mSettings.add_options()
            sstr.str().c_str(), boost::program_options::value(var))
        ;
    }
    
    ...
protected:
    boost::program_options::options_description mSettings;
};


class Engine {
public:
    ...
    Engine(): 
        mConfig(new Config()) 
    {
    }
    ...
    void Engine::run() {
        //merge all system settings into mConfig
        for (SystemMap::const_iterator it = mSystemMap.begin(); it != mSystemMap.end(); ++it)
            mConfig->settings().add(it->second->mSettings);
        
        std::cout << "Loading assault.cfg...";
        mConfig->load("assault.cfg");
        std::cout << "done." << std::endl;
            
        ...
    }
    ...
    boost::shared_ptr mConfig;
};

Great, now there's actually enough stuff to make a *real* application loop. Here is a small main to illustrate how stuff works right now:

class TestSystem: public System {
public:
    TestSystem(): 
        System("TestSystem")
    {
        enableUpdater();
        mSomeConfigSetting = 25;
        addSetting("SomeConfigSetting", &mSomeConfigSetting);
    }
    
    virutal ~TestSystem() {
    }
    
    virtual void update() {
        std::cout << "." << mSomeConfigSetting++;
            
        if (mSomeConfigSetting > 30)
            mChannel.broadcast(TaskManager::StopEvent());
    }
    
    unsigned int mSomeConfigSetting;
};

int main(int argc, char* argv[]) {
    boost::shared_ptr ts(new TestSystem());
    Engine engine;
    
    engine.add(ts);
    
    std::cout << "Running" << std::endl;
    engine.run();
    std::cout << "Done" << std::endl;
    
    return 0;
}

And this is what the config file 'assault.cfg' looks like at this point:

[TestSystem]
SomeConfigSetting = 3

The config layout is basically a single-level tree, pretty common as far as config files go. Actually, Horde3D prefers XML but I'm not really that comfortable with it yet. For now, this'll do just fine.

The code for this tutorial (to be used together with code from the last couple) can be downloaded [here].

Next time (hopefully next week), I'll make some more default systems and we'll transition from console application to an actual windowed application.

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.