Is there a way to replace names in a class structure code? (idk the right terms to use here!)

for (int i = 0; i < master.data[d].floats.Count; i++){
     master.data [d].items [itemID].floats.Add (new Floats (master.data [d].floats[i].name));
        }

I have the code above, and a similar bit of code for “ints/Ints”, “bools/Bools” and more types.

It’s very annoying having to add another set of code for each type. I’d like to be able to have a class of “Types”, with things like “floats”, “Floats” etc, and then replace the code to be something like this…

for (int t = 0; t < type.Count; t++){
     for (int i = 0; i < master.data[d].{type[t].smallName}.Count; i++){
          master.data [d].items [itemID].{type[t].smallName}.Add (new {type[t].bigName} (master.data [d].{type[t].smallName}[i].name));
     }
}

Is there a way to do that?
THANKS!!!

Sounds like a job for Generics! Generic classes and methods - C# | Microsoft Learn

Assuming the class with the .floats and .ints is called MasterData, you could do something like this:

public void DoTheTypes<Tsmall, Tbig>(Func<MasterData, List<Tsmall>> listGetter) {
  for (int i = 0; i < listGetter(master.data[d]).Count; i++){
      listGetter(master.data [d].items [itemID]).Add (new Tbig (listGetter(master.data [d])[i].name));
  }
}

public void DoAllTheTypes() {
  DoTheTypes<int, Int>(m => m.ints);
  DoTheTypes<float, Float>(m => m.floats);
}

For added fun, you could create a list of the matching smallType, BigType, getter, and then loop through that in DoAllTheTypes instead of manually.

PS: I love your monster packs! :wink:

1 Like

Thanks! I’m confused :slight_smile: I had looked into Generics for this twice now, but both times it just didn’t seem to make sense, because I had to declare the type before I wanted to…something like that. I was learning about it using the Learn video: https://unity3d.com/learn/tutorials/topics/scripting/generics

For the first line, what does the “Func” part the “listgetter” part do?

In this code, the GenericTest method should work (it probably has issues still) for the generic type T. However, I’m still unclear about how to replace the “smallName” (“dataFloats” in the example) with the passed string smallName.

I’m guessing maybe the “listgetter” part from the example above is what I’m looking for, but i"m not really sure how.

public void GenericTest<T>(string smallName){
        for (int a = 0; a < master.data.Count; a++) {
            for (int t = 0; t < master.data [a].items.Count; t++) {
                for (int v = 0; v < master.data [a].items [t].dataFloats.Count; v++) {                
                    Debug.Log ("dataName is " + master.data [a].items [t].dataFloats [v].dataName);
                    DataFloats newTestList = new T (master.data [d].floats [i].name);
                }
            }
        }
    }

    public void DoGenericTest(){
        GenericTest<DataFloats> ("dataFloats");
        GenericTest<Floats> ("floats");
        GenericTest<Ints> ("ints");
        GenericTest<GameObjects> ("gameObjects");
    }

Yeah, generics can be complicated to figure out until you’ve used them for a while. Func<> is a way to pass a function as an argument to another function. It’s like Action<> if you’ve seen that, except Func<> can have a return value and Action<> can’t. Func<int, string> means a function that takes an int and returns a string. Like “public string SomeFunction(int num)”. Func<MasterData, T> would be a function that takes a MasterData and returns T. T in my code gets inferred from the DoTheTypes() call in my example. Instead of passing the name of the field “dataFloats” you’re passing a function that just returns that actual field. I don’t know if MasterData is the name of the class, it should be whatever class master.data[a].items[t] is. This should make your code work:

public void GenericTest<T>(Func<MasterData, T> getter){
        for (int a = 0; a < master.data.Count; a++) {
            for (int t = 0; t < master.data [a].items.Count; t++) {
                for (int v = 0; v < getter(master.data [a].items [t]).Count; v++) {                
                    Debug.Log ("dataName is " + getter(master.data [a].items [t])[v].dataName);
                    DataFloats newTestList = new T (master.data [d].floats [i].name); //note: here you call "floats" which is different than "dataFloats"? Is that a typo or on purpose?
                }
            }
        }
    }

    public void DoGenericTest(){
        GenericTest<DataFloats> (m => m.dataFloats);
        GenericTest<Floats> (m => m.floats);
        GenericTest<Ints> m => m.ints);
        GenericTest<GameObjects> (m => m.gameObjects);
    }

If you haven’t used lambdas before (the “m => m.something”), it is a way of creating a small temporary function; in this case “m => m.dataFloats” is basically a short form of this:

private DataFloats MyFunction(MasterData m) { 
  return m.dataFloats; 
}

Lambdas: Lambda expressions - Lambda expressions and anonymous functions - C# reference | Microsoft Learn

1 Like

Thanks – I haven’t used Lambdas before (didn’t even know what they were called!) and I’m new to the way of passing the function as in Func<>. I’ll look into this and see if I can figure it out :smile:

Iv’e got to head out now and stop for the moment. Master is a class, as is Data.

Items is also a class, and DataFloats, Floats, GameObjects etc are all classes. Each one has a string dataName.

// GenericTest, the argument is passing Items and returning a Type? 
    public void GenericTest<T>(Func<Items, T> getter){
        for (int a = 0; a < master.data.Count; a++) {
            for (int i = 0; i < master.data [a].items.Count; i++) {
                // So "getter(master.data [a].items [t])" is passing items[t] and expecting a type as return?  Or...idk.
                for (int v = 0; v < getter(master.data [a].items [i]).Count; v++) {               
                    Debug.Log ("dataName is " + getter(master.data [a].items [i])[v].dataName);



                    // DataFloats newTestList = new T (master.data [d].dataFloats [i].name);  // Was a typo I think
                }
            }
        }
    }

    // If I'm reading this correctly, the Lambda expression is saying "If given i, return i.dataFloats", where i will be the Items class?
    public void DoGenericTest(){
        GenericTest<DataFloats> (i => i.dataFloats);
        GenericTest<Floats> (i => i.floats);
        GenericTest<Ints> (i => i.ints);
        GenericTest<GameObjects> (i => i.gameObjects);
    }

I’ve changed the getter function to Items, T. Am I correct that it’s supposed to return something of type T if given something of type Items? And it would be based on the lambda from the DoGenericTest function, so if given an object of type Items, it’ll return itemsObjectGiven.dataFloats (which itself is a list of DataFloats)?

It’s slowly coming together in my brain.

I’m at a loss, I think. I’m not sure how to go about this – I’m trying right now to take it all back, simplify it so I can figure out how to work these new concepts and build from there, but I must be missing something.

This is as close as I’ve gotten. I can see in the console correct information:

Archer | v = 0 | System.Collections.Generic.List1[DataFloats] | DataFloats`

It shows that getter(master.data [a].items [t]) is a List of dataName, and that getter(master.data [a].items [t])[v].dataName)[v] is DataFloats

However, the commented out line will throw a console error, that T doesn’t contain a definition for dataName.

    public void GenericTest<T>(Func<Items, List<T>> getter){
        for (int a = 0; a < master.data.Count; a++) {
            for (int t = 0; t < master.data [a].items.Count; t++) {
                for (int v = 0; v < getter(master.data [a].items [t]).Count; v++) {    
                    Debug.Log (master.data [a].items [t].name + " | v = " + v + " | " + getter (master.data [a].items [t]) + " | " + getter (master.data [a].items [t]) [v]);
                    //Debug.Log ("dataName is " + getter(master.data [a].items [t])[v].dataName);
                }
            }
        }
    }

    public void DoGenericTest(){
        GenericTest<DataFloats> (m => m.dataFloats);
        //GenericTest<Floats> (m => m.floats);
        //GenericTest<Ints> (m => m.ints);
        //GenericTest<GameObjects> (m => m.gameObjects);
    }

It seems that since T isn’t a known type, the system doesn’t think it have a definition for dataName. Is there any way to bypass this?

Huh – I’ve stumbled upon a better way of googling this question, and then a potential solution. Javascript has an “Eval()” function, which will run code. Since it’s an editor thing, it should be a good solution, I’d think. I need to test it, but if it works, then I should be able to do exactly what I want to do. I hope!

You can put constraints on T. This code will only allow GenericTest on a type which inherits from SomeBaseType, which has dataName. Since the compiler now knows that that is the case, it should allow you to use .dataName.

public class SomeBaseType {
public string dataName;
}
public void GenericTest<T>(T thing) where T : SomeBaseType {
Debug.Log(thing.dataName);
}
1 Like

Yeah, what StarManta said. If all of the things are subclasses of DataClass or whatever it’s called, then you can do:

public void GenericTest<T>(Func<Items, List<T>> getter) where T : DataClass {
1 Like

That works, but only if I specify the class. @makeshiftwings you mention “Subclasses”–

Master is a scriptable object.
Data is a Class
Items is a class
DataFloats, DataInts, Floats, Ints, etc are all classes

So none of them inherit from each other – I think that’s the right term? Is it better for them to inherit from each other, aka is that what you mean by subclass?

Yeah, if they all have a member called “dataName” then it would make sense for them to all inherit from the same base class. Or you could have them all implement the same interface, and have the interface include dataName.

1 Like

I don’t think generics is the right way to approach this particular problem. If you had time to completely rework the design of your system, maybe creating a common interface would allow generics to work for you. However, it’s likely you have constraints that preclude this option such as not owning the source code for all the types.

To answer your original question directly, Reflection is the name of the technology you need in C#. I must warn you however, Reflection is usually a red flag that your design is not ideal for the task at hand. It can be confusing to read and write and almost always exhibits slower execution than an equivalent solution done with, say, interfaces or generics.

In this case it seems you are working against the type system. In my experience this often happens when you are allowing end users to manipulate values using an editor that doesn’t differentiate between types the same way c# does. If you explain more about your use case and design, someone may have ideas on how to avoid this issue in the future.

1 Like

+1 to this. In addition, using Reflection is less reliable - a single typo can ruin things and give you runtime errors (as opposed to compile errors, which are much easier to detect and fix), and it doesn’t run on all platforms (though you should be fine in editor code.)

1 Like

I’m basically building a system that lets me populate my game class structure. At first I just was building a class structure, but then I realized I could do better, make it more versatile, and now what was a “class” before is now what I’m calling a “Category”. Instead of a Class for “Stats” and one for “Races” and one for “CharacterClasses” etc, I have a list of entries in a class called “Data”.

Data[0].name may equal “Stats”, for instance.

And Data has a list called “items” of the class Items. Data & Items both have Lists for various types of information – a class called “Floats” just holds a string name and a float value. This way a data[0].items[0].floats[0].name = “Default Stat Value”.

idk if this is making sense. The List for Floats (And other data holding classes, including ones that hold a list of values instead of a single value), is set in the Data class. This way all Items will have the same set of data lists.

For instance, the single float value “Default Stat Value”, is something that all Items in the Stats (Data class) will require.

I go a bit further though, and allow one Category to reference another. So a Data entry for “Classes” will include a reference to the Data entry “Stats”, but in float value . That’s the Class “DataFloats”, which has a name, and “dataName” (name of the referenced category), and a List of float values. I may call this “Stat Proficiencies”, and now when I edit each race, I can set their default stat proficiency – a value I’ll use later in the actual logic of the game.

The benefit here is that I can add and delete Stats and they’ll be automatically added to all other lists that reference the Stats data list.

Does that make sense?

The problem is that there are a lot of types – floats, ints, bools, strings, Texture2ds, audioClips, gameObjects etc. For each one, I need a single class and a “Data” class. Like “Floats” and “DataFloats”. (Although I think I may be able to combine those into one class…I may try to do that as I need to re-strcuture this anyway, to make it work with the advice given so far).

Whenever I want to code any operation, such as AddItemToDataCategory, the method ends up being very long, as it needs redundant loops for each data type.

public void AddItemToDataCategory(string name, int d){
        // Check name first
        for (int i = 0; i < master.data [d].items.Count; i++) {                                        // For each current item...
            if (master.data [d].items[i].name == master.data[d].newItemName) {                        // if the names match
                // Leave an error in the console
                Debug.LogError("Error:  The name " + master.data[d].newItemName + " has already been assigned to an Item in this Category.");
                return;                                                                                // break the function
            }
        }
        // Add the new item
        master.data[d].items.Add (new Items (master.data[d].newItemName));
        int itemID = master.data[d].items.Count - 1;
        // Now add custom variables
        for (int i = 0; i < master.data[d].floats.Count; i++){
            master.data [d].items [itemID].floats.Add (new Floats (master.data [d].floats[i].name));
        }
        for (int i = 0; i < master.data[d].ints.Count; i++){
            master.data [d].items [itemID].ints.Add (new Ints (master.data [d].ints[i].name));
        }
        for (int i = 0; i < master.data[d].strings.Count; i++){
            master.data [d].items [itemID].strings.Add (new Strings (master.data [d].strings[i].name));
        }
        for (int i = 0; i < master.data[d].bools.Count; i++){
            master.data [d].items [itemID].bools.Add (new Bools (master.data [d].bools[i].name));
        }
        for (int i = 0; i < master.data[d].gameObjects.Count; i++){
            master.data [d].items [itemID].gameObjects.Add (new GameObjects (master.data [d].gameObjects[i].name));
        }
        for (int i = 0; i < master.data[d].texture2Ds.Count; i++){
            master.data [d].items [itemID].texture2Ds.Add (new Texture2Ds (master.data [d].texture2Ds[i].name));
        }
        for (int i = 0; i < master.data[d].audioClips.Count; i++){
            master.data [d].items [itemID].audioClips.Add (new AudioClips (master.data [d].audioClips[i].name));
        }
        for (int i = 0; i < master.data[d].dataSingles.Count; i++){
            master.data [d].items [itemID].dataSingles.Add (new DataSingle (master.data [d].dataSingles[i].name, master.data [d].dataSingles[i].dataName));
        }
        for (int i = 0; i < master.data[d].dataFloats.Count; i++){
            master.data [d].items [itemID].dataFloats.Add (new DataFloats (master.data [d].dataFloats[i].name, master.data [d].dataFloats[i].dataName));
            for (int v = 0; v < master.data [d].dataFloats [i].value.Count; v++) {
                master.data [d].items [itemID].dataFloats [master.data [d].items [itemID].dataFloats.Count - 1].value.Add (0.0f);
            }
        }
        for (int i = 0; i < master.data[d].dataInts.Count; i++){
            master.data [d].items [itemID].dataInts.Add (new DataInts (master.data [d].dataInts[i].name, master.data [d].dataInts[i].dataName));
            for (int v = 0; v < master.data [d].dataInts [i].value.Count; v++) {
                master.data [d].items [itemID].dataInts [master.data [d].items [itemID].dataInts.Count - 1].value.Add (0);
            }
        }
        for (int i = 0; i < master.data[d].dataBools.Count; i++){
            master.data [d].items [itemID].dataBools.Add (new DataBools (master.data [d].dataBools[i].name, master.data [d].dataBools[i].dataName));
            for (int v = 0; v < master.data [d].dataBools [i].value.Count; v++) {
                master.data [d].items [itemID].dataBools [master.data [d].items [itemID].dataBools.Count - 1].value.Add (false);
            }
        }
        for (int i = 0; i < master.data[d].dataStrings.Count; i++){
            master.data [d].items [itemID].dataStrings.Add (new DataStrings (master.data [d].dataStrings[i].name, master.data [d].dataStrings[i].dataName));
            for (int v = 0; v < master.data [d].dataStrings [i].value.Count; v++) {
                master.data [d].items [itemID].dataStrings [master.data [d].items [itemID].dataStrings.Count - 1].value.Add ("");
            }
        }
        for (int i = 0; i < master.data[d].dataGameObjects.Count; i++){
            master.data [d].items [itemID].dataGameObjects.Add (new DataGameObjects (master.data [d].dataGameObjects[i].name, master.data [d].dataGameObjects[i].dataName));
            for (int v = 0; v < master.data [d].dataGameObjects [i].value.Count; v++) {
                master.data [d].items [itemID].dataGameObjects [master.data [d].items [itemID].dataGameObjects.Count - 1].value.Add (null);
            }
        }
        for (int i = 0; i < master.data[d].dataTexture2Ds.Count; i++){
            master.data [d].items [itemID].dataTexture2Ds.Add (new DataTexture2Ds (master.data [d].dataTexture2Ds[i].name, master.data [d].dataTexture2Ds[i].dataName));
            for (int v = 0; v < master.data [d].dataTexture2Ds [i].value.Count; v++) {
                master.data [d].items [itemID].dataTexture2Ds [master.data [d].items [itemID].dataTexture2Ds.Count - 1].value.Add (null);
            }
        }
        for (int i = 0; i < master.data[d].dataAudioClips.Count; i++){
            master.data [d].items [itemID].dataAudioClips.Add (new DataAudioClips (master.data [d].dataAudioClips[i].name, master.data [d].dataAudioClips[i].dataName));
            for (int v = 0; v < master.data [d].dataAudioClips [i].value.Count; v++) {
                master.data [d].items [itemID].dataAudioClips [master.data [d].items [itemID].dataAudioClips.Count - 1].value.Add (null);
            }
        }

        // Now we need to add new items for every custom variable already referencing this category

        for (int a = 0; a < master.data.Count; a++) {                                                // For each data category
            for (int t = 0; t < master.data[a].items.Count; t++){                                    // For each item in the category
                for (int v = 0; v < master.data [a].items [t].dataFloats.Count; v++) {                // For each dataFloat item
                    if (master.data [a].items [t].dataFloats [v].dataName == name) {                // If the dataName of the dataFloat = name
                        master.data [a].items [t].dataFloats [v].value.Add (0.0f);
                    }
                }
                for (int v = 0; v < master.data [a].items [t].dataInts.Count; v++) {                // For each dataInts item
                    if (master.data [a].items [t].dataInts [v].dataName == name) {                    // If the dataName of the dataFloat = name
                        master.data [a].items [t].dataInts [v].value.Add (0);
                    }
                }
                for (int v = 0; v < master.data [a].items [t].dataBools.Count; v++) {                // For each dataBools item
                    if (master.data [a].items [t].dataBools [v].dataName == name) {                    // If the dataName of the dataFloat = name
                        master.data [a].items [t].dataBools [v].value.Add (false);
                    }
                }
                for (int v = 0; v < master.data [a].items [t].dataStrings.Count; v++) {                // For each dataStrings item
                    if (master.data [a].items [t].dataStrings [v].dataName == name) {                // If the dataName of the dataFloat = name
                        master.data [a].items [t].dataStrings [v].value.Add ("");
                    }
                }
                for (int v = 0; v < master.data [a].items [t].dataGameObjects.Count; v++) {            // For each dataGameObjects item
                    if (master.data [a].items [t].dataGameObjects [v].dataName == name) {            // If the dataName of the dataFloat = name
                        master.data [a].items [t].dataGameObjects [v].value.Add (null);
                    }
                }
                for (int v = 0; v < master.data [a].items [t].dataTexture2Ds.Count; v++) {            // For each dataTexture2Ds item
                    if (master.data [a].items [t].dataTexture2Ds [v].dataName == name) {            // If the dataName of the dataFloat = name
                        master.data [a].items [t].dataTexture2Ds [v].value.Add (null);
                    }
                }
                for (int v = 0; v < master.data [a].items [t].dataAudioClips.Count; v++) {            // For each dataAudioClips item
                    if (master.data [a].items [t].dataAudioClips [v].dataName == name) {            // If the dataName of the dataFloat = name
                        master.data [a].items [t].dataAudioClips [v].value.Add (null);
                    }
                }
            }
        }
    }

In that one function, there are 3 sections that could be made smaller if I could somehow replace just a few of the phrases. In the last bit, for instance, “dataAudioClips” (stated 3 times) is the only change between each for loop.

If I want to add a new type, I need to add a new for loop in every method that I’ve created. It gets tedious and boring and I’m sure there’s a better way of doing it.


In my brain, the “Easiest” way (knowing that I don’t know if this is possible or if there is an easier way) would be to have another List of “Types” where all versions of phrases I may need is kept.

AudioClips
audioClips

for instance, so that in the following loop:

for (int i = 0; i < master.data[d].audioClips.Count; i++){
            master.data [d].items [itemID].audioClips.Add (new AudioClips (master.data [d].audioClips[i].name));

I would simply be able to say "For each entry in ‘Types’, do this code but use the proper phrase for where “audioClips” or “AudioClips” etc are. A way to do that would also keep me from having to re-do too much :smile:

EDIT: The problem I’m seeing now is that “DataFloats” and “DataInts” etc, which I can make inherit from a class I"ll call “DataHolder”, can’t inherit from Data or Items. I don’t think they can at least – they each have a list called “value” which is of their unique type. I suppose it could have a list called “valueFloats” etc… but that would only work if I had a way of injecting multiple variable phrases into the code.

This feels like over-generalizing to the point of uselessness, and reinventing five or ten wheels along the way. Classes aren’t a bad thing, and they’re not restrictive; they’re a fundamental data structure! Deciding to do away with classes is kind of like deciding that we have too many names for colors to bother learning them all, so you’re just going to start referring to all colors as “blue”.

If you really want to make things super-general, though, might I recommend you use something based on JSON? (LitJSON is a good library to use for this.) It’ll allow you to create your data structure in human-readable JSON code, readable from a text file (and can be downloaded from the internet, etc), and you can access arbitrarily named items on demand.

1 Like

I thought for a bit it might be useless. (there’s a post somewhere up there about that) But then I started playing with it, and I found it to be quite easy to use. I can still create new custom classes if I need it down the line, but as far as setting up my game data, I found this to be easier than coding the classes by hand. This is, of course, coming from someone who is just now learning a lot of the things you guys already know :smile:

I’ve made some headway. I have a new function, DoDeleteItem()

public void DoDeleteItem<T>(Func<Items, List<T>> getter, Func<Items, string> stringGetter, int d, int t, string catName, int i) where T : DataHolder  {
        Debug.Log ("DoDeleteItem()");
        for (int v = 0; v < getter (master.data [d].items [t]).Count; v++) {
            if (getter (master.data [d].items [t]) [v].dataName == catName) {                    // If the dataName of the reference matched catName
                getter (master.data [d].items [t])[v].value.RemoveAt (i);
            }
        }
    }

This is called from the DeleteItem() bit, which starts like this – the commented out code is what’s been replaced…

    public void DeleteItem(string catName, int data, int i){
        int removeFromCategory = 9999999;
        // First delete the reference to this item from any linked category items
        for (int d = 0; d < master.data.Count; d++){
            for (int t = 0; t < master.data [d].items.Count; t++) {
                DoDeleteItem<DataFloats> (m => m.dataFloats, m => "dataFloats", d, t, catName, i);
                /*for (int v = 0; v < master.data [d].items [t].dataFloats.Count; v++) {
                    if (master.data [d].items [t].dataFloats [v].dataName == catName) {                    // If the dataName of the reference matched catName
                        master.data [d].items[t].dataFloats[v].value.RemoveAt (i);
                    }
                }*/
                for (int v = 0; v < master.data [d].items [t].dataInts.Count; v++) {
                    if (master.data [d].items [t].dataInts [v].dataName == catName) {                    // If the dataName of the reference matched catName
                        master.data [d].items[t].dataInts[v].value.RemoveAt (i);
                    }
                }
.........

(You can ignore the “stringGetter” part, that was just me trying to figure out what i can and can’t do)

So this definitely makes the code shorter, as I only need to call one line per type. It was mentioned that maybe I could have a list of types that I loop through. I need to figure that part out so I can replace the “dataFloats” part in this line with something variable: DoDeleteItem<DataFloats> (m => m.dataFloats, m => "dataFloats", d, t, catName, i);

If so, then I think my main goal will be accomplished.

I have to agree with StarManta; it sounds like you’re just reinventing classes. Why can’t you just make a “Proficiencies” class or struct with floats for each proficiency and then a “Character” class that contains a “Proficiencies” object?

public class Proficiencies {
  public float stealth;
  public float climbing;
  public float swimming;
}

public class Character {
  public Proficiencies proficiencies;

  public void TrySwimming() {
    if(proficiencies.swimming > 0.5f) Debug.Log("You swam!");
  }
}
1 Like