Skip to main content
Clarifying strategy
Source Link
DMGregory
  • 136.3k
  • 22
  • 247
  • 373

For something like this, I'd set up a separate asset file for each "version" of your level that you want to load. We'll use this data structure to transform the scene on start-up. Even though we're always transitioning to the same single scene file, we can use this data to change which spawn points are active and what spawns at each one in the first frame after we arrive in the new scene.

Let's call itthis data structure a "scenario":

For something like this, I'd set up a separate asset file for each "version" of your level that you want to load. Let's call it a "scenario":

For something like this, I'd set up a separate asset file for each "version" of your level that you want to load. We'll use this data structure to transform the scene on start-up. Even though we're always transitioning to the same single scene file, we can use this data to change which spawn points are active and what spawns at each one in the first frame after we arrive in the new scene.

Let's this data structure a "scenario":

Source Link
DMGregory
  • 136.3k
  • 22
  • 247
  • 373

For something like this, I'd set up a separate asset file for each "version" of your level that you want to load. Let's call it a "scenario":

[CreateAssetMenu(fileName="Scenario.asset", menuName="Level Data/Scenario")]
public class Scenario : ScriptableObject {

    // Hold the current scenario for ease of access.
    // (Global variable to be quick & dirty, but you can improve on this later if needed)
    public static Scenario current;

    [System.Serializable]
    public struct SpawnInfo {
        public string name;
        public Transform prefab;
    }    

    [SerializeField]
    List<SpawnInfo> _spawns;

    public Transform GetPrefabForSpawnPoint(string name) {
        // Return the prefab slated for that spawn point name in our list, or null.
        var info = _spawns.Find(i => i.name == name);
        return info.prefab;
    }
}

This will let you create an asset in your project broswer like this:

Create > Level Data > Scenario

You'll create one scenario for each character. Inside a character's scenario asset, you can specify a list of what should spawn at each spawn point in your level. You can also add other parameters - like a time limit or special scoring rule or alternate music track / rendering effects to apply when this character is playing.

Scenario Inspector

Now, at each spawn point, you can add an empty GameObject with a script like this:

public class SpawnPoint : MonoBehaviour {

    void Start() {
        // Ask the current scenario what is supposed to spawn here.
        var prefab = Scenario.current.GetPrefabForSpawnPoint(gameObject.name);
 
        // If nothing is set up to spawn here, abort!
        if (prefab == null)
            return;

        // Otherwise, spawn the selected prefab at this marker object's position & orientation.
        Instantiate(prefab, transform.position, transform.rotation);
    }
}

Name each spawn point game object something clearly identifiable, so you can easily recognize which spawn point is which in your hierarchy view, and match that name when wiring up the spawns in your scenarios.

Note that the names don't have to be unique: you could have 5 spawn points named "Spawn Group A" - then anything the scenario says should spawn at "Spawn Group A" will spawn from all 5 of those locations. This can help when setting up masses of grunt enemies or pickups/powerups.

Lastly, we need your character selection script to set the current scenario before loading the scene. Here's one way to do it:

public class CharacterSelect : MonoBehaviour {

    [System.Serializable]
    public struct CharacterInfo {
        public string name;
        public Scenario scenario;
        public int sceneToLoad;
    }
    
    [SerializeField]
    List<CharacterInfo> _characters;

    public void SelectCharacter(string name) {
        var info = _characters.Find(i => i.name == name);

        // Catch errors where we type a name wrong, or forgot to provide a scenario.
        if(info.scenario == null) {
            Debug.LogErrorFormat("No scenario set for character names {0}!", name);
            return;
        }

        // Remember this character's scenario,
        // so spawn points in the next scene can reference it.
        Scenario.current = info.scenario;

        // Load the appropriate scene for this character
        // (so you can still use an alt scene for some characters, if needed)
        SceneManager.LoadScene(info.sceneToLoad);
    }
}

Instead of having a separate scene-loading function for every character, your UI buttons can all just call this one character select method, and pass the name of the character they should use:

Wiring up the button

Note that here I've used a "stringly typed" strategy for maximum flexibility: your level designers can add new spawn points or even new characters with new names purely in data, with no code changes. The downside is that you get no compile-time validation that the names link up correctly. One typo, and no scene loads or nothing spawns at that location because the strings don't match exactly. If you want tighter control, you can use an enum instead, to have a list of allowed characters / spawn point IDs, and get stronger tool support to verify that only valid IDs are used.