Interact System Example

An example of how my environmental asset Interaction System works.

public abstract class InteractTrigger : MonoBehaviour
{
    public GameObject[] receiverObjs;
    protected IInteractReceiver[] receivers;
    public UnityEvent<InteractData> onTriggerEvent;
    protected bool activationStatus = true;

    public virtual void Awake()
    {
        receivers = new IInteractReceiver[receiverObjs.Length];
        for (int i = 0; i < receiverObjs.Length; i++)
        {
            receivers[i] = receiverObjs[i].GetComponent<IInteractReceiver>();
            if (receivers[i] == null) Debug.LogError("Given receiver object index " + i + " did not have a IInteractReceiver component.");
        }
    }

    public virtual void Trigger()
    {
        if (receivers.Length > 0)
        {
            InteractData data = new InteractData(this, activationStatus);
            foreach (IInteractReceiver receiver in receivers)
            {
                if (receiver != null) receiver.ReceiveTrigger(data);
                else Debug.LogWarning("InteractReceiver was null or was destroyed. Check For InteractReceiver component on receiverObjs");
                
            }
            onTriggerEvent.Invoke(data);
        }
        else
        {
            onTriggerEvent.Invoke(new InteractData(this, activationStatus));
        }
        
    }
}

public struct InteractData
{
    public InteractTrigger trigger;
    public bool activationStatus;

    public InteractData(InteractTrigger trig, bool status)
    {
        trigger = trig;
        activationStatus = status;
    }
}
public interface IInteractReceiver
{
    public abstract void ReceiveTrigger(InteractData data);
}
public class AreaEnterTrigger : InteractTrigger
{
    [SerializeField] private bool deactivateOnTrigger = true;
    [SerializeField] private bool forceOnStatusWhenTriggerExit = false;

    protected void OnTriggerEnter2D(Collider2D collision)
    {
        Trigger();
        activationStatus = !activationStatus;
        if (deactivateOnTrigger)
        {
            gameObject.SetActive(false);    
        }
    }

    protected void OnTriggerExit2D(Collider2D collision)
    {
        if (forceOnStatusWhenTriggerExit) activationStatus = true;
    }


}
public class OpenableChest : MonoBehaviour, IInteractReceiver, ISavable
{
    
    [SerializeField] protected VisualEffect particles;
    [SerializeField] protected bool locked = false;
    [SerializeField] private GameObject lockSprite;
    [SerializeField] private GameObject interactArea;
    [SerializeField] private AudioClip openSFX;
    [SerializeField] private AudioClip unlockSFX;
    protected SpawnObject spawnObject;
    protected Animator anim;
    protected AudioSource audiosource;

    protected const float particlesLifeTime = 1.1f;
    protected float lootBoundsDist;

    public ChestState state = ChestState.Unlocked;

    private void Awake()
    {
        anim = GetComponent<Animator>();
        spawnObject = GetComponent<SpawnObject>();
        audiosource = GetComponent<AudioSource>();

        if (locked)
        {
            state = ChestState.Locked;
        }

        RegisterISavable();
    }

    public void LockChest()
    {
        locked = true;
        state = ChestState.Locked;
    }

    public void UnlockChest()
    {
        locked = false;
        audiosource.clip = unlockSFX;
        state = ChestState.Unlocked;
    }

    public void ReceiveTrigger(InteractData data)
    {
        if (locked)
        {
            StartCoroutine(FlashLockedSprite());
        }
        else
        {
            interactArea.SetActive(false);
            audiosource.clip = openSFX;
            anim.Play("ChestOpen");
            audiosource.Play();
            state = ChestState.Open;
        }

        
    }

    private IEnumerator FlashLockedSprite()
    {
        lockSprite.SetActive(true);
        yield return new WaitForSeconds(1);
        lockSprite.SetActive(false);
    }

    public void SpawnLoot()
    {
        particles.Play();
        StartCoroutine(SpawnItemsAfter(0.3f));
        StartCoroutine(StopParticles());
    }

    private IEnumerator SpawnItemsAfter(float delay)
    {
        yield return new WaitForSeconds(delay);
        spawnObject.Spawn();
    }

    private IEnumerator StopParticles()
    {
        yield return new WaitForSeconds(particlesLifeTime);
        particles.Stop();
    }

    public void RegisterISavable()
    {
        LevelSerializer.RegisterSavableObject(Save, Load);
    }

    public void DeregisterISavable()
    {
        LevelSerializer.DeregisterSavableObject(Save, Load);
    }

    private void OnDestroy()
    {
        DeregisterISavable();
    }

    public void Save(string path)
    {
        ES3.Save<ChestState>(GetComponent<UniqueID>().ID, state, path);
    }

    public void Load(string path)
    {
        state = ES3.Load<ChestState>(GetComponent<UniqueID>().ID, path, ChestState.Unlocked);
        switch (state)
        {
            case ChestState.Unlocked:
                locked = true;
                anim.Play("Dummy");
                break;

            case ChestState.Locked:
                locked = false;
                anim.Play("Dummy");
                break;

            default:
                locked = false;
                anim.Play("ChestOpenStatic");
                interactArea.gameObject.SetActive(false);
                break;
        }
    }
}

public enum ChestState
{
    Locked, Unlocked, Open
}

This class is part of the two abstract classes that make up the base of this system. InteractTrigger simply triggers and interaction between itself and whatever it was set up to trigger with.

InteractData holds data on the interaction such as the activation status, which basically works as an on or off signal, that the receivers interpret. A door opening on an “on'“ signal but not on a “off” signal, for instance.

The IInteractReceiver interface is the other part of the foundation of this system. It simply has a method that receives input from the trigger object. That behaviour is then implemented by whatever class is contracted with the interface.

This is an example of a concrete implementation of the Trigger class. This one triggers an interaction when a trigger enters the trigger area.

This is an example of a concrete implementation of the receiver class. This utilizes an interactArea trigger which causes a trigger when the player is inside an area and presses the interact button, which results in the chest opening.