如何避免Unity中紧密的脚本耦合?


24

不久前,我开始与Unity合作,但我仍然在紧密耦合脚本的问题上挣扎。如何构造代码以避免出现此问题?

例如:

我想在单独的脚本中拥有健康和死亡系统。我还希望拥有不同的可互换步行脚本,这些脚本可让我更改玩家角色的移动方式(如Mario中的基于物理的惯性控件,以及Super Meat Boy中的紧致,抽搐控件)。Health脚本需要保留对Death脚本的引用,以便在玩家的Health达到0时可以触发Die()方法。Death脚本应该保留对所使用的walking脚本的一些引用,以禁止在死亡时行走(I厌倦了僵尸)。

通常创建界面,比如IWalkingIHealthIDeath,这样我就可以改变在心血来潮这些元素没有打破我的代码的其余部分。我希望它们由播放器对象上的单独脚本设置,例如PlayerScriptDependancyInjector。也许这脚本将会为社会IWalkingIHealthIDeath属性,使依赖可以通过从检查关卡设计师通过拖放相应的脚本进行设置。

这样一来,我可以轻松地将行为添加到游戏对象中,而不必担心硬编码的依赖项。

Unity中的问题

问题是,在团结,我不能在检查暴露的接口,如果我写我自己的检查人员,引用不会得到序列化,这是一个很大的不必要的工作。这就是为什么我只剩下编写紧密耦合的代码的原因。我的Death脚本公开了对脚本的引用InertiveWalking。但是,如果我决定要让玩家角色紧密控制,就不能只拖放TightWalking脚本,就需要更改Death脚本。糟透了 我可以应付,但是每次我做这样的事情,我的灵魂都会哭。

Unity中接口的首选替代方法是什么?我该如何解决这个问题?我找到,但是它告诉了我我已经知道的,并且没有告诉我如何在Unity中做到这一点!也讨论了应该做什么,而不是如何做,也没有解决脚本之间紧密耦合的问题。

总而言之,我觉得这些是为那些具有游戏设计背景来到Unity的人而写的,他们只是学习如何编写代码,而对于普通开发人员而言,关于Unity的资源很少。有什么标准的方法可以在Unity中构造代码,还是我必须弄清楚自己的方法?


1
The problem is that in Unity I can't expose interfaces in the inspector我不理解您所说的“在检查器中公开界面”是什么意思,因为我的第一个想法是“为什么不呢?”
jhocking

@jhocking询问统一开发人员。您只是在检查器中看不到它。如果将其设置为公共属性,则其他属性不会出现在此处。
KL 2015年

1
哦,您的意思是将接口用作变量的类型,而不仅仅是使用该接口引用对象。
jhocking


1
@jzx我确实知道并使用SOLID设计,而且我是Robert C. Martins书籍的忠实拥护者,但是这个问题是关于如何在Unity中做到这一点的。不,我没有读过这篇文章,现在正在阅读,谢谢:)
KL

Answers:


14

有几种方法可以避免脚本紧密耦合。Unity的内部是一个SendMessage函数,该函数以Monobehaviour的GameObject为目标时,会将该消息发送到游戏对象上的所有内容。因此,您的健康对象中可能会有这样的内容:

[SerializeField]
private int _currentHealth;
public int currentHealth
{
    get { return _currentHealth;}
    protected set
    {
        _currentHealth = value;
        gameObject.SendMessage("HealthChanged", value, SendMessageOptions.DontRequireReceiver);
    }
}

[SerializeField]
private int _maxHealth;
public int maxHealth
{
    get { return _maxHealth;}
    protected set
    {
        _maxHealth = value;
        if (currentHealth > _maxHealth)
        {
            currentHealth = _maxHealth;
        }
    }
}

public void ChangeHealth(int deltaChange)
{
    currentHealth += deltaChange;
}

这确实简化了,但应该准确显示我的意思。该属性像事件一样引发消息。然后,您的死亡脚本将拦截该消息(如果没有消息可拦截,则SendMessageOptions.DontRequireReceiver将保证您不会收到异常),并在设置好新的运行状况值之后执行操作。像这样:

void HealthChanged(int newHealth)
{
    if (newHealth <= 0)
    {
        Die();
    }
}

void Die ()
{
    Debug.Log ("I am undone!");
    DestroyObject (gameObject);
}

但是,不能保证顺序。根据我的经验,它总是从GameObject上的最高脚本到最低的脚本,但是我不会依靠您自己无法观察到的任何东西。

