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:
- A third-party library doesn't support asmdef
- Old internal code isn't compatible with asmdef and is hard to adapt quickly
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!