The Fire of Ardor: Persisting state - Thank you, ECS!

posted in cpfr
Published July 22, 2019
Advertisement

While filling the new lower dungeon section with (undead) life, I thought that playing the game would take about twice the time, compared to the original version. I was glad that I finally got the opportunity to integrate the lantern that I had to cut from the gamedev challenge submission.

Due to the increased length of the game, it came to my mind that it could be frustrating to start all over after dying. Further, I felt that the game might be too long to play it to the end in one turn. That's why I decided to implement loading and saving games.

Implementation

Loading a savegame is essentially the same as loading a level from file. The level files contain information about the level geometry and the entities within the level. The good news was that I already implemented loading levels from file. The bad news was that this was not enough for the player's savegames. First of all, only loading was implemented. Then, not everything could be loaded from the level file. The player entity for example was added to the world via code.

ECS and savegames

The good thing about the Entity-Component-System pattern is that loading and saving state is relatively easy -- as long as you adhere a very basic rule:
Systems don't hold state. Every mutable information is stored inside the components of some entities (components = data; systems = logic; entities: containers for components).

In my case, I had some data stored inside the systems. Some properties were added for convenience reasons. It was easier to store the data inside the system, rather than in a component. However, most of the properties in the systems were there for performance reasons; values that would have to be queried or computed every frame if not cached. The latter ones are no problem. These properties are fine, since they can be recomputed if needed. The former ones were a problem and I had to remove them. This means that I had to introduce a couple of new component types. For example I added a player component and put every player-related state into that component (although there is always only one player).

Python and JSON serialization

Fortunately, Python comes with an integrated module for JSON serialization and deserialization. This module is able to convert dictionaries to JSON strings and vice versa. Since I don't need dictionaries, but instances of certain classes (my component classes), this was not enough.

In C# there is the Newtonsoft.Json library which converts between JSON and C# objects, which works really nice due to the static type system of the language and the way namespaces work in C#. However, in Python it is not that easy (I never thought I'd actually say that, but there's no reason to be dishonest). Since Python is dynamically typed, there is no fixed set of attributes for a class. Instead, attributes can be added after an object is instantiated and it can not be guaranteed that an attribute is present, except when it is assigned in the constructor. Further, namespaces in Python are not as explicit as in C#, which I would usually appreciate. In this case, however, the dynamic nature of the namespaces makes it difficult to locate a certain type by name. I don't want to discuss C# and Python features here, but this comparison came to my mind, since I use C# at work and Python / Cython at home.

The solution I found uses Python decorators to register component types. The 'component' decorator registers a class under a certain name in the engine. This registration is then used when a level file is either serialized or deserialized. That way, the JSON file can use short, descriptive names for components while being independent from the actual class names. Here is the health component class for example:


@chroniton.component("health")
class HealthComponent(object):
    def __init__(self, currentHealth = 100, maxHealth = 100):
        self.currentHealth = currentHealth
        self.maxHealth = maxHealth

By default, the engine will look for public attributes to serialize (i.e. attributes which names don't start with an underscore) and convert the object to a dictionary that only contains primitive types and thus can be converted to JSON very easily. For special cases, there is also the possibility to implement the special methods 'loadFromDict' and 'dumpToDict', which allow for a custom serialization.

Finally, there must be a fallback solution for components that are not known to the engine. This can come in handy for a level editor that does not load all in-game dependencies. It enables the level designer to load a level, modify it and save it, without losing information about components that are not known to the editor.


UI for loading savegames

Last, but not least, the user has to be able to actually load and save games.
I won't bother anyone with any more text and instead show a screenshot. A picture paints a thousand words:
loadMenu.thumb.png.77951027d4951208bec6d615ecd0ff43.png


Bonus screenshot: Improved sky texture for the town

Yay, okay. I like showing screenshots, even if this one is not savegame-related.
I improved the sky texture of the town level section with some mountains (since we are at the peak of the silver mountain):
newSkybox1.thumb.png.1aeef2e7a6bca6b38351bf4e6e2e1f95.png

newSkybox.thumb.png.41762b23ce3fbeb3d5947915f1a1348f.png

Thank you so much for reading!

2 likes 0 comments

Comments

Nobody has left a comment. You can be the first!
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Advertisement