24
\$\begingroup\$

Some time ago I started working with Unity and I still struggle with the issue of tightly coupled scripts. How can I structure my code to avoid this problem?

For example:

I want to have health and death systems in separate scripts. I also want to have different interchangeable walking scripts that allow me to change the way the player character is moved (physics-based, inertial controls like in Mario versus tight, twitchy controls like in Super Meat Boy). The Health script needs to hold a reference to the Death script, so that it can trigger the Die() method when the players health reaches 0. The Death script should hold some reference to the walking script used, to disable walking on death (I'm tired of zombies).

I would normally create interfaces, like IWalking, IHealth and IDeath, so that I can change these elements at a whim without breaking the rest of my code. I would have them set up by a separate script on the player object, say PlayerScriptDependancyInjector. Maybe that script would have public IWalking, IHealth and IDeath attributes, so that the dependencies can be set by the level designer from the inspector by dragging and dropping appropriate scripts.

That would allow me to simply add behaviors to game objects easily and not worry about hard-coded dependencies.

The problem in Unity

The problem is that in Unity I can't expose interfaces in the inspector, and if I write my own inspectors, the references wont get serialized, and it's a lot of unnecessary work. That's why I'm left with writing tightly-coupled code. My Death script exposes a reference to an InertiveWalking script. But if I decide I want the player character to control tightly, I cant just drag and drop the TightWalking script, I need to change the Death script. That sucks. I can deal with it, but my soul cries every time I do something like this.

Whats the preferred alternative to interfaces in Unity? How do I fix this problem? I found this, but it tells me what I already know, and it does not tell me how to do that in Unity! This too discusses what should be done, not how, it does not address the issue of tight coupling between scripts.

All in all I feel those are written for people who came to Unity with a game design background who just learn how to code, and there is very little resources on Unity for regular developers. Is there any standard way to structure your code in Unity, or do I have to figure out my own method?

\$\endgroup\$
  • 1
    \$\begingroup\$ The problem is that in Unity I can't expose interfaces in the inspector I don't think I understand what you mean by "expose interface in the inspector" because my first thought was "why not?" \$\endgroup\$ – jhocking Feb 12 '15 at 18:32
  • \$\begingroup\$ @jhocking ask the unity devs. You just dont see it in the inspector. It just doesnt appear there if you set it as a public attribute, and all other attributes do. \$\endgroup\$ – K.L. Feb 12 '15 at 18:43
  • 1
    \$\begingroup\$ ohhh you mean to use the interface as the type of the variable, not just referencing an object with that interface. \$\endgroup\$ – jhocking Feb 12 '15 at 18:53
  • 1
    \$\begingroup\$ Have you read the original ECS article? cowboyprogramming.com/2007/01/05/evolve-your-heirachy What about SOLID design? en.wikipedia.org/wiki/SOLID_%28object-oriented_design%29 \$\endgroup\$ – jzx Feb 12 '15 at 19:27
  • 1
    \$\begingroup\$ @jzx I do know and use SOLID design and I'm a big fan of Robert C. Martins books, but this question is all about how to do that in Unity. And no, I didn't read that article, reading it right now, thanks :) \$\endgroup\$ – K.L. Feb 12 '15 at 19:32
14
\$\begingroup\$

There are a few ways you can work to avoid tight script coupling. Internal to Unity is a SendMessage function that when targetted at a Monobehaviour's GameObject sends that message to everything on the game object. So you might have something like this in your health object:

[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;
}

This is really simplified but should show precisely what I'm getting at. The property throwing the message like you would an event. Your death script would then intercept this message (and if nothing is there to intercept it, SendMessageOptions.DontRequireReceiver will guarantee you don't receive an exception) and perform actions based on the new health value after it's been set. Like so:

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

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

There is no guarantee of order in this, however. In my experience it always goes from top most script on a GameObject to bottom most script, but I wouldn't bank on anything you can't observe for yourself.

There is other solutions you could investigate which is building a custom GameObject function that gets Components and only returns those components that have a specific interface instead of Type, it would take a bit longer but could serve your purposes well.

