Designing encounter logic without excessive dependencies

I’m working on implementing random encounters in an RPG, ranging from a random enemy encounter to a cutscene playing. The best way to implement this in my mind is to create some base Encounter class with a virtual OnEncountered method, create a child class for each type of encounter (combat/story/item discovery), and place the encounter’s unique properties (spawn a monster, add an item to the inventory, contact the cutscene system and queue up a cutscene) inside each child’s OnEncounter.

What’s causing me to scratch my head a bit is that while putting the logic inside Encounter makes sense, executing it is a problem: there’s such a diversity of functionality encompassed here that it seems like the only way to give encounters the freedom to do what they need is to give OnEncountered a reference to my game manager class (which has the methods used to spawn monsters, items, and generally execute any global logic). That seems reaaaally sketchy, since Encounter is a tiny child class in one branch of the program, and letting it directly invoke methods on GameManager would be giving it an awful lot of power.

So am I sneaking up on an antipattern here, or is giving Encounter this level of control reasonably acceptable architecture?

If encounters need to be able to spawn stuff, and your spawn logic is held inside the GameManager… yeah, it’s going to need a reference.

The fact it needs the GameManager isn’t a flaw in your design of the Encounter… it’s got to do with your design of the GameManager being where said spawn logic is located.

Also… I’d advise using an interface IEncounter, rather than a base class Encounter. This way you’re not restricted in inheritance when you want to design encounters.

1 Like

That’s a good idea, thank you… do you think it’s worth refactoring things so that the common utility stuff like spawns are less tied to my manager class, or is this a case where it’s clunky, but not worth fixing before it causes explicit problems?

Is there a reason you think it would cause explicit problems?

I don’t know your design, so I couldn’t say exactly. Personally my spawn stuff is a distinct object (actually can be multiple objects):

using UnityEngine;
using System.Collections.Generic;
using System.Linq;

using com.spacepuppy.Utils;

namespace com.spacepuppy.Spawn
{

    [AddComponentMenu("SpacePuppy/Spawn/Spawn Pool")]
    public class SpawnPool : SPNotifyingComponent, ISpawnFactory, ISerializationCallbackReceiver
    {

        #region Static Multiton Interface

        public const string DEFAULT_SPAWNPOOL_NAME = "Spacepuppy.PrimarySpawnPool";

        private static SpawnPool _defaultPool;
        private static List<SpawnPool> _pools = new List<SpawnPool>();

        public static SpawnPool DefaultPool
        {
            get
            {
                if (_defaultPool == null) CreatePrimaryPool();
                return _defaultPool;
            }
        }

        public static SpawnPool Pool(int index)
        {
            if(index < 0 || index > _pools.Count) throw new System.IndexOutOfRangeException();
            if (_defaultPool == null) CreatePrimaryPool();
            if (index == 0) return _defaultPool;

            return _pools[index - 1];
        }

        public static SpawnPool Pool(string name)
        {
            if (_defaultPool != null && _defaultPool.name == name) return _defaultPool;

            //TODO - should cache 'name' for access so this doesn't generate garbage
            var e = _pools.GetEnumerator();
            while(e.MoveNext())
            {
                if (e.Current.name == name) return e.Current;
            }
            return null;
        }

        public static int PoolCount { get { return _pools.Count + 1; } }

        public static void CreatePrimaryPool()
        {
            if (PrimaryPoolExists) return;

            var go = new GameObject(DEFAULT_SPAWNPOOL_NAME);
            _defaultPool = go.AddComponent<SpawnPool>();
        }

        public static bool PrimaryPoolExists
        {
            get
            {
                if (_defaultPool != null) return true;

                _defaultPool = null;
                var point = (from p in GameObject.FindObjectsOfType<SpawnPool>() orderby p.name == DEFAULT_SPAWNPOOL_NAME select p).FirstOrDefault();
                if (!object.ReferenceEquals(point, null))
                {
                    _defaultPool = point;
                    return true;
                }
                else
                {
                    return false;
                }
            }
        }

        #endregion

        #region Fields

        [SerializeField()]
        private List<PrefabCacheOptions> _registeredPrefabs = new List<PrefabCacheOptions>();
        [System.NonSerialized()]
        private Dictionary<GameObject, PrefabCacheOptions> _prefabToCache = new Dictionary<GameObject, PrefabCacheOptions>(com.spacepuppy.Collections.ObjectInstanceIDEqualityComparer<GameObject>.Default);

        #endregion

        #region CONSTRUCTOR

        protected override void Awake()
        {
            base.Awake();
            if (!Object.ReferenceEquals(this, _defaultPool))
            {
                _pools.Add(this);
            }
        }

