UWA问答 | 博客 | 游戏及VR应用性能优化记录分享 | 侑虎科技 - 厚积薄发
type: channel
link: https://blog.uwa4d.com/category/technology
description: UWA旨在诊断与优化游戏和VR项目开发过程中潜在的运行性能问题，帮助项研发团队快速检测项目运行时的性能瓶颈，高效定位问题根源，及时反馈针对瓶颈问题的深度分析和解决方法。
items: UE5多线程｜ThreadPool
items: 如何解决ProtoBuf反序列化中GC高的问题
items: 如何排查优化URP内置Shader冗余
items: UE5多线程｜FRunnableThread
items: 移动端反射探针格式用什么比较合理
items: 为何iPad Pro上设置目标帧率为90时无法生效
items: 游戏AI行为决策——MLP（多层感知机/人工神经网络）
items: 导致Resources文件夹的资源在Android打包后丢失的原因
items: 为什么Android游戏画面在30帧运行时有抖动现象
items: 对Android游戏画面抖动现象的研究

UE5多线程｜ThreadPool
type: item
date: 2025-12-03T10:53:00+02:00
description: 【USparkle专栏】如果你深怀绝技，爱“搞点研究”，乐于分享也博采众长，我们期待你的加入，让智慧的火花碰撞交织，让知识的传递生生不息！ 当有持续时间短，又比较杂的异步任务时，可以使用ThreadPool，用固定数量的工作线程执行任务，不每次都创建新线程。UE4和UE5的线程池有很大区别，UE4线程池会真的创建很多线程，而UE5主要线程池底层复用了TaskGraph的线程，线程池只是逻辑上的概念。 一、创建线程池 线程池在FEngineLoop::PreInitPreStartupScreen函数中创建。 GThreadPool 类型为FQueuedLowLevelThreadPool，是UE5中的新实现，线程数量由FPlatformMisc::NumberOfWorkerThreadsToSpawn()确定。 GIOThreadPool 类型为FQueuedThreadPool，线程数量由FPlatformMisc::NumberOfIOWorkerThreadsToSpawn()确定，Client为4，Server为2。 GBackgroundPriorityThreadPool 类型为FQueuedThreadPool，Client为2，Server为1。 GLargeThreadPool 类型为FQueuedLowLevelThreadPool，数量由FPlatformMisc::NumberOfCoresIncludingHyperthreads()确定。 二、使用线程池 虽然线程池实现比Runnable复杂，但使用方式也比较简单。 1. Async函数 最常见用法，Async函数可设置EAsyncExecution::ThreadPool参数，指定任务在ThreadPool里执行。 函数内部会创建TAsyncQueuedWork封装Function和Promise，然后使用AddQueuedWork接口把任务加到GThreadPool中。 AddQueuedWork是线程池最重要的接口。 2. AsyncPool函数 与Async类似，但可以指定线程池和Work优先级。 3. 手动调用AddQueuedWork AddQueuedWork函数只需要接受IQueuedWork作为参数，TAsyncQueuedWork只是一个子类，我们可以创建子类，做自定义操作，这样也能指定使用哪个线程池。 比如引擎中Encode LightMap的操作，就使用了FAsyncEncode类： 三、线程池实现 1.类型定义 类型定义可分为线程池，线程池线程，任务。 线程池 FQueuedThreadPool：线程池基类，定义了线程池的接口。 Allocate：创建线程池，类型为FQueuedThreadPoolBase。 Create：创建若干工作线程。 AddQueuedWork：向线程池添加任务。 RetractQueuedWork：撤回任务。 AddQueuedWork和RetractQueuedWork是线程池提供给外部调用的主要接口，注意会在多线程中被调用。 FQueuedThreadPool有多种实现： FQueuedThreadPoolBase 最常用，线程池的基础实现，GIOThreadPool和GBackgroundPriorityThreadPool都会使用。 成员： FThreadPoolPriorityQueue QueuedWork：待处理任务的队列。 TArray<FQueuedThread*> QueuedThreads：等待接收任务的空闲线程。 TArray<FQueuedThread*> AllThreads：所有工作线程。 FCriticalSection* SynchQueue：保护任务队列的CriticalSection，因为任务队列会被多线程修改。 FQueuedLowLevelThreadPool 底层线程使用TaskGraph的ThreadPool，UE5中GThreadPool的默认实现。 FQueuedThreadPoolWrapper FQueuedThreadPoolDynamicWrapper FQueuedThreadPoolTaskGraphWrapper 线程池线程 FQueuedThread：继承自FRunnable，表示线程池中的工作线程。可以想象，它大部分时间都处于idle状态，当有任务来时才工作。 成员： DoWorkEvent：通知线程有任务要执行的Event。 QueuedWork：当前线程正在执行的Work。 Thread：Runnable对应的线程。 函数： Run：主函数，可认为是一个等待、执行任务的循环。 DoWork：由ThreadPool调用，传入一…

