I try to make an Editor for a ScriptableObject in order to use it to create an EntityArchetype and it’s kinda works…
Here my code:
using System;
using Unity.Entities;
using UnityEngine;
[CreateAssetMenu(fileName = "New Entity Setting", menuName = "EntitySetting", order = 1)]
[Serializable]
public class EntitySetting : ScriptableObject
{
[SerializeField] public ComponentType[] componentTypes = new ComponentType[0];
}
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Linq;
using Unity.Entities;
using UnityEngine;
using UnityEditor;
[CustomEditor(typeof(EntitySetting))]
public class EntitySettingEditor : Editor
{
EntitySetting entitySetting;
IEnumerable<Type> allPossibleType;
public void OnEnable()
{
entitySetting = (EntitySetting) target;
allPossibleType = Assembly.GetExecutingAssembly().GetTypes().Where(mytype => mytype.GetInterfaces().Contains(typeof(IComponentData)));
}
public override void OnInspectorGUI()
{
//Show used componentTypes
for (int i = 0; i < entitySetting.componentTypes.Length; i++)
{
GUILayout.Label(entitySetting.componentTypes[i].ToString());
}
//Show used all possible ComponentType
foreach (var type in allPossibleType)
{
if (GUILayout.Button(type.ToString()))
{
AddThisType(type);
saveData();
}
}
}
public void AddThisType(Type type)
{
List<ComponentType> componentTypesList = entitySetting.componentTypes.ToList();
//Null ref only in edit mode
componentTypesList.Add(type);
entitySetting.componentTypes = componentTypesList.ToArray();
}
void saveData(){
EditorUtility.SetDirty(entitySetting);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
}
}
But I have two problems with this:
When I hit play all my componentTypes are reset. I’m not very familliar with serialisation, do you think it’s the problem? Need I to make a serializer to save my ScriptableObject? Or maybe there is a better way to save my array of ComponentType inside my ScriptableObject?
It’s only work in Play mode. In edit mode, if I try to add a Type to my componentTypesList, I got a null ref (line 45).
But I haven’t this problem if I first set compobentTypes with the inspector in play mode and then go back to edit Mode to continue the editing. I conclude that if my Type is used somewhere it exist, so no more null ref. I’m right? What can I do in order to have a better workflow?
My guess would be that System.Type and ComponentType are reference types, so when you enter playmode the references break because its a different application domain (or something). For an easy fix instead of storing the Types themselves, I’d save them as a list of strings, where each string is the fully qualified domain name of the type you want to represent. Then use Type.GetType() at runtime to convert from FQDN to the actual System.Type instance
Thank you vestigial for the explanation and the fast respond, I will do that and post the code if I made it work. It’s a bit sad that I need to convert the type at runtime for performance reason, but it’s ok since that trick will fix my two problems
I agree parsing a string to get a type, at runtime, is a little gross. But it does keep the “configuration” in the scriptableobject and editor world, instead of putting it in code files. Personally I define archetypes in .cs files so I have access to the actual types, but it also doesn’t feel good to use C# code to configure the archetypes
Yes defining archetypes in .cs files is maybe a better choice. I wanted to do that because I initially used my EntitySetting ScriptableObject to store variable for my ComponentTypes, like the mesh of a RenderMesh or the speed for a MoveComponent. Since I found that pretty handy I wanted to extend this ScriptableObject with the archetype. Maybe I will finally simply make a reference to an archetype.cs file in the EntitySetting. But first I will try your trick
Actually it turned out to be a terrible idea, lol. I think we are supposed to use Prefabs and prefab variants. But it is possible to define and create archetypes with scriptable objects, you just have to make a scriptable object that wraps the IComponentData type, so you can drag and drop the types around.
So I have done your trick Vestigial: In order to save my types between Edit and Play I save my type.name into a string and load it on Awake with Type.GetType(“savedTypeName”).
I thought that it would be more difficult but it was pretty easy. And finally, this code is so short that it does’t bother me that much to do the convertion on the Awake.
So maybe keep things like that.
I like your warper idea too, it’s clever, but it’s a bit of pain to warp a component data everytime I create a new one. I will try to find a trick to avoid that if no one came up with a better way to save those component types.
Thank you for your help Vestigial
So, if anyone is interested, there is my new code that allow you to define an archetype directly in a scriptable object.
using System;
using System.Collections.Generic;
using Unity.Entities;
using UnityEngine;
[CreateAssetMenu(fileName = "New Entity Setting", menuName = "EntitySetting", order = 1)]
[Serializable]
public class EntitySetting : ScriptableObject
{
[SerializeField] public string[] componentTypesString = new string[0];
public ComponentType[] ComponentTypes()
{
List<ComponentType> componentTypes = new List<ComponentType>();
for (int i = 0; i < componentTypesString.Length; i++)
{
componentTypes.Add(Type.GetType(componentTypesString[i]));
}
return componentTypes.ToArray();
}
}
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Linq;
using Unity.Entities;
using UnityEngine;
using UnityEditor;
[CustomEditor(typeof(EntitySetting))]
public class EntitySettingEditor : Editor
{
EntitySetting entitySetting;
IEnumerable<Type> allPossibleType;
public void OnEnable()
{
entitySetting = (EntitySetting) target;
allPossibleType = Assembly.GetExecutingAssembly().GetTypes().Where(mytype => mytype.GetInterfaces().Contains(typeof(IComponentData)));
}
public override void OnInspectorGUI()
{
for (int i = 0; i < entitySetting.componentTypesString.Length; i++)
{
GUILayout.Label(entitySetting.componentTypesString[i].ToString());
}
foreach (var type in allPossibleType)
{
if (GUILayout.Button(type.ToString()))
{
AddThisType(type);
saveData();
}
}
}
public void AddThisType(Type type)
{
List<string> componentTypesList = entitySetting.componentTypesString.ToList();
componentTypesList.Add(type.ToString());
entitySetting.componentTypesString = componentTypesList.ToArray();
}
void saveData(){
EditorUtility.SetDirty(entitySetting);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
}
}
I also made a version using the TypeIndex instead of a string.
I think it’s maybe faster. It’s works I but there is a strange error with a null ref that looks like a lot with what I experimented before. I have this error only the first time I try to set my scriptable object in Edit mode, If I edit just one time in Play mode the error doesn’t come back again in Edit mode… So It’s kinda OK for me… Interesting isn’t it?
using System.Collections.Generic;
using Unity.Entities;
using UnityEngine;
[CreateAssetMenu(fileName = "New Entity Setting", menuName = "EntitySetting", order = 1)]
public class EntitySetting : ScriptableObject
{
public List<int> componentTypesIndex = new List<int>();
public ComponentType[] ComponentTypes()
{
ComponentType[] componentTypes = new ComponentType[componentTypesIndex.Count];
for (int i = 0; i < componentTypesIndex.Count; i++)
{
componentTypes[i] = ComponentType.FromTypeIndex(componentTypesIndex[i]);
}
return componentTypes;
}
}
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Linq;
using Unity.Entities;
using UnityEngine;
using UnityEditor;
[CustomEditor(typeof(EntitySetting))]
public class EntitySettingEditor : Editor
{
EntitySetting entitySetting;
IEnumerable<Type> allPossibleType;
public void OnEnable()
{
entitySetting = (EntitySetting) target;
allPossibleType = Assembly.GetExecutingAssembly().GetTypes().Where(mytype => mytype.GetInterfaces().Contains(typeof(IComponentData)));
}
public override void OnInspectorGUI()
{
for (int i = 0; i < entitySetting.componentTypesIndex.Count; i++)
{
GUILayout.Label(ComponentType.FromTypeIndex(entitySetting.componentTypesIndex[i]).ToString());
}
foreach (var type in allPossibleType)
{
if (GUILayout.Button(type.ToString()))
{
int componentTypeIndex = ((ComponentType) type).TypeIndex;
if (!entitySetting.componentTypesIndex.Contains(componentTypeIndex))
{
entitySetting.componentTypesIndex.Add(componentTypeIndex);
}
}
}
}
}