基于Odin的Unity编辑器工具开发
Odin 是一个非常好用的Unity Editor工具开发框架,非常简洁,不过会被打入游戏包体内部
Odin使用起来非常简单,使用一些Attribute就可以暴露参数、按钮、生命周期函数,于是这里没有Odin基础教程,大部分是我自己的理解
插件化示例
需求:插件化
这是一个使用C# Attribute自动注册窗口的示例,通过对类进行标注,就可以自动添加到MenuWindow上,不需要改动Menu代码
你可以将这些文件打包成程序集(DLL),选择性加载,以此实现插件化
Attribute
Attribute是C#一个非常好用的功能,可以非常便捷地标注一个类
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false) ] public class PanelAttribute : Attribute { public string Name { get ; } public PanelAttribute (string name ) { Name = name; } }
通过添加新的Attribute类,可以分别添加到不同的Menu上
通过在Attribute中添加属性,可以存储更新信息,比如EditorIcon
具体的Panel
[Panel("Ragdoll" ) ] public class RagdollPanel { [Title("Properties" ) ] [ShowInInspector ] public int AATime = 30 ; }
[Panel("Bake" ) ] public class BakePanel { [Title("Properties" ) ] [ShowInInspector ] public int test = 30 ; }
这里遍历的程序集中所有被PanelAttribute
标注的类,创建出这些类的对象,并提取出Attribute内容
OdinMenuTree
将以侧边栏+内容的形式展示所有的窗口
MenuItem
用于在Editor顶部注册按钮,以便打开这个Menu
public class MainMenu : OdinMenuEditorWindow { protected override OdinMenuTree BuildMenuTree () { var tree = new OdinMenuTree(); var pluginTypes = Assembly.GetExecutingAssembly().GetTypes() .Where(type => type.GetCustomAttributes(typeof (PanelAttribute), true ).Length > 0 ); foreach (var pluginType in pluginTypes) { var attribute = (PanelAttribute)Attribute.GetCustomAttribute(pluginType, typeof (PanelAttribute)); var instance = Activator.CreateInstance(pluginType); tree.Add(attribute.Name, instance); } return tree; } [MenuItem("Tools/Baker Menu" ) ] private static void OpenWindow () { var window = GetWindow<MainMenu>(); window.position = GUIHelper.GetEditorWindowRect().AlignCenter(800 , 600 ); } }
监听Http请求
需求:使用Http请求操控Editor
[Panel("Web" ) ] public class WebPanel { private Thread workerThread; private bool stopFlag; private HttpListener listener; [OnInspectorInit ] void Start () { Debug.Log("init" ); stopFlag = false ; listener = new HttpListener(); listener.Prefixes.Add ("http://localhost:7863/" ); listener.AuthenticationSchemes = AuthenticationSchemes.Anonymous; listener.Start (); workerThread = new Thread(DoWork); workerThread.Start(); } [OnInspectorDispose ] void End () { Debug.Log("dispose" ); stopFlag = true ; listener.Close(); } private void DoWork () { while (!stopFlag) { var result = listener.BeginGetContext(ListenerCallback, listener); Thread.Sleep(500 ); } } public class ActionInput { public string Arg { get ; set ; } } private void ListenerCallback (IAsyncResult result ) { var context = listener.EndGetContext (result); Debug.Log ("Method: " + context.Request.HttpMethod); string url = context.Request.Url.LocalPath.ToString(); Debug.Log ("LocalUrl: " + context.Request.Url.LocalPath); using (var reader = new StreamReader(context.Request.InputStream, context.Request.ContentEncoding)) { string json = reader.ReadToEnd(); var data = JsonConvert.DeserializeObject<ActionInput>(json); Debug.Log("Data: " + data.Arg); } if (url == "/test" ) { var ro = new { message = "This is the response." , timestamp = DateTime.Now }; string jsonResponse = JsonConvert.SerializeObject(ro); byte [] buffer = System.Text.Encoding.UTF8.GetBytes(jsonResponse); context.Response.ContentType = "application/json" ; context.Response.ContentLength64 = buffer.Length; context.Response.OutputStream.Write(buffer, 0 , buffer.Length); } context.Response.Close(); } }
请求测试
import requestsurl = "http://localhost:7863/test" data = { "Arg" : "这是一段测试文本" , } response = requests.post(url, json=data) if response.status_code == 200 : print (response.json()) else : print ("Request failed with status code:" , response.status_code)
输出
{ 'message': 'This is the response.', 'timestamp': '2024 -06 -17 T14: 43 : 25.1490146 +08 : 00 '}
访问主线程
Web服务是跑在一个单独线程中,Unity Editor跑在主线程,于是Web服务无法调用很多API,可以使用delayCall
EditorApplication.delayCall += () => { EditorApplication.isPlaying = true ; };
但是这个API还是有问题:Unity Editor在后台时,是不会刷新UI的,导致你必须点一下Editor窗口,或者一直保持在Editor窗口,才能正常运行
下面是另一个方法,我感觉更好
[Panel("Ragdoll" ) ] public class RagdollPanel { private static SynchronizationContext mainThreadContext; [OnInspectorInit ] void Start () { mainThreadContext = SynchronizationContext.Current; ... } ... private void ListenerCallback (IAsyncResult result ) { mainThreadContext.Post(_ => { EditorApplication.isPlaying = true ; }, null ); } }