Search Unity

Abstract Class VS Interface

Discussion in 'Scripting' started by MaxLevelNoob, May 20, 2020.

  1. MaxLevelNoob

    MaxLevelNoob

    Joined:
    Sep 17, 2019
    Posts:
    11
    CASE

    Classes I have:
    - abstract class Interactable
    - abstract class Destructible

    From these classes I make these,
    - class Door : MonoBehaviour, Interactable, Destructible

    -------------------------------------------------------------------------------------------------------------------------------------

    PROBLEM

    So I run into a problem that this Door class cannot have multiple base classes
    So now I'm thinking Interfaces right?
    But...

    #1 - Destructible has variables such as OnDestruct particles, sprite...etc which I set in the Inspector.
    Interfaces don't allow you to set in the Inspector

    #2 - Destructible has the same code for deducting health and destructing.
    If I end up using Interface, it will have the same code every override

    --------------------------------------------------------------------------------------------------------------------------------------

    QUESTION

    Are there anything I can do to bypass the Multiple Base Classes problem?
    OR
    If the only solution is to use Interface, then is there a way to use it such that I do not have problems #1 & #2?
     
  2. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,537
    It sounds to me like your classes are doing too much.

    What does Door need to inherit from both Interactable and Destructible?

    Why not just have an Interactable script and a Destructible script. And attach the both to your GameObject?

    Just like how when you need collision you add a Rigidbody and a Collider. The RB gives the physics simulation, the Collider gives the physical shape for the simulation.
     
    MaxLevelNoob likes this.
  3. MaxLevelNoob

    MaxLevelNoob

    Joined:
    Sep 17, 2019
    Posts:
    11
    Thanks again for this. So...right now, what would I do if I want to override what happens during an Interact()? I feel like I'd be going back to abstract/interface problem...
     
  4. Giustitia

    Giustitia

    Joined:
    Oct 26, 2015
    Posts:
    113
    You should create a child class from Interactable (like InteractableDoor, for example) and attach it to the door. Another option is to handle it with events: Interactable script just fire an event whenever it interact with something. Get your Door suscribed to this event and handle the response inside the Door script :D
     
    MaxLevelNoob likes this.
  5. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,537
    There's tons of ways to do it...

    For interacting with things the way I do it in most of my games is with an interface.

    So for an example... in 'Prototype Mansion' and 'Garden Variety Body Horror'. We have this interface:

    Code (csharp):
    1.  
    2. using UnityEngine;
    3. using com.spacepuppy;
    4. using com.spacepuppy.AI.Sensors;
    5.  
    6. namespace com.mansion.Entities.GamePlay
    7. {
    8.  
    9.     /// <summary>
    10.     /// The type of button interaction expected
    11.     /// </summary>
    12.     public enum InteractionType
    13.     {
    14.         /// <summary>
    15.         /// Failed to interact.
    16.         /// </summary>
    17.         Failed = 0,
    18.         /// <summary>
    19.         /// A button press simple active.
    20.         /// </summary>
    21.         Press = 1,
    22.         /// <summary>
    23.         /// Button is held for duration of interaction, signal a Release.
    24.         /// </summary>
    25.         Hold = 2
    26.     }
    27.  
    28.     public interface IInteractable : IComponent
    29.     {
    30.    
    31.         /// <summary>
    32.         /// Called by entity when the thing was interacted with.
    33.         /// </summary>
    34.         /// <param name="entity">The entity interacting with it</param>
    35.         /// <param name="aspect">The aspect, if any, used to locate the interactable</param>
    36.         /// <returns></returns>
    37.         InteractionType InteractWith(IEntity entity, IAspect aspect);
    38.         void Release();
    39.  
    40.     }
    41.  
    42. }
    43.  
    All things that can be interacted with implement this interface. Note that the interactable returns how it was interacted with:

    Failed - the interaction didn't work
    Press - a quick action... pressing a button for instance
    Hold - a long action... something where the player can hold onto and release the thing (hence the 'Release' method of the interface)

    We also passed in the IEntity that interacted with it, and the aspect it found the interactable via (aspects in our game are just tokens in the world that highlight things that can be "scene" by the player/npcs/etc).

    Here is how the player can interact with the things (note, NPCs and the sort can also... but their logic is AI driven. This is the player specific way of interacting. The thing being interacted with should be ambiguous of what/who interacted with it):

    Code (csharp):
    1.  
    2. using UnityEngine;
    3. using System.Collections.Generic;
    4. using System.Linq;
    5.  
    6. using com.spacepuppy;
    7. using com.spacepuppy.AI.Sensors;
    8. using com.spacepuppy.Utils;
    9. using com.spacepuppy.SPInput;
    10.  
    11. using com.mansion.Entities.GamePlay;
    12. using com.mansion.UserInput;
    13.  
    14. namespace com.mansion.Entities.Actors.Player
    15. {
    16.  
    17.     public class PlayerInteractActionStyle : SPComponent, IPlayerActionStyle
    18.     {
    19.  
    20.         #region Fields
    21.  
    22.         [SerializeField]
    23.         [DefaultFromSelf]
    24.         private PlayerActionMotor _actionMotor;
    25.  
    26.         [SerializeField()]
    27.         private Sensor _actionSensor;
    28.  
    29.         [System.NonSerialized()]
    30.         private IEntity _entity;
    31.  
    32.         [System.NonSerialized()]
    33.         private IInteractable _heldInteractable;
    34.         [System.NonSerialized]
    35.         private RadicalCoroutine _holdRoutine;
    36.  
    37.         #endregion
    38.  
    39.         #region CONSTRUCTOR
    40.  
    41.         protected override void Awake()
    42.         {
    43.             base.Awake();
    44.  
    45.             _entity = IEntity.Pool.GetFromSource<IEntity>(this);
    46.         }
    47.  
    48.         #endregion
    49.  
    50.         #region Properties
    51.  
    52.         public PlayerActionMotor ActionMotor
    53.         {
    54.             get { return _actionMotor; }
    55.         }
    56.  
    57.         public Sensor ActionSensor
    58.         {
    59.             get { return _actionSensor; }
    60.             set { _actionSensor = value; }
    61.         }
    62.  
    63.         #endregion
    64.  
    65.         #region Methods
    66.  
    67.         public void HoldItem(IInteractable item)
    68.         {
    69.             if (_heldInteractable != null) _heldInteractable.Release();
    70.             _heldInteractable = item;
    71.  
    72.             if(_holdRoutine == null)
    73.             {
    74.                 _holdRoutine = this.StartRadicalCoroutine(this.OnHoldRoutine());
    75.             }
    76.             else if(!_holdRoutine.Active)
    77.             {
    78.                 _holdRoutine.Start(this, RadicalCoroutineDisableMode.Pauses);
    79.             }
    80.         }
    81.  
    82.         private System.Collections.IEnumerator OnHoldRoutine()
    83.         {
    84.             while(true)
    85.             {
    86.                 yield return null;
    87.                 if (_heldInteractable == null)
    88.                 {
    89.                     _holdRoutine.Stop();
    90.                 }
    91.                 else
    92.                 {
    93.                     var input = Services.Get<IInputManager>().GetDevice<MansionInputDevice>(Game.MAIN_INPUT);
    94.                     if ((input != null && input.GetButtonState(MansionInputs.Action) <= ButtonState.None)
    95.                         || _entity.Type == IEntity.EntityType.UndeadPlayer) //undead player can't interact with stuff
    96.                     {
    97.                         _heldInteractable.Release();
    98.                         _heldInteractable = null;
    99.                         _holdRoutine.Stop();
    100.                     }
    101.                 }
    102.             }
    103.         }
    104.  
    105.         private void AttemptActivate()
    106.         {
    107.             var trans = _entity.transform;
    108.             var pos = trans.position.SetY(0f);
    109.             var forw = trans.forward.SetY(0f);
    110.  
    111.             bool ignoreLowPriority = Game.Scenario.IgnoreLowPriorityInteractables;
    112.             var aspects = from a in _actionSensor.SenseAll()
    113.                           let p = a.transform.position.SetY(0f)
    114.                           where !(ignoreLowPriority && a.Precedence < 1f) && a.Entity is IEntity &&
    115.                                 (!(a is PlayerOrientationRespectedVisualAspect) || VectorUtil.AngleBetween(pos - p, a.transform.forward) < (a as PlayerOrientationRespectedVisualAspect).ApproachAngle / 2f)
    116.                           orderby -a.Precedence, VectorUtil.AngleBetween(p - pos, forw), Vector3.Distance(p, pos) ascending
    117.                           select a;
    118.             foreach (var aspect in aspects)
    119.             {
    120.                 var go = GameObjectUtil.GetGameObjectFromSource(aspect);
    121.                 if (go == null) continue;
    122.  
    123.                 IInteractable comp = go.GetComponent<IInteractable>() ?? aspect.Entity.FindComponent<IInteractable>();
    124.                 if (comp == null) continue;
    125.  
    126. #if UNITY_EDITOR
    127.                 Debug.Log(string.Format("INTERACT: {0}({1})", comp.gameObject.name, comp.GetType().Name), comp as UnityEngine.Object);
    128. #endif
    129.  
    130.                 switch (comp.InteractWith(_entity, aspect))
    131.                 {
    132.                     case InteractionType.Failed:
    133.                         continue;
    134.                     case InteractionType.Press:
    135.                         break;
    136.                     case InteractionType.Hold:
    137.                         this.HoldItem(comp);
    138.                         break;
    139.                 }
    140.  
    141.                 //exit foreach loop
    142.                 break;
    143.             }
    144.         }
    145.  
    146.         #endregion
    147.  
    148.         #region IPlayerActionStyle Interface
    149.  
    150.         bool IPlayerActionStyle.OverridingAction
    151.         {
    152.             get
    153.             {
    154.                 return false;
    155.             }
    156.         }
    157.  
    158.         void IPlayerActionStyle.OnStateEntered(PlayerActionMotor motor, IPlayerActionStyle lastState)
    159.         {
    160.  
    161.         }
    162.  
    163.         void IPlayerActionStyle.OnStateExited(PlayerActionMotor motor, IPlayerActionStyle nextState)
    164.         {
    165.  
    166.         }
    167.  
    168.         void IPlayerActionStyle.DoUpdate(MansionInputDevice input, bool isActiveAction)
    169.         {
    170.             if (!isActiveAction || !this.enabled || _entity.Stalled) return;
    171.             //undead player can't interact with stuff
    172.             if (_entity.Type == IEntity.EntityType.UndeadPlayer) return;
    173.  
    174.             if (input.GetButtonState(MansionInputs.Action) == ButtonState.Down)
    175.             {
    176.                 //attempt to activate whatever
    177.                 this.AttemptActivate();
    178.                 _actionMotor.ResetIdleActionTicker(false);
    179.             }
    180.         }
    181.    
    182.         #endregion
    183.  
    184.     }
    185.  
    186. }
    187.  
    So here you can see in 'AttemptActivate' we use our sensor to locate all aspects that can be seen. Then it picks the one closest to and in front of the player out of all those aspects seen. There's also a 'precedence' option on the aspects that filter out higher priority objects (maybe there's an item on the ground and a door to open to leave the scene... we would have the item be higher precedence so that the player doesn't accidentally exit the scene when trying to pick up an item).

    Now for some interactables.

    Here's one where you can poke your partner and he "woo-hoos" like the pilsbury-dough-boy:
    Code (csharp):
    1.  
    2. using UnityEngine;
    3. using System.Collections.Generic;
    4.  
    5. using com.spacepuppy;
    6. using com.spacepuppy.AI.Sensors;
    7. using com.spacepuppy.AI.Sensors.Visual;
    8. using com.spacepuppy.Anim;
    9. using com.spacepuppy.Scenario;
    10. using com.spacepuppy.Tween;
    11. using com.spacepuppy.Utils;
    12.  
    13. using com.mansion.Entities.GamePlay;
    14.  
    15. namespace com.mansion.Scenarios.Episode1
    16. {
    17.  
    18.     [RequireComponentInEntity(typeof(VisualAspect))]
    19.     public class PokeHankInteraction : SPComponent, IInteractable
    20.     {
    21.  
    22.         #region Fields
    23.  
    24.         [SerializeField]
    25.         private float _turnSpeed = 360f;
    26.         [SerializeField]
    27.         [OneOrMany]
    28.         private string[] _doPokeAnims;
    29.         [SerializeField]
    30.         [OneOrMany]
    31.         private string[] _getPokedAnims;
    32.  
    33.         [SerializeField()]
    34.         private Trigger _onActivate;
    35.         [SerializeField]
    36.         private Trigger _onPlayPokeAnim;
    37.         [SerializeField]
    38.         private Trigger _onPlayGetPokedAnim;
    39.         [SerializeField]
    40.         private Trigger _onComplete;
    41.    
    42.         [System.NonSerialized]
    43.         private RadicalCoroutine _routine;
    44.  
    45.         #endregion
    46.  
    47.         #region Methods
    48.  
    49.         private System.Collections.IEnumerator DoPokeRoutine(IEntity player)
    50.         {
    51.             var self = IEntity.Pool.GetFromSource<IEntity>(this);
    52.             if (self == null)
    53.             {
    54.                 _routine = null;
    55.                 yield break;
    56.             }
    57.  
    58.             //start
    59.             Game.Scenario.GameStateStack.Push(GameState.InteractiveScene, Constants.GLOBAL_TOKEN);
    60.             if (_onActivate != null && _onActivate.Count > 0) _onActivate.ActivateTrigger(this, null);
    61.  
    62.             //rotate
    63.             yield return LookAt(player.transform, self.transform, _turnSpeed);
    64.  
    65.             ISPAnimationSource animator;
    66.             //play player anim
    67.             if (_onPlayPokeAnim != null && _onPlayPokeAnim.Count > 0) _onPlayPokeAnim.ActivateTrigger(this, null);
    68.  
    69.             animator = player.FindComponent<ISPAnimationSource>();
    70.             if (animator != null && _doPokeAnims != null && _doPokeAnims.Length > 0)
    71.             {
    72.                 var a = animator.GetAnim(_doPokeAnims.PickRandom());
    73.                 if (a != null) a.Play(QueueMode.PlayNow, PlayMode.StopSameLayer);
    74.                 yield return a;
    75.             }
    76.  
    77.             //play self anim
    78.             if (_onPlayGetPokedAnim != null && _onPlayGetPokedAnim.Count > 0) _onPlayGetPokedAnim.ActivateTrigger(this, null);
    79.  
    80.             LookAt(self.transform, player.transform, _turnSpeed);
    81.             animator = self.FindComponent<ISPAnimationSource>();
    82.             if (animator != null && _getPokedAnims != null && _getPokedAnims.Length > 0)
    83.             {
    84.                 var a = animator.GetAnim(_getPokedAnims.PickRandom());
    85.                 if (a != null) a.Play(QueueMode.PlayNow, PlayMode.StopSameLayer);
    86.             }
    87.        
    88.             //clean up
    89.             if (_onComplete != null && _onComplete.Count > 0) _onComplete.ActivateTrigger(this, null);
    90.             Game.Scenario.GameStateStack.Pop(GameState.InteractiveScene, Constants.GLOBAL_TOKEN);
    91.             _routine = null;
    92.         }
    93.  
    94.         private static Tweener LookAt(Transform observer, Transform target, float speed)
    95.         {
    96.             var dir = (target.position - observer.position).SetY(0f);
    97.             var q = Quaternion.LookRotation(dir, Vector3.up);
    98.             var a = Quaternion.Angle(observer.rotation, q);
    99.             if (a > MathUtil.EPSILON)
    100.             {
    101.                 var dur = a / speed;
    102.                 return SPTween.Tween(observer)
    103.                               .To("rotation", dur, q)
    104.                               .Play(true);
    105.             }
    106.             else
    107.             {
    108.                 return null;
    109.             }
    110.         }
    111.    
    112.         #endregion
    113.  
    114.         #region IInteractable Interface
    115.  
    116.         InteractionType IInteractable.InteractWith(IEntity entity, IAspect aspect)
    117.         {
    118.             if (!this.isActiveAndEnabled) return InteractionType.Failed;
    119.             if (_routine != null && _routine.Active) return InteractionType.Failed;
    120.             if (entity == null || entity.Type != IEntity.EntityType.Player) return InteractionType.Failed;
    121.  
    122.             _routine = this.StartRadicalCoroutine(this.DoPokeRoutine(entity));
    123.  
    124.             return InteractionType.Press;
    125.         }
    126.  
    127.         void IInteractable.Release()
    128.         {
    129.  
    130.         }
    131.  
    132.         #endregion
    133.  
    134.     }
    135.  
    136. }
    137.  
    But how MOST of our interactions work is through what we call our "T&I system", it's sort of like UnityEvents, but will a few extra bells and whistles that allows for creating sequences in game through the inspector.

    So the starter is our hook into the T&I to fire off a 'Trigger' (like UnityEvent):
    Code (csharp):
    1.  
    2. using UnityEngine;
    3.  
    4. using com.spacepuppy;
    5. using com.spacepuppy.AI.Sensors.Visual;
    6. using com.spacepuppy.Scenario;
    7.  
    8. namespace com.mansion.Entities.GamePlay
    9. {
    10.  
    11.     [RequireComponentInEntity(typeof(VisualAspect))]
    12.     public class PlayerInteractable : SPComponent, IInteractable, IObservableTrigger
    13.     {
    14.  
    15.         #region Fields
    16.  
    17.         [SerializeField()]
    18.         private Trigger _onActivate;
    19.  
    20.         #endregion
    21.  
    22.         #region Properties
    23.  
    24.         public Trigger OnActivate
    25.         {
    26.             get { return _onActivate; }
    27.         }
    28.  
    29.         #endregion
    30.  
    31.         #region IPlayerInteractable Interface
    32.  
    33.         public InteractionType InteractWith(IEntity entity, com.spacepuppy.AI.Sensors.IAspect aspect)
    34.         {
    35.             if (!this.isActiveAndEnabled) return InteractionType.Failed;
    36.  
    37.             _onActivate.ActivateTrigger(this, entity);
    38.             return InteractionType.Press;
    39.         }
    40.  
    41.         void IInteractable.Release()
    42.         {
    43.             //release is never use
    44.         }
    45.  
    46.         #endregion
    47.  
    48.         #region IObservableTrigger Interface
    49.  
    50.         Trigger[] IObservableTrigger.GetTriggers()
    51.         {
    52.             return new Trigger[] { _onActivate };
    53.         }
    54.  
    55.         #endregion
    56.  
    57.     }
    58. }
    59.  
    So this is attached to our aspect on say a door... this door transitions between scenes:
    upload_2020-5-20_15-26-54.png

    Note that it triggers a gameobject called e.PlayOpenSequence:
    upload_2020-5-20_15-28-14.png

    Various things happen here...

    We pause the scene for an interaction cutscene.
    We enable a special camera for the cutscene.
    We play an animation on the door.
    We do a special cleanup task that cleans out garbage before transitioning scenes.
    And we trigger some audio to play (the sound of the door opening)

    When the animation is complete we then play e.End:
    upload_2020-5-20_15-29-33.png

    So now we fade out the screen.
    And we tell the game to load a new scene... in this case 'Garden_B', and we do so with the special token "PlayerSpawnNode.FromGreenhouse.West".

    This special token is what tells us where to place the player in the new scene that gets loaded. Since there may be several entrances to that scene.

    Resulting in:
    DoorOpen.gif

    This isn't to say this is how you must do it.

    This is just how I did it.

    The big reason we don't have some explicit "door" script is that not every door behaves the same. Some doors just move the player between rooms:
    upload_2020-5-20_15-43-6.png
    We effectively are faking the whole "resident evil" style door animation and just moving the character from in front of the door to behind the door.

    Where as other times, like the one above with the animation... it actually loads another scene.

    Some play audio, others don't. Some are locked, others not. Some can be kicked down! That's the whole opening of this trailer is Hank kicking down a door to a locked shed cause Hank don't need no stinking key!


    (note, some people may wonder why I constantly use this specific game as source for showing examples... and well... cause it's a game I have access to that I'm allowed to share. Our next title is still under dev and can't be shared)
     
    Last edited: May 20, 2020
    jwinn, wickedwaring and MaxLevelNoob like this.
  6. MaxLevelNoob

    MaxLevelNoob

    Joined:
    Sep 17, 2019
    Posts:
    11
    Great suggestions...This'd work, thank you! I guess I just can't wrap my head around
    Great suggestion, the first point might be too messy for me. But the 2nd one is fantastic! Thank you! Do you know how I can start doing this, should I do the Observer Pattern or use Delegates or does Unity have their own way of doing this? What's the standard way for doing this event system?
     
  7. MaxLevelNoob

    MaxLevelNoob

    Joined:
    Sep 17, 2019
    Posts:
    11
    This is definitely what I hope to achieve...Thanks for the great example and explanation! I will try try to learn this! Stay safe!
     
  8. Giustitia

    Giustitia

    Joined:
    Oct 26, 2015
    Posts:
    113
    I normally use delegates and events, they are kinda straight forward and very flexible. Unity also has UnityEvents, so check some info about it and then choose the one you prefer :D