【问题标题】:Execution Order of Scripts - Awake and OnEnable脚本的执行顺序 - Awake 和 OnEnable
【发布时间】:2019-10-19 15:39:42
【问题描述】:

我遇到了与脚本执行顺序相关的问题,尽管我是一位经验丰富的 Unity 开发人员,但我并不在意这一点。所以我想对此进行解释。

这是我的 MenuContoller 脚本代码:

public class MainMenuController : MonoBehaviour
{  
 [SerializeField] Text bestScoreText;
 [SerializeField] Toggle soundToggle;
 private void OnEnable()
 {
     Init();
 }
 private void Init()
 {
     if (GameManager.Instance == null)
         Debug.Log("null game manager");
     GameManager.Instance.PlayerLives = 0;
     bestScoreText.text = DataStorage.RetrieveBestScore().ToString("D5");
     SoundManager.Instance.IsSoundEnabled = DataStorage.RetrieveSoundStatus() == GameConstants.STAT_ON ? true : false;
     soundToggle.isOn = !SoundManager.Instance.IsSoundEnabled;
 }
}

这是我的 GameManager 脚本代码:

public class GameManager : MonoBehaviour
{
   private static GameManager instance;
   //
   private int levelIndex;
   private int gameScore;   
   private int playerLives;

   void Awake()
   {
      instance = this;
   }

   public static GameManager Instance
   {
      get
      {
          return instance;
      }
   }
}

我在执行期间收到 NullReferenceException:

现在我无法理解 - OnEnable 方法是如何在其他脚本的 Awake 方法之前执行的?

由于这个原因,我得到一个空引用异常。据我了解,所有脚本 Awake 方法都会先执行,然后在 OnEanble 调用项目的所有脚本之后执行。

请向我解释这一点,以便解决我的困惑。

【问题讨论】:

  • 在我看来是个 bug,这是什么统一版本?
  • Unity 版本 2018.4.9f1
  • 您是否注意到您没有提供 GameManager 脚本。
  • 我已经将 GameManager 脚本添加到一个 GameObject...
  • 在此处添加 GameManager 代码

标签: c# unity3d


【解决方案1】:

你没有正确实现单例模式,你应该考虑竞争条件,尤其是当涉及到统一的事件函数时。 正确的单例行为,如果不存在则创建一个实例,还允许您使用预定义的字段生成预定义的单例,也更容易实现其他单例,而无需重复您的代码(样板文件):

using UnityEngine;
public class SingletonPattern<T> : MonoBehaviour, ISingleton where T : MonoBehaviour
{
    #region Static Fields

    private static T instance = null;

    #endregion

    #region Fields
    [SerializeField]
    protected bool destroyOnLoad = true;
    private Transform m_transform = null;
    private GameObject m_gameObject = null;

    private bool m_isInitialized = false;

    #endregion

    #region Static Properties
    public static bool HasInstance
    {
        get { return instance != null; }
    }
    /// <summary>
    /// Gets the singleton instance which will be persistent until Application quits.
    /// </summary>
    /// <value>The instance.</value>
    public static T Instance
    {
        get
        {

            if (instance == null)
            {
                instance = FindObjectOfType<T>();
                // We need to create new instance
                if (instance == null)
                {
                    var _singletonType = typeof(T);
                    // First search in resource if prefab exists for this class
                    string _singletonName = _singletonType.Name;
                    GameObject _singletonPrefab = Resources.Load("Singleton/" + _singletonName, typeof(GameObject)) as GameObject;

                    if (_singletonPrefab != null)
                    {
                        Debug.Log(string.Format("[SingletonPattern] Creating singeton {0} using prefab",_singletonName));
                        instance = (Instantiate(_singletonPrefab) as GameObject).GetComponent<T>();
                    }
                    else
                    {
                        instance = new GameObject().AddComponent<T>();
                    }

                    // Update name 
                    instance.name = _singletonName;
                }
            }

            return instance;
        }

        private set
        {
            instance = value;
        }
    }

    #endregion

    #region Properties

    public Transform CachedTransform
    {
        get
        {
            if (m_transform == null)
            {
                m_transform = transform;
            }

            return m_transform;
        }
    }

    public GameObject CachedGameObject
    {
        get
        {
            if (m_gameObject == null)
            {
                m_gameObject = gameObject;
            }

            return m_gameObject;
        }
    }

    #endregion

    #region MonoCallbacks

    protected virtual void Awake()
    {
        if (instance != null && instance != this)
        {
            Destroy(gameObject);
        }

        if (!m_isInitialized)
        {
            Init();
        }
    }

    protected virtual void Start()
    { }

    protected virtual void Reset()
    {
        // Reset properties
        m_gameObject = null;
        m_transform = null;
        m_isInitialized = false;
    }

