How to use a List in a Function(Advanced)

101 Views Asked by At

I am using static lists of classes, and am trying to simplify or genericize the functions for ease of typing and copy/pasting. I'll try to explain the best I can:

public static List<Resources> allResources = new List<Resources>();
public static List<Resources> allIron = new List<Resources>(); // Iron.cs is child
public static List<Resources> allCoal = new List<Resources>(); // Coal.cs is child

public Iron GetIron(Vector3 position)
    {
        if (allIron.Count > 0)
        {
            for (int i = 0; i < allIron.Count; i++)
            {
                if (!allIron[i].gameObject.activeSelf)
                {
                    allIron[i].trans.position = position;
                    allIron[i].gameObject.SetActive(true);
                    return (Iron)allIron[i];
                }
            }
        }
        Instantiate(prefabIron, position, Quaternion.identity, oreHolder.transform);
        return (Iron)allIron[allIron.Count - 1];
    }

So using this way of Object pooling, in order to have a function that does this, I would have to copy paste this function for each new resource, and change each call to the particular list. What I would like to do, but can't seem to manage, is this:

public Resource GetResource(List<Resource> resources, Vector3 position, GameObject prefab)
    {
        if (resources.Count > 0)
        {
            for (int i = 0; i < resources.Count; i++)
            {
                if (!resources[i].gameObject.activeSelf)
                {
                    resources[i].trans.position = position;
                    resources[i].gameObject.SetActive(true);
                    return resources[i];
                }
            }
        }
        Instantiate(prefab, position, Quaternion.identity, oreHolder.transform);
        return resources[resources.Count - 1];
    }

But when trying to simplify a function like this(way less typing), it doesn't work. Passing a List into a function basically just copies it to be used within the function, and doesn't change the actual Lists entities, even though being static.

So my question is, is this at all possible? Or am I stuck with making a function for each resource, and living in copy/paste hell? Or even better, would there be a way to just make one function that can do it all? All my attempts have failed, any help or insight would be greatly appreciated, cheers!

  • I would like to note, that I could just for-loop through "allResources" since that list contains all of the child classes. However knowing that each new "Get" call is iterating through possibly (100,000 to infinity) would grind my gears just knowing the code is doing that in the background. This is why I made particular lists of each resource, that would cut the iterations down to a measly 10,000, when just looking for a particular item in game.

  • My question actually answered my question. My answer is the last post(website won't allow me to accept my own answer for 3 days). Special thanks to all who replied, you guys are awesome!

3

There are 3 best solutions below

4
WideEyeNow On BEST ANSWER

Not sure how it didn't work the first time, but the second snippet was correct, and the code works beautifully. Here is a better explanation that you can test for yourself:

// Main.cs
public static List<Resources> allResources = new List<Resources>();
public static List<Resources> allIron = new List<Resources>(); // Iron.cs is child
public static List<Resources> allCoal = new List<Resources>(); // Coal.cs is child

public Resources GetResource(List<Resources> resources, Vector3 position, GameObject prefab)
    {
        if (resources.Count > 0)
        {
            for (int i = 0; i < resources.Count; i++)
            {
                if (!resources[i].gameObject.activeSelf)
                {
                    resources[i].trans.position = position;
                    resources[i].gameObject.SetActive(true);
                    return resources[i];
                }
            }
        }
        Instantiate(prefab, position, Quaternion.identity, oreHolder.transform);
        return resources[resources.Count - 1];
    }

So by having static List's of classes, and also having the "Get" call within same script, somehow the List is able to be passed without "copying" itself to be used within the function. So no special calls are needed. And during Instantiate() the Awake() method of Iron.cs is called within the same frame, so as you see Iron.cs adds itself to the List being called , then the next line of code gets it's index and returns the correct script.

// Resources.cs is child of Main.cs

// Iron.cs (child of Resources.cs)

public float amount;

void Awake()
{
    allIron.Add(this);
}

^ as you can see, Iron.cs adds itself to the static List(instantly).

Then a simple call of GetResource() is used where needed. And since each script is a child of the previous, leading back to Main.cs, no other "Get" calls are needed. Especially not GetComponent() which is a performance killer, or worse yet a serious performance killer of an interface(IGetResource.cs).

// Drill.cs (also child of Main.cs)

    if (isPowered)
    {
        timer++;

        if (timer > tick)
        {
            Iron iron = (Iron)GetResource(allIron, pos, prefabIron);

            iron.amount = 55.5f;

            inventory.Add(iron);

            timer = 0;
        }

    }

But as I said please test for yourself, and better yet use a benchmark test(min 100k objects). You will clearly see that this is the fastest and most performant way to do this :)

3
Guru Stron On

Looks like you should consider using generic method constrained to T : Resources.

There are multiple approaches to this, one could be using Dictionary<Type, List<Resources>> - something along this lines:

public static Dictionary<Type, List<Resource>> ByType = new ();
    
public T Get<T>(Vector3 position) where T : Resource
{
    if (ByType.TryGetValue(typeof(T), out List<Resource> ts) && ts.Any())
    {
        for (int i = 0; i < ts.Count; i++)
        {
            if (!ts[i].gameObject.activeSelf)
            {
                ts[i].trans.position = position;
                ts[i].gameObject.SetActive(true);
                return (T)ts[i];
            }
        }
    }
    // TODO: get prefab of corresponding type, 
    // similar approach can be used - create map from type to prefab for supported types:
    var prefab = ...;
    // make Instantiate generic too? or calculate the type from prefab
    Instantiate<T>(prefab, position, Quaternion.identity, oreHolder.transform); 
    ts = ByType[typeof(T)];
    return (T)ts[ts.Count - 1];
}

Notes:

  • I'm not very familiar with concurrency model of Unity and this code is very unsafe in multithreaded environment. Possibly use corresponding collections from System.Collections.Concurrent
  • Possibly precreate the dictionary(s) for all supported types
  • Also you can make this implementation internal and expose only typed ones like GetIron which will just do Get<Iron> (possibly can pass needed prefab too, also in this case you can keep approach with separate collections and pass them into the Get i.e. GetIron() => Get(allIrons, ironPrefab))
  • Also you can look for ideas in DefaultObjectPool implemented for .NET
5
John Alexiou On

I would keep one list, and use .OfType<>() extension to filter for specific classes.

For example

static class Program
{
    static List<Resource> resources = new List<Resource>();

    static void Main(string[] args)
    {
        List<Coal> coal = resources.OfType<Coal>().ToList();
    }
}