Interaction Systems

Affordances Everywhere

The intent was to have many things in the world be interactable, so any system created needed to be generic enough that it could be easily applied elsewhere. Everything that is interactable in the game uses this system, and interactions are used to trigger functions on a variety of external objects, for example, turning on a nearby AudioSource after ending a conversation.

IInteractable

I started by defining a simple interface interactable objects, this defines what every interactable object in the world has available to other classes.

    public interface IInteractable {
        /// <summary>
        ///     Returns true if <paramref name="actor" /> can interact with the object.
        /// </summary>
        /// <param name="actor">The Actor executing the interaction</param>
        /// <returns></returns>
        bool CanInteract(Actor actor);

        /// <summary>
        ///     Interacts with the object
        /// </summary>
        /// <param name="actor">The Actor executing the interaction</param>
        void Interact(Actor actor);
    }

The player interacts with every object using only this interface, it allows the use of multiple class hierarchy's for interactable items without requiring extra code for the player.

InteractableObject

Next I wrote a base class for a generic InteractableObject, that implements the IInteractable interface. It leverages UnityAction (Unity API) to enable drag and drop event creation in the inspector
interactableAction

/// <summary>
///     Base interactable object
/// </summary>
public class InteractableObject : MonoBehaviour, IInteractable {
    public UnityEvent InteractionEvents;

    private bool _interactable = true;

    /// <summary>
    ///     Sets or Gets whether the objects interaction is enabled
    /// </summary>
    protected bool Interactable { get { return _interactable; } set { _interactable = value; } }

    /// <summary>
    ///     Returns true if the <paramref name="actor" /> can interact with the object.
    /// </summary>
    /// <param name="actor">The actor requesting the interaction</param>
    /// <returns></returns>
    public virtual bool CanInteract(Actor actor) {
        return Interactable && isActiveAndEnabled;
    }

    /// <summary>
    ///     Interact with the object
    /// </summary>
    /// <param name="actor">The actor requesting the interaction</param>
    /// <returns></returns>
    public void Interact(Actor actor) {
        if (CanInteract(actor)) {
            DoInteraction(actor);
        }
    }

    /// <summary>
    ///     Activetes/Deactivates the ability to interact with this object
    /// </summary>
    /// <param name="interactable">Active state</param>
    public void SetInteractable(bool interactable) {
        Interactable = interactable;
    }

    /// <summary>
    ///     Init
    /// </summary>
    public virtual void Start() {
#if UNITY_EDITOR
        // Add a debug event in the editor
        InteractionEvents.AddListener(() => Debug.Log("Interacting with " + gameObject.name));
#endif
    }

    /// <summary>
    ///     Executed upon interaction
    /// </summary>
    /// <param name="actor">The actor requesting the interaction</param>
    protected virtual void DoInteraction(Actor actor) {
        InteractionEvents.Invoke();
    }
}

Interactable Item

The player will need to interact with and pick up items, for this we need an inventory system outlined in my Inventory System post and an Interactable Object child class to execute item specific actions.

This class is very simple as all core behaviour is present in the parent class. It adds its Item to the Actor's inventory, and then destroys itself.

/// <summary>
///     An item that can be picked up
/// </summary>
public class InteractableItem : InteractableObject {
    /// <summary>
    ///     The Item data
    /// </summary>
    public Item Item;

    /// <inheritdoc cref="InteractableObject.DoInteraction" />
    protected override void DoInteraction(Actor actor) {
#if UNITY_EDITOR
        Debug.Log("Interacted with " + gameObject.name);
#endif
        base.DoInteraction(actor);

        // Give an item to the actor
        if (actor.GiveItem(Item)) {
            Destroy(gameObject);
        }
    }
}

Lock/Key Based interaction

Sometimes there is a requirement for the player to have a specific item before they can interact with an object, a door or chest for example, this is where the Lock/Key mechanism comes into play.

If the player has Item "A" in their inventory and tries to interact with the lock, it will open and remove the item from the inventory, otherwise it will do nothing. As with the previous class, it uses UnityAction to allow the drag/drop assignment of external class functions to fire in the inspector.

The class fits its current role (door) and any other role that requires an open/close functionality, but this class really should be 2 classes, one with unlocking only and one with open/close for doors etc

/// <summary>
    ///     A Lockable InteractableObject
    /// </summary>
    public class LockableObject : InteractableObject {
        /// <summary>
        ///     Events fired on lock close
        /// </summary>
        public UnityEvent CloseEvents;

        /// <summary>
        ///     Is the lock locked?
        /// </summary>
        public bool IsLocked;

        /// <summary>
        ///     Is the lock open?
        /// </summary>
        public bool IsOpen;

        /// <summary>
        ///     The name of the item that unlocks the object
        /// </summary>
        public string KeyName = "";

        /// <summary>
        ///     Events fired on object open
        /// </summary>
        public UnityEvent OpenEvents;

        /// <summary>
        ///     Events fired on lock unlocking
        /// </summary>
        public UnityEvent UnlockEvents;

        /// <summary>
        ///     Animator to send animation triggers to
        /// </summary>
        private Animator _anim;

        /// <summary>
        ///     Can the actor interact with the LockableObject?
        /// </summary>
        /// <param name="actor">The actor trying to interact</param>
        /// <returns></returns>
        public override bool CanInteract(Actor actor) {
            // block interactions
            if (!Interactable) {
                return false;
            }

            // unlock if can unlock
            if (!IsLocked) {
                return true;
            }

            // try unlock with item
            if (KeyName != "") {
                return actor.Inventory.GetItemCount(new Item(KeyName)) > 0;
            }

            // not locked
            return true;
        }

        /// <summary>
        ///     Init
        /// </summary>
        public override void Start() {
            base.Start();
            _anim = GetComponent<Animator>() ?? GetComponentInChildren<Animator>();
#if UNITY_EDITOR
            // Add debug events in the editor
            UnlockEvents.AddListener(() => Debug.Log("Unlocking"));
            CloseEvents.AddListener(() => Debug.Log("Closing"));
            OpenEvents.AddListener(() => Debug.Log("Opening"));
#endif
        }

        /// <summary>
        ///     Interacts with the door, unlocking, opening and closing.
        /// </summary>
        /// <param name="actor"></param>
        protected override void DoInteraction(Actor actor) {
            if (IsLocked) {
                if (KeyName != "") {
                    if (actor.Inventory.RemoveItem(new Item(KeyName))) {
                        Unlock();
                        return;
                    }
                }
            }

            if (IsOpen) {
                Close();
            }
            else if (!IsOpen) {
                Open();
            }
        }

        /// <summary>
        ///     Closes the LockableObject
        /// </summary>
        private void Close() {
            IsOpen = false;
            _anim.SetTrigger("Close");
            CloseEvents.Invoke();
            InteractionEvents.Invoke();
        }

        /// <summary>
        ///     Opens the door
        /// </summary>
        private void Open() {
            IsOpen = true;
            _anim.SetTrigger("Open");
            OpenEvents.Invoke();
            InteractionEvents.Invoke();
        }

        /// <summary>
        ///     Unlocks the LockableObject
        /// </summary>
        private void Unlock() {
            IsLocked = false;
            UnlockEvents.Invoke();
            InteractionEvents.Invoke();
        }
    }

NPC

As NPC's are interactable, it makes sense that they inherit from InteractableObject (in restrospect, an interface would have been a better fit) to trigger the conversation system (available on my Conversation System post) and any other actions subscribed to in the inspector.

public class NPC : InteractableObject {
        /// <summary>
        ///     The conversation class for this NPC
        /// </summary>
        public Conversation Conversation;

        /// <summary>
        ///     The xml asset for this npc's conversation
        /// </summary>
        public TextAsset ConversationFile;

        public UnityEvent OnConversationStart;

        /// <summary>
        ///     Serialize the conversation to file
        /// </summary>
        [Button]
        public void SerializeConversation() {
            if (Conversation != null) {
                Conversation.SaveAssetFile();
            }
        }

        /// <summary>
        ///     Init
        /// </summary>
        public override void Start() {
            base.Start();
            DeSerializeConversation();
        }

        /// <inheritdoc cref="InteractableObject.DoInteraction" />
        protected override void DoInteraction(Actor actor) {
            StartConversation();
            base.DoInteraction(actor);
        }

        /// <summary>
        ///     Serialize the conversation to file
        /// </summary>
        [Button]
        private void DeSerializeConversation() {
            if (ConversationFile != null) {
                Conversation = new Conversation();
                Conversation.LoadAssetFile(ConversationFile);
            }
        }

        /// <summary>
        ///     Start a conversation
        /// </summary>
        private void StartConversation() {
            FindObjectOfType<ConversationScreen>()
                .StartConversation(Conversation);
            OnConversationStart.Invoke();
        }
    }

interactable-Dependencies-Graph