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

游戏人物移动效果对应实际刷新率对比与Client-side Prediction Interpolation调整优化

前言

在制作游戏中任何图片的移动都会因为FPS设置的问题导致游戏体验度相对弱了很多,但是很多时候又不能放开了做,毕竟图片大了,刷新率在高一些很多手机或PC就无法正常的跑起来,这就很难受,我今天做了个对比测试,用一个60FPS的与低FPS的做了个对比,后面我对10FPS的单独用AI做了个优化,效果还是有一些的,希望能给大家创造一些价值。

项目开始

我这里纯纯的使用手机端做测试效果。

核心 FPS 处理函数与算法逻辑

阶段核心逻辑实现算法说明视觉效果
1. 物理位置计算this.posX_smooth += this.speed * deltaTime;基于真实时间增量(DeltaTime)计算物体的理论物理位置,作为全局参考坐标系。绝对丝滑 (60Hz+)
2. 低频采样模拟if (now - lastUpdate >= jitterInterval) { targetPos = posX_smooth; }模拟低性能环境下的非连续采样。只有在达到设定的时间间隔(如 100ms)时才同步物理坐标。视觉断裂、跳变感
3. 线性插值 (LERP)lastPos + (targetPos - lastPos) * lerpFactor;核心优化算法。在两次采样点之间,根据当前帧距离上次采样的时间进度,手动计算中间补帧。模拟高帧率的连贯性
4. 运动预测补偿posX_optimized += this.speed * deltaTime;当插值进度完成但新采样尚未到达时,根据物体历史速度进行惯性预测,消除微小的停顿感。消除采样延迟抖动

无算法自己跑的各数值FPS与60FPS对比

