Actions corresponding to event functions (Awake(), Start(), OnEnable(), etc.)

Unity event functions need corresponding Actions.

If you want an Awake invocation from a given component, the only way to get it is to code the Awake method in source. That's very limiting. I'll give an example at the end to illustrate why it prevents us from programming reactively.

Sometimes, a derived class needs to delegate tasks to a utility class so we don't have to write the same code in every single derived class. In some cases, inheritance is not the best abstraction. That's why we need an Action in MonoBehaviour that corresponds with each event function. Awake() should invoke an onAwake action. Start() should invoke an onStart action, etc. These actions should invoke in the background without us needing to provide extra configuration.

I sense a counter-argument incoming: "Just implement a derived class of MonoBehaviour and add the actions yourself. It's super easy to add the override keyword to Awake(), Start(), etc."

When I hear anyone on my team say, "it's super easy, the team just needs to remember to do [X]..."
My immediate thought: Potential for human error = bad... must automate.

The override keyword is not easy for everyone. You'll have tech artists on the team who never studied OOP. You can train them, but there will be mistakes from everyone, not just the OOP noobs.

On about half a dozen professional projects, I tried to set up inheritance patterns above MonoBehaviour to make everything quicker. I was the lead on the team, so everyone went along with it. It was a QA nightmare. Juniors and seniors alike constantly forgot to add the override keyword. It was my idea, but I personally forgot frequently as well.

Even though I've been coding OOP for almost 30 years, and I authored the parent classes, we all have habits. When coding in Unity, I have a brain macro that says, 'if you want a Start method, type "void S"-[TAB] and you're good to go.' I have muscle memory for every Unity event function. The override keyword asks me to break that muscle memory. Each time I fail to do so, Intellisense shows an almost unnoticeable warning. It's a recipe for mistakes that don't throw errors. Instead, they emerge later as bugs in QA play-testing.

Inheritance and overrides are not an acceptable solution to this problem. I'd never say 'never use inheritance with MonoBehaviour,' but I do generally recommend not to use common event functions in parent classes so as to avoid the need to ever override them.

I don't see any good reason not to have access to component callbacks without molesting the normal event function workflow. Unity could easily implement Actions that correspond with event functions in MonoBehaviour.

AFAIK, the only third-party solution doing something along these lines is UniRx. UniRx triggers work great once the PlayerLoop is running, but they don't work for all event functions, and they have huge potential for race conditions, since they have the same limited access to component instantiation as we all do.

An example: Event Subscription

The absolute most common task for which I utilize UniRx triggers is event subscription/unsubscription.
Whether you call AddListener(), Register(), use the += operator, or invent your own variation of a pub-sub pattern, Unity requires you to code connection and disconnection individually for each MonoBehaviour via OnEnable and OnDisable.

This is reminiscent of the decades-old argument about automatic memory management. While it's nice to have direct access to memory registers when coding low-level, 95% of tasks don't need it. Several languages abstracted the need to alloc and dealloc each variable manually a long time ago. Pub-sub in Unity has not caught up to this trend yet.

Any time I see myself writing the same code over and over, I want to abstract it. With pub-sub patterns in Unity, you can't. We must plug and unplug manually via OnEnable and OnDisable. If we had onEnable and onDisable actions, we could delegate pub-sub to a single static class and manage it externally, coding it once for every MonoBehaviour.

This is one of the main reasons I use UniRx in nearly every project. I hate writing hundreds of OnEnable/OnDisable pairs manually to set up pub-sub over and over and over. It's one of the most tedious things we do in all of Unity scripting, and it can be easily automated. Without actions internal to MonoBehaviour, UniRx has to ape internal actions by shotgunning extra components on every object and running a custom player loop to check each MonoBehaviour's status, a grossly inefficient bandaid on the problem.

I sympathize, but...

The magic message methods are extremely old and pervasive in the Unity system, there's virtually no way any MORE complexity will be added to the way they work. It's got staples on tweaks on patches on bandaids on cruft on detritus on slag, from the Unity 2 days or even earlier.

Look at the SendMessage() API, this is the mechanism that is used to call all these methods, whether private or public, whether IEnumerator or void, with arguments or not, across all components on a game object, in the order prescribed by the DEO system, etc., etc. There are a lot of magic message methods you're likely not even aware of, as well. Whatever extra wrinkle to their execution you propose, the performance impact on GC or time really must be considered carefully, as well as backward compatibility with all of the code assets people have in their projects.

5 Likes

RE: complexity, I think this adds no complexity on the developer side. Devs don't even need to be aware of the presences of corresponding actions unless they go looking for them. They'd all just go about their business using MonoBehaviour as they always have.

