Forum rules - please read before posting.

void Awake() and AC save loading

edited November 2024 in Technical Q&A

Hey Chris, I hope you can shed a light on an issue I've detected.

My game has pretty strict NPC schedules that need to be followed perfectly, no matter when or how a scene is loaded. To account for this, a long time ago I created a script that interpolates the start time of a path, the expected arrival time, and resizes an AC path based on the current in-game time global variable. So if the player enters the scene before the start time, the path from point A to B is not changed, and an actionlist will then tell the NPC to follow the path. If the player enters the scene after the NPC is supposed to have started the path, but before the expected arrival time, the path nodes that the NPC is supposed to have already walked through are moved to the exact interpolated point along the path, and an actionlist will teleport/move the NPC from the relocated first node. If the player enter the scene after the expected arrival time, ALL nodes will have been moved to the position of the last node, so the same actionlist in practice works as a simple teleport.

Now, over a year ago (I can't remember exactly when, but I could possibly find out if necessary), an AC update broke this script. All calculations and manipulation of paths used to be done under void Start(), but with the AC update, if I remember correctly, the actionlist that runs on start of the scene started running BEFORE my script had the time to reorganize the paths, so the NPCs would start moving from the wrong place. At the time, I fixed this by changing void Start() to void Awake(). I worked on the game for months without noticing any issues related to this.

Now, after a rather long hiatus, I'm back to working on the game. I updated Unity to 6000.0.24f1 and AC to 1.81.7. I have noticed that on a normal playthrough (without saving/loading), my script works the same as it ever did. If in the previous scene the global time variable was 21500, upon changing scenes, my script in the new scene will automatically rearrange the paths to reflect that value. The problem happens when I load a save. I have the option "Always reload scene when loading a save file?" checked (so that the path component is returned to its original form before my script manipulates it). The problem here is that if I create the save at time == 21500, let the game run to 25000, then load the save, the scene will be reloaded, and my script will run BEFORE the global variable changes from 25000 to 21500.

I would like to suggest a minor change in AC: is it possible for all global settings (Global variables, inventories, menus, etc) to be loaded before the scene is reloaded, in this order:

  • Global variables, inventories, menus, etc.
  • Scene reloading (optional).
  • Scene-based data (character locations, etc).

Currently the order appears to be:

  • Scene reloading (optional).
  • Global settings and scene-based data.

Or, if you think this could cause other issues, please advise on how to get a script to run before the OnStart/OnLoad actionlists, but after all the data from a save is loaded?

I'm aware these systems are very complex (I remember us taking a while to troubleshoot the loading order of Component Variables on the Player prefab, which were sometimes loaded incorrectly after the OnStart/Onload cutscene), so any help is greatly appreciated.

Comments

  • I've considered moving the whole method out of void Awake() and calling it at the start of the OnStart/OnLoad actionlists (i.e. after everything is loaded but before the rest of the actionlist sets up the NPCs), but because this is a component attached to each path, I'd have to run an action for each and every path in the scene, which soon becomes cumbersome.

  • I'd be very hesitant against making any changes to the loading process, as there'd be major potential for knock-on issues.

    The exact order of loading is:

    • Scene reloading
    • Global settings
    • Scene data

    In terms of custom events, we have:

    • Event: OnBeforeSceneChange
    • Scene reloading
    • Event: OnInitialiseScene
    • Global settings
    • Scene data
    • Event: OnAfterChangeScene

    I can consider a third event, but it would need to go in between "Global settings" and "Scene data". Would this work as a hook for your initialisation code?

  • edited November 2024

    I wasn't sure it would be necessary to create a new hook, as long as OnAfterChangeScene ran after the scene data was loaded, but definitely before the OnStart/OnLoad actionlists. But when I tried to adapt my script to test this, I ran into a different problem.

    This is the relevant code that I added to my PathsSolution script:

    public Paths path;
    
        protected static List<PathsSolution> allPaths;
    
        public static List<PathsSolution> AllInstances
        {
            get
            {
                if (allPaths == null)
                    allPaths = new List<PathsSolution>();
                return allPaths;
            }
        }
    
        protected static void AddPath(PathsSolution item)
        {
            if (allPaths == null)
                allPaths = new List<PathsSolution>();
            if (allPaths.Contains(item))
                return;
            allPaths.Add(item);
        }
    
        void Awake()
        {
            if (path != null)
            {
                AddPath(this);
            }   
        }    
    
        public void SayHello()
        {
            Debug.Log(gameObject.name + " HELLO!");
        }
    
        public void SetAllPaths()
        {       
            foreach (PathsSolution path in AllInstances)
            {
                path.SetPath();
            }
        }
    
        public void SetPath() 
        {
    
            SayHello();
        // Rest of the code to set the path up here, omitted here for simplicity.
    
        }
    

    I attached a PathsSolution component to the player prefab, and left its Paths field empty (so that it wouldn't add itself to the allPaths list). Then I created an OnAfterChangeScene event, and made an action that sends the player prefab a "SetAllPaths" message.

    When I click play, all the PathsSolution objects in the scene instantly "say hello", as expected. However, when I change scenes (either by entering a new scene, returning to the initial one or loading the game), it throws this error:

    MissingReferenceException: The object of type 'PathsSolution' has been destroyed but you are still trying to access it.
    Your script should either check if it is null or you should not destroy the object.
    UnityEngine.Object+MarshalledUnityObject.TryThrowEditorNullExceptionObject (UnityEngine.Object unityObj, System.String parameterName) (at :0)
    UnityEngine.Bindings.ThrowHelper.ThrowNullReferenceException (System.Object obj) (at :0)
    UnityEngine.Component.get_gameObject () (at :0)
    PathsSolution.SayHello () (at Assets/_EverAfter/Scripts/PathsSolution.cs:65)
    PathsSolution.SetPath () (at Assets/_EverAfter/Scripts/PathsSolution.cs:87)
    PathsSolution.SetAllPaths () (at Assets/_EverAfter/Scripts/PathsSolution.cs:81)
    UnityEngine.GameObject:BroadcastMessage(String, SendMessageOptions)
    AC.ActionSendMessage:Run() (at Assets/AdventureCreator/Scripts/Actions/ActionSendMessage.cs:85)
    AC.d__47:MoveNext() (at Assets/AdventureCreator/Scripts/ActionList/ActionList.cs:470)
    UnityEngine.MonoBehaviour:StartCoroutine(IEnumerator)
    AC.ActionList:ProcessAction(Int32) (at Assets/AdventureCreator/Scripts/ActionList/ActionList.cs:412)
    AC.ActionList:ProcessActionEnd(ActionEnd, Int32, Boolean) (at Assets/AdventureCreator/Scripts/ActionList/ActionList.cs:617)
    AC.ActionList:EndAction(Action) (at Assets/AdventureCreator/Scripts/ActionList/ActionList.cs:576)
    AC.d__47:MoveNext() (at Assets/AdventureCreator/Scripts/ActionList/ActionList.cs:540)
    UnityEngine.MonoBehaviour:StartCoroutine(IEnumerator)
    AC.ActionList:ProcessAction(Int32) (at Assets/AdventureCreator/Scripts/ActionList/ActionList.cs:412)
    AC.RuntimeActionList:BeginActionList(Int32, Boolean) (at Assets/AdventureCreator/Scripts/ActionList/RuntimeActionList.cs:181)
    AC.ActionList:Interact(Int32, Boolean) (at Assets/AdventureCreator/Scripts/ActionList/ActionList.cs:283)
    AC.RuntimeActionList:DownloadActions(ActionListAsset, Conversation, Int32, Boolean, Boolean, Boolean) (at Assets/AdventureCreator/Scripts/ActionList/RuntimeActionList.cs:136)
    AC.AdvGame:RunActionListAsset(ActionListAsset, Conversation, Int32, Boolean, Boolean) (at Assets/AdventureCreator/Scripts/Static/AdvGame.cs:257)
    AC.AdvGame:RunActionListAsset(ActionListAsset, Int32, Int32) (at Assets/AdventureCreator/Scripts/Static/AdvGame.cs:178)
    AC.ActionListAsset:Interact() (at Assets/AdventureCreator/Scripts/ActionList/ActionListAsset.cs:559)
    AC.EventBase:Run(Object[]) (at Assets/AdventureCreator/Scripts/Events/EventBase.cs:126)
    AC.EventSceneSwitch:OnAfterChangeScene(LoadingGame) (at Assets/AdventureCreator/Scripts/Events/Events/EventSceneSwitch.cs:70)
    AC.EventManager:Call_OnAfterChangeScene(LoadingGame) (at Assets/AdventureCreator/Scripts/Managers/EventManager.cs:2201)
    AC.d__52:MoveNext() (at Assets/AdventureCreator/Scripts/Save system/SaveSystem.cs:880)
    UnityEngine.MonoBehaviour:StartCoroutine(IEnumerator)
    AC.SaveSystem:InitAfterLoad(Int32) (at Assets/AdventureCreator/Scripts/Save system/SaveSystem.cs:777)
    AC.MultiSceneChecker:RunStartProcess() (at Assets/AdventureCreator/Scripts/Game engine/MultiSceneChecker.cs:122)
    AC.MultiSceneChecker:Start() (at Assets/AdventureCreator/Scripts/Game engine/MultiSceneChecker.cs:54)

    I have tried doing this with a scene object as well - the action saves its constant ID correctly, then when I change scenes it can't find the constant ID (correctly, because the object isn't in the scene), and when i return to the original scene, it STILL throws the error above, even though an object with that constant ID very much still exists.

    Any idea why this would be happening?

  • Are you unhooking from the event in your OnDisable function? This is a necessary step to prevent events firing on deleted objects.

    The approach I'd look into myself would be to have a single instance of a script in the scene that hooks into OnAfterChangeScene, uses this to handle all the PathSolution business, and then manually calls any "OnLoad" cutscenes previously (now unset) from the Scene Manager, i.e.:

    public Cutscene[] onLoadCutscenes;
    
    void OnAfterChangeScene (LoadingGame loadingGame)
    {
        // Update pathsolutions
    
        if (loadingGame != LoadingGame.No)
            foreach (var cutscene in onLoadCutscenes) { cutscene.Interact (); }
    }
    
  • Thanks! I wasn't hooking it through code, I used the Events Editor to create a OnAfterChangeScene event and run an actionlist that sent a message to my script.

    What I was having issues with was my code above. It was supposed to create a list, which each instance of PathsSolution would add itself to, and then I could use OnAfterChangeScene to iterate through that list, running their SetPath() functions. For some reason that I haven't figured out yet, creating a list of all paths to be iterated through only worked the first time I clicked play - all subsequent times the scene was loaded (either via save or scene switch), the console gave me the reference exception instead.

    I tried your script above, but because the issue I was having was exactly with how to update all PathSolutions objects in the scene at once, I couldn't get it to work.

    The last thing I tried was simply to use OnAfterChangeScene instead of void Start or Awake in the PathsSolution script itself. I was worried this could cause running order issues (which you did account for in your code, but I couldn't include that part because I didn't want every single instance to run the load cutscene).

    That seemed to work though. The path updates seem to always be happening before the OnLoad or OnStart cutscenes sends the NPCs on their paths. I worry this is not guaranteed (depending on PC performance or other unknown factors), so ideally I would use the code you provided above, but I'm a bit stuck. Is my worry correct, or can I trust the order will always be right?

    Anyway, if the order is not guaranteed, instead of trying to create a list by having each instance add itself to it, should I just use FindObjectsOfTypeAll in the external script, iterate through that list to run the SetPath() function, and then run the the load cutscene?

    If so, I was also wondering how to account for Start and Load cutscenes (not just Load ones). I have one of each. The OnStart cutscene runs on start and then calls the OnLoad one at the end. The OnLoad one of course just runs on load.

  • Anyway, if the order is not guaranteed, instead of trying to create a list by having each instance add itself to it, should I just use FindObjectsOfTypeAll in the external script, iterate through that list to run the SetPath() function, and then run the the load cutscene?

    AC has a few hooks of its own into the OnAfterChangeScene event, so by default I wouldn't call the order guaranteed. You should be safe, however, so long as the script is present in the scene file, and has a negative default execution order (so that it registers the event before AC does).

    Otherwise, FindObjectsOfType would be the way I'd go about it - so long as you know all the objects you want to affect are in the scene at the time.

    If so, I was also wondering how to account for Start and Load cutscenes (not just Load ones). I have one of each. The OnStart cutscene runs on start and then calls the OnLoad one at the end. The OnLoad one of course just runs on load.

    You can amend the script to have a separate array of "on start" cutscenes and run them if the loadingGame != LoadingGame.No block is not executed.

  • Cheers, all good now!

Sign In or Register to comment.

Howdy, Stranger!

It looks like you're new here. If you want to get involved, click one of these buttons!

Welcome to the official forum for Adventure Creator.