当前位置: 首页 > java >正文

⭐ 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

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(完整代码)

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);// 用占位图先顶上 UIShowImg.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(主线程调度器)

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

http://www.xdnf.cn/news/16613.html

相关文章:

  • 【EDA】Calma--早期版图绘制工具商
  • AR辅助前端设计:虚实融合场景下的设备维修指引界面开发实践
  • 2025年06月03日 Go生态洞察:语法层面的错误处理支持
  • Java 11 新特性详解与代码示例
  • Spring Boot中的this::语法糖详解
  • 递归推理树(RR-Tree)系统:构建认知推理的骨架结构
  • 力扣热题100--------240.搜索二维矩阵
  • Generative AI in Game Development
  • 板凳-------Mysql cookbook学习 (十二--------7)
  • 亚马逊 Vine 计划:评论生态重构与合规运营策略
  • C++基础:模拟实现queue和stack。底层:适配器
  • 解决mac下git pull、push需要输入密码
  • MySQL(配置)——MariaDB使用
  • 探索 Vim:Linux 下的高效文本编辑利器
  • SBB指令的“生活小剧场“
  • Linux 系统启动与 GRUB2 核心操作指南
  • Kafka运维实战 17 - kafka 分区副本从 1 增加到 3【实战】
  • 作物生长模型Oryza V3实战17:土壤数据集
  • 【RH134 问答题】第 9 章 访问网络附加存储
  • 2025年Solar应急响应公益月赛-7月笔记ing
  • 正运动控制器Zbasic回零详细教程(不带Z信号)
  • 【Linux知识】Linux Shell 脚本中的 `set -ex` 命令深度解析
  • SQL排查、分析海量数据以及锁机制
  • Fast Video generation with sliding tile attention
  • 2-verilog-基础语法
  • flask使用celery通过数据库定时
  • 【Linux我做主】探秘进程状态
  • Java中的有界队列和无界队列详解
  • CMake 目标文件后缀
  • react 项目怎么打断点