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:
using UnityEngine;
using com.spacepuppy;
using com.spacepuppy.AI.Sensors;
namespace com.mansion.Entities.GamePlay
{
/// <summary>
/// The type of button interaction expected
/// </summary>
public enum InteractionType
{
/// <summary>
/// Failed to interact.
/// </summary>
Failed = 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
{
/// <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();
}
}
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):
using UnityEngine;
using System.Collections.Generic;
using System.Linq;
using com.spacepuppy;
using com.spacepuppy.AI.Sensors;
using com.spacepuppy.Utils;
using com.spacepuppy.SPInput;
using com.mansion.Entities.GamePlay;
using com.mansion.UserInput;
namespace com.mansion.Entities.Actors.Player
{
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<IInputManager>().GetDevice<MansionInputDevice>(Game.MAIN_INPUT);
if ((input != null && input.GetButtonState(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()
let p = a.transform.position.SetY(0f)
where !(ignoreLowPriority && a.Precedence < 1f) && a.Entity is IEntity &&
(!(a is PlayerOrientationRespectedVisualAspect) || VectorUtil.AngleBetween(pos - p, a.transform.forward) < (a as PlayerOrientationRespectedVisualAspect).ApproachAngle / 2f)
orderby -a.Precedence, VectorUtil.AngleBetween(p - pos, 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>() ?? aspect.Entity.FindComponent<IInteractable>();
if (comp == null) continue;
#if UNITY_EDITOR
Debug.Log(string.Format("INTERACT: {0}({1})", comp.gameObject.name, comp.GetType().Name), comp as UnityEngine.Object);
#endif
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, bool isActiveAction)
{
if (!isActiveAction || !this.enabled || _entity.Stalled) return;
//undead player can't interact with stuff
if (_entity.Type == IEntity.EntityType.UndeadPlayer) return;
if (input.GetButtonState(MansionInputs.Action) == ButtonState.Down)
{
//attempt to activate whatever
this.AttemptActivate();
_actionMotor.ResetIdleActionTicker(false);
}
}
#endregion
}
}
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:
using UnityEngine;
using System.Collections.Generic;
using com.spacepuppy;
using com.spacepuppy.AI.Sensors;
using com.spacepuppy.AI.Sensors.Visual;
using com.spacepuppy.Anim;
using com.spacepuppy.Scenario;
using com.spacepuppy.Tween;
using com.spacepuppy.Utils;
using com.mansion.Entities.GamePlay;
namespace com.mansion.Scenarios.Episode1
{
[RequireComponentInEntity(typeof(VisualAspect))]
public class PokeHankInteraction : SPComponent, IInteractable
{
#region Fields
[SerializeField]
private float _turnSpeed = 360f;
[SerializeField]
[OneOrMany]
private string[] _doPokeAnims;
[SerializeField]
[OneOrMany]
private string[] _getPokedAnims;
[SerializeField()]
private Trigger _onActivate;
[SerializeField]
private Trigger _onPlayPokeAnim;
[SerializeField]
private Trigger _onPlayGetPokedAnim;
[SerializeField]
private Trigger _onComplete;
[System.NonSerialized]
private RadicalCoroutine _routine;
#endregion
#region Methods
private System.Collections.IEnumerator DoPokeRoutine(IEntity player)
{
var self = IEntity.Pool.GetFromSource<IEntity>(this);
if (self == null)
{
_routine = null;
yield break;
}
//start
Game.Scenario.GameStateStack.Push(GameState.InteractiveScene, Constants.GLOBAL_TOKEN);
if (_onActivate != null && _onActivate.Count > 0) _onActivate.ActivateTrigger(this, null);
//rotate
yield return LookAt(player.transform, self.transform, _turnSpeed);
ISPAnimationSource animator;
//play player anim
if (_onPlayPokeAnim != null && _onPlayPokeAnim.Count > 0) _onPlayPokeAnim.ActivateTrigger(this, null);
animator = player.FindComponent<ISPAnimationSource>();
if (animator != null && _doPokeAnims != null && _doPokeAnims.Length > 0)
{
var a = animator.GetAnim(_doPokeAnims.PickRandom());
if (a != null) a.Play(QueueMode.PlayNow, PlayMode.StopSameLayer);
yield return a;
}
//play self anim
if (_onPlayGetPokedAnim != null && _onPlayGetPokedAnim.Count > 0) _onPlayGetPokedAnim.ActivateTrigger(this, null);
LookAt(self.transform, player.transform, _turnSpeed);
animator = self.FindComponent<ISPAnimationSource>();
if (animator != null && _getPokedAnims != null && _getPokedAnims.Length > 0)
{
var a = animator.GetAnim(_getPokedAnims.PickRandom());
if (a != null) a.Play(QueueMode.PlayNow, PlayMode.StopSameLayer);
}
//clean up
if (_onComplete != null && _onComplete.Count > 0) _onComplete.ActivateTrigger(this, null);
Game.Scenario.GameStateStack.Pop(GameState.InteractiveScene, Constants.GLOBAL_TOKEN);
_routine = null;
}
private static Tweener LookAt(Transform observer, Transform target, float speed)
{
var dir = (target.position - observer.position).SetY(0f);
var q = Quaternion.LookRotation(dir, Vector3.up);
var a = Quaternion.Angle(observer.rotation, q);
if (a > MathUtil.EPSILON)
{
var dur = a / speed;
return SPTween.Tween(observer)
.To("rotation", dur, q)
.Play(true);
}
else
{
return null;
}
}
#endregion
#region IInteractable Interface
InteractionType IInteractable.InteractWith(IEntity entity, IAspect aspect)
{
if (!this.isActiveAndEnabled) return InteractionType.Failed;
if (_routine != null && _routine.Active) return InteractionType.Failed;
if (entity == null || entity.Type != IEntity.EntityType.Player) return InteractionType.Failed;
_routine = this.StartRadicalCoroutine(this.DoPokeRoutine(entity));
return InteractionType.Press;
}
void IInteractable.Release()
{
}
#endregion
}
}
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):
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, IObservableTrigger
{
#region Fields
[SerializeField()]
private Trigger _onActivate;
#endregion
#region Properties
public Trigger OnActivate
{
get { return _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
#region IObservableTrigger Interface
Trigger[] IObservableTrigger.GetTriggers()
{
return new Trigger[] { _onActivate };
}
#endregion
}
}
So this is attached to our aspect on say a door… this door transitions between scenes:
Note that it triggers a gameobject called e.PlayOpenSequence:
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:
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:

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:
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)