使用常量唯一ID创建可编写脚本对象

10
我使用Scriptable Objects来创建Unity游戏中的物品。比如说我创建了一个棍子、一把剑和一个头盔。为了使用它们,我需要将它们拖到列表中,并在游戏开始时为每个物品分配一个ID,该ID也是它在列表中的位置,因为我只需遍历列表来分配ID。现在,我可以通过使用它的索引来访问该物品,或者按照大多数关于ScriptableObjects的教程所示,我们可以将物品和ID添加到字典中以更好地访问它们。
棍子为0,剑为1,头盔为2。现在我决定用一个补丁来移除剑,这样头盔就会得到ID 1。现在每个存档都已经崩溃了,因为剑变成了头盔。
我如何处理分配固定ID的问题,以便这些ID不会改变,而且不会被其他物品占用,直到我真正想要再次分配它?当然,我可以为我创建的每个ScriptableObject硬编码ID,但正如你可以想象的那样,管理硬编码ID似乎不是一个好主意。

ID是用来引用类型还是实例? - Ted Brownlow
ID用于引用哪个ItemObject属于一个物品。物品本身包含一个ID,然后应该使用它从类似于ItemDatabase的地方获取物品基本信息。 - Sonic1305
硬编码ID为什么不是一个好主意?你有成千上万的项目吗? - frankhermes
2
实际上是的,我目前有500多个项目。 - Sonic1305
5个回答

10

以下是我的意见:

  1. 创建自定义属性“ScriptableObjectIdAttribute”,您可以在需要成为ID的属性前使用它。
  2. 制作自定义属性绘制器,以便在第一次访问检查器中的对象时初始化ID,并使其无法由检查器编辑。
  3. (可选)创建基类并从中扩展所有需要唯一ID的ScriptableObject的Id属性。

BaseScriptableObject.cs:

using System;
using UnityEditor;
using UnityEngine;

public class ScriptableObjectIdAttribute : PropertyAttribute { }

#if UNITY_EDITOR
[CustomPropertyDrawer(typeof(ScriptableObjectIdAttribute))]
public class ScriptableObjectIdDrawer : PropertyDrawer {
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
        GUI.enabled = false;
        if (string.IsNullOrEmpty(property.stringValue)) {
            property.stringValue = Guid.NewGuid().ToString();
        }
        EditorGUI.PropertyField(position, property, label, true);
        GUI.enabled = true;
    }
}
#endif

public class BaseScriptableObject : ScriptableObject {
    [ScriptableObjectId]
    public string Id;
}

Weapon.cs :

using System;
using UnityEngine;

[CreateAssetMenu(fileName = "weapon", menuName = "Items/Weapon")]
public class Weapon : BaseScriptableObject {
    public int Attack;
    public int Defence;
}

结果: 在此输入图像描述

1
你的答案真是太棒了,我自己无法达到这个水平。我修改了我的代码,使用Unity自己的GUID系统(从.meta文件): AssetDatabase.TryGetGUIDAndLocalFileIdentifier(property.serializedObject.targetObject, out guid, out local_id) - Not a privileged user
不错的想法,尽管这只对在编辑器中创建ScriptableObjects的情况有用。它不能用于在运行时实例化的SOs,这在某些情况下可能很重要。 此外,当您复制现有的ScriptableObject时,它会出现问题。您需要检查是否存在重复项。 - Meister der Magie

6
创建一个"ID"字符串变量,在可脚本化对象验证时检查其是否为空。如果为空,则创建GUID;否则,不执行任何操作。
public string UID;
        private void OnValidate()
        {
    #if UNITY_EDITOR
            if (UID == "")
            {
                UID = GUID.Generate().ToString();
                UnityEditor.EditorUtility.SetDirty(this);
            }
    #endif
        }

我是如何完成它的,同时还将脚本化对象标记为脏,这样当您重新启动Unity编辑器时就不会重置了。

您还可以进一步将UID字段设置为只读,以防止意外更改。


4
惊人的解决方案。但是,如果一个物品被复制,这将带来一大问题。这意味着重复的ID会迅速堆积。 - Ash Blue

0

这就是我最终使用的一个简单、可避免重复的方法。

public class YourObject : ScriptableObject
{
    public string UID;

    private void OnValidate()
    {
        if (string.IsNullOrWhiteSpace(UID))
        {
            AssignNewUID();
        }
    }

    private void Reset()
    {
        AssignNewUID();
    }

    public void AssignNewUID()
    {
        UID = System.Guid.NewGuid().ToString();
#if UNITY_EDITOR
        UnityEditor.EditorUtility.SetDirty(this);
#endif
    }
}

