More efficient way to spawn enemies in my game...?

Hi everyone! My game is a turn-based dungeon crawler where enemies have a chance to spawn every time the player takes a step forwards. It works, but I feel like it’s way too mish-mash and inefficient, not to mention potentially prone to bugs… When an enemy spawns, I prevent the player from moving, and spawn an enemy from a list, giving it health/attack values. Below is the script attached to my player that handles moving as well as spawning enemies - while I’m not having errors, I’m not too experienced and would really appreciate pointers in the right direction as to what I can improve :slight_smile:

PlayerMovement.cs

using UnityEngine;
using UnityEngine.UI;
using System.Collections;

public class PlayerMovement : MonoBehaviour {

	public static bool CanMoveForward;
	public static bool CanMoveBackward;
	public static bool CanMoveLeft;
	public static bool CanMoveRight;
	public static bool CanRotateLeft;
	public static bool CanRotateRight;

	public static bool CanMoveForwardCache;
	public static bool CanMoveBackwardCache;
	public static bool CanMoveLeftCache;
	public static bool CanMoveRightCache;
	public static bool CanRotateLeftCache;
	public static bool CanRotateRightCache;

	public static bool InCombat = false;

	public static int chancetospawn;

	public static bool CanSpawnEnemy = true;

	public Texture2D EnemyObject;

	public RawImage EnemyImage;

	public Texture2D Enemy1;
	public Texture2D Enemy2;
	public Texture2D Enemy3;
	public Texture2D Enemy4;
	public Texture2D Enemy5;
	public Texture2D Enemy6;
	public Texture2D Enemy7;
	public Texture2D Enemy8;
	public Texture2D Enemy9;
	public Texture2D Enemy10;
	public Texture2D EnemyNull;

	public static int EnemyCurHealth;
	public static int EnemyMaxHealth;
	public static int EnemyDamage;

	//Translation:
	float movSpeed = 4.0f;
	Vector3 pos;
	Transform tr ;
	public static bool moving = false;
	
	//Rotation:
	public static bool rotating = false;
	public float rotSpeed = 360f;
	float rotDegrees = 0f;
	Quaternion rotToAngle ;

	void Start () {
		pos = transform.position;
		tr = transform;

		CanMoveForward = true;
		CanMoveBackward = true;
		CanMoveLeft = true;
		CanMoveRight = true;
		CanRotateLeft = true;
		CanRotateRight = true;
	}

	void SpawnEnemy() {
		CanMoveForwardCache = CanMoveForward;
		CanMoveBackwardCache = CanMoveBackward;
		CanMoveLeftCache = CanMoveLeft;
		CanMoveRightCache = CanMoveRight;
		CanRotateLeftCache = CanRotateLeft;
		CanRotateRightCache = CanRotateRight;

		CanMoveForward = false;
		CanMoveBackward = false;
		CanMoveLeft = false;
		CanMoveRight = false;
		CanRotateLeft = false;
		CanRotateRight = false;

		int i = Random.Range (1, 10);

		if(i == 1) { EnemyObject = Enemy1; EnemyMaxHealth = 11; EnemyCurHealth = 11; EnemyDamage = 1; }
		if(i == 2) { EnemyObject = Enemy2; EnemyMaxHealth = 12; EnemyCurHealth = 12; EnemyDamage = 1; }
		if(i == 3) { EnemyObject = Enemy3; EnemyMaxHealth = 13; EnemyCurHealth = 13; EnemyDamage = 1; }
		if(i == 4) { EnemyObject = Enemy4; EnemyMaxHealth = 14; EnemyCurHealth = 14; EnemyDamage = 2; }
		if(i == 5) { EnemyObject = Enemy5; EnemyMaxHealth = 15; EnemyCurHealth = 15; EnemyDamage = 2; }
		if(i == 6) { EnemyObject = Enemy6; EnemyMaxHealth = 16; EnemyCurHealth = 16; EnemyDamage = 3; }
		if(i == 7) { EnemyObject = Enemy7; EnemyMaxHealth = 17; EnemyCurHealth = 17; EnemyDamage = 3; }
		if(i == 8) { EnemyObject = Enemy8; EnemyMaxHealth = 18; EnemyCurHealth = 18; EnemyDamage = 3; }
		if(i == 9) { EnemyObject = Enemy9; EnemyMaxHealth = 19; EnemyCurHealth = 19; EnemyDamage = 4; }
		if(i == 10) { EnemyObject = Enemy10; EnemyMaxHealth = 19; EnemyCurHealth = 19; EnemyDamage = 5; }

		EnemyImage.GetComponent<RawImage>().texture = EnemyObject;
		InCombat = true;
	}

