Loading

从零开始的nes模拟器-[5] PPU渲染精灵

PPU 精灵渲染

参考

精灵

这里再详细讲一下 Sprite (精灵)的概念。

精灵是一种可以在整个屏幕上自由移动的贴图。精灵通常是8×8 ( 可以理解为和1个tile的尺寸是一样的 ),但它们也可以是8×16(稍微复杂一些)。我将使用8×8的例子。精灵由PPUOAM部分中的256个字节定义。有64个精灵。这是每个精灵4个字节。

但8×8是如此之小。我们如何让马里奥如此之大?我们将多个精灵组合在一起在屏幕上移动。这被称为metasprite

image-20220209104456243

小马里奥由4个精灵组成,大马里奥由8个精灵组成。

Nes 限制了一个扫描线,最多只有 8 个 sprite .

如果要了解 8 * 8 和 8 * 16 的 Sprite 的各自的优劣 , 可以参考这个文档:Sprite size

精灵的表示方法

OAM 全称 Object Attribute Memory,位于 PPU 芯片中,一共 256 bytes,一个 sprite 用 4 bytes,所以总共能表示 64 个 sprite。

4个字节的属性( 详见 )

Byte 0:
Sprite 的 Y 坐标

Byte 1:

76543210
||||||||
|||||||+- Bank ($0000 or $1000) of tiles
+++++++-- Tile number of top of sprite (0 to 254; bottom half gets the next tile)

该字节类似于 name table,sprite 有 2 种模式:

  1. 8 x 8
    整个 byte 类似于 name table,由 PPUCTR 的 bit 3 选取 bank 之后,加上自身数据 x 16 得到偏移量

    因为 pattern table 表示一个 tile 需要 16 byte, 所以要 * 16

  2. 8 x 16
    该模式下 PUCTRL 的 bit 3 不再起作用,bank 由 bit 0 决定,并且偏移量不再是 x 16,而是 x 32,具体参考.

    8 * 16 相当于是 两个 tile , 所以要 * 32.

    下面是一些例子, 表示该字节的各个值映射到的对应内存位置

    • $00: $0000-$001F
    • $01: $1000-$101F
    • $02: $0020-$003F
    • $03: $1020-$103F
    • $04: $0040-$005F
      [...]
    • $FE: $0FE0-$0FFF
    • $FF: $1FE0-$1FFF

Byte 2:

76543210
||||||||
||||||++- Palette (4 to 7) of sprite
|||+++--- Unimplemented
||+------ Priority (0: in front of background; 1: behind background)
|+------- Flip sprite horizontally
+-------- Flip sprite vertically

bit 0-1 决定高 2 bit 的 palette,类似于 attribute table 的功能
bit 5 决定优先级,如果 sprite 像素和 background 像素都不是透明像素的情况下(即 palette index % 4 != 0),则决定了到底显示 sprite 还是 background
bit 6-7 决定是否翻转像素,比如人物往右走设置为不翻转,往左走则设置为垂直翻转

Byte 3:
Sprite 的 X 坐标

代码定义

// OAM是PPU内部的额外内存。它是
// 有通过任何总线连接。它存储下一帧要被绘制的tile的信息
struct sObjectAttributeEntry
{
    uint8_t y;			// Y position of sprite
    uint8_t id;			// ID of tile from pattern memory
    uint8_t attribute;	//  Flags define how sprite should be rendered
    uint8_t x;			// X position of sprite
} OAM[64];

写OAM

OAM 并不存在于 PPU 或 CPU 总线上,需要 CPU 通过 PPU 寄存器或者 DMA 方式才能写入

但是 OAM 并不存在于 PPUCPU 总线上,需要 CPU 通过 PPU 寄存器或者 DMA 方式才能写入

端口地址 读写/位 功能描述 解释
$2003 精灵RAM指针 设置精灵RAM的8位指针
$2004 读写 精灵RAM数据 读写精灵RAM数据, 访问后指针+1
$4014 DMA访问精灵RAM 通过写一个值$xx, 将CPU内存地址为$xx00-$xxFF的数据复制到精灵内存