您还可以研究其他解决方案,这些解决方案正在构建一个自定义GameObject函数,该函数获取Components并仅返回那些具有特定接口而不是Type的组件,这会花费更长的时间,但可以很好地满足您的目的。

此外,您可以编写一个自定义编辑器,以在实际提交分配之前检查分配给该接口的编辑器中某个对象的对象(在这种情况下,您将像正常情况一样分配脚本,以使其可以序列化,但会引发错误)如果未使用正确的界面并拒绝分配)。我之前已经做过这两项工作,可以生成该代码,但是需要一些时间才能首先找到它。


5
SendMessage()确实解决了这种情况,我在许多情况下都使用了该命令,但是像这样的无目标消息系统的效率要比直接引用接收者低。您应谨慎使用此命令来进行对象之间的所有常规通信。
jhocking

2
我个人非常喜欢事件和委托的使用,在这些事件和委托中,组件对象了解更多的内部核心系统,但是核心系统对组件没有任何了解。但是我不是百分百确定这是他们希望设计答案的方式。因此,我回答了一些如何使脚本完全脱钩的问题。
Brett Satoru

1
我还相信,单位资产商店中有一些可用的插件可以暴露Unity Inspector不支持的接口和其他编程结构,值得快速搜索一下。
马修·皮格拉姆

7

我个人从未使用过SendMessage。SendMessage在组件之间仍然存在依赖关系,显示得很差并且很容易破坏。使用接口和/或委托确实消除了曾经使用SendMessage的需要(这比较慢,尽管直到需要时才考虑)。

http://forum.unity3d.com/threads/free-vfw-full-set-of-drawers-savesystem-serialize-interfaces-generics-auto-props-delegates.266165/

用这个家伙的东西。它提供了大量的编辑器内容,例如公开的接口和AND委托。这样有很多东西,或者您甚至可以自己制作。无论您做什么,都不要因为Unity缺少脚本支持而损害您的设计。这些东西默认情况下应该在Unity中。

如果这不是一个选择,请拖放单体行为类,然后将其动态转换为所需的接口。这是更多的工作,而不是优雅的,但是可以完成工作。


2
就我个人而言,我对这样的低级内容过于依赖插件和库感到不安。我可能有点偏执,但是感觉项目中断的风险太大,因为在Unity更新后它们的代码中断了。
jhocking

我一直在使用该框架,并最终停止了运行,因为我有一个很好的理由@jhocking提到了我的想法(它并没有帮助从一个更新到另一个更新,它从一个编辑器助手到一个包含所有内容的系统但厨房水槽同时破坏了向后兼容性[并删除了一个或两个相当有用的功能])
Selali Adob​​or

6

我想到的一种特定于Unity的方法是首先GetComponents<MonoBehaviour>()获取脚本列表,然后将这些脚本转换为特定接口的私有变量。您想在依赖注入程序脚本的Start()中执行此操作。就像是:

MonoBehaviour[] list = GetComponents<MonoBehaviour>();
for (int i = 0; i < list.Length; i++) {
    if (list[i] is IHealth) {
        Debug.Log("Found it!");
    }
}

实际上,通过这种方法,您甚至可以使用typeof()命令和'Type'type来使各个组件告诉依赖项注入脚本其依赖项是什么。换句话说,这些组件为依赖项注入器提供类型列表,然后,依赖项注入器返回这些类型的对象列表。


另外,您可以只使用抽象类而不是接口。抽象类可以实现与接口几乎相同的目的,您可以在Inspector中引用它。


我不能继承多个类,但可以实现多个接口。这可能是一个问题,但我想它总比没有好:)谢谢您的回答!
KL

至于编辑-一旦有了脚本列表,我的代码如何知道哪个脚本无法连接到哪个界面?
KL 2015年

1
编辑也解释了这一点
2015年

1
在Unity 5,你可以用GetComponent<IMyInterface>()GetComponents<IMyInterface>()直接,回报界河成分(S)实现它。
S.TarıkÇetin16年

5

根据我自己的经验,游戏开发人员通常比工业开发人员(较少的抽象层)采用更务实的方法。一方面是因为您希望性能优先于可维护性,另一方面是您不太可能在其他上下文中重用代码(图形和游戏逻辑之间通常存在很强的内在联系)

我完全理解您的关注,尤其是因为对最新设备的性能关注并不那么重要,但是我的感觉是,如果您想将工业OOP最佳实践应用于Unity游戏开发(例如依赖注入,等等...)。


