⭐ Unity 异步加载PPT页面 并 首帧无卡顿显示

在Unity中 加载并显示PPT内容是一个常见的需求,尤其是在教学应用、互动展示中嵌入课件等场景。但默认的同步加载方式会在启动时卡顿甚至白屏,非常影响用户体验。

本篇文章将分享一种首帧加载不卡顿后台异步加载PPT所有页面 的解决方案,适用于Unity2020+ 的任意版本。

✨效果如下

🎯 实现功能

  • 第一帧立即显示第一页幻灯片

  • 其余页面在后台异步加载,不阻塞主线程

  • 避免 Unity 报错 Internal_CreateGameObject can only be called from the main thread

  • 使用 Texture2D 动态创建 PPT 页面 Sprite

  • 支持加载中的占位图


🧠 技术关键点

1. Aspose.Slides 渲染幻灯片为 Bitmap

我们使用 Aspose.Slides 提供的 GetThumbnail() 方法将每一页 PPT 渲染为 System.Drawing.Bitmap

cs 复制代码
var bitmap = slide.GetThumbnail(1f, 1f); // 按 100% 缩放生成 Bitmap

2. 转换 Bitmap 为 Unity 可识别的 Sprite

通过 Texture2D.LoadImage() 将 Bitmap 转换为 Unity 的贴图,再创建为 Sprite 用于 UI 展示。

3. 主线程调度器解决跨线程 UI 更新问题

Unity 要求所有 UI、GameObject 创建必须在主线程执行。为此我们实现了 UnityMainThreadDispatcher,安全地在主线程中回调执行。

🧱 项目结构

✅ PPTCtrl.cs(完整代码)

cs 复制代码
using Aspose.Slides;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;

public class PPTCtrl : MonoBehaviour
{
    public UnityEngine.UI.Image ShowImg;
    public Text pagetext;
    public GameObject LastBtn, NextBtn;
    public Sprite PlaceholderSprite; // 设置一个默认占位图 可以用ppt第一张图

    private Presentation presentation = null;
    private int NowPage = 0;
    private bool isLoading = false;

    private Dictionary<int, Sprite> slideSprites = new Dictionary<int, Sprite>();

    private async void Start()
    {
        UnityMainThreadDispatcher.Instance(); // 初始化主线程调度器

        string PPTPath = Application.dataPath + "/PPT/关于绿色生态环境的论述.pptx";
        presentation = new Presentation(PPTPath);

        // 用占位图先顶上 UI
        ShowImg.sprite = PlaceholderSprite;

        // 优先加载第一页
        await LoadAndCacheSlideAsync(0);

        // 延迟一帧再刷新 UI,避免卡顿
        await Task.Delay(100);
        SwitchPage(0);

        // 后台加载其他页
        _ = Task.Run(() => PreloadOtherSlidesAsync());
    }

    private async Task PreloadOtherSlidesAsync()
    {
        int total = presentation.Slides.Count;

        for (int i = 1; i < total; i++)
        {
            if (!slideSprites.ContainsKey(i))
                await LoadAndCacheSlideAsync(i);
        }
    }

    private async Task LoadAndCacheSlideAsync(int page)
    {
        var slide = presentation.Slides[page];
        var bitmap = slide.GetThumbnail(1f, 1f);
        var bytes = GetBitMapBytes(bitmap);

        await Task.Yield(); // 推出当前线程

        UnityMainThreadDispatcher.Instance().Enqueue(() =>
        {
            var tex = new Texture2D(bitmap.Width, bitmap.Height, TextureFormat.RGBA32, false);
            tex.LoadImage(bytes);
            var sprite = Sprite.Create(tex, new Rect(0, 0, tex.width, tex.height), Vector2.zero);
            slideSprites[page] = sprite;
        });
    }

