Is it okay in OOP to handle object events on outside the object class?

Viewed 137

Hello i make my 2d game with unity and i feel confuse about oop design. There is a 4 class in my game.

  1. StageView : The view(scene) where the game logic run.

  2. ObjectPool : The object pool that can manage the gameobjects, and it is the member field of stage view. (It is not a singleton)

  3. Projectile : The projectile class that can attack the monster.

  4. Monster : The monster class that could be attacked by projectile.

    public class Monster : MonoBehaviour
    {
            private int hp;
        
            public delegate void OnMobHitHandler(Projectile projectile, Monster monster, long damage);
            public delegate void OnMobDieHandler(Monster monster);
            public OnMobHitHandler OnMobHit;
            public OnMobDieHandler OnMobDie;
        
            public void OnTriggerEnter2D(Collider2D collision)
            {
                Projectile projectile = collision.gameObject.GetComponent<Projectile>();
                hp -= projectile.Power;
                OnMobHit?.Invoke(projectile, this, projectile.Power);
                if(hp <= 0)
                  OnMobDie?.Invoke(this);
            }
    }

If the monster got hit, then the fragment object will be created. And if the monster die, then it must be released. But the fragment objects are managed by ObjectPool which is in StageView. (Also monster are managed by ObjectPool samely) So there are 2 choices in my head.

  1. Make Init(Objectpool objectpool) Method and pass the objectpool to monster as a parameter. Then put "create fragment logic" and "Release(die) monster logic" in to the Monster class.

  2. Expose the monster's callback event to the StageView like above example code. Their 2 logics must be in StageView's OnMonsterHit, OnMonsterDie.

I choiced second, because i want to loosen the dependencies of Monster class. (You know, if i don't loosen the dependencies then it's gonna be more complicate later) But sometimes, I think that it may seem more appropriate for the object to handle it internally rather than entrusting it to the outside. I'm very confused which one is more correct in OOP aspect.

2 Answers

I think it's a good idea to make one more layer between ObjectPool and Monster classes that will manage what you want. It will complete Single responsibility principle of SOLID.

So both classes ObjectPool and Monster will not depend on each other and every class will be making their jobs.

I done it next way (similar to your second example): Spawner components containing and sometimes sharing the same Pool ScriptableObject. When a Spawner spawns an object it passes OnDespawn handler to Despawner component on the pooled object. From there the spawned object is on its own.

In a sense Spawners act as a middle (sub-pool) layer that (together with other components) are responsible to setup the freshly pooled object.

Btw Spawner adds Despawner component on the pooled object if it is missing and Despawner have destroy fall-back if OnDespawn handler is not assigned. This ensures all prefabs can work both as pool's prototype and stand-alone instantiable.

The Spawner component goes on a spawner game object (for example tied to some trigger-collision region that calls one of the spawn public methods):

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

namespace SpawnSystem
{
    public class Spawner : MonoBehaviour
    {
        [Header("Pooler")]
        // in this case this is pool randomizer but it could be directly a ScriptableObject pool
        [SerializeField] private PoolAlternator pooler;

        [Header("Debug")]
        // this is just to see in the inspector what is spawned for debugging, it can be skipped in a build
        [SerializeField] [ReadOnly] private List<GameObject> spawned;

        [Header("Events")]

        [SerializeField] private UnityEvent<GameObject> onSpawn;
        [SerializeField] private UnityEvent<GameObject> onDespawn;

        #region Spawn
        public GameObject GetSpawn()
        {
            GameObject spawnedObject = pooler.Spawn();
            SpawnRoutine(spawnedObject);
            return spawnedObject;
        }

        public void Spawn()
        {
            GameObject spawnedObject = pooler.Spawn();
            SpawnRoutine(spawnedObject);
        }

        public void Spawn(Vector3 position)
        {
            GameObject spawnedObject = pooler.Spawn(position);
            SpawnRoutine(spawnedObject);
        }

        public void Spawn(Vector3 position, Quaternion rotation)
        {
            GameObject spawnedObject = pooler.Spawn(position, rotation);
            SpawnRoutine(spawnedObject);
        }

        public void Despawn(GameObject go)
        {
            if (go == null) { return; }
            onDespawn?.Invoke(go);
            spawned.Remove(go);
            pooler.Despawn(go);
        }

        public void DespawnAll()
        {
            for (int i = spawned.Count - 1; i >= 0; i--)
            {
                Despawn(spawned[i]);
            }
        }
        #endregion Spawn

        private void SpawnRoutine(GameObject spawnedObject)
        {
            if (spawnedObject == null) { return; }
            spawned.Add(spawnedObject);
            SetupDespawner(spawnedObject);
            onSpawn?.Invoke(spawnedObject);
        }

        private void SetupDespawner(GameObject spawnedObject)
        {
            Despawner despawner = 
                spawnedObject.GetComponentInChildren<Despawner>() 
                ?? spawnedObject.AddComponent<Despawner>();
            despawner.SetReturnToPool(Despawn);
        }
    }
}

The Despawner component lives at the spawned/pooled object. When the entity is dead (say health handling/changing component detects HP is <= 0) it calls Despawner.Despawn():

using UnityEngine;
using UnityEngine.Events;

namespace SpawnSystem
{
    public class Despawner : MonoBehaviour
    {
        [Tooltip("Fall-back strategy when the object is not spawned from a pool.")]
        [SerializeField] private FallbackStrat fallbackTo = FallbackStrat.Destroy;

        [Header("Events")]

        [SerializeField] UnityEvent<GameObject> onDespawn;

        private enum FallbackStrat { Destroy = 0, Disable = 1 }
        private UnityAction<GameObject> returnToPoolRoutine;

        public void SetReturnToPool(UnityAction<GameObject> returnToPool)
        {
            this.returnToPoolRoutine = returnToPool;
        }

        public void Despawn()
        {
            onDespawn?.Invoke(this.gameObject);
            if (returnToPoolRoutine != null)
            {
                returnToPoolRoutine.Invoke(this.gameObject);
                returnToPoolRoutine = null;
            }
            else { Fallback(this.gameObject); }
        }

        private void Fallback(GameObject go)
        {
            switch (fallbackTo)
            {
                default:
                case FallbackStrat.Destroy:
                    Destroy(go);
                    break;
                case FallbackStrat.Disable:
                    go.SetActive(false);
                    break;
            }
        }
    }
}
Related