To make such a list, you can introduce a struct
public struct WeightedItem<T> {
public float Weight { get; private set; }
public T Payload { get; private set; }
public WeightedItem(float weight, T payload) {
Weight = weight;
Payload = payload;
}
}
List<WeightedItem<GameObject>> list = new();
The algorithm looks like this (ripped out of my libraries)
public bool TryChoose(out int index) {
index = -1;
if(list is null || list.Count == 0)
return false;
var value = Random.value;
if(list.Count == 1 || value == 0f) {
index = 0;
} else if(value == 1f) {
index = list.Count - 1;
} else {
int i = 0;
while(true) {
var p = probabilityOf(list[i].Weight);
if(value < p) break;
value -= p;
i++;
}
index = i;
}
return true;
}
// a convenience that simply gets you the payload with no intermediate steps
public T Choose() {
if(!TryChoose(out var index)) throw new KeyNotFoundException();
return list[index].payload;
}
The main trick here is that probabilityOf function, which needs to cache the total sum of weights to be able not to waste time computing this sum over and over again.
It is essentially computing a probability from the supplied weight in such a way that it compares it to the total weight. Practically
float probabilityOf(float weight) {
var sum = 0f;
for(int i = 0; i < list.Count; i++) sum += list[i].Weight;
return weight / sum;
}
So ideally, when you make your list, you have also saved this sum.
Regarding sorting, I made my sorting work like bubble sort, because I maintain the sorted order on adding, but you can do a quick sort instead (which is significantly better, especially with larger item counts).
To do this, you first make a comparator like this (this code assumes T was already defined, in my code the whole thing is generic, but in this example we used GameObject)
static int descending_weight(WeightedItem<T> a, WeightedItem<T> b)
=> b.weight.CompareTo(a.weight);
Then, before you actually try to select anything, you do
list.Sort(descending_weight);