I’d like to discuss a bit what solution you think is more desired when using interfaces for interaction between objects. Typical example could be universal interaction between Player / Enemy / Animal / Whatever and Single Door / Double Door / Trap Door / Whatever.
Let say we have simple DoorMovement script which is attached on every door. Then we have multiple objects which can interact with the door (Player, Enemy, etc.).
The door have trigger collider attached.
Do you think it’s better to handle the trigger action in DoorMovement script or in Player / Enemy script? Solution #1:
In DoorMovement script OnTriggerEnter would look like this:
if (other.GetComponent<IDoorOpeningAble>() != null)
OpenDoor();
For this case we just need empty IDoorOpeningAble interface to be connected to any script attached to Player / Enemy, etc. This is almost identical approach as when using tags however the advantage is there is no need for if condition chedks in case many types of objects can interact with door.
Solution #2: (my feeling is this is more appropriate)
To handle the triggerEnter / Stay / Exit in Player’s script directly (e.g. DoorInteraction) and DoorMovement script (attached to every single door) would implement IDoorMovementInterface.
Then in DoorInteraction script the code in trigger collider event handler would look like:
IDoorMovement doorMovement = other.GetComponent<IDoorMovement>();
if (doorMovement != null)
doorMovement.OpenDoor();
We can also use GetComponents instead of GetComponent in case we expect more scripts attached to the door object. Advantage of this is we can really have classes with single responsibility. For example treat door movement audio in separate DoorMovementAudio script instead of hardcode it to the DoorMovement script as in future there might be need to have some silent door without audio…
What do you guys think? I’d really love to hear the pros / cons especially from those who already worked on a bigger game projects.
Any discussion on this points is strongly welcome.
Or maybe there is even better solution I haven’t realized yet.
Also I believe it’s better (from the performance point of view) to treat the interaction in the script which is attached to lower amount of objects. E.g. 1 player and 5 enemies vs 150 doors as we’d have 150 event handlers only for TriggerEnter action when the first solution is used.
I usually have an interface called ‘IInteractable’.
Then my agents have a ‘Interactor’ script attached to them, and they can interact with things.
It looks a bit like this:
using UnityEngine;
using com.spacepuppy;
namespace com.mansion.Entities.GamePlay
{
/// <summary>
/// The type of button interaction expected
/// </summary>
public enum InteractionType
{
/// <summary>
/// No interaction expected.
/// </summary>
None = 0,
/// <summary>
/// A button press simple active.
/// </summary>
Press = 1,
/// <summary>
/// Button is held for duration of interaction, signal a Release.
/// </summary>
Hold = 2
}
public interface IInteractable : IComponent
{
InteractionType InteractWith(IEntity entity);
void Release();
}
}
using UnityEngine;
using com.spacepuppy;
using com.spacepuppy.AI.Sensors.Visual;
using com.spacepuppy.Scenario;
namespace com.mansion.Entities.GamePlay
{
[RequireComponentInEntity(typeof(VisualAspect))]
public class PlayerInteractable : SPComponent, IInteractable
{
#region Fields
[SerializeField()]
private Trigger _onActivate;
#endregion
#region IPlayerInteractable Interface
public InteractionType InteractWith(IEntity entity)
{
if (!this.isActiveAndEnabled) return InteractionType.None;
_onActivate.ActivateTrigger(entity);
return InteractionType.Press;
}
void IInteractable.Release()
{
//release is never use
}
#endregion
}
}
And then in my PlayerActionMotor:
//this is called from Update when Input.GetButtonDown("Action"), or the like
private void AttemptActivate()
{
var trans = _entity.transform;
var pos = trans.position;
var forw = trans.forward.SetY(0f);
var aspect = (from a in _actionSensor.SenseAll()
where 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).FirstOrDefault();
var go = ObjUtil.GetAsFromSource<GameObject>(aspect);
if (go != null)
{
IInteractable comp;
if (go.FindComponent<IInteractable>(out comp))
{
switch (comp.InteractWith(_entity))
{
case InteractionType.None:
case InteractionType.Press:
break;
case InteractionType.Hold:
if (_heldInteractable != null) _heldInteractable.Release();
_heldInteractable = comp;
break;
}
}
}
}
There are other interactables that are more complicated… like the PushableObject which is used for pushing things around the scene (player crouches and plays a animation like they’re pushing a heavy box or statue):
using UnityEngine;
using com.spacepuppy;
using com.spacepuppy.Movement;
using com.spacepuppy.Tween;
using com.spacepuppy.Utils;
using com.mansion.Entities.Actors.Player;
namespace com.mansion.Entities.GamePlay
{
[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)
{
if (!this.isActiveAndEnabled) return InteractionType.None;
if (_playerMotor != null) return InteractionType.None;
if (entity.Type != IEntity.EntityType.Player) return InteractionType.None;
var motor = entity.FindComponent<MovementMotor>();
if (motor == null) return InteractionType.None;
_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
}
}
Thanks for the reply. Yes, it looks like the solution I tend to use. You could also return interface to avoid the enumeration and the switch statement. But sure for this case the enum is sufficient. Instead of the enum you’d need extra 3 classes plus 1 interface. Sometimes the pros and cons are pretty much in a ballance ;).
The reason I use the enum is because if it were an interface it’s all or nothing. With the return value I can conditionally be a hold or not depending on context. Unless that is I returned an object of some interface… but then that’d require another object to exist, somethign that could be represented by a int (enum).
Lets say I have an interactable that is a bomb… every time you pick it up (‘hold’) it ticks one more closer to exploding (like hot potato)… and say the 3rd time it doesn’t get ‘held’, and instead just explodes. I can have a ticker in the interactable that increments, and on the 3rd time it returns ‘Press’ and explodes… and the code of the agent interacting with it knows not to play the ‘pickup and hold’ animation.
Of course arguments could be made for other routes, this is just the one I landed on. For example as an interface, I’d still have to test if it is the ‘holdable’ interface… so I don’t have a switch statement, but rather an if statement testing if a type IS something… and personally I’m not a fan of code that is constantly checking if it’s a type unless I really have to (I consider it code smell… not a never do, but a sign things might be off).
Yeah it’s constant fight and ballancing between code smell related problems and complexity of abstraction of very clean solution. Also sometimes clean solution costs so much time in comparison with slightly dirty solution so yes it’s absolutely not black and white as plenty of factors need to be considered.