静态实例和枚举用于引用常见属性

3
我正在解决一个问题,遇到了一个困难:我有多种架构选项,但不确定哪个是未来最佳选择。上下文:我正在为游戏编写代码,游戏使用瓷砖地图。瓷砖具有共同属性,例如,所有地板瓷砖都可以行走,而墙壁则不能(还有其他属性)。因此,让每个瓷砖指向一个共同的引用以区分其属性是有意义的。我想出了几个解决方案,但不确定哪个效率最高或提供的灵活性最大,因此我很好奇哪个会被认为是“最佳”的,无论是一般情况还是我的特定情况。同样,如果有更好的方法没有列出,请告诉我。另外,随着瓷砖类型数量的增加,可能实际上不适合硬编码这些值,并且某种序列化或文件 I/O 可能更有意义。由于我在 C# 中都没有做过,如果您看到任何潜在障碍,也请在回答中包括它们。以下是我的三种方法,我稍微简化了它们使它们更普遍:
方法一:使用扩展方法的枚举:
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
    }
}

方法二:使用巨大的私有构造函数和静态实例

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
    }
}

方法三: 私有构造函数和设置器, 静态实例:

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
    }
}

非常感谢,LR92。
编辑:瓷砖的类型在设计时确定,即不应允许任何类创建新的TileData类型(例如,在示例2和3中,实例)。

你预计会有多少种瓷砖类型(数量级)? - Jerry Federspiel
@JerryFederspiel 不到100,最多50左右。 - LeftRight92
1
每次都会重新创建。创建一个 private static TileData _farmLand,在 getter 中写上 return _farmLand ?? _farmLand = new TileData() { property stuff };。这将返回 _farmLand(如果不为 null),否则它将调用 TileData 构造函数,初始化属性,将其分配给 _farmLand,并返回它。 - Jerry Federspiel
以上只是为了使方法3更有效率,但方法2仍然更好,因为它使用字段访问(非常快)而不是属性访问器(稍微慢一些)。 - Jerry Federspiel
@LeftRight92 请检查我的答案。我建议的方法涉及整洁和清晰的设计。不需要构造函数重载,而且易于扩展。将任何新内容添加到Tile属性中,甚至不会破坏任何东西,而在构造函数重载的情况下,添加新参数将破坏所有实例,其中将使用new TileData() - vendettamit
显示剩余2条评论
6个回答

1

方法二对设计师友好,比方法三略微更有效率。如果您想按系统而不是按图块进行推理,还可以通过方法一的扩展方法来补充。

考虑使用静态工厂来补充您的构造函数:

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;
}

这样可以使用一种类原型继承的方式构建瓷砖类型(但不是在运行时查找原型链,而是在编译时处理)。这应该非常有用,因为在基于瓷砖的游戏中,瓷砖通常很相似,但具有稍微不同的行为或图形。
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;});

注意:在对你的瓷砖地图进行序列化/反序列化时,你将希望为每个瓷砖分配一种一致的ID(特别是这使得与Tiled更容易协作)。你可以通过构造函数传递它(并且作为另一个参数传递给Tweak,因为否则修改后的瓷砖将克隆其父级的ID!)。然后最好有某些东西(单元测试就行),以确保类型为TileData的此类的所有字段具有不同的ID。最后,为了避免重新输入这些ID到Tiled中,你可以制作一些将此类数据导出到Tiled TSX or TMX file(或任何其他地图编辑器的类似文件)的方法。
编辑:最后一个提示。如果你的一致ID是连续的整数,你可以将你的瓷砖数据“编译”成由属性分离的静态数组。这对于性能很重要的系统非常有用(例如,路径查找需要经常查找可走性)。
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.}

如果您的ID不是连续的整数,您可以使用Dictionary<MyIdType,MyPropertyType>来达到相同的目的,并以相同的语法访问它,但它的性能不会那么好。

我需要查找Action<>的定义,但这看起来很有前途...虽然我认为我理解了。 - LeftRight92
扩展方法可以应用于任何类型。扩展方法签名的语法与您的示例相同,不会改变。 - Jerry Federspiel
你如何修改代码以允许多个操作?例如,如果我想要改变一个布尔值和图块图形索引怎么办? - LeftRight92
非常感谢,我还没有使用过序列化,所以将来可能需要解决这个问题,因此未来可能会有另一个相关的问题。再次感谢您的回答。 - LeftRight92
添加了关于使访问TileData方便和高效的注释。 - Jerry Federspiel
显示剩余3条评论