RE: perf op, I 100% agree that allocation of a handful of extra action variables could create a significant GC impact for projects using 1000s of MonoBehaviours. I'm no expert on DOTS, but my understanding is that anyone in that scenario should be using DOTS. It may be different lately, but last I checked a few years ago, we would generally avoid hefty use of MonoBehaviour in a DOTS project because MonoBehaviour is already quite inefficient.

Like in the case of memory allocation, 5% of projects might be hurt by perf op issues with the change I suggest, but they have alternatives, and right now, the 95% are already hurt by lack of the change. Addition of actions to MonoBehaviour shouldn't come without careful benchmarking, but if the garbage collection showed itself to be of relevant concern over this, there are two viable approaches:

1) Give an option somewhere in Project Settings to toggle these actions on in MonoBehaviour, but leave them off by default.
2) Create an alternative to MonoBehaviour (called ReactiveBehaviour or whatever) that we can choose to use if we want, that is identical to MonoBehaviour other than the addition of these actions.

I feel like this would be terrible performance wise. Currently with magic methods, Unity is smart enough not even attempt to marshall the call if it knows a component doesn't implement a magic method. This saves a ton of overhead.

To invoke an action, there is always going to have to be a marshalled call from the C++ side to the C# side just in case something happens to have subscribed to Awake/Start, etc. I don't think that overhead is going to be acceptable.

I wouldn't be surprised if this has been tested internally and was deemed to not be a viable option.

Also Awake is the first thing to run when a component is instantiated, so the only time you could subscribe to an OnAwake delegate for a particular instance is... Awake. So what is the point?

4 Likes

Just want to say that your reasoning is really solid and you discuss your thought processes in great detail. Very nice summary of where we stand.

However, I just don't think this is a good feature. I feel that the new global potential complexity it would add just isn't worth the gain.

And even if one chooses not to use them, professionals will need to know how to use them and reason about them well, because they will encounter them in third party and other code.

And as Spiney points out, already Awake() and OnEnable() happen before AddComponent even gives you a reference to hook up your actions.

I just don't see a good way forward, and given the availability of a simple boiler-plate solution, eh, just doesn't quite seem worth it.

But well argued! :)

2 Likes


Whenever a domain reload occurs the OnPostprocessAllAssets method will execute. You can use this to detect what you don't want to allow in a project. In this case a class inheriting directly from MonoBehaviour.

https://docs.unity3d.com/ScriptReference/AssetPostprocessor.OnPostprocessAllAssets.html

3 Likes

The overall point is to give reactive flexibility to all the event functions, not just the init-time ones. Awake/Start/OnEnable are particularly problematic, since the unpredictability of execution order is a a common refrain. Reactive access would enable us greater flexibility handle any potential up coming race conditions with much cleaner code. Currently, UniRx gives us exactly that AFTER init, so scripts can be react-ready a few frames after they've be initialized, and only through UniRx's inefficient player loop process.

RE: "Awake is the first thing to run" This relevant an dtrue for AddComponent(), sure, but not for serialized situations. In serialized situations, the Awake() methods of other classes that invoked prior, and/or any class attached to Unity's static callbacks can invoke prior to the MonoBehaviour's Awake() method. They can have a prior ref to that MonoBehaviour, and any of them might want to assist w/ configuration on that MonoBehaviour's behalf at the appropriate moment rather than generating async listeners to poll for the appropriate moment.

RE: "always going to have to be marshalled" I don't see why. Unity is able to conditionally marshal via reflection into any class we write that derives from MonoBehaviour. Why wouldn't they be able to use compilation analysis and do the same for any subscription to a MonoBehaviour action? IDEs do that, for example, to show us a list of current references for every method we write.

Thanks!

I'm not sure which boilerplate you mean, though. Does that refer to hard-coding magic functions as we always have, overriding them, UniRx, something else? The only one of these that solves the problem IMO is UniRx, and it only KINDA solves the problem 80% of the time, and always less-efficiently than Unity could directly.

The inheritance experiments I did on that team were back in the 20-teens. Since then, I have become all too familiar with Unity's secret bag of tricks RE: validation, including OnValidate(), OnPostprocessAllAssets(), etc. I 100% agree that if I were to build a toolbox for a team that derives from MonoBehaviour, it should absolutely include validation to check whether event functions are properly overridden. That said, I'd much prefer just about any alternative that doesn't force the whole team to remember to add the override keyword all the time and constantly reap the validation workflow's interruption.