    protected virtual void OnEnable()
    { }

    protected virtual void OnDisable()
    { }

    protected virtual void OnDestroy()
    {
    }

    protected virtual void OnApplicationQuit()
    {
        if (instance == this)
        {
            instance = null;
        }
    }

    #endregion

    #region Methods

    protected virtual void Init()
    {
        // Set as initialized
        m_isInitialized = true;

        // Just in case, handling so that only one instance is alive
        if (instance == null)
        {
            instance = this as T;
        }
        // Destroying the reduntant copy of this class type
        else if (instance != this)
        {
            Destroy(CachedGameObject);
            return;
        }

        // Set it as persistent object
        if (!destroyOnLoad)
        {
            DontDestroyOnLoad(CachedGameObject);
        }
    }

    public void ForceDestroy()
    {

        // Destory
        Destroy(CachedGameObject);
    }

    #endregion
}

创建 GameManager 单例现在很容易:

public class GameManager : SingletonPattern<GameManager>
{

}

现在,如果您随时访问GameManager.Instance,它将创建尚未创建的实例,避免在统一的事件函数中维护竞争条件的麻烦。

如果您有一个GameManager 或任何具有您想在编辑器中预设的属性的单例,而在播放模式下创建的实例将不具备这些属性,则创建一个实例预制件并将其放置在名为 单例的文件夹中 在一个名为 Resources 的文件夹下,因为系统首先检查单例的预定义预制件是否存在并生成它,然后回退到创建一个新的游戏对象并将脚本附加到它.

【讨论】:

    【解决方案2】:

    在您显示的代码中,您尝试获取名为 GameManager 的实例,但后来下面的脚本被命名为 SoundManager

    你应该做的是在你的场景中有一个带有SoundManager脚本的游戏对象。

    然后在MainMenuController 中引用该游戏对象。例如序列化该字段:

    [SerializedField] GameObject soundManagerObj;
    

    然后像这样访问 SoundManager 脚本函数:

    soundManagerObj.GetComponent<SoundManager>().IsSoundEnabled();
    

    问题编辑后

    将 GameManager 转换为单例(使用 this 作为参考):

    public class GameManager : MonoBehaviour
    {
       public static GameManager instance = null;
    
       // Change this to public to access from the other script
       public int levelIndex;
       public int gameScore;   
       public int playerLives;
    
       void Awake()
       {
            if(instance == null)
                instance = this;
            else if (instance != this)
                Destroy(gameObject);
    
            // To keep this objectr from one scene to the next one      
            DontDestryOnLoad(gameObject)
       }
    }
    

    第二件事要考虑,不要在Init() 中使用Instace 和大写字母。而是:

    public class MainMenuController : MonoBehaviour
    {  
     [SerializeField] Text bestScoreText;
     [SerializeField] Toggle soundToggle;
     public GameObject gameManager;
    
     private void OnEnable()
     {
         Init();
     }
     private void Init()
     {
        if (GameManager.instance == null){
            Debug.Log("null game manager");
            Instantiate(gameManager);
        } 
        gameManager.playerLives = 0;
        //...
     }
    }
    

    【讨论】:

    • 对不起,误会我添加了错误的类代码,但现在我已经编辑了问题。
    • 我想通过单例实例变量访问类。
    • 基本上这是一个单场景游戏,在第一次运行时,控制台中显示空引用异常。因此,根据我的理解,GameManager 脚本不可能有多个实例......
    【解决方案3】:

    你的问题是:为什么 OnEnable() 在其他脚本中在 Awake() 之前执行,对吧?

    如果是,那我想我的回答会对你有所帮助,

    我多次遇到过这个问题,我认为这是因为场景中有许多脚本以及同一个“游戏对象”上有许多脚本,因此有时这些脚本会像有时一样在不同的帧中运行结束执行需要 2 或 3 帧,这就是为什么在某些游戏对象中,Awake 方法和 OnEnable 方法在第 1 帧中运行,而在其他游戏对象中,Awake 方法在第 2 或第 3 帧中运行。

    为了解决这个问题,通常我会从子游戏对象中的所有脚本中删除所有 OnEnable 方法,并将其合并到父游戏对象中的一个或多个主脚本中,例如游戏管理器脚本有一个 OnEnable 方法,我通过在游戏管理器中引用它来编写我想在其他子脚本中执行的所有代码。

    【讨论】:

    • 你完全了解了我最近遇到的情况。谢啦!您的回答帮助我可视化了确切的问题,我使用 WaitUntil(() => otherobject.instance != null); 使用协程解决了它然后在那个协程中做我的事情。它就像一个魅力。再次感谢您的帮助。
    猜你喜欢
    • 2017-08-23
    • 2017-03-14
    • 2015-02-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-10-26
    • 1970-01-01
    相关资源
    最近更新 更多