        protected override void Start()
        {
            base.Start();

            var e = _registeredPrefabs.GetEnumerator();
            while(e.MoveNext())
            {
                e.Current.Load();
            }
        }

        protected override void OnDestroy()
        {
            base.OnDestroy();

            if (Object.ReferenceEquals(this, _defaultPool))
            {
                _defaultPool = null;
            }
            else
            {
                _pools.Remove(this);
            }
        }

        #endregion

        #region Properties

        public int Count { get { return _registeredPrefabs.Count; } }

        #endregion

        #region Methods

        public GameObject Spawn(int index, Transform par = null, System.Action<GameObject> initializeProperties = null, ISpawner optionalSpawnPoint = null)
        {
            if (index < 0 || index >= _registeredPrefabs.Count) throw new System.IndexOutOfRangeException();

            var cache = _registeredPrefabs[index];
            var pos = (par != null) ? par.position : Vector3.zero;
            var rot = (par != null) ? par.rotation : Quaternion.identity;
            var obj = cache.Spawn(pos, rot, par, initializeProperties);
            this.SignalSpawned(obj, optionalSpawnPoint);
            return obj.gameObject;
        }

        public GameObject Spawn(int index, Vector3 position, Quaternion rotation, Transform par = null, System.Action<GameObject> initializeProperties = null, ISpawner optionalSpawnPoint = null)
        {
            if (index < 0 || index >= _registeredPrefabs.Count) throw new System.IndexOutOfRangeException();

            var cache = _registeredPrefabs[index];
            var obj = cache.Spawn(position, rotation, par, initializeProperties);
            this.SignalSpawned(obj, optionalSpawnPoint);
            return obj.gameObject;
        }

        public GameObject Spawn(string sname, Transform par = null, System.Action<GameObject> initializeProperties = null, ISpawner optionalSpawnPoint = null)
        {
            //var cache = (from o in _registeredPrefabs where o.ItemName == sname select o).FirstOrDefault();
            PrefabCacheOptions cache = null;
            var e = _registeredPrefabs.GetEnumerator();
            while(e.MoveNext())
            {
                if(e.Current.ItemName == sname)
                {
                    cache = e.Current;
                    break;
                }
            }
            if (cache == null) return null;

            var pos = (par != null) ? par.position : Vector3.zero;
            var rot = (par != null) ? par.rotation : Quaternion.identity;
            var obj = cache.Spawn(pos, rot, par, initializeProperties);
            this.SignalSpawned(obj, optionalSpawnPoint);
            return obj.gameObject;
        }

        public GameObject Spawn(string sname, Vector3 position, Quaternion rotation, Transform par = null, System.Action<GameObject> initializeProperties = null, ISpawner optionalSpawnPoint = null)
        {
            //var cache = (from o in _registeredPrefabs where o.ItemName == sname select o).FirstOrDefault();
            PrefabCacheOptions cache = null;
            var e = _registeredPrefabs.GetEnumerator();
            while (e.MoveNext())
            {
                if (e.Current.ItemName == sname)
                {
                    cache = e.Current;
                    break;
                }
            }
            if (cache == null) return null;

            var obj = cache.Spawn(position, rotation, par, initializeProperties);
            this.SignalSpawned(obj, optionalSpawnPoint);
            return obj.gameObject;
        }

        public GameObject Spawn(GameObject prefab, Transform par = null, System.Action<GameObject> initializeProperties = null, ISpawner optionalSpawnPoint = null)
        {
            var controller = SpawnAsController(prefab, par, initializeProperties, optionalSpawnPoint);
            return (controller != null) ? controller.gameObject : null;
        }

        public GameObject Spawn(GameObject prefab, Vector3 position, Quaternion rotation, Transform par = null, System.Action<GameObject> initializeProperties = null, ISpawner optionalSpawnPoint = null)
        {
            var controller = SpawnAsController(prefab, position, rotation, par, initializeProperties, optionalSpawnPoint);
            return (controller != null) ? controller.gameObject : null;
        }
       
        public T SpawnAsComponent<T>(T prefab, Transform par = null, System.Action<GameObject> initializeProperties = null, ISpawner optionalSpawnPoint = null) where T : class
        {
            var go = GameObjectUtil.GetGameObjectFromSource(prefab);
            if (object.ReferenceEquals(go, null)) return null;

            var controller = SpawnAsController(go, par, initializeProperties, optionalSpawnPoint);
            if (controller == null) return null;

            var result = controller.GetComponent<T>();
            if(result == null)
            {
                this.Despawn(controller);
                return null;
            }
            else
            {
                return result;
            }
        }
       
