抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

基于Odin的Unity编辑器工具开发

Odin是一个非常好用的Unity Editor工具开发框架,非常简洁,不过会被打入游戏包体内部

Odin使用起来非常简单,使用一些Attribute就可以暴露参数、按钮、生命周期函数,于是这里没有Odin基础教程,大部分是我自己的理解

插件化示例

需求:插件化

这是一个使用C# Attribute自动注册窗口的示例,通过对类进行标注,就可以自动添加到MenuWindow上,不需要改动Menu代码

你可以将这些文件打包成程序集(DLL),选择性加载,以此实现插件化

MainMenu

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)
{
// Debug.Log("working...");
var result = listener.BeginGetContext(ListenerCallback, listener);
Thread.Sleep(500);
}
}

// 用于反序列化request中的json
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);
// 读请求中的json
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);
}
// 返回一个json
if (url == "/test")
{
// 匿名类型(Anonymous Type)
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 requests

url = "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-17T14: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);
}
}

评论