Configurable Enter Play Mode

TL;DR; Try out the experimental Configurable Play Mode option in Editor ProjectSettings to improve iteration time when you don't mind scripts not being fully reset.

Play Mode is one of the key concepts in game development with Unity. The ability to iterate fast in the Editor is a great advantage which allows developers to prototype faster, fail faster, and create great games more easily. The time it takes to enter PlayMode is a vital metric when it comes to fast iterations. With an empty project it typically takes 1.5-2.5 seconds. However, with the project evolving, time to enter Play Mode might grow significantly to 10-25 seconds, crippling productivity.
We profiled and analyzed Enter Play Mode to understand where the time is spent. As a result, we would like to offer options to skip the most time consuming parts during Enter Play Mode and let you decide whether accuracy or speed is preferred during development.

How much time can be saved
Domain Reload and Scene Reload are the main contributors to the cost to Enter Play Mode - typically their cumulative impact is around 80-90% of Enter Playmode time.
Domain Reload is a generic way to achieve a clean state of game and Editor C# code (besides its main role of reloading a new scripting code). Similarly Scene Reload is a generic way to get a clean state of GameObjects and associated MonoBehaviors/ScriptableObjects. Generic approaches are simple and allow not to think about behavior differences, but incur huge time costs in some cases and reduce productivity.
As you can see below, the effect of skipping Domain Reload and Scene Reload is very promising.

It should be noted that the outcome depends on the project and might vary depending on Scene complexity and used packages and AssetStore plugins.

How to enable
You can find the options in the Editor Project Setting.

ProjectSettings → Editor → Enter Play Mode Settings

The same settings are configurable through the EditorSettings scripting API that we have added to the UnityEditor namespace:

  • bool EditorSettings.enterPlayModeOptionsEnabled;
  • EnterPlayModeOptions EditorSettings.enterPlayModeOptions;

EnterPlaymodeOptions allows you to disable Domain and/or Scene Reload.

What exactly is skipped
From a high-level perspective, Enter Play Mode consists of the following main stages:

  • [Backup current Scenes]. Allows us to restore state during Exit Play Mode. Is optional - only happens when Scene is dirty.
  • Reset scripting state (reload scripting domain, aka “Domain Reload”).
  • Resets Scene state (Scene Reload).
  • Update scene (2 times - without rendering and with rendering).

Combination of Domain Reload and Scene Reload is the main job of Enter Play Mode state. It resets the game world and simulates the startup behavior as the game would run in the player.
To help you understand those options, I would like to provide a scripting events diagram for Enter Play Mode and highlight differences.

What are the side effects
Important differences due to skipped Domain Reload
The main side effect is that the scripting state is not globally reinitialized and it is up to you to decide what has to be reset.

  • Fields marked with [NonSerialized] attribute keep their values - as script serialization step is skipped completely and scripts remain the same, all data is carried to the Play Mode.
  • Static variables keep their values - because Domain Reload does not happen types stay initialized.

  • Static events keep their subscribers.

  • There is no extra OnDisable/OnEnable calls for ExecuteInEditMode scripts.

Static variables can be reinitialized with the help of the RuntimeInitializeOnLoad (RuntimeInitializeOnLoadMethod) attribute.

using UnityEngine;
public class StaticCounterExampleFixed : MonoBehaviour
{
    static int counter;

    [RuntimeInitializeOnLoadMethod]
    static void Init()
    {
            counter = 0;
    }
}

Important differences due to skipping Scene Reload
Avoiding Scene Reload is much more complex to implement and align with the normal loading path, but should have minimal side effects. However, due to its tight connection to Domain Reload, there are a couple of important differences:

  • ScriptableObject, MonoBehaviour fields which are not serialized into the build ([NonSerialized], private or internal) keep their values - as existing objects are not recreated and constructors are not called. Watch out for null private and internal fields of array/List type - they are converted to an empty array/List object during Domain Reload and stay non null for game scripts.
  • ExecuteInEditMode/ExecuteAlways scripts are not destroyed/awaken - no OnDestroy/Awake calls for those. Watch out for Awake/OnEnable methods which check EditorApplication.isPlaying property - Awake is not called and OnEnable is called only when EditorApplication.isPlaying is already true on Play Mode change.