        public T SpawnAsComponent<T>(T prefab, Vector3 position, Quaternion rotation, Transform par = null, System.Action<GameObject> initializeProperties = null, ISpawner optionalSpawnPoint = null) where T : class
        {
            var go = GameObjectUtil.GetGameObjectFromSource(prefab);
            if (object.ReferenceEquals(go, null)) return null;

            var controller = SpawnAsController(go, position, rotation, par, initializeProperties, optionalSpawnPoint);
            if (controller == null) return null;

            var result = controller.GetComponent<T>();
            if (result == null)
            {
                this.Despawn(controller);
                return null;
            }
            else
            {
                return result;
            }
        }

        public T SpawnAsComponent<T>(GameObject prefab, Transform par = null, System.Action<GameObject> initializeProperties = null, ISpawner optionalSpawnPoint = null) where T : class
        {
            if (object.ReferenceEquals(prefab, null)) return null;

            var controller = SpawnAsController(prefab, par, initializeProperties, optionalSpawnPoint);
            if (controller == null) return null;

            var result = controller.GetComponent<T>();
            if (result == null)
            {
                this.Despawn(controller);
                return null;
            }
            else
            {
                return result;
            }
        }

        public T SpawnAsComponent<T>(GameObject prefab, Vector3 position, Quaternion rotation, Transform par = null, System.Action<GameObject> initializeProperties = null, ISpawner optionalSpawnPoint = null) where T : class
        {
            if (object.ReferenceEquals(prefab, null)) return null;

            var controller = SpawnAsController(prefab, position, rotation, par, initializeProperties, optionalSpawnPoint);
            if (controller == null) return null;

            var result = controller.GetComponent<T>();
            if (result == null)
            {
                this.Despawn(controller);
                return null;
            }
            else
            {
                return result;
            }
        }

        public SpawnedObjectController SpawnAsController(GameObject prefab, Transform par = null, System.Action<GameObject> initializeProperties = null, ISpawner optionalSpawnPoint = null)
        {
            var cache = this.FindPrefabCache(prefab);
            var pos = (par != null) ? par.position : Vector3.zero;
            var rot = (par != null) ? par.rotation : Quaternion.identity;

            if (cache != null)
            {
                var controller = cache.Spawn(pos, rot, par, initializeProperties);
                this.SignalSpawned(controller, optionalSpawnPoint);
                return controller;
            }
            else
            {
                var obj = PrefabUtil.Create(prefab, pos, rot, par);
                if (obj != null)
                {
                    var controller = obj.AddOrGetComponent<SpawnedObjectController>();
                    controller.Init(this);
                    if (initializeProperties != null) initializeProperties(obj);
                    controller.SetSpawned();
                    this.SignalSpawned(controller, optionalSpawnPoint);
                    return controller;
                }
                else
                {
                    return null;
                }
            }
        }

        public SpawnedObjectController SpawnAsController(GameObject prefab, Vector3 position, Quaternion rotation, Transform par = null, System.Action<GameObject> initializeProperties = null, ISpawner optionalSpawnPoint= null)
        {
            var cache = this.FindPrefabCache(prefab);
            if (cache != null)
            {
                var controller = cache.Spawn(position, rotation, par, initializeProperties);
                this.SignalSpawned(controller, optionalSpawnPoint);
                return controller;
            }
            else
            {
                var obj = PrefabUtil.Create(prefab, position, rotation, par);
                if (obj != null)
                {
                    var controller = obj.AddOrGetComponent<SpawnedObjectController>();
                    controller.Init(this);
                    if (initializeProperties != null) initializeProperties(obj);
                    controller.SetSpawned();
                    this.SignalSpawned(controller, optionalSpawnPoint);
                    return controller;
                }
                else
                {
                    return null;
                }
            }
        }
       

        /// <summary>
        /// Return an object to the pool.
        /// </summary>
        /// <param name="obj"></param>
        /// <returns>Returns true if the object was returned to the pool, false if destroyed.</returns>
        internal bool Despawn(SpawnedObjectController obj)
        {
            if (Object.ReferenceEquals(obj, null)) throw new System.ArgumentNullException("obj");

            obj.BroadcastMessage(SPSpawnConstants.MSG_ONDESPAWN, SendMessageOptions.DontRequireReceiver);
            var n = new DeSpawnNotification(obj);
            Notification.PostNotification<DeSpawnNotification>(obj, n, false);
            Notification.PostNotification<DeSpawnNotification>(this, n, false);

            var e = _registeredPrefabs.GetEnumerator();
            while(e.MoveNext())
            {
                if(e.Current.Contains(obj))
                {
                    e.Current.Despawn(obj);
                    return true;
                }
            }

            //if we reached here, it's not managed by this pool... destroy
            GameObject.Destroy(obj.gameObject);
            return false;
        }

