如何使用脚本对象创建动态变量系统

时间:2019-01-09 14:52:33

标签: c# unity3d

this talk中,我学习了如何使用可编写脚本的对象创建变量,如何创建FloatVariable,DoubleVariable,StringVariable等类。但是,在同一个谈话中,那个家伙说他使用了一个更具动态性的变量系统,从而阻止了创建多个类来处理所有变量类型。

在第一个系统中,我有一个名为ImageFillSetter的C#脚本,该脚本提供了两个float变量和一个Image脚本,它将两个变量的除法返回到图像的fillAmount变量。

但是,当我得到一个Double变量,并且想要使用该值设置进度条时,我需要创建另一个名为ImageFillSetterDouble的脚本,并放入这些变量。如果我需要使用Integers创建一个?每次创建这样的脚本时,都需要创建两个副本来处理其他数字变量类型? 使用此动态变量系统,应该可以解决此问题,但是我不知道如何启动/创建此系统。

代码如下:

[CreateAssetMenu(menuName="Variable/Float")]
public class FloatVariable : ScriptableObject, ISerializationCallbackReceiver
{
    public float initialValue;
    [NonSerialized] 
    public float value;

    public void OnAfterDeserialize()
    {
        value = initialValue;
    }

    public void OnBeforeSerialize() { }
}

我想要的是这样的东西(完全假设,我知道这是行不通的)

[CreateAssetMenu(menuName="Variable")]
public class Variable : ScriptableObject, ISerializationCallbackReceiver
{
    public var initialValue;
    [NonSerialized] 
    public var value;

    public void OnAfterDeserialize()
    {
        value = initialValue;
    }

    public void OnBeforeSerialize() { }
}

2 个答案:

答案 0 :(得分:0)

看看Generics

有一个像这样的抽象类

public abstract class ValueAsset<T> : ScriptableObject
{
    public T value;

    // Add your methods

    // Here some more examples also using the T value. They might also be abstract but they don't have to be

    // return a T
    public T GetValue()
    {
        return value;
    }

    // or pass a T
    public void SetValue(T input)
    {
        value = input;
    }
}

您永远不会实例化该类,但是现在可以从中派生多个实现,例如

[CreateAssetMenu(fileName = "new int", menuName = "ValueAssets/int")]
public class IntValue : ValueAsset<int>
{
    // Maybe constructors here or additional fields and methods
}

[CreateAssetMenu(fileName = "new float", menuName = "ValueAssets/float")]
public class FloatValue : ValueAsset<float>
{
    // Maybe constructors here or additional fields
}

您还可以具有多个通用值,例如

public abstract class OtherExample<TKey, TValue> : ScriptableObject
{
    // Note that this is just an example
    // Dictionary is not serializable
    public Dictionary<TKey, TValue> values = new Dictionary<TKey, TValue>();

    public void AddPair(TKey key, TVakue value)
    {
        values.Add(key, value);
    }
}

并实现类似

public OneImplementation : OtherExample<string, GameObject>
{
    //...
}

将其用于参考值(组件,GameObject等)的方式相同


因此,对于IntValue,方法GetValue将返回int,而SetValue将以int作为参数。他们采用相同的方式在float中返回FloatValue


ImageFillSetter<T>做同样的事情,您可以使方法abstract并为不同的T值实现不同的行为(例如不同的解析等)

注意:我不知道为什么,但是过去我注意到了

public ValueAsset<T> valueAsset;
即使稍后实现,

也不会在检查器中序列化,因此您必须在实现中使用正确的类型来实现字段。您仍然可以在运行时覆盖它,但是如果不需要它,可以跳过整个FetchValue部分,而无论如何都使用valueReference-只是为了完整性而添加了它。

public abstract class ImageFillSettet<T> : MonoBehaviour
{
    // Will not appear in the Inspector
    public ValueAsset<T> ValueAsset;

    // Override this in implementation
    protected abstract void FetchValue();

    // Use it for Initializing the value
    private void Awake ()
    {
        FetchValue();
    }

    public abstract void SetFill();   
}

晚一点

public class ImageFillSetterFloat : ImageFillSetter<float>
{
    // Show in the inspector
    [SerializeField] private FloatValue valueReference;

    // Provide the reference to the base class
    protected override void Fetch value()
    {
        valueAsset = valueReference;
    }

    public override void SetFill()
    {
        // Use valueReference for something
    }
}

答案 1 :(得分:0)

我知道有一个可以接受的答案,但是我觉得链接视频中描述的ScriptableObject变量的用法被误解了。

我认为您最好将FloatVariable设置为与计算无关。

比方说,该计算是针对玩家健康状况的,您的填充值将由currentHealth/maxHealth计算。

public class PlayerHealth: MonoBehaviour
{
    [SerializeField] private FloatVariable floatReference;
    [SerializeField] private float maxHealth;
    [SerializeField] private float currentHealth;

    void Update()
    {
        this.floatReference.value = currentHealth/maxHealth;
    }
}


public class ImageFillSetter: MonoBehaviour
{
     [SerializeField] private FloatVariable floatReference;
     [SerializeField] private Image imageReference;

     void Update()
    {
        this.imageReference.fill = this.floatReference.value;
    }
}

或者说,玩家的健康状况存储为两倍:

public class PlayerHealth: MonoBehaviour
{
    [SerializeField] private FloatVariable floatReference;
    [SerializeField] private double maxHealth;
    [SerializeField] private double currentHealth;

    void Update()
    {
        this.floatReference.value = (float)(currentHealth/maxHealth);
    }
}

现在假设您添加了一个输入字段,可在其中输入填充值作为百分比字符串(例如“ 76”):

public class FillInput: MonoBehaviour
{
    [SerializeField] private FloatVariable floatReference;
    [SerializeField] private Input input;

    void Update()
    {
        if(Input.GetKeyDown(KeyCode.Enter))
        {
            this.floatReference.value = float.Parse(input.text)/100f;
        }
    }
}

ImageFillSetter将“观察” FloatVariable,而无需知道该浮点数是如何计算的。

这样,您只需拥有一个可用于任何图像和任何数据源的ImageFillSetter,同时具有一种或多种更改填充的方式,而无需对ImageFillSetter进行任何更改。

例如,假设您要使用相同的方法来指示异步级别的加载进度:

public class FillInput: MonoBehaviour
{
    [SerializeField] private FloatVariable floatReference;
    private AsyncOperation loadOperation;

    void LoadLevelAsync(string levelName)
    {
        this.loadOperation = SceneManager.LoadLevelAsync(levelName, LoadSceneMode.Additive);
    }

    void Update()
    {
        this.floatReference.value = this.loadOperation?.progress ?? 0;
    }
}

只要您的ImageFillSetter引用相同的FloatVariable,这将无需进行任何其他更改即可工作。

将FloatVariable(或您拥有的任何原语,例如DoubleVariable)视为存储在数据库中的值。任何人都可以读取该值,任何人都可以保存一个新值。将所有可能的值计算结果存储在数据库中,而不是进行计算并仅存储答案,这很奇怪。

这不会改变您需要为每个原语编写可脚本实现的事实:

  • FloatVariable
  • DoubleVariable
  • StringVariable
  • BoolVariable

但是您只需要一个,如derHugo答案的第一部分所示。