主线程上的网络请求导致帧率延迟峰值

Framerate lag spikes from network requests on main thread

我目前正在开展一个项目,使用 OpenAI 的 GPT3 为 NPC 对话提供支持。每个 NPC 都会在对话中发言时向我的服务器发送 POST 请求,returns NPC 通过其本地 AudioSource 播放的音频文件。

视频在这里:https://www.youtube.com/watch?v=pygM6yDE9hI

我遇到的一个问题是抖动和短时滞后。

我是 Unity 的新手,但根据分析器,我认为罪魁祸首是处理网络请求的协程。

下面是我实现此功能的代码:

     // Update is called once per frame
 void Update()
 {
     int index = -1;
     if (conversation != null)
     {
         if (!conversation.processing)
         {
             if (conversation.currentSpeaker.Equals(this.id))
             {
                 Debug.Log(this.id + " getting response");
                 conversation.processing = true;
                 StartCoroutine(getResponse(conversation));
             }
         }
     }
     else if (Datastore.Instance.id2conversation.TryGetValue(id, out index))
     {
         conversation = Datastore.Instance.conversations[index];
     }
 }

 IEnumerator getResponse(Conversation conversation)
 {
     WWWForm form = new WWWForm();
     form.AddField("id", this.id);
     var www = UnityWebRequest.Post("http://" + Datastore.Instance.host + ":3000/generate", form);

     yield return www.SendWebRequest();

     if (interrupted) yield break;

     if (www.isNetworkError)
     {
         Debug.Log(www.error);
     }
     else
     {
         if (www.GetResponseHeaders().Count > 0)
         {
             var jsonData = JSON.Parse(www.downloadHandler.text);

             string stringData = jsonData["audioContent"]["data"].ToString();
             byte[] rawdata = AudioHelpers.ConvertToByteStream(stringData);

             AudioClip clip = AudioHelpers.ConvertToAudioClip(rawdata);

             this.audioSource.clip = clip;
             this.audioSource.Play();

             this.animator.SetBool(this.talkingBoolHash, true);

             Debug.Log("Response Recieved");

             yield return new WaitForSeconds(clip.length);

             if (interrupted) yield break;

             this.conversation.currentSpeaker = jsonData["nextSpeaker"].ToString().Replace("\"", "");
             this.conversation.processing = false;
             this.animator.SetBool(this.talkingBoolHash, false);
         }
     }
 }

如何提高此代码的性能以消除帧率下降的时期?

是否可以通过 Unity Jobs 将此代码移动到另一个线程?

如有任何帮助,我们将不胜感激。

编辑: 深度剖析器:

为 JSON 解析实现线程。新代码:

// Update is called once per frame
    void Update()
    {
        int index = -1;
        if (conversation != null)
        {
            if (!conversation.processing)
            {
                if (conversation.currentSpeaker.Equals(this.id))
                {
                    Debug.Log(this.id + " getting response");
                    conversation.processing = true;
                    StartCoroutine(getResponse(conversation));
                }
            }
        }
        else if (Datastore.Instance.id2conversation.TryGetValue(id, out index))
        {
            conversation = Datastore.Instance.conversations[index];
        }

    }

    Task<(byte[], string)> ParseAudioData(string rawJson)
    {
        try
        {
            return Task.Run(() => {
                var jsonData = JSON.Parse(rawJson);
                string stringData = jsonData["audioContent"]["data"].ToString();
                byte[] rawdata = AudioHelpers.ConvertToByteStream(stringData);
                string nextSpeaker = jsonData["nextSpeaker"].ToString().Replace("\"", "");
                return Task.FromResult((rawdata, nextSpeaker));
        });
    }
    catch(Exception e)
    {
        Debug.LogException(e);
        throw;
    }
}

    IEnumerator getResponse(Conversation conversation)
    {
        WWWForm form = new WWWForm();
        form.AddField("id", this.id);

        var www = UnityWebRequest.Post("http://" + Datastore.Instance.host + ":3000/generate", form);

        yield return www.SendWebRequest();

        if (interrupted) yield break;

        if (www.isNetworkError)
        {
            Debug.Log(www.error);
        }
        else
        {
            if (www.GetResponseHeaders().Count > 0)
            {
                Task<(byte[], string)> t = ParseAudioData(www.downloadHandler.text);

                yield return t;

                AudioClip clip = AudioHelpers.ConvertToAudioClip(t.Result.Item1);

                this.audioSource.clip = clip;
                this.audioSource.Play();

                this.animator.SetBool(this.talkingBoolHash, true);

                Debug.Log("Response Recieved");

                yield return new WaitForSeconds(clip.length);

                if (interrupted) yield break;

                this.conversation.currentSpeaker = t.Result.Item2;
                this.conversation.processing = false;
                this.animator.SetBool(this.talkingBoolHash, false);
            }
        }
    }

最终编辑:在下面的答案的帮助下它开始工作了!

我的最终代码:

Task<JSONNode> ParseJsonData(string rawJson)
{
    try
    {
        return Task.Run(() =>
        {
            JSONNode jsonData = JSON.Parse(rawJson);
            return jsonData;
        });
    }
    catch (Exception e)
    {
        Debug.LogException(e);
        throw;
    }
}

