Game boy模拟器(10):计时器
Posted 妇男主人
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Game boy模拟器(10):计时器相关的知识,希望对你有一定的参考价值。
自从第一台计算机组装在一起以来,它们的一个基本功能就是计时:根据计时器协调动作。即使是最简单的游戏也有时间元素:例如,Pong 需要以特定的速度在屏幕上移动球。为了处理这些时间问题,每个游戏机都有某种形式的计时器,以允许事情在给定的时刻或以特定的速率发生。
GameBoy 也不例外,它包含一组根据可编程计划自动递增的寄存器。在本系列的这一部分中,我将研究计时器的结构和操作,以及如何使用它来播种伪随机数生成器,例如俄罗斯方块及其各种克隆中包含的生成器。
定时器结构
GameBoy 的 CPU,如本系列第一部分所述,在 4,194,304Hz 时钟上运行,有两个内部测量执行每条指令所用的时间:T 时钟,随时钟步长增加,M-时钟,以速度的四分之一 (1,048,576Hz) 递增。这些时钟用作定时器的源,定时器依次以 M 时钟速率的四分之一进行计数:262,144Hz。在本文中,我将这个最终值称为计时器的“基本速度”。
GameBoy 的计时器硬件提供两个独立的计时器寄存器:系统通过以预定速率递增每个寄存器中的值来工作。“分频器”计时器永久设置为以 16384Hz 的频率递增,即基本速度的十六分之一;由于它只是一个 8 位寄存器,它的值在达到 255 后将归零。“计数器”定时器更具可编程性:它可以设置为四种速度之一(基数除以 1、4、16 或64),并且可以设置为在溢出超过 255 时返回一个不为零的值。此外,定时器硬件将向 CPU 发送中断,如第 8 部分所述,每当“计数器”计时器确实溢出。
定时器使用的寄存器有四个;它们作为 I/O 页面的一部分供系统使用,就像图形和中断寄存器一样:
定时器寄存器
Address | Register | Details | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0xFF04 | Divider | Counts up at a fixed 16384Hz; reset to 0 whenever written to | ||||||||||||
0xFF05 | Counter | Counts up at the specified rate Triggers INT 0x50 when going 255->0 | ||||||||||||
0xFF06 | Modulo | When Counter overflows to 0, it's reset to start at Modulo | ||||||||||||
0xFF07 | Control |
|
由于“计数器”计时器在溢出时会触发中断,因此如果游戏需要定期发生某些事情,它会特别有用。然而,Gameboy 游戏通常可以使用垂直消隐来达到大致相同的效果,因为它以几乎 60Hz 的正常速度发生;垂直消隐处理程序不仅可以用于刷新屏幕内容,还可以用于检查键盘和更新游戏状态。因此,在传统的 Gameboy 游戏中很少需要使用计时器,尽管它可以在图形演示中发挥更大的作用。
实现定时器仿真
本系列文章中开发的仿真使用 CPU 的时钟作为基本时间单位。因此,为与 CPU 时钟同步运行并由调度函数更新的计时器维护时钟是最简单的。在这个阶段,将 DIV 寄存器作为一个独立于可控计时器的实体保持很方便,再次以最快计时器步长的 1/16 的速率递增:
源码
timer.js
TIMER = {
_div: 0,
_tma: 0,
_tima: 0,
_tac: 0,
_clock: {main:0, sub:0, div:0},
reset: function() {
TIMER._div = 0;
TIMER._sdiv = 0;
TIMER._tma = 0;
TIMER._tima = 0;
TIMER._tac = 0;
TIMER._clock.main = 0;
TIMER._clock.sub = 0;
TIMER._clock.div = 0;
LOG.out('TIMER', 'Reset.');
},
step: function() {
TIMER._tima++;
TIMER._clock.main = 0;
if(TIMER._tima > 255)
{
TIMER._tima = TIMER._tma;
MMU._if |= 4;
}
},
inc: function() {
var oldclk = TIMER._clock.main;
TIMER._clock.sub += Z80._r.m;
if(TIMER._clock.sub > 3)
{
TIMER._clock.main++;
TIMER._clock.sub -= 4;
TIMER._clock.div++;
if(TIMER._clock.div==16)
{
TIMER._clock.div = 0;
TIMER._div++;
TIMER._div &= 255;
}
}
if(TIMER._tac & 4)
{
switch(TIMER._tac & 3)
{
case 0:
if(TIMER._clock.main >= 64) TIMER.step();
break;
case 1:
if(TIMER._clock.main >= 1) TIMER.step();
break;
case 2:
if(TIMER._clock.main >= 4) TIMER.step();
break;
case 3:
if(TIMER._clock.main >= 16) TIMER.step();
break;
}
}
},
rb: function(addr) {
switch(addr)
{
case 0xFF04: return TIMER._div;
case 0xFF05: return TIMER._tima;
case 0xFF06: return TIMER._tma;
case 0xFF07: return TIMER._tac;
}
},
wb: function(addr, val) {
switch(addr)
{
case 0xFF04: TIMER._div = 0; break;
case 0xFF05: TIMER._tima = val; break;
case 0xFF06: TIMER._tma = val; break;
case 0xFF07: TIMER._tac = val&7; break;
}
}
};
Timer.js:时钟增量
TIMER = {
_clock: {
main: 0,
sub: 0,
div: 0
},
_reg: {
div: 0,
tima: 0,
tma: 0,
tac: 0
},
inc: function()
{
// Increment by the last opcode's time
TIMER._clock.sub += Z80._r.m;
// No opcode takes longer than 4 M-times,
// so we need only check for overflow once
if(TIMER._clock.sub >= 4)
{
TIMER._clock.main++;
TIMER._clock.sub -= 4;
// The DIV register increments at 1/16th
// the rate, so keep a count of this
TIMER._clock.div++;
if(TIMER._clock.div == 16)
{
TIMER._reg.div = (TIMER._reg.div+1) & 255;
TIMER._clock.div = 0;
}
}
// Check whether a step needs to be made in the timer
TIMER.check();
}
};
Z80.js:调度器
while(true)
{
// Run execute for this instruction
var op = MMU.rc(Z80._r.pc++);
Z80._map[op]();
Z80._r.pc &= 65535;
Z80._clock.m += Z80._r.m;
Z80._clock.t += Z80._r.t;
// Update the timer
TIMER.inc();
Z80._r.m = 0;
Z80._r.t = 0;
// If IME is on, and some interrupts are enabled in IE, and
// an interrupt flag is set, handle the interrupt
if(Z80._r.ime && MMU._ie && MMU._if)
{
// Mask off ints that aren't enabled
var ifired = MMU._ie & MMU._if;
if(ifired & 0x01)
{
MMU._if &= (255 - 0x01);
Z80._ops.RST40();
}
}
Z80._clock.m += Z80._r.m;
Z80._clock.t += Z80._r.t;
// Update timer again, in case a RST occurred
TIMER.inc();
}
从这里开始,可控定时器由不同的基速划分组成,使得检查定时器值是否需要递增以及提供寄存器作为内存 I/O 页面的一部分变得相对简单。以下代码段与 MMU I/O 页面处理程序之间的接口留给读者作为练习。
Timer.js:注册检查和更新
check: function()
{
if(TIMER._reg.tac & 4)
{
switch(TIMER._reg.tac & 3)
{
case 0: threshold = 64; break; // 4K
case 1: threshold = 1; break; // 256K
case 2: threshold = 4; break; // 64K
case 3: threshold = 16; break; // 16K
}
if(TIMER._clock.main >= threshold) TIMER.step();
}
},
step: function()
{
// Step the timer up by one
TIMER._clock.main = 0;
TIMER._reg.tima++;
if(TIMER._reg.tima > 255)
{
// At overflow, refill with the Modulo
TIMER._reg.tima = TIMER._reg.tma;
// Flag a timer interrupt to the dispatcher
MMU._if |= 4;
}
},
rb: function(addr)
{
switch(addr)
{
case 0xFF04: return TIMER._reg.div;
case 0xFF05: return TIMER._reg.tima;
case 0xFF06: return TIMER._reg.tma;
case 0xFF07: return TIMER._reg.tac;
}
},
wb: function(addr, val)
{
switch(addr)
{
case 0xFF04: TIMER._reg.div = 0; break;
case 0xFF05: TIMER._reg.tima = val; break;
case 0xFF06: TIMER._reg.tma = val; break;
case 0xFF07: TIMER._reg.tac = val & 7; break;
}
}
播种伪随机数生成器
许多游戏的一个主要组成部分是不可预测性:例如,俄罗斯方块会将未知模式的棋子扔到井下,游戏包括使用这些棋子建造行。理想情况下,计算机通过生成随机数来提供不可预测性,但这与计算机的有条不紊性质背道而驰;计算机不可能提供真正随机的数字模式。存在各种算法来生成表面上看起来像是随机的数字序列,这些被称为伪随机数生成 (PRNG) 算法。
PRNG 通常作为一个公式实现,给定一个特定的输入数字,将产生另一个与输入几乎没有关系的数字。对于俄罗斯方块,不需要这么复杂的东西;相反,以下代码用于生成看似随机的块。
Tetris.asm:选择新块
BLK_NEXT = 0xC203
BLK_CURR = 0xC213
REG_DIV = 0x04
NBLOCK: ld hl, BLK_CURR ; Bring the next block
ld a, (BLK_NEXT) ; forward to current
ld (hl),a
and 0xFC ; Clear out any rotations
ld c,a ; and hold onto previous
ld h,3 ; Try the following 3 times
.seed: ldh a, (REG_DIV) ; Get a "random" seed
ld b,a
.loop: xor a ; Step down in sevens
.seven: dec b ; until zero is reached
jr z, .next ; This loop is equivalent
inc a ; to (a%7)*4
inc a
inc a
inc a
cp 28
jr z, .loop
jr .seven
.next: ld e,a ; Copy the new value
dec h ; If this is the
jr z, .end ; last try, just use this
or c ; Otherwise check
and 0xFC ; against the previous block
cp c ; If it's the same again,
jr z, .seed ; try another random number
.end: ld a,e ; Get the copy back
ld (BLK_NEXT), a ; This is our next block
Tetris 块选择器的基础是 DIV 寄存器:由于选择程序每几秒只运行一次,因此寄存器在任何给定的运行中都会有一个未知值,因此它可以公平地近似随机数源。模拟定时器系统后,可以模拟俄罗斯方块及其克隆体的全部功能。
迄今为止一直被忽视的游戏仿真的一个方面是声音的产生,以及声音与仿真速度的同步。除了模拟器产生声音的方面之外,是将声音输出到浏览器的方法;本系列的下一部分将研究围绕声音输出机制的问题,以及是否可以将连贯的策略放在一起以在 javascript 中产生声音。
以上是关于Game boy模拟器(10):计时器的主要内容,如果未能解决你的问题,请参考以下文章