脚本目标彼此对话!

一款十分简略的游戏或许只依靠于一个脚本,在某个当地依靠于一个宏大的“管理器”目标。但事实上,我信任即便是像《Pong》这样基础的游戏也会基于不同的目标(游戏备注:如球,球拍,带有分数的UI等)运用不同的脚本去执行。

从开发者的角度来看,将你的逻辑分解成多个脚本,每个脚本都放在场景中的一个目标上,这是十分舒畅的,由于它能够协助你更好地构思场景中每个游戏目标的人物。经过遵从“关注点别离”原则,你能够保证你的代码库在游戏玩法拓展和增加更多目标时易于理解和维护。

可是,这也意味着你的各种脚本需求相互作用才能整合到完整的游戏玩法中。让咱们回到《Pong》,拥有一个四处移动但却不检查是否与球拍发生碰撞的球,或许一个不更新的UI计分板都是毫无用处的。

这便是为什么在Unity中创立游戏时,即便是最基本的游戏,你也需求找到一种办法让你的脚本相互沟通。

今日,咱们来看看3种办法!

办法1:运用GetComponent

好吧-首要,让咱们看看最简略的技术:运用Unity的内置 GetComponent() 函数,并直接拜访其他脚本。

基本上,经过在有它的目标上调用 GetComponent<MyScript>() ,您取得对c#实例的引证,该实例答应您调用其一切公共办法。

例如,假定我在场景中有一个“Manager”目标,在它上面我有这个 UIManager ,它处理我的Pong游戏的计分板。这个c#脚本包括一个 UpdateScores() 函数,用于更改界面中的当时分数标签。具体的完成细节并不重要——咱们只假定它正确地获取了两个玩家的当时分数,并相应地修改了UI标签:

using UnityEngine;public class UIManager : MonoBehaviour
{  
    public void UpdateScores() 
    {   
        _UpdatePlayer1Score();    
        _UpdatePlayer2Score();  
    }
}

由于这个函数是公共的,我能够很简略地调用它,只要我有一个引证到我的实例 UIManager 在场景中-我能够得到这个引证 GetComponent() ,像这样:

GameObject managerObject = GameObject.Find("Manager");
UIManager uiManager = managerObject.GetComponent<UIManager>();
uiManager.UpdateScores();

这段代码能够放在项目中的任何脚本中:无论如何它都能够作业,由于它从当时上下文中检索所需的一切,以引证恰当的财物并触发正确的逻辑。

所以,例如,我能够把它放在 BallManager 脚本中,这样每当球退出板时,分数就会更新:

using UnityEngine;
public class BallManager : MonoBehaviour 
{   
    public void ExitsBoard()
    {      
        GameObject managerObject = GameObject.Find("Manager"); 
        UIManager uiManager = managerObject.GetComponent<UIManager>(); 
        uiManager.UpdateScores();   
    }
}

当然,假如您的函数需求一些输入参数,它也能够作业。咱们完全能够经过选手的分数来协助咱们 UIManager 打印出来:

using UnityEngine;
public class UIManager : MonoBehaviour
{  
    public void UpdateScores(int player1Score, int player2Score) 
    {      
        _UpdatePlayer1Score(player1Score);   
        _UpdatePlayer2Score(player2Score);   
    }
}
using UnityEngine;
public class BallManager : MonoBehaviour 
{   
    private int _player1Score;
    private int _player2Score; 
    public void ExitsBoard()
    {      
        GameObject managerObject = GameObject.Find("Manager"); 
        UIManager uiManager = managerObject.GetComponent<UIManager>();
        uiManager.UpdateScores(_player1Score, _player2Score);
    }
}

注意:运用 _GetComponent()_ 并不局限于脚本之间的通讯!你能够运用 _GetComponent()_ 在玩家上引证“Rigidbody”组件,然后根据玩家的输入更新速度,或许运用“SpriteRenderer”让它在人物被击中时闪烁,等等。

所以 GetComponent() 是十分便利的-仅有的问题是,它不是很有效,它或许会导致一些优化问题,假如你在你的应用程序的要害路径运用它。

