How can i improve Level spawning and game functionality ?

I am using unity for game development for the last 1.5 years but my approach from the first game till the last game i did remain the same for handling level spawning and other game functionalities like making a manager and then in the list do everything. How can I perform all these functionalities in the better way and why? and what are the problems in this approach
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[System.Serializable]
public class EnemiesData
{
    public Transform enemyPrefeb;
    public Transform enemySpawnPoint;
}

[System.Serializable]

public class Enemies
{
    public string levelName; // Name of the Level
    public int levelObjective; // total objective in current level like if play has to kill 5 
    //enemies then 5 objective
    public string objectiveText;
    public List<EnemiesData> enemyDataList;

}

public class DogManager : MonoBehaviour
{
public static DogManager Instance;

[SerializeField] private GameObject playerPrefeb;

[SerializeField] private List<Enemies> enemiesList;

[SerializeField] private List<Transform> PlayerSpawnPoint;

public int currentLevel = 0;


private void Awake()
{
    Instance = this;
    currentLevel = 5; //lets suppose current level is 5
    SpwanEnemies();
    SpawnPlayer();

}
private void Start()
{
    ShowObjective();
}

#region SpawnRegion
private void SpwanEnemies()
{
    for (int i = 0; i < enemiesList[currentLevel].enemyDataList.Count; i++)
    {
        var tempEnemy = enemiesList[currentLevel].enemyDataList*;*

Instantiate(tempEnemy.enemyPrefeb, tempEnemy.enemySpawnPoint.position, tempEnemy.enemySpawnPoint.rotation);
}
}

private void SpawnPlayer()
{
//Spawn Player
}
#endregion

#region Objective

void ShowObjective()
{
// show objective
}

#endregion

#region player Healh

void CheckPlayerHealth()
{

}

void PlayerDied()
{
// Do player died functaionality
}

#endregion

#region Level Progress

void LevelComplete()
{
// Do level complete functaionality
}

void LevelFail()
{
// Do level fail functaionality
}

#endregion

Hey there,

this is something that is not really answerable in an objective way. All answers you can get on this will be a personal opinion - please take it as such. However i applaud you for this level of self-criticism and search for self-improvement - too few people do this i think.

Your current approach works and that is the most crucial thing imo. There are other ways to do things but they wont help you if your game/product does not work. Please judge my propositions on this.

The approach you are currently using has in my opinion one central flaw:

Monolithic design

Everything is in your main class. The larger your game, the larger the class.

Sure, it is handy to have all the data in one spot and it is easy to build the game, just add another variable and another function and you are done. This however can lead down a slippery slope. When your main script reaches a few thousand lines of code you will struggle to seperate the behaviour into smaller classes. This takes a lot of time and nerves.

Now what are the benefits if you split this up?
There are multiple. I hope i can find the right words. One major thing that gets on my nerves with Unity is that it is really difficult to write Unit-Tests for MonoBehaviours.

This however becomes way easier when you have a MonoBehaviour which is just a container for other classes which contain the actual behaviour.

So for your example you could have your DogManager which is a MonoBehaviour. Then there are other classes: (examples)

  • LevelProgressManager
  • EntitySpawner
  • PlayerBehaviour

These could be simple classes, no inheritance from MonoBehaviour. ā†’ Suddenly you can use NUnit and Moq to create tests which help you in providing a bug-free game.

This also opens up a lot of doors:
Since your behaviour is now encapsuled into Smaller classes, you can exchange these behaviours wheras you had to have some if/else differentiation before.

You can have a LevelProgressManager that does generic levels but then there could be a bonus level that has to do a few things differently for example what should be done when the level was failed ā†’ no problem, use inheritance:

 public class BonusLevelProgressManager : LevelProgressManager

You can simply assign that to your DogManager where before there was a LevelProgressManager, everything still works, behaviour has changed. Scripts remain small.

Same for EntitySpawner. Example:

public class EntitySpawner
{
    public GameObject Entity;
    public Transform defaultPosition;
    
    public virtual GameObject Spawn()
    {
        return Spawn(defaultPosition);
    }

    public virtual GameObject Spawn(Transform transform)
    {
        return GameObject.Instantiate(Entity, transform.position, transform.rotation);
    }
}

public class PlayerSpawn : EntitySpawner
{
    public System.Action<GameObject> OnPlayerSpawned;

    public override GameObject Spawn(Transform transform)
    {
        GameObject go = base.Spawn(transform);
        OnPlayerSpawned?.Invoke(go);
        return go;
    }
}

Use Inheritance to reuse code, make behaviour exchangable (keyword here is dependency injection). Example above: You can now have an EntitySpawner script which has a sub-version that is made to spawn the player which contains a callback to which i can subscribe to trigger functions when the player is spawned. This additionally is a good method to reduce interconnection of classes.

The MAJOR point why you want to do this: It keeps things generic.

This way you can reuse the code that you write now in your next project. You might also have enemies and a player and need a way to spawn them, just copy the classes, you are ready to go. With your current approach you will have to sort through your main script and extract the parts that are actually needed which can be done but is prone to provide more issues than it solves.

That were a lot of words - rant over. I hope something above makes sense, it is late and iā€™m not really sure what i just wrote :smiley: