【问题标题】:Proper way to execute a long task as to not affect frame rate in Unity在 Unity 中执行长时间任务以不影响帧速率的正确方法
【发布时间】:2018-06-13 15:55:01
【问题描述】:

我有一个在游戏开始时发生的反序列化任务。我基本上需要从持久路径中提取一些图像并从中创建一堆资产。图像可以很大(10-50MB)并且可以有很多,所以基本上这可以让我的框架永远冻结在单个任务上。我尝试使用Coroutines,但我可能会误解如何正确使用它们。

由于协程确实是单线程的,它们不会让我在 UI 运行时完成创建这些资产。我也不能只创建一个新线程来完成这项工作,并在完成回调后跳回主线程,因为 Unity 不允许我访问他们的 API(我正在创建 Texture2D、Button()、父对象等.)。

我该怎么办?我真的需要创建一个庞大的IEnumerable 函数并每隔一行代码放置一堆yield return null 吗?这似乎有点过分了。有没有办法调用需要访问 Unity 中的主线程的耗时方法,并让 Unity 根据需要将其分散到尽可能多的帧中,这样它就不会阻塞 UI?

这是Deserialize 方法的示例:

public IEnumerator Deserialize()
    {
        // (Konrad) Deserialize Images
        var dataPath = Path.Combine(Application.persistentDataPath, "Images");
        if (File.Exists(Path.Combine(dataPath, "images.json")))
        {
            try
            {
                var images = JsonConvert.DeserializeObject<Dictionary<string, Item>>(File.ReadAllText(Path.Combine(dataPath, "images.json")));
                if (images != null)
                {
                    foreach (var i in images)
                    {
                        if (!File.Exists(Path.Combine(dataPath, i.Value.Name))) continue;

                        var bytes = File.ReadAllBytes(Path.Combine(dataPath, i.Value.Name));
                        var texture = new Texture2D(2, 2);
                        if (bytes.Length <= 0) continue;
                        if (!texture.LoadImage(bytes)) continue;

                        i.Value.Texture = texture;
                    }
                }

                Images = images;
            }
            catch (Exception e)
            {
                Debug.Log("Failed to deserialize Images: " + e.Message);
            }
        }

        // (Konrad) Deserialize Projects.
        if (Projects == null) Projects = new List<Project>();
        if (File.Exists(Path.Combine(dataPath, "projects.json")))
        {
            try
            {
                var projects = JsonConvert.DeserializeObject<List<Project>>(File.ReadAllText(Path.Combine(dataPath, "projects.json")));
                if (projects != null)
                {
                    foreach (var p in projects)
                    {
                        AddProject(p);
                        foreach (var f in p.Folders)
                        {
                            AddFolder(f, true);
                            foreach (var i in f.Items)
                            {
                                var image = Images != null && Images.ContainsKey(i.ParentImageId)
                                    ? Images[i.ParentImageId]
                                    : null;
                                if (image == null) continue;

                                i.ThumbnailTexture = image.Texture;

                                // (Konrad) Call methods that would normally be called by the event system
                                // as content is getting downloaded.
                                AddItemThumbnail(i, true); // creates new button
                                UpdateImageDescription(i, image); // sets button description
                                AddItemContent(i, image); // sets item Material
                            }
                        }
                    }
                }
            }
            catch (Exception e)
            {
                Debug.Log("Failed to deserialize Projects: " + e.Message);
            }
        }

        if (Images == null) Images = new Dictionary<string, Item>();

        yield return true;
    }

所以这需要大约 10 秒才能完成。它需要反序列化驱动器中的图像,创建按钮资产,设置一堆育儿关系等。我会很感激任何想法。

附言。我还没有更新到实验性的 .NET 4.6,所以我还在使用 .NET 3.5。

好的,阅读下面的 cmets,我想我可以尝试一下。我将 IO 操作放入不同的线程中。他们不需要 Unity API,所以我可以完成这些并存储 byte[] 并在完成后将字节加载到纹理中。试试看:

