0

I'm making a game with trains. I need to serialize which train is used for each level. So I can simply do:

public class Level {
    Track track;
    Train train;
    [...]
}

public class Train {
    Wagon[] wagons;
}

public class Wagon {
    float length;
    float weight;
}

//Example:
level = new Level (track, new Wagon (1f,2f));

But then, I need to have types of Wagons I can choose from. Like so:

public class Wagon {
    float length;
    float weight;
    public static Wagon Small { get { return new Wagon (1f,2f); }}
    public static Wagon Big { get { return new Wagon (10f,20f); }}
}

//Example:
level = new Level (track, Wagon.Small);

That works. But if I want to tweak a wagon parameter, I have to deserialize, modify and reserialize each Wagon.

So I came up with this:

public enum WagonType {
    Small,
    Big
}

public class WagonParams {
    float length;
    float weight;
}

public class Wagon {
    WagonType type;
    WagonParams params { get { return WagonFactory.GetParams(type); }}
}

public WagonFactory {
    static WagonParams small;
    static WagonParams big;
    static WagonFactory () {
        small = new WagonParams (1f,2f);
        big = new WagonParams (10f,20f);
    }
    public static WagonParams GetParams (WagonType type) {
        switch (type) {
            case WagonType.Small: return small;
            case WagonType.Big: return big;
        }
    }
}

//Example:
level = new Level (track, new Wagon(WagonType.Small));

which seems a bit like an overkill, but works.

Now say that I want a SmallWagon but with more weight, I can do:

public interface IOverrideWagonField {
    WagonParams Override (WagonParams params);
}

public class OverrideWeight : IOverrideWagonField {
    float weight;
    WagonParams Override (WagonParams params) {
        return new WagonParams (params.length, weight);
    }
}

public class Wagon {
    WagonType type;
    IOverrideField overrider;
    WagonParams params { get { return WagonFactory.GetParams(type,overrider); }}
}

public WagonFactory {
    static WagonParams small;
    static WagonParams big;
    static WagonFactory () {
        small = new WagonParams (1f,2f);
        big = new WagonParams (10f,20f);
    }
    public static WagonParams GetParams (WagonType type, IOverrideWagonField overrider) {
        switch (type) {
            case WagonType.Small: return overrider != null? overrider.Override(small) : small;
            case WagonType.Big: return overrider != null? overrider.Override(big) : big;
        }
    }
}

