【问题标题】:Wait for GetComponent() to load等待 GetComponent() 加载
【发布时间】:2021-07-29 17:26:36
【问题描述】:

在我的程序刚启动后调用 GetComponent() 时,我发现该方法有时不能足够快地返回组件,以防止稍后代码尝试访问组件成员变量时出现空引用异常。我的问题是 - 有没有办法等待 GetComponent 完成找到它需要的东西?我知道可以使用协程等待,但是还有其他方法可以通过某种 lambda 回调或事件来做到这一点吗?

public class GameManager : MonoBehaviour
{
    public bool AutosaveEnabled = true;
    public static GameManager Instance;
    [HideInInspector] public InputManager InputManager;
    [HideInInspector] public UIManager UIManager;
...

    private void Awake()
    {
        Setup();
    }

    public void Setup()
    {
        if (Instance == null)
        {
            Instance = this;
        }
    else
    { 
         throw new Exception();
    }

        UIManager = GetComponent<UIManager>();
...
        UIManager.Setup();
...
}


public class UIManager : StateMachine, IUIManager
{
    public static UIManager Instance;
    public ITitleMenu TitleMenu;    

    public void Setup()
    {
        if (Instance == null)
        {
            Instance = this;
        }
    else
    { 
         throw new Exception();
    }

        TitleMenu = GetComponentInChildren<ITitleMenu>();

    }


    private void SetupScene()
    {
       UIManager.Instance.ChangeState<TitleMenuState>();
    }
...
}

public interface ITitleMenu : IMenu
{
    void ExitGame();
    void LoadTitleMenuScene();    
    void OnNewGameClick();
}

public interface IMenu
{
    public void Setup(IUIManager uiManager);
    public void SetActive(bool toggle);
    int selectedIndex { get; set; }
    int previouslySelectedIndex { get; set; }
    TextMeshProUGUI[] Options { get; set; }
    void OnControllerMoveDown();
    void OnControllerMoveUp();
    void OnControllerConfirm();
}


public class TitleMenu : MenuBase, ITitleMenu
{
    private enum MenuElements { Continue, NewGame, Controls, VideoSettings, AudioSettings, ExitGame };

    public void Setup(IUIManager uiManager)
    {
        this.uiManager = uiManager;
        DataManager.Instance.SaveFileMetadata = GameManager.Instance.SaveFileManager.GetSaveFileMetadata();
        if (DataManager.Instance.SaveFileMetadata.Count > 0)
        {
            Options[(int)MenuElements.Continue].transform.parent.gameObject.SetActive(true);
            selectedIndex = (int)MenuElements.Continue;
        }
        else
        {
            Options[(int)MenuElements.Continue].transform.parent.gameObject.SetActive(false);
            selectedIndex = (int)MenuElements.NewGame;
        }

        previouslySelectedIndex = selectedIndex;
    }
...
}

public class StateMachine : MonoBehaviour, IStateMachine
{
    public virtual State CurrentState
    {
        get
        {
            return _currentState;
        }
        set
        {
            if (_currentState == value)
            {
                return;
            }

            if (_currentState != null)
            {
                _currentState.Exit();
            }

            _currentState = value;

            if (_currentState != null)
            {
                _currentState.Enter();
            }
        }
    }
    protected State _currentState;

    public virtual T GetState<T>() where T : State
    {
        T target = GetComponent<T>();
        if (target == null)
        {
            target = gameObject.AddComponent<T>();
            target.Initialize();
        }
        return target;
    }
}
...


public class TitleMenuState : UIState
{
    protected override void OnMove(object sender, InfoEventArgs<Vector2> e)
    {
        if (e.info.y == 1)
        {
            owner.TitleMenu.OnControllerMoveUp();
        }
        else if (e.info.y == -1)
        {
            owner.TitleMenu.OnControllerMoveDown();
        }
    }

    protected override void OnInteract(object sender, EventArgs e)
    {
        owner.TitleMenu.OnControllerConfirm();
    }

    public override void Enter()
    {
        owner.TitleMenu.SetActive(true);
        owner.TitleMenu.Setup(owner);
        EventManager.UIMoveEvent += OnMove;
        EventManager.UISubmitEvent += OnInteract;
    }

    public override void Exit()
    {
        UIManager.Instance.TitleMenu.SetActive(false);
        EventManager.UIMoveEvent -= OnMove;
        EventManager.UISubmitEvent -= OnInteract;
    }
}

public abstract class State : MonoBehaviour
{
    public virtual void Enter()
    {
    }

