Devlog #004
Posted by Kenneth Ellersdorfer
Hello capitalists!
Testing has begun, and I'm busy fixing issues, stabilizing and rebalancing the game!
Testing
The game has now entered the internal testing phase ๐.
I've been inviting friends who enjoy management/business simulation games, along with other developers I'm acquainted with. The testing process is straightforward: we have a designated internal tester section on our Discord server where builds are shared and feedback can be provided. Testers can report bugs, offer feedback, and make feature requests there, and I transfer them to my Google document, which serves as my current to-do list.
Thus far, I've received feedback, generally positive, which is fantastic for maintaining motivation to enhance and expand the game. Additionally, various bugs and game design flaws were reported and have since been resolved. As the game advances, the plan is to gradually invite more individuals to participate in the testing process. However, at present, the tester group remains relatively small, comprising only six people.
In terms of my personal journey, witnessing the game (and its engine!) that I've dedicated years to finally reach a stage where it can engage players is truly remarkable. It's an immensely gratifying experience and serves as powerful motivation for me. Since the start of the testing I've pushed more than 30 changes spanning bugfixes, new features, game mechanic changes and optimization.
Savegames
Recently, I've made the decision to enhance the savegame feature to facilitate the seamless transition of the game to a new version. To comprehend the challenges encountered during this implementation, it's necessary to delve into the previous methodology:
Previously, I had a serialization system in place within the engine, responsible for serializing assets into binary data. The serialization process functioned (roughly) as follows:
public override void Serialize(BinaryDataSerializer stream)
{
stream.Serialize(ref someIntegerValue);
stream.Serialize(ref someFloatValue);
}
The BinaryDataSerializer class invoked the Serialize() method either once for serialization or multiple times for deserialization (deserialize, reference resolving and post load - but that's not really important here). It then directly read from or wrote to a binary data stream as bytes.
The drawback of this approach was that any modification, addition, removal, or rearrangement within Serialize() would cause the entire process to fail. Consequently, I couldn't load older savegames with newer game versions without encountering significant difficulties in implementing compatibility. Due to time constraints and the priority of making the game itself, I opted to extract all method declarations of this serializer class into an interface, making it easier to switch the implementation in the future.
Now it was time to replace the existing implementation. But with what?
The straightforward solution was to develop a new serializer class that implements the same interface but serializes data in a file format that is adaptable to changes such as additions, removals, or reorderings within the Serialize method. The decision I made was XML.
Primarily because of my familiarity with it, its existing usage for storing game data, and the pre-existing infrastructure for reading and writing within the game data loader of the engine. What made this journey extra-pleasant was that the entire serializer had unit tests set up so that I just needed to implement the new code, run the unit tests until all of them pass and then replace the constructor call. That was it!
So the new serialization code looks like this (actual code from the current weather system serialization):
public void Serialize(IDataSerializer stream)
{
stream.SerializeExternalRef(ref currentState, "currentState");
stream.SerializeExternalRef(ref nextState, "nextState");
stream.Serialize(ref t, "t");
stream.Serialize(ref currentStateTimeLeft, "currentStateTimeLeft");
stream.Serialize(ref solarEfficiency, "solarEfficiency");
stream.Serialize(ref rainyness, "rainyness");
stream.Serialize(ref light, "light");
}
Now, the serializer supports:
- Adding new data: If a field does not exist on deserialize, the default value is assumed
- Reordering data: Since every call is named now it is easy to just look up an xml node of that name in the element that's currently being explored
- Removing data: Since the serializer is now name-based, if something is not requested for deserialization it is just ignored!
In additional to these steps I've introduced savegame versioning so that when a savegame is loaded the game knows the current savegame version and the version of the loaded savegame. This lets me run migrations to either patch the raw xml code or control how references to game data is loaded (for example required if a building's identifier is changed).
In order to implement a new migration all you need to do is implement this class:
public abstract class SavegameMigration
{
public abstract int FromVersion { get; }
public abstract int ToVersion { get; }
public abstract void MigrateXml(XDocument root);
public abstract void PostProcessExternalRefId(ref string id);
}
A migration helper will figure out an update path and tell the user if migration is possible or not via the user interface. When the user loads the savegame it will be migrated automatically and (hopefully if I didn't mess up) it will load correctly.
So, the infrastructure now exists to make the game backwards compatible with old saves. Nice! ๐
Balancing
While balancing the game I've come across a few challenges and had an idea I haven't seen done by anyone else I know yet.
The main issue with balancing is having an overview with all relevant data which is easy to read and allows me to see the outcome of taking several paths ingame in numbers. To resolve this issue I've implemented a system I call "Tweak Tables".
These tables can be directly exported to an Excel file from the game data and from within my IDE (JetBrains Rider).
Similarly, they can be re-imported after editing directly via the IDE.
Here is a glimpse of what these tables look like:
This way I have all relevant data in sight and can comfortably tweak the balance of the game with a powerful editing tool.
The technical implementation of this is rather lame and classic (and a bit ugly, but I don't care!).
All fields in the Excel sheets are either "Bindings" or "Values".
The bindings bind to a specific field in the game object's XML code, establishing a 2-way binding.
The values are just a one-way binding (implemented as a getter) and allow me to write additional data (like the "Value gain per second") I can use in my tweaking process.
So far, this method has greatly aided me in obtaining a comprehensive understanding and fine-tuning the game to ensure the desired balance between different paths the player can take while playing the game.
Thanks for reading,
Have a nice day!