//Example:
level = new Level(track, new Wagon(WagonType.Small,null)); //Normal Wagon
level = new Level(track, new Wagon(WagonType.Small,new OverrideWeight (23f)); //Override weight

What bothers me (beside the complexity) is that every wagon use memory for a IOverrideWagonField that they might not use.

The system falls short if you want to override multiple fields. It would need an array of IOverrideWagonField, but it would create a new object for each overrode field. (Override() method calls "new")

So I feel this system is far from perfect.

What do you think? How would you go about implementing such a system?

1

Looks like a case of over-engineering, personally I would keep it simple: treat all wagons the same.

Base code:

public class Wagon
{
    public float Length { get; set; }
    public float Weight { get; set; }
}

public class Train
{
    public List<Wagon> Wagons { get; set; }
}

public class Level
{
    public Train Train { get; set; }

    public static Level Deserialize(TextReader textReader)
    {
        if (textReader == null) throw new ArgumentNullException(nameof(textReader));
        var serializer = new XmlSerializer(typeof(Level));
        var deserialize = serializer.Deserialize(textReader);
        var level = deserialize as Level;
        return level;
    }

    public static void Serialize(TextWriter textWriter, Level level)
    {
        if (textWriter == null) throw new ArgumentNullException(nameof(textWriter));
        if (level == null) throw new ArgumentNullException(nameof(level));
        var serializer = new XmlSerializer(typeof(Level));
        serializer.Serialize(textWriter, level);
    }
}

Demo:

public class Demo
{
    public Demo()
    {
        // some level
        var level = new Level
        {
            Train = new Train
            {
                Wagons = new List<Wagon>(new[]
                {
                    new Wagon
                    {
                        Length = 100.0f,
                        Weight = 100.0f
                    },
                    new Wagon
                    {
                        Length = 200.0f,
                        Weight = 200.0f
                    }
                })
            }
        };

        // serialize
        string xml;
        using (var writer = new StringWriter())
        {
            Level.Serialize(writer, level);
            xml = writer.ToString();
        }

        // deserialize
        using (var reader = new StringReader(xml))
        {
            var level1 = Level.Deserialize(reader);
        }
    }
}

You could sub-class Wagon as follows:

public class SmallWagon : Wagon
{
    public SmallWagon()
    {
        Length = 10.0f;
        Weight = 10.0f;
    }
}

public class LargeWagon : Wagon
{
    public LargeWagon()
    {
        Length = 10000.0f;
        Weight = 10000.0f;
    }
}

But you will have to include these types in Wagon for serialization to work:

[XmlInclude(typeof(SmallWagon))]
[XmlInclude(typeof(LargeWagon))]
public class Wagon
{
    public float Length { get; set; }
    public float Weight { get; set; }
}

You can somehow mitigate this tedious job by gathering them automatically and using the other constructor:

var extraTypes = Assembly
    .GetExecutingAssembly()
    .GetTypes()
    .Where(s => s.BaseType == typeof(Wagon))
    .ToArray();
var serializer = new XmlSerializer(typeof(Level), extraTypes);

Still, feels like a code smell as now there are many types of wagons:

<?xml version="1.0" encoding="utf-16"?>
<Level xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <Train>
    <Wagons>
      <Wagon xsi:type="SmallWagon">
        <Length>10</Length>
        <Weight>10</Weight>
      </Wagon>
      <Wagon xsi:type="LargeWagon">
        <Length>10000</Length>
        <Weight>10000</Weight>
      </Wagon>
      <Wagon>
        <Length>100</Length>
        <Weight>100</Weight>
      </Wagon>
      <Wagon>
        <Length>200</Length>
        <Weight>200</Weight>
      </Wagon>
    </Wagons>
  </Train>
</Level>

To ease the process of building preset wagons I would either use some static class or some primitive builder system:

public static class WagonHelper
{
    public static Wagon CreateLargeWagon()
    {
        return new Wagon
        {
            Length = 10000.0f,
            Weight = 10000.0f
        };
    }

    public static Wagon CreateSmallWagon()
    {
        return new Wagon
        {
            Length = 10.0f,
            Weight = 10.0f
        };
    }
}

// or

public abstract class WagonBuilder
{
    public abstract Wagon Build();
}

public class LargeWagonBuilder : WagonBuilder
{
    public override Wagon Build()
    {
        return new Wagon
        {
            Length = 10000.0f,
            Weight = 10000.0f
        };
    }
}

Stick to KISS principle,

Now regarding serializing many times there's nothing wrong about it, Unity3D does that very frequently to handle things such as assembly reload after a rebuild and so on.

  • Basically you're saying that I should stick with deserializing every time I need to change a value. I know that works, but I was wondering about what kind of architecture would work without the need to do it. What if I want to tweak Weight at runtime? – Souk21 Aug 14 '16 at 7:39
  • Beside that, in my implementation, the WagonParams are shared as a single object (flyweight) stored in memory, in opposition to have every params for every wagon stored separately in memory. – Souk21 Aug 14 '16 at 7:46
  • No, in your case you will want to work against value in memory and possibly serialize only when your application exits or user saves its session. The example of intensive serialization is when external factors are in play and that it becomes cheaper to serialize than manually figuring them out: e.g. Unity does serialization when its window has lost the focus and source files have been changed -> it's simpler in the end to serialize all then restore than anything else. As for the WagonParams IMO it doesn't really makes sense ... unless you wish to make your life harder :) – Aybe Aug 14 '16 at 18:02

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.