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