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();
}
}
}
EventManager.TriggerEvent
to check if the HUD is still ineventDictionary
. When it is not, I would set break-points inStopListening
andStartListening
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\$. 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\$