游戏软件开发,硬件的架构影响有多大?
本代主机的情况请看叶老师的答案,说的很清楚了。
PS3这代机器其实已经很好了,再往上一代的PS2是个游戏史上数得着的奇葩机器。我们都知道硬件T&L是从GEFORCE这代显卡才开始采用的,而过去的几何与光源运算全是扔给CPU做的,显卡只负责光栅化之后的工作填。XBOX和NGC(也就是后来的WII)都跟上了时代的潮流,而PS2仍然沿用的是90年代的思路,一个极为强大的CPU EE+单纯的光栅化渲染器GS。于是这主机就出现了一个奇观:爷爷辈的VOODOO1,第一代GPU GEFORCE1, 第一代可编程GPU GEFORCE3,技术相差10年原理完全不同的显卡同台竞争,而且游戏要实现互相移植!
游戏机原理统一是PS3和XBOX360这代才开始的,过去游戏机之间硬件原理完全不同。游戏机的硬件构架之恐怖,最资深的PC程序员在最可怕的噩梦中都不会梦到。
比如SS,这是一台纯2D主机,但是要做3D游戏。于是SEGA公司提供了一个思路:2D游戏机一般有专门的处理器负责显示画面上的角色(活动块,直译精灵),它有将方形的图像单位做旋转变形的功能,所以就将一个活动块视为一个多边形,按照透视关系做相应的变形拉伸,看上去好像是一块3D材质贴图。而CPU负责几何运算,提供活动块的位置、大小和扭曲量。这和现代3D游戏的差别,就如同鸟与火箭的原理差别。
再早的机器就更夸张了。
比如MD CD 32X这个机器,它是3台机器并联,2台是16位总线,1台是32位总线(而且这台还是双CPU的),两边总线不能直接交换数据,一般开发商的思路就是干脆让16位CPU计算背景,32位CPU计算前景画面,两边直接输出成视频信号再做合成。
有人可能会说,反正都是编程嘛——谁告诉你的?早期游戏机是没有处理器的,人家用的是分离式逻辑电路(当时集成电路很罕见),想要改变程序?自己做个板子插门电路吧。
最BT的还是FC主机,游戏卡里带专用芯片是家常便饭,典型的是任天堂出的MMC,控制内存翻页的。熟悉FC的人可能听说过MAPPER这个词,它就是专门用来模拟MMC硬件的函数库。MAPPER编号是开发模拟器的人给各游戏厂商专有MMC指定的代号,这个代号我记得有200多个。好了,你要是任天堂,怎么给200种游戏机写驱动?
有兴趣的可以看看这个介绍
32X之死——游戏机硬件增强的历史 - 未曾发生的游戏史 - 知乎专栏
这是FC的MMC3芯片
影响很大。硬件的功能及性能直接影响游戏软件的开发。
题主谈及PS3和Xbox360,本人参与过两个相关游戏项目的开发,就简单介绍一下它们硬件的差异如何影响软件开发。
PS3的特别之处,是它有一个主要的CPU(称为PPU),另外有6个可用的辅助处理单元(称为SPU)。软件开发难处在于,那些SPU各有256KB本地内存,开发者要把想并行计算的数据从主内存打包发送到SPU,完成工作后再把结果抄回来!SPU还用另一套指令集、编译程序!
PS3 Cell处理器
而Xbox360则是3核CPU,能正常地访问512MB主内存(统一内存架构)。虽然不是x86架构,但能直接用正常的多线程方式来做并行。
Xbox360 Xenon处理器
至于SDK,微软提供Direct3D 9变种的API,移植PC的代码变得简单得多。而PS3的图形API则是非常低阶的,显存的地址分配都要开发者自己实现。另外,游戏的优化是需要针对个别CPU/GPU去做的,例如用该CPU/SPU的SIMD指令集去编程。
如果同时还要跨平台至Wii,那才是恶梦!因为Wii的"GPU"是固定管道的!Shader Model 1.0都没有啊!做个材质效果要设置几十个渲染状态,像IQ题一样!想用cube map做反射效果要先把它贴在球体上渲染至frame buffer(render target都没有!),然后抄到纹理,再把纹理当作sphere map来用啊!!Wii所有内存加起来100MB都没有,而且有分快主内存、慢主内存、帧缓冲内存、纹理内存啊!!你说怎么影响软件开发!!
吐完。
在早期 PC,画图的方法就是写显存,在屏幕上画条线就是把线条覆盖到的像素对应的显存给写成 1,换言之,显示屏上所有的像素在都有对应的地址。但是游戏机不同。早期「游戏机」的显存并不直接存储像素,相反它存储了构成游戏画面的数据结构,比如各个图块的内容(起始地址)、位置、调色板,高级些的主机还有大小旋转等。有些像什么呢?像 HTML+CSS……对,就是 HTML4 时代做无 flash 页游。
游戏机和 PC 统一架构是第八代才做到的,当然苗头是在第六代,这个过程 M¥ 功不可没……
给你们上一段 FC 的技术资料(经过逆向工程得到),让你们感受下 1980 年代的十一区为了榨取机器性能都干了些什么事情
+--------+
| 4. PPU |
+--------+
A. 概述
-------
镜像 (也被称为"shadowing") 是将特殊的地址或抵制范围通过硬件映射到其他地址的一种处理.
B. 内存映射
-----------
这里有两 (2) 片内存映射. 第一部分被称为 "RAM Memory Map",是一段不算长的指向NES本身
物理RAM的区域。第二部分是 "Programmer Memory Map",是比较长的描述全部NES和他如何被使
用以及如何被操作的内存区域.
RAM Memory Map
+---------+-------+--------------------+
| Address | Size | Description |
+---------+-------+--------------------+
| $0000 | $1000 | Pattern Table #0 |
| $1000 | $1000 | Pattern Table #1 |
| $2000 | $800 | Name Tables |
| $3F00 | $20 | Palettes |
+---------+-------+--------------------+
Programmer Memory Map
+---------+-------+-------+--------------------+
| Address | Size | Flags | Description |
+---------+-------+-------+--------------------+
| $0000 | $1000 | C | Pattern Table #0 |
| $1000 | $1000 | C | Pattern Table #1 |
| $2000 | $3C0 | | Name Table #0 |
| $23C0 | $40 | N | Attribute Table #0 |
| $2400 | $3C0 | N | Name Table #1 |
| $27C0 | $40 | N | Attribute Table #1 |
| $2800 | $3C0 | N | Name Table #2 |
| $2BC0 | $40 | N | Attribute Table #2 |
| $2C00 | $3C0 | N | Name Table #3 |
| $2FC0 | $40 | N | Attribute Table #3 |
| $3000 | $F00 | R | |
| $3F00 | $10 | | Image Palette #1 |
| $3F10 | $10 | | Sprite Palette #1 |
| $3F20 | $E0 | P | |
| $4000 | $C000 | F | |
+---------+-------+-------+--------------------+
C = Possibly CHR-ROM
N = Mirrored (see Subsection G)
P = Mirrored (see Subsection H)
R = Mirror of $2000-2EFF (VRAM)
F = Mirror of $0000-3FFF (VRAM)
C. Name Tables
--------------
NES使用马赛克矩阵进行图形显示; 这样的格子被叫做 Name Table. 马赛克是 8x8像素 [pixels].
完整的 Name Table 有 32*30 个马赛克 (256*240 像素). 紧记: NTSC和PAL单元在显示上有不同
的刷新率.
Name Tables 之中的马赛克的资料被保存在 Pattern Table 之中 (continue on).
D. Pattern Tables
-----------------
Pattern Table 包括了 Name Table 所需要的 8x8 的马赛克. 他保存了NES调色板中所有16色的
4-bit 颜色矩阵的低两 (2) 位 [bit]. 例如:
VRAM Contents of Colour
Addr Pattern Table Result
------ --------------- --------
$0000: %00010000 = $10 --+ ...1.... Periods are used to
.. %00000000 = $00 | ..2.2... represent colour 0.
.. %01000100 = $44 | .3...3.. Numbers represent
.. %00000000 = $00 +-- Bit 0 2.....2. the actual palette
.. %11111110 = $FE | 1111111. colour #.
.. %00000000 = $00 | 2.....2.
.. %10000010 = $82 | 3.....3.
$0007: %00000000 = $00 --+ ........
$0008: %00000000 = $00 --+
.. %00101000 = $28 |
.. %01000100 = $44 |
.. %10000010 = $82 +-- Bit 1
.. %00000000 = $00 |
.. %10000010 = $82 |
.. %10000010 = $82 |
$000F: %00000000 = $00 --+
上面的 Pattern Table 的结果就是字符 'A',就像右上角的 "Colour Result" 部分显示的.
E. Attribute Tables
-------------------
Attribute Table 的每一个字节都描述了显示器上的一个 4*4 马赛克组. 有许多种方法来描述
Attribute Table 中一 (1) 个字节的函数是怎样的:
* 保存了 32*32 像素格子 中每 16*16 像素 的高两 (2) 位.
* 保存了 十六 (16) 个 8x8 马赛克中的 高两 (2) 位.
* 保存了 四 (4) 个 4*4 像素格子 的高两 (2) 位.
这样说确实很乱; 下面的两个图表将有所帮助:
+------------+------------+
| Square 0 | Square 1 | #0-F represents an 8x8 tile
| #0 #1 | #4 #5 |
| #2 #3 | #6 #7 | Square [x] represents four (4) 8x8 tiles
+------------+------------+ (i.e. a 16x16 pixel grid)
| Square 2 | Square 3 |
| #8 #9 | #C #D |
| #A #B | #E #F |
+------------+------------+
真正的 attribute byte 格式如下 (与上面的相比较):
Attribute Byte
(Square #)
----------------
33221100
||||||+--- Upper two (2) colour bits for Square 0 (Tiles #0,1,2,3)
||||+----- Upper two (2) colour bits for Square 1 (Tiles #4,5,6,7)
||+------- Upper two (2) colour bits for Square 2 (Tiles #8,9,A,B)
+--------- Upper two (2) colour bits for Square 3 (Tiles #C,D,E,F)
F. 调色板
---------
NES有两个 16色 "调色板": 图形调色板和子图形调色板. 因为他们不储存物理RGB值,所以
比起一个真正的调色板,它们更像 "查找表格".
写到 $3F00-3FFF 的 D7-D6 字节将被忽略.
G. Name Table 镜像
------------------
需要紧记的一点是在理解NES的时候,有许多镜像表格. 即使在使用 CHR-ROM-mapped Name Tables
(mapper-specific).
NES本身只有 2048 ($800) 字节的RAM给 Name Tables. 然而,就像在 Subsection B 中所表现
得那样 NES 有升至 四 (4) 个 Name Tables 的地址容量.
缺省的是许多 carts 伴随的是 "水平" 和 "垂直" 的镜像,允许你修改 Name Tables 所指向的NES
PPU RAM 的位置. 这个镜像表格同时作用于两 (2) 个Name Tables; 你不可以独自选择 Name Tables.
下面的表格将帮助理解NES中遇到的所有镜像类型. 请注意显示的地址 (大小为 12-bit) 都是 NES PPU
RAM 的 Name Table 的一部分; 有人为认为这些与VRAM区域中的 "$2xxx" 同义:
Name NT#0 NT#1 NT#2 NT#3 Flags
+--------------------------+------+------+------+------+-------+
| Horizontal | $000 | $000 | $400 | $400 | |
| Vertical | $000 | $400 | $000 | $400 | |
| Four-screen | $000 | $400 | $800 | $C00 | F |
| Single-screen | | | | | S |
| CHR-ROM mirroring | | | | | C |
+--------------------------+------+------+------+------+-------+
F = 依赖于扩展的 2048 ($800) RAM (kept on the cart) 的四屏镜像,导致四 (4) 个独立的物理
Name Tables.
S = 拥有映射表 [mapper] 的单屏游戏,允许你选择哪个PPU RAM区域来使用 ($000, $400, $800,
$C00); 所有的 NT 都指向同样的 PPU RAM 地址.
C = 映射表 #68 (Afterburner 2),允许你将 CHR-ROM 镜像到 NES 的 PPU RAM 区域中 Name Tables
区域. 很自然的这使得 Name Table 成为 ROM 基础,并且不能对它写入. 然而,这个特点可以通
过映射表本身进行控制,使得你打开或关闭这个特点.
H. 调色板镜像
-------------
镜像发生在图形调色板和子图形调色板之间. 任何写入 $3F00 的数据都被镜像到 $3F10. 任何写入 $3F04
的数据都被镜像到 $3F14,如此. 如此...
在图形调色板和子图形调色板的高三 (3) 色 Colour #0 被定义为透明 (实际上那里储存的颜色不被显示).
PPU 使用 $3F00 来定义背景色.
另一个长一些的解释,如下:
* $0D 被写入 $3F00 (镜像到 $3F00)
* $03 被写入 $3F08 (镜像到 $3F08)
* $1A 被写入 $3F18
* $3F08 被读入 累加器 [accumulator]
PPU 使用 $0D 作为背景色,不论 $3F08 饱含了 $03 的值 (因为所有的调色板中的 colour #0 都被定义为
透明色,它不被显示). 最后,累加器将保存 $1A 的值,它是 $3F18 的镜像. 然后 $1A 不被显示,因为
colour #0 为透明色.
图形和子图形调色板都被镜像到其他的VRAM区域; $3F20-3FFF 分别是他们的镜像.
被写入 $3F00-3FFF 的 D7-D6 字节被忽略.
I. 背景卷轴
-----------
NES能够独立于在背景之上的字画面来卷动背景 (pre-rendered Name Table + Pattern Table + Attribute
Table). 背景能被水平和竖直卷动.
卷轴工作如下:
Horizontal Scrolling Vertical Scrolling
0 512
+-----+-----+ +-----+ 0
| | | | |
| A | B | | A |
| | | | |
+-----+-----+ +-----+
| |
| B |
| |
+-----+ 480
Name Table "A" 是通过在寄存器 $2000 中的 Bits D1-D0 来指定的,"B" 接下来的 Name Table (由于镜
像,这是动态的). 它在同时使用水平和竖直卷轴的游戏时不工作.
背景将跨越多个 Name Table,就像这里展示的:
+---------------+---------------+
| Name Table #2 | Name Table #3 |
| ($2800) | ($2C00) |
+---------------+---------------+
| Name Table #0 | Name Table #1 |
| ($2000) | ($2400) |
+---------------+---------------+
写到水平卷轴地址为 $2005 的值得范围是 0 至 256.
写到垂直卷轴的值是 0-239; 超过 239 的值时不被考虑的 (例如: 248 被认为是 -8).
J. 屏幕和子图形的层
-------------------
在NES显示的时候使用的是一种特殊的规则:
FRONT BACK
+----+-----------+----+-----------+-----+
| CI | OBJs 0-63 | BG | OBJs 0-63 | EXT |
+----+-----------+----+-----------+-----+
| SPR-RAM | | SPR-RAM |
| BGPRI==0 | | BGPRI==1 |
+-----------+ +-----------+
CI 的意思是 'Colour Intensity' [颜色亮度], 与 $2001 的 D7-D5 等价. BG 是 背景 [BackGround],EXT
是 扩展端口视频信号 [EXTension port video signal].
'BGPRI' 描述的是 SPR-RAM 中的 'Background Priority' [背景优先权] bit,在 per-sprite 基础上 (D5,
Byte 2).
OBJ 数目描述真正的子图形数目,不是 马赛克索引值 [Title Index values].
FRONT 被认为是在其他所有层上被看到的 (最后绘制),BACK 被认为是其他所有层之下的 (最先绘制).
K. 子图形和 SPR-RAM
-------------------
NES支持64个子图形. 每个子画面的大小可以是 8x8 或者 8x16 像素. 子画面数据被保存在 VRAM 的 Partt-
ern Table 区域.
子画面的特征,比如 flipping 和 优先权,被保存在 SPR-RAM. SPR-RAM 的格式如下:
+-----------+-----------+-----+------------+
| Sprite #0 | Sprite #1 | ... | Sprite #63 |
+-+------+--+-----------+-----+------------+
| |
+------+----------+--------------------------------------+
+ Byte | Bits | Description |
+------+----------+--------------------------------------+
| 0 | YYYYYYYY | Y Coordinate - 1. Consider the coor- |
| | | dinate the upper-left corner of the |
| | | sprite itself. |
| 1 | IIIIIIII | Tile Index # |
| 2 | vhp000cc | Attributes |
| | | v = Vertical Flip (1=Flip) |
| | | h = Horizontal Flip (1=Flip) |
| | | p = Background Priority |
| | | 0 = In front |
| | | 1 = Behind |
| | | c = Upper two (2) bits of colour |
| 3 | XXXXXXXX | X Coordinate (upper-left corner) |
+------+----------+--------------------------------------+
Tile Index # 被获得的方法与 Name Table 数据一样.
大小为 8x16 的子画面函数有些不同. 一个有偶数 Tile Index # 的 8x16 子画面使用在 VRAM 中 $2000
的 Pattern Table; 奇数 Tile Index #s 使用 $1000. *注意*: 寄存器 $2000 对 8x16子画面无效.
所有 64 个子图形都包括一个内部优先权; 子画面 #0 的优先权比 #63高 (子画面 #0 应当被最后绘制,
etc...).
只有八 (8) 个子图形可以被显示在同一个扫描线 [scan-line] 上. 每个 SPR-RAM 入口都被检测来知道他
是不是与其他的子图形在同一水平线上. 记得,这是在每个扫描线的基础上被执行的,不是在每个子画面
的基础上 (例如,做256次,而不是 256/8 或者 256/16次).
(注意: 在一个真正的NES单元,如果子画面被关闭了很长一段时间 ($2001的D4是0),SPR-RAM 将被降低. 一
个被建议的观点是 SPR-RAM 是一个真正的 DRAM,D4 控制这个 DRAM 的刷新周期).
L. 子图形 #0 点击标记
---------------------
PPU有能力演算出子图形 #0的位置,并且把它的发现储存到 $2002 的 D6. 工作的方式如下:
PPU扫描第一个真正的不透明 "子图形像素" 和第一个不透明 "背景像素". 背景像素是被Name Table使用中
的马赛克. 记得colour #0 被定义为透明.
导致 D6 被设置为 *IS* 的像素被绘制.
下面的例子也许有所帮助. 下面是两个马赛克. 透明色 (colour #0) 被通过下划线字符 ('_') 定义. 一个
星号 ('*') 在 D6 被设置时描述.
Sprite BG Result
------ -- ------
__1111__ ________ __1111__
_111111_ _______2 _1111112
11222211 ______21 11222211
112__211 + _____211 = 112__*11 '*' will be drawn as colour #2
112__211 ____2111 112_2211
11222211 ___21111 11222211
_111111_ __211111 _1111111
__1111__ _2111111 _2111111
这同样适用于那些在 BG 下面 (通过 'Background Priority' SPR-RAM bit),可是上面的例子应当为 'BG+Sprite'.
同样,D6在每个VBlank之后清空 (设置为0).
M. 水平和竖直空白
-----------------
就像其他的控制台,NES有一个更新: 显示设备重新定位电子枪来显示可视数据. 最普通的显示设备是电视机. NTSC
设备每秒钟刷新60次,PAL每秒50次.
电子枪绘制像素时从左向右: 这种过程导致一 (1) 条水平扫描线被绘制. 在电子枪绘制完成一条完整的扫描线后,电
子枪必须回到显示设备的左边,准备好去绘制下一条扫描线. 电子枪回到左边的过程叫做 水平空白期 [Horizontal
Blank period] (HBlank).
当电子枪完成绘制所有扫描线,它必须回到显示设备的最上端; 电子枪复位回到显示设备的最上端的时间叫做 竖直空
白期 [Vertial Blank period] (VBlank).
就像你能从下面的图表中看到的,电子枪或多或少的工作在 'Z' 字形轨迹直到到达 VBlank,然后过程重复:
+-----------+
+--->|***********| <-- Scanline 0
| | ___---~~~ | <-- HBlank
V |***********| <-- Scanline 1
B | ___---~~~ | <-- HBlank
l | ... | ...
a | ... | ...
n |***********| <-- Scanline 239
k +-----+-----+
| |
+--VBlank--+
NTSC的NES是下面的刷新方式和屏幕格式:
+--------+ 0 ----+
| | |
| | |
| Screen | +-- (0-239) 256x240 on-screen results
| | |
| | |
+--------+ 240 --+
| ?? | +-- (240-242) Unknown
+--------+ 243 --+
| | |
| VBlank | +-- (243-262) VBlank
| | |
+--------+ 262 --+
竖直空白 (VBlank) 标记被包括在 $2002 的 D7中. 它指出PPU是否处于VBlank. 一段程序可以通过读 $2002 重置D7.
N. $2005/2006 矩阵编码
----------------------
有关于 $2005 和 $2006 寄存器的详细信息,参考 Loopy 的 $2005/2006 文档. 他的文档提供了的关于这些寄存器
如何工作的完整准确的信息. 联系 Loopy 获得更多信息.
O. PPU 怪癖
-----------
第一次读VRAM是无效的. 由于这种现象,NES返回 pseudo 缓冲值,而不是期待的线性值. 看下面的例子:
VRAM $2000 contains $AA $BB $CC $DD.
VRAM incrementation value is 1.
The result of execution is printed in the comment field.
LDA #$20
STA $2006
LDA #$00
STA $2006 ; VRAM address now set at $2000
LDA $2007 ; A=?? VRAM Buffer=$AA
LDA $2007 ; A=$AA VRAM Buffer=$BB
LDA $2007 ; A=$BB VRAM Buffer=$CC
LDA #$20
STA $2006
LDA #$00
STA $2006 ; VRAM address now set at $2000
LDA $2007 ; A=$CC VRAM Buffer=$AA
LDA $2007 ; A=$AA VRAM Buffer=$BB
就像演示的,PPU将在第一次读入执行后传递增加他的内部地址. 这 *仅适用* 于 VRAM $0000-3FFF (例如: 调色板信
息和他们各自的镜像不必忍受这种现象).
P. 注意
-------
PPU 将会在访问 $2007 后自动增加VRAM地址,加1或者32 (基于 $2000 的 D2).
学习游戏软件开发要具备以下条件:
1、基础编程:C/C++基础编程,WinAPI(windows programming),数据结构,游戏算法 ( Game Mathematics ),C/C++语言在游戏中的应用、游戏算法、数据结构和STL在游戏中的应用等。
2、DirectX图形编程:DirectX基础概念,Key Board及Mouse控制,预告篇控制,2D/3D图像输出,游戏效果音,游戏背景音乐,DirectX的Network Socket,OpenGL的图形编程应用。DirectX游戏网络编程应用,游戏开发人员的用户图形接口设计、MMORPG游戏服务器编程。人工智能技术在游戏编程中的应用等。
3、Socket 编程技术:掌握网络基础理论、关于数据通讯的基础概念、网络基础概念以及其Model等,Socket Programming入门、Socket Programming基础、Socket Programming高级、Application实验等,Socket Programming实战。
4、游戏服务器架设:游戏服务器架构设计,Windows2003、Unix、Linux服务器架设等。
5、高级编程实战:Java游戏程序开发,SQL服务器2000编程,利用XML编程网络游、Messenger、坦克、五子棋、象棋、俄罗斯方块等简单游戏的编程方法与技巧等。
6、项目实战开发:游戏服务器编程项目制作开发,游戏客户端编写、项目制作开发、游戏编程技巧、实时表现图形的窍门等。
又挖出一个很老的问题。
虽然貌似已经有重磅回答,但是我觉得似乎与题主的思路有点儿偏差,还是有可以补充的余地。
题主有这样的疑惑,是因为通常在PC开发甚至在诸如手机应用开发当中,往往我们并不关心具体的硬件架构细节,只是对接SDK,然后编译(或者交叉编译)成功即可。
其实对于游戏软件开发,如果SDK完备,也是可以这么做的。问题在于相比较于一般的应用软件,游戏属于软实时系统,对帧率有着较为固定的要求,因此对于处理时间(也就是性能)有着较高的要求。
有两种策略可以达到这种性能要求:
- 在硬件性能上给出充裕的余量
- 在软件优化上努力提高性能
大部分PC和手机,采用的是策略1。而游戏主机(游戏机),采用的是策略2。
因为这样的策略不同,造成了(可以以差不多同一质量跑同一款游戏的)游戏主机的价格远低于PC或者手机。
另一方面,也因为游戏主机提供了统一的硬件规格,使得采用策略2成为可能。而对于PC和手机,型号规格太多,差别太大,采用策略2的技术可行性本身就有问题。
所以,(主机游戏的开发)之所以那么贴着硬件架构,是为了最大限度压榨硬件性能,降低硬件成本,从而扩大用户基数造成的。