Game boy模拟器:精灵

Posted 妇男主人

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Game boy模拟器:精灵相关的知识,希望对你有一定的参考价值。

在本系列之前,模拟器已扩展为启用键盘输入,这意味着可以玩井字游戏。这样做留下的问题是游戏必须盲目进行:没有指示下一步将在何处进行,也没有按键将您移动到游戏中的哪个位置。传统上,二维游戏机通过使用精灵解决了这个问题:可移动对象块可以独立于背景放置,并且包含与背景分开的数据。GameBoy 在这方面也不例外:它允许将精灵放置在背景上方或下方,并且多个精灵可以同时出现在屏幕上。一旦在模拟器中实现了这一点。

简介:GameBoy 精灵

GameBoy 精灵是图形图块,就像用于背景的图块一样:这意味着每个精灵都是 8x8 像素。如上所述,精灵可以放置在屏幕上的任何位置,包括屏幕外的一半或全部,并且它可以放置在背景的上方或下方。从技术上讲,这意味着背景下方的精灵通过背景颜色值为 0 的位置显示出来。

精灵优先级


在上图中,背景上方的精灵通过其中间显示背景,因为精灵中的这些像素被设置为颜色0;以同样的方式,背景让其下方的精灵穿过背景颜色为 0 的地方。为了在模拟器中模拟这一点,最简单的过程是渲染背景下方的精灵,然后是背景本身,最后是它上面的精灵。然而,这是一种有点幼稚的算法,因为它重复了精灵渲染过程;首先绘制背景更简单,然后根据其优先级和该位置的背景颜色确定精灵中的给定像素是否应该出现。

精灵渲染的伪代码

For each row in sprite
    If this row is on screen
        For each pixel in row
            If this pixel is on screen
	        If this pixel is transparent
		    * Do nothing
		Else
		    If the sprite has priority
		        Draw pixel
		    Else if this pixel in the background is 0
		        Draw pixel
		    Else
		        * Do nothing
		    End If
		End If
	    End If
        End For
    End If
End For

GameBoy 精灵系统的另一个复杂问题是精灵可以在渲染时被硬件水平或垂直“翻转”;这节省了游戏空间,因为(例如)向后飞行的宇宙飞船可以用与向前运动相同的精灵表示,并应用适当的翻转。

精灵数据:对象属性内存

GameBoy 可以在称为对象属性内存 (OAM) 的专用内存区域中保存有关 40 个精灵的信息。40 个精灵中的每一个在与其关联的 OAM 中都有四个字节的数据,如下详述。

精灵的 OAM 数据

ByteDescription
0Y-coordinate of top-left corner
(Value stored is Y-coordinate minus 16)
1X-coordinate of top-left corner
(Value stored is X-coordinate minus 8)
2Data tile number
3Options
BitDescriptionWhen 0When 1
7Sprite/background priorityAbove backgroundBelow background
(except colour 0)
6Y-flipNormalVertically flipped
5X-flipNormalHorizontally flipped
4PaletteOBJ palette #0OBJ palette #1

为了在渲染扫描线时更轻松地访问此信息,构建一个结构来保存精灵数据很有用,该数据根据 OAM 的内容填充。当数据写入 OAM 时,MMU 配合图形仿真可以更新此结构以供以后使用。其实现如下。

MMU.js:OAM访问

    rb: function(addr)
    {
		switch(addr & 0xF000)
		{
		    ...
		    case 0xF000:
		        switch(addr & 0x0F00)
			{
			    ...
			    // OAM
			    case 0xE00:
			        return (addr < 0xFEA0) ? GPU._oam[addr & 0xFF] : 0;
			}
		}
    },

    wb: function(addr)
    {
		switch(addr & 0xF000)
		{
		    ...
		    case 0xF000:
		        switch(addr & 0x0F00)
			{
			    ...
			    // OAM
			    case 0xE00:
			        if(addr < 0xFEA0) GPU._oam[addr & 0xFF] = val;
				GPU.buildobjdata(addr - 0xFE00, val);
				break;
			}
		}
    }

