背景:
我正在為 Unity 構建一個編輯器擴展(盡管這個問題與 Unity 并不嚴格相關)。用戶可以從下拉串列中選擇一個二元運算,并在輸入上執行該運算,如圖所示:

該代碼是從一個教程拍攝,并使用一個列舉
問題
根據我之前用其他語言編程的經驗,以及我希望允許用戶進行不需要用戶在核心代碼中編輯 switch 陳述句的可擴展操作,我希望生成的代碼看起來像這樣(無效) C#代碼:
... snip ...
// OperatorSelection.GetSelections() is automagically populated by inheritors of the GenericOperation class
// So it would represent a collection of types?
// so the confusion is primarily around what type this should be
public GenericOperations /* ?? */ MathOperations = GenericOperation.GetOperations();
// this gets assigned by the editor when the user clicks
// the dropdown, but I'm unclear on what the type should
// be since it can be one of several types
// from the MathOperations collection
public Operation /* ?? */ operation;
public override object GetValue(NodePort port)
{
float a = GetInputValue<float>("a", this.a);
float b = GetInputValue<float>("b", this.b);
result = 0f;
result = operation(a, b);
return result;
}
... snip ...
參考行為 為了清楚地說明我希望實作的行為型別,這里是 Python 中的參考實作。
class GenericOperation:
@classmethod
def get_operations(cls):
return cls.__subclasses__()
class AddOperation(GenericOperation):
def __call__(self, a, b):
return a b
if __name__ == '__main__':
op = AddOperation()
res = op(1, 2)
print(res) # 3
print(GenericOperation.get_operations()) # {<class '__main__.AddOperation'>}
具體問題 所以最終這歸結為三個相互關聯的問題:
我分配給什么樣的型別,
MathOperations以便它可以保存 的子型別的集合GenericOperation?我如何獲得 的子型別
GenericOperation?我分配什么型別
operation,可以是幾種型別之一?
到目前為止的作業
I have been looking into generics and reflection from some of the following sources, but so far none seem to provide exactly the information I'm looking for.
- https://docs.microsoft.com/en-us/dotnet/csharp/fundamentals/types/generics
- https://igoro.com/archive/fun-with-c-generics-down-casting-to-a-generic-type/
- Using enum as generic type parameter in C#
- https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/generics/generics-and-reflection
Edit: I edited the comments in the C# psuedocode to reflect that the primary confusion boils down to what the types should be for MathOperations and operation, and to note that the editor itself selects the operation from the MathOperations when the user clicks on the dropdown. I also changed the question so that they can be answered factually.
uj5u.com熱心網友回復:
通常我會說你的問題很廣泛,用例非常棘手,需要很多不那么簡單的步驟來解決。但我看到你在研究和你的問題上也付出了很多努力,所以我會嘗試做同樣的事情(小圣誕禮物);)
一般來說,我認為泛型不是你想在這里使用的。泛型總是需要編譯時常量引數。
因為我只是在打電話,不知道我現在不能給你一個完整的解決方案,但我希望我能把你帶入正確的軌道。
1.通用介面或基類
我認為最簡單的事情寧愿是一個通用的介面,例如
public interface ITwoFloatOperation
{
public float GetResult(float a, float b);
}
一個共同的abstract基類當然也可以。(你甚至可以在方法上尋找某個屬性)
然后有一些實作,例如
public class Add : ITwoFloatOperation
{
public float GetResult(float a, float b) => a b;
}
public class Multiply : ITwoFloatOperation
{
public float GetResult(float a, float b) => a * b;
}
public class Power : ITwoFloatOperation
{
public float GetResult(float a, float b) Mathf.Pow(a, b);
}
... etc
2. 使用反射查找所有實作
然后您可以使用反射(您已經在正確的軌道上)以便自動找到該介面的所有可用實作,例如這樣
using System.Reflection;
using System.Linq;
...
var type = typeof(ITwoFloatOperation);
var types = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(s => s.GetTypes())
.Where(p => type.IsAssignableFrom(p));
3. 在 Unity 中存盤/序列化選定的型別
現在您擁有所有型別...
但是,為了在 Unity 中真正使用這些,您將需要一個額外的特殊類,它[Serializable]可以存盤一個型別,例如
[Serializable]
// See https://docs.unity3d.com/ScriptReference/ISerializationCallbackReceiver.html
public class SerializableType : ISerializationCallbackReceiver
{
private Type type;
[SerializeField] private string typeName;
public Type Type => type;
public void OnBeforeSerialize()
{
typeName = type != null ? type.AssemblyQualifiedName : "";
}
public void OnAfterDeserialize()
{
if(!string.NullOrWhiteSpace(typeName)) type = Type.GetType(typeName);
}
}
4.界面型別選擇和下拉下拉
然后,由于您不想手動輸入名稱,因此您需要一個特殊的下拉選單,其中包含實作您的界面的給定型別(您看到我們正在連接點)。
我可能會使用一個屬性,例如
[AttributeUsage(AttributeTarget.Field)]
public ImplementsAttribute : PropertyAttribute
{
public Type baseType;
public ImplementsAttribute (Type type)
{
baseType = type;
}
}
然后您可以將該欄位公開為例如
[Implements(typeof (ITwoFloatOperation))]
public SerializableType operationType;
and then have a custom drawer. This depends of course on your needs. Honestly my editor scripting knowledge is more based on MonoBehaviour etc so I just hope you can somehow translate this into your graph thingy.
Something like e.g.
[CustomPropertyDrawer(typeof(ImplementsAttribute))]
public class ImplementsDrawer : PropertyDrawer
{
// Return the underlying type of s serialized property
private static Type GetType(SerializedProperty property)
{
// A little bit hacky we first get the type of the object that has this field
var parentType = property.serializedObject.targetObject.GetType();
// And then once again we use reflection to get the field via it's name again
var fi = parentType.GetField(property.propertyPath);
return fi.FieldType;
}
private static Type[] FindTypes (Type baseType)
{
var type = typeof(ITwoFloatOperation);
var types = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(s => s.GetTypes())
.Where(p => type.IsAssignableFrom(p));
return types.OrderBy(t => t.AssemblyQualifiedName).ToArray();
}
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
label = EditorGUI.BeginProperty(position, label, property);
var implements = attribute as ImplementsAttribute;
if (GetType(property) != typeof (SerializableType))
{
EditorGUI.HelpBox(position, MessageType.Error, "Implements only works for SerializableType!");
return;
}
var typeNameProperty = property.FindPropertyRelative("typeName");
var options = FindTypes (implements.baseType);
var guiOptions = options.Select(o => o.AssemblyQualifiedName).ToArray();
var currentType = string.IsNullOrWhiteSpace(typeNameProperty.stringValue) ? null : Type.GetType(typeNameProperty.stringValue);
var currentIndex = options.FindIndex(o => o == curtentType);
var newIndex = EditorGUI.Popup(position, label.text, currentIndex, guiOptions);
var newTypeName = newIndex >= 0 ? options[newIndex] : "";
property.stringValue = newTypeName;
EditorGUI.EndProperty();
}
}
5. Using the type to create an instance
Once you somehow can store and get the desired type as a last step we want to use it ^^
Again the solution would be reflection and the Activator which allows us to create an instance of any given dynamic type using Activator.CreateInstance
so once you have the field you would e.g. do
var instance = (ITwoFloatOperation) Activator.CreateInstance(operationType.Type));
var result = instance.GetResult(floatA, floatB);
Once all this is setup an working correctly ( ^^ ) your "users"/developers can add new operations as simple as implementing your interface.
Alternative Approach - "Scriptable Behaviors"
Thinking about it further I think I have another - maybe a bit more simple approach.
This option is maybe not what you were targeting originally and is not a drop-down but we will rather simply use the already existing object selection popup for assets!
You could use something I like to call "Scriptable Behaviours" and have a base ScriptableObject like
public abstract class TwoFloatOperation : ScriptableObject
{
public abstract float GetResult(float a, float b);
}
And then multiple implementations (note: all these have to be in different files!)
[CreateAssetMenu (fileName = "Add", menuName = "TwoFloatOperations/Add")]
public class Add : TwoFloatOperation
{
public float GetResult(float a, float b) => a b;
}
[CreateAssetMenu (fileName = "Multiply", menuName = "TwoFloatOperations/Multiply")]
public class Multiply : TwoFloatOperation
{
public float GetResult(float a, float b) => a * b;
}
[CreateAssetMenu (fileName = "Power", menuName = "TwoFloatOperations/Power"]
public class Power : TwoFloatOperation
{
public float GetResult(float a, float b) Mathf.Pow(a, b);
}
Then you create one instance of each vis the ProjectView -> Right Click -> Create -> TwoFloatOperations
Once you did this for each type you can simply expose a field of type
public TwoFloatOperation operation;
and let Unity do all the reflection work to find instances which implement this in the assets.
You can simply click on the little dot next to the object field and Unity will list you all available options and you can even use the search bar to find one by name.
Advantage:
- 不需要臟、昂貴和容易出錯的反射
- 基本上都是基于編輯器已經內置的功能 -> 減少對序列化等的擔憂
壞處:
- 這與背后的實際概念有點
ScriptableObject不同,因為通常會有多個具有不同設定的實體,而不僅僅是一個 - 如您所見,您的開發人員不僅必須繼承某種型別,還必須額外添加
CreateAssetMenu屬性并實際創建一個實體才能使用它。
如上所述,在電話上輸入此內容,但我希望這對您的用例有所幫助,并讓您了解我將如何處理此問題
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/398647.html
