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

LWJGL教程(3)——时间

时间

1. 获取系统时间
  • 核心目的:实现高精度计时(毫秒/纳秒级)
  • 实现方案
    // GLFW方案(推荐)
    public double getTime() {return glfwGetTime(); // 返回GLFW初始化后的秒数
    }// Java原生方案
    public double getTime() {return System.nanoTime() / 1_000_000_000.0;
    }
    
  • 关键点
    • GLFW方案与OpenGL上下文同步,精度更高
    • System.nanoTime()提供纳秒级精度但需手动转换单位
2. 增量时间(Delta Time)计算
  • 计算原理
    当前帧时间
    减去
    上一帧时间
    得到Delta
  • 代码实现
    double lastLoopTime; // 存储上一帧时间public void init() {lastLoopTime = getTime(); // 初始化时间基准
    }public float getDelta() {double currentTime = getTime();float delta = (float)(currentTime - lastLoopTime); // 计算时间差lastLoopTime = currentTime; // 更新基准时间timeCount += delta; // 累加到统计窗口return delta;
    }
    
3. FPS/UPS统计系统
  • 核心变量

    变量名类型作用
    timeCountfloat累计时间窗口(1秒间隔)
    fpsint最终计算的帧率
    fpsCountint临时帧数计数器
    upsint最终计算的更新率
    upsCountint临时更新计数器
  • 统计机制

    // 在游戏循环中的调用点
    void update() {  // 逻辑更新updateUPS(); // upsCount++
    }void render() {  // 画面渲染updateFPS(); // fpsCount++
    }// 统计计算方法
    public void update() {if (timeCount > 1f) {       // 达到1秒统计窗口fps = fpsCount;         // 保存帧数fpsCount = 0;           // 重置计数器ups = upsCount;         // 保存更新数upsCount = 0;           // 重置计数器timeCount -= 1f;        // 保留时间余量}
    }
    
4. FPS计算原理
  • 数学公式
    FPS = 采样窗口内渲染的帧数 / 采样窗口时长
  • 实现特点
    1. 滑动窗口统计:每满1秒计算一次
    2. 余量保留timeCount -= 1f 保证时间连续性
    3. 实时更新:每帧累计,每秒输出结果
5. Delta时间应用场景
  1. 物理运动
    position += velocity * delta
  2. 动画控制
    animationProgress += animationSpeed * delta
  3. 游戏逻辑
    cooldownTimer -= delta
6. 设计思想
  1. 时间解耦:分离游戏逻辑和渲染帧率
  2. 硬件无关:通过delta实现不同性能设备一致体验
  3. 性能监控:FPS/UPS作为优化基准指标
  4. 模块化设计:计时功能封装为独立组件
7. 最佳实践建议
  1. 初始化时机:在游戏循环开始前初始化计时器
  2. Delta获取点:每帧开始时优先获取delta
  3. 统计更新点:在游戏循环末尾更新统计
  4. 精度选择
    • 普通游戏:毫秒级足够
    • VR/竞技游戏:推荐纳秒级精度

完整实现参考:https://github.com/SilverTiger/lwjgl3-tutorial/blob/master/src/main/java/silvertiger/tutorial/lwjgl/core/Timer.java
授权协议:MIT License © 2014-2018 Heiko Brumme

Timer实例代码

/** The MIT License (MIT)** Copyright © 2014, Heiko Brumme** Permission is hereby granted, free of charge, to any person obtaining a copy* of this software and associated documentation files (the "Software"), to deal* in the Software without restriction, including without limitation the rights* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell* copies of the Software, and to permit persons to whom the Software is* furnished to do so, subject to the following conditions:** The above copyright notice and this permission notice shall be included in all* copies or substantial portions of the Software.** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE* SOFTWARE.*/
package silvertiger.tutorial.lwjgl.core;import static org.lwjgl.glfw.GLFW.glfwGetTime;/*** The timer class is used for calculating delta time and also FPS and UPS* calculation.** @author Heiko Brumme*/
public class Timer {/*** System time since last loop.*/private double lastLoopTime;/*** Used for FPS and UPS calculation.*/private float timeCount;/*** Frames per second.*/private int fps;/*** Counter for the FPS calculation.*/private int fpsCount;/*** Updates per second.*/private int ups;/*** Counter for the UPS calculation.*/private int upsCount;/*** Initializes the timer.*/public void init() {lastLoopTime = getTime();}/*** Returns the time elapsed since <code>glfwInit()</code> in seconds.** @return System time in seconds*/public double getTime() {return glfwGetTime();}/*** Returns the time that have passed since the last loop.** @return Delta time in seconds*/public float getDelta() {double time = getTime();float delta = (float) (time - lastLoopTime);lastLoopTime = time;timeCount += delta;return delta;}/*** Updates the FPS counter.*/public void updateFPS() {fpsCount++;}/*** Updates the UPS counter.*/public void updateUPS() {upsCount++;}/*** Updates FPS and UPS if a whole second has passed.*/public void update() {if (timeCount > 1f) {fps = fpsCount;fpsCount = 0;ups = upsCount;upsCount = 0;timeCount -= 1f;}}/*** Getter for the FPS.** @return Frames per second*/public int getFPS() {return fps > 0 ? fps : fpsCount;}/*** Getter for the UPS.** @return Updates per second*/public int getUPS() {return ups > 0 ? ups : upsCount;}/*** Getter for the last loop time.** @return System time of the last loop*/public double getLastLoopTime() {return lastLoopTime;}
}

Timer 类详细解析

类定义
public class Timer { ... }

作用:游戏时间管理核心类,负责计算时间差、帧率和更新率

字段解析
  1. lastLoopTime (double)

    • 作用:记录上一次游戏循环开始的时间点
    • 单位:秒(GLFW时间系统)
    • 用途:计算两帧之间的时间差(delta)
  2. timeCount (float)

    • 作用:累计时间计数器
    • 单位:秒
    • 用途:控制FPS/UPS统计窗口(1秒间隔)
  3. fps (int)

    • 作用:存储计算出的帧率结果
    • 含义:每秒实际渲染的帧数
  4. fpsCount (int)

    • 作用:帧数临时计数器
    • 用途:在1秒窗口期内累计渲染帧数
  5. ups (int)

    • 作用:存储计算出的更新率结果
    • 含义:每秒实际执行的逻辑更新次数
  6. upsCount (int)

    • 作用:更新次数临时计数器
    • 用途:在1秒窗口期内累计逻辑更新次数
方法解析
  1. init()

    public void init() {lastLoopTime = getTime(); // 初始化时间基准点
    }
    
    • 作用:初始化计时器
    • 关键操作:将lastLoopTime设置为当前GLFW时间
  2. getTime()

    public double getTime() {return glfwGetTime(); // 调用GLFW原生计时器
    }
    
    • 作用:获取高精度时间
    • 返回值:自GLFW初始化以来的秒数
    • 精度:毫秒级(实际精度取决于系统)
  3. getDelta()(核心方法)

    public float getDelta() {double time = getTime(); // 当前时间float delta = (float)(time - lastLoopTime); // 计算时间差lastLoopTime = time; // 更新上次记录时间timeCount += delta; // 累计到时间窗口return delta; // 返回时间差
    }
    
    • delta计算原理
      当前时间 - 上一帧时间 = 帧间时间差
    • 作用:提供精确的帧间时间差
    • 后续影响:timeCount用于FPS/UPS计算
  4. updateFPS()

    public void updateFPS() {fpsCount++; // 增加帧计数器
    }
    
    • 调用时机:每完成一帧渲染后调用
    • 作用:累计渲染帧数
  5. updateUPS()

    public void updateUPS() {upsCount++; // 增加更新计数器
    }
    
    • 调用时机:每完成一次逻辑更新后调用
    • 作用:累计逻辑更新次数
  6. update()(统计核心)

    public void update() {if (timeCount > 1f) { // 达到1秒统计窗口fps = fpsCount;   // 保存帧数fpsCount = 0;     // 重置计数器ups = upsCount;   // 保存更新数upsCount = 0;     // 重置计数器timeCount -= 1f;  // 保留不足1秒的余量}
    }
    
    • FPS计算原理
      FPS = 1秒内累计的渲染帧数
    • 作用:每1秒更新一次FPS/UPS实际值
    • 设计亮点:保留时间余量保证统计精度
  7. getFPS()

    public int getFPS() {return fps > 0 ? fps : fpsCount;
    }
    
    • 作用:获取当前帧率
    • 设计亮点:未满1秒时返回临时计数
  8. getUPS()

    public int getUPS() {return ups > 0 ? ups : upsCount;
    }
    
    • 作用:获取当前更新率
    • getFPS()同理
  9. getLastLoopTime()

    public double getLastLoopTime() {return lastLoopTime;
    }
    
    • 作用:获取上次循环时间点
    • 用途:在Game类的sync()方法中用于帧率控制

问题解答

1. 这些方法的作用?
  • 时间基础getTime()提供高精度时间基准
  • 核心计时getDelta()计算帧间时间差
  • 性能统计
    • updateFPS()/updateUPS()收集数据
    • update()进行统计计算
    • getFPS()/getUPS()获取结果
  • 初始化init()建立时间基准点
2. FPS如何计算?

计算流程

  1. 每帧渲染完成 → 调用updateFPS()增加计数器
  2. 每帧获取delta → 累加到timeCount
  3. timeCount > 1秒
    fps = fpsCount;    // 记录当前秒内帧数
    fpsCount = 0;      // 重置计数器
    timeCount -= 1.0f; // 保留剩余时间
    
  4. 未满1秒时:getFPS()返回当前累计帧数

数学表达
FPS = 帧数 / 时间窗口(1秒固定窗口)

3. 与Game类的关联
  1. 初始化阶段

    // 在Game.init()中
    timer.init();
    
  2. 游戏循环中

    // 可变步长循环示例
    float delta = timer.getDelta();
    update(delta);
    render();
    timer.updateFPS(); // 渲染后调用
    
  3. 帧率控制

    // Game.sync()方法
    double lastLoopTime = timer.getLastLoopTime();
    double now = timer.getTime();
    
  4. 性能监控

    // 可实时显示FPS/UPS
    debugDisplay("FPS: " + timer.getFPS());
    
4. Delta计算原理

计算公式
delta = 当前帧时间 - 上一帧时间

计算流程

public float getDelta() {double now = glfwGetTime();         // 获取当前GLFW时间float delta = (float)(now - last);  // 计算差值last = now;                        // 更新基准点return delta;
}

物理意义
真实两帧之间的时间间隔(秒),用于实现帧率无关的移动:

position += velocity * delta; // 匀速运动
5. 对游戏开发的启发
  1. 时间驱动设计

    • 所有运动/动画应依赖delta而非固定值
    • 示例:物体移动距离 = 速度 × delta
  2. 性能监控必备

    实际运行
    性能统计
    优化决策
    目标调整
  3. 模块化设计

    • 计时功能封装在独立类
    • 游戏循环清晰分离:输入/更新/渲染
  4. 统计优化技巧

    • 滑动窗口统计(保留timeCount余量)
    • 避免频繁计算:1秒更新一次结果
  5. 跨平台时间方案

    • 使用GLFWglfwGetTime()而非System.currentTimeMillis()
    • 优势:更高精度(微秒级)且与渲染API同步
  6. 两种更新模式支持

    // 可变步长
    update(float delta) // 固定步长
    update() // 内部使用固定interval
    

最佳实践建议

  • 在游戏循环开始时获取delta
  • 逻辑更新后立即统计UPS
  • 渲染完成后立即统计FPS
  • 每帧显示或记录性能数据

问答

饥荒和我的世界等这种3D游戏计算fps时是否采用上面的方式去计算?fps和设备性能,程序复杂度,以及后台应用的数量和玩游戏时当前内存的占用比例没有关系吗?为什么在获取fps的代码中没有提及这些影响因素?


一、FPS计算的核心逻辑是否相同?

答案:是,但工业级实现更复杂

  • 基础原理一致:所有游戏都遵循 帧数/时间窗口 的核心公式
  • 实现差异
    方案教程方案商业游戏方案
    计时精度秒级(1秒窗口)毫秒级+多窗口统计
    数据采样简单计数器滑动窗口/加权平均
    显示方式直接输出原始值平滑处理(EMA算法)
    性能影响忽略关联GPU/CPU负载监控

    例如《我的世界》采用 30帧移动平均 避免数值剧烈跳动


二、FPS与性能因素的关联性

1. 核心影响因素
FPS
设备性能
程序复杂度
后台应用
GPU渲染能力
CPU物理计算
场景多边形数量
光影特效复杂度
内存占用率
后台进程CPU占用
2. 教程未提及的原因
  • 教学目标不同:聚焦计时机制原理而非性能优化
  • 关注层分离
    • 基础教程:解决 “如何计算”
    • 进阶开发:解决 “为何波动”
  • 跨平台差异:内存/后台影响需操作系统级监控(如Windows API/Linux proc)

三、商业游戏如何处理性能关联?

1. 动态监控系统(以《饥荒》为例)
// 伪代码:工业级FPS监控
void updatePerformanceStats() {// 1. 基础FPS计算(类似教程方案)float delta = getDelta(); fpsCounter++;timeAccum += delta;// 2. 性能因素关联if(timeAccum > 0.5f) { // 0.5秒采样窗口fps = fpsCounter * 2; // 关键性能指标绑定perfReport.set("FPS", fps).set("CPU_Load", getProcessCPULoad()).set("RAM_Used", getMemoryUsage()).set("GPU_Temp", getGPUTemperature());// 3. 动态降级机制if(fps < 30) {reduceShadowQuality(); // 自动降低画质}fpsCounter = 0;timeAccum = 0;}
}
2. 后台影响应对策略
  • 内存占用
    • 采用 LRU资源卸载(如《我的世界》区块卸载)
    • 后台超过阈值时触发 纹理降级
  • CPU竞争
    • Windows平台:SetProcessPriority(BOOST_PRIORITY)
    • Linux平台:nice -n -20 提高进程优先级
  • 跨平台方案
    // 示例:跨平台内存监控
    #if defined(_WIN32)MEMORYSTATUSEX memInfo;memInfo.dwLength = sizeof(memInfo);GlobalMemoryStatusEx(&memInfo);
    #elif defined(__linux__)sysinfo(&memInfo); // 读取/proc/meminfo
    #endif
    

四、为什么需要更复杂的实现?

  1. 避免视觉卡顿

    • 原始1秒窗口在帧时间波动时会出现 数值剧烈跳动
    • 解决方案:采用 指数移动平均(EMA)
      FPS_{display} = α·FPS_{current} + (1-α)·FPS_{previous} \quad (α=0.1)
      
  2. 精准定位瓶颈

    • 区分 GPU瓶颈(高draw call导致FPS↓)
    • 识别 CPU瓶颈(复杂物理计算导致FPS↓)

    专业工具:RenderDoc/Intel GPA

  3. 防止误导玩家

    • 简单计数器在 加载场景 时FPS=0,易引发误解
    • 商业方案:冻结显示显示预估帧率

总结:工业级FPS系统的核心差异

维度基础教程商业游戏
计时精度1秒窗口多级窗口(100ms/500ms/1s)
数据显示原始值平滑处理+异常值过滤
性能关联绑定CPU/GPU/内存监控
应对措施动态画质调整/资源卸载
平台适配忽略操作系统级API调用

实际开发中,《饥荒》等游戏会通过 引擎内置性能分析器(如Unity Profiler/Unreal Insights)实现更精细的监控,这些底层机制远超基础教程范畴。

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

相关文章:

  • JWT原理及利用手法
  • 基于单片机倾角测量仪/角度测量/水平仪
  • spring-ai-alibaba如何上传文件并解析
  • 【高等数学】第四章 不定积分——第四节 有理函数的积分
  • 元学习算法的数学本质:从MAML到Reptile的理论统一与深度分析
  • 人脸识别:AI 如何精准 “认人”?
  • 【新手向】PyTorch常用Tensor shape变换方法
  • Spring Boot 订单超时自动取消的 3 种主流实现方案
  • 响应式编程入门教程第九节:UniRx 高级特性与自定义
  • LeetCode|Day20|9. 回文数|Python刷题笔记
  • DOM型XSS破坏
  • PID控制原理分析及应用(稳态误差详细分析)(一)
  • 如何升级Docker部署的Dify
  • API接口签名和敏感信息加密使用国密SM方案
  • 数据结构——时间复杂度
  • Python知识点4-嵌套循环break和continue使用死循环
  • 论文分享(一)
  • Spring MVC上下文容器在Web容器中是如何启动的(源码深入剖析)?
  • Self-Consistency:跨学科一致性的理论与AI推理的可靠性基石
  • Linux操作系统从入门到实战(十一)回车换行问题与用户缓冲区问题
  • 通俗易懂神经网络:从基础到实现
  • 学习日志15 python
  • 零基础入门 AI 运维:Linux 部署全栈项目实战(MySQL+Nginx + 私有化大模型)
  • 【1】计算机视觉方法(更新)
  • selenium4 web自动化测试
  • 面向对象基础笔记
  • QFutureInterface和QFuture间联系与区别
  • 《计算机网络》实验报告五 DNS协议分析与测量
  • 两个数据表的故事第 2 部分:理解“设计”Dk
  • ThinkPHP8极简上手指南:开启高效开发之旅