这便是为什么有另一种跨脚本发送消息和触发逻辑的办法:运用事情,更精确地说,运用 UnityEvent s。

办法2:运用UnityEvents

咱们说过 GetComponent() 很好,但效率也很低。但这仅仅问题之一。榜首种办法的另一个大问题是,它将脚本严密地耦合在一起!

当然,从技术上讲,您是在独自的c#文件中编写逻辑的。可是,您有必要保证与其他目标“同享”的函数是公共的,而且两端的原型是匹配的。而且,为了运用 GetComponent() ,你明显需求将它们作为场景中目标的组件。

换句话说,你的代码库的一切不同部分都依靠于彼此,乃至场景安排也很重要,这很烦人,对吧?

为了在必定程度上缓解这个问题并更好地别离逻辑,切换到事情或许会很有趣。一般的办法是:

  • 在包括逻辑的类中创立一个静态事情
  • Awake()Start() 钩子中界说事情
  • 为咱们的事情增加一个监听器,将它链接到一个回调函数
  • 最终从其他脚本调用它(以实践触发回调逻辑)

总而言之,咱们能够像这样重新制作 UIManagerBallManager :

using UnityEngine;
using UnityEngine.Events;
public class UIManager : MonoBehaviour 
{   
    public static UnityEvent scoresChanged; 
    private void Awake() 
    {  
        scoresChanged = new UnityEvent();
        scoresChanged.AddListener(_OnScoresChanged);
    }       
    private void _OnScoresChanged() 
    {      
        _UpdatePlayer1Score();    
        _UpdatePlayer2Score(); 
    }
}
using UnityEngine;
public class BallManager : MonoBehaviour
{   
    public void ExitsBoard()
    {       
        UIManager.scoresChanged.Invoke(); 
    }
}

您能够看到,它使 UIManager 略微杂乱了一些,但它也使更新 BallManager 中的分数变得愈加简略!咱们不再需求引证场景中的特定目标,只需求调用 Invoke()

现在,重要的是要理解咱们仍然需求在场景的某个当地实例化咱们的脚本,不然事情将无法真正可用。可是它改进了咱们的代码作用域,而且略微解开了逻辑的各个部分。

相同,假如需求的话,咱们能够经过声明 UnityEvent 和其他输入来传递一些额定的参数:

using UnityEngine;
using UnityEngine.Events;
public class UIManager : MonoBehaviour 
{   
    public static UnityEvent<int, int> scoresChanged; 
    private void Awake()
    {     
        scoresChanged = new UnityEvent<int, int>();  
        scoresChanged.AddListener(_OnScoresChanged); 
    }       
    private void _OnScoresChanged(int player1Score, int player2Score)
    {      
        _UpdatePlayer1Score(player1Score);      
        _UpdatePlayer2Score(player2Score);  
    }
}
using UnityEngine;
public class BallManager : MonoBehaviour
{  
    private int _player1Score; 
    private int _player2Score; 
    public void ExitsBoard() 
    {      
        UIManager.scoresChanged.Invoke(_player1Score, _player2Score); 
    }
}

此外,事情还具有一些十分棒的功用:它们答应你精确地命名和定位游戏循环的特定时间(游戏备注:例如“分数发生了变化”)。

此时,您能够看到 UnityEvent 很简略创立和设置,可是有必要一个接一个地界说和准备一切这些变量或许有点麻烦。假如您只要几个重要的触发器,那么您能够界说一些 UnityEvent ;可是假如你进入更杂乱的体系,更杂乱的沟通,你就会开始在任何当地积累越来越多的变量。

而且,更糟糕的是:由于这些变量是在项目中的特定脚本中界说的,所以你肯定需求在场景中的某个当地实例化该脚本以使事情可用。

这意味着,尽管您的体系没有 GetComponent() 那么杂乱,但这些事情仍然会在脚本之间创立某种方式的依靠联系。

可是,假如您有一个更集中的东西,能够在需求时处理事情的重新调度,并笼统出发射器和接收器的依靠联系,那会怎么样呢?这要归功于一种称为“大局事情管理器”的形式。

