Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Have a look at our Games Focus blog post series which will show what Unity is doing for all game developers – now, next year, and in the future.
    Dismiss Notice

Interfaces vs Components

Discussion in 'Scripting' started by Nigey, Jan 9, 2018.

  1. Nigey

    Nigey

    Joined:
    Sep 29, 2013
    Posts:
    1,129
    Hi Guys,

    Just interested to know whether you use interfaces, and how you break them up cleanly between component use and interface use. Here's two examples I use:

    Code (CSharp):
    1.     public interface IDamageable
    2.     {
    3.         void TakeDamage(float amount);
    4.     }
    This would very easily go onto any component that can take damage, and can be handled in it's own way. You COULD make this into a script, which acts as it's own component and Serialize some UnityEvents for it. Any other scripts could just subscribe to the damage script. That requires manual fiddling though and doesn't feel very clean to me.

    An example of when I'd use a Component is when you have an GameObject which is pick-up-able. Here you wouldn't neccessarily have an interface of IPickupable. Instead you could just have a component that you search for, to see whether you can pick up the object.

    I mean there's various ways to do these things, but what I'm beginning to find is that interfaces only promote abstraction, and not neccessarily de-coupling. Subscribing to an interface in a class forces it's behaviour to exist. Where-as in a component you can just add/remove them.

    I mean maybe in my examples a ScriptableObject would be better. In ScriptableObject's I'm slightly inexperienced.

    What's everyone else's approach to them?

    Thanks!
     
  2. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,089
    I use interfaces all the time. I was so elated the day Unity finally added support to interfaces in 'GetComponent' (I had rolled my own version prior to that, and it was much slower).

    What I would say is the benefit here... and I actually have something similar to this, I just call it 'IInteractable' instead. But what I would say is that the interface allows you to define multiple different kinds of things that can be "picked up", or in my case "interacted with".

    Code (csharp):
    1.  
    2.     public interface IInteractable : IComponent
    3.     {
    4.  
    5.         /// <summary>
    6.         /// Called by entity when the thing was interacted with.
    7.         /// </summary>
    8.         /// <param name="entity">The entity interacting with it</param>
    9.         /// <param name="aspect">The aspect, if any, used to locate the interactable</param>
    10.         /// <returns></returns>
    11.         InteractionType InteractWith(IEntity entity, IAspect aspect);
    12.         void Release();
    13.  
    14.     }
    15.  
    Note - an IInteractable can be either 'pressed' or 'held'.

    Now in my situation I have a generic implementation that sort of just uses UnityEvent (well in my case Trigger, but the same thing) to allow generic set up just like you described.

    Code (csharp):
    1.  
    2.     public class PlayerInteractable : SPComponent, IInteractable
    3.     {
    4.  
    5.         #region Fields
    6.  
    7.         [SerializeField()]
    8.         private Trigger _onActivate;
    9.  
    10.         #endregion
    11.  
    12.         #region IPlayerInteractable Interface
    13.  
    14.         public InteractionType InteractWith(IEntity entity, com.spacepuppy.AI.Sensors.IAspect aspect)
    15.         {
    16.             if (!this.isActiveAndEnabled) return InteractionType.Failed;
    17.  
    18.             _onActivate.ActivateTrigger(this, entity);
    19.             return InteractionType.Press;
    20.         }
    21.  
    22.         void IInteractable.Release()
    23.         {
    24.             //release is never use
    25.         }
    26.  
    27.         #endregion
    28.  
    29.     }
    30.  
    But I also have other things that are more specific... like PushableObject:
    Code (csharp):
    1.  
    2.     [RequireComponent(typeof(Rigidbody))]
    3.     public class PushableObject : SPComponent, IInteractable
    4.     {
    5.  
    6.         #region Fields
    7.  
    8.         [SerializeField]
    9.         [DefaultFromSelf(UseEntity =true)]
    10.         private Collider _collision;
    11.  
    12.         [System.NonSerialized]
    13.         private Rigidbody _body;
    14.  
    15.         [System.NonSerialized()]
    16.         private MovementMotor _playerMotor;
    17.  
    18.         [System.NonSerialized()]
    19.         private bool _inTransition;
    20.  
    21.         #endregion
    22.  
    23.         #region CONSTRUCTOR
    24.  
    25.         protected override void Awake()
    26.         {
    27.             base.Awake();
    28.  
    29.             _body = this.GetComponent<Rigidbody>();
    30.         }
    31.  
    32.         #endregion
    33.  
    34.         #region Properties
    35.  
    36.         public Rigidbody Body
    37.         {
    38.             get { return _body; }
    39.         }
    40.  
    41.         public Collider Collision
    42.         {
    43.             get { return _collision; }
    44.         }
    45.  
    46.         public bool InTransition
    47.         {
    48.             get { return _inTransition; }
    49.         }
    50.  
    51.         #endregion
    52.  
    53.         #region Methods
    54.  
    55.         public void MoveToPosition(Vector3 targPos, float speed)
    56.         {
    57.             _inTransition = true;
    58.             var dist = (_body.position - targPos).magnitude;
    59.             SPTween.Tween(_body)
    60.                    .To("position", targPos, dist / speed)
    61.                    .OnFinish((s, e) =>
    62.                    {
    63.                        _inTransition = false;
    64.                    })
    65.                    .Play(true);
    66.         }
    67.  
    68.         #endregion
    69.  
    70.  
    71.         #region IPlayerInteractable Interface
    72.  
    73.         public InteractionType InteractWith(IEntity entity, com.spacepuppy.AI.Sensors.IAspect aspect)
    74.         {
    75.             if (!this.isActiveAndEnabled) return InteractionType.Failed;
    76.             if (_playerMotor != null) return InteractionType.Failed;
    77.             if (entity.Type != IEntity.EntityType.Player) return InteractionType.Failed;
    78.  
    79.             var motor = entity.FindComponent<MovementMotor>();
    80.             if (motor == null) return InteractionType.Failed;
    81.  
    82.             _playerMotor = motor;
    83.             var style = _playerMotor.States.GetState<PlayerPushMovementStyle>();
    84.             style.BeginPush(this);
    85.  
    86.             return InteractionType.Hold;
    87.         }
    88.  
    89.         public void Release()
    90.         {
    91.             if (_playerMotor == null) return;
    92.  
    93.             var style = _playerMotor.States.GetState<PlayerPushMovementStyle>();
    94.             style.EndPush();
    95.             _playerMotor = null;
    96.         }
    97.  
    98.         #endregion
    99.  
    100.  
    101.     }
    102.  
    Because they all implement the same interface I can generically access them the same way in my 'PlayerInteractActionStyle':
    Code (csharp):
    1.  
    2.     public class PlayerInteractActionStyle : SPComponent, IPlayerActionStyle
    3.     {
    4.  
    5.         #region Fields
    6.  
    7.         [SerializeField]
    8.         [DefaultFromSelf]
    9.         private PlayerActionMotor _actionMotor;
    10.  
    11.         [SerializeField()]
    12.         private Sensor _actionSensor;
    13.  
    14.         [System.NonSerialized()]
    15.         private IEntity _entity;
    16.  
    17.         [System.NonSerialized()]
    18.         private IInteractable _heldInteractable;
    19.         [System.NonSerialized]
    20.         private RadicalCoroutine _holdRoutine;
    21.  
    22.         #endregion
    23.  
    24.         #region CONSTRUCTOR
    25.  
    26.         protected override void Awake()
    27.         {
    28.             base.Awake();
    29.  
    30.             _entity = IEntity.Pool.GetFromSource<IEntity>(this);
    31.         }
    32.  
    33.         #endregion
    34.  
    35.         #region Properties
    36.  
    37.         public PlayerActionMotor ActionMotor
    38.         {
    39.             get { return _actionMotor; }
    40.         }
    41.  
    42.         public Sensor ActionSensor
    43.         {
    44.             get { return _actionSensor; }
    45.             set { _actionSensor = value; }
    46.         }
    47.  
    48.         #endregion
    49.  
    50.         #region Methods
    51.  
    52.         public void HoldItem(IInteractable item)
    53.         {
    54.             if (_heldInteractable != null) _heldInteractable.Release();
    55.             _heldInteractable = item;
    56.  
    57.             if(_holdRoutine == null)
    58.             {
    59.                 _holdRoutine = this.StartRadicalCoroutine(this.OnHoldRoutine());
    60.             }
    61.             else if(!_holdRoutine.Active)
    62.             {
    63.                 _holdRoutine.Start(this, RadicalCoroutineDisableMode.Pauses);
    64.             }
    65.         }
    66.  
    67.         private System.Collections.IEnumerator OnHoldRoutine()
    68.         {
    69.             while(true)
    70.             {
    71.                 yield return null;
    72.                 if (_heldInteractable == null)
    73.                 {
    74.                     _holdRoutine.Stop();
    75.                 }
    76.                 else
    77.                 {
    78.                     var input = Services.Get<IGameInputManager>().GetDevice<MansionInputDevice>(Game.MAIN_INPUT);
    79.                     if ((input != null && input.GetCurrentButtonState(MansionInputs.Action) <= ButtonState.None)
    80.                         || _entity.Type == IEntity.EntityType.UndeadPlayer) //undead player can't interact with stuff
    81.                     {
    82.                         _heldInteractable.Release();
    83.                         _heldInteractable = null;
    84.                         _holdRoutine.Stop();
    85.                     }
    86.                 }
    87.             }
    88.         }
    89.  
    90.         private void AttemptActivate()
    91.         {
    92.             var trans = _entity.transform;
    93.             var pos = trans.position.SetY(0f);
    94.             var forw = trans.forward.SetY(0f);
    95.  
    96.             bool ignoreLowPriority = Game.Scenario.IgnoreLowPriorityInteractables;
    97.             var aspects = from a in _actionSensor.SenseAll()
    98.                           where !(ignoreLowPriority && a.Precedence < 1f) && a.gameObject.EntityHasComponent<IEntity>()
    99.                           let p = a.transform.position.SetY(0f)
    100.                           orderby -a.Precedence, VectorUtil.AngleBetween(p, forw), Vector3.Distance(p, pos) ascending
    101.                           select a;
    102.             foreach (var aspect in aspects)
    103.             {
    104.                 var go = GameObjectUtil.GetGameObjectFromSource(aspect);
    105.                 if (go == null) continue;
    106.  
    107.                 IInteractable comp = go.GetComponent<IInteractable>() ?? go.FindComponent<IInteractable>();
    108.                 if (comp == null) continue;
    109.  
    110.                 switch (comp.InteractWith(_entity, aspect))
    111.                 {
    112.                     case InteractionType.Failed:
    113.                         continue;
    114.                     case InteractionType.Press:
    115.                         break;
    116.                     case InteractionType.Hold:
    117.                         this.HoldItem(comp);
    118.                         break;
    119.                 }
    120.  
    121.                 //exit foreach loop
    122.                 break;
    123.             }
    124.         }
    125.  
    126.         #endregion
    127.  
    128.         #region IPlayerActionStyle Interface
    129.  
    130.         bool IPlayerActionStyle.OverridingAction
    131.         {
    132.             get
    133.             {
    134.                 return false;
    135.             }
    136.         }
    137.  
    138.         void IPlayerActionStyle.OnStateEntered(PlayerActionMotor motor, IPlayerActionStyle lastState)
    139.         {
    140.  
    141.         }
    142.  
    143.         void IPlayerActionStyle.OnStateExited(PlayerActionMotor motor, IPlayerActionStyle nextState)
    144.         {
    145.  
    146.         }
    147.  
    148.         void IPlayerActionStyle.DoUpdate(MansionInputDevice input)
    149.         {
    150.             //undead player can't interact with stuff
    151.             if (_entity.Type == IEntity.EntityType.UndeadPlayer) return;
    152.  
    153.             if (input.GetCurrentButtonState(MansionInputs.Action) == ButtonState.Down)
    154.             {
    155.                 //attempt to activate whatever
    156.                 this.AttemptActivate();
    157.                 _actionMotor.ResetIdleActionTicker(false);
    158.             }
    159.         }
    160.  
    161.         #endregion
    162.  
    163.     }
    164.  
    Any time I want to add a new interaction... the actual arbitrator of it doesn't need to change. They just plug in and go.

    Like when we were recording the voice acting for our last title I got a bunch of "woo-hoos" and other sounds from my buddy. We were joking around about being poked like the pilsbury dough boy.

    And for a lark I just tossed it in the game real quick to surprise my artist/partner. And he had a good laugh.

    All I had to do was implement the IInteractable interface, which played a sound clip, and triggered an animation. I tossed that on the 'Hank' character. And now if you walked up to Hank and pressed A he went "woo-hoo". No extra work needed since the interface took care of it up front.

    Here's a video example, it should start at the beginning where the player starts poking the boat and Hank. The interacting with items in the scene that pops up a message box also implement IInteractable as well.


    They definitely promote abstraction first and foremost.

    The de-coupling comes in a different form. Take for example my 'PlayerInteractActionStyle' component, it's de-coupled from 'PushableObject', 'PlayerInteractable' and 'PlayerPokeHank' scripts. It doesn't care what those specifically are, nor needs to directly access them, but rather indirectly accesses them.

    As for the add/remove thing... I mean, I can remove the 'PushableObject' or other script. But I don't think that's what you mean... I think you mean that once you define a script as a IInteractable, it's always an IInteractable (or IPickupable). In which case I'd say you should still stick to the whole keeping classes simple. Don't make a component that's both a interactable and handles the health of something. Note that my IInteractables are all still just code relating to being interacted with and only that.

    This actually makes me think of another good use of interfaces. And that is you can have both a Component and a ScriptableObject that implements the same interface. Allowing you flexibility in setting things up. There's a few places I do this as well... but I won't bore you with more code on that.
     
    Last edited: Jan 9, 2018
    CheekySparrow78 and Nigey like this.
  3. Nigey

    Nigey

    Joined:
    Sep 29, 2013
    Posts:
    1,129
    As always @lordofduct, nice comprehensive response. Yeah that cleans up some areas of use. Now that just leaves some refactoring of the pattern. Do you still use the puppies framework on Git? Any chance of a link? Thanks!

    Completely scratch that, it's at you signature! Oops.
     
  4. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,089
    We're currently moving from Unity 5 to Unity2017, I'm taking that time to move over to the newer API, tap into the newer features, and clean up a lot of the naming conventions. So the newer git is here (it's in limbo right now as I'm currently updating):
    https://github.com/lordofduct/spacepuppy-unity-framework-3.0

    And yeah, I still use it.

    Were still using Unity 5 and Unity2017 in tandem with one another. Since the next 2 episodes of the game I showed above still use the same setup and thusly will be stuck on Unity5.

    But I'm also working on some other games in Unity2017 at the same time. Upgrading and expanding on an older game we made (How Now Sea Cow), as sort of a way to test out the new port of Spacepuppy. As well as working on the alpha portions of future titles while my artist/designer does the bulk of the art stuff for the episodes of Prototype Mansion.
     
    Nigey likes this.
unityunity