扫描二维码关注官方公众号
返回列表
+ 发新帖
查看: 288|回复: 0

[转载发布] 解决MV游戏明明FPS为60但运行时不流畅、有抖动感的问题

[复制链接]
累计送礼:
0 个
累计收礼:
0 个
  • TA的每日心情
    开心
    2025-2-4 02:05
  • 签到天数: 110 天

    连续签到: 2 天

    [LV.6]常住居民II

    2327

    主题

    395

    回帖

    1万

    积分

    管理员

    Rank: 9Rank: 9Rank: 9

    VIP
    6
    卡币
    10465
    OK点
    16
    推广点
    0
    同能卷
    0
    积分
    13215

    灌水之王

    发表于 2024-3-7 03:36:28 | 显示全部楼层 |阅读模式
    不知道以前有没有人提出过,MV的游戏有时候明明FPS没有降低,但是游戏玩起来就是卡卡的,尤其是在地图滚动时。

    最近终于下定决心去研究这个问题,发现问题出在MV的以帧为基础的刷新机制上。

    给不懂代码的人:

    命名为JitterFix.js,然后放到plugin文件夹中,直接启用。

    代码发布到public domain,你可以随意不署名甚至署你自己的名字使用(当然你愿意署我的名更好)。
    JAVASCRIPT 代码
    1. /*:
    2. * @plugindesc Fix the graphics jitter mostly noticeable when scrolling maps.
    3. * @author Zumi Kua
    4. *
    5. * @help just enable this plugin, no need to do anything.
    6. */
    7. /*: zh
    8. * @plugindesc 修复游戏地图在滚动时不定时出现的卡顿感
    9. * @author Zumi Kua
    10. *
    11. * @help 实际原因为performance.now返回的数值似乎会有误差,导致两帧之间的deltaTime在1/60上下来回横跳,进而导致在一次
    12. * requestAnimationFrame回调中一次updateScene也没调用,然后在下一次requestAnimationFrame回调中连续调用两次updateScene
    13. * 无法解决在非60Hz的显示器上的卡顿问题
    14. *
    15. * 使用requestAnimationFrame作为参数提供的DOMHighResTimeStamp似乎不会有该问题
    16. *
    17. */
    18. SceneManager.update = function(stamp){
    19.     try{
    20.         this.tickStart();
    21.         if(Utils.isMobileSafari()){
    22.             this.updateInputData();
    23.         }
    24.         this.updateManagers();
    25.         this.updateMain(stamp);
    26.         this.tickEnd();
    27.     }catch(e){
    28.         this.catchException(e);
    29.     }
    30. };
    31. const DEBUG = false;
    32. SceneManager.updateMain = function(stamp){
    33.     if(Utils.isMobileSafari()){
    34.         this.changeScene();
    35.         this.updateScene();
    36.     }else{
    37.         let fTime = (stamp - this._currentTime) / 1000;
    38.         if(fTime > 0.25) fTime = 0.25;
    39.         this._currentTime = stamp;
    40.         this._accumulator += fTime;
    41.         const old_ftime = fTime;
    42.         const old_accu = this._accumulator;
    43.         let i = 0;
    44.         while(this._accumulator >= this._deltaTime){
    45.             i++;
    46.             this.updateInputData();
    47.             this.changeScene();
    48.             this.updateScene();
    49.             this._accumulator -= this._deltaTime;
    50.         }
    51.         if(DEBUG && i !== 1){
    52.             console.log(i, old_ftime, old_accu);
    53.         }
    54.     }
    55.     this.renderScene();
    56.     this.requestUpdate();
    57. };
    复制代码


    致对原因感兴趣的人:

    首先来看MV的游戏主循环代码:
    JAVASCRIPT 代码
    1. SceneManager.updateMain = function(){
    2.     if(Utils.isMobileSafari()){
    3.         this.changeScene();
    4.         this.updateScene();
    5.     }else{
    6.         var newTime = this._getTimeInMsWithoutMobileSafari();
    7.         var fTime = (newTime - this._currentTime) / 1000;
    8.         if(fTime > 0.25) fTime = 0.25;
    9.         this._currentTime = newTime;
    10.         this._accumulator += fTime;
    11.         const old_accu = this._accumulator;
    12.         let i = 0;
    13.         while(this._accumulator >= this._deltaTime){
    14.             ++i;
    15.             this.updateInputData();
    16.             this.changeScene();
    17.             this.updateScene();
    18.             this._accumulator -= this._deltaTime;
    19.         }
    20.         if(i !== 1){
    21.             console.log(i, fTime, old_accu);
    22.         }
    23.     }
    24.     this.renderScene();
    25.     this.requestUpdate();
    26. };
    复制代码


    关于MV的刷新代码为什么这么写,可以参考:
    https://gafferongames.com/post/fix_your_timestep/
    如果你有Unity的使用经验的话也可以回忆一下Unity的Update和fixedUpdate。


    这个updateMain函数每次窗口重绘时都会被调用,可以理解为一般游戏的gameLoop循环体。

    理想状态下,每次重绘距离上次的时间应该都为1/60(假设我们的显示器刷新率为60hz),那么,每次重绘都会对应一次updateScene。

    当此次重绘距离上次的时间大于1/60,则此次调用updateScene后,多余的时间会记录在accumulator中,供下次使用

    而当此次重绘距离上次的时间小于1/60,则该帧不会进行updateScene调用,全部的时间会记录在accumulator中,供下次使用

    那么,如果每次重绘距离上次重绘的时间在1/60左右反复横跳,即每次重绘的时间大概是这样呢:

    代码
    1. 2 0.01699999999254942 0.033333333612730484
    2. 0 0.015999999828636646 0.01633333343391655
    3. 2 0.018000000156462193 0.03433333359037874
    4. 0 0.016000000294297934 0.01633333371331332
    5. 2 0.01699999999254942 0.033333333705862736
    6. 0 0.015999999828636646 0.016333333527048802
    7. 2 0.018000000156462193 0.03433333368351099
    8. 0 0.015999999828636646 0.016333333340784285
    9. 2 0.01699999999254942 0.0333333333333337
    复制代码

    (请看中间列,它代表了每次重绘距离上次重绘的时间,顺带一提,第一列是该帧调用updateScene的次数)

    可以看出,当距离上次重绘的时间小于1/60时,因为目前距离上一逻辑帧经过的时间不足一逻辑帧的时间,所以这次重绘不会调用updateScene。

    当距离上次重绘的时间大于1/60时,加上上一帧小于1/60所留下的时间,我们现在有了两逻辑帧所需要的时间,这次重绘会调用两次updateScene。

    而这样重绘时间在大于1/60与小于1/60之间来回横跳,正是卡顿感的成因。

    而至于原因,目前我还没法给出一个确切的答案,不过解决方法倒是有:
    requestAnimationFrame也会传一个当前时间作为参数,它传回的这个当前时间比我们用_getTimeInMsWithoutMobileSafari获得的要准确的多,将我们通过_getTimeInMsWithoutMobileSafari获取的时间戳改为requestAnimationFrame传回的时间戳,卡顿的情况也的确消失了。
    (这也引出了一个这个现象可能的原因,MV在调用updateMain之前也调用了一些其他的函数,而这些函数执行消耗的时间不固定,且消耗的时间长到无法忽略)

    另:如果你的显示器刷新率不是60hz,那么卡顿感应该是无法解决的。(以144hz为例,3帧中必然有2帧是不会调用updateScene的)。

    这个也发布于rpgmakerweb,作者也是我:https://forums.rpgmakerweb.com/i ... -stuttering.144992/


                 本帖来自P1论坛作者逸豫,因Project1站服务器在国外有时候访问缓慢不方便作者交流学习,经联系P1站长fux2同意署名转载一起分享游戏制作经验,共同为国内独立游戏作者共同创造良好交流环境,原文地址:https://rpg.blue/forum.php?mod=viewthread&tid=488839  若有侵权,发帖作者可联系底部站长QQ在线咨询功能删除,谢谢。
    天天去同能,天天有童年!
    回复 送礼论坛版权

    使用道具 举报

    文明发言,和谐互动
    文明发言,和谐互动
    高级模式
    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    关闭

    幸运抽奖

    社区每日抽奖来袭,快来试试你是欧皇还是非酋~

    立即查看

    聊天机器人
    Loading...

    QQ|Archiver|手机版|小黑屋|同能RPG制作大师 ( 沪ICP备12027754号-3 )

    GMT+8, 2025-3-10 16:13 , Processed in 0.132887 second(s), 53 queries .

    Powered by Discuz! X3.4

    Copyright © 2001-2020, Tencent Cloud.

    快速回复 返回顶部 返回列表