Calling Methods from Non-asmdef Classes in Unity: A Workaround

This article is a translated version of my original post on Qiita. Original (Japanese): https://qiita.com/segur/items/826a157ee4d7f277c081

flowchart LR
    subgraph asmdef
        ClassWithinAsmdef
    end
    RelayClass
    ClassOutsideAsmdef

    RelayClass -->|Register Action from outside| ClassWithinAsmdef
    ClassWithinAsmdef -.->|Notify Action firing| RelayClass
    RelayClass --> ClassOutsideAsmdef

Calling Methods from Non-asmdef Classes in Unity

Sometimes you encounter situations where you want to call methods from non-asmdef classes within asmdef:

Here's the conclusion:
It is impossible to directly call methods of non-asmdef classes from within asmdef!
However, it is possible to call them indirectly!

If There's No asmdef, There's No Problem

First, consider a situation where there is no asmdef. For example, if you want to implement a process to calculate BMI when updating a user's height and weight, you might want to reference BmiUtility from UserProfile as follows:

flowchart LR
    UserProfile --> BmiUtility
public class UserProfile
{
    public double Height { get; private set; }
    public double Weight { get; private set; }
    public double Bmi { get; private set; }

    public UserProfile(double height, double weight)
    {
        Height = height;
        Weight = weight;
    }

    public void SetHeight(double height)
    {
        Height = height;
        Bmi = BmiUtility.CalcBmi(Height, Weight);
    }

    public void SetWeight(double weight)
    {
        Weight = weight;
        Bmi = BmiUtility.CalcBmi(Height, Weight);
    }
}
public static class BmiUtility
{
    public static double CalcBmi(double height, double weight)
    {
        return weight * 10000 / (height * height);
    }
}

Without asmdef, you can reference directly!

Calling Methods from asmdef to Non-asmdef Classes Is Not Possible!

However, if UserProfile is within asmdef and BmiUtility is outside, you'll encounter a reference error!

flowchart LR
    subgraph asmdef
        UserProfile 
    end
    BmiUtility

    UserProfile -->|External reference is NG| BmiUtility

That's troubling!

Reverse the Reference to Point from Outside to Inside

Turn the idea around! Create a relay class outside asmdef and set up references from outside asmdef to inside it. Here's what it looks like in a diagram:

flowchart LR
    subgraph asmdef
        UserProfile 
    end
    UserBehaviour
    BmiUtility

    UserBehaviour -->|Reference from outside is OK| UserProfile 
    UserProfile -.->|Notify| UserBehaviour
    UserBehaviour --> BmiUtility

First, introduce Action in UserProfile like this:

using System;

public class UserProfile
{
    public double Height { get; private set; }
    public double Weight { get; private set; }
    public double Bmi { get; private set; }

    public Action OnSetHeight;
    public Action OnSetWeight;

    public UserProfile(double height, double weight)
    {
        Height = height;
        Weight = weight;
    }

    public void SetHeight(double height)
    {
        Height = height;
        OnSetHeight?.Invoke();
    }

    public void SetWeight(double weight)
    {
        Weight = weight;
        OnSetWeight?.Invoke();
    }

    public void SetBmi(double bmi)
    {
        Bmi = bmi;
    }
}

Next, create a UserBehaviour class outside asmdef to reference UserProfile:

using UnityEngine;

public class UserBehaviour : MonoBehaviour
{
    public UserProfile User { get; set; }

    private void Awake()
    {
        User.OnSetHeight += CalcBmi;
        User.OnSetWeight += CalcBmi;
    } 

    private void OnDestroy()
    {
        User.OnSetHeight -= CalcBmi;
        User.OnSetWeight -= CalcBmi;
    } 

    private void CalcBmi()
    {
        var bmi = BmiUtility.CalcBmi(User.Height, User.Weight);
        User.SetBmi(bmi);
    }
}

Now, when height or weight is updated, UserBehaviour.CalcBmi() gets executed! You can indirectly call the non-asmdef method from within asmdef!

public static class BmiUtility
{
    public static double CalcBmi(double height, double weight)
    {
        return weight * 10000 / (height * height);
    }
}

You don't need to change BmiUtility. Using Action, you can reverse class reference directions!

flowchart LR
    subgraph asmdef
        ClassWithinAsmdef
    end
    RelayClass
    ClassOutsideAsmdef

    RelayClass -->|Register Action from outside| ClassWithinAsmdef
    ClassWithinAsmdef -.->|Notify Action firing| RelayClass
    RelayClass --> ClassOutsideAsmdef

Con: Methods Cannot Be Called Until Registered in Action

Though useful, there are cons. The main downside is that until methods are registered in Action, nothing happensβ€”even if you fire the action.

For instance, unless the following is executed:

User.OnSetHeight += CalcBmi;

And before the action firing like:

OnSetHeight?.Invoke();

Occurs, nothing can be done. If you fire an action within the constructor, BmiUtility.CalcBmi can't be executed because no method has been registered at that point. The execution order is crucial when designing classes.

Con: Writing OnDestroy Is Tedious

In the sample code, UserBehaviour inherits MonoBehaviour. Thus, if you write:

private void Awake()
{
    User.OnSetHeight += CalcBmi;
}

It’s safer to include:

private void OnDestroy()
{
    User.OnSetHeight -= CalcBmi;
}

But it's easy to forget since it will still work without writing it. (Is there a smarter way?)

Con: May Become a Technical Debt

In a refactoring case where BmiUtility becomes an asmdef class, it should be directly referenced by UserProfile. But if you don't realize:

flowchart LR
    subgraph asmdef
        UserProfile 
    end
    UserBehaviour
    subgraph AnotherAsmdef
        BmiUtility
    end

    UserBehaviour -->|Register Action from outside| UserProfile 
    UserProfile -.->|Notify Action firing| UserBehaviour
    UserBehaviour --> BmiUtility

Even though this state functions, it might remain unaddressed, leading to technical debt without those who knew its historical addition. If you notice it, discard the relay class entirely!

flowchart LR
    subgraph asmdef
        UserProfile 
    end
    subgraph AnotherAsmdef
        BmiUtility
    end

    UserProfile --> BmiUtility

It's especially streamlined now!

In Conclusion

Using Action, it's possible to indirectly call methods from outside asmdef! I hope this helps you!