Randomly select a list

Hi, I’m really really new to C# and unity and to be honest I’m just diving right in and trying to learn as practically as possible. My lack of knowledge does mean however that I randomly encounter roadblocks such as this that are likely incredibly simply solved with just a small amount of understanding I don’t have. In other words, I’m almost certain there is an INCREDIBLY simple answer to this problem and the pulling-hair level of anger it’s been inducing in me is not worth it.

I’m working on a project with a heavy element of randomisation; to a certain extent, dialogue will be random, as will another similar feature I currently refer to as “keywords”. I want several keyword lists, grouped by theme (E.G. a list of medical keywords [“doctorate”, “mask”, “stethoscope”,“EMT”, “training”] but larger than this) and then, upon calling a routine I want one of the lists to be chosen at random, then a random handful of the keywords from that list to be chosen also. So far I’ve tried naming all the List objects that store the keywords under the same scheme, “keywordlist1”, “keywordlist2” etc, and then having a string that stores “keywordlist” + a Random.Range operation, and then using GetType().GetField().GetValue() to find the list with that name, but that broke in a very odd way. It would correctly form the name, but the GetType and so on would yield a smaller list with items from the wrong list.

Feel free to ask me for any better explanation you need, here is how the lists are currently being stored.

public List<string> keywordset1 = new List<string>() {"aaaaa","bbbbb", "ccccc","ddddd","eeeee","fffff","gggggg","hhhhh","iiiiii", "jjjjjj"};
public List<string> keywordset2 = new List<string>() {"kkkkkk","lllllll", "mmmmm","nnnnn","ooooo","ppppp","qqqqq","rrrrr","ssssss", "tttttt"};
public List<string> keywordset3 = new List<string>() {"uuuuuu","vvvvvv", "wwwwww","xxxxx","yyyyyy","zzzzzz","11111","222222","333333", "444444"};

The tl;dr question is: how do I pick one of these lists at random, then pick random words within them?

Thanks for any help I get, and please be patient - I’m a total newcomer! :]

Hey and welcome!

From what you described, you should be able to just store your keywordsets as a list of lists:

List<List<string>> keywordSets = new List<List<string>>();
keywordSets.Add(keywordSet1);
keywordSets.Add(keywordSet2);
keywordSets.Add(keywordSet3);

Now you can get a random number (setIndex) between 0 and (keywordSets.Count -1) and use that as the index of the keywordSet you want to chose from. Then calculate a random number (wordIndex) between 0 and (keywordSet[setIndex].Count -1) to chose the random word saved in keywordSet[setIndex][wordIndex].

int setIndex = Random.Range(0, keywordSets.Count - 1);
int wordIndex = Random.Range(0, keywordSets[setIndex].Count - 1);
string randomWord = keywordSets[setIndex][wordIndex];

Hope this helps!

Yes, I forgot to mention I’d thought about how it could be done with lists inside lists, but the reason I’d not tried that yet is for some reason I was really struggling to wrap my head around C#'s list syntax. I’ll try implementing this later on, but it seems like exactly what I’m looking for. Thank you!

What @Yoreki post will work for you. Basically put your lists into a list then use a random index to get a random element.

If you don’t want to get the same element twice, you could use a ‘shuffle’ too. Sometimes I find this is helpful.

using System;
using System.Collections.Generic;

namespace Util
{
    public static class ListUtils
    {
        private static Random rng = new Random(); 

        public static void Shuffle<T>(this IList<T> list) 
        { 
            int n = list.Count; 
            while (n > 1) { 
                n--; 
                int k = rng.Next(n + 1); 
                T value = list[k]; 
                list[k] = list[n]; 
                list[n] = value; 
            } 
        }
    }
}

to use it, you basically just can call myList.Shuffle(); and then it will re-arrange all the elements in that list.
Then if you iterate through, it gives you the elements in a shuffled order.

Will that guarantee no duplicates? Because I do need to have none

Can also use an array of lists: List<string>[ ] SomeLists;. But it can be done with just what you have:

List<int> randList = null;
int n=Random.Range(1,3+1);
if(n==1) randList=keywordList1;
else if(n==1) randList=keywordList2;
else randList=keywordList3;

string w = randList[0]; // or anything besides a 0

It works since lists and arrays are alway pointer-types. randList is aimed at one of your lists, in the exact same way list1,2, and 3 are aimed at “their” lists.

If you want to get no duplicates then just put your string in a HashSet (see: https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.hashset-1?view=netcore-3.1)

A set guarantees no duplicate.

If you wanted to use the shuffle with the set you could do something like

HashSet mySet;
List myList = mySet.ToList().Shuffle();

Then if you wanted to use it you can just iterate through the list… and it will be in a shuffled order.

foreach(string s in myList) {
// order will be shuffled
}

It’s not about the original list not having duplicates. It’s about the randomizer not choosing the same item twice.

Separate issue: shuffling the list shuffles the actual list. Often that break other things which use the list, or simply makes it hard to debug. To do it without messing up the starting list, can shuffle a list of every index: 0,1,2 … N-1, then use them as look-ups: randList[N*], where i goes from 0.*

Okay, the randomisation works I just have no idea what I’m doing with C# so it’s not behaving how I want it to. Here I have the script that stores the keyword lists and holds a routine that randomly selects a set, then 5 words from that set. Currently this will give duplicates but I’m not worrying about that for now.

