You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Unity中Dice与DiceSet类设计:单元素集合等价于单个对象的实现

骰子系统设计疑问

我想在Unity里实现以下需求,但不确定可行性:

  • 创建Dice类和DiceSet类,DiceSet包含List<Dice>,希望仅含单个骰子的DiceSet能和Dice隐式等价。有没有办法在列表只有一个元素时返回该元素,否则返回列表(比如用Linq)?
  • 当前Dice和DiceSet没有继承关系,我考虑让Dice继承自DiceSet并限制集合内骰子数量为1,但担心单个Dice会在不该被当作DiceSet的场景中被误判,是不是我想多了?
  • 另外希望两个Dice相加能生成DiceSet,是不是应该让DiceSet包含List<DiceSet>?
  • 补充:我希望能从字符串解析生成骰子(比如"3d6+1"会创建包含3个6面骰子且带+1修正值的DiceSet)。

后来我考虑放弃DiceSet的概念,让Dice自身包含集合,哪怕只有一个骰子也还是Dice类。下面是我的设计示例,请问这个思路对吗?

public class Dice : IDice, IRollable, IModifiable
{
    // 存储每种骰子的数量
    public Dictionary<Dice, int> Set { get; set; }

    // 实现IDice接口
    public int NumSides => SideValues.Count;
    public List<int> SideValues { get; private set; }

    // 实现IModifiable接口
    public int Modifier { get; set; }
    public int ModifiedValue => RolledValue + Modifier;

    // 实现IRollable接口
    public int RolledValue { get; protected set; }
    public void Roll() {
        RolledValue = SideValues[Random.Range(0, NumSides + 1)];
    }

    // Dice构造函数
    public Dice(int sides = 6, int quantity = 1, int modifier = 0) {
        SideValues = new List<int>(Math.Max(sides, 1));
        for(int i = 1; i < NumSides + 1; i++) {
            SideValues.Add(i);
        }
        Modifier = modifier;
        AddDice(this, quantity);
    }

    // 向集合中添加骰子
    public void AddDice(Dice dice, int quantity){
        if(Set.ContainsKey(dice))
            Set[dice] += quantity;
        else
            Set.Add(dice, quantity);
    }

    // 字符串转换
    public override string ToString() {
        // 待实现
    }
    public void FromString(string diceString){
        // 待实现
    }
}

思路点评与优化建议

你的思路方向是对的,用单一Dice类同时承载单个骰子和骰子集合的逻辑,能简化类型转换的问题,但当前代码存在几个关键问题需要修正:

  1. 循环依赖问题:Dictionary<Dice, int> Set用Dice作为键,会导致构造函数调用AddDice(this, quantity)时,尝试把当前未完全初始化的Dice实例作为键存入集合,引发逻辑混乱;而且Dice默认的相等性判断是引用相等,后续添加相同规格的骰子会被当成不同键存储。

    优化方案:单独定义DiceSpec类存储骰子核心规格(面数、面值),用它作为Dictionary的键,避免循环依赖:

    public class DiceSpec
    {
        public int NumSides { get; }
        public List<int> SideValues { get; }
    
        public DiceSpec(int sides)
        {
            NumSides = Math.Max(sides, 1);
            SideValues = Enumerable.Range(1, NumSides).ToList();
        }
    
        // 重写相等判断和哈希值,确保相同规格的骰子被视为同一键
        public override bool Equals(object obj) => obj is DiceSpec spec && NumSides == spec.NumSides;
        public override int GetHashCode() => NumSides.GetHashCode();
    }
    

    随后将Dice类里的Set改为Dictionary<DiceSpec, int>。

  2. Roll方法逻辑错误:Random.Range(0, NumSides + 1)的上限是开区间,会导致索引越界(SideValues的最大索引是NumSides-1),应改为Random.Range(0, NumSides)。

  3. 构造函数初始化问题:Set字段未在构造函数中初始化,调用AddDice时会抛出空引用异常,需在构造函数开头添加Set = new Dictionary<DiceSpec, int>();。

  4. 单个骰子与集合的逻辑统一:当前RolledValue只记录单个骰子的结果,当Dice作为集合时,需要修改Roll方法遍历所有骰子规格计算总和:

    public void Roll()
    {
        RolledValue = 0;
        foreach(var pair in Set)
        {
            var spec = pair.Key;
            var count = pair.Value;
            for(int i = 0; i < count; i++)
            {
                RolledValue += spec.SideValues[Random.Range(0, spec.NumSides)];
            }
        }
    }
    
  5. 加法逻辑实现:重载+运算符,让两个Dice实例相加生成新的Dice集合:

    public static Dice operator +(Dice a, Dice b)
    {
        var result = new Dice();
        // 复制a的骰子集合
        foreach(var pair in a.Set)
        {
            result.AddDice(pair.Key, pair.Value);
        }
        // 复制b的骰子集合
        foreach(var pair in b.Set)
        {
            result.AddDice(pair.Key, pair.Value);
        }
        // 合并修正值
        result.Modifier = a.Modifier + b.Modifier;
        return result;
    }
    
  6. 字符串解析逻辑:FromString方法可以用正则表达式匹配(\d*)d(\d+)([+-]\d+)?格式的字符串,拆分出骰子数量、面数、修正值,再生成对应的Dice实例。

另外,关于你最初考虑的Dice继承DiceSet的方案,确实可能带来类型混淆——比如在需要单个骰子的场景中传入了集合类型,反而增加逻辑复杂度,所以用单一Dice类承载集合逻辑的方案更简洁。


内容的提问来源于stack exchange,提问作者Martin Bocanegra

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.03 11:31:06