Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Static Instances and Enums for referencing common properties

I'm working on a problem at the moment and I've run into an issue where I have multiple architectural options, but am not sure which is going to be the best option going forward.

Context: I am writing some code for a game, which uses a tile map. Tiles have common attributes, for example, all floor tiles are walkable, while walls are not (along with other properties). Therefore, it makes sense to have some kind of reference to which each tile can point at a common reference to discern what its properties are.

I have come up with a few solutions, however am uncertain as to which is the most efficient or will provide the greatest flexibility moving forward. Therefore, I am curious as to which would be considered 'best', either in general or for my specific case. Similarly, if there is a better way of doing this that I haven't listed, please let me know.

(As an aside, as the number of tile types grow, I may also run into the issue where it may not be practical to hard code these values and some kind of serialization or file I/O might make more sense. As I have done neither in C#, if you see any potential stumbling blocks here, it would be similarly appreciated if you could include them in your answer.)

Below are each of my three approaches, which I have simplified slightly to make them more general:

Approach #1: Enum with Extension Methods:

public enum TileData{
    WALL,
    FLOOR,
    FARMLAND
    //...etc
}

public static class TileDataExtensions{

    public static int IsWalkable(this TileData tile){
        switch(tile){
        case TileData.FLOOR:
        case TileData.FARMLAND:
            return true;
        case TileData.WALL:
            return false;
        }
    }

    public static int IsBuildable(this TileData tile){
        switch(tile){
        case TileData.FLOOR:
            return true;
        case TileData.WALL:
        case TileData.FARMLAND:
            return false;
        }
    }

    public static Zone ZoneType(this TileData tile){
        switch(tile){
        case TileData.WALL:
        case TileData.FLOOR:
            return Zone.None;
        case TileData.FARMLAND:
            return Zone.Arable;
        }
    }

    public static int TileGraphicIndex(this TileData tile){
        switch(tile){
        case TileData.WALL:
            return 0;
        case TileData.FLOOR:
            return 1;
        case TileData.FARMLAND:
            return 2;

        }
    }

    public enum Zone{
        Shipping,
        Receiving,
        Arable,
        None
    }
}

Approach #2: Huge Private Constructor & Static Instances

public class TileData{

    public bool IsWalkable{get;};
    public bool IsBuildSpace{get;};
    public Zone ZoneType{get;};
    public int TileGraphicIndex{get;};

    public static TileData FLOOR    = new TileData(true, true, Zone.None, 1);
    public static TileData WALL     = new TileData(false, false, Zone.None, 0);
    public static TileData FARMLAND = new TileData(true, false, Zone.Arable, 2);
    //...etc

    private TileData(bool walkable, bool buildSpace, Zone zone, int grahpicIndex){
        IsWalkable = walkable;
        IsBuildSpace = buildSpace;
        ZoneType = zone;
        TileGraphicIndex = grahpicIndex;
    }

    public enum Zone{
        Shipping,
        Receiving,
        Arable,
        None
    }
}

Approach #3: Private Constructor and Setters, with Static Instances:

public class TileData{

    public bool IsWalkable{get; private set;};
    public bool IsBuildSpace{get; private set;};
    public Zone ZoneType{get; private set;};
    public int TileGraphicIndex{get; private set;};


    public static TileData FLOOR{
        get{
            TileData t = new TileData();
            t.IsBuildSpace = true;
            t.TileGraphicIndex = 1;
            return t;
        }
    }
    public static TileData WALL{
        get{
            TileData t = new TileData();
            t.IsWalkable = false;
            return t;
        }
    }
    public static TileData FARMLAND{
        get{
            TileData t = new TileData();
            t.ZoneType = Zone.Arable;
            t.TileGraphicIndex = 2;
            return t;
        }
    }
    //...etc

    //Constructor applies the most common values
    private TileData(){
        IsWalkable = true;
        IsBuildSpace = false;
        ZoneType = Zone.None;
        TileGraphicIndex = 0;
    }

    public enum Zone{
        Shipping,
        Receiving,
        Arable,
        None
    }
}

Many thanks, LR92

EDIT: The types of tiles are determined before compile time by the designer, ie no class should be allowed to create new TileData types (ie, in examples 2&3, instances).

like image 226
LeftRight92 Avatar asked Nov 28 '25 11:11

LeftRight92


2 Answers

Approach 2 is friendly to the designer and is slightly more efficient than Approach 3. It can also be supplemented by Approach 1's extension methods if you want to do some reasoning system-by-system instead of tile-by-tile.

Consider supplementing your constructor with a static factory:

private TileData(bool walkable, bool buildSpace, Zone zone, int grahpicIndex){
    IsWalkable = walkable;
    IsBuildSpace = buildSpace;
    ZoneType = zone;
    TileGraphicIndex = grahpicIndex;
}

private static TileData Tweak(TileData parent, Action<TileData> tweaks) {
    var newTile = parent.MemberwiseClone();
    tweaks(newTile);
    return newTile;
}

This allows you to build your tile types with a sort of prototypal inheritance (except instead of looking up the chain of prototypes at runtime, it will be baked in). This should be very useful as it is common in tile-based games to have tiles that are mostly similar but have slightly different behaviors or graphics.

public readonly static TileData GRASS =          new TileData(etc.);
public readonly static TileData WAVY_GRASS =     Tweak(GRASS, g => g.TileGraphicIndex = 10);
public readonly static TileData JERKFACE_GRASS = Tweak(GRASS, g => g.IsWalkable = false);
public readonly static TileData SWAMP_GRASS =    Tweak(GRASS, g => {g.TileGraphicIndex = 11; g.IsBuildable = false;});

Note: when you serialize/deserialize your tile maps, you'll want to have a consistent ID of some sort assigned to each tile (in particular, this makes working with Tiled easier). You could pass that in to the constructor (and to Tweak, as another argument, because otherwise the tweaked tile will have cloned the ID of its parent!). It would be good then to have something (a unit test would be fine) that ensures that all fields of this class of type TileData have distinct IDs. Finally, to avoid having to re-enter these IDs into Tiled, you could make something that exports the data from this class into a Tiled TSX or TMX file (or similar file for whatever map editor you ultimately go with).

EDIT: One last tip. If your consistent IDs are consecutive ints, you can "compile" your tile data into static arrays split out by property. This can be useful for systems in which performance is important (for example, pathfinding will need to look up walkability a lot).

public static TileData[] ById = typeof(TileData)
                                .GetFields(BindingFlags.Static | BindingFlags.Public)
                                .Where(f => f.FieldType == typeof(TileData))
                                .Select(f => f.GetValue(null))
                                .Cast<TileData>()
                                .OrderBy(td => td.Id)
                                .ToArray();
public static bool[] Walkable = ById.Select(td => td.IsWalkable).ToArray();

// now you can have your map just be an array of array of ids
// and say things like: if(TileData.Walkable[map[y][x]]) {etc.}

If your ids are not consecutive ints, you can use Dictionary<MyIdType, MyPropertyType> for the same purpose and access it with the same syntax, but it wouldn't perform as well.

like image 65
Jerry Federspiel Avatar answered Nov 30 '25 00:11

Jerry Federspiel


Let's try to solve your requirement with more object oriented approach. Less conditional more polymorphism. In my opinion if you have more chances to come up with new Types of Tiles apart from the mentioned ones. Means the design should be extensible and should be open for minimal change to introduce new component.

For e.g. Let's keep the Tile class a base class.

public abstract class Tile
{
    public Tile()
    {
        // Default attributes of a Tile
        IsWalkable = false;
        IsBuildSpace = false;
        ZoneType = Zone.None;
        GraphicIndex = -1;
    }

    public virtual bool IsWalkable { get; private set; }
    public virtual bool IsBuildSpace { get; private set; }
    public virtual Zone ZoneType { get; private set; }
    public virtual int GraphicIndex { get; private set; }

    /// <summary>
    /// Factory to build the derived types objects
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <returns></returns>
    public static T Get<T>() where T : Tile, new()
    {
        return new T();
    }
}

Now we have defined a Tile with default attributes. If required more default attributes of a Tile can be added as Vitual properties. Since this class is abstract one can not simply create the object so a Derived class has to be introduced which would be our specific type of Tile e.g. Wall, Floor etc.

public class Floor : Tile
{
    public override bool IsBuildSpace
    {
        get { return true; }
    }

    public override bool IsWalkable
    {
        get { return true; }
    }
    public override int GraphicIndex
    {
        get { return 1; }
    }
}

public class Wall : Tile
{
    public override int GraphicIndex
    {
        get {  return 0; }
    }

    public override Zone ZoneType
    {
        get { return Zone.Arable; }
    }
}

If a new type of tile has to be created. Just inherit the class from Tile and Override the properties which would require to have specific values instead of defaults.

Crafting a tile would be done via base class just by invoking the generic static factory method Get<>() that will only accept a derived type of Tile:

        Tile wallLeft = Tile.Get<Wall>();
        Tile floor = Tile.Get<Floor>();

So Everything is Tile and represents a different set of values of defined properties. They can be identified Either by their type or values of properties. And more importantly as you can see we got rid of all the If..Else, Switch case, Constructor overloads. Hows that sound?

Extending the Tile with new attributes

So for e.g. a new property/attribute is required on Tiles e.g. Color simple add a Virtual property to Tile class named Color. In constructor give it a default value. Optinally (not mandatory) Override the property in child classes if your tile should be in special color.

Introducing new Type of Tile

Simply derive the New Tile type with Tile class and override required properties.

like image 22
vendettamit Avatar answered Nov 29 '25 23:11

vendettamit