    public void SwitchPage(int page)
    {
        if (isLoading || page < 0 || page >= presentation.Slides.Count)
            return;

        NowPage = page;
        pagetext.text = (page + 1) + " / " + presentation.Slides.Count;
        LastBtn.SetActive(page > 0);
        NextBtn.SetActive(page < presentation.Slides.Count - 1);

        if (slideSprites.TryGetValue(page, out Sprite sprite))
        {
            ShowImg.sprite = sprite;
        }
        else
        {
            ShowImg.sprite = PlaceholderSprite;
            StartCoroutine(LoadSlideAsync(page));
        }
    }

    private IEnumerator LoadSlideAsync(int page)
    {
        isLoading = true;

        yield return Task.Run(async () =>
        {
            await LoadAndCacheSlideAsync(page);
            UnityMainThreadDispatcher.Instance().Enqueue(() =>
            {
                if (NowPage == page && slideSprites.ContainsKey(page))
                {
                    ShowImg.sprite = slideSprites[page];
                }
                isLoading = false;
            });
        });
    }

    private byte[] GetBitMapBytes(Bitmap bm)
    {
        try
        {
            using (MemoryStream ms = new MemoryStream())
            {
                bm.Save(ms, ImageFormat.Png);
                return ms.ToArray();
            }
        }
        catch (Exception e)
        {
            Debug.LogWarning("Get Bytes failed: " + e);
            return null;
        }
    }

    public void ClickNext() => SwitchPage(NowPage + 1);
    public void ClickLast() => SwitchPage(NowPage - 1);
}

✅ UnityMainThreadDispatcher.cs(主线程调度器)

cs 复制代码
using System;
using System.Collections.Generic;
using UnityEngine;

public class UnityMainThreadDispatcher : MonoBehaviour
{
    private static readonly Queue<Action> _executionQueue = new Queue<Action>();
    private static UnityMainThreadDispatcher _instance = null;

    public static UnityMainThreadDispatcher Instance()
    {
        if (_instance == null)
        {
            var go = GameObject.Find("MainThreadDispatcher");
            if (go == null)
            {
                go = new GameObject("MainThreadDispatcher");
                DontDestroyOnLoad(go);
                _instance = go.AddComponent<UnityMainThreadDispatcher>();
            }
            else
            {
                _instance = go.GetComponent<UnityMainThreadDispatcher>();
                if (_instance == null)
                    _instance = go.AddComponent<UnityMainThreadDispatcher>();
            }
        }
        return _instance;
    }

    public void Enqueue(Action action)
    {
        if (action == null) return;

        lock (_executionQueue)
        {
            _executionQueue.Enqueue(action);
        }
    }

    private void Update()
    {
        lock (_executionQueue)
        {
            while (_executionQueue.Count > 0)
            {
                _executionQueue.Dequeue()?.Invoke();
            }
        }
    }
}

🧪 使用建议

  • 推荐在场景中设置好一个 Loading 占位图;

  • PPT 文件建议不要超过 20 页,避免加载耗时;

  • 若希望更平滑的加载体验,可增加 分帧加载或加 AsyncGPUReadback 机制进一步优化;

  • 若使用 IL2CPP 构建,需特别处理 System.Drawing 依赖问题

相关推荐
那个村的李富贵11 小时前
Unity打包Webgl后 本地运行测试
unity·webgl
nnsix12 小时前
Unity OpenXR开发HTC Vive Cosmos
unity·游戏引擎
nnsix12 小时前
Unity OpenXR,扳机键交互UI时,必须按下扳机才触发
unity·游戏引擎
nnsix13 小时前
Unity XR 编辑器VR设备模拟功能
unity·编辑器·xr
老朱佩琪!13 小时前
Unity访问者模式
unity·游戏引擎·访问者模式
轻竹办公PPT13 小时前
上传PDF直接生成PPT,适合工作汇报和总结场景
python·pdf·powerpoint
不定时总结的那啥13 小时前
Unity实现点击Console消息自动选中预制体的方法
unity·游戏引擎
nnsix14 小时前
Unity OpenXR 关闭手柄的震动
unity·游戏引擎
CreasyChan14 小时前
Unity 中的反射使用详解
unity·c#·游戏引擎·游戏开发
Jessica巨人14 小时前
Shader显示为黑色
unity·shader