Souk21
Souk21

Reputation: 91

How to deal with a great numbers of variables in C#

I'm making a game about genetics, and I stumble into a little "problem". (it's more of a question in fact)

I have a DNA class with a lot of variables that stores the infos about each "creature" in the game.

public class DNA {
    float size;
    float speed;
    float drag;
    float eyeSize;
    int numberOfEyes;
    float color_r;
    [...]
}

Now let's imagine I want to average two DNAs.

I could do:

DNA AverageDNAs (DNA dna1, DNA dna2) {
    DNA newDNA = new DNA ();
    newDNA.size = (dna1.size+dna2.size)/2f;
    newDNA.speed = (dna1.speed+dna2.speed)/2f;
    [...]
}

But it seems pretty long, and every time I'm going to do some calculation, I'll need to rewrite every variable one by one.

So I made a function that stores all the variables (normalized between 0 and 1) into a list

public class DNA {
    public float size;
    public float speed;
    [...]
    private List<float> tempList;
    public List<float> ToList() {
        if (tempList == null) {
        tempList = new List<float>();
        toReturn.Add (size/sizemax);
        toReturn.Add (speed/speedmax);
        [...]
        }
        return tempList
    }
    public static ADN fromList(List<float> list) {
        ADN toReturn = new ADN();
        toReturn.size = list[0]*sizemax;
        [...]
    }
}

Now, I can do:

DNA AverageDNAs (DNA dna1, DNA dna2) {
    List<float> dnaList1 = dna1.ToList();
    List<float> dnaList2 = dna2.ToList();
    List<float> newDNA = new List<float>();
    for (int i = 0; i<dnaList1.Count; i++) {
        newDNA.Add ((dnaList1[i]+dnaList2[i])/2f);
    }
    return DNA.fromList(newDNA);
}

It's easier to implement new functions and calculations, but it's pretty heavy (partly due to Lists creation), and not very pretty (I guess ?).

I would not prefer using just a List<float> for the sake of readability.

I wanted to know if there's a better way to handle that kind of situations?

(Please pardon my poor english, as I'm not a native speaker)

Upvotes: 4

Views: 541

Answers (6)

atlaste
atlaste

Reputation: 31116

I'm assuming here you're not always doing the same operation, and the operation depends on the type of object.

First thing to do is to add an extra abstraction layer to float's that defines the base functionality. I assume you want to add semantics, e.g.:

public interface IDNAComputable
{
    IDNAComputable Grow(float length);
    // etc.
}

If you end up with most methods being 'empty', make it an abstract class and define some base implementation that does nothing.

Next, add a class that wraps a float, int, string or whatever you like. You might want to make semantic classes like weight, height, etc like I did. In this case I use a struct, but if you cram a lot of stuff in there you might want to use a class. The main thing here is that they all share the same interface that we can use to do stuff:

public struct Height : IDNAComputable
{
    // implement members
}

Updated It seems that you want to cut things and so on as well. Might want to add an extra enum that describes the properties. Since they map to integers, you can do a lot of fun stuff with them, including random indexes, etc.

public enum DNAProperty : int
{
    BodyWidth,
    BodyHeight,
    MouthWidth,
    MouthHeight,
    // ...
}

Last thing to do is build the DNA thing itself:

public class DNA
{
    private Width bodyWidth;
    private Height bodyHeight;
    private Width mouthWidth;
    private Height mouthHeight;
    // etc.

    public void Update(Func<IDNAComputable, IDNAComputable, DNAProperty> op)
    {
        bodyHeight = (Height)op(bodyHeight, DNAProperty.BodyHeight);
        bodyWidth = (Width)op(bodyWidth, DNAProperty.BodyWidth);
        // etc for all other stuff.
    }

    public void Merge(Func<IDNAComputable, IDNAComputable, IDNAComputable, DNAProperty> op, DNA otherCreature)
    {
        bodyHeight = (Height)op(bodyHeight, otherCreature.bodyHeight, DNAProperty.BodyHeight);
        bodyWidth = (Width)op(bodyWidth, otherCreature.bodyWidth, DNAProperty.BodyWidth);
        // etc for all other stuff.
    }
}

Note that the overhead is not that bad, it basically uses a few extra indications and virtual function calls; it should be fast enough for your use case.

Last thing to do is to define the actual functionality.

public void GrowCreature(DNA creature, float height) 
{
   creature.Update((a)=>a.Grow(height));
}

update Some practical examples based on your comments:

Right now I'm using the ToList() method to compare two DNAs (to know how much they differ), to apply a mutation to a random variable, and to cross two DNAs (I'm not averaging, it was just an example)

Calculate the difference is easy with this approach, even if your 'height' doesn't attribute the same as the 'width'. You can even introduce a 'body mass' property if you like etc:

double diff = 0;
creature.Merge((lhs, rhs, prop) => { diff += lhs.Difference(rhs); return lhs; });

... or if you only want to use a certain set of properties for that, you can do that as well:

double diff = 0;
creature.Merge((lhs, rhs, prop) => 
    { 
        if (prop == DNAProperty.BodyMass)
        {
            diff += lhs.Difference(rhs); 
        } 
    return lhs; });

List is pretty useful, though, as I just have to pick crossover points, iterate the lists, and "cut"

Don't need a list for that:

double diff = 0;
creature.Merge((lhs, rhs, prop) => 
    { 
        diff += lhs.Difference(rhs); 

        if (diff > SomeCutThreshold) { return lhs; }
        else { return rhs; }
    });

... or perhaps you do want to know where the cut point will be?

cut points meant: for(int i=0; i<dna1.count;i++) { newList[i] = i>cutPoint? dna1[i] : dna2[i] }

double diff = 0;
DNAProperty cutpoint = (DNAProperty)int.MaxValue;
creature.Merge((lhs, rhs, prop) => 
    { 
        diff += lhs.Difference(rhs); 

        if (diff > SomeCutThreshold) { cutpoint = prop; diff = double.MinValue; }
        return lhs;
    });

creature.Merge((lhs, rhs, prop) => ((int)prop > (int)cutpoint)?lhs:rhs);

To conclude

While this might seem like more work, if you make the right abstractions and inheritance trees you should end up with less code, not more. For example, most basic functionality between 'Height' and 'Width' is probably shared. I'd also try to end up with 1 or 2 overloads of the 'Func' thing, no more.

Upvotes: 1

Jamiec
Jamiec

Reputation: 136114

You could define an interface/class model around a Trait which knows how to "combine" with a similar trait. For example, a trair represented as a float, where the combination is an average could be expressed as:

public class FloatTrait 
{
    private float val;
    public FloatTrait(float val)
    {
        this.val = val;
    }
    public float Value{get { return this.val; }}

    public FloatTrait CombineWith(FloatTrait t)
    {
       return new FloatTrait((this.Value + t.Value)/2.0f);
    }
}

You can then have properties in your DNA object which represent traits, not absolute values

public class DNA
{
    public FloatTrait Size{get;set;}
    public FloatTrait Speed{get;set;}
}

The addition of a dictionary to reference these makes it much simpler to combine them (Worth noting: Ive used string literals as the key, it would be more appropriate to use an enumeration! h/t @Luaan)

public class DNA
{
    private Dictionary<string, FloatTrait> traits = new Dictionary<string, FloatTrait>() {
        {"Size", new FloatTrait(5.0)},
        {"Speed", new FloatTrait(50.0)},
    }

    public DNA(Dictionary<string, FloatTrait> dict)
    {
        this.traits = dict;
    }

    public FloatTrait this[string key]{ get{ return traits[key]; } }

    public float Size{ get{ return traits["Size"].Value; }
    public float Speed{ get{ return traits["Speed"].Value; }

    public DNA CombineWith(DNA other)
    {
        var newDict = this.traits.ToDictionary(k => k.Key
                                               v => v.Value.CombineWith(other[v.Key]));
        return new DNA(newDict);
    }
}   

You could extend this to support "traits" other than those represented by a float. You could also alter the mechanism for "combining" traits if averaging is not appropriate.

Live example of this working: http://rextester.com/MVH5197

Upvotes: 1

NeddySpaghetti
NeddySpaghetti

Reputation: 13495

I think a better approach is to define your own arithmetic operators on the DNA class. See this link on how to do it.

public class DNA {
    float size;
    float speed;
    float drag;
    float eyeSize;
    int numberOfEyes;
    float color_r;
    [...]

   public static DNA operator +(DNA dna1, dna1 rhs) 
   {
       DNA newDNA = new DNA ();
       newDNA.size = (dna1.size+dna2.size);
       newDNA.speed = (dna1.speed+dna2.speed);
       [...]

       return newDNA;
   }
}

Then you can do:

var newDna = dna1 + dna2;

I guess internally you can use @Luaan suggestion to avoid retyping the same thing over and over, but the usage is neater with the operators IMHO.

 public static DNA operator +(DNA dna1, dna1 rhs) 
 {
      return Cross(dna1, dna2, (d1, d2) => d1 + d2);  
 }

Upvotes: 4

shay__
shay__

Reputation: 3990

Use Dictionary instead of the fields. So that when you need to get the average you can use:

foreach(var item in this._dict)
{
    avgDNA._dict[item.Key] = (item.Value + other._dict[item.Key]) / 2;
}

That way you don't care about new fields.

Upvotes: 1

Adam Prescott
Adam Prescott

Reputation: 945

Can you use reflection to get the properties, then loop through them on your list of objects?

In case you're not familiar with reflection, here's an MSDN example that should help: https://msdn.microsoft.com/en-us/library/kyaxdd3x(v=vs.110).aspx

Upvotes: 0

Luaan
Luaan

Reputation: 63742

If you're always applying the same operation to all, you could use a parameter to specify the operation to be performed:

public DNA Cross(DNA dna1, DNA dna2, Func<float, float, float> operation)
{
  var newDNA = new DNA();

  newDNA.size = operation(dna1.size, dna2.size);
  newDNA.speed = operation(dna1.speed, dna1.speed);
  // And all the rest
}

This allows you to reuse this method for a wide range of different operations - as long as they always operate on the same amount of fields:

// Average
(d1, d2) => (d1 + d2) / 2f
// Sum
(d1, d2) => d1 + d2
// Random
(d1, d2) => RandomBool() ? d1 : d2

etc.

If you do want to apply the operation more selectively, you could also expand the Cross method to take an enum specifying which fields exactly you want to update. And of course, you might want to add another Func (or Action, depending on how comfortable you are with mutating arguments) to update the rest in any way you want.

Upvotes: 8

Related Questions