最新文章
泰课在线 | 微信拼团成功后如何获取课程?
08-09 17:57
Unity教程 | 使用ARKit为iOS开发AR应用
07-31 17:23
Unity Pro专业版7折订阅四选一工具包之VR开发与艺术设计
07-28 11:47
网友使用虚幻UE4实现CAVE 多通道立体渲染的沉浸式环境
07-27 11:57
VR晕动症调查:未来5年内大部分VR晕动症将得到解决
07-27 11:26
AMD CEO:未来3-5年最重要 希望5年达1亿VR用户
07-27 10:44
如何实现Unity编辑器中的协程
Unity编辑器中何时需要协程
在定制Unity编辑器时,常常需要启动额外的协程或线程来处理任务。例如,在执行界面更新操作时,可能会涉及大量计算。当用户不断修改某个参数,如从1变化到2,这个过程会经历无数中间步骤,调用多次Update方法。如果直接在Update方法中不断刷新界面,很容易导致界面卡死。此时,在一个协程中进行优化,只保留用户最后一次参数修正,省去中间步骤,能显著改善这种情况。这既属于Unity编辑器开发的内容,也涉及性能优化,这里将其归类到优化部分进行讨论。
解决问题思路
在Unity官网的questions板块,有很多人搜索如何在Unity编辑器中实现协程的问题。后来有人提到了一个关键方法:EditorApplication.update。通过将需要执行的协程传递给这个方法,就可以在编辑器环境下实现自动循环调用。
老外的实现方式
下面是找到的一位老外实现的代码:
using UnityEngine;
using UnityEditor;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
public static class EditorCoroutineRunner
{
private class EditorCoroutine : IEnumerator
{
private Stack<IEnumerator> executionStack;
public EditorCoroutine(IEnumerator iterator)
{
this.executionStack = new Stack<IEnumerator>();
this.executionStack.Push(iterator);
}
public bool MoveNext()
{
IEnumerator i = this.executionStack.Peek();
if (i.MoveNext())
{
object result = i.Current;
if (result != null && result is IEnumerator)
{
this.executionStack.Push((IEnumerator)result);
}
return true;
}
else
{
if (this.executionStack.Count > 1)
{
this.executionStack.Pop();
return true;
}
}
return false;
}
public void Reset()
{
throw new System.NotSupportedException("This Operation Is Not Supported.");
}
public object Current
{
get { return this.executionStack.Peek().Current; }
}
public bool Find(IEnumerator iterator)
{
return this.executionStack.Contains(iterator);
}
}
private static List<EditorCoroutine> editorCoroutineList;
private static List<IEnumerator> buffer;
public static IEnumerator StartEditorCoroutine(IEnumerator iterator)
{
if (editorCoroutineList == null)
{
editorCoroutineList = new List<EditorCoroutine>();
}
if (buffer == null)
{
buffer = new List<IEnumerator>();
}
if (editorCoroutineList.Count == 0)
{
EditorApplication.update += Update;
}
// add iterator to buffer first
buffer.Add(iterator);
return iterator;
}
private static bool Find(IEnumerator iterator)
{
// If this iterator is already added
// Then ignore it this time
foreach (EditorCoroutine editorCoroutine in editorCoroutineList)
{
if (editorCoroutine.Find(iterator))
{
return true;
}
}
return false;
}
private static void Update()
{
// EditorCoroutine execution may append new iterators to buffer
// Therefore we should run EditorCoroutine first
editorCoroutineList.RemoveAll(
coroutine => { return coroutine.MoveNext() == false; }
);
// If we have iterators in buffer
if (buffer.Count > 0)
{
foreach (IEnumerator iterator in buffer)
{
// If this iterators not exists
if (!Find(iterator))
{
// Added this as new EditorCoroutine
editorCoroutineList.Add(new EditorCoroutine(iterator));
}
}
// Clear buffer
buffer.Clear();
}
// If we have no running EditorCoroutine
// Stop calling update anymore
if (editorCoroutineList.Count == 0)
{
EditorApplication.update -= Update;
}
}
}
使用方法是在自己的类的Start方法中稍作修改,并增加一个协程函数,示例代码如下:
void Start()
{
rope = gameObject.GetComponent<QuickRope>();
#if UNITY_EDITOR
//调用方法
EditorCoroutineRunner.StartEditorCoroutine(OnThreadLoop());
#endif
}
public IEnumerator OnThreadLoop()
{
while (true)
{
Debug.Log("Looper");
yield return null;
}
}
建议加上#if UNITY_EDITOR预处理指令。这个类基本能满足需求。不过,需要注意的是,脚本需要先执行到Start方法。可能需要将GameObject做成Prefab,然后从场景中删除,再将Prefab拖回场景,才能在编辑状态下触发脚本的Start方法,从而启动循环。
存在的问题及改进
使用上述代码一段时间后,会发现一些问题。一旦循环开始,就无法停止,即使将GameObject从场景中删除或隐藏也没有效果。为了解决这些问题,并简化脚本,我对其进行了重写,代码如下:
using UnityEngine;
using UnityEditor;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
public static class EditorCoroutineLooper
{
private static Dictionary<IEnumerator, MonoBehaviour> m_loopers = new Dictionary<IEnumerator, MonoBehaviour>();
private static bool M_Started = false;
/// <summary>
/// 开启Loop
/// </summary>
/// <param name="mb">脚本</param>
/// <param name="iterator">方法</param>
public static void StartLoop(MonoBehaviour mb, IEnumerator iterator)
{
if (mb != null && iterator != null)
{
if (!m_loopers.ContainsKey(iterator))
{
m_loopers.Add(iterator, mb);
}
else
{
m_loopers[iterator] = mb;
}
}
if (!M_Started)
{
M_Started = true;
EditorApplication.update += Update;
}
}
private static List<IEnumerator> M_DropItems = new List<IEnumerator>();
private static void Update()
{
if (m_loopers.Count > 0)
{
var allItems = m_loopers.GetEnumerator();
while (allItems.MoveNext())
{
var item = allItems.Current;
var mb = item.Value;
//卸载时丢弃Looper
if (mb == null)
{
M_DropItems.Add(item.Key);
continue;
}
//隐藏时别执行Loop
if (!mb.gameObject.activeInHierarchy)
{
continue;
}
//执行Loop,执行完毕也丢弃Looper
IEnumerator ie = item.Key;
if (!ie.MoveNext())
{
M_DropItems.Add(item.Key);
}
}
//集中处理丢弃的Looper
for (int i = 0; i < M_DropItems.Count; i++)
{
if (M_DropItems[i] != null)
{
m_loopers.Remove(M_DropItems[i]);
}
}
M_DropItems.Clear();
}
if (m_loopers.Count == 0)
{
EditorApplication.update -= Update;
M_Started = false;
}
}
}
调用方法从原来的:
EditorCoroutineRunner.StartEditorCoroutine(OnThreadLoop());
改为现在的:
EditorCoroutineLooper.StartLoop(this, OnThreadLoop());
使用这个脚本时,需要传递两个参数,一个是自己的脚本,另一个是协程函数。其原理是代码会检测脚本的状态,当脚本关闭或卸载时,会自动停止循环调用。
通过上述改进,在Unity编辑器中实现协程的功能更加完善,使用起来也更加方便。