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 like roadOwnerObject.

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 in Tile.kt.
  • Clone before saving: Create a deep copy of GameInfo in UncivFiles.kt to avoid race conditions during serialization.
  • Rebuild after loading: Call setTransients() on GameInfo, Civilization, and Tile objects to restore the object graph and cached collections.
  • Handle edge cases: Use custom serializers in JsonSerializers.kt for complex types and explicitly annotate lazy properties as documented in GameInfo.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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →