You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

HoloLens2添加TextWriter后按钮粘滞卡顿问题求助

问题:HoloLens2测验应用中CSV写入导致按钮交互粘滞

我使用Unity 2019.4.20f1(因特殊要求必须使用该版本)开发面向HoloLens2的测验类应用,应用会随机展示预池化题目,用户点击选项按钮作答。但添加TextWriter记录题目、答案、耗时到CSV的功能后,HoloLens2上的按钮出现「粘滞」现象(手部网格触碰按钮后短暂停留),尝试过协程但仅延迟卡顿时机,且题切换时间有限不能过度延迟,求解决办法。

现有代码:

using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Events;
using Microsoft.MixedReality.Toolkit.Input;
using Microsoft.MixedReality.Toolkit.UI;
using System.IO;
using System;

public class QuizUI : MonoBehaviour
{
    [SerializeField] private QuizManager quizManager;
    [SerializeField] private GameObject startButton;
    [SerializeField] private GameObject imageHolder;
    [SerializeField] private Image questionImage;
    [SerializeField] private GameObject questionModel;
    [SerializeField] private GameObject optionHolder;
    [SerializeField] private List<GameObject> options;
    [SerializeField] private GameObject endInfo;

    private Question question;
    private bool answered = false;
    private GameObject clone;
    public Stopwatch stopwatch;
    public bool val;
    string filename = "";
    private string clickedBtn;

    private void Awake()
    {
        imageHolder.gameObject.SetActive(false);
        optionHolder.gameObject.SetActive(false);
        endInfo.gameObject.SetActive(false);

        filename = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments) + "/PurdueLogfile.csv";
    }

    private void OnEnable()
    {
        var interactable = startButton.GetComponent<Interactable>();
        interactable.OnClick.AddListener(() => Prepare());
    }

    private void OnDisable()
    {
        var interactable = startButton.GetComponent<Interactable>();
        interactable.OnClick.RemoveListener(() => Prepare());
    }

    private void Prepare()
    {
        startButton.gameObject.SetActive(false);
        imageHolder.gameObject.SetActive(true);
        optionHolder.gameObject.SetActive(true);
        
        //add the listner to all the buttons
        for (int i = 0; i < options.Count; i++)
        {
            GameObject localBtn = options[i];
            var interactable = options[i].GetComponent<Interactable>();
            interactable.OnClick.AddListener(() => OnClick(localBtn));
        }

        //log start time
        TextWriter tw = new StreamWriter(filename, true);
        tw.WriteLine("[" + System.DateTime.UtcNow.ToString("dd-MM-yyyy HH:mm:ss.fff") + "]" + "," + "Start");
        tw.Close();
        
        quizManager.StartGame();
        
    }

    public void SetQuestion(Question question)
    {
        //set the question
        this.question = question;
        //check for questionType
        switch (question.questionType)
        {
            case QuestionType.IMAGE:
                questionImage.transform.parent.gameObject.SetActive(true);
                questionImage.transform.gameObject.SetActive(true);    //activate image holder
                questionModel.gameObject.SetActive(false);
                questionImage.sprite = question.questionImg;           //set the image sprite
                break;

            case QuestionType.MODEL:
                questionImage.transform.parent.gameObject.SetActive(true);
                questionImage.transform.gameObject.SetActive(false);
                questionModel.gameObject.SetActive(true);
                clone = Instantiate(question.questionMdl, new Vector3(0, -0.15f, 0.8f), Quaternion.Euler (0f, -90f, 0f)) as GameObject;
                clone.transform.SetParent(questionModel.transform);
                break;
        }

        //assign options to respective option buttons
        for (int i = 0; i < options.Count; i++)
        {
            options[i].transform.GetChild(0).GetComponentInChildren<TextMesh>().text = question.options[i];
        }

        answered = false;

        //stopwatch to start
        stopwatch = new Stopwatch();
        stopwatch.Start();
    }

    public void OnClick(GameObject btn)
    {
        if (!answered)
        {
            answered = true;
            bool val = quizManager.Answer(btn.name);
            UnityEngine.Debug.Log(btn.name);
            UnityEngine.Debug.Log(val);
            clickedBtn = btn.name;

            //stopwatch to stop
            stopwatch.Stop();   
            UnityEngine.Debug.Log(stopwatch.Elapsed);

            //log answers
            TextWriter tw = new StreamWriter(filename, true);
            tw.WriteLine("[" + System.DateTime.UtcNow.ToString("dd-MM-yyyy HH:mm:ss.fff") + "]" + "," + quizManager.selectedQuestion.questionID + "," + quizManager.selectedQuestion.questionType + "," + stopwatch.ElapsedMilliseconds + "," + val + "," + clickedBtn);
            tw.Close();

            //destroy 3D models
            DestroyImmediate(clone, true);
        }
    }

    public void EndGame()
    {
        imageHolder.gameObject.SetActive(false);
        optionHolder.gameObject.SetActive(false);
        endInfo.gameObject.SetActive(true);
    }
}

