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:
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.
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:
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.