1
让我们尝试用更多的面向对象方法来解决您的需求。在我看来,如果您有更多机会提出除所提到的瓷砖类型之外的新瓷砖类型,那么设计应该是可扩展的,并且应该对引入新组件的最小更改开放。少条件,多多态。例如,让我们将Tile类保留为基类。
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();
    }
}

现在我们已经定义了一个带有默认属性的Tile。如果需要更多的默认属性,可以将其添加为虚拟属性。由于这个类是抽象的,所以不能直接创建对象,因此必须引入一个派生类,这将是我们特定类型的Tile,例如Wall、Floor等。
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; }
    }
}

如果需要创建新类型的瓷砖,只需从Tile类继承,并覆盖需要具有特定值而不是默认值的属性。

通过调用通用静态工厂方法Get<>()来使用基类制作瓷砖,该方法仅接受Tile的派生类型:

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

所以一切都是瓷砖,代表了不同的定义属性值集合。它们可以通过类型或属性值来识别。更重要的是,正如您所看到的,我们摆脱了所有的If..ElseSwitch caseConstructor overloads。听起来怎么样?
扩展具有新属性的瓷砖
例如,需要在Tiles上添加一个新的属性/特性,例如颜色,只需在Tile类中添加一个虚拟属性Color。在构造函数中给它一个默认值。如果您的瓷砖应该是特殊颜色,则可以选择(非强制性)在子类中覆盖该属性。
引入新类型的瓷砖
只需从Tile类派生新的Tile类型并覆盖所需属性即可。

1
尽管这个设计非常聪明和纯粹的面向对象,但我还是觉得为每种类型的瓷砖创建一个新的(虽然是派生的)类是很多额外工作。不过这是一个好的回答,所以点赞+1。 - LeftRight92
1
很高兴你喜欢它!! :) - vendettamit

0
为什么不直接重载构造函数?
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

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

    public TileData(){
        IsWalkable = true;
        IsBuildSpace = false;
        ZoneType = Zone.None;
        TileGraphicIndex = 0;
    }

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

因为我不希望构造函数可以公开访问。可用的瓷砖类型是预先确定的(在编译时)。抱歉,我在原始帖子中应该更明显地表达这一点。 - LeftRight92
私有构造函数重载没有问题。 - Jerry Federspiel
因为它把你的三种解决方案中的两种都合并到了一起。 - maraaaaaaaa
我猜,但我觉得在两种创建静态实例的方式之间混合和匹配只会稍微令人困惑...我不认为这样有什么好处。 - LeftRight92

0

关于创建每种类型的瓷砖的方法,有什么想法吗?

public class Tile{
    public TileType Type { get; private set; }
    public bool IsWalkable { get; private set; }
    public bool IsBuildSpace { get; private set; }
    public Zone ZoneType { get; private set; }
    public int TileGraphicIndex { get; private set; }

    private Tile() {
    }

    public static Tile BuildTile(TileType type){
        switch (type) {
            case TileType.WALL:
                return BuildWallTile();
            case TileType.FLOOR:
                return BuildFloorTile();
            case TileType.FARMLAND:
                return BuildFarmlandTile();
            default:
                throw ArgumentException("type");
        }
    }

    public static Tile BuildWallTile()
    {
        return new Tile {
            IsWalkable = false,
            IsBuildSpace = false,
            ZoneType = Zone.None,
            TileGraphicIndex = 1,
            Type = TileType.WALL
        };
    }

    public static Tile BuildFloorTile()
    {
        return new Tile {
            IsWalkable = true,
            IsBuildSpace = None,
            ZoneType = Zone.None,
            TileGraphicIndex = 1,
            Type = TileType.FLOOR
        };
    }

    public static Tile BuildFarmlandTile()
    {
        return new Tile {
            IsWalkable = true,
            IsBuildSpace = false,
            ZoneType = Zone.Arable,
            TileGraphicIndex = 2,
            Type = TileType.FARMLAND
        };
    }

    public enum Zone{
        Shipping,
        Receiving,
        Arable,
        None
    }
    public enum TileType{
        WALL,
        FLOOR,
        FARMLAND
        //...etc
    }
}