public IEnumerator Deserialize()
    {
        var dataPath = Path.Combine(Application.persistentDataPath, "Images");
        var bytes = new Dictionary<Item, byte[]>();
        var done = false;
        new Thread(() => {
            if (File.Exists(Path.Combine(dataPath, "images.json")))
            {
                var items = JsonConvert.DeserializeObject<Dictionary<string, Item>>(File.ReadAllText(Path.Combine(dataPath, "images.json"))).Values;
                foreach (var i in items)
                {
                    if (!File.Exists(Path.Combine(dataPath, i.Name))) continue;

                    var b = File.ReadAllBytes(Path.Combine(dataPath, i.Name));
                    if (b.Length <= 0) continue;

                    bytes.Add(i, b);
                }
            }
            done = true;
        }).Start();

        while (!done)
        {
            yield return null;
        }

        var result = new Dictionary<string, Item>();
        foreach (var b in bytes)
        {
            var texture = new Texture2D(2, 2);
            if (!texture.LoadImage(b.Value)) continue;

            b.Key.Texture = texture;
            result.Add(b.Key.Id, b.Key);
        }

        Debug.Log("Finished loading images!");
        Images = result;

        // (Konrad) Deserialize Projects.
        if (Projects == null) Projects = new List<Project>();
        if (File.Exists(Path.Combine(dataPath, "projects.json")))
        {
            var projects = JsonConvert.DeserializeObject<List<Project>>(File.ReadAllText(Path.Combine(dataPath, "projects.json")));
            if (projects != null)
            {
                foreach (var p in projects)
                {
                    AddProject(p);
                    foreach (var f in p.Folders)
                    {
                        AddFolder(f, true);
                        foreach (var i in f.Items)
                        {
                            var image = Images != null && Images.ContainsKey(i.ParentImageId)
                                ? Images[i.ParentImageId]
                                : null;
                            if (image == null) continue;

                            i.ThumbnailTexture = image.Texture;

                            // (Konrad) Call methods that would normally be called by the event system
                            // as content is getting downloaded.
                            AddItemThumbnail(i, true); // creates new button
                            UpdateImageDescription(i, image); // sets button description
                            AddItemContent(i, image); // sets item Material
                        }
                    }
                }
            }
        }

        if (Images == null) Images = new Dictionary<string, Item>();

        yield return true;
    }

我不得不承认它有一点帮助,但仍然不是很好。看着探查器,我刚走出大门就得到了相当大的摊位:

这是我的反序列化例程导致它:

有什么办法可以解决这个问题?

【问题讨论】:

  • 就像你说的,你可以使用线程来进行这种长期的努力,但是你不需要任何特殊的方式来移动结果。您可以从工作线程中设置一个简单的标志,Update() 可以不时监控。你不能使用协同程序,因为它们是原子的,如果你不中断文件加载,你很容易阻止你的游戏
  • 但是文件加载不是问题。在File.ReadAllBytes 上花费的实际时间并不多。实际的瓶颈在于对 Unity API 的调用,在 AddProject 我创建按钮、分配新父级、设置文本等。在这些调用中,我必须使用慢速方法,如 GetComponentsInChildren GetComponent 等。我认为这些调用导致一个摊位。
  • 不,导致停顿的原因是使用 File.ReadAllBytes 加载文件并序列化 json。由于您没有使用 Unity 的 API,因此将这两个放在一个线程中,将结果存储在一个数组中。等待它完成然后使用LoadImage 将每个加载到Texture2D。在你这样做之前,责怪 Unity 的 API 还为时过早。
  • 哦,我明白你的意思了。在运行时创建动态内容并不是制作高效游戏的方法。 设计它。创造它。烤吧
  • 就像我说的,对那些非原子操作使用不同的线程。您的反序列化代码只会导致 FPS 大幅下降。与流行的看法相反,协程从来都不是一个好主意,它为糟糕的编码实践开创了先例。 amazon.com/Engine-Architecture-Second-Jason-Gregory/dp/…

标签: c# unity3d .net-3.5 coroutine


【解决方案1】:

将工作分散到多个帧有两种主要方式:

  1. 多线程和
  2. 协程

多线程有你指出的限制,所以协程似乎合适。

使用协程要记住的关键是,在运行 yield 语句之前,它们不允许下一帧开始。另一件要记住的事情是,如果你过于频繁地让步,根据你的帧率,你每秒会达到多少次让步返回是有上限的,所以你不希望过早让步,否则将需要完成工作的时间太长了。

您想要的是函数经常有机会让步,但您不希望这个机会总是被抓住。最好的方法是使用Stopwatch class(确保使用全名或在文件顶部添加“使用”语句)或类似名称。

这是您的第二个代码 sn-p 的示例修改。

