A question on Unity C# OOD best practices

I apologize in advance if this is a dumb question, but I have some design level questions about scripting in Unity. I am a computer science student, and I am used to working with objects and their relationships, but I am a little confused on the best practice for unity (specially c# scripting). For instance, if I were programming a game in java I would have a Player class which HAS a position, HAS hitpoints, HAS speed, etc. This is all standard class composition. I get a little tripped up though with Unity’s format, as it seems to be that I can take two different directions.

  1. A monobehaviour deriving Player class, which has other information like hitpoints and speed.

  2. A non-monobehaviour Player class with standard constructors and such, which HAS a GameObject member which is it’s geometry/animation/etc.

With (1), we have access to Monobehaviour’s vast array of methods and functionality, but in order to set up our Player we’d need to either use public variables or some sort of factory method constructor since void Start() takes no arguments, ie.

public static PlayerObject getPlayerObj(param1, param2, param3, ..., paramN)
{
   GameObject go = Instantiate(somePrefab) as GameObject;
   PlayerObject po = go.getComponent<PlayerObject>();
 
   po.p1 = param1;
   ...
   return po;
}

For (2), we can use a constructor and do not need a factory method or “Inspector public variables” to initialize. We can also use MonoBehaviour.Instantiate to setup our gameobject member.

In both cases, we have a gameObject member which references the game object the player represents. The difference is in the initializing and also the ability to manage game objects. That is, in order to update, we need something attached to an object, not an object attached to a script. Sure we can have a “GameManager” mono script on an empty object which manually updates players but this seems… not as intended. It seems like the correct choice here is (1), but it’s just different than Im used to in say java or c++ where the Player class would HAVE geometry and positional information.

I guess I just want to know what is the best way to most effectively use Unity’s unique style of scripting-gameobject fusion to make games that are more complex than dropping a few values for public variables in the inspector? Is it Monobehaviours (which have update functions and can handle themselves) with other data initialized by factories, or non-Monobehaviours with gameobject members which need to manually update since there is no Monobehaviour script attached to that gameObject member (the gameobject is “attached” to the script instead, thus some higher Mono script would have to loop them in IT’S update function).

Thanks very much for any input, just trying to avoid the pain of doing this the wrong way.

This question has a ton of different, valid answers…

  • Since you lack constructor access for monobehaviours, factories can certainly have their place.
  • You may also want to look into dependency injection. I’ve found this to be fairly useful.
  • Some people likely treat game objects and associated scripts as views, if you want to really split concerns.

The component based system takes some getting used to, but it ends up being fairly natural once you grow accustomed. I’d suggest not worrying too much about most of these issues until you get a feel for the system and the context of your project.

Thanks for the reply.
Yeah you’re right, I guess it depends on the game context. I think i’ve come to the conclusion that if the class represents something that exists as an object in the game, I’m going to make it a Monobehaviour. Helper classes such as Stats, etc. can all be helper Non-Monobehaviour classes that are instantiated by the Dependency Injection pattern / factory (Which I have trouble seeing the difference between, perhaps it’s subtle. They both initialize dependencies in the host class.) This way, all game objects are truly GameObjects, and I think it will be less confusing that way. Thanks again for pointing out the pattern, seems to make a lot of sense in Unity.

I don’t understand why you say that. I’ve worked on video game in C and C++, and they usually go the GameObject/Component approach because of its versatility, easiness to maintain and endless potential combination.

Having everything defining a player within a class is a strict Object-Oriented approach - 1 object = 1 class - that you only see from people who never really worked in video games.

However, Components design is not against Object-Oriented, but only a different way of combining them.

For example, a player class that would have EVERYTHING needed inside itself, would be HUGE; position, motion, physic, skeleton, animation, sound, mesh rendering, material, mapping, AI, pathfinding, input, etc. It would be insane! Instead, we split the classes into smaller logical chunk that perform a single autonomous task. A MeshRenderer is only there to feed a mesh to the rendering pipeline. A Transform class is there to manage the position, orientation, parenting and general transform matrix of a spatial object.

Even our Player class is splitted into smaller chunk as we use an object-oriented finite state machine. Our input are also splitted into different class, because input on a PC are not the same as on a cellphone.

The idea here is not to stick to a specific design paradigm just for the sake of it, but to use the most logical, easiest to maintain pattern.

I tend to think of it like this:

Anything that has an independent position is a game object. The relationship between game objects and sub game objects is associated with how the sub objects position might be offset by its parent. I generally try to stay away from the idea of game objects as folders, since again, the essential element of a game object is its transform. I think I’m in the minority here though as I think many people find game objects as folders offers more utility (you can easily spot groups in editor/inspector). I also tend not to rely on prefabs as much as many people, since I’ve found that using the visual designer tends to be harder to maintain than code.

As for DI vs factory - it can be much more nuanced - but essentially, DI can be used for passing a few instances to many, factories are used to parameterize the creation of many instances. So, one natural application of DI would be passing around factories (super duper common). I tend to find that’s more indirection than I generally need or want. But YMMV.

This stuff also might be useful… it might not be. I really like it though, and use it all over the place. This code is entirely not production ready, but it should be pretty straight forward… the idea is that you can mark properties in code with attributes that point to dependent components. Again, this code should work - but might have some ideas that I haven’t fleshed out or named well.

So the usage is like this:

public class TacticalTroop : DecoratedBehaviour {

    // fetch a TacticalTroopController from the game object (GetComponent<TacticalTroopController>)
    [FromGameObject]
    protected TacticalTroopController Controller { get; set; }

    // fetch a Renderer from this game object or any child (GetComponentInChildren<Renderer>)
    [FromChild]
    protected Renderer Renderer { get; set ;}

The base class ‘decoratedbehaviour’ automatically resolves these inter-game object dependencies using the following mess of code. Generally this kind of thing is most useful for gui screens, but I use it all over the place. It’s really just a set of short hand for GetComponent or GetComponentsInChildren, etc. I think it makes these dependencies clear and obvious, which I tend to like.

using System;
using System.Collections.Generic;
using System.Reflection;
using P = InstancePropertyAttribute;
using C = InstanceClassAttribute;
using SP = StaticPropertyAttribute;
using SC = StaticAttribute;

public static class InstanceAttribute
{
    private static readonly Dictionary<Type, D> m_CACHE = new Dictionary<Type, D>();

    public static void ActUpon(Object o)
    {
        D d;
        Type t = o.GetType();
        if (m_CACHE.TryGetValue(t, out d) == false)
        {
            d = GetAttributes(o);
            m_CACHE.Add(t, d);
        }

        for (int index = 0; index < d.Cs.Length; index++)
        {
            var c = d.Cs[index];
            c.ActUpon(o);
        }

        for (int index = 0; index < d.Ps.Length; index++)
        {
            var p = d.Ps[index];
            p.PropAttr.ActUpon(o, p.Info);
        }
    }

    private static D GetAttributes(Object o)
    {
        Type t = o.GetType();
        var members = t.GetMembers(BindingFlags.Instance
                                    | BindingFlags.NonPublic
                                    | BindingFlags.Public);

        var properties = new List<PropMemb>();


        // Roll all the instance attributes into D, since the static ones are...static, just fire them here.
        C[] cls_attr = (C[])t.GetCustomAttributes(typeof(C), true);

        foreach (SC sc in t.GetCustomAttributes(typeof(SC), true))
            sc.ActUpon(t);

        for (int index = 0; index < members.Length; index++)
        {
            MemberInfo member = members[index];

            PropertyInfo prop = member as PropertyInfo;
            if (prop == null) continue;

            P[] attrib = (P[])prop.GetCustomAttributes(typeof(P), true);
            if (attrib.Length == 0) continue;
            properties.Add(new PropMemb(attrib[0], prop));

            SP[] sps = (SP[])prop.GetCustomAttributes(typeof(SP), true);
            foreach (var sp in sps) sp.ActUpon(t, prop);
        }

        return new D(cls_attr, properties.ToArray());
    }

    #region Nested type: D

    private class D
    {
        public D(InstanceClassAttribute[] cs, PropMemb[] ps)
        {
            Cs = cs;
            Ps = ps;
        }

        public readonly C[] Cs;
        public readonly PropMemb[] Ps;
    }

    #endregion

    #region Nested type: PropMemb

    private class PropMemb
    {
        public PropMemb(InstancePropertyAttribute prop_attr, PropertyInfo info)
        {
            PropAttr = prop_attr;
            Info = info;
        }

        public readonly PropertyInfo Info;
        public readonly P PropAttr;
    }

    #endregion
}

public abstract class StaticAttribute : Attribute
{
    public abstract void ActUpon(Type type);
}

public abstract class StaticPropertyAttribute : Attribute
{
    public abstract void ActUpon(Type type, PropertyInfo info);
}


public abstract class InstanceClassAttribute : Attribute
{
    public abstract void ActUpon(Object instance);
}

public abstract class InstancePropertyAttribute : Attribute
{
    public abstract void ActUpon(Object instance, PropertyInfo attached_property);
}

and…

public class DecoratedBehaviour : MonoBehaviour {
    protected void Awake() {
        InstanceAttribute.ActUpon( this );
        OnAwake();
    }

    protected virtual void OnAwake() { }

}


public class RequirementException : Exception {
    public RequirementException() { }
    public RequirementException( string message ) : base( message ) { }
}

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class FromChildByName : InstancePropertyAttribute {
    private String Name;
    private bool IsRequired;
    public FromChildByName(String name, bool is_required = true) { 
        Name = name;
        IsRequired = is_required;
    }
    public override void ActUpon( object instance, PropertyInfo attached_property ) {
        Component c = instance as Component;
        Debug.Assert( c != null, "c != null" );

        var possibles = c.GetComponentsInChildren( attached_property.PropertyType, true );
        for( int index = 0 ; index < possibles.Length ; index++ ) {
            var possible = possibles[ index ];
            if( possible.name == Name ) {
                attached_property.SetValue( instance, possible, null );
                return;
            }
        }
        if (IsRequired) {
            var msg = String.Format( "{0} ({1}) requires a {2} {3}",
                                     instance.GetType().Name,
                                     c.GetPathyName(),
                                     attached_property.PropertyType.Name,
                                     GetType().Name );

            throw new RequirementException( msg );
        }
    }
}

[AttributeUsage( AttributeTargets.Property, AllowMultiple = false, Inherited = true )]
public class FromGameObject : InstancePropertyAttribute {
    public FromGameObject( bool is_required = true ) { IsRequired = is_required; }

    protected virtual object GetComponent( Component src, Type target ) { return src.GetComponent( target ); }
    
    public override void ActUpon( object instance, PropertyInfo attached_property ) {
        Type t = attached_property.PropertyType;
        Component c = (Component) instance;
        object target = GetComponent( c, t );
        if( target != null ) {
            attached_property.SetValue( instance, target, null );
        } else if( IsRequired ) {
            var msg = String.Format( "{0} ({1}) requires a {2} {3}",
                                     instance.GetType().Name,
                                     c.GetPathyName(),
                                     t.Name,
                                     GetType().Name );

            throw new RequirementException( msg );
        }
    }
    public bool IsRequired;
}

public class FromAllChildren : FromGameObject {
    public FromAllChildren( bool is_required = true ) : base(is_required){ }

    protected override object GetComponent(Component src, Type target) {
        Type item_type = target.GetElementType();
        if( item_type == null ) throw new ArgumentException("FromAllChildren must be array type failed in: " + src.GetType() + " attemptnig to get:" + target.Name );
        
        Component[] targets = src.GetComponentsInChildren( item_type, true );
        var ret = Array.CreateInstance( item_type, targets.Length );
        Array.Copy( targets, ret, ret.Length );

        return ret;
    }
}

public class FromParents : FromGameObject{
    public FromParents(bool is_required = true) : base(is_required) { }
    protected override object GetComponent(Component src, Type target) {
        return src.GetComponentInParents(target, false);
    }
}

public class FromChild : FromGameObject
{
    public FromChild(bool is_required = true) : base(is_required) { }
    protected override object GetComponent(Component src, Type target) {
        return src.GetComponentInChildren( target );
    }
}

And finally, some other utilities:

public static class go_extensions {

    public static bool TryGetComponent<T>( this Component go, out T target ) where T : Component {
        var t = go.gameObject.GetComponent<T>();
        if( t != null ) {
            target = t;
            return true;
        }
        target = null;
        return false;
    }


    public static T GetComponentInParents<T>( this MonoBehaviour go, bool is_required = true) where T : MonoBehaviour{
        return GetComponentInParents<T>(go.transform, is_required);
    }

    public static T GetComponentInParents<T>( this GameObject go, bool is_required = true) where T: MonoBehaviour {
        return GetComponentInParents<T>(go.transform, is_required);
    }

    public static T GetComponentInParents< T >( this Transform go, bool is_required = true ) where T : MonoBehaviour {

        T target;
        var localgo = go;
        do {
            target = localgo.GetComponent< T >();
            localgo = localgo.transform.parent;
        } while (target == null  localgo != null);
        if( target == null  is_required ) {
            Debug.LogError( "Unable to find required component of type:" + typeof( T ).Name + " in objecct " + lo.GetPathyName( go.gameObject ) );
        }
        return target;
    }

    public static Component GetComponentInParents( Transform go, Type target_type, bool is_required = true ) {
        // TODO: generic impl could use this guy - but then requires casting.
        Component target;
        var localgo = go;
        do
        {
            target = localgo.GetComponent(target_type);
            localgo = localgo.transform.parent;
        } while (target == null  localgo != null);
        if (target == null  is_required)
        {
            Debug.LogError("Unable to find required component of type:" + target_type.Name + " in objecct " + lo.GetPathyName(go.gameObject));
        }
        return target;
    }
    public static Component GetComponentInParents(this Component go, Type target_type, bool is_required = true ) {
        return GetComponentInParents(go.transform, target_type, is_required );
    }

    public static T GetInterfaceInChildren< T >( this GameObject go ) {
        var monos = go.GetComponentsInChildren< MonoBehaviour >();
        for( int index = 0 ; index < monos.Length ; index++ ) {
            var mono = monos[ index ];

            if( mono is T ) {
                return ( T ) ( System.Object ) mono;
            }
        }
        return default( T );
    }

    public static List< T > GetInterfacesInChildren< T >( this GameObject go ) {
        List< T > list = new List< T >();
        var monos = go.GetComponentsInChildren< MonoBehaviour >();
        for( int index = 0 ; index < monos.Length ; index++ ) {
            var mono = monos[ index ];
            if( mono is T ) {
                list.Add( ( T ) ( System.Object ) mono );
            }
        }
        return list;
    }

    public static String GetPathyName( this Component mb ) { return GetPathyName( mb.gameObject ); }

    public static String GetPathyName( this GameObject go ) {
        string path = "/" + go.name;
        while( go.transform.parent != null ) {
            go = go.transform.parent.gameObject;
            path = "/" + go.name + path;
        }
        return path;
    }
}

Not saying this stuff is the best possible way, or even a best practice. But I’ve found these tags useful and clear.

Sorry LightStriker, I didn’t communicate that very well. Of course you should use helper classes to hold different peices of data, like a gameObject which holds transform and other information. My real question was about whether to use a Monobehaviour class instantiated by a factory or Dependency injection, or a non-monobehaviour class which has an actual constructor that can be made with “new”.

Thank you for the additional comments, and for the code you provided. That’s actually quite a nice shorthand implementation for getcomponents, so thanks for sharing. As far as DI vs. factory, I think I see what you’re talking about. I might try to find another video tutorial online to really get it nailed in as far as how to implement DI containers vs. factories, etc.

This has a nice discussion of DI including an implementation (I’m not a huge fan of the implementation, but I saw some stuff by this guy on gamasutra so he must be doing something right).

It might be useful to think of Object.Instantiate() as the factory itself. Player is not a class, it’s the prefab you pass to Object.Instantiate() (or drag and drop in the scene). This lets you decide what a Player is in the editor, by attaching and configuring the components it needs.

A solution specific for each specific situation, design and structure. I cannot answer what is best without more information about the flow of the data, serialization, and construction of the game itself. There’s virtually endless number of design and paradigm combination that can be used for a single problem.

Most - +90% - of the time, you will simply use Unity’s editor to build the data structure you need. (Ex.: Add the Player’s MonoBehaviour to a GameObject, drag the GameObject into a prefab, etc.)

More fancy design are usually used when the game design requires it; such as auto-generated levels or character customization.

You also have to take into account the most important opposite pole in video game production; Code-driven vs Data-driven. Should your design really be driven by code, or should a non-coder have access to tools to build exactly the same result out of data? Or should it be a mix of the two? There’s an endless count of shades of gray between the two extremes and some may multiply greatly the cost of development, maintenance or implementation.

Example… Do you really need to code a “factory” for the player, or could someone just drag the prefab in a scene?

An example of the “Player” class we use;

public class Player : MonoBehaviour
{
// Collection of value defining how fast the player move
    public PlayerStat Stat;

 // Platform specific input, instantiated at runtime base on the platform
    public InputModule Input;

// Player's visual, instantiated at run time on player's choice
    public GameObject Visual;

// Machine handling player's state
    public StateMachine Machine;
}

public class PlayerStat

public abstract class InputModule

public class InputKeyboard : InputModule
public class InputMobile : InputModule

public abstract class PlayerState : State

public abstract class PlayerGroundState : PlayerState
public abstract class PlayerAirState : PlayerState

public class PlayerRunState : PlayerGroundState
public class PlayerJumpState : PlayerAirState

That was very informative, thank you for that. I’m thinking prefabs could be the way to go. That is, instead of defining everything in code with a factory, just define it by dragging things to a prefab and then using that prefab as the full object. This particular game would be a tower defense, so I could have tower prefabs, a terrain prefab, an “Enemy” prefab for each kind of enemy I have, etc.

Usually, building prefab for every “pieces” of the game is the way to go. After that, if you need to place a tower somewhere in the game at run time, the code simply makes a copy of the existing prefab and place it at the right spot.

Generating this kind of data should not happen at runtime, but should be made in the editor. The alternative, creating and assembling everything from the code, would mean thousand of lines of code that could be avoided completely. After all, that’s the purpose of the existence of Unity’s editor!

I agree with almost everything LightStriker says here. Especially the quoted bit. I think we developers tend to latch onto ‘best practices’ and golden rules, but the reality is that the approach that produces the best result will probably mostly revolve around the team itself, the individual members, their individual skills/experience and their collective needs. It really, really depends…

That said, I’ll share an experience from my current project that’s probably pretty widely applicable, and I think works quite well…

I’ve found that it’s extremely useful to be able to set up specific, isolated test scenes. Especially for GUI stuff. My game has a bunch of drag and drop screens for equipment and shops, etc. The end result for each of these screens is a prefab. I want to be able to run each scene independently and load a bunch of test data into one of them, so I can test/tweek really quickly (but without the formality of strict unit tests). So each screen has it’s own dedicated test scene that I can run directly.

I’ve found that using a DI container is a really natural and easy way to make it so that I can test that screen -in isolation-, while also using the resulting prefab in my real game scene. There are some nuances, like being able to redefine how certain buttons work (a “leave shop” button at minimum), or different shop / player configurations that I want to be able to load up quickly, without inadvertently applying inappropriate setting changes to prefabs (a mistake I constantly make when dealing with prefabs - and has resulted in me making far fewer ‘tweekables’ in prefabs (a very personal preference)).

Basically, this kind of thing results in getting different kinds of behaviours out of different prefabs depending on what they get dragged onto instead of tweekable properties. Admittedly, this kind of solution isn’t appropriate for everyone, but for my needs, it’s working great and is the right trade off between being able to test directly and avoiding over-engineering.

This kind of thing can certainly be done w/o DI. But I’ve found it to be a great fit with some pretty minimal downside.