代码:寄存器方式

定义

// 一些辅助变量 , 简单来说就是保存了cpu和ppu的oam通讯地址的寄存器, 对应CPU内存布局的 0x2005 的OAM_ADDR寄存器
uint8_t oam_addr = 0x00;



// 方便DMA使用的指针
public:
uint8_t* pOAM = (uint8_t*)OAM;

cpu写寄存器

// OAM Address
oam_addr = data;

// OAM Data
pOAM[oam_addr] = data;

代码:DMA方式

首先要定义和DMA相关的量

我们要把这些变量放进总线类(Bus class),因为总线掌管时钟信息,DMA又要和时钟协同工作

    // 游戏精灵的传送使用DMA机制,精灵一共有64个,每个精灵大小为4个字节
    // DMA传送开始时,CPU停止clock,改为DMA传送或者读取一次数据,每个周期还是只能进行读或者写一次操作
    // dma_page和dma_addr组成一个16位的CPU地址,
    // 通过DMA机制,将CPU中的数据,一个字节一个字节的送到PPU中
    // 由于开始时需要在CPU的偶数周期进行第一次读,所以需要513或者514个周期
    uint8_t dma_page = 0x00;
    uint8_t dma_addr = 0x00;
    uint8_t dma_data = 0x00;

DMA 会占用 512 个 CPU 时钟(奇数 CPU 周期还会再加一个时钟,前期可以先不考虑), 所以要加一个标记记录是否有额外周期。

    // DMA传输需要精确传送。原则上需要
    // 512个周期来读取和写入256字节的OAM内存,一个
    // 先读后写。但是,CPU需要处于“偶数”状态
    // 时钟周期,因此可能需要一个虚拟的空闲周
    bool dma_dummy = true;

DMA传输需要精确计时。从理论上讲,读取和写入256个字节的OAM内存需要512个周期,即先读后写。然而,CPU需要处于一个“偶”的时钟周期,因此可能需要一个虚拟的空闲周期

DMA启用的标记位

	// DMA传送发生标志
	bool dma_transfer = false;

写DMA

在cpu写接口加入这段语句

if (addr == 0x4014)
	{
		// A write to this address initiates a DMA transfer
		dma_page = data;
		dma_addr = 0x00;
		dma_transfer = true;						
	}

可以将DMA理解为独立于CPU,PPU的东西,要在时序上统筹起来,而DMA的工作时序是以CPU的时序为标准的,所以要和CPU同步

