关于游戏中主循环结构与定时的问题。

weloveit 2006-03-12 04:54:09
这是一段J2ME的游戏定时器与调度器的代码,可能很多人见过的。只是在下看的不是很明白,思路理不清楚。所以上来问问。

package game.engine.core;

public class Clock {
// Clock是否运行:
private boolean running;

// 当前hardware clock:
private int thisTime;

// record the last hardware clock when calling stop():
private int lastTime;

// systemTime从0开始递增,和硬件时钟同步:
private int systemTime;
// systemOffset就是硬件时钟和systemTime的差:
private int systemOffset;
// 上一次停止的systemTime:
private int pauseAt;

// virtualTime starts from 0.
private int virtualTime;
// virtualOffset records how long the clock paused:
private int virtualOffset;

private int frameStart;
private int frameEnd;
private int frameCount;


/** Creates a new instance of clock */
public Clock() {
reset();
}
public void reset() {
running = false;
// get the hardware clock:
thisTime = (int)System.currentTimeMillis();
lastTime = thisTime;
// and systemTime starts from 0:
systemTime = 0;
systemOffset = thisTime;
pauseAt = 0;
// init virtual time:
virtualTime = 0;
virtualOffset = 0;
// init frame time:
frameStart = 0;
frameEnd = 0;
frameCount = 0;
}

// 同步hardware clock:
private void update() {
lastTime = thisTime;
thisTime = (int)System.currentTimeMillis();
// increase the systemTime:
systemTime += (thisTime - lastTime);
}

// 启动Clock:
public void start() {
if(!running) {
running = true;
update();
virtualOffset += (systemTime - pauseAt);
System.out.println("[start]");
}
}

// 停止Clock:
public void stop() {
if(running) {
running = false;
update();
pauseAt = systemTime;
System.out.println("[stop] at " + pauseAt);
}
}

// Clock是否运行:
public boolean isRunning() {
return this.running;
}

// 开始Frame:
public void beginFrame() {
if(running) {
update();
frameCount++;
frameStart = frameEnd;
frameEnd = systemTime - virtualOffset;
virtualTime = frameStart;
System.out.println("[beginFrame] virtual time = " + virtualTime + ", systemTime = " + systemTime);
}
}

// 将virtual time提前到Frame结束:
public void advanceToEnd() {
if(running) {
virtualTime = frameEnd;
System.out.println("[advanceToEnd] virtual time = " + virtualTime);
}
}

public int getVirtualTime() {
return virtualTime;
}

// 获取系统时间:
public int getSystemTime() {
update();
return systemTime;
}

// 获取Frame开始时间:
public int getFrameStart() {
return frameStart;
}

// 获取Frame结束时间:
public int getFrameEnd() {
return frameEnd;
}

public int getFrameCount() {
return frameCount;
}


}





package game.engine.core;
import game.engine.core.*;
public class Scheduler {

public static void main(String[] args) {
Scheduler scheduler = new Scheduler();
scheduler.start();
int time = 1000;
do {
scheduler.waitUntil(time);
time += 1000;
scheduler.executeFrame();
} while(scheduler.getSystemTime()<10000);
}

/** Creates a new instance of Scheduler */
public Scheduler() {
}
// clock:
private Clock clock = new Clock();

// 启动Scheduler:
public void start() {
clock.start();
}

// 停止Scheduler:
public void stop() {
clock.stop();
}

public int getSystemTime() {
return clock.getSystemTime();
}

public int getVirtualTime() {
return clock.getVirtualTime();
}

// 执行完整的一帧:
public void executeFrame() {
System.out.println("-- start execute frame --");
clock.beginFrame();
int started = clock.getSystemTime();
// do time task:
System.out.println("doing time tasks...");
try {
Thread.sleep(500);
}catch(InterruptedException ie) {}
clock.advanceToEnd();
// do frame task:
System.out.println("doing frame tasks...");
try {
Thread.sleep(200);
}catch(InterruptedException ie) {}
// do render task:
int end = clock.getSystemTime();
int elapsed = end - started;
int frameLength = clock.getFrameEnd() - clock.getFrameStart();
System.out.println("elapsed: " + elapsed + ", frame: " + frameLength);
System.out.println("cpu usage: " + (elapsed * 100 / frameLength) + "%");
// cleanup:
System.out.println("-- end execute frame --\n");
}
public void waitUntil(int time) {
try {
while(clock.getSystemTime()<time)
Thread.sleep(1);
}
catch(InterruptedException ie) {}
}

}

