我有一个计算量很大的关卡生成算法。因此,调用它总是导致游戏屏幕冻结。在游戏仍继续渲染加载屏幕以指示游戏未冻结时,如何在第二个线程上放置该功能?
我有一个计算量很大的关卡生成算法。因此,调用它总是导致游戏屏幕冻结。在游戏仍继续渲染加载屏幕以指示游戏未冻结时,如何在第二个线程上放置该功能?
Answers:
更新: 2018年,Unity将推出C#作业系统,以减轻工作负担并利用多个CPU内核。
以下答案早于该系统。它仍然可以工作,但是现代Unity中可能有更好的选择,具体取决于您的需求。特别是,作业系统似乎解决了一些有关手动创建的线程可以安全访问的限制,如下所述。例如,开发人员尝试使用预览报告执行射线广播并并行构造网格。
我邀请有使用该工作系统经验的用户添加他们自己的答案,以反映引擎的当前状态。
过去,我曾在Unity中将线程用于重量级任务(通常是图像和几何处理),与在其他C#应用程序中使用线程没有太大不同,但有两个警告:
因为Unity使用的是.NET的较旧的子集,所以有些新的线程功能和库是我们无法立即使用的,但基础知识就在那里。
正如Almo在上面的评论中指出的那样,许多Unity类型不是线程安全的,并且如果您尝试在主线程上构造,使用甚至比较它们,它们将引发异常。注意事项:
一种常见情况是在尝试访问GameObject或Monobehaviour引用之前,先检查其是否为null。myUnityObject == null为从UnityEngine.Object派生的任何东西调用一个重载的运算符,但是System.Object.ReferenceEquals()在某种程度上可以解决此问题-请记住,使用重载将Destroy()ed GameObject进行比较等于null,但尚未将ReferenceEqual等于null。
从Unity类型读取参数通常在另一个线程上是安全的(只要您仔细检查上述空值,它就不会立即引发异常),但是请注意Philipp的警告,即主线程可能正在修改状态在阅读时。为了避免读取不一致的状态,您需要对允许哪些人修改内容和时间进行约束,这可能会导致导致难以确定的错误,因为这些错误取决于我们可以在线程之间进行的毫秒级计时。不能随意复制。
随机和时间静态成员不可用。如果需要随机性,请为每个线程创建一个System.Random实例;如果需要计时信息,请创建System.Diagnostics.Stopwatch。
Mathf函数,向量,矩阵,四元数和颜色结构在线程之间都可以正常工作,因此您可以分别进行大多数计算
创建游戏对象,附加Monobehaviours或创建/更新纹理,网格,材质等都需要在主线程上进行。过去,当我需要使用它们时,我建立了一个生产者-消费者队列,我的工作线程在其中准备原始数据(例如,大量矢量/颜色要应用于网格或纹理),主线程上的Update或Coroutine轮询数据并应用它。
在没有这些注释的情况下,这是我经常用于线程工作的模式。我不保证这是一种最佳实践风格,但是可以完成工作。(欢迎发表评论或进行改进,我知道线程是一个非常深入的主题,我只知道其基础知识)
using UnityEngine;
using System.Threading;
public class MyThreadedBehaviour : MonoBehaviour
{
bool _threadRunning;
Thread _thread;
void Start()
{
// Begin our heavy work on a new thread.
_thread = new Thread(ThreadedWork);
_thread.Start();
}
void ThreadedWork()
{
_threadRunning = true;
bool workDone = false;
// This pattern lets us interrupt the work at a safe point if neeeded.
while(_threadRunning && !workDone)
{
// Do Work...
}
_threadRunning = false;
}
void OnDisable()
{
// If the thread is still running, we should shut it down,
// otherwise it can prevent the game from exiting correctly.
if(_threadRunning)
{
// This forces the while loop in the ThreadedWork function to abort.
_threadRunning = false;
// This waits until the thread exits,
// ensuring any cleanup we do after this is safe.
_thread.Join();
}
// Thread is guaranteed no longer running. Do other cleanup tasks.
}
}
如果您不需要严格地在线程之间分配工作来提高速度,而您只是在寻找一种使其不阻塞的方式,以便让您的游戏其余部分不断发展,那么在Unity中轻巧的解决方案就是Coroutines。这些功能可以完成一些工作,然后将控制权交还给引擎以继续其工作,并在以后的时间无缝恢复。
using UnityEngine;
using System.Collections;
public class MyYieldingBehaviour : MonoBehaviour
{
void Start()
{
// Begin our heavy work in a coroutine.
StartCoroutine(YieldingWork());
}
IEnumerator YieldingWork()
{
bool workDone = false;
while(!workDone)
{
// Let the engine run for a frame.
yield return null;
// Do Work...
}
}
}
这不需要任何特殊的清理注意事项,因为引擎(据我所知)可以为您消除被破坏对象的协程。
该方法的所有局部状态在产生和恢复时都将保留,因此出于许多目的,就好像它在另一个线程上不间断地运行(但在主线程上运行具有所有便利)。您只需要确保它的每次迭代都足够短,以免不会不合理地减慢主线程的速度。
通过确保重要的操作没有被收益分开,您可以获得单线程行为的一致性-知道主线程上没有其他脚本或系统可以修改正在处理的数据。
收益率返回行为您提供了一些选择。您可以...
yield return null 在下一帧的Update()之后恢复yield return new WaitForFixedUpdate() 在下一个FixedUpdate()之后恢复yield return new WaitForSeconds(delay) 经过一定的游戏时间后恢复播放yield return new WaitForEndOfFrame() 在GUI完成渲染后恢复yield return myRequest这里myRequest是一个WWW例如,为了恢复一旦所请求的数据通过网络或光盘加载完毕。yield return otherCoroutine这里otherCoroutine是一个协程实例,之后继续otherCoroutine完成。这通常用于将yield return StartCoroutine(OtherCoroutineMethod())执行链接到一个新的协程,该协程本身可以在需要时产生。
在实验上,如果要在同一上下文中链接执行,则跳过第二步StartCoroutine并仅yield return OtherCoroutineMethod()完成写入即可达到相同的目标。
StartCoroutine如果要与第二个对象关联运行嵌套协程,则在a中进行包装可能仍然有用yield return otherObject.StartCoroutine(OtherObjectsCoroutineMethod())
...取决于您何时希望协程采取下一轮行动。
或者yield break;在协程结束之前停止协程,这可能是您return;提早使用传统方法的一种方式。
您可以将繁重的计算放入另一个线程中,但是Unity的API并不是线程安全的,您必须在主线程中执行它们。
好了,您可以在Asset Store上尝试使用此软件包,这将帮助您更轻松地使用线程。http://u3d.as/wQg您只需使用一行代码即可启动线程并安全地执行Unity API。
使用Svelto.Tasks,您可以轻松地将多线程例程的结果返回到主线程(并因此返回统一函数):
http://www.sebaslab.com/svelto-taskrunner-run-serial-and-parallel-asynchronous-tasks-in-unity3d/