        /// <summary>
        /// If a GameObject is being destroyed, this method will purge it from the SpawnPool.
        /// </summary>
        /// <param name="obj"></param>
        internal void Purge(SpawnedObjectController obj)
        {
            if (Object.ReferenceEquals(obj, null)) throw new System.ArgumentNullException("obj");

            var n = new SpawnPoolPurgedNotification(obj);
            Notification.PostNotification<SpawnPoolPurgedNotification>(obj, n, false);
            Notification.PostNotification<SpawnPoolPurgedNotification>(this, n, false);

            var e = _registeredPrefabs.GetEnumerator();
            while(e.MoveNext())
            {
                if(e.Current.Contains(obj))
                {
                    e.Current.Purge(obj);
                    return;
                }
            }
        }

        /// <summary>
        /// Match an object to its prefab if this pool manages the GameObject.
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        private PrefabCacheOptions FindPrefabCache(GameObject obj)
        {
            //TODO - figure out the best way to match a gameobject to the cache pool.
            //as it stands this depends on the prefab being a shared instance across all scripts... 
            //I am unsure if that's how unity works, and what the limitations are on that.
            //consider creating a system of relating equal prefabs

            //test if the object is the prefab in question
            if (_prefabToCache.ContainsKey(obj)) return _prefabToCache[obj];

            var controller = obj.FindComponent<SpawnedObjectController>();
            if (controller != null && controller.Pool == this)
            {
                var e = _registeredPrefabs.GetEnumerator();
                while(e.MoveNext())
                {
                    if (e.Current.Contains(controller)) return e.Current;
                }
            }

            return null;
        }

        private void SignalSpawned(SpawnedObjectController obj, ISpawner spawnPoint)
        {
            if (obj == null) return;


            var n = new SpawnNotification(this, obj.gameObject, spawnPoint);
            obj.gameObject.BroadcastMessage(SPSpawnConstants.MSG_ONSPAWN, n, SendMessageOptions.DontRequireReceiver);
            Notification.PostNotification(obj, n, false);
            Notification.PostNotification(this, n, false);
        }

        #endregion

        #region ISerializationCallbackReceiver Interface

        void ISerializationCallbackReceiver.OnBeforeSerialize()
        {
            _registeredPrefabs.RemoveAll((c) => Object.ReferenceEquals(c, null));
        }

        void ISerializationCallbackReceiver.OnAfterDeserialize()
        {
            _prefabToCache.Clear();

            _registeredPrefabs.RemoveAll((c) => Object.ReferenceEquals(c, null));

            var e = _registeredPrefabs.GetEnumerator();
            PrefabCacheOptions cache;
            while(e.MoveNext())
            {
                cache = e.Current;
                cache.Init(this);
                if (!Object.ReferenceEquals(cache.Prefab, null)) _prefabToCache[cache.Prefab] = cache;
            }
        }

        #endregion

        #region Special Types

        [System.Serializable()]
        public class PrefabCacheOptions
        {

            public string ItemName;
            public GameObject Prefab;

            public int CacheSize;
            public int ResizeBuffer;
            public int LimitAmount;

            [System.NonSerialized()]
            private SpawnPool _owner;
            [System.NonSerialized()]
            private HashSet<SpawnedObjectController> _instances = new HashSet<SpawnedObjectController>(com.spacepuppy.Collections.ObjectReferenceEqualityComparer<SpawnedObjectController>.Default);
            [System.NonSerialized()]
            private HashSet<SpawnedObjectController> _activeInstances = new HashSet<SpawnedObjectController>(com.spacepuppy.Collections.ObjectReferenceEqualityComparer<SpawnedObjectController>.Default);

            public PrefabCacheOptions()
            {

            }

            public int Count
            {
                get { return _instances.Count + _activeInstances.Count; }
            }

            public bool Contains(SpawnedObjectController obj)
            {
                return _instances.Contains(obj) || _activeInstances.Contains(obj);
            }

            internal void Init(SpawnPool owner)
            {
                _owner = owner;
            }

            internal void Load()
            {
                if (_instances != null) return;

                this.Clear();
                if (Prefab == null) return;

                for (int i = 0; i < CacheSize; i++)
                {
                    _instances.Add(this.CreateCacheInstance(i));
                }
            }

