DEEP DIVE

Anatomy of a Binary Merge Conflict in Unreal Assets

2026-01-29  ·  11 min read

Anatomy of a Binary Merge Conflict in Unreal Assets

When two artists modify the same Blueprint asset and neither has a lock, the version control system eventually surfaces a conflict. What that conflict looks like — and what your actual options are — depends heavily on whether your VCS has any structural understanding of the binary format involved.

This is a detailed look at what happens at the byte and object-reference level when a binary merge conflict occurs in an Unreal asset. Understanding the anatomy of the conflict matters because it changes which resolution paths are actually available to you, and it explains why the "just resolve it" UX in most VCS tooling is a lie for binary files.

What a .uasset Is at the Binary Level

An Unreal .uasset file is a serialized package containing one or more UObject exports. The file header declares the Unreal package format version, an import table (references to objects in other packages), an export table (the objects defined in this package), and the export data for each object — serialized with Unreal's own binary format, optionally LZ4-compressed.

For a Blueprint asset specifically, the export table typically contains:

  • The Blueprint class object itself
  • The ClassGeneratedBy reference (pointing to the Blueprint)
  • The EventGraph (serialized byte code if compiled, or raw node graph data)
  • Per-variable defaults and property override data
  • Component tree entries for each component added to the Blueprint

Object references between exports use integer indices into the import and export tables — not path strings, not GUIDs. An import reference to a mesh asset reads as something like import[12], where import[12] maps to /Game/Assets/Environment/SM_Pillar.SM_Pillar. The specific integer is assigned at serialization time and can differ between saves of the same logical content.

Why Line-by-Line Merge Doesn't Apply

A standard 3-way merge algorithm works on sequences of text lines. It identifies a common ancestor, finds the ranges each branch modified, and tries to interleave them. This assumes that lines are the atomic unit of content and that non-overlapping line ranges from two branches can be safely combined.

Neither assumption holds for .uasset binary content.

First, the binary content has no line boundaries. The "lines" at the byte level are meaningless for semantic interpretation. A 3-way merge at the byte level can produce output that has neither parse errors nor visible corruption but is semantically invalid — for example, an import table whose indices no longer match the references embedded in the export payloads.

Second, the object reference model is dense with cross-dependencies. Import table entry 12 pointing to SM_Pillar is not a standalone piece of content — it's a numbered slot that export payloads throughout the file refer to by integer index. If Branch A and Branch B both added new assets to the Blueprint (one added a mesh component, one added a particle system), their respective saves may have assigned different integer indices to their new imports. A naive merge of the two import tables — even if the bytes happened to interleave correctly — would produce mismatched reference indices in the export payloads that still use the original integer assignments.

A Concrete Conflict Scenario

Two artists, working on separate branches of an early-stage open-world game, are both modifying Content/Blueprints/Interactive/BP_DockCrane.uasset. Artist A is adding a new skeletal mesh component for crane cable physics. Artist B is updating the Blueprint's interaction event graph — changing how the player trigger radius is calculated.

Artist A's save: adds import[18] pointing to SK_CraneCable_Physics, adds a new SkeletalMeshComponent export at export[9], serializes updated component tree with reference to import[18].

Artist B's save: modifies export[3] (the event graph), changing the radius value from 200 to 350 and adding a new function node. Does not touch component data. Their file still has the original import table (17 entries, no import[18]).

The conflict: both exports originated from the same common ancestor commit. Neither artist knows about the other's changes. The VCS has two diverged binary objects that it cannot automatically merge.

Resolution options at this point: pick Artist A's version and discard Artist B's event graph changes, pick Artist B's version and discard Artist A's cable component, or manually re-apply one set of changes on top of the other in Unreal Editor. The third option is the correct one — and it requires an engineer who understands both changes to open the asset in Unreal and manually reconstruct the intended state.

The Component Reference Problem in Levels

In .umap files, the conflict anatomy gets more complicated because levels contain actor instances — not just class definitions. An actor instance in a level references its archetype (the Blueprint class), has per-instance property overrides for transform, and may have component override data stored inline.

When two level designers both place actors in a map — Artist C placed 12 foliage instances in the harbor district, Artist D moved an existing light actor and changed its intensity — their respective level saves both have modified export tables with different actor instance counts and different ordered export entries.

The cross-reference issue: Unreal's level file references actors by their export index and their ActorGUID. GUIDs provide stable identity across saves; export indices do not. A merge tool that understands GUID-keyed actor identity (the way Diversion's structural parser does) can say: "Actor GUID 3F8A2C... is present in both versions with different property values — show the diff of those properties." A tool that sees only bytes sees: "binary conflict, 280 MB vs 283 MB, cannot merge."

Cross-Scene Object References

Unreal projects in late development frequently have cross-level object references — a Blueprint in Level A references an actor in Level B via a soft object path, or a data layer definition in one sublevel is consumed by actors in another. These references survive in the serialized .uasset as path strings in import table entries.

Merge conflicts involving assets with cross-scene references are particularly risky because a "resolve by picking one side" approach can produce a valid-looking asset that has dangling references — paths that used to resolve in the pre-branch state no longer resolve post-merge because the referenced asset was moved or renamed in one branch but not the other.

This is a class of corruption that doesn't surface immediately. The asset loads, the Blueprint compiles, the level opens — and then at a random point during gameplay, the soft reference resolves to null and something breaks. This is why automated merge resolution for binary assets is dangerous without structural validation post-merge.

What Actually Helps

The realistic toolkit for binary merge conflict resolution in Unreal:

  • Lock before edit, always, for levels. This is the only prevention that actually works. If you can't get a conflict, you don't have to resolve one.
  • Structural diff to understand divergence before resolution. Knowing that Artist A added a component and Artist B changed an event graph means you can re-apply both changes intentionally in Unreal Editor. Going in blind means you might accidentally discard correct changes.
  • Keep the common ancestor. When resolving manually, keep a copy of the common ancestor version to verify what baseline you're starting from. "Pick theirs + apply my changes" is safer than "merge" when you can see what each change actually was.
  • Post-merge validation in the editor. After manually resolving, open the asset in Unreal Editor, check for missing references in the content browser, run a Map Check on any levels involved, and verify that all Blueprint compiles succeed. Don't commit the resolved asset until you've done this.

We're not saying structural-aware tooling eliminates the need for an engineer to make judgment calls on binary merge conflicts. The judgment call — "which version of this Blueprint do we base the resolution on?" — is irreducibly human. What structural tooling changes is whether you make that call with information (you know exactly what changed on each branch) or without it (you know two things disagree and you're guessing which to keep).

The teams that handle binary merge conflicts well have two things in common: strong lock discipline that keeps conflicts rare, and a structured protocol for the conflicts that do happen — who resolves them, with what tooling, and what validation happens before the resolved asset merges to main.