0
\$\begingroup\$

I'm trying to create a character selection menu in Unity, where each character has their own version of the level that follows.

There are about 10 characters. Each character has a different location where they spawn in the level, and each enemy has a different spawn position as well, depending on which character was selected.

What I have been doing is to create separate levels (scene files) for each character. Then in the character selection menu, I load the level depending on the level selected by the user.

This seems to be a lot of code and data repetition, as the scene is the same in all levels; it's just that the spawn positions and enemies are different.

Is there any way have only one level scene, and get the spawn position and enemy spawns based on the character?

This is the code I have so far in GameManager.cs:

public GameObject enemy;
public Transform enemySpawnPoint;

public GameObject player;
public Transform playerSpawnPoint;

public static GameManager instance;

private void Awake()
{
    if (instance == null)
    {
        instance = this;
    }
}

void Start()
{
    StartCoroutine("SpawnEnemies");
    SpawnPlayer();
}    

IEnumerator SpawnEnemies()
{
    while (true)
    {
        yield return new WaitForSeconds(1.0f);
        SpawnEnemy();
    }
}

public void SpawnEnemy()
{        
    Vector3 enemySpawnPos = enemySpawnPoint.position;

    Instantiate(enemy, enemySpawnPos, Quaternion.identity);

}

public void SpawnPlayer()
{        
    Vector3 playerSpawnPos = playerSpawnPoint.position;

    Instantiate(player, playerSpawnPos, Quaternion.identity);

}

...and the character selection script:

public void Level1()
{
    SceneManager.LoadScene(1);
}

public void Level2()
{
    SceneManager.LoadScene(2);
} 

// ...

public void Level10()
{
    SceneManager.LoadScene(10);
}

Now the issue is, if I want to make any changes in one scene, like the skybox or background color, I will have to open all the other level scenes and make the changes.

Another thing is that if I want to add new levels, I will have to do the same thing, which seems like a lot of repetition.

Is there some way to set the enemy prefab, enemy spawn positions, player prefab and player spawn position once the character has been selected?

For example, if the user clicks on level1, all the preceding variables are set automatically and so on.....

\$\endgroup\$
2

1 Answer 1

0
\$\begingroup\$

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

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

\$\endgroup\$
1
  • \$\begingroup\$ thanks for the detailed explanation. I will follow this and use it to create the character selection menu..... \$\endgroup\$ Commented Apr 8, 2020 at 15:15

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .