Need help with my scene randomizer button

I have a simple children’s flashcard app I’m working on and I have a “shuffle” button that shows random Letters from my pool of scenes. My problem is that it will sometimes repeat the same letter its already on i.e shows the “E” scene twice in a row. I have zero coding experience so I’m sure there is a simple solution to my problem, I just cant find it. This is the code I’m using on the button.

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

public class LoadRandomScene : MonoBehaviour{
   
    public int levelGenerate;
    public void LoadTheLevel()
    {
        levelGenerate = Random.Range(1, 27);
        SceneManager.LoadScene(levelGenerate);
    }

}

Chances are a simple singleton will be enough for what you want. Then you have a few choices depending on how you want to do things. If you want to only visit a letter once until all letters are used up, you could just have a list of all scene names and remove the one you land on. Or… you can do the other way which is generate a random number and then add to a list the scene name.

Then each time you go to generate a new number, see if that scene already exist in the list and then pick again if it does.

If you simply don’t want to repeat, then just track which one was picked last and if you hit it again, pick another random value.

You want to shuffle the array with something called a Fisher-Yates shuffling algorithm.
And yes you want either a singleton or a static class which will keep track of the list/array.

The easiest variant of implementing the shuffle itself is just

static public void Shuffle(int[] array) {
  for(int i = array.Length - 1; i >= 0; i--) {
    int r = Random.Range(0, array.Length);
    int t = array[i];
    array[i] = array[r];
    array[r] = t;
  }
}

Would this script replace my existing random scene script? I appreciate the help and will look into the Fisher-Yates shuffling algorithm and singleton. I’m kind of learning on the fly here, at least now I have some direction!

Basically you do the following

public class SceneLoader : MonoBehaviour {

  static int _currentLevel;
  static string[] _levels;

  static public void LoadNextLevel() {
    if(_levels == null) { // lazy initialization of names
      generateLevelNames();
      shuffleArray(_levels);
    }

    if(_currentLevel == _levels.Length - 1) Debug.LogError("No more levels");
      else SceneManager.LoadScene(_levels[++_currentLevel]); // always increments by 1
  }

  void Awake() {
    _currentLevel = -1; // will be increased to 0 at first call of the above method
    Object.DontDestroyOnLoad(this); // makes sure this object is not destroyed on scene switch
  }

  void OnDestroy() => _levels = null; // makes sure the names don't linger around

  static private void generateLevelNames() {
    _levels = new string[26];
    for(int i = 0; i < _levels.Length; i++) {
      char letter = (char)('A' + i);
      _levels[i] = $"Scene_{letter}"; // Scene_A, Scene_B ...
    }
  }

  static private void shuffleArray<T>(T[] array) {
    for(int i = array.Length - 1; i >= 0; i--) {
      int r = Random.Range(0, array.Length);
      var t = array[i]; array[i] = array[r]; array[r] = t; // just a swap
    }
  }

}

In this example, your scenes are supposed to be called Scene_A, Scene_B, and so on to Scene_Z.

You are supposed to call SceneLoader.LoadNextLevel() from wherever, each time a scene is finished, and once in the very beginning. Of course this can be expanded further.

I did this from my head, tell me if something doesn’t work, there could be a typo or sth.

edit:
Added DontDestroyOnLoad, you want this to be persistent until you remove it yourself
edit2:
Tried to simplify the code as much as I could

So I copied the script you wrote, there aren’t any errors. I attached the script to a gameobject which I then placed on the OnClick() of my shuffle button. I also changed the names of all my scenes to match your script. When I click on the shuffle button in the main title screen, it takes me to a random letter scene but after that the shuffle button only plays the same scene over again. Basically it only loads the first scene that was loaded, after that it just repeats. I apologize for my inexperience and I really appreciate your help so far.

That’s strange. When you say ‘it just repeats’ do you mean after you call LoadNextLevel?

It repeats after I click on my shuffle button. For more context, I have 27 total scenes, 1 Main title screen and 26 Letter scenes. I have the same shuffle button on all the scenes. I click on the shuffle button from the Main title screen and it takes me to a random letter scene as intended, but once I’m on that letter scene, I click on the shuffle button again and it just loads that same letter scene, without shuffling to a new random letter scene.

Well quite possibly the Awake method gets called again after you load a new scene.
Let’s change this behavior so it doesn’t do that.

public class SceneLoader : MonoBehaviour {

  static int _currentLevel;
  static string[] _levels;

  static public void LoadNextLevel() {
    if(_levels == null) { // lazy initialization of names
      _currentLevel = -1;
      Object.DontDestroyOnLoad(this);
      generateLevelNames();
      shuffleArray(_levels);
    }

    if(_currentLevel == _levels.Length - 1) Debug.LogError("No more levels");
      else SceneManager.LoadScene(_levels[++_currentLevel]); // always increments by 1
  }

  void OnDestroy() => _levels = null; // makes sure the names don't linger around

  static private void generateLevelNames() {
    _levels = new string[26];
    for(int i = 0; i < _levels.Length; i++) {
      char letter = (char)('A' + i);
      _levels[i] = $"Scene_{letter}"; // Scene_A, Scene_B ...
    }
  }

  static private void shuffleArray<T>(T[] array) {
    for(int i = array.Length - 1; i >= 0; i--) {
      int r = Random.Range(0, array.Length);
      var t = array[i]; array[i] = array[r]; array[r] = t; // just a swap
    }
  }

}

One error, it says that “this” is not valid in a static property, in the line Object.DontDestroyOnLoad(this)

This is btw, slightly different than the proposed singleton pattern. But in your case I don’t think you really need one.

If you care about what the differences are, well singletons are better suited to Unity because static information tends to linger for longer, for example when you exit play model, which is usually undesirable. Here we make sure we clear the relevant fields on destroy, HOWEVER, if you don’t, the whole thing might get stuck in an invalid state (which is easily solved by removing the script and adding it again).

The other reasons are mostly from object oriented standpoint, but that kind of reasoning is moot for the most part, unless your project is really big and/or has many moving parts and/or several collaborators.

Let’s fix this issue with the above code so that you can reset the internal state manually. You just add this

void Reset() => _levels = null;

You can now select Reset in the top-right corner in the inspector.
Let’s also add a little something so that we can see what the next level will be.

Create a new folder and call it Editor
in it place a new script called SceneLoaderEditor

using UnityEngine;
using UnityEditor;

[CustomEditor(typeof(SceneLoader))]
public class SceneLoaderEditor : UnityEditor.Editor {

  public override OnInspectorGUI() {
    base.OnInspectorGUI();
    var nextLevelName = "<Uninitialized>";
    if(SceneLoader._levels is not null) {
      var next = SceneLoader._currentLevel + 1;
      if(next >= 0 && next < SceneLoader._levels) {
        nextLevelName = SceneLoader._levels[next];
      } else {
        nextLevelName = "<Out of range>";
      }
    }
    var enbl = GUI.enabled;
    GUI.enabled = false;
    EditorGUILayout.StringField($"Next Level", nextLevelName);
    GUI.enabled = enbl;
  }

}

Now you should be able to inspect the internal state of this script.
Edit: Don’t try this ^^ yet, I should really run this first, to weed out the errors. I.e. quite possibly you should make the original _levels and _currentLevel variables public, can’t work this out from my head.
Edit2: Made a silly mistake, fields are static anyway, and yes they should be declared public. I’ll make a better version of this in the following posts.

Ah yes, sure. That’s why I did that in Awake.

ok bring back Awake like this

void Awake() => Object.DontDestroyOnLoad(this);

and remove this line from LoadNextLevel

So I’ve made all the changes you suggested and It’s working much better now, my only issue now is that when its gets to Scene_Z, the button stops working and no longer loads new scenes. Also after shuffling through what I assume is all 26 scenes, it runs out of levels when I would like it to keep shuffling again. When I go back to the home screen and try to use the shuffle button again it still has no levels. I think it might be time for me to take some C# classes haha I really appreciate all the help you’ve given with this though, seriously. I have to leave for work now, but will keep trying stuff when I get back. Thank you Thank You

Sure, I’ve never implemented the support for anything more than the basics.
But it’s not that hard to do so.

public class SceneLoader : MonoBehaviour {

  static int _currentLevel;
  static string[] _levels;

  static public void LoadNextLevel() {
    if(_levels == null) { // lazy initialization of names
      _currentLevel = -1;
      generateLevelNames();
      shuffleArray(_levels);

    } else if(_currentLevel >= _levels.Length) {
      _currentLevel = -1;
      shuffleArray(_levels);

    }

    SceneManager.LoadScene(_levels[++_currentLevel]);
  }

  void Awake() => Object.DontDestroyOnLoad(this);
  void OnDestroy() => Reset();
  void Reset() => _levels = null;

  static private void generateLevelNames() {
    _levels = new string[26];
    for(int i = 0; i < _levels.Length; i++) {
      char letter = (char)('A' + i);
      _levels[i] = $"Scene_{letter}"; // Scene_A, Scene_B ...
    }
  }

  static private void shuffleArray<T>(T[] array) {
    for(int i = array.Length - 1; i >= 0; i--) {
      int r = Random.Range(0, array.Length);
      var t = array[i]; array[i] = array[r]; array[r] = t;
    }
  }

}

Let’s reintroduce that editor script properly
First lets abandon lazy initialization in the main class

public class SceneLoader : MonoBehaviour {

  static int _currentLevel;
  static string[] _levels;

  static public void LoadNextLevel() {
    if(_currentLevel >= _levels.Length)
      reshuffleAndRestart();

    SceneManager.LoadScene(_levels[++_currentLevel]);
  }

  void Awake() {
    initialize();
    Object.DontDestroyOnLoad(this);
  }

  void OnDestroy() => Reset();
  void Reset() => _levels = null;

  static private void initialize() {
    if(_levels == null) {
      generateLevelNames();
      reshuffleAndRestart();
    }
  }

  public string GetNextLevelName()
    => _levels != null &&
         _currentLevel >= -1 && _currentLevel < _levels.Length - 1
             ? _levels[_currentLevel + 1] : null;

  static private reshuffleAndRestart() {
    _currentLevel = -1;
    shuffleArray(_levels);
  }

  static private void generateLevelNames() {
    _levels = new string[26];
    for(int i = 0; i < _levels.Length; i++) {
      char letter = (char)('A' + i);
      _levels[i] = $"Scene_{letter}"; // Scene_A, Scene_B ...
    }
  }

  static private void shuffleArray<T>(T[] array) {
    for(int i = array.Length - 1; i >= 0; i--) {
      int r = Random.Range(0, array.Length);
      var t = array[i]; array[i] = array[r]; array[r] = t;
    }
  }

}

then put this in the Editor folder

using UnityEngine;
using UnityEditor;

[CustomEditor(typeof(SceneLoader))]
public class SceneLoaderEditor : UnityEditor.Editor {

  public override OnInspectorGUI() {
    base.OnInspectorGUI();
    var target = this.target as SceneLoader;
    var nextLevelName = target.GetNextLevelName()?? "<ERROR>";
    var enbl = GUI.enabled;
    GUI.enabled = false;
    EditorGUILayout.StringField($"Next Level", nextLevelName);
    GUI.enabled = enbl;
  }

}

this should work now

You can also make it so that it wraps around, but then you need to make sure you can reshuffle and restart on demand.

But at this point we can also introduce a singleton pattern.
I.e.

public class SceneLoader : MonoBehaviour {

  static SceneLoader _instance;
  static public SceneLoader GetInstance() => _instance;

  int _currentLevel;
  string[] _levels;

  public void LoadNextLevel() {
    if(_currentLevel >= _levels.Length)
      ReshuffleAndRestart();

    _currentLevel = advanceAndWrapAround(_currentLevel, _levels.Length);
    SceneManager.LoadScene(_levels[_currentLevel]);
  }

  private int advanceAndWrapAround(int index, int count) {
    if(++index == count) index = 0;
    return index;
  }

  void Awake() {
    initialize();
    Object.DontDestroyOnLoad(this);
  }

  void OnDestroy() {
    _levels = null;
    _instance = null;
  }

  void Reset() {
    _levels = null;
    initialize();
  }

  private void initialize() {
    if(_levels == null) {
      _instance = this;
      generateLevelNames();
      ReshuffleAndRestart();
    }
  }

  public string GetNextLevelName()
    => _levels != null &&
          _currentLevel >= -1 && _currentLevel < _levels.Length - 1
             ? _levels[_currentLevel + 1] : null;

  public void ReshuffleAndRestart() { // this can now be called externally
    _currentLevel = -1;
    shuffleArray(_levels);
  }

  private void generateLevelNames() {
    _levels = new string[26];
    for(int i = 0; i < _levels.Length; i++) {
      char letter = (char)('A' + i);
      _levels[i] = $"Scene_{letter}"; // Scene_A, Scene_B ...
    }
  }

  private void shuffleArray<T>(T[] array) {
    for(int i = array.Length - 1; i >= 0; i--) {
      int r = Random.Range(0, array.Length);
      var t = array[i]; array[i] = array[r]; array[r] = t;
    }
  }

}

In this setup, if you want to access public methods, you need to call them like this

SceneLoader.GetInstance().LoadNextLevel();

But you can do this from anywhere in your code (this was the case before as well, but now the code itself is a tiny bit better and easier to handle from dev perspective).

Now we can add a button to our inspector code

using UnityEngine;
using UnityEditor;

[CustomEditor(typeof(SceneLoader))]
public class SceneLoaderEditor : UnityEditor.Editor {

  public override OnInspectorGUI() {
    base.OnInspectorGUI();
    var target = this.target as SceneLoader;
    var nextLevelName = target.GetNextLevelName()?? "<ERROR>";
    var enbl = GUI.enabled;
    GUI.enabled = false;
    EditorGUILayout.StringField("Next Level", nextLevelName);
    GUI.enabled = enbl;
    if(GUILayout.Button("Reshuffle")) target.ReshuffleAndRestart();
  }

}

I’ve finally been able to implement all your suggestions and from what I can tell everything seems to be working perfectly now. Scene_Z still wasnt loading the next scene but after a little more investigation I realized that I never attached the script to the shuffle button on Scene_Z, whoops. Now I can upload this build to test flight and get it out to some testers. Thank you so much for all your help, seriously, you are a life saver my friend.

1 Like