void Bus::clock()
{
    // 时钟, 模拟器的核心
    // 整个模拟器的运行"节奏"
    // 其中,ppu时钟通常用来决定整个模拟器速度
	ppu.clock();


      // CPU的时钟周期比PPU和APU的慢3倍,也就是说,PPU和APU每执行3次,CPU执行一次
      // 这是NES模拟器中规定的
      // 使用nSystemClockCounter来记录ppu的时钟周期. 除3取余即可得到cpu时钟
	if (nSystemClockCounter % 3 == 0)
	{
		// 判断是否发生DMA传送(将OAM数据传送到PPU中)
		if (dma_transfer)
		{
			// 可能存在cpu的周期bug
                        // 这里虽然在cpu的周期中,但实际cpu不进行clock,
                        // 此时DMA占用cpu周期
                        // 通常占用513或514个周期
                        // (等待写入完成时为 1 个等待状态周期,如果在奇数 CPU 周期中为 + 1,则为
                        // 256 个交替读 / 写周期。) dma_dummy的默认是true
			if (dma_dummy)
			{
				 // 执行周期必须是偶数,需要等待一个cpu周期
				if (nSystemClockCounter % 2 == 1)
				{
					 // DMA开始传送
					dma_dummy = false;
				}
			}
			else
			{
				// DMA can take place!
				if (nSystemClockCounter % 2 == 0)
				{
					// 偶数周期从cpu中读取数据,
                                        // dma_page存储着 0x(dma_page)xx数据
                                        // xx通常从0开始 ,0xFF结束,
                                        // 64个精灵,每个精灵4个字节,所以是256个字节,也就是0xFF大小
                                        // dma_page是地址的高位,dma_addr是地址的低位
                                        // dma_data即为从cpu中读取的数据
					dma_data = cpuRead(dma_page << 8 | dma_addr);
				}
				else
				{
					// On odd clock cycles, write to PPU OAM
                                        // (我估计是读卡带里的内容)
					ppu.pOAM[dma_addr] = dma_data;
					
					dma_addr++;
			
                                         // dma_addr 是 8 位的, 每次写入 OAM 地址 + 1 , 加到 256 之后 就会回归 0 了
					if (dma_addr == 0x00)
					{
						dma_transfer = false;
						dma_dummy = true;
					}
				}
			}
		}
		else
		{
			// 没有发生DMA传送时,CPU时钟正常进行。
			cpu.clock();
		}		
	}

	// PPU的中断通常发生在vertical blanking period,垂直消隐期间
        // 也就是scanline大于251?? 的时候,属于在屏幕的下方,
        // 此时cpu需要进行下一帧的名称表的准备
        // 不可能在绘制的时间进行准备,只能等到绘制结束,否则会产生很多问题
        // 唯一的机会就是在垂直消隐期间进行,也是nes设计巧妙之处
	if (ppu.nmi)
	{
		ppu.nmi = false;
		cpu.nmi();
	}

	nSystemClockCounter++;
}

渲染精灵

流程

从宏观上来看,需要几个步骤

  1. scanline结尾,搜索OAM,确定不多于8个将会在下一个scanline显示的sprites.
  2. 一个scanline每扫描一个点 ( 1 个 cycle ),sprites的x坐标减1
  3. 如果某个 sprite 的 x 坐标 减为 0 , 开始绘制这个 Sprite
  4. 解 决绘制时 (有可能会) 遇到的sprites优先级的问题

代码

定义内部变量

// in class ppu_2c02
private:
sObjectAttributeEntry spriteScanline[8]; // 保存将要绘制的 sprites
uint8_t sprite_count; // 统计要绘制的 sprites 数

准备sprites

在ppu时钟函数里添加渲染的代码, 这里添加渲染精灵的代码不会影响渲染背景的代码。

if (cycle == 257 && scanline >= 0)
    {


        // 我们已经到达一条可见扫描线的末端。现在是决定的时候了
        // 哪些精灵在下一条扫描线上可见,并预加载此信息
        // 我们可以在扫描线扫描行时使用缓冲区。
        // 首先,清除精灵记忆。此内存用于存储
        // 要渲染的精灵。这不是奥姆。

        std::memset(spriteScanline, 0xFF, 8 * sizeof(sObjectAttributeEntry));

        // 最大支持8个
        sprite_count = 0;


        for (uint8_t i = 0; i < 8; i++)
        {
            sprite_shifter_pattern_lo[i] = 0;
            sprite_shifter_pattern_hi[i] = 0;
        }

        // Thirdly, Evaluate which sprites are visible in the next scanline. We need
        // to iterate through the OAM until we have found 8 sprites that have
        // Y-positions and heights that are within vertical range of the next
        // scanline. Once we have found 8 or exhausted the OAM we stop. Now, notice
        // I count to 9 sprites. This is so I can set the sprite overflow flag in
        // the event of there being > 8 sprites.
        uint8_t nOAMEntry = 0;

        // New set of sprites. Sprite zero may not exist in the new set, so clear
        // this flag.
        bSpriteZeroHitPossible = false;
        // 在每一行中的257列的周期中,将下一行需要绘制的精灵进行预写入内存中
        while (nOAMEntry < 64 && sprite_count < 9)
        {
            // Note the conversion to signed numbers here
            int16_t diff = ((int16_t)scanline - (int16_t)OAM[nOAMEntry].y);
            // If the difference is positive then the scanline is at least at the
            // same height as the sprite, so check if it resides in the sprite
            // vertically depending on the current "sprite height mode" FLAGGED

            if (diff >= 0 && diff < (control.sprite_size ? 16 : 8))
            {
                // Sprite is visible, so copy the attribute entry over to our
                // scanline sprite cache. Ive added < 8 here to guard the array
                // being written to.
                if (sprite_count < 8)
                {
                    // Is this sprite sprite zero?
                    if (nOAMEntry == 0)
                    {
                        // It is, so its possible it may trigger a
                        // sprite zero hit when drawn
                        bSpriteZeroHitPossible = true;
                    }

                    memcpy(&spriteScanline[sprite_count], &OAM[nOAMEntry], sizeof(sObjectAttributeEntry));
                    sprite_count++;
                }
            }

            nOAMEntry++;
        } // End of sprite evaluation for next scanline

        // Set sprite overflow flag
        status.sprite_overflow = (sprite_count > 8);

    }

**移位寄存器的生成( 配合翻转) **

要确定绘制的精灵的像素点,要用到移位寄存器(具体含义参照上一篇博文),这里会详解移位寄存器的生成。

要理解移位寄存器的生成,首先要理解翻转。翻转本质上就是调整渲染行序或者列序

8 * 8 的模式

image-20220210113148859

这些8 * 8 的 sprite 存储的信息其实和Name table类似, 都是保存了sprite 对应的 tile 在pattern table上的 id(参考上面介绍的 sprite 的 4 个 byte 的 功能), 只不过 Name Table 还包含了 Attribute Table, 但这些sprite 就没有和 Attribute Table 相关的信息, 所以也只能索引到调色板,没有办法确定具体颜色。

8*16 的模式

和8 * 8 的识别方式类似。但是只能识别前 8 * 8 部分对应的pattern id (因为一个tile 也就 8 * 8 ), 后8 * 8 部分要独立开来计算。

image-20220210113449708

代码

准备内部变量

// 移位寄存器数组,原理和背景渲染的移位寄存器一样	
uint8_t sprite_shifter_pattern_lo[8];
uint8_t sprite_shifter_pattern_hi[8];

每一帧的开始,都要重置移位寄存器

if (scanline == -1 && cycle == 1)
{
// Effectively start of new frame, so clear vertical blank flag
status.vertical_blank = 0;
// Clear sprite overflow flag
status.sprite_overflow = 0;
// Clear the sprite zero hit flag
status.sprite_zero_hit = 0;
// Clear Shifters
for (int i = 0; i < 8; i++)
{
 sprite_shifter_pattern_lo[i] = 0;
 sprite_shifter_pattern_hi[i] = 0;
}
}

实现翻转的代码框架

// 遍历当前 scanline 会绘制的精灵
for (uint8_t i = 0; i < sprite_count; i++)
{
    
   //临时内部变量
    uint8_t sprite_pattern_bits_lo, sprite_pattern_bits_hi;
    uint16_t sprite_pattern_addr_lo, sprite_pattern_addr_hi;
    
  // 这里分情况求出地址 sprite_pattern_addr_lo, sprite_pattern_addr_hi
  // ctrl 寄存器表明是 8 * 8 模式
  if (!control.sprite_size)
  {
     // 如果是垂直翻转
      .....
          
     // 如果不是垂直翻转、
      .....

  }//表明是 8 * 16 模式
  else
  {
    // 如果是垂直翻转
      .....
          
     // 如果不是垂直翻转、
      .....
          
  }

    
  // Hi bit plane equivalent is always offset by 8 bytes from lo bit plane
 sprite_pattern_addr_hi = sprite_pattern_addr_lo + 8;

 // 把位信息提取出来
 sprite_pattern_bits_lo = ppuRead(sprite_pattern_addr_lo);
 sprite_pattern_bits_hi = ppuRead(sprite_pattern_addr_hi);
  
  // 如果是水平翻转
  if (spriteScanline[i].attribute & 0x40)
  {
  	  // 水平反转位信息

  }

    // 最后把位信息保存回移位寄存器数组
    sprite_shifter_pattern_lo[i] = sprite_pattern_bits_lo;
    sprite_shifter_pattern_hi[i] = sprite_pattern_bits_hi;

}

8 * 8 模式

// 8x8 Sprite Mode - The control register determines the pattern table
if (!(spriteScanline[i].attribute & 0x80))// 0100 0000
{
  // 非垂直翻转,直接组装出地址就好 
  sprite_pattern_addr_lo = 
    (control.pattern_sprite << 12  )  // Which Pattern Table? 0KB or 4KB offset
  | (spriteScanline[i].id   << 4   )  // Which Cell? Tile ID * 16 (16 bytes per tile)
  | (scanline - spriteScanline[i].y); // Which Row in cell? (0->7)
              
}
else
{
  // 垂直翻转,组装地址和上面稍微有点不同
  sprite_pattern_addr_lo = 
    (control.pattern_sprite << 12  )  // Which Pattern Table? 0KB or 4KB offset
  | (spriteScanline[i].id   << 4   )  // Which Cell? Tile ID * 16 (16 bytes per tile)
  | (7 - (scanline - spriteScanline[i].y)); // Which Row in cell? (7->0)
}

8*16 模式

// 8x16 Sprite Mode - The sprite attribute determines the pattern table
if (!(spriteScanline[i].attribute & 0x80))
{
    // 非垂直翻转 
    if (scanline - spriteScanline[i].y < 8)
    {
        // 读上半部分
        sprite_pattern_addr_lo = 
            ((spriteScanline[i].id & 0x01)      << 12)  // Which Pattern Table? 0KB or 4KB offset
            | ((spriteScanline[i].id & 0xFE)      << 4 )  // Which Cell? Tile ID * 16 (16 bytes per tile)
            | ((scanline - spriteScanline[i].y) & 0x07 ); // Which Row in cell? (0->7)
    }
    else
    {
        //  读下半部分
        sprite_pattern_addr_lo = 
            ( (spriteScanline[i].id & 0x01)      << 12)  // Which Pattern Table? 0KB or 4KB offset
            | (((spriteScanline[i].id & 0xFE) + 1) << 4 )  // Which Cell? Tile ID * 16 (16 bytes per tile)
            | ((scanline - spriteScanline[i].y) & 0x07  ); // Which Row in cell? (0->7)
    }
}
else
{
    // 垂直翻转
    if (scanline - spriteScanline[i].y < 8)
    {
        // 读上半部分
        sprite_pattern_addr_lo = 
            ( (spriteScanline[i].id & 0x01)      << 12)    // Which Pattern Table? 0KB or 4KB offset
            | (((spriteScanline[i].id & 0xFE) + 1) << 4 )    // Which Cell? Tile ID * 16 (16 bytes per tile)
            | (7 - (scanline - spriteScanline[i].y) & 0x07); // Which Row in cell? (0->7)
    }
    else
    {
         //  读下半部分
        sprite_pattern_addr_lo = 
            ((spriteScanline[i].id & 0x01)       << 12)    // Which Pattern Table? 0KB or 4KB offset
            | ((spriteScanline[i].id & 0xFE)       << 4 )    // Which Cell? Tile ID * 16 (16 bytes per tile)
            | (7 - (scanline - spriteScanline[i].y) & 0x07); // Which Row in cell? (0->7)
    }
}

水平位翻转

//出处:  https://stackoverflow.com/a/2602885
auto flipbyte = [](uint8_t b)
{
    b = (b & 0xF0) >> 4 | (b & 0x0F) << 4;
    b = (b & 0xCC) >> 2 | (b & 0x33) << 2;
    b = (b & 0xAA) >> 1 | (b & 0x55) << 1;
    return b;
};

