Mailbag: Civilization and ContentRefs
Sometimes, there are questions. And sometimes they happen to be in e-mails I receive through this blog. I usually take the time to answer them if I’m not too busy and occasionally, these answers can get very long. This time it’s about Civilization and referencing content in Duality.
GameObjects in board games
How would you use the Component-based GameObject design when approaching a game like Civilization, taking into account landscape, units, cities, etc.?
The solution to your problem might not be obvious, but it really isn’t as complicated as you might think. Not everything in a component-based approach has to be done utilizing a lot of objects. Sometimes it can be more suitable to focus on fewer, more complex objects. How would you implement such system without a component-based game object system? Since the actual implementation of a “Component” is completely free, it is entirely possible to apply most other code designs to component-based systems as well, although not being equally useful for all of them.
The landscape is a typical case for a single, specialized object. You can implement it as a Component or set of Components which you add to a single “Landscape” GameObject consisting of your Tilemap data, a specialized Renderer for visual display and probably some game logic bound to it. I’m picturing something like this:
Landscape GameObject +-- Transform +-- Tilemap (Generic implementation of a tilemap, data only) +-- TilemapRenderer (Accesses Tilemaps and renders them using a Tileset) +-- Landscape (Rules, Game-related logic)
Units could be implemented as individual objects that are bound to your global landscape object by their Unit logic.
Unit GameObject +-- Transform +-- SpriteRenderer (Or similar) +-- Unit (Keeping it bound to the Landscape, Rules, Game-related logic)
You could also split up the “Unit” Component into its own micro-hierarchy to serve different kinds of map objects:
Component +-- MapObject (Anchoring something to a tile position, general logic) +-- Unit (Controlled, can walk, fight, etc.) +-- City (Builds stuff) +-- Collectible (When walked over, provides a bonus)
This should cover some basic setup. Of course, there has to be some kind of global database to store the Tileset used by the Tilemaps, Unit Types, etc. This is what Resources are for. In Duality, you can not only derive from the basic “Component” class, but also from the “Resource” class to store that kind of information globally available.
Components in board games
But what’s the big advantage of Components for that kind of game?
There is no need for Components in this case! But they’re not standing in your way either.
Component-based approaches are specialized in modeling the interaction of objects which are not necessarily known when laying out the object management system or engine structures. Their greatest strengths are extensibility and modularity. Unlike in classic inheritance hierarchies they allow you to dynamically combine different pieces of functionality for each object individually. They obviously reach their full potential in games with a more versatile environment or game world and allow non-programmers to creatively combine existing functionality in whatever way they need.
A “boardgame” like civilization behaves in many ways perpendicular to that concept. In most cases, all Units, Cities, Tiles and other game elements are similar in their ruleset and logic. They don’t need Components and you, as a developer, don’t usually need the ability to dynamically construct new kinds of objects or introduce new kinds of behavior later.
ContentRef<T>struct is used wherever referencing Resources. What is that good for?
I use this construct and a centralized ContentProvider in Duality because of several reasons:
- Due to the editors design concept, it should be possible to dynamically reload any Resource whenever necessary, even while the game is running. That means, there needs to be a central register of currently loaded Resources to keep track of what to update when a reload occurs: The ContentProvider. It also assures nothing is loaded twice, just because it is requested twice.
- Why doesn’t it return Texture, but
ContentRef<Texture>? Well, imagine the user deletes a Resource in the editor. Updating existing instances won’t suffice anymore – we need some kind of hook to actually set all references to that Texture to null, and the ContentRef indirection layer is exactly that. When accessing a ContentRef that refers to a deleted Resource, it will let go of old data and immediately show that something is missing.
- ContentRef also enables us to talk about Resources that aren’t even there, which isn’t uncommon during development time. Without ContentRef, requesting non-existent Textures would result in a couple of null references. We can’t compare them, tell what they where trying to retrieve or even if they are intentional or not. ContentRef always holds both path an reference, solving all of these problems. That also allows deleted-and-then-restored Textures to magically re-appear throughout the level. As a bonus, it will be the ideal location to implement streaming behavior in the future, because it allows referencing “not-yet-fully-loaded” Resources and using them as soon as they’re available.
- It is possible to pass around ContentRefs without loading the references Resource. If not done explicitly, they will be loaded on their first access. Referencing them doesn’t mean implicitly loading them, although you can always request doing so explicitly.
- Duality uses semi-automated Serialization based on Reflection mechanisms. When serializing a ContentRef, only the path is serialized, preventing Resources to be dragged into the serialization hierarchy without the need for manually handling such cases. It also takes care of retrieving the actual Resources after deserialization – as soon as they are needed.
Will there be more Mailbag posts?