@Entry @Component struct Index { @State posX_smooth: number = 0; @State posX_jitter: number = 0; @State selectedFps: number = 15; @State isRunning: boolean = true; private screenWidth: number = 360; private lastTime: number = 0; private jitterLastUpdate: number = 0; private speed: number = 80; // 降低移动速度,从200调至80,更方便观察刷新率细节 onPageShow() { this.runLoop(); } runLoop() { let interval = 16; // ~60fps 的物理更新频率 setInterval(() => { if (!this.isRunning) return; const now = Date.now(); if (this.lastTime === 0) { this.lastTime = now; this.jitterLastUpdate = now; } const deltaTime = (now - this.lastTime) / 1000; this.lastTime = now; // 1. 标准移动逻辑(平滑参考):每 16ms 计算一次位移 this.posX_smooth += this.speed * deltaTime; if (this.posX_smooth > 300) { this.posX_smooth = -50; // 循环滚动 } // 2. 刷新率问题演示:仅在满足用户选择的 FPS 时间间隔时,才更新视觉坐标 const jitterInterval = 1000 / this.selectedFps; if (now - this.jitterLastUpdate >= jitterInterval) { // 将“物理位置”同步到“视觉位置”,造成跳变感 this.posX_jitter = this.posX_smooth; this.jitterLastUpdate = now; } }, interval); } build() { Stack() { // 1. 梯度渐变背景 (深邃生命科学风格) Column() .width('100%') .height('100%') .linearGradient({ angle: 135, colors: [['#0F2027', 0], ['#203A43', 0.5], ['#2C5364', 1]] }) // 2. 内容层 Column({ space: 30 }) { Text("生命体运动刷新率对比") .fontSize(28) .fontWeight(FontWeight.Bold) .fontColor('#FFFFFF') .margin({ top: 60 }) .textShadow({ radius: 10, color: '#44FFFFFF', offsetX: 2, offsetY: 2 }) Text("模拟微观粒子在不同采样率下的视觉差异") .fontSize(14) .fontColor('#A0A0A0') // 演示区域 Column({ space: 50 }) { // 顺滑组 VStack({ title: "标准刷新 (60Hz)", posX: this.posX_smooth, opacityValue: 1, color: '#4CAF50' }) // 模拟组 VStack({ title: `模拟刷新 (${this.selectedFps}Hz)`, posX: this.posX_jitter, opacityValue: 0.8, color: this.selectedFps < 30 ? '#F44336' : '#FFEB3B' }) } .width('90%') .padding(20) .backgroundColor('#1AFFFFFF') .borderRadius(20) .border({ width: 1, color: '#33FFFFFF' }) // 玻璃拟态效果 .backgroundBlurStyle(BlurStyle.Thin) // 控制区 Column({ space: 20 }) { Text("调节模拟刷新率") .fontColor('#FFFFFF') .fontSize(16) Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Center, alignContent: FlexAlign.Center }) { ForEach([5, 10, 15, 24, 30, 60], (fps: number) => { Button(`${fps} FPS`) .onClick(() => { this.selectedFps = fps; }) .backgroundColor(this.selectedFps === fps ? '#007DFF' : '#33FFFFFF') .fontColor('#FFFFFF') .borderRadius(10) .width('40%') .margin(5) }) } .width('100%') } .width('90%') .padding(20) .backgroundColor('#0D000000') .borderRadius(20) } .width('100%') .height('100%') } } } @Component struct VStack { @Prop title: string = ""; @Prop posX: number = 0; @Prop opacityValue: number = 1; @Prop color: string = "#FFFFFF"; build() { Column() { Row() { Circle({ width: 8, height: 8 }).fill(this.color).margin({ right: 8 }) Text(this.title) .fontColor('#E0E0E0') .fontSize(14) } .width('100%') .margin({ bottom: 15 }) // 运动轨道 Stack({ alignContent: Alignment.Start }) { // 轨道线 Rect() .width('100%') .height(2) .fill('#22FFFFFF') // 粒子 (demo.png) Image($r('app.media.demo')) .width(60) .height(60) .offset({ x: this.posX, y: 0 }) .shadow({ radius: 20, color: this.color }) .interpolation(ImageInterpolation.High) } .width('100%') .height(80) .opacity(this.opacityValue) } } }

优化算法效果

算法核心函数抽离

/** * 刷新率优化核心循环 (每 16ms 执行一次) * @param now 当前系统时间 * @param deltaTime 距离上一帧的时间间隔 */functionupdateFrameOptimized(now:number,deltaTime:number){// A. 计算物理参考位置 (标准速度同步)this.posX_smooth+=this.speed*deltaTime;// B. 模拟低频采样 (例如 10FPS)constjitterInterval=1000/this.selectedFps;if(now-this.jitterLastUpdate>=jitterInterval){this.lastJitterPos=this.posX_jitter;// 记录起点this.targetJitterPos=this.posX_smooth;// 设定终点this.posX_jitter=this.posX_smooth;// 更新采样点this.jitterLastUpdate=now;this.lerpFactor=0;// 重置插值进度}// C. 插值算法优化:在 10FPS 的采样缝隙中“凭空”创造流畅度if(this.lerpFactor<1){// 计算插值进度:在采样周期内完成 0.0 到 1.0 的平滑过渡this.lerpFactor+=deltaTime*this.selectedFps;if(this.lerpFactor>1)this.lerpFactor=1;// 执行线性插值 (Linear Interpolation)this.posX_optimized=this.lastJitterPos+(this.targetJitterPos-this.lastJitterPos)*this.lerpFactor;}else{// 预测移动:在等待下一次采样时,按照速度惯性继续微移,防止“原地踏步”this.posX_optimized+=this.speed*deltaTime;}}

@Entry @Component struct Index { @State posX_smooth: number = 0; @State posX_jitter: number = 0; @State posX_optimized: number = 0; // 算法优化后的位置 @State selectedFps: number = 10; @State isRunning: boolean = true; private screenWidth: number = 360; private lastTime: number = 0; private jitterLastUpdate: number = 0; private speed: number = 80; // 算法插值相关变量 private lastJitterPos: number = 0; private targetJitterPos: number = 0; private lerpFactor: number = 0; onPageShow() { this.runLoop(); } runLoop() { let interval = 16; setInterval(() => { if (!this.isRunning) return; const now = Date.now(); if (this.lastTime === 0) { this.lastTime = now; this.jitterLastUpdate = now; } const deltaTime = (now - this.lastTime) / 1000; this.lastTime = now; // 1. 标准移动 (参考系) this.posX_smooth += this.speed * deltaTime; if (this.posX_smooth > 300) { this.posX_smooth = -50; } // 2. 10FPS 采样逻辑 const jitterInterval = 1000 / this.selectedFps; if (now - this.jitterLastUpdate >= jitterInterval) { this.lastJitterPos = this.posX_jitter; this.targetJitterPos = this.posX_smooth; this.posX_jitter = this.posX_smooth; this.jitterLastUpdate = now; this.lerpFactor = 0; // 重置插值进度 } // 3. 算法优化:线性插值 (LERP) + 预测 // 虽然采样只有 10FPS,但我们在每帧 (16ms) 进行平滑补偿 if (this.lerpFactor < 1) { this.lerpFactor += deltaTime * (this.selectedFps); // 在采样周期内完成移动 if (this.lerpFactor > 1) this.lerpFactor = 1; // 核心算法:当前视觉位置 = 上一次采样点 + (目标采样点 - 上一次采样点) * 进度 this.posX_optimized = this.lastJitterPos + (this.targetJitterPos - this.lastJitterPos) * this.lerpFactor; } else { // 预测移动:在等待下一次采样时,按照速度惯性继续微移 this.posX_optimized += this.speed * deltaTime; } }, interval); } build() { Stack() { // 1. 梯度渐变背景 Column() .width('100%') .height('100%') .linearGradient({ angle: 135, colors: [['#0F2027', 0], ['#203A43', 0.5], ['#2C5364', 1]] }) // 2. 内容层 Column({ space: 20 }) { Text("AI 插值算法优化演示") .fontSize(28) .fontWeight(FontWeight.Bold) .fontColor('#FFFFFF') .margin({ top: 40 }) Text("在 10FPS 采样下实现 60FPS 的丝滑度") .fontSize(14) .fontColor('#00E5FF') // 演示区域 Column({ space: 30 }) { // 顺滑组 (60Hz) VStack({ title: "原生 60Hz 采样 (丝滑)", posX: this.posX_smooth, opacityValue: 1, color: '#4CAF50' }) // 优化组 (10Hz + 算法) VStack({ title: `10Hz 采样 + 线性插值算法 (模拟丝滑)`, posX: this.posX_optimized, opacityValue: 1, color: '#007DFF' }) // 原始低帧率组 (10Hz) VStack({ title: `原生 10Hz 采样 (肉眼可见卡顿)`, posX: this.posX_jitter, opacityValue: 0.6, color: '#F44336' }) } .width('95%') .padding(15) .backgroundColor('#1AFFFFFF') .borderRadius(20) .backgroundBlurStyle(BlurStyle.Thin) // 控制区 Column({ space: 15 }) { Text("调节基础采样率") .fontColor('#FFFFFF') .fontSize(16) Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Center }) { ForEach([5, 10, 24, 60], (fps: number) => { Button(`${fps} FPS`) .onClick(() => { this.selectedFps = fps; }) .backgroundColor(this.selectedFps === fps ? '#007DFF' : '#33FFFFFF') .fontColor('#FFFFFF') .borderRadius(10) .width('20%') .margin(5) }) } .width('100%') } .width('95%') .padding(15) .backgroundColor('#0D000000') .borderRadius(20) } .width('100%') .height('100%') } } } @Component struct VStack { @Prop title: string = ""; @Prop posX: number = 0; @Prop opacityValue: number = 1; @Prop color: string = "#FFFFFF"; build() { Column() { Row() { Circle({ width: 8, height: 8 }).fill(this.color).margin({ right: 8 }) Text(this.title) .fontColor('#E0E0E0') .fontSize(14) } .width('100%') .margin({ bottom: 15 }) // 运动轨道 Stack({ alignContent: Alignment.Start }) { // 轨道线 Rect() .width('100%') .height(2) .fill('#22FFFFFF') // 粒子 (demo.png) Image($r('app.media.demo')) .width(60) .height(60) .offset({ x: this.posX, y: 0 }) .shadow({ radius: 20, color: this.color }) .interpolation(ImageInterpolation.High) } .width('100%') .height(80) .opacity(this.opacityValue) } } }

效果总结

  • 原生 10FPS:每秒刷新 10 次,物体移动呈【瞬移】状态,容易引起视觉疲劳。
  • 10FPS + LERP 优化:虽然有效数据只有 10 次,但屏幕每秒依然渲染 60 次,中间的 50 次渲染由算法根据轨迹预测生成。视觉上几乎等同于 60FPS 的流畅度。

升级优化效果5~120FPS全优化

这套 Demo 实现了一套模拟现代游戏引擎中【客户端预测与平滑】(Client-side Prediction & Interpolation)的核心算法。


核心解释:无论屏幕刷新率是多少,物体的真实物理位置(posX_smooth)始终基于真实时间增量(DeltaTime)进行线性累加。
人为地“掐断”物理层的实时反馈。只有当到达设定的采样时间点(如 10FPS,即每 100ms)时,才会从物理层“偷”一个坐标给它。


公式:当前位置 = 上一帧位置 + (移动速度 * 帧间隔时间)。


预算层的处理:

动态速度估算:算法不依赖于代码中写死的 speed,而是通过计算最近两次采样点(currentSamplePos - lastSamplePos)的位移差,除以采样时间间隔,计算出物体的体感速度(estimatedVelocity)。

外推预测 (Extrapolation):在等待下一次采样的过程中,算法会启动一个本地计时器(renderTime)。它假设物体会按照刚才计算出的体感速度继续运动。

计算公式:预测位置 = 最后一次采样坐标 + (体感速度 * 距离上次采样的时间)。

@Entry @Component struct Index { @State posX_smooth: number = 0; @State posX_jitter: number = 0; @State posX_optimized: number = 0; @State selectedFps: number = 10; @State isRunning: boolean = true; // 核心增强变量 private lastTime: number = 0; private jitterLastUpdate: number = 0; private speed: number = 80; // 运动状态捕捉 private lastSamplePos: number = 0; private currentSamplePos: number = 0; private estimatedVelocity: number = 0; // 动态速度估算 private renderTime: number = 0; // 自定义渲染时钟 onPageShow() { this.runLoop(); } runLoop() { let interval = 8; // 将物理更新频率提升至 ~120fps (1000ms / 120 ≈ 8ms) setInterval(() => { if (!this.isRunning) return; const now = Date.now(); if (this.lastTime === 0) { this.lastTime = now; this.jitterLastUpdate = now; } const deltaTime = (now - this.lastTime) / 1000; this.lastTime = now; this.renderTime += deltaTime; // 1. 物理引擎参考 (物理层) this.posX_smooth += this.speed * deltaTime; if (this.posX_smooth > 320) this.posX_smooth = -40; // 2. 动态采样逻辑 (传输层) const jitterInterval = 1000 / this.selectedFps; if (now - this.jitterLastUpdate >= jitterInterval) { const intervalSec = (now - this.jitterLastUpdate) / 1000; this.lastSamplePos = this.currentSamplePos; this.currentSamplePos = this.posX_smooth; // 核心升级:动态计算采样间的瞬时速度,用于更精准的预测 if (intervalSec > 0) { this.estimatedVelocity = (this.currentSamplePos - this.lastSamplePos) / intervalSec; } this.posX_jitter = this.currentSamplePos; this.jitterLastUpdate = now; this.renderTime = 0; // 重置渲染本地时钟,同步采样点 } // 3. 全局通用优化算法 (表现层 - 状态预测渲染) // 使用 Hermite 插值或简单的二次预测,这里采用更稳定的速度增量预测法 // 这种算法不依赖于特定的 FPS,它会自动适应任何采样频率 let predictionOffset = this.estimatedVelocity * this.renderTime; // 限制预测范围,防止在极低 FPS 或剧烈加减速时产生飞出去的幻觉 (Error Correction) let rawOptimized = this.currentSamplePos + predictionOffset; // 软同步:让优化后的位置向物理位置进行平滑靠拢 this.posX_optimized = rawOptimized * 0.8 + this.posX_smooth * 0.2; }, interval); } build() { Stack() { // 1. 梯度渐变背景 Column() .width('100%') .height('100%') .linearGradient({ angle: 135, colors: [['#0F2027', 0], ['#203A43', 0.5], ['#2C5364', 1]] }) // 2. 内容层 Column({ space: 20 }) { Text("AI 插值算法优化演示") .fontSize(28) .fontWeight(FontWeight.Bold) .fontColor('#FFFFFF') .margin({ top: 40 }) Text("通用状态预测与误差纠正算法 (支持至 120Hz)") .fontSize(14) .fontColor('#00E5FF') // 演示区域 Column({ space: 30 }) { // 顺滑组 (120Hz) VStack({ title: "原生 120Hz 采样 (极度丝滑)", posX: this.posX_smooth, opacityValue: 1, color: '#4CAF50' }) // 优化组 (通用预测算法) VStack({ title: `全帧率通用预测算法 (当前采样: ${this.selectedFps}Hz)`, posX: this.posX_optimized, opacityValue: 1, color: '#007DFF' }) // 原始低帧率组 VStack({ title: `原生 ${this.selectedFps}Hz 采样 (未优化)`, posX: this.posX_jitter, opacityValue: 0.6, color: '#F44336' }) } .width('95%') .padding(15) .backgroundColor('#1AFFFFFF') .borderRadius(20) .backgroundBlurStyle(BlurStyle.Thin) // 控制区 Column({ space: 15 }) { Text("调节基础采样率") .fontColor('#FFFFFF') .fontSize(16) Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Center }) { ForEach([5, 15, 30, 60, 90, 120], (fps: number) => { Button(`${fps} FPS`) .onClick(() => { this.selectedFps = fps; }) .backgroundColor(this.selectedFps === fps ? '#007DFF' : '#33FFFFFF') .fontColor('#FFFFFF') .borderRadius(10) .width('25%') .margin(5) }) } .width('100%') } .width('95%') .padding(15) .backgroundColor('#0D000000') .borderRadius(20) } .width('100%') .height('100%') } } } @Component struct VStack { @Prop title: string = ""; @Prop posX: number = 0; @Prop opacityValue: number = 1; @Prop color: string = "#FFFFFF"; build() { Column() { Row() { Circle({ width: 8, height: 8 }).fill(this.color).margin({ right: 8 }) Text(this.title) .fontColor('#E0E0E0') .fontSize(14) } .width('100%') .margin({ bottom: 15 }) // 运动轨道 Stack({ alignContent: Alignment.Start }) { // 轨道线 Rect() .width('100%') .height(2) .fill('#22FFFFFF') // 粒子 (demo.png) Image($r('app.media.demo')) .width(60) .height(60) .offset({ x: this.posX, y: 0 }) .shadow({ radius: 20, color: this.color }) .interpolation(ImageInterpolation.High) } .width('100%') .height(80) .opacity(this.opacityValue) } } }

所有代码均已贡献,希望能对大家有些帮助。

http://www.jsqmd.com/news/432998/

相关文章:

  • DeepSeek V4,下周正式登场!
  • Mask2Former图像分割技术解析[特殊字符]
  • 2026年3月手持激光焊机厂家推荐,产能专利环保三维数据全面透视 - 品牌鉴赏师
  • 【无人机编队】基于人工势场算法的多无人机复杂障碍物环境下的自主避障与路径规划附Matlab代码
  • Benchmark:大数组随机访问,和取模乘法
  • 【机器人】四足机器人+正运动设计+逆运动学解算+步态设计Matlab程序
  • 降AI工具年度盘点:2026上半年哪些工具值得续费? - 还在做实验的师兄
  • 2026年3月市场青睐的超高压反应釜厂商,速来了解,深海设备水压测试/等静压设备,超高压反应釜厂家口碑推荐 - 品牌推荐师
  • 【电力系统】PMSM电机定子绕组匝间短路故障、电机故障诊断+转子磁场损失Matlab代码
  • 知网AIGC检测不通过?别慌,这套方案帮我一次过关 - 还在做实验的师兄
  • GitHub上那些star过千的C++学习仓库与面试资源,我都整理好了!
  • 第二类斯特林数列
  • 供应链计划到底怎么做?三层计划、六个动作,一次讲清!
  • 免费降AI神器2026:新用户必看的省钱攻略 - 还在做实验的师兄
  • 信息类专业毕业设计中常见问题与难点总结
  • 蓝桥/16/B.4/水质检测
  • 多维衰老表型的蛋白质组图谱
  • 京东e卡回收,闲置秒变真金白银 - 京顺回收
  • Kriging代理模型+RSM响应面分析+NSGAII多目标优化+熵权法-TOPSIS决策MATLAB代码
  • 从0到1搭建企业数据中心:AI应用架构师的实战步骤
  • 论文AI率100%怎么降?过来人的三步降AI攻略(附实测截图) - 还在做实验的师兄
  • 龙虾机器人:让 AI 替你动手,效率直接拉满!
  • 2026最新降AI率工具测评:花了800块测完这些,帮你省踩坑的钱 - 还在做实验的师兄
  • 年薪128万!2026年转行AI大模型岗,是普通IT人最后的“阶级跃迁”机会
  • 多肽定制合成丨Peforelin CAS号:147859-97-0
  • AI率从92%降到5%:我的实操复盘和工具组合方案 - 还在做实验的师兄
  • 太空光伏电池的联合环境试验
  • 【Proteus仿真-开源】基于51单片机的智能温室大棚【详细流程介绍】 - 少年
  • DeepSeek降AI指令怎么写?附15条实测有效的Prompt模板 - 还在做实验的师兄
  • 2026降AI工具第一梯队:知网实测数据说话 - 还在做实验的师兄