Additionally, you can write a custom editor that checks an object being assigned to a position in the editor for that Interface before actually committing the assignment (in this case you would be assigning the script as normal allowing it to be serialized, but throwing an error if it didn't use the correct interface and denying the assignment). I've done both of these before and can produce that code but will need some time to find it first.

\$\endgroup\$
  • 5
    \$\begingroup\$ SendMessage() does address this situation and I use that command in many situations, but an untargeted message system like this is less efficient than directly having a reference to the receiver. You should be careful of relying on this command for all routine communication between objects. \$\endgroup\$ – jhocking Feb 12 '15 at 19:01
  • 2
    \$\begingroup\$ I'm a personal fan of the use of events and delegates where Component objects know about the more internal core systems but the core systems don't know anything about the component pieces. But I wasn't one hundred percent sure that was the way they wanted their answer designed. So I went with something that answered how to get scripts uncoupled completely. \$\endgroup\$ – Brett Satoru Feb 12 '15 at 19:11
  • 1
    \$\begingroup\$ I also believe that there are some plugins available in the unity asset store that can expose interfaces and other programming constructs not supported by the Unity Inspector, it might be worth having a quick search. \$\endgroup\$ – Matthew Pigram Feb 13 '15 at 4:20
7
\$\begingroup\$

I personally NEVER use SendMessage. There's still a dependency between your components with SendMessage, it's just very poorly shown and easy to break. Using interfaces and/or delegates really removes the need to use SendMessage ever( which is slower, although that shouldn't be a concern until it needs to be ).

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

Use this guy's stuff. It provides a heap of editor stuff like exposed interfaces, AND delegates. There are heaps of stuff out there like this, or you can even make your own. Whatever you do, DO NOT compromise your design because of Unity's lacking script support. These things should be in Unity by default.

If this isn't an option, drag and drop monobehaviour classes instead and dynamically cast them to the required interface. It's more work, and less elegant, but it gets the job done.

\$\endgroup\$
  • 2
    \$\begingroup\$ Personally I'm leery of becoming too dependent on plugins and libraries for low-level stuff like this. I'm probably being a little paranoid, but it feels like too much risk of having my project break because their code breaks after a Unity update. \$\endgroup\$ – jhocking Feb 13 '15 at 14:12
  • \$\begingroup\$ I was using that framework and eventually stopped since I had the very reason @jhocking mentions gnawing at the back of my mind (and it didn't help that from one update to another, it went from an editor helper to a system that included everything but the kitchen sink while breaking backwards compatibility [and removing a rather useful feature or two]) \$\endgroup\$ – Selali Adobor Feb 13 '15 at 15:48
6
\$\begingroup\$

A Unity-specific approach that occurs to me would be to initially GetComponents<MonoBehaviour>() to get a list of scripts, and then cast those scripts into private variables for the specific interfaces. You'd want to do this in Start() on the dependency injector script. Something like:

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

In fact, with this method you could even have the individual components tell the dependency injection script what their dependencies are, using the typeof() command and 'Type' type. In other words, the components give the dependency injector a list of types, and then the dependency injector returns a list of objects of those types.


Alternatively, you could just use abstract classes instead of interfaces. An abstract class can accomplish pretty much the same purpose as an interface, and you can reference that in the Inspector.

\$\endgroup\$
  • \$\begingroup\$ I cant inherit from multiple classes while I can implement multiple interfaces. This could be a problem, but I guess it's a lot better than nothing :) Thanks for your answer! \$\endgroup\$ – K.L. Feb 12 '15 at 19:02
  • \$\begingroup\$ as for the edit - once I have the list of scripts, how does my code know which script to cant to which interface? \$\endgroup\$ – K.L. Feb 12 '15 at 20:49
  • 1
    \$\begingroup\$ edited to explain that too \$\endgroup\$ – jhocking Feb 12 '15 at 21:30
  • 1
    \$\begingroup\$ In Unity 5, you can use GetComponent<IMyInterface>() and GetComponents<IMyInterface>() directly, whch returns the component(s) that implements it. \$\endgroup\$ – S. Tarık Çetin Apr 11 '16 at 15:17
5
\$\begingroup\$

From my own experience, game dev traditionally involves a more pragmatic approach than industrial dev (with less abstraction layers). Partly because you want to favour performance over maintainability, and also because you're less likely to reuse your code in other contexts (there's usually an strong intrinsic coupling between the graphics and the game logic)

I totally understand your concern, especially because performance concern is less critical with recent devices, but my feeling is that you'll have to develop your own solutions if you want to apply industrial OOP best practices to Unity game development (such as dependency injection, etc...).

\$\endgroup\$
2
\$\begingroup\$

Have a look at IUnified (http://u3d.as/content/wounded-wolf/iunified/5H1), which lets you expose interfaces in the editor, just like you mention. There is a little bit more wiring up to do compared to normal variable declaration, that's all.

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

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

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

In addition, as another answers mention, using SendMessage doesn't remove the coupling. Instead it makes it not error out at compile time. Very bad - as you scale up your project it can become a nightmare to maintain.

If you are further looking to decouple your code without using interface in the editor, you can use a strongly typed event-based system (or even a loosely-typed one actually, but again, harder to maintain). I have based mine on this post, which is super convenient as a starting point. Be mindful of creating a bunch of events as it could trigger the GC. I worked around the problem with an object pool instead, but that's mostly because I work with mobile.

\$\endgroup\$
1
\$\begingroup\$

Source: 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;
        }

    }
}
\$\endgroup\$
0
\$\begingroup\$

I use a few different strategies to get around the limitations you mention.

One of those that I don't see mentioned is using a MonoBehavior that exposes access to the different implementations of a given interface. For example, I have an interface that defines how Raycasts are implemented named IRaycastStrategy

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

I then implement it in different classes with classes like LinecastRaycastStrategy and SphereRaycastStrategy and implement a MonoBehavior RaycastStrategySelector that exposes a single instance of each type. Something like RaycastStrategySelectionBehavior, which is implemented something like:

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;
        }
    }
}

If the implementations are likely to reference Unity functions in their constructions (or just to be on the safe side, I just forgot this when typing up this example) use Awake to avoid issues with calling constructors or referencing Unity functions in the editor or outside the main thread

This strategy does have some drawbacks, especially when you have to manage many different implementations that are rapidly changing. Issues with the lack of compile-time checking can be helped by using a custom editor, since the enum value can be serialized without any special work (although I usually forgo this, since my implementations don't tend to be that numerous).

I use this strategy because of the flexibility it affords you by being based on MonoBehaviors without actually forcing you to implement the interfaces using MonoBehaviors. This gives you "the best of both worlds" since the Selector can be accessed using GetComponent, the serialization is all handled by Unity and the Selector can apply logic at run-time to automatically switch implementations based on certain factors.

\$\endgroup\$

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.