// Flip Patterns Horizontally
sprite_pattern_bits_lo = flipbyte(sprite_pattern_bits_lo);
sprite_pattern_bits_hi = flipbyte(sprite_pattern_bits_hi);

这个代码很妙, 举个例子

1110 1001

=》 0000 1110 | 1001 0000 = 1001 1110

=》 0010 0011 | 0100 1000 = 0110 1011

=》0001 0101 | 1000 0010 = 1001 0111

移位寄存器更新

移位寄存器的作用参照上一篇博客,这里不详细解说了。

上面介绍了移位寄存器的生成,这里介绍渲染过程中移位寄存器的更新的代码(原理和背景渲染的移位寄存器差不多

简单来说就是每经过一次有效渲染,寄存器就左移一位。

代码

// 精灵的移位寄存器更新
// 确保是 scanline 的 visable 区域
if (mask.render_sprites && cycle >= 1 && cycle < 258)
  {
    for (int i = 0; i < sprite_count; i++)
    {
      if (spriteScanline[i].x > 0) 
      {
         // 还没到渲染位置
        spriteScanline[i].x--;
      }
      else
      {
         // 到渲染位置了
        sprite_shifter_pattern_lo[i] <<= 1;
        sprite_shifter_pattern_hi[i] <<= 1;
      }
    }
  }
};

计算像素点颜色

OAM 的 Sprite 的 attribute 信息 + 移位寄存器 -> 具体颜色索引。

