0
\$\begingroup\$

There is quite a lot of code involved in this so I'm going to keep this as basic as possible.

Scenario

I have a scene that loads & unloads other scenes to produce the environment I need. There are a number of Event Triggers and Listeners throughout. Long story short the HUD is listening for Level.LOADED which happens every time a new level is loaded in via LoadScene. The Level class is attached to each level scene via script component.

Problem

When level1 is unloaded and level2 is loaded the Level.LOADED event fires as planned. Where it gets weird is that the HUD, which has remained alive in a different scene the whole time, stops listening to Level.LOADED after level1 is unloaded.

I've logged all the events being listened for, during Level1 I see LEVEL_LOADED on the HUD, after level2 is loaded I do not see LEVEL_LOADED on the HUD anymore. I also logged everything that has run through StopListening() and I never see Level.LOADED go through there so I'm rather confused as to why it's missing from the listen-to list of the EventManager. Since Level.LOADED is a public static member of the Level class I wouldn't think that unloading Level1 would be an issue. I've also tried making Level.LOADED more global by moving it to the game level (persistent) with no success.

Question

Why does my HUD object stop listening for Level.LOADED once Level1 is unloaded?

Notes

  • I tried moving Level.LOADED to a class that is never loaded/unload with no success.

EDIT with "solution"

I found the issue, an unexpected release of EventManager when an adjacent scene is unloaded.

Consider my project heiarchy:

MainScene - persistent

  • EventManager script

GameScene - active, persistent

  • HUD

LevelX - changes per level

  • Level stuff

MainScene and GameScene are never unloaded. I eventually found out that the EventManager's OnDestroy is triggered when LevelX (any level) is unloaded. This destroys my event list and then on Init a new one is created, empty. I have NO IDEA how scene load / unload is effecting my EventManager class as it's on MainScene

My solution was to make my EventManager's eventDictionary a static variable, this way it does not get destroyed when the EventManager is destroyed.

This leads me to post another question. Why does my script from one (permanent) scene get destroyed from a script in a completely different scene when that scene is unloaded? I plan to post a completely separate question with a recreatable example demonstrating this new problem.

EDIT PS

The GameScene does not OnDestroy when LevelX unloads so I made MainScene the active scene and GameScene becomes destroyed on level unload. I thought this was the issue until I noticed that EventManager is still getting destroyed.

The answer I accepted is more relevant to the OP context. Thx to @Evorlor

Code

EventManager.cs

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

public class EventManager : MonoBehaviour
{
    private List<Event> eventDictionary;
    private static bool _errorDisplayed = false;

    private static EventManager eventManager;

    public static EventManager instance
    {
        get
        {
            if (!eventManager)
            {
                eventManager = FindObjectOfType(typeof(EventManager)) as EventManager;

                if (!eventManager)
                {
                    if (!EventManager._errorDisplayed)
                    {
                        EventManager._errorDisplayed = true;
                        Debug.LogWarning("There needs to be one active EventManger script on a GameObject in your scene.");
                    }
                }
                else
                {
                    eventManager.Init();
                }
            }

            return eventManager;
        }
    }

    void Init()
    {
        if (eventDictionary == null)
        {
            eventDictionary = new List<Event>();
        }
    }

    void OnDestroy() {
        EventManager.instance.eventDictionary.Clear();
    }

    public static bool HasListener(string eventName, Action<object> listener)
    {
        if (EventManager.instance == null
            || EventManager.instance.eventDictionary.Count <= 0) return false;

        bool ret = false;
        EventManager.instance.eventDictionary.ForEach(e =>
        {
            if (e.eventName == eventName && e.listener == listener)
            {
                ret = true;
            }
        });

        return ret;
    }

    public static void StartListening(string eventName, Action<object> listener)
    {
        if (!EventManager.instance)
        {
            Debug.LogError("EventManager has not been initted. Is there one added to your scene yet?");
            return;
        }

        //Debug.Log("EventManager.StartListening: " + eventName);

        Event e = new Event(eventName, listener);
        EventManager.instance.eventDictionary.Add(e);
    }

    public static void StopListening(string eventName, Action<object> listener)
    {
        //Debug.Log("EventManager.StopListening: " + eventName);

        //System.Diagnostics.StackTrace stackTrace = new System.Diagnostics.StackTrace();
        //Debug.Log(stackTrace.GetFrame(1).GetMethod().Name);

        if (EventManager.instance == null
            || EventManager.instance.eventDictionary.Count <= 0) return;

        EventManager.instance.eventDictionary.ForEach(e =>
        {
            if (e.eventName == eventName && e.listener == listener)
                EventManager.instance.eventDictionary.Remove(e);
        });

        //Debug.Log("event listener count: " + EventManager.instance.eventDictionary.Count);
    }