	void EnemySpawned() {
		chancetospawn = 0;

		CanMoveForward = CanMoveForwardCache;
		CanMoveBackward = CanMoveBackwardCache;
		CanMoveLeft = CanMoveLeftCache;
		CanMoveRight = CanMoveRightCache;
		CanRotateLeft = CanRotateLeftCache;
		CanRotateRight = CanRotateRightCache;

		PlayerStats.CurrentXP += 12;

		Debug.Log("Enemy defeated. You have gained 12 XP points.");

		EnemyImage.GetComponent<RawImage>().texture = EnemyNull;
		EnemyDamage = 0;
		EnemyCurHealth = 0;
		EnemyMaxHealth = 0;
		InCombat = false;
	}
		
	void Update () {

		if(CameraLook.AtLeftEnd == true && Input.GetButtonUp("Fire2")) {
			RotLeft();
		}
		if(CameraLook.AtRightEnd == true && Input.GetButtonUp("Fire2")) {
			RotRight();
		}

		if (InCombat && EnemyCurHealth <= 0) {
			EnemySpawned();
		}

		Debug.DrawRay(transform.position, transform.forward, Color.red);
		//Input:
		if (!moving && !rotating) {
			if (Input.GetKey(KeyCode.D) && tr.position == pos && CanMoveRight) {
				MoveRight();
			} else if (Input.GetKey(KeyCode.A) && tr.position == pos && CanMoveLeft) {
				MoveLeft();
			} else if (Input.GetKey(KeyCode.W) && tr.position == pos && CanMoveForward) {
				MoveForward();
			} else if (Input.GetKey(KeyCode.S) && tr.position == pos && CanMoveBackward) {
				MoveBackward();
			} else if (Input.GetKey(KeyCode.Q) && tr.position == pos && CanRotateLeft) {
				RotLeft();
			} else if (Input.GetKey(KeyCode.E) && tr.position == pos && CanRotateRight) {
				RotRight();
			}
		}


		
		//Translation:
		if (moving) {
			if (Vector3.Distance(transform.position,pos) <0.05f){
				transform.position = pos;
				moving=false;
//				Debug.Log("FINISHED MOVE!!!!!!!!");
				if (chancetospawn == 1) {
					SpawnEnemy();
				}
			} else {
				transform.position = Vector3.MoveTowards(transform.position, pos, Time.deltaTime * movSpeed);
			}
		}
		
		//Rotation:
		if (rotating) {
			if (Quaternion.Angle(transform.rotation,rotToAngle) <10f) {
				transform.rotation = rotToAngle;
				rotating=false;

			} else {
				transform.rotation = Quaternion.RotateTowards(transform.rotation, rotToAngle, rotSpeed * Time.deltaTime);
			}
		}

		if(EnemyCurHealth <= 0) {
			EnemyCurHealth = 0;
		}

	}


	public void MoveForward() {
		if (tr.position == pos && CanMoveForward) {
			pos += transform.forward;
			moving=true;
			chancetospawn = Random.Range(1, 10);
		}
	}

	public void MoveBackward() {
		if (tr.position == pos && CanMoveBackward) {
			pos += -transform.forward;
			moving=true;
			chancetospawn = Random.Range(1, 10);
		}
	}

	public void MoveLeft() {
		if (tr.position == pos && CanMoveLeft) {
			pos += -transform.right;
			moving=true;
			chancetospawn = Random.Range(1, 10);
		}
	}

	public void MoveRight() {
		if (tr.position == pos && CanMoveRight) {
			pos += transform.right;
			moving=true;
			chancetospawn = Random.Range(1, 10);
		}
	}

	public void RotLeft() {
		if (tr.position == pos && CanRotateLeft) {
			rotDegrees -= 90f;
			rotToAngle = Quaternion.Euler(0, rotDegrees, 0);
			rotating = true;
		}
	}

	public void RotRight() {
		if (tr.position == pos && CanRotateRight) {
			rotDegrees += 90f;
			rotToAngle = Quaternion.Euler(0, rotDegrees, 0);
			rotating = true;
		}
	}
}

Instead of fully assigning the value to your enemies in the code, create prefab and store them in a array. Then your code sums up to :

public GameObject [] prefabs;
void SpawnEnemy()
{
        Instantiate(this.prefabs[Random.Range(0, this.prefabs.Length)]);
}

All the values are set in the editor.

Have a look at unity scriptable objects,
Here is a good link tutorial for that:


Then you can use a pooling system for each enemy character, this is a personal one i usually use on my projects:

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

public class PoolingSystem : MonoBehaviour {

    public GameObjectPool[] gameObjectPool;

    //Each pool private properties.
    [System.Serializable]
    public class GameObjectPool
    {
        public string Nome;
        public GameObject prefabToPool;
        public int count;
        private List<GameObject> instantiatedObjectList = new List<GameObject>();

        //Instantiate individually each pool.
        public void FillObjectList()
        {
            if (prefabToPool != null)
            {
                for (int x = 0; x < count; x++)
                {
                    AddNewObjectToList();
                    instantiatedObjectList[x].SetActive(false);
                }
            }
            else
            {
                Debug.LogError("You didn't assign one of the prefabs to the pooling system.");
                UnityEditor.EditorApplication.isPlaying = false;
            }
        }