            internal void Clear()
            {
                using(var lst = com.spacepuppy.Collections.TempCollection.GetList<SpawnedObjectController>(_instances))
                {
                    _instances.Clear();

                    var e = lst.GetEnumerator();
                    while (e.MoveNext())
                    {
                        e.Current.DeInit();
                        Object.Destroy(e.Current);
                    }
                }
            }

            /// <summary>
            /// 
            /// </summary>
            /// <param name="par"></param>
            /// <param name="initializeProperties">A method that initializes properties on the gameobject before OnSpawned message is called.</param>
            /// <returns></returns>
            internal SpawnedObjectController Spawn(Vector3 pos, Quaternion rot, Transform par, System.Action<GameObject> initializeProperties)
            {
                if(_instances.Count == 0)
                {
                    int cnt = this.Count;
                    int newSize = cnt + ResizeBuffer;
                    if (LimitAmount > 0) newSize = Mathf.Min(newSize, LimitAmount);

                    if (newSize > cnt)
                    {
                        for (int i = cnt; i < newSize; i++)
                        {
                            _instances.Add(CreateCacheInstance(i));
                        }
                    }
                    else
                    {
                        return null;
                    }
                }

                //var cntrl = _instances[0];
                //_instances.RemoveAt(0);
                var cntrl = _instances.Pop();

                _activeInstances.Add(cntrl);

                par.AddChild(cntrl.transform);
                cntrl.transform.position = pos;
                cntrl.transform.rotation = rot;
                if (initializeProperties != null) initializeProperties(cntrl.gameObject);
                cntrl.SetSpawned();

                return cntrl;
            }

            internal void Despawn(SpawnedObjectController obj)
            {
                if (!_activeInstances.Contains(obj)) return;

                _activeInstances.Remove(obj);

                obj.SetDespawned();
                obj.transform.parent = _owner.transform;
                obj.transform.localPosition = Vector3.zero;
                obj.transform.rotation = Quaternion.identity;

                _instances.Add(obj);
            }

            internal void Purge(SpawnedObjectController obj)
            {
                if(_activeInstances.Contains(obj))
                {
                    _activeInstances.Remove(obj);
                }
                if(_instances.Contains(obj))
                {
                    _instances.Remove(obj);
                }
            }




            private SpawnedObjectController CreateCacheInstance(int index)
            {
                var obj = PrefabUtil.Create(this.Prefab, Vector3.zero, Quaternion.identity);
                obj.name = this.ItemName + (index + 1).ToString("000");
                var cntrl = obj.AddComponent<SpawnedObjectController>();
                cntrl.Init(_owner, this.ItemName);

                obj.transform.parent = _owner.transform;
                obj.transform.localPosition = Vector3.zero;
                obj.transform.rotation = Quaternion.identity;

                //obj.DeactivateAll();
                obj.SetActive(false);

                return cntrl;
            }

        }

        #endregion



        #region Static Methods

        public static void DeSpawn(GameObject go)
        {
            if (go == null) throw new System.ArgumentNullException("go");

            var c = go.GetComponent<SpawnedObjectController>();
            if (c == null) return;

            c.Kill();
        }

        #endregion


    }

}

WIth this I can use either the ‘DefaultPool’ which is just a generic spawner that usese GameObject.Instantiate… where as in a scene I might define a pool for that specific scene that has catalogues of recyclable types which I might access by name.

Completely distinct from my GameManager… primarily because the concept of a GameManager is not defined in my framework at all.

This way I can write scripts recyclable from project to project.

Why not give out references to your GameManager which are limited to the appropriate interfaces?

Although the real question is why do you have a GameManager at all. Sounds like the class has too much to do.

1 Like

I think that’s probably the core problem. I’ve been playing with solutions based on everyone’s feedback, and the main issue I keep coming back to is that without a global manager class, managing game flow becomes a lot harder. By having a single object that has references to the database, game state, and every entity spawned in the world, doing things like loading save games, changing game modes from overworld to town, and all that jazz become as simple as checking the database, loading/deleting the appropriate entities, and changing an enumeration that tracks game state. Without a centralized point of management, that same task becomes an issue of tracking down multiple disparate logic managers and hoping they’re all present and functional.

If you want the advantages of a single central manager, you are just going to have to deal with the fact that your code will have a lot of interwoven dependencies.

Assuming I want to get away from that, would you recommendation be to effectively create one manager class per category of functionality (spawning, serialization, and so forth), and simply give anything that needed that functionality the references to find and invoke the necessary managers?

I tend to avoid managers all together, and give responsibility over to individual entities. That’s just my style, but it works for me.

You need some managers to control game state, but that doesn’t need to be extensive.

That makes a lot more sense; thank you for weighing in :slight_smile: