Unity6 Game Development · Lesson 32

Assembly definitions and project dependency boundaries

Put a real MonoBehaviour in an explicitly configured runtime assembly.

Unity 6 Editor walkthrough

The Unity script needs the Editor and the setup listed below. The separate C# companion practice runs in SharpRune without UnityEngine.

Naming and compilation are separate architectural tools

A namespace allows two assemblies to contain clearly named types without deciding which files compile into either assembly. An asmdef asset instead creates a Unity compilation boundary for scripts in its folder and applicable descendants. AssemblyBoundaryProbe deliberately prints its runtime assembly name, making that difference observable rather than relying on an abstract folder diagram. This is a small runtime assembly with no custom dependencies; it still needs normal Unity engine references because the component derives from MonoBehaviour. Do not copy a portable domain project’s no-engine assumption into this engine-facing assembly and then interpret the resulting compiler errors as an importer problem.

Configure the assembly asset before interpreting the log

Place the block-namespaced component in the specified Runtime folder and create SharpRune.InteractionDemo.asmdef there using Unity’s asset workflow. Keep its name exact, Any Platform enabled, normal engine references available and the reference list empty for the baseline. Start calls GetType().Assembly.GetName().Name, so the observed string describes the assembly that actually contains this imported type. Root Namespace is a creation convenience rather than a command that rewrites an existing source file. A wrong folder placement or another nested assembly definition changes the boundary and can legitimately produce a different name. Wait for compilation before testing attachment or entering Play.

using UnityEngine;
namespace SharpRune.InteractionDemo
{
    public sealed class AssemblyBoundaryProbe : MonoBehaviour
    {
        private void Start()
        {
            Debug.Log("Component assembly: " + GetType().Assembly.GetName().Name, this);
        }
    }
}

Check a dependency graph without pretending to import an asmdef

The companion graph says Game depends on Rules and Tools depends on Game. Adding Tools to Rules as an additional dependency direction is not what the first check asks: it asks whether Tools can depend directly on Rules, which introduces no cycle and returns True. The second proposed edge makes Rules depend on Game; because Game already reaches Rules, that edge would close a cycle and returns False. Reaches uses a visited set so traversal can terminate even if supplied data is already problematic. This is an architectural graph check, not a complete implementation of Unity’s assembly importer or all its reference restrictions.

Avoid leaking Editor-only or predefined-assembly dependencies

An engine runtime assembly cannot simply reach back into a type left in a predefined Assembly-CSharp boundary. Move a shared contract into an assembly both sides can reference instead of adding an impossible back edge. Editor-only tooling should be another assembly restricted to Editor and should reference the runtime assembly, while the runtime side remains independent of UnityEditor. Auto Referenced governs implicit use by predefined assemblies; it is not permission for every asmdef to reference every other one. References, platform filters and engine-reference settings each change different compilation assumptions. Review their concrete Inspector values rather than interpreting a folder named Editor as a universal packaging guarantee.

Practice: observe a real assembly boundary, then add a tool boundary

Import the runtime asset exactly as described and verify that the component log names SharpRune.InteractionDemo. In a copied project area, move the script outside its asmdef scope and observe the changed compilation boundary after Unity finishes importing. Restore the baseline before creating a sibling Editor assembly that explicitly references the runtime one. Add a small Editor-only helper there, not in the runtime folder, and check the platform restriction in the asmdef Inspector. The important result is a real dependency direction that can be built into a Player without accidentally bringing Editor APIs along, not merely a successful Console log in the Editor.

  • Hint: a folder name and a namespace are not substitutes for reading the actual asmdef Inspector.
  • Hint: keep runtime-to-Editor dependency absent and Editor-to-runtime dependency explicit.

Practice: reject self-edges and explain a longer cycle

Extend the supplied graph with Presentation depending on Game. Test whether Rules can depend on Presentation, whether a node can depend on itself and whether adding an already existing harmless edge changes the rule. Return a path explaining a rejected cycle rather than only a Boolean, so a reviewer can see the chain of dependencies. Keep graph validation separate from applying the change: a passing graph test does not create an asset, resolve a missing package or prove a Unity compilation succeeded. This exercise also exposes the difference between a proposed dependency direction and the order in which a human happens to list project folders.

  • Hint: before adding owner -> dependency, ask whether dependency already reaches owner.
  • Hint: name the full rejected cycle rather than saying only that a reference is invalid.

Keep a compilation receipt and an architectural rule receipt

The engine receipt should include the asmdef name, source folder, platform restriction, reference list and reported component assembly. The portable receipt records the supplied graph and its two expected decisions. Neither substitutes for the other. The article uses versioned Unity6000.0 assembly documentation and a real C#9 MonoBehaviour, but local validation did not compile against Unity assemblies or run an Editor. Desktop execution established the graph function’s outputs only. These boundaries make the lesson useful when the project later extracts pure rules, engine presentation and Editor tooling into independently reviewed units rather than treating every source file as part of one implicit assembly.

Runs in Unity 6 Editor

Unity Editor example

Set up in Unity 6

  1. Create Assets/SharpRune/InteractionDemo/Runtime and save AssemblyBoundaryProbe.cs there with its public class and block-scoped namespace unchanged.
  2. In that Runtime folder, use Create > Scripting > Assembly Definition and name the asset SharpRune.InteractionDemo. Set its Name to SharpRune.InteractionDemo and Root Namespace to SharpRune.InteractionDemo.
  3. Keep Assembly Definition References empty, Any Platform enabled, Auto Referenced enabled and No Engine References disabled. Do not mark this runtime assembly Editor-only.
  4. Wait for compilation, create an active empty object, attach the namespaced AssemblyBoundaryProbe component, then enter Play mode and inspect the reported assembly name.
  5. For a separate extension, create an Editor-only assembly in a sibling Editor folder and explicitly reference SharpRune.InteractionDemo. Do not make the runtime assembly reference Editor APIs or a predefined Assembly-CSharp type.

AssemblyBoundaryProbe.cs

Download Unity script ↗
using UnityEngine;
namespace SharpRune.InteractionDemo
{
    public sealed class AssemblyBoundaryProbe : MonoBehaviour
    {
        private void Start()
        {
            Debug.Log("Component assembly: " + GetType().Assembly.GetName().Name, this);
        }
    }
}

What to observe in Unity

With the stated asmdef imported, the real attached component reports SharpRune.InteractionDemo as its assembly. Without that asset or with a different folder boundary, the reported assembly legitimately differs.

The console practice below isolates the C# logic. It does not run a Unity scene or emulate Unity engine callbacks.

C# companion practice

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;

var graph = new Dictionary<string, string[]> { ["Rules"] = Array.Empty<string>(), ["Game"] = new[] { "Rules" }, ["Tools"] = new[] { "Game" } };
Console.WriteLine(CanAdd("Tools", "Rules"));
Console.WriteLine(CanAdd("Rules", "Game"));
bool CanAdd(string owner, string dependency) => owner != dependency && !Reaches(dependency, owner, new HashSet<string>());
bool Reaches(string node, string goal, HashSet<string> seen)
{
    if (node == goal) return true;
    if (!seen.Add(node)) return false;
    return graph.TryGetValue(node, out var dependencies) && dependencies.Any(next => Reaches(next, goal, seen));
}
Open C# practice

C# practice output

True
False
Try this
An asmdef changes a compilation boundary; a namespace only changes naming.

Further reading