Nonserialized fields for game scripts should not be an issue, as game scripts are not active in Edit Mode, however, the ones marked with ExecuteInEditMode/ExecuteAlways might change themselves or touch fields of other game scripts. This can be mitigated by initializing affected fields in an OnEnable callback.

Known issues

Call for feedback
We believe that if your project is currently slow to enter Play Mode, this feature will speed things up significantly. However, we understand that the side effects of the feature might be difficult to handle and we’re looking forward to hearing what you think, which issues you encounter in you project/ package/AssetStore plugin and how we can make life easier for you!

Big thanks to @sinitreo , @chrisk , @Peter77 , @Baste , @Hyp-X and others for providing a valuable feedback.

15 Likes

Thank you for this feature. In my opinion, this had one of the largest positive impacts on our workflow so far (and we've been using Unity since the 3.x days).
Our playmode cycles have never been this fast and a lot less time is spent creating/maintaining a poor-man's pseudo-playmode state for edit-time. We can just enter playmode now!

I don't disable scene-reloading, as that one caused trouble for us. But I'm already plenty happy with not having to reload the domain.

It would be awesome to see script recompilation cycles without having domain reloads, but this one is probably not feasible without .NET core.

After selecting "Enter Play Mode Settings" and hitting play for the first time, Unity just crashed to desktop straight away.

Stacktrace seems to point to an ECS / jobs issue.

[quote]
Entering Playmode with Reload Scene disabled. If you experience any issues, please disable "Enter Play Mode Options (Experimental)" in Editor Settings
Stacktrace:

at <0xffffffff>
at (wrapper managed-to-native) Unity.Jobs.LowLevel.Unsafe.JobsUtility.CreateJobReflectionData (System.Type,System.Type,Unity.Jobs.LowLevel.Unsafe.JobType,object,object,object) [0x0000e] in :0
at Unity.Jobs.LowLevel.Unsafe.JobsUtility.CreateJobReflectionData (System.Type,Unity.Jobs.LowLevel.Unsafe.JobType,object,object,object) [0x00008] in :0
at Unity.Jobs.IJobExtensions/JobStruct1<Unity.Entities.GatherChunksAndOffsetsJob>.Initialize () [0x0002d] in <dfc62061cb3f4b1bb57c5179a5db6830>:0
at Unity.Jobs.IJobExtensions.Schedule<Unity.Entities.GatherChunksAndOffsetsJob> (Unity.Entities.GatherChunksAndOffsetsJob,Unity.Jobs.JobHandle) [0x0000a] in <dfc62061cb3f4b1bb57c5179a5db6830>:0
at Unity.Entities.ComponentChunkIterator.PreparePrefilteredChunkLists (int,Unity.Entities.UnsafeMatchingArchetypePtrList,Unity.Entities.EntityQueryFilter,Unity.Jobs.JobHandle,Unity.Jobs.LowLevel.Unsafe.ScheduleMode,Unity.Collections.NativeArray
1&,void*&) [0x000dc] in D:\Documents\BovineLabs\Shattered\Library\PackageCache\com.unity.entities@0.1.1-preview\Unity.Entities\Iterators\ComponentChunkIterator.cs:857
at Unity.Entities.JobChunkExtensions.ScheduleInternal (Unity.Entities.GatherEntitiesJob&,Unity.Entities.EntityQuery,Unity.Jobs.JobHandle,Unity.Jobs.LowLevel.Unsafe.ScheduleMode) [0x0000f] in D:\Documents\BovineLabs\Shattered\Library\PackageCache\com.unity.entities@0.1.1-preview\Unity.Entities\IJobChunk.cs:111
at Unity.Entities.JobChunkExtensions.Schedule (Unity.Entities.GatherEntitiesJob,Unity.Entities.EntityQuery,Unity.Jobs.JobHandle) [0x00001] in D:\Documents\BovineLabs\Shattered\Library\PackageCache\com.unity.entities@0.1.1-preview\Unity.Entities\IJobChunk.cs:88
at Unity.Entities.ComponentChunkIterator.CreateEntityArray (Unity.Entities.UnsafeMatchingArchetypePtrList,Unity.Collections.Allocator,Unity.Entities.ArchetypeChunkEntityType,Unity.Entities.EntityQuery,Unity.Entities.EntityQueryFilter&,Unity.Jobs.JobHandle&,Unity.Jobs.JobHandle) [0x0002b] in D:\Documents\BovineLabs\Shattered\Library\PackageCache\com.unity.entities@0.1.1-preview\Unity.Entities\Iterators\ComponentChunkIterator.cs:251
at Unity.Entities.EntityQuery.ToEntityArray (Unity.Collections.Allocator) [0x00013] in D:\Documents\BovineLabs\Shattered\Library\PackageCache\com.unity.entities@0.1.1-preview\Unity.Entities\Iterators\EntityQuery.cs:524
at Unity.Entities.EntityQueryBuilder.ForEach (Unity.Entities.EntityQueryBuilder/F_EC`1) [0x00033] in D:\Documents\BovineLabs\Shattered\Library\PackageCache\com.unity.entities@0.1.1-preview\Unity.Entities\EntityQueryBuilder_ForEach.gen.cs:144
at BovineLabs.Vision.Systems.VisionEntityHybridSystem.OnUpdate () [0x00001] in D:\Documents\BovineLabs\Shattered\Packages\com.bovinelabs.vision\Vision\Systems\VisionEntityHybridSystem.cs:33
at Unity.Entities.ComponentSystem.InternalUpdate () [0x00048] in D:\Documents\BovineLabs\Shattered\Library\PackageCache\com.unity.entities@0.1.1-preview\Unity.Entities\ComponentSystem.cs:800
at Unity.Entities.ComponentSystemBase.Update () [0x0000d] in D:\Documents\BovineLabs\Shattered\Library\PackageCache\com.unity.entities@0.1.1-preview\Unity.Entities\ComponentSystem.cs:284
at Unity.Entities.ComponentSystemGroup.OnUpdate () [0x0002b] in D:\Documents\BovineLabs\Shattered\Library\PackageCache\com.unity.entities@0.1.1-preview\Unity.Entities\ComponentSystemGroup.cs:602
at Unity.Entities.ComponentSystem.InternalUpdate () [0x00048] in D:\Documents\BovineLabs\Shattered\Library\PackageCache\com.unity.entities@0.1.1-preview\Unity.Entities\ComponentSystem.cs:800
at Unity.Entities.ComponentSystemBase.Update () [0x0000d] in D:\Documents\BovineLabs\Shattered\Library\PackageCache\com.unity.entities@0.1.1-preview\Unity.Entities\ComponentSystem.cs:284
at Unity.Entities.ScriptBehaviourUpdateOrder/DummyDelegateWrapper.TriggerUpdate () [0x00001] in D:\Documents\BovineLabs\Shattered\Library\PackageCache\com.unCrash!!!
[/quote]

Is this meant to be supported yet? If it is I'll have a look at debugging it over the weekend to figure out if it's something I'm doing or something the package is doing. First look it seems to be crashing in one of my GameObjectConversion systems.

-edit-

After a clean restart, re-enabling the feature i can't seem to replicate the crash. Everything seems to work well. That I'm going to have to tweak one small section of code to be compliant.

Once released, Fast Enter-to-play should be the default mode going forward. I'm guessing within a year also? Once experienced the fast mode, there is no way you want to go back. In my opinion, it should've been the default mode from the beginning.

Well, it's better late than never and everyone should start thinking about the new ways of initializing the game. Anyway, big thanks for the initiative and I'm happy that it's a big step forward.

However, people will not optin unless 2019.3 releases (I find that too many developers say they will not touch beta Unity and I still think that you should've released it on 2019.2 as a preview if you really wanted feedbacks earlier) and don't despair for the lack of feedback. I'll try to push it once 2019.3 release and I'm sure you will have plenty of feedback soon enough. 2019.3 still on-track for mid-Nov?

I want to point out that we still have to get rid of AppDomain reload for the compilation; it will allow insanely fast compilation as well. Make the fast iteration complete.

As you already know it can be done by managing Assembly load/unload ONLY the changed assemblies through AssemblyLoadContext. I hope there is a team who is looking into this already.

One of the big problems AppDomain reload is that not only it takes a really long time to unload/reload whole Assemblies regardless it's changed or not, but Unity Editor will freeze if you are using native DLL with p/invoke, thus we can't use many native frameworks out there.

Please keep the good work.

Cheers!

6 Likes

Been using this for a few days. Great feature, but at the moment sadly very unstable at least for me.
I crash about 1 in 3 times when I make a code change.

Based off stack trace it appears to be the Burst asynchronous compiler.

2 Likes


Could you please file a bugreport - we'll ensure there is no crash.


Yes, com.unity.entities is supported since 0.2.0-preview.

nice post (especially the diagram with enter play mode lifecycle. should be put in the docs along with exit play mode and script compilation counterparts).

@alexeyzakharov could you also share some best practices about how to properly support this feature in code? (e.g. when to initialize serialized state vs non-serialized state vs static state etc...)

1 Like

[quote=“M_R”, post:8, topic: 763599]
nice post (especially the diagram with enter play mode lifecycle. should be put in the docs along with exit play mode and script compilation counterparts).
[/quote]
Thanks! Yes,we’ll add the diagram to a manual section here https://docs.unity3d.com/2019.3/Documentation/Manual/ConfigurableEnterPlayModeDetails.html

[quote=“M_R”, post:8, topic: 763599]
@alexeyzakharov could you also share some best practices about how to properly support this feature in code? (e.g. when to initialize serialized state vs non-serialized state vs static state etc…)
[/quote]
When updating internal projects to use the feature I kept those statements in mind:

  • Game initialization is driven by Awake/OnEnable and shutdown by OnDestroy/OnDisable - this is already a best practice for Unity games.

  • Editor scripts use OnEnable to alter Editmode/Playmode behavior.

  • Avoid using global static variables shared between editor and game scripts.

  • Prefer singletons which are initialized with Awake/RuntimeInitializeOnLoad to having a many scattered static variables.

  • Do heavy loading in Awake or in a lazy init way.

  • Add tests to ensure Editor functionality survives domain reload and playmode changes.

The procedure I’ve used is quite simple:

  • Enable the mode.

  • Enter Play Mode 2-3 time consecutively.

  • Check that there are no errors in the console.

  • Check that the game functions properly - requires help from the game team.

  • If there are errors or different behavior - enable scene reload.

  • Investigate which static variables might be carried to the next game session - move those to RuntimeInitializeOnLoad helper method.

I didn’t have issues with private fields serialization, but if you experienced those mark private array/lists game script references game script doesn’t care about with [NotSerialized] as [mention|+PS7cKhMl9amzehFfJu7jA==] mentioned here . (although, I still think we might be able to fix this).
Generally, skipping the domain reload is a huge win in iteration time and requires minimal code modifications (none in some cases if you were already following general MonoBehaviour lifecycly patterns) and ongoing support. Skipping scene reload saves extra time, but also requires extra fixes which are more workarounds unfortunately, and this is where the most of the work is to get the feature out of the experimental.

3 Likes

This is by far the biggest potential problem in an established codebase. Is there any possible way you guys could provide diagnostics that would help detect the problems in a mature codebase, without having to run through a game's myriad pathways to ensure some tiny static didn't sneak its way through? At least getting some detection from Unity's side would help us find these things more quickly and in a methodical way rather than hoping we have enough coverage.

5 Likes

I upgraded from burst 1.2.0p8 to 1.2.0p9 and disabled domain reloading again and have not yet been able to crash it in about 30min of use so far. Even trying hard doing some dodgy things.

The burst changelog for p9 somewhat related to the last exception in the stacktrace I was getting so I'm hoping it's all sorted. I'll continue testing and see how it goes.

1 Like

It would be nice if first-party Unity packages actually supported this, like the new input system. (case 1192017)

15 Likes

Yes! Please, please, that diagram is amazing -- I've had such a struggle with understanding how Editor scripts and OnValidate interact with pressing Play, Domain reload and whatnot.

FYI: URP's DebugManager can start giving an NRE sometimes when using this feature. To reiterate, it doesn't happen all the time. However once it starts happening, it happens every time one tries to enter play mode.

In case anyone else ends-up here, you can get it working again by disabling the "Enter Play Mode" feature, starting your game (it'll work fine) then stop and re-enable the feature. HTH

Skimming the code, I find myself wondering if it's after I do a non-debug build...? I'll keep that in mind. (DebugManager's Lazy-initialized constructor has a check for Debug.isDebugBuild which I'd hazard is leaving lying around an instance that /is/ constructed but has no members. (not checked.))

NullReferenceException: Object reference not set to an instance of an object
  at UnityEngine.Rendering.DebugManager.UpdateActions () [0x0001b] in D:\Users\User\Dev\Unity3D\LevelDesignTest2019\Library\PackageCache\com.unity.render-pipelines.core@7.1.6\Runtime\Debugging\DebugManager.Actions.cs:165
  at UnityEngine.Rendering.DebugUpdater.Update () [0x00001] in D:\Users\User\Dev\Unity3D\LevelDesignTest2019\Library\PackageCache\com.unity.render-pipelines.core@7.1.6\Runtime\Debugging\DebugUpdater.cs:18
(Filename: Library/PackageCache/com.unity.render-pipelines.core@7.1.6/Runtime/Debugging/DebugManager.Actions.cs Line: 165)

Really nice feature, started using it. Thanks!

So, we need to manage resetting some static variables and so on when they causing problems. I found that [RuntimeInitializeOnLoad] and [RuntimeInitializeOnLoadMethod] is not all-case usable for this because they firing when you go to playing mode, and when you go back to editing mode - they are not. I needed resetting variables in both cases and I ended up with this, attached to any persistent gameobject on scene:

#if UNITY_EDITOR
using UnityEngine;
using UnityEditor;

[ExecuteAlways]
public class DOMAINKEEPER : MonoBehaviour {
    void OnEnable() {
        if (EditorSettings.enterPlayModeOptionsEnabled &&
            EditorSettings.enterPlayModeOptions.HasFlag(EnterPlayModeOptions.DisableDomainReload)) {
            EditorApplication.playModeStateChanged += PlaymodeStateChange;
        }
    }
    void OnDisable() {
        EditorApplication.playModeStateChanged -= PlaymodeStateChange;
    }
    void PlaymodeStateChange(PlayModeStateChange playModeStateChange) {
        if (playModeStateChange == PlayModeStateChange.ExitingPlayMode ||
            playModeStateChange == PlayModeStateChange.ExitingEditMode) {
            // variables resetting code
        }
    }
}
#endif

It is working in my case, but did I do it right? Maybe it needs another event or switching from 'exiting' to 'entering' ones? What will be more closer to disabled domain reload moment in time?

upd: Checked the entering events, they does not fit.

1 Like

This is probably a better flow for detecting play vs editor mode. Although instead of just setting IsPlaying and EditorModeEnabled you would probably want those to fire events that other code can subscribe to. This was pulled from some code that was specific to handling ECS worlds transitions.

Why this is all so complex right now is just due to how the flow has evolved over time. Even Unity's own developers can't keep it straight. I've seen their own code where they document the flow inline in some feature just to remind themselves how it works (and comment on how nuts it is). Sort of a clue they should not be just making it more complex without stopping and fixing it.

#if UNITY_EDITOR
private static void OnPlaymodeChanged(UnityEditor.PlayModeStateChange change)
        {
            switch (change)
            {
                case UnityEditor.PlayModeStateChange.EnteredEditMode:
                    IsPlaying = false;
                    EditorModeEnabled = true;
                    break;
            }
        }
#endif

 [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
        private static void Init()
        {

            EditorModeEnabled = false;
            IsPlaying = true;

#if UNITY_EDITOR
            UnityEditor.EditorApplication.playModeStateChanged -= OnPlaymodeChanged;
            UnityEditor.EditorApplication.playModeStateChanged += OnPlaymodeChanged;
#endif
}
3 Likes

those property names are to die for

1 Like

I got frequent crashes on Enter Playmode as well when Reload Domain is disabled. Crashes in EntityDataAccess.AddComponent. Is a little harder to reproduce in a small project, but it seems to happen when quickly entering playmode while there is still some entity reload stuff going on.
Bug case is 1209152.

If a MonoBehaviour is decorated with the [ExecuteInEditMode] attribute, the Component does not receive its Awake() event when entering playmode, which is causing all sorts of issues for me. Tested with Unity 2019.3.0f4.

I added the following Code/Component to a GameObject and just pressed Play&Stop in the editor and checked the Console Window what events were triggered:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

//[ExecuteAlways]
[ExecuteInEditMode]
public class TestCode : MonoBehaviour
{
    void Awake()
    {
        Debug.LogFormat("Awake: isPlaying={0}", Application.isPlaying);
    }

    void Start()
    {
        Debug.LogFormat("Start: isPlaying={0}", Application.isPlaying);
    }

    void OnEnable()
    {
        Debug.LogFormat("OnEnable: isPlaying={0}", Application.isPlaying);
    }

    void OnDisable()
    {
        Debug.LogFormat("OnDisable: isPlaying={0}", Application.isPlaying);
    }

    void OnDestroy()
    {
        Debug.LogFormat("OnDestroy: isPlaying={0}", Application.isPlaying);
    }
}

First what works. If the Component does NOT use [ExecuteInEditMode], it receives these events, which make perfect sense to me:
5355819--541269--upload_2020-1-10_7-51-14.png

If the Component does use the [ExecuteInEditMode] attribute, events look like this:
5355819--541272--upload_2020-1-10_7-51-33.png

What happens when entering play mode, showed as "Press PLAY" here, does not look right to me.

When the new "Enter Play Mode" feature is enabled, the Component does not receive its Awake event when pressing play. I don't think this is on purpose, because Components without the [ExecuteInEditMode] attribute receive their Awake event, as well as when not using the new Enter Play Mode feature, in which case they receive their Awake event too.

Not having Awake for "isPlaying" requires me to move various one time initialization code from Awake to OnEnable and then implement more complex code to handle what happens when OnEnable is called multiple times as shown above.

Another smell when entering play mode is that "OnDisable" isn't called when isPlaying is still false. The problem with that is because OnEnable was called with isPlaying=false, yet when entering play mode we first receive the OnDisable with isPlaying=true event. These do not match, making it difficult to correctly intialize/deinitialize things.

When the new "Enter Play Mode" feature is disabled, the Component receives multiple OnDisable/OnEnable events when playmode is being entered. Is this on purpose? It does not cause issues for me, but why is Unity doing it? Maybe it makes enter play mode faster when OnDisable/OnEnable events are being sent only once.

2 Likes

Hi @Peter77 ,
Thanks for the good question and detailed test!

This is a result of disabled "Full Scene Reload".
I've changed the Awake behavior deliberately to be able to avoid Editor scripts initialization multiple times.
That is reflected on the diagram above (the skipped Awake part).

The reasoning behind that was to avoid second loading that is usually done in Awake. And the assumption was that given that script is marked with ExecuteAlways/ExecuteInEditMode it has to be able to use OnDisable/OnEnable to restart in order to be able to survive domain reload correctly. However, in case Awake has checks for playmode and does differrent init for play/edit modes, then yes, I can see the issue. Moreover, the same is applicable to scripts not marked with ExecuteAlways and get Awake on scene loading in EditMode.
It is quite challenging to provide a fast "reset" path for the scene and not break existing scripts. Basically the benefit from the whole scene reset diminishes then to be only "skip reading scene from disk and deserialization". Which for many of games is not the biggest contributor.


Do you have different init paths for the editor script in edit and playmode? How much time does it save for you when Scene Reload disabled?

We usually follow rule "don't do what you don't have to do", and a variant of it "don't do what is later not used" :). But sometimes it is almost impossible to follow it and not to break existing projects. We are discussing how to proceed with this change and the "fast scene reset".


It is a part of serialization during domain reload. And it would be beneficial to not call those callbacks, yes. But there are use cases when those are used to detect exact moment of scripts unload before domain reload or very first moment after domain is inited (before InitializeOnLoad and Awake). So we decided not to change existing behavior until we provide a clear and consistent lifecycle notifications.
We are using this feature internally and have a lot of feedback thanks to you and others who tried it and contributed. There are some fundamental questions we need to answer to ensure we have a right vision for a scalable playmode/simulation workflow in the Editor.

3 Likes