GPU.js: 精灵结构

    _oam: [],
    _objdata: [],

    reset: function()
    {
        // In addition to previous reset code:
		for(var i=0, n=0; i < 40; i++, n+=4)
		{
		    GPU._oam[n + 0] = 0;
		    GPU._oam[n + 1] = 0;
		    GPU._oam[n + 2] = 0;
		    GPU._oam[n + 3] = 0;
		    GPU._objdata[i] = {
		        'y': -16, 'x': -8,
				'tile': 0, 'palette': 0,
				'xflip': 0, 'yflip': 0, 'prio': 0, 'num': i
		    };
		}
    },

    buildobjdata: function(addr, val)
    {
		var obj = addr >> 2;
		if(obj < 40)
		{
		    switch(addr & 3)
		    {
		        // Y-coordinate
		        case 0: GPU._objdata[obj].y = val-16; break;
			
			// X-coordinate
			case 1: GPU._objdata[obj].x = val-8; break;
	
			// Data tile
			case 2: GPU._objdata[obj].tile = val; break;
	
			// Options
			case 3:
			    GPU._objdata[obj].palette = (val & 0x10) ? 1 : 0;
			    GPU._objdata[obj].xflip   = (val & 0x20) ? 1 : 0;
			    GPU._objdata[obj].yflip   = (val & 0x40) ? 1 : 0;
			    GPU._objdata[obj].prio    = (val & 0x80) ? 1 : 0;
			    break;
		    }
		}
    }

精灵调色板

如上所述,GPU 为精灵提供了两个调色板的选择:40 个精灵中的每一个都可以使用两个调色板之一,如其 OAM 条目中指定的那样。除了背景调色板之外,这些对象调色板还存储在 GPU 中,并且可以通过 I/O 寄存器以与背景调色板大致相同的方式进行更改。

GPU.js:精灵调色板处理

    _pal: {
        bg: [],
		obj0: [],
		obj1: []
    },

    wb: function(addr)
    {
        switch(addr)
		{
		    // ...
		    // Background palette
		    case 0xFF47:
		        for(var i = 0; i < 4; i++)
				{
				    switch((val >> (i * 2)) & 3)
				    {
				        case 0: GPU._pal.bg[i] = [255,255,255,255]; break;
					case 1: GPU._pal.bg[i] = [192,192,192,255]; break;
					case 2: GPU._pal.bg[i] = [ 96, 96, 96,255]; break;
					case 3: GPU._pal.bg[i] = [  0,  0,  0,255]; break;
				    }
				}
			break;
	
		    // Object palettes
		    case 0xFF48:
		        for(var i = 0; i < 4; i++)
				{
				    switch((val >> (i * 2)) & 3)
				    {
				        case 0: GPU._pal.obj0[i] = [255,255,255,255]; break;
					case 1: GPU._pal.obj0[i] = [192,192,192,255]; break;
					case 2: GPU._pal.obj0[i] = [ 96, 96, 96,255]; break;
					case 3: GPU._pal.obj0[i] = [  0,  0,  0,255]; break;
				    }
				}
			break;
	
		    case 0xFF49:
		        for(var i = 0; i < 4; i++)
				{
				    switch((val >> (i * 2)) & 3)
				    {
				        case 0: GPU._pal.obj1[i] = [255,255,255,255]; break;
					case 1: GPU._pal.obj1[i] = [192,192,192,255]; break;
					case 2: GPU._pal.obj1[i] = [ 96, 96, 96,255]; break;
					case 3: GPU._pal.obj1[i] = [  0,  0,  0,255]; break;
				    }
				}
			break;
		}
    }

渲染精灵

GameBoy 图形系统在遇到屏幕时渲染每一行:这不仅包括背景,还包括其下方和上方的精灵。换句话说,精灵的渲染必须添加到扫描线渲染器中,作为绘制背景后发生的过程。就像背景一样,LCDC 寄存器中有一个启用精灵的开关,这必须添加到 GPU 的 I/O 处理中。

由于精灵可以位于屏幕上的任何位置,包括位于屏幕外的某个位置,因此渲染器必须检查哪些精灵位于当前扫描线内。最简单的算法是检查每个人的位置,如果精灵落在扫描线的边界内,则渲染适当的线。可以通过预先计算的图块集以与背景相同的方式检索精灵数据。将这些事情放在一起的一个例子如下。

