⭐ 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 依赖问题

相关推荐
沧海归城6 小时前
Unity_XR控制手部动画
unity·游戏引擎·xr
★YUI★16 小时前
学习游戏制作记录(冻结敌人时间与黑洞技能)7.30
学习·游戏·unity·c#
软***c18 小时前
PPT文件密码解密工具推荐:Tenorshare PassFab for PPT绿色免安装一键解除密码限制,附详细教程和下载地址
运维·服务器·powerpoint·ppt密码解密·ppt解密工具
还债大湿兄2 天前
3D游戏引擎的“眼睛“:相机系统深度揭秘与技术实现
数码相机·3d·游戏引擎
死也不注释2 天前
【第四章自定义编辑器窗口_扩展默认的编辑器窗口_扩展Hierarchy窗口(8/11)】
unity·编辑器
BuHuaX2 天前
Unity_UI_NGUI_缓动
ui·unity·c#·游戏引擎·游戏策划
DaLiangChen2 天前
Unity 实时 CPU 使用率监控
unity·游戏引擎
cyr___2 天前
Unity教程(二十四)技能系统 投剑技能(中)技能变种实现
学习·游戏·unity·游戏引擎
星星火柴9363 天前
开发笔记 | 实现人物立绘的差分效果
笔记·unity·游戏程序·优香