2
这是一个重复安全的方法吗?当复制一个可脚本化对象时,Reset() 方法不会被调用。 - JuZDePeche

0
基于BlueBossa的解决方案,我解决了复制安全问题。
using UnityEditor;
using UnityEngine;

public class ScriptableObjectIdAttribute : PropertyAttribute { }

#if UNITY_EDITOR
[CustomPropertyDrawer(typeof(ScriptableObjectIdAttribute))]
public class ScriptableObjectIdDrawer : PropertyDrawer {
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
        GUI.enabled = false;
        
        Object owner = property.serializedObject.targetObject;
        // This is the unity managed GUID of the scriptable object, which is always unique :3
        string unityManagedGuid = AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(owner));
        
        if (property.stringValue != unityManagedGuid) {
            property.stringValue = unityManagedGuid;
        }
        EditorGUI.PropertyField(position, property, label, true);
        
        GUI.enabled = true;
    }
}
#endif

public class ScriptableObjectWithGUID : ScriptableObject {

    [ScriptableObjectId]
    public string id;
}

复制一个ScriptableObjectWithGUID实例,直到对其进行任何更改之前,它仍然具有相同的id值,所以这是需要记住的事情。只要您不计划复制未做任何实际更改的SO,您应该没问题 - 设置并忘记。

Image of the id visible on a scriptable object


0
只是想分享一下我根据BlueBossa的回答改进的解决方案版本。它包括了SO的相等成员(我认为这是拥有唯一ID的主要原因),在属性抽屉中添加了重新生成ID的按钮,并且对缺失或重复ID进行了预构建检查。
using System;
using System.Collections.Generic;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.Build;
#endif

public class ScriptableObjectIdAttribute : PropertyAttribute
{
}

public class ScriptableObjectWithId : ScriptableObject, IEquatable<ScriptableObjectWithId>
{
    [field: ScriptableObjectId] [field: SerializeField] public string Id { get; private set; }

    public bool Equals(ScriptableObjectWithId other)
    {
        if (ReferenceEquals(null, other)) return false;
        if (ReferenceEquals(this, other)) return true;
        return Id == other.Id;
    }

    public override bool Equals(object obj)
    {
        if (ReferenceEquals(null, obj)) return false;
        if (ReferenceEquals(this, obj)) return true;
        if (obj.GetType() != GetType()) return false;
        return Equals((ScriptableObjectWithId)obj);
    }

    public override int GetHashCode()
    {
        // ReSharper disable once NonReadonlyMemberInGetHashCode
        return HashCode.Combine(Id);
    }

    public static bool operator ==(ScriptableObjectWithId left, ScriptableObjectWithId right)
    {
        return Equals(left, right);
    }

    public static bool operator !=(ScriptableObjectWithId left, ScriptableObjectWithId right)
    {
        return !Equals(left, right);
    }
}

#if UNITY_EDITOR


public class SoChecker : BuildPlayerProcessor
{
    public override int callbackOrder => -1;

    public override void PrepareForBuild(BuildPlayerContext buildPlayerContext)
    {
        var ids = new HashSet<string>();

        var guids = AssetDatabase.FindAssets("t:" + typeof(ScriptableObjectWithId));
        foreach (var guid in guids)
        {
            var path = AssetDatabase.GUIDToAssetPath(guid);
            var so = AssetDatabase.LoadAssetAtPath<ScriptableObjectWithId>(path);
            if (string.IsNullOrEmpty(so.Id))
            {
                Debug.LogError("SO doesn't have ID", so);
                throw new Exception();
            }
            if (!ids.Add(so.Id))
            {
                Debug.LogError("SO has the same ID as some other SO", so);
                throw new Exception();
            }
        }
    }
}

[CustomPropertyDrawer(typeof(ScriptableObjectIdAttribute))]
public class ScriptableObjectIdDrawer : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        if (string.IsNullOrEmpty(property.stringValue))
        {
            property.stringValue = Guid.NewGuid().ToString();
        }

        var propertyRect = new Rect(position);
        propertyRect.xMax -= 100;
        var buttonRect = new Rect(position);
        buttonRect.xMin = position.xMax - 100;

        GUI.enabled = false;
        EditorGUI.PropertyField(propertyRect, property, label, true);
        GUI.enabled = true;

        if (GUI.Button(buttonRect, "Regenerate ID"))
        {
            property.stringValue = Guid.NewGuid().ToString();
        }
    }
}
#endif

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