GPU.js:使用精灵渲染扫描线

    renderscan: function()
    {
        // Scanline data, for use by sprite renderer
	var scanrow = [];

        // Render background if it's switched on
        if(GPU._switchbg)
	{
	    var mapoffs = GPU._bgmap ? 0x1C00 : 0x1800;
	    mapoffs += ((GPU._line + GPU._scy) & 255) >> 3;
	    var lineoffs = (GPU._scx >> 3);
	    var y = (GPU._line + GPU._scy) & 7;
	    var x = GPU._scx & 7;
	    var canvasoffs = GPU._line * 160 * 4;
	    var colour;
	    var tile = GPU._vram[mapoffs + lineoffs];

	    // If the tile data set in use is #1, the
	    // indices are signed; calculate a real tile offset
	    if(GPU._bgtile == 1 && tile < 128) tile += 256;

	    for(var i = 0; i < 160; i++)
	    {
	        // Re-map the tile pixel through the palette
	        colour = GPU._pal.bg[GPU._tileset[tile][y][x]];

	        // Plot the pixel to canvas
	        GPU._scrn.data[canvasoffs+0] = colour[0];
	        GPU._scrn.data[canvasoffs+1] = colour[1];
	        GPU._scrn.data[canvasoffs+2] = colour[2];
	        GPU._scrn.data[canvasoffs+3] = colour[3];
	        canvasoffs += 4;

		// Store the pixel for later checking
		scanrow[i] = GPU._tileset[tile][y][x];

	        // When this tile ends, read another
	        x++;
	        if(x == 8)
	        {
	    	    x = 0;
	    	    lineoffs = (lineoffs + 1) & 31;
	    	    tile = GPU._vram[mapoffs + lineoffs];
	    	    if(GPU._bgtile == 1 && tile < 128) tile += 256;
	        }
	    }
	}

	// Render sprites if they're switched on
	if(GPU._switchobj)
	{
	    for(var i = 0; i < 40; i++)
	    {
	        var obj = GPU._objdata[i];

			// Check if this sprite falls on this scanline
			if(obj.y <= GPU._line && (obj.y + 8) > GPU._line)
			{
			    // Palette to use for this sprite
			    var pal = obj.pal ? GPU._pal.obj1 : GPU._pal.obj0;
	
	        	// Where to render on the canvas
			    var canvasoffs = (GPU._line * 160 + obj.x) * 4;
	
			    // Data for this line of the sprite
			    var tilerow;
	
			    // If the sprite is Y-flipped,
			    // use the opposite side of the tile
			    if(obj.yflip)
			    {
			        tilerow = GPU._tileset[obj.tile]
				                      [7 - (GPU._line - obj.y)];
			    }
			    else
			    {
			        tilerow = GPU._tileset[obj.tile]
				                      [GPU._line - obj.y];
			    }
	
			    var colour;
			    var x;
	
			    for(var x = 0; x < 8; x++)
			    {
			        // If this pixel is still on-screen, AND
					// if it's not colour 0 (transparent), AND
					// if this sprite has priority OR shows under the bg
					// then render the pixel
					if((obj.x + x) >= 0 && (obj.x + x) < 160 &&
					   tilerow[x] &&
					   (obj.prio || !scanrow[obj.x + x]))
					{
				        // If the sprite is X-flipped,
					    // write pixels in reverse order
					    colour = pal[tilerow[obj.xflip ? (7-x) : x]];
		
					    GPU._scrn.data[canvasoffs+0] = colour[0];
					    GPU._scrn.data[canvasoffs+1] = colour[1];
					    GPU._scrn.data[canvasoffs+2] = colour[2];
					    GPU._scrn.data[canvasoffs+3] = colour[3];
		
					    canvasoffs += 4;
					}
			    }
			}
	    }
	}
    },
    
    rb: function(addr)
    {
        switch(addr)
		{
		    // LCD Control
		    case 0xFF40:
		        return (GPU._switchbg  ? 0x01 : 0x00) |
			       (GPU._switchobj ? 0x02 : 0x00) |
			       (GPU._bgmap     ? 0x08 : 0x00) |
			       (GPU._bgtile    ? 0x10 : 0x00) |
			       (GPU._switchlcd ? 0x80 : 0x00);
	
		    // ...
		}
    },

    wb: function(addr, val)
    {
        switch(addr)
		{
		    // LCD Control
		    case 0xFF40:
		        GPU._switchbg  = (val & 0x01) ? 1 : 0;
				GPU._switchobj = (val & 0x02) ? 1 : 0;
				GPU._bgmap     = (val & 0x08) ? 1 : 0;
				GPU._bgtile    = (val & 0x10) ? 1 : 0;
				GPU._switchlcd = (val & 0x80) ? 1 : 0;
			break;
	
		    // ...
		}
    }

接下来

有了精灵,图 1 中运行的井字游

以上是关于Game boy模拟器:精灵的主要内容,如果未能解决你的问题,请参考以下文章

Game boy模拟器:运行内存

Game boy模拟器:图形

Game boy模拟器:内存池

Game boy模拟器:完整的 Z80 内核CPU源码

Game boy模拟器:CPU

Game boy模拟器:输入