Use a custom script template. I even have a PowerShell script to automate the process of installing the template into every release of the editor that is currently installed.

# Replaces the Unity script template files with symbolic links to my custom templates
$customTemplatePath = "C:\Users\Ryiah\Dropbox\Source\Unity\ScriptTemplates"
$unityBaseInstallPath = "C:\Program Files\Unity\Hub\Editor"

$unityVersionPaths = Get-ChildItem -Path $unityBaseInstallPath -Directory
foreach ($versionPath in $unityVersionPaths)
{
    $templatePath = Join-Path $versionPath.FullName "Editor\Data\Resources\ScriptTemplates"
    if (Test-Path $templatePath)
    {
        # Replaces all the template files that have a corresponding custom file
        $templateFiles = Get-ChildItem -Path $templatePath -File
        foreach ($templateFile in $templateFiles)
        {
            $customFile = Join-Path $customTemplatePath $templateFile.Name
            if (Test-Path $customFile)
            {
                Remove-Item $templateFile.FullName -Force
                New-Item -ItemType SymbolicLink -Path $templateFile.FullName -Target $customFile -Force
                Write-Host "Replaced $templateFile with symlink to $customFile"
            }
        }
    }
}
1 Like


I do that a lot. I don't really like Unity's default script template customization workflow, but I make editor tools for source generation pretty frequently, including alternatives to Project-View-right-click/Create/C# Script. But in order to prevent override mistakes, the template would have to included every overridden event function. I find the default template annoying enough w/ just Start() and Update(). I like templates to be as clear of clutter as possible, unless they're task-specific.

Nice! I'll probably use that if you don't mind. I usually script tools in-engine out of habit. I'm super lazy about building dev ops tools out-of-engine for personal projects.


Because dynamic subscription is as the name implies: dynamic. So you could pass around an System.Object reference somewhere, someone may cast it back to a MonoBehaviour and subscribe to any of the actions. This is fully dynamic and could even happen in a separate assembly loaded at runtime. Unity does not use reflection to call the magic methods. Unity analysis the types if they implement certain callbacks and don't even schedule them for certain callbacks including Update or physics callbacks

Also on a side note: .NET MulticastDelegates are notoriously in efficient when it comes to subscribing / unsubscribing. It's fine for a WinForms application where the callbacks are setup once in a fire-and-forget manner. When you plan in often subscribing and unsubscribing, it's very in efficient. The delegate will create a new delegate with w complete new invokation array. So the more things are subscribed to the same event, the larger the GC chunks are that are dropped. .NET does this for thread safety because the easiest solution to achieve thread safety is to duplicate the data. In a game engine this is not very efficient. So just hypothetically if Unity would implement events for those kind of things, it should use something like a List based approach.

Note that Unity has currently listed 65 messages on the MonoBehaviour component and this is not necessarily an exhaustive list. Implementing an event for every message would to total overkill. As it was already mentioned, for some callbacks it just does not make any sense to have them as an event. Relying on the execution order during initialization is generally a bad idea. Awake is there to initialize the component itself, no external messing around. Start is meant to communicate with other components.

You rarely need such events and out of those 65 messages mentioned above, you probably would need only a handful. So you are free to implement these in the cases you need them. So it really make no sense for the engine to implement anything like that. We've seen it in the past. When they implement some functionality just for "some" / the most common messages, people will be confused and complain why this exotic message doesn't have it. This happened with the shortcut properties as well and was one of the reasons whey they were removed, even though they were quite useful.

So I don't see any benefit if Unity would step in and messing the whole engine up for features that almost nobody would use and that can be implemented by the user easily on their own. Custom messaging and event systems are developed on a daily basis and they are specialized to the actual usecase.

2 Likes


Go ahead. I built it because I wanted to try out templates but didn't want to manually copy it whenever I installed a new release which was quite frequent at the time.

1 Like


So a reference to a component, to register to OnAwake in... Awake. So you can do something after it has finished waking up.

Isn't that called Start?

1 Like


Here's the closest I can come without using new or override, but it just shifts you from having to remember one of those to having to remember to add On to the method name.

using System.Reflection;
using UnityEngine;
using UnityEngine.Events;

public abstract class MyMonoBehaviour : MonoBehaviour
{
    public UnityEvent onAwake;

    private void Awake()
    {
        var method = GetType().GetMethod("OnAwake", BindingFlags.Instance | BindingFlags.NonPublic);
        if (method != null)
            method.Invoke(this, null);

        onAwake.Invoke();

        Debug.Log("Awake.");
    }
}
using UnityEngine;