2

看看IUnified(http://u3d.as/content/wounded-wolf/iunified/5H1),它使您可以在编辑器中公开界面,就像您提到的那样。与普通的变量声明相比,要做的事情还很多。

public interface IPinballValue{
   void Add(int value);
}

[System.Serializable]
public class IPinballValueContainer : IUnifiedContainer<IPinballValue> {}

public class MyClassThatExposesAnInterfaceProperty : MonoBehaviour {
   [SerializeField]
   IPinballValueContainer value;
}

此外,作为另一个答案,使用SendMessage不会删除耦合。相反,它使它不会在编译时出错。非常糟糕-在您扩展项目时,它可能成为维护噩梦。

如果您进一步希望在不使用编辑器中的界面的情况下解耦代码,则可以使用强类型的基于事件的系统(甚至可以使用松散类型的系统,但实际上又很难维护)。我的帖子基于我,这是一个非常方便的起点。请注意创建一系列事件,因为这可能会触发GC。我使用对象池解决了这个问题,但这主要是因为我使用移动设备。


1

资料来源:http : //www.udellgames.com/posts/ultra-useful-unity-snippet-developers-use-interfaces/

[SerializeField]
private GameObject privateGameObjectName;

public GameObject PublicGameObjectName
{
    get { return privateGameObjectName; }
    set
    {
        //Make sure changes to privateGameObjectName ripple to privateInterfaceName too
        if (privateGameObjectName != value)
        {
            privateInterfaceName = null;
            privateGameObjectName = value;
        }
    }
}

private InterfaceType privateInterfaceName;
public InterfaceType PublicInterfaceName
{
    get
    {
        if (privateInterfaceName == null)
        {
            privateInterfaceName = PublicGameObjectName.GetComponent(typeof(InterfaceType)) as InterfaceType;
        }
        return privateInterfaceName;
    }
    set
    {
        //Make sure changes to PublicInterfaceName ripple to PublicGameObjectName too
        if (privateInterfaceName != value)
        {
            privateGameObjectName = (privateInterfaceName as Component).gameObject;
            privateInterfaceName = value;
        }

    }
}

0

我使用几种不同的策略来解决您提到的限制。

我没有提到的其中之一是使用MonoBehavior,它公开了对给定接口的不同实现的访问。例如,我有一个定义如何实现Raycasts的接口,名为IRaycastStrategy

public interface IRaycastStrategy 
{
    bool Cast(Ray ray, out RaycastHit raycastHit, float distance, LayerMask layerMask);
}

然后,我实现它在不同的班级与班级喜欢LinecastRaycastStrategySphereRaycastStrategy和实施MonoBehavior RaycastStrategySelector暴露每种类型的单个实例。类似的东西RaycastStrategySelectionBehavior,它实现了类似的东西:

public class RaycastStrategySelectionBehavior : MonoBehavior
{

    private readonly Dictionary<RaycastStrategyType, IRaycastStrategy> _raycastStrategies
        = new Dictionary<RaycastStrategyType, IRaycastStrategy>
        {
            { RaycastStrategyType.Line, new LineRaycastStrategy() },
            { RaycastStrategyType.Sphere, new SphereRaycastStrategy() }
        };

    public RaycastStrategyType StrategyType;


    public IRaycastStrategy RaycastStrategy
    {
        get
        {
            IRaycastStrategy raycastStrategy;
            if (!_raycastStrategies.TryGetValue(StrategyType, out raycastStrategy))
            {
                throw new InvalidOperationException("There is no registered implementation for the selected strategy");
            }
            return raycastStrategy;
        }
    }
}

如果实现可能在其构造中引用Unity函数(或者只是为了安全起见,在键入此示例时我只是忘记了这一点)Awake,则可以避免调用构造函数或在编辑器中或在主体外部引用Unity函数时出现问题线

这种策略确实有一些缺点,尤其是当您必须管理许多快速变化的实现时。缺少编译时检查的问题可以通过使用自定义编辑器来解决,因为枚举值无需任何特殊工作就可以序列化(尽管我通常不这样做,因为我的实现并不那么多)。

我之所以使用这种策略,是因为它通过基于MonoBehaviors提供了灵活性,而实际上并没有强迫您使用MonoBehaviors来实现接口。这使您“两全其美”,因为可以使用来访问Selector GetComponent,序列化全部由Unity处理,并且Selector可以在运行时应用逻辑以根据某些因素自动切换实现。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.