办法:运用大局事情管理器event manager

简而言之,大局事情管理器的想法是,不是直接在脚本中设置事情,而是创立一个名为 EventManager 的c#脚本,然后在场景中实例化一次。然后,一切事情的发射、存储和检索都将经过该实例完成。

一般,你能够像下面这样创立一个大局事情管理器类:

  • 首要,你需求创立一个c#字典来存储事情,例如用字符串作为事情名称的键:
using UnityEngine;
using UnityEngine.Events;
using System.Collections.Generic;
public class EventManager : MonoBehaviour
{   
    private Dictionary<string, UnityEvent> _events;
}
  • 然后,您将创立此 EventManager 的单例,以保证一直引证相同的仅有实例:
public class EventManager : MonoBehaviour
{  
    private Dictionary<string, UnityEvent> _events; 
    private static EventManager _eventManager;  
    public static EventManager instance
    {       
    get {    
            if (!_eventManager)
            {       
                _eventManager = FindObjectOfType(typeof(EventManager)) as EventManager; 
                if (!_eventManager)       
                    Debug.LogError("There needs to be one active EventManager script on a GameObject in your scene.");    
                else             
                    _eventManager.Init();     
            }         
                return _eventManager;     
            }   
        }   
    void Init() 
    {     
    if (_events == null)       
    _events = new Dictionary<string, UnityEvent>();   
    }
}
  • 现在,您已经准备好增加 AddListener()RemoveListener() 办法,以便能够运用事情名称界说接收器。
    AddListener() 函数中,您将实践创立 UnityEvent 目标,并在 _events 字典中注册它(假如它还不存在)。这样,假如没有人在监听这个事情,它实践上并没有被创立,也不会白白占用内存空间!:)
public class EventManager : MonoBehaviour
{   
    private Dictionary<string, UnityEvent> _events; 
    private static EventManager _eventManager; 
    public static EventManager instance { ... } 
    void Init() { ... }   
    public static void AddListener(string evtName, UnityAction listener) 
    {   
        UnityEvent evt = null;    
        if (instance._events.TryGetValue(evtName, out evt))
        {       
            evt.AddListener(listener);    
        }      
        else 
        {    
            evt = new UnityEvent();     
            evt.AddListener(listener);    
            instance._events.Add(evtName, evt);     
        }  
    }   
    public static void RemoveListener(string evtName, UnityAction listener) 
    {     
        if (_eventManager == null) return;   
        UnityEvent evt = null;  
        if (instance._events.TryGetValue(evtName, out evt))  
        evt.RemoveListener(listener);  
    }
}

当然,当咱们注册一个监听器时,咱们有必要传入要运用的回调函数,这是一个类型为 UnityAction 的参数(即一个简略的零参数办法)。

  • 最终,您只需求增加 Trigger() 函数,相同运用事情名称—明显,只要当事情在 _events 字典中注册时,咱们才会宣布该事情:
public class EventManager : MonoBehaviour
{  
    private Dictionary<string, UnityEvent> _events;  
    private static EventManager _eventManager; 
    public static EventManager instance { ... }  
    void Init() { ... }  
    public static void AddListener(string evtName, UnityAction listener)    { ... }   
    public static void RemoveListener(string evtName, UnityAction listener)    { ... }    
    public static void Trigger(string evtName) 
    {     
        UnityEvent evt = null;     
        if (instance._events.TryGetValue(evtName, out evt))   
        evt.Invoke();   
    }
}

您将注意到,咱们的其他脚本需求的一切办法(即 Trigger()AddListener()RemoveListener() )都是公共的和静态的。这意味着一旦咱们在场景中增加了这个脚本,那么咱们就不需求做任何其他事情来拜访事情体系,并在c#逻辑中宣布或接收事情:

using UnityEngine;
sing UnityEngine.Events;
public class UIManager : MonoBehaviour 
{   
    private void Awake()
    {    
        EventManager.AddListener("scores_changed", _OnScoresChanged);  
    }     
    private void _OnScoresChanged()
    {   
        _UpdatePlayer1Score();   
        _UpdatePlayer2Score(); 
    }
}
using UnityEngine;
public class BallManager : MonoBehaviour 
{  
    public void ExitsBoard() 
    {   
        EventManager.Trigger("scores_changed");   
    }
}