Task<(byte[], string)> ParseAudioData(JSONNode jsonData)
{
    try
    {
        return Task.Run(() => {
            string stringData = jsonData["audioContent"]["data"].ToString();
            byte[] rawdata = AudioHelpers.ConvertToByteStream(stringData);
            string nextSpeaker = jsonData["nextSpeaker"].ToString().Replace("\"", "");
            return (rawdata, nextSpeaker);
        });
    }
    catch (Exception e)
    {
        Debug.LogException(e);
        throw;
    }
}
IEnumerator PlayDialog(AudioClip clip, string nextSpeaker)
{
    this.audioSource.clip = clip;
    this.audioSource.Play();
    this.animator.SetBool(this.talkingBoolHash, true);

    yield return new WaitForSeconds(clip.length);

    if (interrupted) yield break;

    this.conversation.currentSpeaker = nextSpeaker;
    this.conversation.processing = false;
    this.animator.SetBool(this.talkingBoolHash, false);
}

IEnumerator getResponse(Conversation conversation)
{
    WWWForm form = new WWWForm();
    form.AddField("id", this.id);
    var www = UnityWebRequest.Post("http://" + Datastore.Instance.host + ":3000/generate", form);

    yield return www.SendWebRequest();

    if (interrupted) yield break;

    if (www.isNetworkError)
    {
        Debug.Log(www.error);
    }
    else
    {
        if (www.GetResponseHeaders().Count > 0)
        {
            ParseJsonData(www.downloadHandler.text).ContinueWith((jsonData) => {
                ParseAudioData(jsonData.Result).ContinueWith((t) =>
                {
                    // https://github.com/PimDeWitte/UnityMainThreadDispatcher
                    UnityMainThreadDispatcher.Instance().Enqueue(() =>
                    {
                        AudioClip clip = AudioHelpers.ConvertToAudioClip(t.Result.Item1);
                        StartCoroutine(PlayDialog(clip, t.Result.Item2));
                        Debug.Log("Response Recieved");
                    });
                });
            });
           
        }
    }
}

我想说延迟尖峰可能来自 JSON 解析并将其转换为 AudioClip。您或许可以在不同的线程上执行此操作:

Task<byte[]> ParseJsonData (string rawJson)
{
    try
    {
        return Task.Run(() =>
        {
            jsonData = JSON.Parse(rawJson);
            string stringData = jsonData["audioContent"]["data"].ToString();
            return AudioHelpers.ConvertToByteStream(stringData);
        });
    }
    catch (Exception e)
    {
        UnityEngine.Debug.LogException(e);
        throw;
    }
}

并这样称呼它:

IEnumerator getResponse(Conversation conversation)
{
    WWWForm form = new WWWForm();
    form.AddField("id", this.id);
    var www = UnityWebRequest.Post("http://" + Datastore.Instance.host + ":3000/generate", form);

    yield return www.SendWebRequest();

    if (interrupted) yield break;

    if (www.isNetworkError)
    {
        Debug.Log(www.error);
    }
    else
    {
        if (www.GetResponseHeaders().Count > 0)
        {
            ParseJsonData(rawJson)
                .ContinueWith(ParseAudioData);
            ParseAudioData(www.downloadHandler.text).ContinueWith((rawData) =>
            {
                // https://github.com/PimDeWitte/UnityMainThreadDispatcher
                UnityMainThreadDispatcher.Instance.Enqueue(() =>
                {
                    AudioClip clip = AudioHelpers.ConvertToAudioClip(rawData);
                    StartCoroutine(PlayDialog(clip));
                    Debug.Log("Response Recieved");
                });
        }
    }
}

IEnumerator PlayDialog (AudioClip clip)
{
    this.audioSource.clip = clip;
    this.audioSource.Play();
    this.animator.SetBool(this.talkingBoolHash, true);
    yield return new WaitForSeconds(clip.length);
    if (interrupted) yield break;
    this.conversation.currentSpeaker = jsonData["nextSpeaker"].ToString().Replace("\"", "");
    this.conversation.processing = false;
    this.animator.SetBool(this.talkingBoolHash, false);
}

这会将操作发送到新线程,并在操作结束时继续您的代码。如果滞后峰值来自解析 json 这应该会有所帮助。当 运行 来自其他线程的代码时要小心,因为您无法访问大部分 Unity 功能并且它们都是异步的。

编辑:在 MainThread 上包含了一些用于 运行 代码的实用程序,因为正如其他人所指出的,如果不摆弄同步上下文,这将无法工作。

此外,我建议您尝试将您的方法与单一职责分开。截至目前,您的协程正在下载内容、解析内容、播放音频和更新对话状态。这对于单个函数来说已经相当多了。

你的 JSON 也应该是一个结构化的 class,解析它并仍然通过它的字符串哈希访问内容是没有意义的。

如果这仍然不能解决您的问题,您可能需要更深入地研究探查器并检查到底是什么导致它在主线程中花费太多时间。