Quick answer: Extract the shared types (interfaces, events, data structs) into a third asmdef. Both original asmdefs reference the shared one instead of each other. This breaks the cycle via dependency inversion, a clean architectural pattern.

Here is how to fix Unity Assembly Definition circular reference. You have Player.asmdef and Enemy.asmdef. Player references Enemy (to call Enemy.TakeDamage). Enemy references Player (to call Player.AddScore on kill). Unity errors: “Cyclic reference detected between assemblies: Player and Enemy.” Your project stops compiling. Remove the reference and half your calls fail to resolve. The fix is not to shuffle references but to rethink the architecture.

The Symptom

Unity console shows one of:

All scripts fail to compile until the cycle is broken, even scripts unrelated to the cycle.

What Causes This

Two-way references. The classic case: A calls B, B calls A. Each side needs to see the other’s types. Neither direction is “wrong” intuitively, but the compiler cannot build either without the other.

Transitive cycles. A -> B -> C -> A. No direct two-way ref, but through a third party, it loops. Harder to spot because you have to trace multi-step paths.

Feature growth over time. A used to be independent. Later, you added a reference from A to B for a new feature. The original B->A reference is forgotten until compile fails.

Shared types in wrong asmdef. A type used by both A and B lives in one of them. Both need to reference the other to use it. Moving the type to a shared asmdef removes the cycle.

The Fix

Step 1: Identify the cycle. Read the error message carefully — it names the two or more asmdefs involved. For transitive cycles, use the Assembly Definition window:

// Unity 2023.2+ has Window > Analysis > Assembly Definition Dependency Graph
// Older versions: inspect each asmdef's References field and trace manually

Draw the graph on paper if needed. Find the cycle.

Step 2: Extract shared types. Create a new asmdef, MyGame.Shared, that contains types used by both sides of the cycle:

// Before: Player references Enemy, Enemy references Player

// After: Both reference Shared
Player.asmdef  -> Shared.asmdef
Enemy.asmdef   -> Shared.asmdef

// In Shared:
public interface IDamageable
{
    void TakeDamage(int amount);
}

public interface IScoreContributor
{
    int GetScoreValue();
}

Player and Enemy now depend on interfaces in Shared instead of each other. Player iterates IDamageable references; Enemy implements both IDamageable and IScoreContributor.

Step 3: Use events for decoupled communication. Instead of direct calls, raise events defined in Shared:

// In Shared.asmdef
public static class GameEvents
{
    public static Action<int> EnemyKilled;
}

// In Enemy.asmdef
void Die()
{
    GameEvents.EnemyKilled?.Invoke(scoreValue);
    Destroy(gameObject);
}

// In Player.asmdef
void OnEnable() { GameEvents.EnemyKilled += HandleKill; }
void OnDisable() { GameEvents.EnemyKilled -= HandleKill; }

void HandleKill(int score) { totalScore += score; }

Enemy raises events without knowing Player exists. Player subscribes without knowing which Enemy fired. Fully decoupled.

Step 4: Consider if the cycle is a design smell. Sometimes the correct answer is not “break the cycle” but “why do these two modules know about each other so deeply?” A Player asmdef that needs to know about Enemy specifics is tightly coupled. Refactor so the common behavior is a contract (interface) and each side only knows the abstract contract.

Recommended asmdef Hierarchy

A clean architecture for a medium-sized game:

MyGame.Core           # utilities, math, no dependencies
MyGame.Data           # data structures, references Core
MyGame.Shared         # interfaces, events; references Data
MyGame.Gameplay       # gameplay systems; references Shared
MyGame.UI             # UI; references Shared, Gameplay
MyGame.Main           # scene bootstrap; references everything below

Dependencies flow downward. No asmdef depends on something above it in the stack. Adding a new feature means adding types to the appropriate layer, not plumbing new references between unrelated layers.

Runtime vs Editor

If a circular reference only appears when you add editor code (asmdef with Editor platform only), the editor asmdef can reference the runtime asmdef but not vice versa. Put editor-only extensions in a dedicated .Editor asmdef.

“Circular references are a design smell, not a technical limit. The compiler is telling you your architecture has a loop.”

Related Issues

For asmdef test-related issues, see Play Mode Tests Not Running. For Addressables and asmdef interaction, Addressables Build Cache Corrupt covers related build-pipeline issues.

Extract to Shared asmdef. Use interfaces and events. Dependencies flow one way.