    public static void TriggerEvent(string eventName, object opts = null)
    {
        //Debug.Log("TriggerEvent: " + eventName);

        if (EventManager.instance == null) {
            Debug.LogError("This shouldn't happen unless gameplay stopped"); // @jkr
        }

        //Debug.Log("TriggerEvent length: " + EventManager.instance.eventDictionary.Count);

        EventManager.instance.eventDictionary.ForEach(e =>
        {
            //Debug.Log(e.eventName);
            if (e.eventName == eventName)
            {
                //if (eventName.Equals(Level.LOADED)) {
                //    Debug.Log(e.listener);
                //}
                e.listener.Invoke(opts);
            }
        });
    }
}

class Event
{
    public string eventName;
    public Action<object> listener;

    public Event(string eventName, Action<object> listener)
    {
        this.eventName = eventName;
        this.listener = listener;
    }
}

Level.cs

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

// @jkr

public class Level : MonoBehaviour {

    public static string BEGIN = "Level.BEGIN"; // countdown finished @jkr
    public static string FINISH = "Level.FINISH"; // time for the next level @jkr
    public static string LOADED = "Level.LEVEL_LOADED";

    private const int LEVEL_BEGIN_DELAY = 1;
    private const int LEVEL_FINISH_DELAY = 1;

    public static Level instance;

    private int _levelId;
    private int _timeLimit = 1;

    // Use this for initialization
    void Start () {
        Debug.Log("Level.Start: " + this.gameObject.scene.name);

        // self assign level id;
        string levelName = this.gameObject.scene.name;
        string id = levelName.Substring(6);
        this._levelId = int.Parse(id);

        if (Level.instance == this) {
            Debug.Log("Should never be set to the same instance, definitely check this out @jkr");
            return;
        }

        Level.instance = this;

        EventManager.StartListening(GameTimer.FINISH, this.GameTimerFinishHandler);

        //GameTimer.instance.StopWithSeconds(this._timeLimit);

        EventManager.TriggerEvent(Level.LOADED, null);
        Invoke("Begin", Level.LEVEL_BEGIN_DELAY);
    }

    void OnDestroy() {
        //Debug.Log("Level.OnDestroy: " + this.gameObject.scene.name);

        EventManager.StopListening(GameTimer.FINISH, this.GameTimerFinishHandler);
    }

    void GameTimerFinishHandler(object opts)
    {
        Invoke("Finish", Level.LEVEL_FINISH_DELAY);
    }

    void Begin() {
        EventManager.TriggerEvent("Level.BEGIN", new Dictionary<string, object>() { { "this", this } });
    }

    void Finish()
    {
        EventManager.TriggerEvent("Level.FINISH", new Dictionary<string, object>() { { "this", this } });
    }

    public static int TimeLimit {
        get {
            return Level.instance._timeLimit;
        }
    }

    public static int Id {
        get {
            return Level.instance._levelId;
        }
    }
}

HUD.cs

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

// @jkr

public class HUD : MonoBehaviour {

    private int _score = 0;

    // Use this for initialization
    void Start () {
        Debug.Log("HUD.Start");
        EventManager.StartListening(Level.LOADED, LevelLoadedHandler);

        //EventManager.StartListening(, PointsChangeHandler); // points

        EventManager.StartListening(GameTimer.START, GameTimerStartHandler);
        EventManager.StartListening(GameTimer.CHANGE, GameTimerChangeHandler);
        EventManager.StartListening(GameTimer.FINISH, GameTimerFinishHandler);

        EventManager.StartListening(Target.HIT, TargetHitHandler);

        this.LevelText = 0;
        this.TimeLeftText = 0;
        this.ScoreText = 0;
    }

    private void LevelLoadedHandler(object opts) {
        Debug.Log("HUD.LevelLoadedHandler: " + Level.Id);
        this.LevelText = Level.Id;

        //Debug.Log("Level Loaded; timeLimit: " + Level.TimeLimit);
        this.TimeLeftText = Level.TimeLimit;
        this.CaptionText = "Get Ready!";
    }

    private void TargetHitHandler(object opts) {
        IDictionary dopts = (IDictionary)opts;

        if (!dopts.Contains("this")) return;
        Target target = (Target)dopts["this"];

        if (!target) return;

        this.ScoreText += target.TargetValue;
    }

    private void GameTimerStartHandler(object opts) {
        this.GameTimerChangeHandler(opts);

        this.CaptionText = "";
    }

    private void GameTimerChangeHandler(object opts) {
        int timeLeft = GameTimer.instance.Seconds;

        this.TimeLeftText = timeLeft;
    }

    private void GameTimerFinishHandler(object opts)
    {
        this.GameTimerChangeHandler(opts);

        this.CaptionText = "Round Over";
    }

