Search code examples
c#unity-game-engineeventsprefab

How to interact with prefab instance independetly to other its instances?


I am new in C# and Unity3D and I'm struggle using events so I need HELP. So I'm making hypercasual game to improve my skills. In the game I want if player hits an item to different things happened. For example: triggering bomb collider will destroy the bomb and play explosive animation, triggering dollar — destroy it, play dollar animation, add score. PROBLEM !!! When Player hits bomb ALL bombs and money destroying and playing animation. I want to affect on every prafab independetly. I DON"T HAVE ANY STATIC FIELDS.

A SHORT VIDEO OF BEHAVIOR IN THE EDITOR
https://gfycat.com/farawaynegativedore

I have classes Bomb and Money that are inherited from abstract class Item. (Bomb and Money differs only with methods, so I include only Bomb).

In Item.cs I get refference to an event script.

public abstract class Item : MonoBehaviour
{
    protected OnCollisionEvent _onCollisionEventScript;
 
    private void Awake()
    {
        _onCollisionEventScript = FindObjectOfType<OnCollisionEvent>();
    }
 
    protected virtual void HandleAnimation() { }
 
    protected virtual void DestroyOnHitting() { }

In Bomb.cs I subscribe it to an event.

public class Bomb : Item
{
    private void OnEnable()
    {
        _onCollisionEventScript.InteractOnCollision += HandleAnimation;
        _onCollisionEventScript.InteractOnCollision += DestroyOnHitting;
    }

    private void OnDisable()
    {
        _onCollisionEventScript.InteractOnCollision -= HandleAnimation;
        _onCollisionEventScript.InteractOnCollision -= DestroyOnHitting;
    }

    protected override void HandleAnimation()
    {
        Debug.Log("EXPLOSION");
    }

    protected override void DestroyOnHitting()
    {
        Destroy(gameObject);
    }
}

In OnCollisionEvent.cs, which is attached to Player, I invoke the event in "voidOnTriggerEnter()"

public class OnCollisionEvent : MonoBehaviour
{
    public event UnityAction InteractOnCollision;
 
    private void OnTriggerEnter(Collider other)
    {
        InteractOnCollision?.Invoke();
    }
}

In SpawManager.cs I spawn random prefab from List of prefabs at fixed position along Xaxis and at random position along Yaxis

public class SpawnManager : MonoBehaviour
{
    [SerializeField] private List<Item> ItemPrefabs;
    [SerializeField] private float SpawnRepeatRate;
 
    private readonly List<Item> _items = new();
    private float _time = 0f;
 
    private void Start()
    {
        foreach (var itemPrefab in ItemPrefabs)
            _items.Add(itemPrefab);
    }
 
    private void Update()
    {
        _time += Time.deltaTime;
 
        Item randomItem = _items[Random.Range(0, _items.Count)];
 
        if (_time >= SpawnRepeatRate)
        {
            Instantiate(randomItem, GetRandomSpawnPosition(), Quaternion.identity);
 
            _time = 0f;
        }
    }
 
    private Vector3 GetRandomSpawnPosition()
    {
        int[] CoordinatesOnY = new int[11];
        CoordinatesOnY[0] = 2;
 
        for (int i = 1; i < CoordinatesOnY.Length; i++)
            CoordinatesOnY[i] = CoordinatesOnY[i - 1] + 2;
 
        int randomSpawnPositionAlongY = Random.Range(CoordinatesOnY[0], CoordinatesOnY.Length);
 
        Vector3 spawnPosition = new Vector3(24f, randomSpawnPositionAlongY, 0f);
 
        return spawnPosition;
    }
}

Is my mistake on using events and passing references or in implementing SpawnManager and passing references?

I don't have any static fields. Maybe I use event wrong. I thought that in SpawnManager.cs I should create a list/array of references to prefabs, but I don't know how to implement it.


Solution

  • So your problem is that all your bombs and money listen to the same single event. That FindObjectOfType<OnCollisionEvent>(); basically almost equals having it static in your case.

    You should rather invert this logic and either let the trigger on the player handle what should happen

    public class OnCollisionEvent : MonoBehaviour
    {
        private void OnTriggerEnter(Collider other)
        {
            if(other.TryGetComponent<Item>(out var item))
            {
                // only trigger the callback/method on the specific item you currently collide with
                // I would pass on the reference so you can use GetComponent to access other 
                // components on the player e.g. for damage and money management
                item.TriggerEvent(this);
            }
        }
    }
    

    then on your item class you can simply have a

    public abstract class Item : MonoBehaviour
    {
        public void TriggerEvent(OnCollisionEvent player)
        {
            HandleAnimation(player);
    
            DestroyOnHitting();
        }
     
        protected virtual void HandleAnimation(OnCollisionEvent  player) {
            // inheritors can use the player reference to access other components and 
            // e.g. inflict damage or increase money etc
        }
     
        protected virtual void DestroyOnHitting() { }
    }
    

    The alternative would be to completely invert the logic flow and rather handle the event on the item

    public abstract class Item : MonoBehaviour
    {
        protected void OnTriggerEnter(Collider other)
        {
            // `Player` would be a central component on your player object 
            // that can have further references like e.g. to the damage and money management etc
            if(other.TryGetComponent<Player>(out var player))
            {
                HandleAnimation(player);
    
                DestroyOnHitting();
            }
        }
     
        protected virtual void HandleAnimation(Player player) { }
     
        protected virtual void DestroyOnHitting() { }
    }
    

    and get rid of the OnCollisionEvent at all.