我们还没有真正的任务要执行,所以只好用两个Thread.sleep()来表示执行任务,分别是500ms和200ms,在main()方法中每1s执行一次executeFrame()方法,可以看到如下输出:
-- start execute frame --
[beginFrame] virtual time = 0, systemTime = 1002
doing time tasks...
[advanceToEnd] virtual time = 992
doing frame tasks...
elapsed: 701, frame: 992
cpu usage: 70%
-- end execute frame --
-- start execute frame --
[beginFrame] virtual time = 992, systemTime = 2003
doing time tasks...
[advanceToEnd] virtual time = 1993
doing frame tasks...
elapsed: 701, frame: 1001
cpu usage: 70%
-- end execute frame --
...
CPU使用率是70%,如果把任务时间延长大于1s,比如700ms和500ms,虚拟时间就变慢,因为CPU不能在1s内处理完任务,输出的CPU使用率100%(除了第一次计算):
-- start execute frame --
[beginFrame] virtual time = 0, systemTime = 1002
doing time tasks...
[advanceToEnd] virtual time = 992
doing frame tasks...
elapsed: 1201, frame: 992
cpu usage: 121%
-- end execute frame --
-- start execute frame --
[beginFrame] virtual time = 992, systemTime = 2203
doing time tasks...
[advanceToEnd] virtual time = 2193
doing frame tasks...
elapsed: 1202, frame: 1201
cpu usage: 100%
-- end execute frame --
-- start execute frame --
[beginFrame] virtual time = 2193, systemTime = 3415
doing time tasks...
[advanceToEnd] virtual time = 3405
doing frame tasks...
elapsed: 1202, frame: 1212
cpu usage: 99%
-- end execute frame --
...
有一些微小的误差,因为调度器自身的代码也会占用一点时间,并且currentTimeMillies()只能精确到毫秒级,不过用户是感觉不出来的,只有当CPU使用达100%时,游戏才会变慢



...全文
169 7 打赏 收藏 转发到动态 举报
写回复
用AI写文章
7 条回复
切换为时间正序
请发表友善的回复…
发表回复
weloveit 2006-03-18
  • 打赏
  • 举报
回复
除了println这个输出,C++中用的是cout。其它代码基本上都是一样的啊。
看到不少UP,想来应该也有人对这个如何保持固定帧速的问题感兴趣的吧
Arqui 2006-03-16
  • 打赏
  • 举报
回复
up
新鲜鱼排 2006-03-15
  • 打赏
  • 举报
回复
up
wowxw 2006-03-15
  • 打赏
  • 举报
回复
UP!
weloveit 2006-03-14
  • 打赏
  • 举报
回复
为什么没人回呢?这么多高手呆这里面
Messagebus 2006-03-14
  • 打赏
  • 举报
回复
因为不搞java
weloveit 2006-03-12
  • 打赏
  • 举报
回复
首先,这几个变量中
// 当前hardware clock:
private int thisTime;

// record the last hardware clock when calling stop():
private int lastTime;

// systemTime从0开始递增,和硬件时钟同步:
private int systemTime;
// systemOffset就是硬件时钟和systemTime的差:
private int systemOffset;
// 上一次停止的systemTime:
private int pauseAt;

// virtualTime starts from 0.
private int virtualTime;
// virtualOffset records how long the clock paused:
private int virtualOffset;

thisTime和lastTime分别对应硬件时间的当前和上一个时间。而systemTime就是游戏中的时间,systemOffset就是硬件时间与游戏时间之差。那virtualTime和virtualOffset 是用

来干嘛的

关于调度器scheduler的问题,scheduler调用clock来定时的。且根据作者的解释是这样的:
我们准备开始设计游戏内核。
通常,游戏和桌面Windows程序不同(扫雷等桌面游戏除外),它不能依赖消息驱动,因为游戏通常只有一个画面,而且需要高速更新。因此,游戏只能靠时钟驱动。大部分游戏都

在一个时钟驱动下定时渲染一帧画面。此外,游戏需要接收用户输入,处理游戏事件(比如碰撞检测等),可能还需要AI计算,这些任务都必须巧妙地在两幅画面切换的时间段里

处理完毕,因此,一个高效而灵活的调度器就是游戏进程的核心。
调度器由时钟驱动器和任务管理器组成。
时钟驱动器负责在需要的时候向调度器提供准确的虚拟时间,虚拟时间流逝时,游戏就运行,虚拟时间停止时,游戏就暂停,虚拟时间还可以比真实时间慢或者快,以便调节游戏

运行速度,所有的任务都依赖虚拟时间,并且它们不知道时钟是否暂停,这样就大大简化了时间的表示和运算,我们可以随时暂停时钟从而能够方便地调试。
任务管理器维护一个任务列表,并且知道下一个即将运行的任务是什么。一共有3种任务:
• 渲染任务:在游戏运行期间,负责更新画面,在每一帧的结束时间点执行。
• 帧任务:在每个帧时间段内执行一次的任务,比如绘制背景。
• 时间任务:在指定的时间点和时间段触发,执行一次或多次,完成后即被删除。
调度器的作用便是把时钟提供的虚拟时间按固定时间段(比如20ms)分成若干帧,在一个帧的时间段里,依次调度所有的帧任务和定时任务,最后执行渲染任务,从而完成一个完

