On the back of my recent raycasting hurdle-crossing, I've shifted focus to another of the high priority items from last year's roadmap post: a refactor of file IO (input/output) handling - the bits of code that handle importing and exporting scenes, game objects, game configuration, and user configuration files.
This update is going to be pretty dry/programming oriented and won't have any exciting screenshots. Sorry for anybody who's not into that!
As with the raycasting thing, most of the engine's file IO code was written very early in development with the aim of getting something up and running quickly and with the expectation that it'd need to be replaced with something more bespoke once I had a clear picture of what my needs were.
The result was file IO code spread across a few different places. I consolidated a lot of this with a ResourceManager class that I implemented a couple of years ago, but it was still a couple of big lists of "else if" statements that more or less did the same thing and required a bunch of chances in several places in order to modify or add anything.
That said, the ResourceManager provides a bunch of important functionality and was work I needed to do regardless. It allows the main engine class to not know anything about where data comes from by allowing it to call loadObject() or saveObject() functions - this is important and worthwhile for keeping things simple and easy to read in the main engine class, but leaves me with code for loading and saving an object's colour (as an example) in different functions spread across the ResourceManager's files.
Since Icicle is a project that's a little bit bigger than just In the Snowy Winter's Wake (and since it's good practice anyway), I wanted to come up with a solution that allowed me to not only centralise all this stuff and group it based on what it was related to, but also to make sure that that stuff could be versioned in a way that let new versions of the engine load files made with/for older versions of the engine. In addition to a way to bring definitions for saving/loading/etc. a particular file's properties in one place, I also needed a way to have more than one definition for each thing without making something crazy complex.
The solution I came up with was inspired a little bit by the save handling implementation in the "Remonkeyed" engine (used by Day of the Tentacle Remastered and Full Throttle Remastered), which keeps a struct for every save version, and uses that for packing/unpacking binary save data.
Since Icicle's data files are plain text and contain lots of fields with variable length strings, that approach feels like it's not a good fit (for example, I'd have to put hard upper limits on how long an object description could be, and that amount of space would need to be allocated/padded out in each save file even for short descriptions). I instead created base classes for each file (at the moment, .scene and .object files, but this same system will be used for game.rc, icicle.rc, and .save files) that look something like this:
static std::string getProperty();
virtual std::string exportData(GameObject* object) = 0;
virtual bool readData(GameObject* object, std::string line, std::ifstream& input) = 0;
This is extended by property-specific classes that look like this (there's a little extra code that I've removed to keep things simple, but this should give the right idea). There are still length limits on many properties (descriptions can be as long as they need to be though), but the code within exportData() and readData() is now something I can modify in one place and have that take effect everywhere, so addressing that sort of stuff is now a lot easier.
class SceneTitle01 : public FileVersionSceneProp
static std::string getProperty()
return "tt ";
std::string exportData(Scene* scene)
exportLine = '\0';
sprintf(exportLine, (getProperty() + "%s").c_str(), scene->stateList[*stateIndex]->title.c_str());
bool readData(Scene* scene, std::string line, std::ifstream& input)
These derived classes (that's what we call classes that extend another class) are then thrown together into a map (a data type that stores key/value pairs) with whatever's returned by getProperty() being the key for that property class' instance.
Let's say that in the future, there'll be a 0.2 version of the .scene file that has a different format for titles (there definitely will be - the "tt" identifier is definitely going to change). I'll write a SceneTitle02 class that also extends FileVersionSceneProp, and that'll go into another map that's specifically for version 0.2 definitions. The previous 0.1 map will still exist, and if the title definition is the only thing that's changed, the version 0.2 map can be filled with 0.1 definitions for everything else, meaning I don't have to write any duplicate code. Huzzah!
This has been stuff I've been thinking about for the past year as well, but a workable solution that I like really only came together within the past few days.
I wanted to get this stuff done sooner rather than later because it makes a lot of changes that I want to do easier/less work in the long run. For example, the raycasting changes open up the possibility of doing animated object colours (let's say that the Healer's fire flickers between orange and red), but without this new file IO stuff, adding save/load code for that would be a lot more awkward.
I'm also planning to put information on how in-editor input should be parsed into the same place to further reduce how spread out relevant code is, and that will make maintaining the editor tools a lot simpler as well.
Hope that's been an interesting read. As with the raycasting thing, this opens up a lot of doors, and makes me pretty excited to move forward! :D