How Unciv Handles Transient References During Game State Serialization
Unciv marks runtime-only object references with Kotlin's @Transient annotation to exclude them from JSON serialization, clones the game state before saving to avoid concurrency issues, and rebuilds the complete object graph via setTransients() methods after deserialization.
Unciv, the open-source Android reimplementation of Civilization V, persists game states using libGDX's JSON serializer. Because the live game graph contains circular references, cached calculations, and volatile runtime data that cannot be reliably stored, the codebase implements a coordinated three-phase strategy to handle transient references during serialization.
The Three-Phase Serialization Strategy
Phase 1: Marking Fields as Transient
Any property that should not be persisted—including object back-references and derived caches—is annotated with Kotlin's @Transient. This instructs the libGDX JSON serializer to skip the field when writing to disk, preventing serialization of stale or circular data.
In core/src/com/unciv/logic/map/tile/Tile.kt, the roadOwnerObject property demonstrates this pattern:
class Tile {
@Transient private var roadOwnerObject: Civilization? = null
// This reference is rebuilt after loading, not stored in JSON
}
Phase 2: Cloning Before Serialization
To prevent concurrent modification errors during the save process, Unciv creates a deep clone of the entire GameInfo tree before serialization. According to the implementation in core/src/com/unciv/logic/files/UncivFiles.kt, this ensures the serializer operates on a stable snapshot while the live game continues running.
val clonedGame = gameInfo.clone()
val jsonString = Json().toJson(clonedGame)
Phase 3: Restoring Transient References
After JSON is read back into objects, Unciv calls setTransients() to rebuild the object graph. This method traverses the loaded data and re-establishes all the references that were excluded from the JSON payload.
GameInfo.setTransients()orchestrates the restoration process by invoking setTransients on child objects.Civilization.setTransients()recreates internal caches and unit lists.Tile.setTransients()restores owner references likeroadOwnerObject.
The comment in core/src/com/unciv/logic/civilization/managers/UnitManager.kt explains this regeneration: "Used during load game via setTransients to regenerate a Civilization's list from the serialized Tile fields."
Critical Implementation Details
Custom Serialization Rules
Not all types serialize automatically with libGDX's default rules. The file core/src/com/unciv/logic/multiplayer/apiv2/JsonSerializers.kt provides custom serializers for types like Instant and UUID that require special handling during game state serialization.
Enum and Lazy Property Handling
As documented in core/src/com/unciv/logic/GameInfo.kt, enums serialize as their primitive values, and extra enum fields are ignored during deserialization. The same file warns that properties declared with by lazy should explicitly use @Transient for safety, a guideline reinforced in core/src/com/unciv/ui/screens/civilopediascreen/FormattedLine.kt regarding fields without backing fields.
Summary
- Mark with
@Transient: Exclude runtime references and cached data from JSON output using Kotlin's transient annotation, as seen inTile.kt. - Clone before saving: Create a deep copy of
GameInfoinUncivFiles.ktto avoid race conditions during serialization. - Rebuild after loading: Call
setTransients()onGameInfo,Civilization, andTileobjects to restore the object graph and cached collections. - Handle edge cases: Use custom serializers in
JsonSerializers.ktfor complex types and explicitly annotate lazy properties as documented inGameInfo.kt.
Frequently Asked Questions
Why does Unciv clone the game state before serializing?
The clone operation in UncivFiles.kt prevents concurrent modification exceptions. If the live game changed while the serializer was reading the object graph, the resulting JSON would be corrupted. Operating on a clone ensures a consistent snapshot while the original game state continues running.
What happens if a developer forgets to mark a field as @Transient?
Unmarked reference fields would be serialized as nested objects, potentially creating duplicate data or circular reference errors. Upon deserialization, these fields might contain stale object copies rather than references to the actual live objects, causing subtle gameplay bugs or memory leaks.
How does setTransients() handle circular references between civilizations and tiles?
The setTransients() method in GameInfo establishes a deterministic restoration order. It first sets up the top-level game components, then passes references downward to civilizations and tiles, allowing each entity to link back to its owners without creating infinite loops during the rebuild process.
Are lazy properties automatically excluded from serialization?
While Kotlin's by lazy delegates are typically not serialized, the codebase explicitly marks them with @Transient as a safety measure. The GameInfo.kt file documents this requirement, noting that explicit annotation prevents accidental serialization of backing fields that should be recalculated at runtime.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →