1

I am a bit stumped on how to achieve this, so I have simplified the problem as much as I can, and hope to get a suggestion or two.

I have a class called creature.

class Creature
{
    private string m_species;//This needs to be updated whenever any of the other variables change. Based on a list of user-defined species.
    private string m_name;
    private int m_numberOfLegs;
    private SkinColorEnum m_skinColor;
    //etc...
}

I would like to somehow be able to define species, and then compare instances of Creature against this. (A human would have pink or black skin. An orc would have green skin.)

To make it more difficult, I would also like to serialize/deserialize both the instances of creature, and instances of species. That way I can make editors where I define my creatures and species, and after importing into my actual program (game, in this case) do a comparison and figure out which species each creature belongs to.

So the definitions of species would be something like:

class Species
{//Not actual c# syntax. Just trying to explain.
    Creature.m_name;
    Creature.m_numberOfLegs;
    Creature.m_skinColor;
}

Human:

m_name == ?; //Don't care what the name is.
m_numberOfLegs == 2; //Must have 2 legs.
m_skinColor == (Black || Pink);//Need to be able to specify multiple possible values.

Orc:

m_name == ?; //Don't care what the name is.
m_numberOfLegs == 2; //Must have 2 legs.
m_skinColor == Green;//Need to be able to specify multiple possible values, even though we only need 1 for orc.

Is there any C# construct that can do this? Maybe there is some way to use Attributes? Inheritance? I can't think of anything that would work well.

Features like giving me compiler errors if I add variables to Creature but not Species would be a bonus.

Use case:

1. The user starts the "EditSpeciesAndCreatures" executable.
2. The user defines a list of species. Each species has for every variable in "Creature" defined either: A set of matching values, a single value or no value(indicating it does not matter what that variable is).
3. The user defines a list of creatures. Simply setting each variable to something. Strings as strings, ints as ints, enums as enums. Nothing unusual.
4. The user saves the two lists to file. (Serializing)
5. The user starts "The Game", and the game loads the file. (Deserializing)
6. All the values in each Creature are set as they were defined in the file.
7. All the values in each Species are set as they were defined in the file.
8. A comparison is run with every creature against every Species. The first match results in Creature's m_species variable being set to that Species.
9. All creatures are now marked as their respective species.

Additional use case:

10. One or more of the creatures change. Skin color, number of legs, anything.
11. A new comparison is run for these Creatures, and a new species is found, unless the changed variable did not matter to any of the defined Species.
Pumpkim
  • 23
  • 7
  • Please add the code you have tried so we can help you better. – Aldert Oct 07 '18 at 13:03
  • See https://stackoverflow.com/questions/8447/what-does-the-flags-enum-attribute-mean-in-c – Rui Jarimba Oct 07 '18 at 13:14
  • @RuiJarimba That's really cool. I didn't know you could use the [Flags] attribute to turn your enum into a bitmask. Useful, but only solves enum types. I'll need something that encapsulates my "Creature" class somehow, can be defined runtime and works with any(at least most) types. – Pumpkim Oct 07 '18 at 13:29
  • @Aldert I'm afraid that would only make matters worse. There's a lot of code that would be distracting. (Unity integration among other things) And this is more of an abstract discussion. I don't have code that tries to do this, because I'd need to know what to try first. If that makes sense. – Pumpkim Oct 07 '18 at 13:33
  • Could you add some use-cases for this? I can see how you'd like to define this, but how would you use these constructs somewhere higher-up (the "compare" part)? – V0ldek Oct 07 '18 at 14:04
  • @V0ldek A fair request. Adding use case to the bottom now. – Pumpkim Oct 07 '18 at 14:27

2 Answers2

1

So you need to maintain various Species instances, which shall be editable from the GUI. And you need to maintain a set of Creates, each belonging under a given Species. Then you have two different classes and a reference between them:

public class Species
{
    // species name, e.g. 'Human', 'Orc'
    public string Name { get; set; }

    public int NumberOfLegs { get; set; }

    // if SkinColorEnum is a [Flags]-enum, then it can have multiple values here
    public SkinColorEnum SkinColor { get; set; }
}

public class Creature
{
    // the species of this creature
    public Species Species { get; set; }

    // creature name, e.g. 'Adam', 'Eve', or `Balogog` (an orc)
    public string Name { get; set; }

    // we assume only a single skin color value is used, even though SkinColorEnum were a Flags
    public SkinColorEnum SkinColor { get; set; }
}

Note that there is absolutely no need to duplicate NumberOfLegs, because they can be assumed a constant for all creatures belonging under the same species. However, should you need to represent situations where e.g. a leg was chopped off, you could use a nullable type int? and do this:

public int NumberOfLegs
{
    get => numberOfLegs ?? Species.NumberOfLegs;
    set => numberOfLegs = value;
}
private int? numberOfLegs;

All objects in the game can be then held under a single World class:

public class World
{
    public List<Species> Species { get; set; }
    public List<Creature> Creatures { get; set; }
}

This is then super-easy to serialize, e.g. by using XmlSerializer.

Ondrej Tucny
  • 27,626
  • 6
  • 70
  • 90
  • This is my fall-back solution. For every variable in Creature that is not an enum, I would have to make a list of acceptable values in Species. Tough to maintain, but it does get the job done. I was hoping C# has some kind of built in functionality for this kind of thing. – Pumpkim Oct 07 '18 at 14:42
  • I read your 'use case'. It seems you are reinventing the wheel. *Serialization* is here to do *everything* under point (4) and under points (5) to (9). What you consider a 'fall-back solution' should be the *primary* solution for your case. – Ondrej Tucny Oct 07 '18 at 14:52
  • Please not that I mistakenly named both classes a `Creature`. Naturally, the first one was supposed to be `Species`. Fixed by answer. – Ondrej Tucny Oct 07 '18 at 14:53
  • Well, yes. I am using serialization for that part... That's just to get the data from the editor to the game. The reason your solution is the fall-back is that it's messy. I have to make sure both Creature and Species is up-to-date at all times. It's a source of potential errors. The compiler will NOT tell me when I've forgotten to handle a variable in Species. And I have to manually write and maintain the compare function. It's not clean, and I was hoping C# offered a better solution. – Pumpkim Oct 07 '18 at 15:00
  • The information and code samples you provided hardly provide any evidence for something being messy; rather, they show you haven't come up with a complete working solution so far. Don't bother there will be many properties at the end of the day. Focus on a viable *architecture*. – Ondrej Tucny Oct 07 '18 at 15:30
1

Okay, so based on your use-case I'd propose something like this:

interface IFeature<T> where T : IEquatable<T>
{
    bool IsMatch(T creatureFeature);
}

class FeatureAny<T> : IFeature<T> where T : IEquatable<T>
{
    public bool IsMatch(T creatureFeature) => true;
}

class FeatureSingle<T> : IFeature<T> where T : IEquatable<T>
{
    private T _feature;

    public FeatureSingle(T feature) => _feature = feature;

    public bool IsMatch(T creatureFeature) => _feature.Equals(creatureFeature);
}

class FeatureMany<T> : IFeature<T> where T : IEquatable<T>
{
    private ISet<T> _features;

    public FeatureMany(params T[] features) => _features = new HashSet<T>(features);

    public bool IsMatch(T creatureFeature) => _features.Contains(creatureFeature);
}

class FeatureFactory
{
    public IFeature<T> MakeFeature<T>(params T[] features) where T : IEquatable<T>
    {
        if(!features.Any())
        {
            return new FeatureAny<T>();
        }
        else if(features.Length == 1)
        {
            return new FeatureSingle<T>(features.Single());
        }
        else
        {
            return new FeatureMany<T>(features);
        }
    }
}

enum SkinColor
{
    White,
    Black,
    Green
}

interface ICreature
{
    string Name { get; set; }
    int NumberOfLegs { get; set; }
    SkinColor SkinColor { get; set; }

    ISpecies Species { get; set; }
}

interface ISpecies
{
    IFeature<string> Name { get; set; }
    IFeature<int> NumberOfLegs { get; set; }
    IFeature<SkinColor> SkinColor { get; set; }
}

Now deciding the Species for each Creature would be iterating over the defined species and calling a subroutine like this:

static bool IsCreatureOfSpecies(ICreature creature, ISpecies species) =>
    species.Name.IsMatch(creature.Name) &&
    species.NumberOfLegs.IsMatch(creature.NumberOfLegs) &&
    species.SkinColor.IsMatch(creature.SkinColor);

Example definitions for a species:

var humanSpecies = new Species
{
    Name = FeatureFactory.MakeFeature(),
    NumberOfLegs = FeatureFactory.MakeFeature(2),
    SkinColor = FeatureFactory.MakeFeature(SkinColor.White, SkinColor.Black)
};

You can influence comparison by making a custom type for a feature and overriding the Equals method. Serialization and deserialization should be straightforward and defined for each implementation of IFeature. The only thing that might grind your gears is the IsCreatureOfSpecies method, as it is required to fill it up every time you change the ISpecies definition. Also, ICreature definition depends on ISpecies (they have to have complementary fields).

It's hard for me to find a solution that would warn you at compile time, but you can get away with this using reflection. Firstly, you can write your IsCreatureOfSpecies method so that it iterates over all properties in ISpecies that are of type IFeature<T> and find a property with the same name in the ICreature. Secondly, you can safeguard yourself by unit testing. Simply make sure that the interfaces are complementary using reflection and test the IsCreatureOfSpecies comprehensively.

V0ldek
  • 9,623
  • 1
  • 26
  • 57
  • Oh wow, now that's an interesting approach. I _think_ this should address most of my concerns. I'm going to give this a whirl and see how it runs. Of course I'm going to have to comment the shit out of the code so it doesn't kill someone later down the road :P I'm marking this as the solution, as it was exactly the kind of thing I was asking for. – Pumpkim Oct 07 '18 at 18:11