因而,这个大局事情管理器能够很简略地从代码库中的任何当地界说新事情,而且它提供了超弱耦合(由于仅有的要求是将咱们的 EventManager 放在场景中!)。

就像以前一样,咱们能够经过略微扩展 EventManager 类来处理带有数据的事情——咱们所要做的便是创立自己的 TypedEvent 类型(基于 UnityEvent<object> ,这样咱们就能够传入任何咱们想要的参数类型),并为承载数据的事情创立等效的办法:

[System.Serializable]
public class TypedEvent : UnityEvent<object> { }
public class EventManager : MonoBehaviour
{   
    private Dictionary<string, UnityEvent> _events; 
    private Dictionary<string, TypedEvent> _typedEvents; 
    private static EventManager _eventManager;  
    public static EventManager instance { ... }   
    void Init()
    {  
        if (_events == null)   
        _events = new Dictionary<string, UnityEvent>();  
        if (_typedEvents == null)      
        _typedEvents = new Dictionary<string, TypedEvent>(); 
    }  
    public static void AddListener(string evtName, UnityAction listener)    { ... }  
    public static void RemoveListener(string evtName, UnityAction listener)    { ... }   
    public static void Trigger(string evtName) { ... }  
    public static void AddListener(string evtName,                                   UnityAction<object> listener)   
    {   
TypedEvent evt = null;       
if (instance._typedEvents.TryGetValue(evtName, out evt))
{      
    evt.AddListener(listener);    
}     
else 
{   
    evt = new TypedEvent();     
    evt.AddListener(listener);     
    instance._typedEvents.Add(evtName, evt);     
}
    } 
    public static void RemoveListener(string evtName,UnityAction<object> listener)   
    {  
        if (_eventManager == null) return;     
        TypedEvent evt = null;    
        if (instance._typedEvents.TryGetValue(evtName, out evt)) 
        evt.RemoveListener(listener);  
    }  
    public static void Trigger(string evtName, object data)  
    {   
        TypedEvent evt = null;    
        if (instance._typedEvents.TryGetValue(evtName, out evt))  
        evt.Invoke(data);
    }
}

代码几乎是相同的,仅仅咱们的 UnityAction 回调现在需求一个 object 输入参数,而且咱们在 Invoke() 事情时传递这个额定的数据。

即便咱们在事情数据中只要一个参数,由于它的类型是 object ,假如需求的话,咱们实践上能够创立一个数组来传递多个参数:

using UnityEngine;
using UnityEngine.Events;
public class UIManager : MonoBehaviour 
{  
    private void Awake()
    {      
        EventManager.AddListener("scores_changed", _OnScoresChanged);  
    }      
    private void _OnScoresChanged(object data) 
    {    
        int[] scores = (int[])data;   
        _UpdatePlayer1Score(scores[0]);  
        _UpdatePlayer2Score(scores[1]);  
    }
}
using UnityEngine;
public class BallManager : MonoBehaviour 
{  
    private int _player1Score;  
    private int _player2Score;  
    public void ExitsBoard()
    {      
        EventManager.Trigger("scores_changed", new int[] {      _player1Score, _player2Score   });  
    }
}

这个东西十分灵活和强大,一旦你准备好了 EventManager c#脚本,那么剩下的代码库就能够十分轻松地运用事情了!:)

总结

解耦游戏体系十分重要,由于它能够协助你维护游戏的强健和模块化架构。另一方面,这也伴随着一种自然的权衡:你的各种体系需求相互讨论,这样你才能真正取得整体的游戏玩法逻辑。

要做到这一点,您有几个办法,其中有咱们在这里看到的3个办法:内置的 GetComponent()UnityEvent s,乃至是大局事情管理器。

那么,您以为怎么样:我是否忘记了其他用于脚本通讯的好办法?请留下你自己的定见