我猜这是工厂模式,我的想法正确吗? - LeftRight92
非常相似,是的。 - Diego
除了将方法从静态实例切换到方法之外,从实际角度来看,这种方法与第3种方法(原始问题中的方法)有何不同? - LeftRight92
实际上并不是这样,我误解了那种方法。 - Diego

0

只是在Diego的回答上进行扩展,这些方法可以作为字段以保持代码的整洁性。

public class Tile{
    public TileType Type { get; private set; }
    public bool IsWalkable { get; private set; }
    public bool IsBuildSpace { get; private set; }
    public Zone ZoneType { get; private set; }
    public int TileGraphicIndex { get; private set; }
    private Tile() { }

    public static Tile BuildTile(TileType type){
        switch (type) {
        case TileType.WALL: return BuildWallTile();
        case TileType.FLOOR: return BuildFloorTile();
        case TileType.FARMLAND: return BuildFarmlandTile();
        default: throw ArgumentException("type");
        }
    }
    public static Tile wall {
        get {
            return new Tile {
                IsWalkable = false, 
                IsBuildSpace = false, 
                ZoneType = Zone.None, 
                TileGraphicIndex = 1,
                Type = TileType.WALL
             };
         }
     }
     public static Tile floor {
          get {
              return new Tile {
                  IsWalkable = true, 
                  IsBuildSpace = None, 
                  ZoneType = Zone.None, 
                  TileGraphicIndex = 1,
                  Type = TileType.FLOOR
              };
          }
     }
     public static Tile farmland {
         get {
             return new Tile {
                 IsWalkable = true, 
                 IsBuildSpace = false, 
                 ZoneType = Zone.Arable, 
                 TileGraphicIndex = 2, 
                 Type = TileType.FARMLAND
              };
          }
    }
    public enum Zone{
        Shipping,
        Receiving,
        Arable,
        None
    }
    public enum TileType{ WALL, FLOOR, FARMLAND //...etc }
}

使用方法:

Tile myWallTile = Tile.wall;
Tile myFloorTile = Tile.floor;

0

我想提出一个完全不同的(并且自认为是疯狂的)方法,与迄今为止的许多建议不同。如果你愿意完全放弃类型安全,请考虑以下内容:

public interface IValueHolder
{
    object Value {get; set;}
}

public class IsWalkable : Attribute, IValueHolder
{
    public object Value {get; set;}
    public IsWalkable(bool value)
    {
        Value = value;
    }
}

public class IsBuildSpace : Attribute, IValueHolder
{
    public object Value {get; set;}
    public IsBuildSpace(bool value)
    {
        Value = value;
    }
}

public enum Zone
{
    None,
    Arable,
}

public class ZoneType : Attribute, IValueHolder
{
    public object Value {get; set;}
    public ZoneType(Zone value)
    {
        Value = value;
    }
}

public class TileGraphicIndex : Attribute, IValueHolder
{
    public object Value {get; set;}
    public TileGraphicIndex(int value)
    {
        Value = value;
    }
}

public class TileAttributeCollector
{
    protected readonly Dictionary<string, object> _attrs;

    public object this[string key]
    {
        get
        {
            if (_attrs.ContainsKey(key)) return _attrs[key];
            else return null;
        }

        set
        {
            if (_attrs.ContainsKey(key)) _attrs[key] = value;
            else _attrs.Add(key, value);
        }
    }

    public TileAttributeCollector()
    {
        _attrs = new Dictionary<string, object>();

        Attribute[] attrs = Attribute.GetCustomAttributes(this.GetType()); 
        foreach (Attribute attr in attrs)
        {
            IValueHolder vAttr = attr as IValueHolder;
            if (vAttr != null)
            {
                this[vAttr.ToString()]= vAttr.Value;
            }
        }
    }
}

[IsWalkable(true), IsBuildSpace(false), ZoneType(Zone.Arable), TileGraphicIndex(2)]
public class FarmTile : TileAttributeCollector
{
}

使用示例:

FarmTile tile = new FarmTile();

// read, can be null.
var isWalkable = tile["IsWalkable"];

// write
tile["IsWalkable"] = false;

// add at runtime.
tile["Mom"]= "Ingrid Carlson of Norway";

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接