using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using Random = UnityEngine.Random;
[System.Serializable]
public class KeywordOptions : MonoBehaviour
{
    private List<List<string>> keywordSets = new List<List<string>>();
    private List<string> keywordSet1 = new List<string>() {"aaaaa","bbbbb", "ccccc","ddddd","eeeee","fffff","gggggg","hhhhh","iiiiii", "jjjjjj"};
    private List<string> keywordSet2 = new List<string>() {"kkkkkk","lllllll", "mmmmm","nnnnn","ooooo","ppppp","qqqqq","rrrrr","ssssss", "tttttt"};
    private List<string> keywordSet3 = new List<string>() {"uuuuuu","vvvvvv", "wwwwww","xxxxx","yyyyyy","zzzzzz","11111","222222","333333", "444444"};
    public List<string> chosenKeywords = new List<string>();
    private int randomKeywordPosition;
    private int randomKeywordSetPosition;
    private string randomKeyword;
   
    public void Start()
    {
        keywordSets.Add(keywordSet1);
        keywordSets.Add(keywordSet2);
        keywordSets.Add(keywordSet3);
    }

    public void chooseKeywordSet()
   
    {
        randomKeywordSetPosition = Random.Range(0, keywordSets.Count - 1);
        Debug.Log(randomKeywordSetPosition);
        for (int i = 0; i < 5; i++)
        {
             randomKeywordPosition = Random.Range(0, keywordSets[randomKeywordSetPosition].Count - 1);
             randomKeyword = keywordSets[randomKeywordSetPosition][randomKeywordPosition];
             chosenKeywords.Add(randomKeyword);
         }
   
    }

}

I also have a keywords class:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[System.Serializable]
public class Keywords
{
   

    public List<string> keywords;

   
}

and then i have an “enemy” script which is how I’m trying to implement this:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Random = UnityEngine.Random;
using System.IO;
public class Enemy : MovingObject
{
    public KeywordOptions keywordOptions;
    public Keywords keywords;
    private List<string> enemyKeywords = new List<string>();

    void KeywordGeneration()
    {
        keywordOptions.chooseKeywordSet();
        keywords = new Keywords();
        foreach (string keyword in keywordOptions.chosenKeywords)
        {
            enemyKeywords.Add(keyword);
        }
        keywords.keywords = enemyKeywords;
        enemyKeywords.Clear();
        //keywords.keywordSet = keywordOptions.chosenKeywordSet;
    }
    protected override void Start()
    {
       GameObject.Find("EspionageManager").GetComponent<KeywordOptions>();
        KeywordGeneration();
        Debug.Log(this.name + "is ready!");

For brevity, I removed all of the unrelated code from the enemy script here.

What results from running this and having 2 enemies spawned is this: the first enemy to spawn has a keywords class attached to him populated with exactly what I want: 5 randomly selected keywords from 1 randomly selected set. The second enemy to spawn has the same 5 keywords, plus another randomly selected 5 from another randomly selected set, essentially as if it is building upon the original list of 5 each time it runs the script. Why is that? Doesn’t each enemy have its own instance of a keywords class? I have other code based on a youtube tutorial for dialogue, the only difference for which is that it uses string[ ] rather than List and doesn’t select words from within an array, rather it just selects a random array and that does not have this problem.

Evidently I think this is something to do with using chosenKeywords.Add(), but the reason I’m having such trouble is that I don’t really understand instancing or classes very well it seems. If anyone can point me in the right direction I’d much appreciate it.

This.

As you already assumed, the main problem here is that you save the chosenKeywords as a public attribute to the KeywordOptions class, and then simply access it in enemy. Since each time you call ChoseKeywordSet(), you simply add new words to chosenKeywords, the list only grows, which is not what you want. What you want is to get a fresh list of chosen keywords each time you call ChoseKeywordSet().
So to fix this you should simply make ChoseKeywordSet return its result, and not save it as a public attribute to the class. So you’ll want to move your current chosenKeywords definition into the method, do what you do now, and then return it after you are done:

public void ChooseKeywordSet()
    {
        List<string> chosenKeywords = new List<string>();
        randomKeywordSetPosition = Random.Range(0, keywordSets.Count - 1);
        Debug.Log(randomKeywordSetPosition);
        for (int i = 0; i < 5; i++)
        {
             randomKeywordPosition = Random.Range(0, keywordSets[randomKeywordSetPosition].Count - 1);
             randomKeyword = keywordSets[randomKeywordSetPosition][randomKeywordPosition];
             chosenKeywords.Add(randomKeyword);
         }
        return chosenKeywords;
    }

Now, each time the method is called, we create a new empty list and return it once we are done. To use this as you did before, simply keep track of the return value in enemy, like so:

enemyKeywords = keywordOptions.ChoseKeywordSet();

As you see, the foreach loop also becomes unnecessary. It technically always was, even tho in your implementation it actually had somewhat of a use. Had you assigned the reference, the first enemy too would have had 10 keywords as soon as the second one called ChoseKeywordSet() in your implementation, since it would refer to the then updated list. You (accidentally?) prevented this by copying the actual strings that were in the list. Anyways, this would be how to do it.

The above should fix your problem. As you mentioned yourself tho, you should probably look into objects and instances a bit more. On top of that, objects do not have to derive from Monobehaviour and do not have to be added as components to a GameObject. Since KeywordOptions is mostly a helper class, i would probably have made it static, or a singleton. Or each enemy could have its own instance of it. These things right now most likely dont tell you a lot, but after you’ve looked into object oriented programming a bit more, you will probably realise what i meant.
I can also only recommend you to look into the Gamedev tutorial series by Sebastian Lague on youtube. It’s about Unity and C#, starts at the very beginning, offers neat exercises and goes over everything you need to know to get started.