    private string CaptionText {
        set
        {
            //Debug.Log("CaptionText SET: " + value.ToString());
            Text t = GameObject.Find("Caption Text").GetComponent<Text>();

            //if (value.Equals(""))
            //{
            //    t.gameObject.SetActive(false);
            //    return;
            //}

            //t.gameObject.SetActive(true);
            t.text = value.ToString();
        }
    }

    private int LevelText {
        set
        {
            Text t = GameObject.Find("Level Text").GetComponent<Text>();
            t.text = value.ToString();
        }
    }

    private int TimeLeftText {
        set {
            Text t = GameObject.Find("Time Left Text").GetComponent<Text>();
            t.text = value.ToString();
        }
    }

    private int ScoreText
    {
        get {
            return this._score;
        }
        set
        {
            this._score += value;

            Text t = GameObject.Find("Score Text").GetComponent<Text>();
            t.text = this._score.ToString();
        }
    }
}
\$\endgroup\$
  • \$\begingroup\$ What I would do to troubleshoot this is set a debugger break-point in EventManager.TriggerEvent to check if the HUD is still in eventDictionary. When it is not, I would set break-points in StopListening and StartListening to check for registrations and un-registrations of the HUD. When it is still in there, I would keep debugging in steps until I find out something. Could you try this? What do you find? \$\endgroup\$ – Philipp Aug 21 '18 at 15:21
  • \$\begingroup\$ @Philipp Before the level change there are 22 listeners, after there are 5. As expected, many are unregistered via the` OnDestroy. The HUD singleton is still visible in 1. the heirarchy and 2. the Scene / Game. That said, no STOP is ever called for Level.LOADED` - It's almost like the item is garbage collected out of EventManager's list. I've never dealt with anything like this in c# so I'm not sure what to try next :/ \$\endgroup\$ – Jacksonkr Aug 21 '18 at 17:23
  • \$\begingroup\$ Where is the EventManager GameObject? Might there ever be more than one? Like one in each level, even if all but the first one created are ignored? \$\endgroup\$ – Ed Marty Aug 22 '18 at 11:59
  • \$\begingroup\$ @EdMarty I have Main Scene, Game Scene, and Level X. The Event Manager is in the Main Scene which is persistent the entire time. \$\endgroup\$ – Jacksonkr Aug 22 '18 at 13:32
2
\$\begingroup\$

I think your approach is too complicated. Unity SceneManager has built-in events for when a scene/level is loaded or unloaded. Unity also has two messages, OnEnable and OnDisable. They are called when a component is enabled or disabled, respectively. This is where you should register and unregister for events.

So what you should do is clean up all of the extra code used to listen for scenes/levels being loaded and unloaded. From there, you can add this code to HUD.cs:

/// <summary>
/// Unity will call this when script is enabled, but you can also register for events elsewhere
/// </summary>
private void OnEnable()
{
    SceneManager.sceneLoaded += OnSceneLoaded;
    SceneManager.sceneUnloaded += OnSceneUnloaded;
}

/// <summary>
/// Unity will call this when the script is disabled, but you can also unregister for events elsewhere
/// </summary>
private void OnDisable()
{
    SceneManager.sceneLoaded -= OnSceneLoaded;
    SceneManager.sceneUnloaded -= OnSceneUnloaded;
}

/// <summary>
/// Called from the event registered above
/// </summary>
/// <param name="scene">Scene which was loaded</param>
/// <param name="loadSceneMode">Whether this scene was loaded additively or as a single scene</param>
private void OnSceneLoaded(Scene scene, LoadSceneMode loadSceneMode) 
{
    Debug.Log(scene.name + " was loaded as a(n) " + loadSceneMode + " scene!");
}

/// <summary>
/// Called from the event registered above
/// </summary>
/// <param name="scene">Scene which was unloaded</param>
private void OnSceneUnloaded(Scene scene)
{
    Debug.Log(scene.name + " was unloaded!");
}

Be sure to include the UnityEngine.SceneManagement namespace.

In general, you want to use Unity's UnityAction for event handling. This is done like such:

//Class 1
public UnityAction MyAction;

private void CallAction()
{
    MyAction?.Invoke(); //In .NET 4+

    if(MyAction != null) //In .NET 2.5
    {
        MyAction();
    }
}

//Class 2
private void OnEnable()
{
    Class1.MyAction += DoIt;
}

private void OnDisable()
{
    Class1.MyAction -= DoIt;
}

private void DoIt()
{
    //This is called when CallAction() is called.
}
\$\endgroup\$
  • \$\begingroup\$ That helps with the Level.LOADED issue but I still can't get the rest of my Level events to play through so I'm stuck with the same odd issue :/ \$\endgroup\$ – Jacksonkr Aug 22 '18 at 14:41
  • 1
    \$\begingroup\$ I updated with some over-simplified code to show how you should be handling events in C#/Unity. You probably want to delete your EventManager all together \$\endgroup\$ – Evorlor Aug 22 '18 at 14:47

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.