如果一个像素点出现了几个精灵,那么早出现的精灵的像素会覆盖后出先精灵的像素。(也就是早出现的精灵优先级更高,

代码

// 三个临时变量
uint8_t fg_pixel = 0x00;   // 颜色索引的前两位
uint8_t fg_palette = 0x00; // 颜色索引的后两位
uint8_t fg_priority = 0x00; //记录精灵的优先级,和像素颜色无关,但是后续渲染会用到这个量。
if (mask.render_sprites)
{
  //遍历此扫描线的所有精灵。这是为了保持精灵优先级。只要我们找到一个精灵的非透明像素,我们就可以中止
  bSpriteZeroBeingRendered = false;

  for (uint8_t i = 0; i < sprite_count; i++)
  {
    //扫描线cycle 刚好渲染到精灵的像素
    // 移位寄存器接管下面的工作
    if (spriteScanline[i].x == 0) 
    {
	
      // 组成前两位
      uint8_t fg_pixel_lo = (sprite_shifter_pattern_lo[i] & 0x80) > 0;
      uint8_t fg_pixel_hi = (sprite_shifter_pattern_hi[i] & 0x80) > 0;
      fg_pixel = (fg_pixel_hi << 1) | fg_pixel_lo;

      // 组成后两位
      fg_palette = (spriteScanline[i].attribute & 0x03) + 0x04;

        // 检查优先级
      fg_priority = (spriteScanline[i].attribute & 0x20) == 0;

	  //如果像素不是透明的,我们渲染它,但是不检查其余部分,因为列表中较早的精灵优先级更高
      if (fg_pixel != 0)
      {
        if (i == 0) // Is this sprite zero?
        {
          bSpriteZeroBeingRendered = true;
        }

        break;
      }				
    }
  }		
}

背景与精灵的较量

我们知道,一个像素点,有时候既可以按照 background 的颜色信息渲染, 也可以按照 sprite 的颜色信息渲染。那到底要怎么做决定呢?

如果其中一个是透明色,那么结果显而易见。

如果两者都不是透明色,那么就要参照 oamattribute byte 里面的属性。

代码

//现在我们有一个背景像素和一个前景像素。它们需要结合起来。精灵可以隐藏在非“透明”的背景贴图后面,这是PPU的另一个巧妙之处
uint8_t pixel = 0x00;   // The FINAL Pixel...
uint8_t palette = 0x00; // The FINAL Palette...

// 都是透明色... 
if (bg_pixel == 0 && fg_pixel == 0)
{
  pixel = 0x00;
  palette = 0x00;
}
else if (bg_pixel == 0 && fg_pixel > 0)
{
  //背景像素是透明的
  //前景像素是可见的
  pixel = fg_pixel;
  palette = fg_palette;
}
else if (bg_pixel > 0 && fg_pixel == 0)
{
//背景像素是可见的
//前景像素是透明的
  pixel = bg_pixel;
  palette = bg_palette;
}
else if (bg_pixel > 0 && fg_pixel > 0)
{
  // 如果sprite 优先级更高
  if (fg_priority)
  {
    pixel = fg_pixel;
    palette = fg_palette;
  }
  else
  {
    pixel = bg_pixel;
    palette = bg_palette;
  }

精灵0命中

精灵0命中这个特性可以用在 屏幕分割中的 Splite X Scroll上面。 ( 上一篇博文讲了屏幕滚动 ,这里讲的是屏幕分割,相对来说更高级)

split X scroll 需要用到 sprite 0 hit,主要的作用可以用来制作静止的分数,血条等等,举个马里奥的例子:

image-20220209121453555

深色部分是不会像下面浅色部分滚屏的,感觉上面和下面分割开来,且只有水平方向的分割,所以叫做 split X scroll,下面来看看是如何实现的。

先来了解什么叫做 sprite 0 hit,sprite 0 hit 就是说如果第 0 个精灵的不透明像素与背景不透明像素重合的话,就将 0x2002 PPUSTATUS 状态寄存器的 bit 6 置 1,表示触发了 sprite 0 hit

这有什么作用?它是 PPU 与 CPU 同步的一种手段,当 V_Blank 触发 NMI 时,CPU 只是知道当前帧渲染完了,准备下一帧,这个时间同步不精细。于是创造一个 sprite 0 hit,编程人员将 sprite 0 放在一个特定位置,当触发 sprite 0 hit 时,CPU 就知道,哦,原来渲染到这条 scanline 了

我们就可以利用这个特性来实现 split X scroll,我们就以超级马里奥为例子,来看 split X scroll 如何实现的。

首先看超级马里奥的 sprite 0 在哪儿:

image-20220209121653407

这是游戏刚开始的“两个”精灵,其中上面那个就是 sprite 0,具体在哪儿?

image-20220209121759573

具体的,就是在金币的下方,金币是背景,且不是使用的通用背景色,sprite 0 也不是透明色,所以每一帧渲染到这一行的 sprite 0 所在的位置时就会触发 sprite 0 hit。

注: 我的理解是这个特性不是必要的,只是为了实现某个功能的时候会用到。

代码

变量定义

// Sprite Zero Collision Flags
bool bSpriteZeroHitPossible = false;
bool bSpriteZeroBeingRendered = false;

这些变量的修改赋值,在其他部分有涉及到(自己往上面看)

实现代码

// Sprite Zero Hit detection
// 精灵0命中在前景和后景都不是透明的颜色的时候发生
if (bSpriteZeroHitPossible && bSpriteZeroBeingRendered)
{
	// Sprite 0是前景和背景之间的碰撞 , 所以它们必须都是启用的
  if (mask.render_background & mask.render_sprites)
  {
	//屏幕的左边缘有特定的开关来控制其外观。简单来说,这是用来实现平滑滚动的。
    if (~(mask.render_background_left | mask.render_sprites_left))
    {
      if (cycle >= 9 && cycle < 258)
      {
        status.sprite_zero_hit = 1;
      }
    }
    else
    {
      if (cycle >= 1 && cycle < 258)
      {
        status.sprite_zero_hit = 1;
      }
    }
  }
}
posted @ 2022-02-15 15:16  CHZarles  阅读(750)  评论(0)    收藏  举报