如何解决ProtoBuf反序列化中GC高的问题
type: item
date: 2025-12-02T04:26:00+02:00
description: 1）如何解决ProtoBuf反序列化中的GC高的问题 2）如何在编辑器中集成实时查看Mipmap使用层级工具 这是第455篇UWA技术知识分享的推送，精选了UWA社区的热门话题，涵盖了UWA问答、社区帖子等技术知识点，助力大家更全面地掌握和学习。 UWA社区主页：community.uwa4d.com UWA QQ群：793972859 From UWA AI 问答 Q：有没有比较好的解决ProtoBuf反序列化GC的方案？ A：在Unity项目中，使用ProtoBuf进行反序列化时产生的GC主要来源于两方面：内存分配的临时对象（如字符串、装箱操作）和反射机制的低效调用。以下是针对这些问题的成熟且高效的解决方案。 1. 使用[Preserve]与强制AOT初始化避免反射GC IL2CPP环境下，由于AOT编译限制，ProtoBuf生成代码中的泛型反射方法可能未被提前编译，导致运行时尝试动态调用而崩溃或触发大量GC。为确保所有必要的反射初始化代码被保留： using Google.Protobuf.Reflection; using UnityEngine.Scripting; [Preserve] private void ForceReflectionInitialization() { // 显式调用每个消息类型的ForceReflectionInitialization FileDescriptor.ForceReflectionInitialization<MyMessage1>(); FileDescriptor.ForceReflectionInitialization<MyMessage2>(); // 添加所有用到的消息类型 } 此方法无需调用，仅需存在即可被链接器保留。配合[Preserve]特性防止代码剥离，可有效解决因AOT缺失导致的异常及性能损耗。 2. 避免字符串与值类型装箱，实现0GC解析辅助 参考TMP文本优化思路，可对ProtoBuf解析过程中的输出处理进行定制，减少中间对象分配。例如，在自定义的日志系统或UI更新中直接复用StringBuilder写入，而非通过ToString()创建新字符串。 全局复用StringBuilder用于格式化输出。 对数字字段转字符串时，采用无GC的NumberFormatter.NumberToString(ref char[] buffer, int value)方式，将结果写入预分配字符数组。 自定义IFormatter实现，绕过装箱操作，直接处理基本类型。 3. 使用原生字节访问替代高开销API 对于高频解析场景（如网络同步），考虑绕过完整反序列化流程，直接读取字节流中的关键字段： // 使用CodedInputStream直接读取特定字段 var input = CodedInputStream.CreateInstance(data); while (!input.IsAtEnd) { var tag = input.ReadTag(); switch (tag.FieldNumber) { case 1: int hp = input.ReadInt32(); break; // 只解析必要字段 } } 这种方式跳过对象构建开销，适用于只关心部分数据的轻量级更新。 4. 缓冲池管理Message实例 频繁创建/销毁ProtoBuf对象会加剧GC压力。建议使用对象池缓存常用Message实例： public class MessagePool<T> where T : IMessage, new() { private static readonly Stack<T> _pool = new(); public static T Get() => _pool.Count > 0 ? _pool.Pop() : new T(); public static void Release(T msg) { _pool.Push(msg); } } 结合Clear()重置状态后复用，显著降低内存分配频率。 综上，核心策略是：强制AOT初始化 + 禁止代码剥离 + 复用缓冲区 + 裁剪不必要的反序列化流程。这些方法已在多个上线项目中验证，能有效将ProtoBuf相关GC降至接近零。 分享链接： https://www.uwa4d.com/main/uwa-shared.html?shareId=228136d0-985e-4af9-8c83-f84b04834677 From 问答社区 Q：最近在优化贴图内存，请问有没有开源库或者计算方法，可以检测贴图在屏幕上的占比情况？ 比如，一张1024的贴图，如果渲染时一直使用很高层级的Mipmap，就说明其实不需要102…

如何排查优化URP内置Shader冗余
type: item
date: 2025-11-25T04:07:00+02:00
description: 1）如何排查优化URP内置Shader冗余 2）运行时Shader内存下降的原因 这是第454篇UWA技术知识分享的推送，精选了UWA社区的热门话题，涵盖了UWA问答、社区帖子等技术知识点，助力大家更全面地掌握和学习。 UWA社区主页：community.uwa4d.com UWA QQ群：793972859 From 问答社区 Q1：请教一下Shader冗余应该怎么查，似乎好几个Shader运行时都有两份？ A：资源冗余最常见的原因是AssetBundle没有依赖打包导致的，可以使用UWA的在线AssetBundle检测进行冗余检测先试试。 Q2：测了AssetBundle，确实有冗余，但AssetBundle冗余的Shader和运行时冗余的Shader好像又不一致。实际运行时的冗余都是Hidden/Universal Render Pipeline/xxx。这又是为什么呢？ A：这些是URP的Shader，通常是URP Asset的引用导致的，因为URP Asset会引用这些Shader。而内存中出现两份Shader，说明内存中出现了两个来源路径不一样的URP Asset，通常一份是在PlayerSetting中引用的URP Asset，另外一份可能来自AssetBundle中动态加载的URP Asset。RendererData里面会引用到PostProcessData，PostProcessData就会引用这些Shader，如果代码里面动态加载的AssetBundle里面也有这种资源，也会引用一份Shader进内存，就会造成冗余。 Q3：请问这个怎么处理，直接删去吗？ A：一般只处理内存占用比较大的即可，其他的内存占用比较小，冗余开销也不大。比如Hidden/Universal Render Pipeline/Uberpost，需要删除其中用不到的Keyword来降低占用；又比如Hidden/Universal Render Pipeline/HBAO，看是否确实要用到，用不到就解除引用。 欢迎大家转至社区交流： https://answer.uwa4d.com/question/69245652244ce21ce9ec095c From 问答社区 Q：请问在游戏运行过程中不同时刻截帧后同一个Shader的内存占用为什么不一样？用UWA的资源列表观察完整生命周期曲线后更明显了，全程都在轻微但持续地下降，这是什么原因呢？ A：这是因为，内存分析工具统计到的是在Unity引擎层Native Object处的Shader内存。引擎会在ShaderCreateGPUProgram操作时，将Shader Code信息转化为运行时GPU实际要用到的信息。因此会表现为Shader本体资源内存下降，但系统层Native Heap和GFX内存都会显著上升。 这个现象在UWA DAY 2025的话题中正好有相应的原理讨论和实验，分别为： Unity移动游戏工业级性能优化指南 3.0 《心动小镇》内存优化经验谈 欢迎大家转至社区交流： https://answer.uwa4d.com/question/69245c90244ce21ce9ec095f 无论是社区里开发者们的互助讨论，还是AI基于知识沉淀的快速反馈，核心都是为了让每一个技术难题都有解、每一次踩坑都有回响。本期分享分别来自UWA AI问答和UWA问答社区，希望这些从真实开发场景中提炼的经验，能直接帮你解决当下的技术卡点，也让你在遇到同类问题时，能更高效地找到破局方向。 封面图来源于网络 今天的分享就到这里。生有涯而知无涯，在漫漫的开发周期中，我们遇到的问题只是冰山一角，UWA社区愿伴你同行，一起探索分享。欢迎更多的开发者加入UWA社区。 UWA官网：www.uwa4d.com UWA社区：community.uwa4d.com UWA学堂：edu.uwa4d.com 官方技术QQ群：793972859

UE5多线程｜FRunnableThread
type: item
date: 2025-11-19T08:27:00+02:00
description: 【USparkle专栏】如果你深怀绝技，爱“搞点研究”，乐于分享也博采众长，我们期待你的加入，让智慧的火花碰撞交织，让知识的传递生生不息！ UE游戏包与编辑器中都有众多线程，多线程可以充分利用CPU多核特性，提升游戏表现，而且现代CPU核数越来越多，游戏多线程就更有必要了。 一、线程类型 线程可分为专用线程和线程池中线程。 专用线程为GameThread，RenderThread，StatsThread等，它们各自都干专门的事情，比如GameThread用于驱动游戏逻辑，RenderThread用于渲染，StatsThread用于干性能分析。在事情干完后会进入阻塞状态，不消耗CPU资源。 线程池线程包括PoolThread和TaskGraphThread，每个线程用于干多种异步任务。游戏中许多任务并发量大，但持续时间短，比如求解动画蓝图，每帧都开几个线程求解，求解完再销毁线程无疑很浪费。另外有很多琐碎的多线程任务，单独为它们开线程也很浪费。因此UE使用了线程池，池中线程会循环利用，不断执行不同的异步任务，当没有任务时也处于阻塞状态。 使用多线程时，UE已对底层平台接口进行了封装，开发者无需关注平台差异，直接使用UE提供的统一接口即可。 常用的多线程方式包括：RunnableThread、ThreadPool 和 TaskGraph。这三者在底层线程的实现机制上有所不同，对外提供的接口种类丰富，部分接口还支持通过参数指定所使用的线程实现方式。 二、FRunnable & FRunnableThread 基本的线程使用方式为FRunnable与FRunnableThread的组合，用于创建专用线程，比如AsyncLoadingThread与渲染线程等。FRunnable负责逻辑，FRunnableThread负责具体线程，线程承载了逻辑，两者一一对应，好处是上层逻辑与底层平台接口分离。 FRunnable FRunnable类本身代表一个抽象的“可运行”对象，只有几个接口，不涉及线程细节，是我们写逻辑的地方，理论上可以在任意线程执行。 接口如下： Init Runnable的初始化，初始化可能成功，可能失败，失败会立即返回，线程结束。 Run执行逻辑主体，初始化成功后执行。 Exit Run中任务执行完后的正常退出接口，执行清理操作。 Stop由其他线程调用，用于中途停止该Runnable以及背后的线程，但具体如何停止要我们自己实现。 GetSingleThreadInterface，当UE强制单线程模式时，返回用于Tick执行的实例，用的不多。 上述接口不用我们调用，对应线程创建好后会自动调用。 示例FAsyncloadingThread： 该类用于处理异步资源加载，会另开一个线程加载资源，继承了FRunnable，内部的Thread变量存储线程。 Init函数没做什么，Run函数如下： 主体是一个while循环，StopTaskCounter是循环退出条件，为Atomic计数器，当未被设置时就不断处理加载，被设置后即退出。 Stop函数如下，会设置StopTaskCounter变量： 创建线程，启动Run逻辑。 只需要下面一行代码即可，详细会在下文介绍： FRunnableThread FRunnableThread是平台线程的抽象，也是基类，与平台相关的线程操作由多个子类完成，包括： FRunnableThreadWin FRunnableThreadUnix FRunnableThreadApple FRunnableThreadAndroid 我们不需要继承和修改这些类，使用即可。 成员变量： FString ThreadName：线程名。 FRunnable* Runnable：对应Runnable。 FEvent* ThreadInitSyncEvent：同步的Event。 EThreadPriority ThreadPriority：线程优先级，UE自己抽象了几个枚举。 接口： Kill：结束线程，UE建议不要用操作系统的Kill接口，强杀线程会导致泄露和死锁，应该调用Runnable的Stop方法。 WaitForCompletion：忙等，直到线程执行完。 Suspend：让线程挂起或继续执行。 SetThreadPriority：设置线程优先级。 FRunnableThreadWin 看下常见的Windows平台子类如何实现。 Windows平台的线程为内核对象，通过HANDLE持有索引，这里的Thread就是底层的线程。 Kill方法内调用了Runnable的Stop，该函数由我们自己实现，然后可选忙等，最后调用操作系统的CloseHandle方法释放线程内核对象。 Windows有TerminateThr…

移动端反射探针格式用什么比较合理
type: item
date: 2025-11-18T04:06:00+02:00
description: 1）移动端反射探针格式用什么比较合理 2）不同平台打包热更容灾问题的解决方案 这是第453篇UWA技术知识分享的推送，精选了UWA社区的热门话题，涵盖了UWA问答、社区帖子等技术知识点，助力大家更全面地掌握和学习。 UWA社区主页：community.uwa4d.com UWA QQ群：793972859 From UWA AI 问答 Q：移动端反射探针格式用什么比较合理？ A：在移动端使用反射探针时，选择合理的纹理格式是平衡画质与性能的关键。不恰当的格式会显著增加内存占用和带宽消耗，影响帧率和功耗。 一、推荐格式 1. ASTC（Adaptive Scalable Texture Compression） 推荐理由： 这是目前移动平台（尤其是iOS和现代Android设备）上最理想的选择。ASTC具有极高的灵活性，支持从4x4到12x12等多种压缩比率，并且能同时处理RGB、RGBA以及HDR数据。 应用场景： LDR探针：可选用ASTC 6x6或8x8，在保证足够清晰度的同时将体积压缩到最小。 HDR探针：必须使用支持HDR的格式。ASTC 4x4 HDR是最佳选择，它能在较低的内存开销下保留高光等重要细节，非常适合需要真实感反射的场景。 2. ETC2/EAC 推荐理由： 作为OpenGL ES 3.0的标准，ETC2兼容性极佳，几乎所有现代Android设备都支持。对于需要广泛兼容性的项目，它是可靠的备选方案。 应用场景： LDR探针：使用ETC2 RGB。虽然压缩效率略逊于ASTC，但兼容性最好。 注意：ETC2本身不支持原生HDR。如果必须在不支持ASTC HDR的设备上使用HDR探针，通常引擎会回退到未压缩的RGB16F或RGB111110F格式，但这会极大增加内存消耗，应尽量避免。 二、核心优化策略 优先启用ASTC：在Unity的Player Settings中，为iOS和Android分别设置首选的ASTC格式。这能确保在支持的设备上自动使用最优的压缩方案。 谨慎使用HDR：仅对需要捕捉强烈高光（如金属车漆、水面反光）的关键探针开启HDR。大部分环境反射使用LDR的ASTC格式即可满足需求，能大幅降低内存占用。 控制分辨率：根据探针的作用范围和距离玩家的远近来调整分辨率。远离玩家或覆盖大范围区域的探针可使用64x64甚至32x32；而用于特写或关键物体（如角色）附近的探针才使用128x128或256x256。分辨率是影响内存和渲染时间的最主要因素。 利用盒体投影（Box Projection）：对于室内或空间受限的环境，启用盒体投影能让一个探针更准确地模拟出角落和墙壁的反射，从而减少所需探针的数量，间接优化了整体性能。 综上所述，ASTC是移动端反射探针的首选格式，特别是结合其HDR特性，能在性能和视觉质量之间取得最佳平衡。 分享链接： https://www.uwa4d.com/main/uwa-shared.html?shareId=c97db263-6273-40bd-8d91-b9737aa44a16 From UWA AI 问答 Q：目前，不同平台的热更打包工作分散在不同的机器上进行。考虑到容灾需求，如果机器挂了或者硬盘挂了，重新打包可能导致玩家需要再次下载几GB的热更新资源。除了定期备份整个工程或Library文件夹之外，是否有更好的应对方案？另外，有没有什么方法能够确保不同机器、不同工程环境下，可以打出一致的AssetBundle包（Meta文件已经受版本管理控制）？ A：为确保多台机器在不同环境或地理位置下能打出一致的AssetBundle（AB包），并有效应对机器或硬盘故障导致的热更风险，需从构建一致性、缓存管理和容灾备份三个层面进行系统性设计。 1. 确保构建一致性 统一构建环境：所有打包机器必须使用完全相同的Unity版本、操作系统补丁、JDK/Python等依赖环境。建议通过Docker镜像或虚拟机模板固化环境，避免因环境差异导致GUID或序列化数据不一致。 共享与锁定Library：Library文件夹中的ScriptAssemblies、BeeTreeCache等缓存直接影响AB生成。应将关键缓存目录设为只读，并通过网络存储（如NFS）或CI/CD流水线统一分发，确保所有打包机基于相同中间产物构建。 禁用随机因子：关闭Random Seed、时间戳嵌入等可能导致输出差异的功能。若使用Addressables，确保Build Script和Provider配置完全一致。 2. 构建缓存与增量复用 集中式构建缓存服务：采用Unity Cloud Build Cache或自建Artifactory/Nexus服务器，存储每次成功构建的AB包及其元数据（Hash、清单）。当某台打包机故障…

为何iPad Pro上设置目标帧率为90时无法生效
type: item
date: 2025-11-11T04:22:00+02:00
description: 1）为何iPad Pro上设置目标帧率为90时无法生效 2）为什么在Dedicated Server发布的程序中内存数值异常 这是第452篇UWA技术知识分享的推送，精选了UWA社区的热门话题，涵盖了UWA问答、社区帖子等技术知识点，助力大家更全面地掌握和学习。 UWA社区主页：community.uwa4d.com UWA QQ群：793972859 From 问答社区 Q1：为什么iPad Pro上设置目标帧率为90时无法生效，仍然以120帧运行？ 在使用Application.targetFrameRate设置目标帧率为90时，在iPad Pro M2芯片上无法生效，目标帧率变为120，在iPhone 14 Pro上测试则能够以80的帧率运行。 在和平精英上用PerfDog采集，可以设置为90的选项，实际以80的帧率在跑，但在Demo中测试无法达到同样的效果。 A：使用UWA AI问答的功能尝试解读这个问题。 分享链接： https://www.uwa4d.com/main/uwa-shared.html?shareId=f3ea7cc1-879f-4f94-aeb5-b29860099ae2 该问题通常由垂直同步（VSync）与帧率控制机制的交互导致，尤其是在高刷新率设备如iPad Pro上。 核心原因分析 在Unity引擎中，Application.targetFrameRate是控制移动平台帧率的主要方式。然而，当垂直同步（VSync）开启时，它会优先于targetFrameRate生效。系统会强制将应用的帧率锁定到屏幕刷新率的整数分之一（如120Hz、60Hz、40Hz等），以避免画面撕裂。因此，即使设置了targetFrameRate=90，系统也无法稳定输出非刷新率整除的帧率（90不是120的整除因子），最终会退回到设备支持的最高帧率（120fps）或下一个可行的分频值。 此外，部分iOS设备（尤其是支持ProMotion技术的iPad Pro）具有动态刷新率调节能力，其底层图形驱动可能进一步影响帧率锁定行为，使得targetFrameRate的控制不够精确。 解决方案 1. 关闭VSync并依赖 targetFrameRate 确保在项目设置或运行时关闭垂直同步： QualitySettings.vSyncCount = 0; // 关闭VSync Application.targetFrameRate = 90; // 设置目标帧率为90 这是实现非标准帧率（如90fps）最直接的方法。关闭VSync后，targetFrameRate将正常生效。 2. 使用引擎级帧平滑（Frame Pacing） 在使用虚幻引擎时，可通过CVar精确控制帧率。对于目标帧率90，在配置文件中设置： r.setframepace 90 此方法直接对接设备刷新周期，能更可靠地在支持的设备上实现90fps。 3. 验证设备与系统支持 确认当前iPad Pro型号及iOS系统版本是否完整支持90Hz的输出模式。某些情况下，操作系统或驱动可能限制了非标准刷新率的使用。 4. 监控实际帧率 使用UWA GOT Online报告中的“FPS均值”和“>40ms帧数占比”指标，结合真机性能测试，验证帧率是否真正稳定在90fps，并评估GPU负载与功耗表现。 综上，欲在iPad Pro上稳定运行90fps，关键在于关闭VSync并通过Application.targetFrameRate明确指定目标帧率，同时确保引擎和设备层面的支持。 Q2：如果已经关闭垂直同步，还可能是什么原因？ A：即使已关闭垂直同步（VSync），在iPad Pro上设置Application.targetFrameRate = 90仍可能无法生效，原因如下： 1. 动态刷新率机制（ProMotion）的底层限制 iPad Pro搭载的ProMotion技术允许屏幕刷新率在120Hz、60Hz、40Hz、30Hz等特定档位间动态切换。系统倾向于选择最接近目标帧率的标准分频值。由于90不是120的整除因子，系统可能无法为90fps创建稳定的刷新周期，最终退回到默认的120Hz模式。 2. Unity引擎的帧时间计算与平台适配问题 根据Unity文档，移动平台上的targetFrameRate实际效果受限于屏幕刷新率的整数分频。即使VSync关闭，Unity的帧调度器仍可能因无法精确匹配90Hz的间隔而出现抖动或回落至120Hz。此外，若未将targetFrameRate设置为屏幕刷新率的约数（如120 → 60, 40, 30），系统可能忽略该设置。 3. 后台进程或系统策略干预 iOS系统可能会因性能管理、温控或后台任务（如应用更新、索引服务）临时提升设备性能至最大刷…

游戏AI行为决策——MLP（多层感知机/人工神经网络）
type: item
date: 2025-11-06T04:22:00+02:00
description: 【USparkle专栏】如果你深怀绝技，爱“搞点研究”，乐于分享也博采众长，我们期待你的加入，让智慧的火花碰撞交织，让知识的传递生生不息！ 你一定听说过神经网络的大名，你有想过将它用于游戏AI的行为决策上吗？其实在（2010年发布的）《最高指挥官2》中就有应用了，今天请允许我班门弄斧一番，与大家一同用C# 实现最经典的神经网络 —— 多层感知机（Multilayer Perceptron，简称MLP）。 一、前言 神经网络或者深度学习，总给人一种「量子力学」的感觉，总感觉它神秘无比，又无所不能。我未学习神经网络之前，总以为它是某种能够修改自身代码的代码，否则怎么能做到从「不会」变成「会」的呢？但在亲自学习后才会明白，它并没有做到这种地步，但依旧十分神奇。多层感知机是最基础的神经网络，很多其它类别的神经网络都是在这之上的变形。可以说，学会它是迈入深度学习的第一步。 多层感知机虽说经典，但并不过时。提到神经网络，大多数人脑海里想到的大概也就是类似这样的图片： 这就是一张典型的多层感知机结构图，看着好像很复杂，但实现它所需要用到的数学原理和编程知识都不难，早年间，研究神经网络的学者们还用C语言实现呢！ 二、什么是多层感知机 现在进入正题，我们先来简单讲讲MLP的原理（如果你对此十分熟悉，只是对代码实现感兴趣，那可以跳过这部分）。 既然叫「多层感知机」，那有单个的感知机吗？那是自然，单个感知机的结构十分简单： 它其实就是个算式（为方便理解，我将其分成两部分）： 将传入感知机的多个输入x，与对应的权重w相乘（输入的数量与权重的数量是一样的，且数量是任意的，本例中用了3个），再加上偏置b就可以得出一个计算值sum。再将这个计算值传入一个函数f(x)就可得到感知机的最终输出out。 相信你肯定能理解，只是可能对f(x)函数有些好奇，它具体内容是什么呢？这个函数也被称为「激活函数」，为什么叫这个名字？这就得提感知机的另一个名字 —— 人工神经元。其实感知机正是受神经元结构启发而被提出来的： 神经元会通过树突接受输入信号并汇总，而神经元对于各个输入刺激的响应强度并不相同，所以我们给各个输入设置了相应权重来模拟这个现象。之后，将处理的信息通过轴突传给末梢（终端）。但实际上，只有汇总的信号强度大于一定程度时神经元才会向末梢传递信号，而模拟这个现象的就是「激活函数」。 那偏置b又是模拟什么的？其实它是从数学角度考虑的、方便调整输入加权和的变量值而已。 既然感知机被称为「人工神经元」，那多层感知机岂不就是「人工神经网络」？一点没错，我们现在所说的「神经网络」，基本都是指人工神经网络，而不是真正的、生物的神经网络。而「神经网络」起初就是指多层感知机，只不过现在种类多了，定义也变宽泛了。 结合我们对单个感知机的认识，再看看多层感知机： 但这里的单个感知机（后面用「神经元」来代称）怎么输出了多个值（看标蓝色的那部分）？这种结构图可能会误导某些人，我做个解释，这里的每一条线并不是输出，可以看到它是有箭头的，每条线表示它所指向的那个神经元的一个权重。加上箭头是为了表示数据传递的方向。 不难看出，它就是将多个感知机以层为单位进行了组合，每层都有任意数量（每层的数量可以不同）的感知机，并将一层感知机的输出作为下一层的输入，依次套娃下去。像图中，下一层神经元的权重数量 = 上一层的神经元数量，称为全连接，是神经网络中常见的连接方式，本文也只考虑这种连接方式。 这里的「输入层」其实就是输入的数据（是的，这一层不是神经元），类似之前的x0、x1、x2；「输出层」就是用于输出的神经元所组成的层，有了多个感知机，我们也可以得到多个输出；夹在「输入层」与「输出层」之间的就叫「隐藏层」，因为在实际使用神经网络时，就只是输入一组数值作为「输入层」，再看看「输出层」得到的结果，并不关心中间的运算。 我们常说的「深度学习」里的「深度」指的就是神经网络中「隐藏层」的层数（只不过现在这个词有点被炒作了），当一个神经网络的隐藏层超过3层时，它就是「深度神经网络」。 通过改变神经网络的结构或者调节神经网络的权重和偏置，我们可以用神经网络近似任何的函数、甚至是一些摸不着头脑的规律。 比如影响小明今天玩不玩网游的因素有：今日作业量、心情、本月剩余流量、今天是星期几，但我们并不知道这些因素与小明玩不玩的具体数学关系，只能大概地推断：今天小明作业多，不会玩游戏；又或者今天是星期六，虽然作业还有很多，但他还是会玩游戏……可一旦知道具体数学关系，我们就可以通过计算准确预测小明是否会玩游戏，就像我们知道了牛顿力学公式，就可以根据物体的质量和被射出的力来计算它的运动轨迹一样。 所以我们所关心的、实际所使用的都是这种已经设置好正确权重和偏置的神经网络，像在与GPT聊天时怕「污染数据库」这类事就不用操心了。 要如…

导致Resources文件夹的资源在Android打包后丢失的原因
type: item
date: 2025-11-05T04:42:00+02:00
description: 1）导致Resources文件夹的资源在Android打包后丢失的原因 2）GPU Culled Primitive中的xyz三平面剔除与视锥体剔除有什么区别 3）按照不升级Unity引擎小改动快速适配安卓16KB页面要求操作后还是闪退 这是第451篇UWA技术知识分享的推送，精选了UWA社区的热门话题，涵盖了UWA问答、社区帖子等技术知识点，助力大家更全面地掌握和学习。 UWA社区主页：community.uwa4d.com UWA QQ群：793972859 From UWA AI 问答 Q：请问Resources文件夹的资源在Android打包后丢失是什么导致的呢？有resources.load显式调用的。 A：Resources文件夹中的资源在Android打包后丢失，尽管有Resources.Load显式调用，通常并非资源未被打包进APK，而是由以下几种原因导致加载失败或资源无法访问： 1. 路径与大小写问题 Resources.Load的路径是不区分大小写的，但某些Android设备（尤其是定制ROM）的文件系统对路径敏感。如果代码中指定的路径与实际文件夹/文件名的大小写不完全一致，可能在部分设备上加载失败。确保路径字符串与Resources文件夹内的资源命名（包括嵌套文件夹）保持一致。 此外，路径应使用正斜杠“/”，且不包含“.asset”等扩展名。 2. 构建过程中资源被剥离或冲突 同名资源冲突：若项目中存在多个Resources文件夹，并且包含同名但不同内容的资源，Unity在构建时行为不确定，可能导致错误的资源被打包或覆盖。 Addressables或AssetBundle冲突：如果同一资源同时被标记为Addressable或打包进AssetBundle，又存在于Resources文件夹，可能会因构建流程混乱导致资源引用异常。建议避免资源重复管理。 3. 热更新或补丁机制干扰 使用Application Patching或自定义热更方案时，若处理不当可能导致StreamingAssets目录下原始资源被替换或删除。虽然Resources资源通常打包在主APK内，但在某些增量更新逻辑中，如果误操作了内部文件结构，也可能影响资源读取。 4. 安装路径变化（特定厂商手机） 部分国产手机（如华为、荣耀）将应用安装到SD卡时，路径中可能出现特殊字符（如 -），若项目中有自定义资源加载逻辑依赖路径拼接，未正确转义可能导致文件打开失败。虽然Resources.Load本身不受此影响，但如果涉及从外部加载补充资源并动态注册，则需注意。 5. 资源未实际打入包体 虽然Resources文件夹下的资源默认会被打包，但以下情况可能导致例外： 构建过程中出现异常中断，导致资源未完整写入。 使用了条件编译或脚本化构建流程，意外排除了Resources文件夹。 可通过解压APK文件（位于AndroidStudioProject/app/build/outputs/apk/或Unity导出目录），检查assets/bin/Data/resources.assets是否存在且包含预期资源，以确认资源是否成功打包。 排查建议 使用UWA GOT Online报告中的Resources.Load调用详情页，查看该资源的加载次数、耗时及是否返回null。 在真机运行时添加日志，捕获Resources.Load返回结果，确认是否为空。 检查目标设备的存储路径和安装方式，排除厂商ROM特殊行为影响。 清理构建环境，重新执行完整构建流程，避免增量构建残留问题。 综上，最可能的原因是路径问题或构建过程中的资源冲突，建议优先验证资源是否存在以及加载路径准确性。 分享链接： https://www.uwa4d.com/main/uwa-shared.html?shareId=88f1787d-7dc5-4d8a-91c2-09dc8d9c2258 From UWA AI 问答 Q：报告中的GPU Culled Primitive中的XY平面和Z平面剔除，和视锥体剔除是一样的吗？ A：GPU Culled Primitive中的剔除机制与视锥体剔除密切相关，但“XY平面和Z平面剔除”这一表述并不准确，实际并不存在独立的“XY平面”或“Z平面”剔除方式。通常所说的图元剔除主要指视锥体剔除（Frustum Culling），它是一种基于摄像机视锥空间的裁剪技术，判断物体是否位于可视范围内。 在图形渲染流程中，图元（如三角形）经过顶点着色器处理后，会进行一系列裁剪操作。其中，被裁剪掉的图元数量（culled_primitives）反映的是那些经过顶点计算后，最终因不在摄像机视野内而未被光栅化的图元。这部分图元的产生，主要受视锥体剔除机制影响 —— 即只有处于视锥内…

为什么Android游戏画面在30帧运行时有抖动现象
type: item
date: 2025-10-28T05:10:00+02:00
description: 1）为什么Android游戏画面在30帧运行时有抖动现象 2）团结引擎VS Skinning开启ENABLE_VS_SKINNING_MORE_BONES后，性能反而下降 这是第450篇UWA技术知识分享的推送，精选了UWA社区的热门话题，涵盖了UWA问答、社区帖子等技术知识点，助力大家更全面地掌握和学习。 UWA社区主页：community.uwa4d.com UWA QQ群：793972859 From UWA AI 问答 Q1：Android平台下，游戏以30帧运行时，即便整体性能稳定，仍普遍存在画面抖动现象，想探讨一下这可能是由于什么原因造成的？ A：画面抖动（Jitter）是Android平台30帧游戏中常见但易被忽视的问题，即便整体FPS稳定，仍可能因以下原因导致视觉上的不流畅或抖动感： 1. 帧生成时间不均匀（Frame Pacing问题） 这是最核心的原因。即使平均帧率为30FPS，若帧的生成间隔不均匀（如一帧耗时28ms，下一帧耗时38ms），会导致视觉上出现“微卡顿”或“抖动”。Android设备尤其在高刷新率屏幕（如90Hz、120Hz）下，若未正确配置帧同步策略，GPU与显示子系统之间的帧提交节奏失配，极易引发此问题。 解决方案： 确保关闭Optimized Frame Pacing（部分模拟器和低端设备存在兼容性问题，实测可缓解抖动）。 使用Choreographer回调来对齐VSync信号，确保帧更新时机精准。 在Unity中，避免仅依赖Application.targetFrameRate=30，因其在动态刷新率设备上可能导致帧率倍数下降（如从30→15）。 可结合定时重设帧率策略： IEnumerator SetFrameRatePeriodically() { while (true) { yield return new WaitForSeconds(10); Application.targetFrameRate = 31; yield return new WaitForSeconds(0.1f); Application.targetFrameRate = 30; } } 2. GPU与CPU工作负载波动引起帧时间波动 即使平均性能达标，若每帧CPU或GPU耗时波动大（如某些帧CPU耗时接近33ms，其他帧仅15ms），会造成帧间隔不一致，表现为抖动。 排查建议： 使用UWA GOT Online的CPU每帧耗时曲线、>40ms帧数占比指标，分析是否存在高波动性。 检查是否存在偶发性任务（如GC、资源加载、逻辑计算集中爆发），导致单帧骤增。 3. 渲染管线与垂直同步机制冲突 Android设备厂商常自定义显示策略，部分机型在非原生刷新率（如60Hz设备跑30Hz）时，系统VSync调度可能出现偏差，导致帧呈现时间错位。 优化方向： 避免频繁切换目标帧率，保持帧率恒定。 在支持的设备上尝试使用SurfaceFlinger相关调试工具观察实际显示帧时序。 若使用自定义渲染管线，检查后处理、阴影等异步操作是否引入延迟。 4. 输入与渲染不同步 触摸输入更新频率（通常60Hz或更高）与30Hz渲染不同步，可能导致操作反馈延迟感叠加视觉抖动。 改进措施： 尽量将关键UI更新与渲染帧对齐。 对高频输入做插值或预测处理，提升交互平滑度。 综上，画面抖动多源于帧间时间不一致性而非平均帧率不足。建议优先通过UWA性能报告中的Jank均值、>40ms帧数占比及CPU每帧耗时曲线进行量化分析，并针对性优化帧生成节奏与负载均衡。 Q2：有什么解决方案？ A：针对Android平台30帧下画面抖动问题，可采取以下系统性解决方案： 1. 精准控制帧率同步（核心措施） 关闭Optimized Frame Pacing：该功能在部分设备（如MuMu模拟器）存在兼容性问题，易导致帧提交延迟。在Player Settings中禁用此选项可显著改善抖动。 使用Choreographer对齐VSync：通过Android原生API确保每帧更新与系统刷新信号同步，避免因调度偏差导致的帧间隔不均。 周期性重设目标帧率：应对动态刷新率设备（如120Hz降为60Hz时30帧变15帧），可定时重置targetFrameRate以强制系统重新计算： IEnumerator SetFrameRate() { while (true) { yield return new WaitForSeconds(10); Application.targetFrameRate = 31; yield return new WaitForSeconds(0.1f); Application.targetFrameRate = 30; } } 2. 优化…

对Android游戏画面抖动现象的研究
type: item
date: 2025-10-22T05:05:00+03:00
description: 【USparkle专栏】如果你深怀绝技，爱“搞点研究”，乐于分享也博采众长，我们期待你的加入，让智慧的火花碰撞交织，让知识的传递生生不息！ 一、前言 近期笔者一直在研究一个专题：在Android平台下，游戏以30帧运行时，即便整体性能稳定，仍普遍存在画面抖动现象。即使是知名厂商的游戏也不例外。例如笔者测试《星穹轨道》时发现： 角色跑动时留意那盆绿植会比较明显 可能是出于功耗的考虑，一进来就默认推荐了30帧，玩起来也有持续性的画面抖动，不像是偶发的卡顿，我的手机性能是较好的，不至于连30帧都跑不流畅。 通过一段时间的研究，对这个问题的原理和解决方案有了一定的见解。 二、原因分析 1. 显示机制 Android的显示机制是一个典型的生产者-消费者模型。应用端负责生产画面，通过调用SwapBuffer将内容写入BufferQueue，系统端（SurfaceFlinger进程）则从BufferQueue中取出内容，执行后续的合成与显示流程。 SurfaceFlinger取BufferQueue的节奏是根据系统Vsync-sf信号调节的，一般跟屏幕刷新率走，比如60hz那么就是16.6ms为一个周期定期触发，当游戏为30帧时，平均是33.3ms生产一个Buffer，生产的速率远远慢于消耗的速率，当Vsync-sf信号来临但BufferQueue里面为空时，系统会重复显示上一帧的内容。 理想情况下，在60Hz屏幕下运行30帧，SurfaceFlinger应该每两个周期消耗一帧内容。然而通过分析发现，实际的消耗周期并不均匀：有时一个周期消耗，有时甚至三个周期消耗。导致每帧画面在屏幕上的停留时间不一致，从而在视觉上产生抖动感。 2. 量化工具 接下来使用Perfetto来看一下这个游戏，Perfetto是Android上系统级别的Trace工具，可以使用它来观察具体的BufferQueue、Vsync情况。 Perfetto有一些配置，针对画面抖动问题需要确认开启ATrace的gfx、view模块： 下图中标注了每一次Buffer被消耗时，距离上一次的周期数： 这里筛选出了Trace数据中相关的轨道，从BufferQueue里面可以看到大多数情况是2个周期消耗一次，偶尔会有1、3个周期出现。 Perfetto生成的Trace文件可以通过SQL进行统计。笔者统计了每次BufferQueue减少时的时间点，进一步计算了这些时间间隔的标准差。该指标可用于量化画面抖动的严重程度，为后续方案对比与优化提供了数据基础。 完整SQL内容在附录中提供 三、目前的解决方案 1. Swappy方案 Swappy是Google GameSDK的一部分，这里是它的官方介绍： https://developer.android.com/games/sdk/frame-pacing?hl=zh-cn *上述网址需要使用VPN打开 它是大多数游戏引擎内置的方案，以Unity引擎为例直接设置一下生效就行。 此方案核心是通过两个EGL拓展来达成它的目的： 通过setPresentationTime，为Buffer设置一个时间戳，避免此Buffer被过快消耗掉。 回到之前我们的例子，如果这里设置了一个允许被消耗的时间戳，那么这里不会存在1个周期就消耗掉的情况： 利用EGL_KHR_fence_sync，可以追踪上一帧GPU处理完毕再将当前帧Swap进去。由于实际游戏运行过程中不会是平稳地每次33.3ms Swap一次，会有波动，此策略可以避免局部生产过快导致的BufferQueue堆积。 Hook了Swappy中的一些函数，将其标注到了Perfetto中以观察其运行过程： 当然，笔者在实际使用过程中，也是遇到了不少问题： 掉帧，常见于开60帧的情况，因为要等上一帧GPU结束，不同手机追踪到的GPU时间差异大，有的要10几ms，再加上Swappy是以周期为单位进行等待的，很容易延后的时间太多，最终导致帧率下降严重，所以建议60帧时不启用Swappy机制。 延迟明显，一方面是渲染线程进行Swap的平均时间是要延后很多的，另一个方面是主线程是先做逻辑Update，然后通过WaitForPendingPresent等待上一帧渲染线程结束后，再继续，从Update到最终呈现的链路多了一个流程，30帧时预估延迟会增加30~40ms左右，体感明显。 不稳定，不同机型效果差异很大，观察了一下发现有些机型setPresentationTime调用后，实际生效的时间并不精确，还有些机型60hz的屏幕刷新率但程序返回的值是59、58这些奇怪的值，可能会导致功能失效、锁帧之类的问题。 总的来说Swappy对画面抖动是有一定效果的，上述问题有些也好处理，只是此方案虽然开启简单，但决策前仍然要权衡、多机型…