        //Insert a new object slot to the item list.
        private void AddNewObjectToList()
        {
            GameObject newObj = Instantiate(prefabToPool, Vector3.zero, Quaternion.identity);
            newObj.SetActive(false);
            instantiatedObjectList.Add( newObj ) ;
        }

        //Returns an item from the list
        public GameObject GetInactiveItem()
        {
            if (!DoWeHaveFreeItems()) AddNewObjectToList();
            GameObject activeObj = instantiatedObjectList[instantiatedObjectList.Count-1] ;
            instantiatedObjectList.Remove(activeObj);
            activeObj.SetActive(true);
            return activeObj;
        }

        //Checks for items not used in the list.
        private bool DoWeHaveFreeItems()
        {
            if (instantiatedObjectList.Count > 0) return true;
            else return false;
        }

        //Checks if that item belongs in this item pool.
        public bool DoesItBelongHere(GameObject toTest)
        {
            if (!DoWeHaveFreeItems()) AddNewObjectToList();
            if (toTest.name == instantiatedObjectList[0].name) return true;
            else return false;
        }

        //After an item is no longer usefull it get deactivated and reinserted back into the list.
        public void ReInsertBackIntoList(GameObject objToReinsertBackToTheList)
        {
            objToReinsertBackToTheList.SetActive(false);
            instantiatedObjectList.Add(objToReinsertBackToTheList);
        }

    }

    //Create an editor De-Buggable gizmo so you know an object could not be created because something went wrong yet an object shoulda
    //been instantiated at the position this gizmo is at.
    public class ErrorGameObjectPoolingSystem : MonoBehaviour
    {
        public float gizmoSize = 2.0f;
        public Color cor = Color.red;
        private void OnDrawGizmos()
        {
            Gizmos.color = cor;
            Gizmos.DrawWireSphere(transform.position, gizmoSize);
        }
    }
    private GameObject CreateErrorGameObj()
    {
        GameObject newObj = new GameObject();
        newObj.AddComponent<ErrorGameObjectPoolingSystem>();
        return newObj;
    }

    //Instantiate GameobjectArrays for the pool system at the start.
    private void Start()
    {
        foreach (GameObjectPool x in gameObjectPool)
        {
            x.FillObjectList();
        }
    }

    //Returns an inactive object.
    //Transform and scale ARE NOT reset every deactivation.
    //If in the future we may add a normalization feature so it is used in games where we need to alter gameobjects transform at runtime.
    public GameObject InstantiatePoolObject(string gameObjName)
    {
        foreach (GameObjectPool obj in gameObjectPool)
        {
            if (obj.Nome == gameObjName)
                return obj.GetInactiveItem();
        }
        Debug.LogError("There is no GameObject with such a name. Empty game was assigned");
        return CreateErrorGameObj();
    }
    public GameObject InstantiatePoolObject(int objPosition)
    {
        try
        {
            return gameObjectPool[objPosition].GetInactiveItem();
        }
        catch (System.Exception)
        {
            Debug.LogError("There is no GameObject with that index. Empty game was assigned");
            return CreateErrorGameObj();
        }
    }

    //Unused at this point. May be usefull sometime in the future to get list index with the name.
    private int GetIndex(string prefabPoolName)
    {
        for (int i = 0; i < gameObjectPool.Length; i++)
        {
            if (gameObjectPool*.Nome == prefabPoolName)*

return i;
}
return -1;
}

//While it is “destroying” the gameobject if it does not have any in the list to compare it to it will create a new instance of it to compare it to.
//Comparison is done by name. So changing the name will break item connection with the pooling system.
public void DestroyPoolItem(GameObject objToDeactivate)
{
foreach (GameObjectPool poolItem in gameObjectPool)
{
if (poolItem.DoesItBelongHere(objToDeactivate))
{
poolItem.ReInsertBackIntoList(objToDeactivate);
return;
}
}
Debug.LogError(“This item does not have a pool or the item name has been changed during runtime. Still destroyed before it shapeshifts again!”);
Destroy(objToDeactivate);
}
public void DestroyPoolItem(GameObject objToDeactivate, float time)
{
StartCoroutine(WaitB4Deactivating(objToDeactivate,time));
}
private IEnumerator WaitB4Deactivating(GameObject objToDeactivate, float howLong)
{
yield return new WaitForSeconds(howLong);
foreach (GameObjectPool poolItem in gameObjectPool)
{
if (poolItem.DoesItBelongHere(objToDeactivate))
{
poolItem.ReInsertBackIntoList(objToDeactivate);
yield break;
}
}
Debug.LogError(“This item does not have a pool or the item name has been changed during runtime. Still destroyed before it shapeshifts again!”);
Destroy(objToDeactivate);
}

}

That would be a much more effective and more scalable solution.