Quick answer: Assembly definitions must form a DAG. Break A↔B cycles by extracting shared interfaces/events into a third assembly both depend on.
Adding an asmdef reference fails: “Cyclic assembly references detected.” Gameplay references UI, UI references Gameplay — a cycle the compiler can’t resolve.
Why Cycles Are Forbidden
Each asmdef compiles to a separate DLL. To compile A, the compiler needs B already built; to build B it needs A — impossible. Dependencies must be acyclic.
Extract a Shared Contract Assembly
Create a third assembly, e.g. Game.Contracts, holding only interfaces, events, and data types both sides need:
Game.Contracts (interfaces, ScriptableObject events — no deps)
↑ ↑
Game.Gameplay Game.UI
Gameplay and UI both reference Contracts. They communicate through interfaces/events — no direct dependency between them. Cycle broken.
Event-Based Decoupling
ScriptableObject-based events in the Contracts assembly let Gameplay raise events that UI listens to, with neither knowing the other’s concrete types.
Or Merge
If two assemblies are genuinely tightly coupled and splitting them adds no value, merge them into one. Asmdefs are for genuine module boundaries, not arbitrary folders.
Verifying
Project compiles. The dependency graph is a DAG (visualize via the asmdef inspector’s reference list). Compile times improve as modules are isolated.
“Asmdefs must be acyclic. A shared contracts assembly is the standard cycle-breaker.”
Design the assembly graph top-down: Contracts at the bottom, feature modules above, app/bootstrap at the top. Cycles become impossible by construction.