我有一个计算量很大的关卡生成算法。因此,调用它总是导致游戏屏幕冻结。在游戏仍继续渲染加载屏幕以指示游戏未冻结时,如何在第二个线程上放置该功能?
我有一个计算量很大的关卡生成算法。因此,调用它总是导致游戏屏幕冻结。在游戏仍继续渲染加载屏幕以指示游戏未冻结时,如何在第二个线程上放置该功能?
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/