解决方案

核心问题:主线程IO阻塞

当前代码在主线程中直接执行文件写入操作,HoloLens2的存储IO操作相对耗时,会阻塞UI交互线程,导致按钮反馈延迟、手部网格粘滞。协程如果只是延迟执行但仍在主线程,无法根本解决问题。

具体改进方案

1. 使用后台线程处理文件写入

创建线程安全的日志队列,把需要写入的日志内容先存入队列,再启动后台线程异步处理写入,完全不占用主线程资源。

修改代码步骤:

  • 添加线程安全队列和后台线程相关字段
  • 重构日志写入逻辑,将日志内容入队,后台线程循环处理队列
using System.Collections.Concurrent;
using System.Threading;

public class QuizUI : MonoBehaviour
{
    // 新增:线程安全的日志队列
    private ConcurrentQueue<string> logQueue = new ConcurrentQueue<string>();
    private Thread logThread;
    private bool isLoggingThreadRunning = false;

    private void Awake()
    {
        imageHolder.gameObject.SetActive(false);
        optionHolder.gameObject.SetActive(false);
        endInfo.gameObject.SetActive(false);

        filename = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments) + "/PurdueLogfile.csv";
        
        // 启动后台日志线程
        StartLogThread();
    }

    private void StartLogThread()
    {
        isLoggingThreadRunning = true;
        logThread = new Thread(ProcessLogQueue)
        {
            IsBackground = true, // 后台线程随程序退出自动终止
            Priority = ThreadPriority.BelowNormal // 降低线程优先级,避免抢占UI线程资源
        };
        logThread.Start();
    }

    private void ProcessLogQueue()
    {
        while (isLoggingThreadRunning)
        {
            if (logQueue.TryDequeue(out string logLine))
            {
                try
                {
                    // 使用File.AppendAllText简化写入,自动处理打开/关闭流
                    File.AppendAllText(filename, logLine + Environment.NewLine);
                }
                catch (Exception e)
                {
                    UnityEngine.Debug.LogError($"日志写入失败:{e.Message}");
                }
            }
            else
            {
                // 队列为空时短暂休眠,减少CPU占用
                Thread.Sleep(10);
            }
        }
    }

    private void OnDestroy()
    {
        // 终止日志线程前,先处理完队列中剩余的日志
        isLoggingThreadRunning = false;
        if (logThread != null && logThread.IsAlive)
        {
            logThread.Join(1000); // 等待最多1秒,确保剩余日志写入完成
        }
    }

    // 替换原有的直接写入逻辑,改为入队
    private void Log(string content)
    {
        logQueue.Enqueue(content);
    }

    // 修改Prepare中的日志调用
    private void Prepare()
    {
        // ...其他原有代码
        
        // 替换原TextWriter写入逻辑
        Log($"[{System.DateTime.UtcNow.ToString("dd-MM-yyyy HH:mm:ss.fff")}],Start");
        
        quizManager.StartGame();
    }

    // 修改OnClick中的日志调用
    public void OnClick(GameObject btn)
    {
        if (!answered)
        {
            answered = true;
            bool val = quizManager.Answer(btn.name);
            UnityEngine.Debug.Log(btn.name);
            UnityEngine.Debug.Log(val);
            clickedBtn = btn.name;

            stopwatch.Stop();   
            UnityEngine.Debug.Log(stopwatch.Elapsed);

            // 替换原TextWriter写入逻辑
            Log($"[{System.DateTime.UtcNow.ToString("dd-MM-yyyy HH:mm:ss.fff")}],{quizManager.selectedQuestion.questionID},{quizManager.selectedQuestion.questionType},{stopwatch.ElapsedMilliseconds},{val},{clickedBtn}");

            Destroy(clone, true); // 替换DestroyImmediate为异步Destroy
        }
    }
}

2. 额外优化点

  • 替换DestroyImmediate为Destroy:DestroyImmediate会在主线程立即执行销毁操作,可能阻塞帧渲染,改用异步的Destroy更合适。
  • 预加载模型:如果题目模型较多,提前预池化模型对象,避免在SetQuestion中临时Instantiate导致的卡顿。

3. 验证和调试

  • 部署到HoloLens2后,观察按钮交互是否恢复流畅
  • 检查CSV文件是否正常生成,日志内容是否完整

内容的提问来源于stack exchange,提问作者PolarBear22

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.14 17:15:52