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:
public interface IDamageable
{
void TakeDamage(float amount);
}
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.
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”.
public interface IInteractable : IComponent
{
/// <summary>
/// Called by entity when the thing was interacted with.
/// </summary>
/// <param name="entity">The entity interacting with it</param>
/// <param name="aspect">The aspect, if any, used to locate the interactable</param>
/// <returns></returns>
InteractionType InteractWith(IEntity entity, IAspect aspect);
void Release();
}
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.
public class PlayerInteractable : SPComponent, IInteractable
{
#region Fields
[SerializeField()]
private Trigger _onActivate;
#endregion
#region IPlayerInteractable Interface
public InteractionType InteractWith(IEntity entity, com.spacepuppy.AI.Sensors.IAspect aspect)
{
if (!this.isActiveAndEnabled) return InteractionType.Failed;
_onActivate.ActivateTrigger(this, entity);
return InteractionType.Press;
}
void IInteractable.Release()
{
//release is never use
}
#endregion
}
But I also have other things that are more specific… like PushableObject:
[RequireComponent(typeof(Rigidbody))]
public class PushableObject : SPComponent, IInteractable
{
#region Fields
[SerializeField]
[DefaultFromSelf(UseEntity =true)]
private Collider _collision;
[System.NonSerialized]
private Rigidbody _body;
[System.NonSerialized()]
private MovementMotor _playerMotor;
[System.NonSerialized()]
private bool _inTransition;
#endregion
#region CONSTRUCTOR
protected override void Awake()
{
base.Awake();
_body = this.GetComponent<Rigidbody>();
}
#endregion
#region Properties
public Rigidbody Body
{
get { return _body; }
}
public Collider Collision
{
get { return _collision; }
}
public bool InTransition
{
get { return _inTransition; }
}
#endregion
#region Methods
public void MoveToPosition(Vector3 targPos, float speed)
{
_inTransition = true;
var dist = (_body.position - targPos).magnitude;
SPTween.Tween(_body)
.To("position", targPos, dist / speed)
.OnFinish((s, e) =>
{
_inTransition = false;
})
.Play(true);
}
#endregion
#region IPlayerInteractable Interface
public InteractionType InteractWith(IEntity entity, com.spacepuppy.AI.Sensors.IAspect aspect)
{
if (!this.isActiveAndEnabled) return InteractionType.Failed;
if (_playerMotor != null) return InteractionType.Failed;
if (entity.Type != IEntity.EntityType.Player) return InteractionType.Failed;
var motor = entity.FindComponent<MovementMotor>();
if (motor == null) return InteractionType.Failed;
_playerMotor = motor;
var style = _playerMotor.States.GetState<PlayerPushMovementStyle>();
style.BeginPush(this);
return InteractionType.Hold;
}
public void Release()
{
if (_playerMotor == null) return;
var style = _playerMotor.States.GetState<PlayerPushMovementStyle>();
style.EndPush();
_playerMotor = null;
}
#endregion
}
Because they all implement the same interface I can generically access them the same way in my ‘PlayerInteractActionStyle’:
public class PlayerInteractActionStyle : SPComponent, IPlayerActionStyle
{
#region Fields
[SerializeField]
[DefaultFromSelf]
private PlayerActionMotor _actionMotor;
[SerializeField()]
private Sensor _actionSensor;
[System.NonSerialized()]
private IEntity _entity;
[System.NonSerialized()]
private IInteractable _heldInteractable;
[System.NonSerialized]
private RadicalCoroutine _holdRoutine;
#endregion
#region CONSTRUCTOR
protected override void Awake()
{
base.Awake();
_entity = IEntity.Pool.GetFromSource<IEntity>(this);
}
#endregion
#region Properties
public PlayerActionMotor ActionMotor
{
get { return _actionMotor; }
}
public Sensor ActionSensor
{
get { return _actionSensor; }
set { _actionSensor = value; }
}
#endregion
#region Methods
public void HoldItem(IInteractable item)
{
if (_heldInteractable != null) _heldInteractable.Release();
_heldInteractable = item;
if(_holdRoutine == null)
{
_holdRoutine = this.StartRadicalCoroutine(this.OnHoldRoutine());
}
else if(!_holdRoutine.Active)
{
_holdRoutine.Start(this, RadicalCoroutineDisableMode.Pauses);
}
}
private System.Collections.IEnumerator OnHoldRoutine()
{
while(true)
{
yield return null;
if (_heldInteractable == null)
{
_holdRoutine.Stop();
}
else
{
var input = Services.Get<IGameInputManager>().GetDevice<MansionInputDevice>(Game.MAIN_INPUT);
if ((input != null && input.GetCurrentButtonState(MansionInputs.Action) <= ButtonState.None)
|| _entity.Type == IEntity.EntityType.UndeadPlayer) //undead player can't interact with stuff
{
_heldInteractable.Release();
_heldInteractable = null;
_holdRoutine.Stop();
}
}
}
}
private void AttemptActivate()
{
var trans = _entity.transform;
var pos = trans.position.SetY(0f);
var forw = trans.forward.SetY(0f);
bool ignoreLowPriority = Game.Scenario.IgnoreLowPriorityInteractables;
var aspects = from a in _actionSensor.SenseAll()
where !(ignoreLowPriority && a.Precedence < 1f) && a.gameObject.EntityHasComponent<IEntity>()
let p = a.transform.position.SetY(0f)
orderby -a.Precedence, VectorUtil.AngleBetween(p, forw), Vector3.Distance(p, pos) ascending
select a;
foreach (var aspect in aspects)
{
var go = GameObjectUtil.GetGameObjectFromSource(aspect);
if (go == null) continue;
IInteractable comp = go.GetComponent<IInteractable>() ?? go.FindComponent<IInteractable>();
if (comp == null) continue;
switch (comp.InteractWith(_entity, aspect))
{
case InteractionType.Failed:
continue;
case InteractionType.Press:
break;
case InteractionType.Hold:
this.HoldItem(comp);
break;
}
//exit foreach loop
break;
}
}
#endregion
#region IPlayerActionStyle Interface
bool IPlayerActionStyle.OverridingAction
{
get
{
return false;
}
}
void IPlayerActionStyle.OnStateEntered(PlayerActionMotor motor, IPlayerActionStyle lastState)
{
}
void IPlayerActionStyle.OnStateExited(PlayerActionMotor motor, IPlayerActionStyle nextState)
{
}
void IPlayerActionStyle.DoUpdate(MansionInputDevice input)
{
//undead player can't interact with stuff
if (_entity.Type == IEntity.EntityType.UndeadPlayer) return;
if (input.GetCurrentButtonState(MansionInputs.Action) == ButtonState.Down)
{
//attempt to activate whatever
this.AttemptActivate();
_actionMotor.ResetIdleActionTicker(false);
}
}
#endregion
}
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.
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.
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):
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.