public class Example : MyMonoBehaviour
{
    private void OnAwake()
    {
        Debug.Log("OnAwake.");
    }
}
1 Like

[quote=“Bunny83”, post:13, topic: 950208]
Custom messaging and event systems are developed on a daily basis and they are specialized to the actual usecase.
[/quote]

I’m building a badass event system right now. That’s why I have this stuff on the brain. I had to create a custom player loop and scan all relevant MonoBehaviours every frame to do the same kind of thing UniRx is doing to access OnEnable & OnDisable in the background.

“Relying on the execution order during initialization is generally a bad idea.” Agreed, that’s why I want reactivity built-in. You said at the there’s no benefit to them “messing up the whole engine,” but yes there is. There’s a reason the entire web dev community has been steering toward reactive techniques for the past decade. “Messing up the whole engine” is about as hyperbolic as it gets.

RE: multi-cast delegate inefficiency, for example, they benchmark 10X faster than UnityEvent objects, which are built on top of them. I get that no delegation pattern is going to be great for GC, but I’ve seen countless devs reference handfuls of UnityEvent objects in a majority of their MonoBehaviours. Modern CPUs can eat thousands of them for breakfast, even on mobile & untethered VR. If you have 5000+ MonoBehaviours instantiated simultaneously, the disucssion becomes moot, because DOTS.

RE: “dynamic” I don’t see why boxing/unboxing would prevent the compiler from identifying the target on the managed side. The compiler has to invoke the target at some point. It can be found, no matter how many times it’s been boxed/unboxed. I’m not an expert in compiler analysis, but it’s Unity’s job to compile the assemblies in Assets and Packages. If the invocation comes from elsewhere, they can just prevent that by analyzing the known invocations within the assemblies under Unity’s control, and error-handling any others. I assume that’s how they prevent most attack vectors. Why would a valid invocation ever come directly from an unmanaged DLL? If we intend to marshal an invocation from somewher unmanaged, I’d assume that would fail unless we map it to the magic function and/or the would-be action on the managed side. Isn’t the problem of Unity not having full control of external assemblies just as true for would-be actions as it is for any other marshalled memory?

“Unity analysis the types if they implement certain callbacks.” That’s reflection. How else would they know my class has a method called “Start” other than reflecting into it?

RE: “shortcut properties” - In my 15 yrs of Unity dev, I’ve never heard of these. Very curious what they are, although I’ll probably be sad they’re deprecated already. I googled and didn’t find much.

I don’t want or expect they’d implement actions for every event function. I’d be quite satisfied with Awake(), Start(), OnEnable(), OnDisable(), OnDestroy(), CollisionEnter/Exit, and TriggerEnter/Exit. After that, if devs complained that there’s no onPreRender action, they could easily give a boilerplate excuse: “Any looping callback or any other callback with strict performance constraints is incompatible with MonoBehaviour actions.”

They could easily implement this feature with the option to disable it. It would break nothing.


Nope, there could be 100 other MonoBehaviours that need to execute their Awake functions before the instance in question gets to invoke Start() but after its Awake() method, and any one of those might be the provider or recipient of either hard-or-loose-coupled dependencies between the instance in question or any of its utilities, services, etc.

I frequently hear senior devs telling junior devs "don't worry about execution order... just follow single-responsibility principle and let Unity take care of execution order for you." That's silly advice IMO because single-responsibility principle doesn't eliminate all dependencies. It eliminates all superfluous dependencies, of which there could be a large amount. But when all the unneeded dependencies are gone, the critical dependencies still all execute in a certain order. The fact that we've always only had crappy access to that order via the execution order interface is not a necessity of a component-based system. It just happens to be the way Unity set up the engine so far. It can be better, and this is just one of the problems that reactive techniques solve quite effectively.

Yeah, I've definitely considered that approach, but I fear juniors fresh off a diet of Youtube video tutorials would struggle with any slight naming convention change.

Adding Actions for these messages will not solve the unpredictability of execution order.

Forgetting or not understanding how new and override works is not a Unity problem, it's a issue with your team and how to train them. This is a very weak argument. There are warnings for a reason, if you ignore them it's your own fault or you could change your settings and treat them as errors.

This can easily solved by your own code, there is no need to bloat the engine code.

You request that these Actions should only be added for "some" of the Unity messages. Who decides which messages? You, because you only need them for some specific messages?

After reading your comments, I can tell that you have some assumptions how Unity and C# work which are wrong. I don't have enough time to list them all, but it's better to research a topic before assuming how things work. Noone is all knowing but you can usually trust what Bunny83 says, for example he is right that Unity doesn't use reflection for these callbacks.

2 Likes