You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Unity物品生成系统:脚本实例化后保留ScriptableObject编辑能力

嘿,这个问题我之前做物品系统的时候也碰到过!你的装饰器+蓝图+Builder思路其实挺靠谱的,Inspector只显示最后一个装饰器属性的核心问题是Unity的序列化机制——它只会默认识别最外层的对象实例,之前叠加的装饰器属性被“藏”起来了。给你几个可行的解决方案,按推荐程度排序:

1. 改用复合组件模式(最推荐)

放弃装饰器的层层包装,把不同物品类型的属性拆成独立的可序列化类,直接在ItemBlueprint里存放这些组件的实例(允许为空)。这样Unity Inspector能直接显示所有启用的属性,完全不会有覆盖问题。

示例代码:

// 定义各类型的属性类
[Serializable]
public class ConsumableProperties
{
    public string description;
    public int healAmount;
}

[Serializable]
public class EquipmentProperties
{
    public EquipmentSlot equipmentSlot; // 假设你有这个枚举
    public int attackBonus;
}

// 物品蓝图类
public class ItemBlueprint : ScriptableObject
{
    public string itemName;
    public Sprite icon;
    
    // 可空的属性组件,为空则表示物品不具备该类型能力
    public ConsumableProperties consumableProps;
    public EquipmentProperties equipmentProps;
}

// ItemBuilder逻辑调整
public static class ItemBuilder
{
    public static Item Build(ItemBlueprint blueprint)
    {
        var item = new Item(blueprint.itemName, blueprint.icon);
        
        // 添加消耗品能力
        if (blueprint.consumableProps != null)
        {
            item.AddComponent(new ConsumableComponent(blueprint.consumableProps.description, blueprint.consumableProps.healAmount));
        }
        
        // 添加装备能力
        if (blueprint.equipmentProps != null)
        {
            item.AddComponent(new EquipmentComponent(blueprint.equipmentProps.equipmentSlot, blueprint.equipmentProps.attackBonus));
        }
        
        return item;
    }
}

这种方式的优势是直观、易扩展——后续要加新的物品类型(比如任务物品、可投掷物品),只需要新增对应的[Serializable]属性类就行,完全不用改现有逻辑,Inspector里的编辑体验也和原生字段一致。

2. 用[SerializeReference]改进装饰器序列化

如果你坚持想用装饰器模式,可以借助Unity 2020.1及以上版本支持的[SerializeReference]特性,让Unity序列化并显示所有层级的装饰器属性。

示例代码:

// 基类调整
public abstract class Item
{
    public string Name { get; protected set; }
}

public abstract class ItemDecorator : Item
{
    [SerializeReference] protected Item wrappedItem;
    
    protected ItemDecorator(Item item)
    {
        wrappedItem = item;
        Name = item.Name;
    }
}

// 具体装饰器
[Serializable]
public class ConsumableDecorator : ItemDecorator
{
    public string description;
    public int healAmount;
    
    public ConsumableDecorator(Item item) : base(item) { }
}

[Serializable]
public class EquipmentDecorator : ItemDecorator
{
    public EquipmentSlot equipmentSlot;
    public int attackBonus;
    
    public EquipmentDecorator(Item item) : base(item) { }
}

// 蓝图类调整
public class ItemBlueprint : ScriptableObject
{
    public string baseItemName;
    // 直接存装饰器实例,用SerializeReference序列化
    [SerializeReference] public List<ItemDecorator> decorators;
}

使用[SerializeReference]后,Unity Inspector会显示每个装饰器的具体类型和对应的属性,你可以在蓝图里按顺序添加装饰器,编辑器会逐一展示它们的字段。不过要注意,这个特性对序列化的类有一些要求(比如必须有[Serializable]标记,不能是泛型类等),而且在Inspector里添加装饰器时需要手动选择类型。

3. 自定义Inspector编辑器

如果不想改现有代码结构,就写一个自定义Editor类,手动遍历所有装饰器并绘制它们的属性。这种方式需要你熟悉Unity的Editor API,但能完全自定义显示效果。

示例代码(简化版):

using UnityEditor;

[CustomEditor(typeof(ItemBlueprint))]
public class ItemBlueprintEditor : Editor
{
    public override void OnInspectorGUI()
    {
        base.OnInspectorGUI();
        var blueprint = target as ItemBlueprint;
        if (blueprint == null) return;

        // 假设你的蓝图里有一个获取所有已应用装饰器的方法
        var allDecorators = blueprint.GetAllAppliedDecorators();
        
        EditorGUILayout.Space();
        EditorGUILayout.LabelField("All Decorator Properties", EditorStyles.boldLabel);
        
        foreach (var decorator in allDecorators)
        {
            EditorGUILayout.Space();
            EditorGUILayout.LabelField(decorator.GetType().Name, EditorStyles.label);
            
            // 用反射获取装饰器的所有可序列化字段并绘制
            var fields = decorator.GetType().GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
            foreach (var field in fields)
            {
                var value = field.GetValue(decorator);
                // 针对不同字段类型做适配,这里简化处理
                if (field.FieldType == typeof(string))
                {
                    var newValue = EditorGUILayout.TextField(field.Name, (string)value);
                    field.SetValue(decorator, newValue);
                }
                else if (field.FieldType.IsEnum)
                {
                    var newValue = EditorGUILayout.EnumPopup(field.Name, (Enum)value);
                    field.SetValue(decorator, newValue);
                }
                else if (field.FieldType == typeof(int))
                {
                    var newValue = EditorGUILayout.IntField(field.Name, (int)value);
                    field.SetValue(decorator, newValue);
                }
            }
        }
        
        // 保存修改
        if (GUI.changed)
        {
            EditorUtility.SetDirty(blueprint);
        }
    }
}

这个方法的好处是不用动现有运行时代码,但缺点是需要维护Editor代码,而且反射绘制字段的方式可能不如原生序列化稳定,复杂类型(比如Sprite、自定义类)需要额外适配。

总的来说,最推荐第一种复合组件的方式,它最符合Unity的序列化习惯,后续维护和扩展成本最低。

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

火山引擎 最新活动