    public virtual void Exit()
    {
    }

    public virtual void Initialize()
    {
    }

}


public abstract class UIState : State
{
    protected UIManager owner;

    public override void Initialize()
    {
        owner = UIManager.Instance;
    }

    protected virtual void OnInteract(object sender, EventArgs e)
    {
    }

    protected virtual void OnCancel(object sender, EventArgs args)
    {
    }

    public override void Enter()
    {
    }

    public override void Exit()
    {
    }

    protected virtual void OnMove(object sender, InfoEventArgs<Vector2> e)
    {

    }

    public virtual bool IsStateOfType(UIStates state)
    {
        return false;
    }
}

现在游戏在我调用 owner.TitleMenu.SetActive() 的 TitleMenuState.Enter() 中崩溃,因为 TitleMenu 为空。

层次结构:

【问题讨论】:

  • GetComponent() 不是异步的。它是一个阻塞调用,如果组件存在,它将被返回...
  • 这个问题对我来说没有任何意义。 GetComponent 返回一个组件。在它能够返回一个之前,它不会返回。它当然永远不会返回 null ,然后再返回一些东西......我不知道它在 c# 中是如何工作的。我错过了什么?
  • 对于初学者 - 你在哪里运行 GetComponent? “其他代码”在哪里运行?如果“其他代码”在主线程中运行(这意味着它只是在 before GetComponent 甚至启动之前被调用),那么 Omi 的答案可能就足够了。但是从问题如何表明事情同时发生......如果“其他代码”在非主线程中运行......那么其他同步方法可能是必要的。需要更多细节
  • Unity 不允许其 API 的一部分被主线程以外的线程使用。 GetComponent 是如果在主线程以外的任何东西上使用它将彻底崩溃的方法之一。它也是同步的,立即返回组件(或 null,如果这样的组件不在游戏对象上)。
  • 以上代码中没有任何内容调用UIManager.Setup,因此如果代码按原样运行,则可能是 owner.TitleMenu 将始终为 null

标签: c# unity3d


【解决方案1】:

TitleMenu = GetComponentInChildren&lt;ITitleMenu&gt;();GameManager 游戏对象的UIManager 组件中运行时,子游戏对象TitleMenu 处于非活动状态,而该子游戏对象上带有ITitleMenu。并且,来自the documentation on GetComponentInChildren(强调我的):

使用深度优先搜索返回 GameObject 或其任何子对象中 Type 类型的组件。

只有在活动的 GameObject 上找到组件才会返回。

所以这将返回null。这与未能“足够快”地返回无关。

一个非常简单的解决方法是使用GetComponentsInChildren,它有一个可选的includeInactive 参数,允许搜索非活动对象。使用GetComponentsInChildren,以includeInactive为真应该有预期的结果,只需要索引第一个元素(因为它返回一个数组):

public class UIManager : StateMachine, IUIManager
{
    public static UIManager Instance;
    public ITitleMenu TitleMenu;    

    public void Setup()
    {
        if (Instance == null)
        {
            Instance = this;
        }
    else
    { 
         throw new Exception();
    }

    TitleMenu = GetComponentsInChildren<ITitleMenu>(true)[0];

}

【讨论】:

    【解决方案2】:

    您应该在使用组件之前调用 GetComponent()。您还可以查看脚本执行顺序菜单(编辑 - 项目设置 - 脚本执行顺序)

    【讨论】:

    • 在哪里调用 GetComponent 并不重要,因为只要它附加到相关的 GameObject,它就会返回组件。竞争条件与此无关。
    • 我离开时没有代码或图片。而且,这也很重要。尝试在 Awake() 中使用组件,但在 Start() 中调用它。
    • 这就是引用在 C# 中的工作方式,它与 GetComponent 无关。如果您使用 lazy-init 属性 来引用该组件,您可以在 StartAwake 中调用它,如果该组件附加到 GameObject,它将始终返回该组件。
    • 脚本执行顺序应该在极少数情况下使用,即您的脚本被许多其他脚本使用但需要先初始化。但即便如此,也有更好的模式,比如拥有各种根脚本,以确保所有场景都已加载,然后以受控顺序处理组件的初始化,并将逻辑传播到具有协同程序的多个帧,以避免长时间加载屏幕冻结(简单游戏的杀伤力过大,但大型游戏必须如此)。
    猜你喜欢
    • 2011-11-16
    • 2016-07-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-21
    • 2014-07-10
    • 2020-03-08
    相关资源
    最近更新 更多