public IEnumerator Deserialize()
{
    var dataPath = Path.Combine(Application.persistentDataPath, "Images");
    var bytes = new Dictionary<Item, byte[]>();
    var done = false;
    new Thread(() => {
        if (File.Exists(Path.Combine(dataPath, "images.json")))
        {
            var items = JsonConvert.DeserializeObject<Dictionary<string, Item>>(File.ReadAllText(Path.Combine(dataPath, "images.json"))).Values;
            foreach (var i in items)
            {
                if (!File.Exists(Path.Combine(dataPath, i.Name))) continue;

                var b = File.ReadAllBytes(Path.Combine(dataPath, i.Name));
                if (b.Length <= 0) continue;

                bytes.Add(i, b);
            }
        }
        done = true;
    }).Start();

    while (!done)
    {
        yield return null;
    }

    // MOD: added stopwatch and started
    System.Diagnostics.Stopwatch watch = new System.Diagnostics.Stopwatch();
    int MAX_MILLIS = 5; // tweak this to prevent frame rate reduction
    watch.Start();


    var result = new Dictionary<string, Item>();
    foreach (var b in bytes)
    {
        // MOD: Check if enough time has passed since last yield
        if (watch.ElapsedMilliseconds() > MAX_MILLIS)
        {
            watch.Reset();
            yield return null;
            watch.Start();
        }
        var texture = new Texture2D(2, 2);
        if (!texture.LoadImage(b.Value)) continue;

        b.Key.Texture = texture;
        result.Add(b.Key.Id, b.Key);
    }

    Debug.Log("Finished loading images!");
    Images = result;

    // (Konrad) Deserialize Projects.
    if (Projects == null) Projects = new List<Project>();
    if (File.Exists(Path.Combine(dataPath, "projects.json")))
    {
        var projects = JsonConvert.DeserializeObject<List<Project>>(File.ReadAllText(Path.Combine(dataPath, "projects.json")));
        if (projects != null)
        {
            foreach (var p in projects)
            {
                AddProject(p);
                foreach (var f in p.Folders)
                {
                    AddFolder(f, true);
                    foreach (var i in f.Items)
                    {
                        // MOD: check if enough time has passed since the last yield
                        if (watch.ElapsedMilliseconds() > MAX_MILLIS)
                        {
                            watch.Reset();
                            yield return null;
                            watch.Start();
                        }
                        var image = Images != null && Images.ContainsKey(i.ParentImageId)
                            ? Images[i.ParentImageId]
                            : null;
                        if (image == null) continue;

                        i.ThumbnailTexture = image.Texture;

                        // (Konrad) Call methods that would normally be called by the event system
                        // as content is getting downloaded.
                        AddItemThumbnail(i, true); // creates new button
                        UpdateImageDescription(i, image); // sets button description
                        AddItemContent(i, image); // sets item Material
                    }
                }
            }
        }
    }

    if (Images == null) Images = new Dictionary<string, Item>();

    yield return true;
}

编辑:对于那些想要更多一般建议的人的进一步说明......

两个主要系统是多线程和协程。它们的优缺点是:

  • 协程优势:
    • 很少设置。
    • 没有数据共享或锁定问题。
    • 可以执行任何统一主线程操作。
  • 多线程优势:
    • 不会占用主线程的时间,为您留出尽可能多的 CPU 资源
    • 可以利用完整的 CPU 内核而不是主线程剩余的内核。

总而言之,协程最适合快速解决方案或需要对统一对象进行修改时。但是,如果需要执行大量处理,最好尽可能多地卸载到另一个线程。如今,很少有设备的内核少于两个(可以肯定地说非用于玩游戏的设备?)。

在这种情况下,混合解决方案是可能的,将一些工作卸载到单独的线程,并将依赖于统一的工作保持在主线程上。这是一个强大的解决方案,协程可以让它变得简单。

tating 成就 例如,我做了一个 voxel engine,它将算法的运行卸载到一个单独的线程上,然后在主线程上创建实际的网格,允许 50-70%减少生成网格所需的时间,也许更重要的是减少对游戏最终性能的影响。它通过在线程之间来回传递的作业队列来做到这一点。

【讨论】:

    猜你喜欢
    • 2021-06-30
    • 2015-12-04
    • 2012-09-26
    • 2021-05-05
    • 1970-01-01
    • 1970-01-01
    • 2019-04-08
    • 2013-02-04
    相关资源
    最近更新 更多