整的帧。
我们为什么不用多线程来执行这些任务,把调度功能交给操作系统呢?第一,通常游戏中会有很多个任务,为每个任务创建线程是不经济而且低效的,很多任务只执行一次,大量

开销被花在线程的创建和销毁上,而且我们很难控制多线程同步。第二,由于操作系统通常是抢先式多任务,我们无法得知一个任务是否会被另一个任务中断或者能否在一帧内完

成。
调度器就像是在模拟一个线程管理器,它必须非常灵活,首先,调度器依次执行队列中的任务,可以精确计算每个任务花费的时间。如果用户硬件配置不高,执行一个完整的帧花

费的时间多于预算,则可以在下一个帧里跳过不必要的任务(比如关闭特效,特效任务就是可以跳过的),如果执行一个完整的帧花费的时间小于预算,则可以多执行一些任务,

比如多画一些特效或多做一些AI运算等,这些都由调度器自动调节。
调度器的另一个功能是实现平滑地添加和删除任务,通常游戏中的任务更新都很频繁。如果使用多线程,则线程的创建和销毁将耗费大量时间。
调度器执行一帧的流程如下:
• 从时钟驱动器获得虚拟时间;
• 执行时间任务;
• 执行帧任务;
• 执行渲染任务;
• 从时钟驱动器获得虚拟时间并计算这一帧的执行时间;
• 执行清理任务,包括从任务管理器中删除过期任务。
由于只需要用到很少的API,我决定用J2SE来写内核,这样便于调试,等测试无误再移植到MIDP上。
参考:Marshall:"Game Programming Gems 3"


我对游戏架构的印像是,在游戏开始后,做了一些初始化的工作后进后主循环。主循环中应该就是要进行定时的地方,比如说帧速率为30fps,就是接近每33ms处理一帧。
假设虚拟码如下:
gameLoop
{
static fTimer;
fTimer+=g_fElapsedTime;
if(fTimer>=33)
{
fTimer=0;
Render();
}
}

这样子倒是挺好理解的,定义一个全局逝去时间g_fElapsedTime,就相当于之前的那个clock类中的systemTime(用于存储游戏运行了多久)。然后隔33ms,重设一次fTimer=0;
和执行一次渲染操作,当然这里这个Render函数,里面应该包含了渲染任务,帧任务,时间任务之类的全部操作。这也还是有问题的,若有一次执行Render()发了50ms呢,可能好

几次呢,那游戏不是变的忽快忽慢的??根据前文的作者所提到的:
首先,调度器依次执行队列中的任务,可以精确计算每个任务花费的时间。如果用户硬件配置不高,执行一个完整的帧花费的时间多于预算,则可以在下一个帧里跳过不必要的任

务(比如关闭特效,特效任务就是可以跳过的),如果执行一个完整的帧花费的时间小于预算,则可以多执行一些任务,比如多画一些特效或多做一些AI运算等,这些都由调度器

自动调节。

似乎用这个scheduler类可以在固定的时间内执行这三种任务渲染任务,帧任务,时间任务。而且如果在33ms内无法执行完这三种任务。则scheduler会自动调节放弃某些任务,可

是我在代码中看不出来这种情况啊,前面他用两个sleep()代替任务时,就可以看到后一次输入中,一个sleep(700),一个sleep(500).那么scheduler还是执行了1200ms啊(他这里面

是假设1000ms执行一个Render(),那么这种情况下游戏速度还是变慢了啊!

应该说来这个由clock和scheduler构成的定时功能应该是比后一个简陋的gameloop要好的,只是我无法理解它的全部代码。理不清思路而已。
还有就是clock类中的这两个函数,它为什么把执行帧的操作放在clock中,advanceToEnd()又是什么意思。

// 开始Frame:
public void beginFrame() {
if(running) {
update();
frameCount++;
frameStart = frameEnd;
frameEnd = systemTime - virtualOffset;
virtualTime = frameStart;
System.out.println("[beginFrame] virtual time = " + virtualTime + ", systemTime = " + systemTime);
}
}

// 将virtual time提前到Frame结束:
public void advanceToEnd() {
if(running) {
virtualTime = frameEnd;
System.out.println("[advanceToEnd] virtual time = " + virtualTime);
}
}

8,301

社区成员

发帖
与我相关
我的任务
社区描述
游戏开发相关内容讨论专区
社区管理员
  • 游戏开发
  • 呆呆敲代码的小Y
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