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)计算
- 计算原理:
- 代码实现:
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统计系统
-
核心变量:
变量名 类型 作用 timeCount float 累计时间窗口(1秒间隔) fps int 最终计算的帧率 fpsCount int 临时帧数计数器 ups int 最终计算的更新率 upsCount int 临时更新计数器 -
统计机制:
// 在游戏循环中的调用点 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秒计算一次
- 余量保留:
timeCount -= 1f
保证时间连续性 - 实时更新:每帧累计,每秒输出结果
5. Delta时间应用场景
- 物理运动:
position += velocity * delta
- 动画控制:
animationProgress += animationSpeed * delta
- 游戏逻辑:
cooldownTimer -= delta
6. 设计思想
- 时间解耦:分离游戏逻辑和渲染帧率
- 硬件无关:通过delta实现不同性能设备一致体验
- 性能监控:FPS/UPS作为优化基准指标
- 模块化设计:计时功能封装为独立组件
7. 最佳实践建议
- 初始化时机:在游戏循环开始前初始化计时器
- Delta获取点:每帧开始时优先获取delta
- 统计更新点:在游戏循环末尾更新统计
- 精度选择:
- 普通游戏:毫秒级足够
- 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 { ... }
作用:游戏时间管理核心类,负责计算时间差、帧率和更新率
字段解析
-
lastLoopTime
(double)- 作用:记录上一次游戏循环开始的时间点
- 单位:秒(GLFW时间系统)
- 用途:计算两帧之间的时间差(delta)
-
timeCount
(float)- 作用:累计时间计数器
- 单位:秒
- 用途:控制FPS/UPS统计窗口(1秒间隔)
-
fps
(int)- 作用:存储计算出的帧率结果
- 含义:每秒实际渲染的帧数
-
fpsCount
(int)- 作用:帧数临时计数器
- 用途:在1秒窗口期内累计渲染帧数
-
ups
(int)- 作用:存储计算出的更新率结果
- 含义:每秒实际执行的逻辑更新次数
-
upsCount
(int)- 作用:更新次数临时计数器
- 用途:在1秒窗口期内累计逻辑更新次数
方法解析
-
init()
public void init() {lastLoopTime = getTime(); // 初始化时间基准点 }
- 作用:初始化计时器
- 关键操作:将
lastLoopTime
设置为当前GLFW时间
-
getTime()
public double getTime() {return glfwGetTime(); // 调用GLFW原生计时器 }
- 作用:获取高精度时间
- 返回值:自GLFW初始化以来的秒数
- 精度:毫秒级(实际精度取决于系统)
-
getDelta()
(核心方法)public float getDelta() {double time = getTime(); // 当前时间float delta = (float)(time - lastLoopTime); // 计算时间差lastLoopTime = time; // 更新上次记录时间timeCount += delta; // 累计到时间窗口return delta; // 返回时间差 }
- delta计算原理:
当前时间 - 上一帧时间 = 帧间时间差
- 作用:提供精确的帧间时间差
- 后续影响:
timeCount
用于FPS/UPS计算
- delta计算原理:
-
updateFPS()
public void updateFPS() {fpsCount++; // 增加帧计数器 }
- 调用时机:每完成一帧渲染后调用
- 作用:累计渲染帧数
-
updateUPS()
public void updateUPS() {upsCount++; // 增加更新计数器 }
- 调用时机:每完成一次逻辑更新后调用
- 作用:累计逻辑更新次数
-
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实际值
- 设计亮点:保留时间余量保证统计精度
- FPS计算原理:
-
getFPS()
public int getFPS() {return fps > 0 ? fps : fpsCount; }
- 作用:获取当前帧率
- 设计亮点:未满1秒时返回临时计数
-
getUPS()
public int getUPS() {return ups > 0 ? ups : upsCount; }
- 作用:获取当前更新率
- 与
getFPS()
同理
-
getLastLoopTime()
public double getLastLoopTime() {return lastLoopTime; }
- 作用:获取上次循环时间点
- 用途:在Game类的
sync()
方法中用于帧率控制
问题解答
1. 这些方法的作用?
- 时间基础:
getTime()
提供高精度时间基准 - 核心计时:
getDelta()
计算帧间时间差 - 性能统计:
updateFPS()
/updateUPS()
收集数据update()
进行统计计算getFPS()
/getUPS()
获取结果
- 初始化:
init()
建立时间基准点
2. FPS如何计算?
计算流程:
- 每帧渲染完成 → 调用
updateFPS()
增加计数器 - 每帧获取delta → 累加到
timeCount
- 当
timeCount > 1秒
:fps = fpsCount; // 记录当前秒内帧数 fpsCount = 0; // 重置计数器 timeCount -= 1.0f; // 保留剩余时间
- 未满1秒时:
getFPS()
返回当前累计帧数
数学表达:
FPS = 帧数 / 时间窗口
(1秒固定窗口)
3. 与Game类的关联
-
初始化阶段:
// 在Game.init()中 timer.init();
-
游戏循环中:
// 可变步长循环示例 float delta = timer.getDelta(); update(delta); render(); timer.updateFPS(); // 渲染后调用
-
帧率控制:
// Game.sync()方法 double lastLoopTime = timer.getLastLoopTime(); double now = timer.getTime();
-
性能监控:
// 可实时显示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. 对游戏开发的启发
-
时间驱动设计
- 所有运动/动画应依赖delta而非固定值
- 示例:
物体移动距离 = 速度 × delta
-
性能监控必备
-
模块化设计
- 计时功能封装在独立类
- 游戏循环清晰分离:输入/更新/渲染
-
统计优化技巧
- 滑动窗口统计(保留timeCount余量)
- 避免频繁计算:1秒更新一次结果
-
跨平台时间方案
- 使用GLFW
glfwGetTime()
而非System.currentTimeMillis()
- 优势:更高精度(微秒级)且与渲染API同步
- 使用GLFW
-
两种更新模式支持
// 可变步长 update(float delta) // 固定步长 update() // 内部使用固定interval
最佳实践建议:
- 在游戏循环开始时获取delta
- 逻辑更新后立即统计UPS
- 渲染完成后立即统计FPS
- 每帧显示或记录性能数据
问答
饥荒和我的世界等这种3D游戏计算fps时是否采用上面的方式去计算?fps和设备性能,程序复杂度,以及后台应用的数量和玩游戏时当前内存的占用比例没有关系吗?为什么在获取fps的代码中没有提及这些影响因素?
一、FPS计算的核心逻辑是否相同?
答案:是,但工业级实现更复杂
- 基础原理一致:所有游戏都遵循
帧数/时间窗口
的核心公式 - 实现差异:
方案 教程方案 商业游戏方案 计时精度 秒级(1秒窗口) 毫秒级+多窗口统计 数据采样 简单计数器 滑动窗口/加权平均 显示方式 直接输出原始值 平滑处理(EMA算法) 性能影响 忽略 关联GPU/CPU负载监控 例如《我的世界》采用 30帧移动平均 避免数值剧烈跳动
二、FPS与性能因素的关联性
1. 核心影响因素
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
提高进程优先级
- Windows平台:
- 跨平台方案:
// 示例:跨平台内存监控 #if defined(_WIN32)MEMORYSTATUSEX memInfo;memInfo.dwLength = sizeof(memInfo);GlobalMemoryStatusEx(&memInfo); #elif defined(__linux__)sysinfo(&memInfo); // 读取/proc/meminfo #endif
四、为什么需要更复杂的实现?
-
避免视觉卡顿
- 原始1秒窗口在帧时间波动时会出现 数值剧烈跳动
- 解决方案:采用 指数移动平均(EMA)
FPS_{display} = α·FPS_{current} + (1-α)·FPS_{previous} \quad (α=0.1)
-
精准定位瓶颈
- 区分 GPU瓶颈(高draw call导致FPS↓)
- 识别 CPU瓶颈(复杂物理计算导致FPS↓)
专业工具:RenderDoc/Intel GPA
-
防止误导玩家
- 简单计数器在 加载场景 时FPS=0,易引发误解
- 商业方案:冻结显示 或 显示预估帧率
总结:工业级FPS系统的核心差异
维度 | 基础教程 | 商业游戏 |
---|---|---|
计时精度 | 1秒窗口 | 多级窗口(100ms/500ms/1s) |
数据显示 | 原始值 | 平滑处理+异常值过滤 |
性能关联 | 无 | 绑定CPU/GPU/内存监控 |
应对措施 | 无 | 动态画质调整/资源卸载 |
平台适配 | 忽略 | 操作系统级API调用 |
实际开发中,《饥荒》等游戏会通过 引擎内置性能分析器(如Unity Profiler/Unreal Insights)实现更精细的监控,这些底层机制远超基础教程范畴。